mirror of
https://github.com/documenso/documenso.git
synced 2026-10-04 02:33:54 +10:00
122 lines
3.6 KiB
TypeScript
122 lines
3.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { ZTwoFactorChallengeMetadataSchema } from '../types/two-factor-challenge';
|
|
import {
|
|
isChallengeExpired,
|
|
shouldConsumeChallengeAfterFailure,
|
|
TWO_FACTOR_CHALLENGE_MAX_ATTEMPTS,
|
|
} from './two-factor-challenge';
|
|
|
|
describe('ZTwoFactorChallengeMetadataSchema', () => {
|
|
it('parses minimal valid metadata and normalizes the redirect path', () => {
|
|
const result = ZTwoFactorChallengeMetadataSchema.parse({
|
|
redirectPath: '/documents?page=2',
|
|
authMethod: 'oauth',
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
redirectPath: '/documents?page=2',
|
|
authMethod: 'oauth',
|
|
});
|
|
});
|
|
|
|
it('normalizes a same-origin absolute URL down to a path', () => {
|
|
const result = ZTwoFactorChallengeMetadataSchema.parse({
|
|
redirectPath: 'http://localhost:3000/settings/security',
|
|
authMethod: 'oauth',
|
|
});
|
|
|
|
expect(result.redirectPath).toBe('/settings/security');
|
|
});
|
|
|
|
it('rejects cross-origin redirect paths', () => {
|
|
const result = ZTwoFactorChallengeMetadataSchema.safeParse({
|
|
redirectPath: 'https://evil.example.com/phish',
|
|
authMethod: 'oauth',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects unknown authentication methods', () => {
|
|
const result = ZTwoFactorChallengeMetadataSchema.safeParse({
|
|
redirectPath: '/',
|
|
authMethod: 'carrier-pigeon',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('accepts a deferred link action', () => {
|
|
const result = ZTwoFactorChallengeMetadataSchema.parse({
|
|
redirectPath: '/',
|
|
authMethod: 'oauth',
|
|
action: {
|
|
type: 'link-oauth-account',
|
|
provider: 'google',
|
|
providerAccountId: 'subject-123',
|
|
email: 'user@example.com',
|
|
},
|
|
});
|
|
|
|
expect(result.action?.provider).toBe('google');
|
|
});
|
|
|
|
it('rejects unknown keys on the metadata (strict)', () => {
|
|
const result = ZTwoFactorChallengeMetadataSchema.safeParse({
|
|
redirectPath: '/',
|
|
authMethod: 'oauth',
|
|
accessToken: 'must-never-be-stored',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects unknown keys on the action (strict)', () => {
|
|
const result = ZTwoFactorChallengeMetadataSchema.safeParse({
|
|
redirectPath: '/',
|
|
authMethod: 'oauth',
|
|
action: {
|
|
type: 'link-oauth-account',
|
|
provider: 'google',
|
|
providerAccountId: 'subject-123',
|
|
email: 'user@example.com',
|
|
idToken: 'must-never-be-stored',
|
|
},
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects an action with an invalid email', () => {
|
|
const result = ZTwoFactorChallengeMetadataSchema.safeParse({
|
|
redirectPath: '/',
|
|
authMethod: 'oauth',
|
|
action: {
|
|
type: 'link-oauth-account',
|
|
provider: 'google',
|
|
providerAccountId: 'subject-123',
|
|
email: 'not-an-email',
|
|
},
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
// The `>=` boundary matters: an off-by-one here would grant one extra guess
|
|
// per challenge (or one extra valid moment past expiry).
|
|
describe('challenge consumption boundaries', () => {
|
|
it('consumes the challenge at exactly the maximum attempts, not before', () => {
|
|
expect(shouldConsumeChallengeAfterFailure(TWO_FACTOR_CHALLENGE_MAX_ATTEMPTS - 1)).toBe(false);
|
|
expect(shouldConsumeChallengeAfterFailure(TWO_FACTOR_CHALLENGE_MAX_ATTEMPTS)).toBe(true);
|
|
});
|
|
|
|
it('treats the expiry instant itself as expired', () => {
|
|
const expiresAt = new Date('2026-01-01T00:10:00.000Z');
|
|
|
|
expect(isChallengeExpired({ expiresAt, now: new Date('2026-01-01T00:09:59.999Z') })).toBe(false);
|
|
expect(isChallengeExpired({ expiresAt, now: expiresAt })).toBe(true);
|
|
});
|
|
});
|