mirror of
https://github.com/documenso/documenso.git
synced 2026-10-04 02:33:54 +10:00
chore: tests
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user