chore: tests

This commit is contained in:
Lucas Smith
2026-09-14 14:16:23 +10:00
parent df815e5b44
commit fe2641ff57
4 changed files with 36 additions and 132 deletions
+20
View File
@@ -30,6 +30,26 @@ jobs:
- name: Build app
run: npm run build
unit_tests:
name: Unit Tests
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: ./.github/actions/node-install
- name: Copy env
run: cp .env.example .env
# Includes the 2FA enforcement drift guard (packages/trpc), which is the
# only check that catches a session route without enforcement middleware.
- name: Run unit tests
run: npm run test -w @documenso/lib -w @documenso/trpc
build_docker:
name: Build Docker Image
runs-on: ubuntu-latest
@@ -104,38 +104,18 @@ describe('ZTwoFactorChallengeMetadataSchema', () => {
});
});
describe('shouldConsumeChallengeAfterFailure', () => {
it('does not consume below the maximum attempts', () => {
// 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(1)).toBe(false);
});
it('consumes at exactly the maximum attempts', () => {
expect(shouldConsumeChallengeAfterFailure(TWO_FACTOR_CHALLENGE_MAX_ATTEMPTS)).toBe(true);
});
it('consumes beyond the maximum attempts', () => {
expect(shouldConsumeChallengeAfterFailure(TWO_FACTOR_CHALLENGE_MAX_ATTEMPTS + 5)).toBe(true);
});
it('treats the expiry instant itself as expired', () => {
const expiresAt = new Date('2026-01-01T00:10:00.000Z');
it('honors a custom maximum', () => {
expect(shouldConsumeChallengeAfterFailure(2, 3)).toBe(false);
expect(shouldConsumeChallengeAfterFailure(3, 3)).toBe(true);
});
});
describe('isChallengeExpired', () => {
const expiresAt = new Date('2026-01-01T00:10:00.000Z');
it('is not expired before the expiry instant', () => {
expect(isChallengeExpired({ expiresAt, now: new Date('2026-01-01T00:09:59.999Z') })).toBe(false);
});
it('is expired at exactly the expiry instant', () => {
expect(isChallengeExpired({ expiresAt, now: expiresAt })).toBe(true);
});
it('is expired after the expiry instant', () => {
expect(isChallengeExpired({ expiresAt, now: new Date('2026-01-01T00:10:00.001Z') })).toBe(true);
});
});
+5 -64
View File
@@ -7,27 +7,10 @@ import {
computeTwoFactorEnforcementStatus,
evaluateInstanceTwoFactorEnforcementUpdate,
isInstanceTwoFactorEnforcementActive,
isTwoFactorDeadlineExpired,
isTwoFactorGracePeriodReduction,
isTwoFactorSatisfied,
MAX_TWO_FACTOR_DEADLINE_TIMER_DELAY_MS,
} from './two-factor';
describe('isTwoFactorSatisfied', () => {
it('is satisfied only when the user is enrolled and the session is verified', () => {
expect(isTwoFactorSatisfied({ userTwoFactorEnabled: true, sessionTwoFactorVerified: true })).toBe(true);
});
it('is not satisfied when the session is unverified', () => {
expect(isTwoFactorSatisfied({ userTwoFactorEnabled: true, sessionTwoFactorVerified: false })).toBe(false);
});
it('is not satisfied when the user is not enrolled', () => {
expect(isTwoFactorSatisfied({ userTwoFactorEnabled: false, sessionTwoFactorVerified: true })).toBe(false);
expect(isTwoFactorSatisfied({ userTwoFactorEnabled: false, sessionTwoFactorVerified: false })).toBe(false);
});
});
describe('calculateTwoFactorDeadline', () => {
it('adds the grace period to a single anchor', () => {
const anchor = new Date('2026-01-01T00:00:00.000Z');
@@ -74,28 +57,6 @@ describe('calculateTwoFactorDeadline', () => {
});
});
describe('isTwoFactorDeadlineExpired', () => {
const deadline = new Date('2026-01-08T00:00:00.000Z');
it('is not expired before the deadline', () => {
const now = new Date('2026-01-07T23:59:59.999Z');
expect(isTwoFactorDeadlineExpired({ deadline, now })).toBe(false);
});
it('counts the deadline instant itself as expired', () => {
const now = new Date('2026-01-08T00:00:00.000Z');
expect(isTwoFactorDeadlineExpired({ deadline, now })).toBe(true);
});
it('is expired after the deadline', () => {
const now = new Date('2026-01-08T00:00:00.001Z');
expect(isTwoFactorDeadlineExpired({ deadline, now })).toBe(true);
});
});
describe('isTwoFactorGracePeriodReduction', () => {
const anchor = new Date('2026-01-01T00:00:00.000Z');
const now = new Date('2026-01-02T00:00:00.000Z');
@@ -741,37 +702,17 @@ describe('evaluateInstanceTwoFactorEnforcementUpdate', () => {
describe('calculateTwoFactorDeadlineTimerDelay', () => {
const now = new Date('2026-06-01T00:00:00.000Z');
it('returns the exact delay for a deadline within the timer range', () => {
const deadline = new Date(now.getTime() + 5_000);
expect(calculateTwoFactorDeadlineTimerDelay({ deadline, now })).toBe(5_000);
});
it('returns 0 for a deadline at the current instant (deadline counts as expired)', () => {
expect(calculateTwoFactorDeadlineTimerDelay({ deadline: now, now })).toBe(0);
});
it('returns 0 for a past deadline', () => {
const deadline = new Date(now.getTime() - 60_000);
expect(calculateTwoFactorDeadlineTimerDelay({ deadline, now })).toBe(0);
});
it('returns the delay at exactly the setTimeout maximum', () => {
const deadline = new Date(now.getTime() + MAX_TWO_FACTOR_DEADLINE_TIMER_DELAY_MS);
it('returns the delay at exactly the setTimeout maximum and null just past it', () => {
const atMax = new Date(now.getTime() + MAX_TWO_FACTOR_DEADLINE_TIMER_DELAY_MS);
const pastMax = new Date(now.getTime() + MAX_TWO_FACTOR_DEADLINE_TIMER_DELAY_MS + 1);
expect(calculateTwoFactorDeadlineTimerDelay({ deadline, now })).toBe(MAX_TWO_FACTOR_DEADLINE_TIMER_DELAY_MS);
});
it('returns null when the deadline exceeds the setTimeout maximum', () => {
const deadline = new Date(now.getTime() + MAX_TWO_FACTOR_DEADLINE_TIMER_DELAY_MS + 1);
expect(calculateTwoFactorDeadlineTimerDelay({ deadline, now })).toBeNull();
});
it('returns null for a deadline months away', () => {
const deadline = new Date('2026-12-01T00:00:00.000Z');
expect(calculateTwoFactorDeadlineTimerDelay({ deadline, now })).toBeNull();
expect(calculateTwoFactorDeadlineTimerDelay({ deadline: atMax, now })).toBe(MAX_TWO_FACTOR_DEADLINE_TIMER_DELAY_MS);
expect(calculateTwoFactorDeadlineTimerDelay({ deadline: pastMax, now })).toBeNull();
});
});
@@ -16,28 +16,12 @@ describe('normalizeTwoFactorScopeResult', () => {
expect(normalizeTwoFactorScopeResult(TWO_FACTOR_CTX_TEAM)).toEqual({ type: 'ctxTeam' });
});
it('normalizes an empty descriptor to no resources (instance assert only)', () => {
expect(normalizeTwoFactorScopeResult({})).toEqual({ type: 'resources', resources: [] });
});
it('normalizes single IDs and ID arrays', () => {
expect(normalizeTwoFactorScopeResult({ envelope: 'envelope_1' })).toEqual({
type: 'resources',
resources: [{ kind: 'envelope', envelopeIds: ['envelope_1'], documentIds: [], templateIds: [] }],
});
expect(normalizeTwoFactorScopeResult({ envelope: ['a', 'b'] })).toEqual({
type: 'resources',
resources: [{ kind: 'envelope', envelopeIds: ['a', 'b'], documentIds: [], templateIds: [] }],
});
});
it('normalizes every resource kind to its resource-IDs variant', () => {
const result = normalizeTwoFactorScopeResult({
organisation: 'org_1',
organisationReference: 'my-org-url',
team: 1,
teamReference: 7,
teamReference: ['my-team-url', 7],
envelope: 'envelope_1',
document: 2,
template: 3,
@@ -59,7 +43,7 @@ describe('normalizeTwoFactorScopeResult', () => {
{ kind: 'organisation', organisationIds: ['org_1'] },
{ kind: 'organisationReference', references: ['my-org-url'] },
{ kind: 'team', teamIds: [1] },
{ kind: 'teamReference', references: [7] },
{ kind: 'teamReference', references: ['my-team-url', 7] },
{ kind: 'envelope', envelopeIds: ['envelope_1'], documentIds: [], templateIds: [] },
{ kind: 'document', documentIds: [2] },
{ kind: 'template', templateIds: [3] },
@@ -77,13 +61,6 @@ describe('normalizeTwoFactorScopeResult', () => {
});
});
it('accepts a string-or-number teamReference (URL slug or ID)', () => {
expect(normalizeTwoFactorScopeResult({ teamReference: ['my-team-url', 7] })).toEqual({
type: 'resources',
resources: [{ kind: 'teamReference', references: ['my-team-url', 7] }],
});
});
it('merges descriptor arrays per kind so each kind resolves in one batch', () => {
expect(normalizeTwoFactorScopeResult([{ envelope: 'a' }, { envelope: ['b'], recipient: 7 }])).toEqual({
type: 'resources',
@@ -94,17 +71,9 @@ describe('normalizeTwoFactorScopeResult', () => {
});
});
it('accepts ID sets at the bulk cap', () => {
const ids = Array.from({ length: TWO_FACTOR_ENFORCEMENT_MAX_BULK_IDS }, (_, i) => `envelope_${i}`);
expect(normalizeTwoFactorScopeResult({ envelope: ids })).toEqual({
type: 'resources',
resources: [{ kind: 'envelope', envelopeIds: ids, documentIds: [], templateIds: [] }],
});
});
it('rejects ID sets above the bulk cap instead of truncating', () => {
it('rejects ID sets above the bulk cap instead of truncating, including after merging', () => {
const oversized = Array.from({ length: TWO_FACTOR_ENFORCEMENT_MAX_BULK_IDS + 1 }, (_, i) => `envelope_${i}`);
const half = Array.from({ length: TWO_FACTOR_ENFORCEMENT_MAX_BULK_IDS / 2 + 1 }, (_, i) => `envelope_${i}`);
try {
normalizeTwoFactorScopeResult({ envelope: oversized });
@@ -112,21 +81,15 @@ describe('normalizeTwoFactorScopeResult', () => {
} catch (error) {
expect(AppError.parseError(error).code).toBe(AppErrorCode.LIMIT_EXCEEDED);
}
});
it('rejects when merged descriptors exceed the bulk cap combined', () => {
const half = Array.from({ length: TWO_FACTOR_ENFORCEMENT_MAX_BULK_IDS / 2 + 1 }, (_, i) => `envelope_${i}`);
expect(() => normalizeTwoFactorScopeResult([{ envelope: half }, { envelope: half }])).toThrowError(AppError);
});
});
describe('assertScopeBudget', () => {
it('allows counts within the budget', () => {
it('rejects only above the budget', () => {
expect(() => assertScopeBudget(TWO_FACTOR_ENFORCEMENT_MAX_UNIQUE_SCOPES)).not.toThrow();
});
it('rejects counts above the budget', () => {
try {
assertScopeBudget(TWO_FACTOR_ENFORCEMENT_MAX_UNIQUE_SCOPES + 1);
expect.unreachable();