mirror of
https://github.com/documenso/documenso.git
synced 2026-10-04 10:43:53 +10:00
719 lines
23 KiB
TypeScript
719 lines
23 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
calculateTwoFactorDeadline,
|
|
calculateTwoFactorDeadlineTimerDelay,
|
|
computeOrganisationTwoFactorEnforcementStatus,
|
|
computeTwoFactorEnforcementStatus,
|
|
evaluateInstanceTwoFactorEnforcementUpdate,
|
|
isInstanceTwoFactorEnforcementActive,
|
|
isTwoFactorGracePeriodReduction,
|
|
MAX_TWO_FACTOR_DEADLINE_TIMER_DELAY_MS,
|
|
} from './two-factor';
|
|
|
|
describe('calculateTwoFactorDeadline', () => {
|
|
it('adds the grace period to a single anchor', () => {
|
|
const anchor = new Date('2026-01-01T00:00:00.000Z');
|
|
|
|
const deadline = calculateTwoFactorDeadline({ anchors: [anchor], gracePeriodDays: 7 });
|
|
|
|
expect(deadline).toEqual(new Date('2026-01-08T00:00:00.000Z'));
|
|
});
|
|
|
|
it('uses the latest anchor when multiple are provided', () => {
|
|
const earlier = new Date('2026-01-01T00:00:00.000Z');
|
|
const latest = new Date('2026-02-01T00:00:00.000Z');
|
|
|
|
const deadline = calculateTwoFactorDeadline({
|
|
anchors: [earlier, latest],
|
|
gracePeriodDays: 1,
|
|
});
|
|
|
|
expect(deadline).toEqual(new Date('2026-02-02T00:00:00.000Z'));
|
|
});
|
|
|
|
it('ignores null and undefined anchors', () => {
|
|
const anchor = new Date('2026-01-01T00:00:00.000Z');
|
|
|
|
const deadline = calculateTwoFactorDeadline({
|
|
anchors: [null, anchor, undefined],
|
|
gracePeriodDays: 0,
|
|
});
|
|
|
|
expect(deadline).toEqual(anchor);
|
|
});
|
|
|
|
it('returns the anchor instant for a 0 day grace period', () => {
|
|
const anchor = new Date('2026-01-01T12:34:56.000Z');
|
|
|
|
const deadline = calculateTwoFactorDeadline({ anchors: [anchor], gracePeriodDays: 0 });
|
|
|
|
expect(deadline).toEqual(anchor);
|
|
});
|
|
|
|
it('returns null when no anchors are provided', () => {
|
|
expect(calculateTwoFactorDeadline({ anchors: [], gracePeriodDays: 7 })).toBeNull();
|
|
expect(calculateTwoFactorDeadline({ anchors: [null, undefined], gracePeriodDays: 7 })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('isTwoFactorGracePeriodReduction', () => {
|
|
const anchor = new Date('2026-01-01T00:00:00.000Z');
|
|
const now = new Date('2026-01-02T00:00:00.000Z');
|
|
|
|
it('detects a reduced grace period while the previous grace is active', () => {
|
|
expect(
|
|
isTwoFactorGracePeriodReduction({
|
|
previous: { anchors: [anchor], gracePeriodDays: 30 },
|
|
next: { anchors: [anchor], gracePeriodDays: 7 },
|
|
now,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('detects a reduction caused by an earlier anchor set', () => {
|
|
const laterEnforcedFrom = new Date('2026-01-10T00:00:00.000Z');
|
|
|
|
expect(
|
|
isTwoFactorGracePeriodReduction({
|
|
previous: { anchors: [anchor, laterEnforcedFrom], gracePeriodDays: 7 },
|
|
next: { anchors: [anchor], gracePeriodDays: 7 },
|
|
now,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('is not a reduction when the deadline stays the same', () => {
|
|
expect(
|
|
isTwoFactorGracePeriodReduction({
|
|
previous: { anchors: [anchor], gracePeriodDays: 7 },
|
|
next: { anchors: [anchor], gracePeriodDays: 7 },
|
|
now,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('is not a reduction when the deadline moves later', () => {
|
|
expect(
|
|
isTwoFactorGracePeriodReduction({
|
|
previous: { anchors: [anchor], gracePeriodDays: 7 },
|
|
next: { anchors: [anchor], gracePeriodDays: 30 },
|
|
now,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('is not a reduction when the previous grace has already expired', () => {
|
|
const nowAfterExpiry = new Date('2026-03-01T00:00:00.000Z');
|
|
|
|
expect(
|
|
isTwoFactorGracePeriodReduction({
|
|
previous: { anchors: [anchor], gracePeriodDays: 7 },
|
|
next: { anchors: [anchor], gracePeriodDays: 0 },
|
|
now: nowAfterExpiry,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('is not a reduction when enforcement was not previously configured', () => {
|
|
expect(
|
|
isTwoFactorGracePeriodReduction({
|
|
previous: null,
|
|
next: { anchors: [anchor], gracePeriodDays: 0 },
|
|
now,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('is not a reduction when the update disables enforcement', () => {
|
|
expect(
|
|
isTwoFactorGracePeriodReduction({
|
|
previous: { anchors: [anchor], gracePeriodDays: 7 },
|
|
next: null,
|
|
now,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('computeTwoFactorEnforcementStatus', () => {
|
|
const anchor = new Date('2026-01-01T00:00:00.000Z');
|
|
const deadline = new Date('2026-01-08T00:00:00.000Z');
|
|
|
|
const graceWindow = { anchors: [anchor], gracePeriodDays: 7 };
|
|
|
|
it('is not required when no grace window is configured', () => {
|
|
expect(
|
|
computeTwoFactorEnforcementStatus({
|
|
graceWindow: null,
|
|
userTwoFactorEnabled: false,
|
|
sessionTwoFactorVerified: false,
|
|
now: new Date('2026-01-01T00:00:00.000Z'),
|
|
}),
|
|
).toEqual({ required: false });
|
|
});
|
|
|
|
it('is not required when the grace window has no anchors', () => {
|
|
expect(
|
|
computeTwoFactorEnforcementStatus({
|
|
graceWindow: { anchors: [null, undefined], gracePeriodDays: 7 },
|
|
userTwoFactorEnabled: false,
|
|
sessionTwoFactorVerified: false,
|
|
now: new Date('2026-01-01T00:00:00.000Z'),
|
|
}),
|
|
).toEqual({ required: false });
|
|
});
|
|
|
|
it('is required but not blocked within grace while unsatisfied', () => {
|
|
expect(
|
|
computeTwoFactorEnforcementStatus({
|
|
graceWindow,
|
|
userTwoFactorEnabled: false,
|
|
sessionTwoFactorVerified: false,
|
|
now: new Date('2026-01-02T00:00:00.000Z'),
|
|
}),
|
|
).toEqual({
|
|
required: true,
|
|
deadline,
|
|
isDeadlineExpired: false,
|
|
isSatisfied: false,
|
|
isBlocked: false,
|
|
});
|
|
});
|
|
|
|
it('is required and satisfied within grace when enrolled and verified', () => {
|
|
expect(
|
|
computeTwoFactorEnforcementStatus({
|
|
graceWindow,
|
|
userTwoFactorEnabled: true,
|
|
sessionTwoFactorVerified: true,
|
|
now: new Date('2026-01-02T00:00:00.000Z'),
|
|
}),
|
|
).toEqual({
|
|
required: true,
|
|
deadline,
|
|
isDeadlineExpired: false,
|
|
isSatisfied: true,
|
|
isBlocked: false,
|
|
});
|
|
});
|
|
|
|
it('blocks after the deadline while unsatisfied', () => {
|
|
expect(
|
|
computeTwoFactorEnforcementStatus({
|
|
graceWindow,
|
|
userTwoFactorEnabled: false,
|
|
sessionTwoFactorVerified: false,
|
|
now: new Date('2026-02-01T00:00:00.000Z'),
|
|
}),
|
|
).toEqual({
|
|
required: true,
|
|
deadline,
|
|
isDeadlineExpired: true,
|
|
isSatisfied: false,
|
|
isBlocked: true,
|
|
});
|
|
});
|
|
|
|
it('does not block after the deadline when satisfied', () => {
|
|
expect(
|
|
computeTwoFactorEnforcementStatus({
|
|
graceWindow,
|
|
userTwoFactorEnabled: true,
|
|
sessionTwoFactorVerified: true,
|
|
now: new Date('2026-02-01T00:00:00.000Z'),
|
|
}),
|
|
).toEqual({
|
|
required: true,
|
|
deadline,
|
|
isDeadlineExpired: true,
|
|
isSatisfied: true,
|
|
isBlocked: false,
|
|
});
|
|
});
|
|
|
|
it('does not block when enrolled but the session is unverified within grace', () => {
|
|
const status = computeTwoFactorEnforcementStatus({
|
|
graceWindow,
|
|
userTwoFactorEnabled: true,
|
|
sessionTwoFactorVerified: false,
|
|
now: new Date('2026-01-02T00:00:00.000Z'),
|
|
});
|
|
|
|
expect(status).toMatchObject({ required: true, isSatisfied: false, isBlocked: false });
|
|
});
|
|
|
|
it('blocks an enrolled user with an unverified session after the deadline', () => {
|
|
const status = computeTwoFactorEnforcementStatus({
|
|
graceWindow,
|
|
userTwoFactorEnabled: true,
|
|
sessionTwoFactorVerified: false,
|
|
now: new Date('2026-02-01T00:00:00.000Z'),
|
|
});
|
|
|
|
expect(status).toMatchObject({ required: true, isSatisfied: false, isBlocked: true });
|
|
});
|
|
|
|
it('counts the deadline instant itself as expired', () => {
|
|
const status = computeTwoFactorEnforcementStatus({
|
|
graceWindow,
|
|
userTwoFactorEnabled: false,
|
|
sessionTwoFactorVerified: false,
|
|
now: deadline,
|
|
});
|
|
|
|
expect(status).toMatchObject({ required: true, isDeadlineExpired: true, isBlocked: true });
|
|
});
|
|
|
|
it('is not expired just before the deadline instant', () => {
|
|
const status = computeTwoFactorEnforcementStatus({
|
|
graceWindow,
|
|
userTwoFactorEnabled: false,
|
|
sessionTwoFactorVerified: false,
|
|
now: new Date(deadline.getTime() - 1),
|
|
});
|
|
|
|
expect(status).toMatchObject({ required: true, isDeadlineExpired: false, isBlocked: false });
|
|
});
|
|
|
|
it('uses the latest anchor for the deadline', () => {
|
|
const laterEnforcedFrom = new Date('2026-01-10T00:00:00.000Z');
|
|
|
|
const status = computeTwoFactorEnforcementStatus({
|
|
graceWindow: { anchors: [anchor, laterEnforcedFrom], gracePeriodDays: 7 },
|
|
userTwoFactorEnabled: false,
|
|
sessionTwoFactorVerified: false,
|
|
now: new Date('2026-01-09T00:00:00.000Z'),
|
|
});
|
|
|
|
expect(status).toMatchObject({
|
|
required: true,
|
|
deadline: new Date('2026-01-17T00:00:00.000Z'),
|
|
isDeadlineExpired: false,
|
|
});
|
|
});
|
|
|
|
it('blocks immediately with a 0 day grace period', () => {
|
|
const status = computeTwoFactorEnforcementStatus({
|
|
graceWindow: { anchors: [anchor], gracePeriodDays: 0 },
|
|
userTwoFactorEnabled: false,
|
|
sessionTwoFactorVerified: false,
|
|
now: anchor,
|
|
});
|
|
|
|
expect(status).toMatchObject({ required: true, deadline: anchor, isBlocked: true });
|
|
});
|
|
});
|
|
|
|
describe('isInstanceTwoFactorEnforcementActive', () => {
|
|
it('is active when the setting exists, is enabled and the license grants the flag', () => {
|
|
expect(isInstanceTwoFactorEnforcementActive({ setting: { enabled: true }, isLicensed: true })).toBe(true);
|
|
});
|
|
|
|
it('is inactive when the setting row is missing', () => {
|
|
expect(isInstanceTwoFactorEnforcementActive({ setting: null, isLicensed: true })).toBe(false);
|
|
});
|
|
|
|
it('is inactive when the setting is disabled', () => {
|
|
expect(isInstanceTwoFactorEnforcementActive({ setting: { enabled: false }, isLicensed: true })).toBe(false);
|
|
});
|
|
|
|
it('is inactive on an unlicensed instance even when a row is enabled', () => {
|
|
expect(isInstanceTwoFactorEnforcementActive({ setting: { enabled: true }, isLicensed: false })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('computeOrganisationTwoFactorEnforcementStatus', () => {
|
|
const memberCreatedAt = new Date('2026-01-01T00:00:00.000Z');
|
|
const graceStartedAt = new Date('2025-12-01T00:00:00.000Z');
|
|
|
|
const baseOptions = {
|
|
memberCreatedAt,
|
|
userTwoFactorEnabled: false,
|
|
userTwoFactorGraceStartedAt: graceStartedAt,
|
|
sessionTwoFactorVerified: false,
|
|
};
|
|
|
|
it('is not required when the organisation does not require 2FA', () => {
|
|
const status = computeOrganisationTwoFactorEnforcementStatus({
|
|
...baseOptions,
|
|
organisationSettings: {
|
|
twoFactorRequired: false,
|
|
twoFactorGracePeriodDays: 7,
|
|
twoFactorEnforcedFrom: new Date('2026-01-05T00:00:00.000Z'),
|
|
},
|
|
now: new Date('2026-12-01T00:00:00.000Z'),
|
|
});
|
|
|
|
expect(status).toEqual({ required: false });
|
|
});
|
|
|
|
it('derives the deadline from max(member.createdAt, enforcedFrom, graceStartedAt) + gracePeriodDays', () => {
|
|
const enforcedFrom = new Date('2026-01-10T00:00:00.000Z');
|
|
|
|
const status = computeOrganisationTwoFactorEnforcementStatus({
|
|
...baseOptions,
|
|
organisationSettings: {
|
|
twoFactorRequired: true,
|
|
twoFactorGracePeriodDays: 7,
|
|
twoFactorEnforcedFrom: enforcedFrom,
|
|
},
|
|
now: new Date('2026-01-12T00:00:00.000Z'),
|
|
});
|
|
|
|
expect(status).toMatchObject({
|
|
required: true,
|
|
deadline: new Date('2026-01-17T00:00:00.000Z'),
|
|
isDeadlineExpired: false,
|
|
isBlocked: false,
|
|
});
|
|
});
|
|
|
|
it('anchors on member.createdAt when enforcedFrom is null and grace started earlier', () => {
|
|
const status = computeOrganisationTwoFactorEnforcementStatus({
|
|
...baseOptions,
|
|
organisationSettings: {
|
|
twoFactorRequired: true,
|
|
twoFactorGracePeriodDays: 7,
|
|
twoFactorEnforcedFrom: null,
|
|
},
|
|
now: new Date('2026-01-08T00:00:00.000Z'),
|
|
});
|
|
|
|
expect(status).toMatchObject({
|
|
required: true,
|
|
deadline: new Date('2026-01-08T00:00:00.000Z'),
|
|
isDeadlineExpired: true,
|
|
isBlocked: true,
|
|
});
|
|
});
|
|
|
|
it('an admin 2FA reset (later graceStartedAt) restarts the organisation grace window', () => {
|
|
const restartedGraceStartedAt = new Date('2026-02-01T00:00:00.000Z');
|
|
|
|
const status = computeOrganisationTwoFactorEnforcementStatus({
|
|
...baseOptions,
|
|
userTwoFactorGraceStartedAt: restartedGraceStartedAt,
|
|
organisationSettings: {
|
|
twoFactorRequired: true,
|
|
twoFactorGracePeriodDays: 7,
|
|
twoFactorEnforcedFrom: null,
|
|
},
|
|
now: new Date('2026-02-02T00:00:00.000Z'),
|
|
});
|
|
|
|
expect(status).toMatchObject({
|
|
required: true,
|
|
deadline: new Date('2026-02-08T00:00:00.000Z'),
|
|
isDeadlineExpired: false,
|
|
isBlocked: false,
|
|
});
|
|
});
|
|
|
|
it('is satisfied (never blocked) for an enrolled user with a verified session', () => {
|
|
const status = computeOrganisationTwoFactorEnforcementStatus({
|
|
...baseOptions,
|
|
userTwoFactorEnabled: true,
|
|
sessionTwoFactorVerified: true,
|
|
organisationSettings: {
|
|
twoFactorRequired: true,
|
|
twoFactorGracePeriodDays: 0,
|
|
twoFactorEnforcedFrom: null,
|
|
},
|
|
now: new Date('2027-01-01T00:00:00.000Z'),
|
|
});
|
|
|
|
expect(status).toMatchObject({
|
|
required: true,
|
|
isSatisfied: true,
|
|
isDeadlineExpired: true,
|
|
isBlocked: false,
|
|
});
|
|
});
|
|
|
|
it('an enrolled user with an unverified session (null/API context) is not satisfied', () => {
|
|
const status = computeOrganisationTwoFactorEnforcementStatus({
|
|
...baseOptions,
|
|
userTwoFactorEnabled: true,
|
|
sessionTwoFactorVerified: false,
|
|
organisationSettings: {
|
|
twoFactorRequired: true,
|
|
twoFactorGracePeriodDays: 0,
|
|
twoFactorEnforcedFrom: null,
|
|
},
|
|
now: new Date('2027-01-01T00:00:00.000Z'),
|
|
});
|
|
|
|
expect(status).toMatchObject({
|
|
required: true,
|
|
isSatisfied: false,
|
|
isBlocked: true,
|
|
});
|
|
});
|
|
|
|
it('blocks organisation access immediately with a 0 day grace period', () => {
|
|
const status = computeOrganisationTwoFactorEnforcementStatus({
|
|
...baseOptions,
|
|
organisationSettings: {
|
|
twoFactorRequired: true,
|
|
twoFactorGracePeriodDays: 0,
|
|
twoFactorEnforcedFrom: null,
|
|
},
|
|
now: memberCreatedAt,
|
|
});
|
|
|
|
expect(status).toMatchObject({ required: true, deadline: memberCreatedAt, isBlocked: true });
|
|
});
|
|
});
|
|
|
|
describe('evaluateInstanceTwoFactorEnforcementUpdate', () => {
|
|
const now = new Date('2026-06-01T00:00:00.000Z');
|
|
|
|
const satisfiedActor = { userTwoFactorEnabled: true, sessionTwoFactorVerified: true };
|
|
const unsatisfiedActor = { userTwoFactorEnabled: false, sessionTwoFactorVerified: false };
|
|
|
|
const storedDisabled = { enabled: false, gracePeriodDays: 7, enforcedFrom: null };
|
|
|
|
// Enabled with an active grace window: enforcedFrom 1 day ago + 30 days.
|
|
const storedEnabledActiveGrace = {
|
|
enabled: true,
|
|
gracePeriodDays: 30,
|
|
enforcedFrom: '2026-05-31T00:00:00.000Z',
|
|
};
|
|
|
|
describe('license gate', () => {
|
|
it('rejects enabling on an unlicensed instance', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedDisabled,
|
|
update: { enabled: true, gracePeriodDays: 7 },
|
|
isLicensed: false,
|
|
actor: satisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toEqual({ allowed: false, reason: 'UNLICENSED' });
|
|
});
|
|
|
|
it('rejects changing values while enabled on an unlicensed instance', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedEnabledActiveGrace,
|
|
update: { enabled: true, gracePeriodDays: 60 },
|
|
isLicensed: false,
|
|
actor: satisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toEqual({ allowed: false, reason: 'UNLICENSED' });
|
|
});
|
|
|
|
it('rejects an unlicensed disabled→disabled no-op write', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedDisabled,
|
|
update: { enabled: false, gracePeriodDays: 7 },
|
|
isLicensed: false,
|
|
actor: satisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toEqual({ allowed: false, reason: 'UNLICENSED' });
|
|
});
|
|
|
|
it('rejects an unlicensed disable that also changes the grace period', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedEnabledActiveGrace,
|
|
update: { enabled: false, gracePeriodDays: 60 },
|
|
isLicensed: false,
|
|
actor: satisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toEqual({ allowed: false, reason: 'UNLICENSED' });
|
|
});
|
|
|
|
it('allows an unlicensed disable-only update with values unchanged from stored', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedEnabledActiveGrace,
|
|
update: { enabled: false, gracePeriodDays: storedEnabledActiveGrace.gracePeriodDays },
|
|
isLicensed: false,
|
|
// Even an unenrolled admin may disable — walking the policy back must
|
|
// never be gated on satisfying it.
|
|
actor: unsatisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toEqual({
|
|
allowed: true,
|
|
next: {
|
|
enabled: false,
|
|
gracePeriodDays: storedEnabledActiveGrace.gracePeriodDays,
|
|
enforcedFrom: storedEnabledActiveGrace.enforcedFrom,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('enable-time guard', () => {
|
|
it('rejects enabling when the acting admin does not satisfy the policy', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedDisabled,
|
|
update: { enabled: true, gracePeriodDays: 0 },
|
|
isLicensed: true,
|
|
actor: unsatisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toEqual({ allowed: false, reason: 'ENABLE_REQUIRES_ACTOR_TWO_FACTOR' });
|
|
});
|
|
|
|
it('rejects enabling when the actor is enrolled but the session is unverified', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedDisabled,
|
|
update: { enabled: true, gracePeriodDays: 7 },
|
|
isLicensed: true,
|
|
actor: { userTwoFactorEnabled: true, sessionTwoFactorVerified: false },
|
|
now,
|
|
});
|
|
|
|
expect(decision).toEqual({ allowed: false, reason: 'ENABLE_REQUIRES_ACTOR_TWO_FACTOR' });
|
|
});
|
|
|
|
it('does not apply the guard when updating values while already enabled', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedEnabledActiveGrace,
|
|
update: { enabled: true, gracePeriodDays: 60 },
|
|
isLicensed: true,
|
|
actor: unsatisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toMatchObject({ allowed: true });
|
|
});
|
|
});
|
|
|
|
describe('enforcedFrom handling', () => {
|
|
it('sets enforcedFrom to now on an off→on transition', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedDisabled,
|
|
update: { enabled: true, gracePeriodDays: 14 },
|
|
isLicensed: true,
|
|
actor: satisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toEqual({
|
|
allowed: true,
|
|
next: { enabled: true, gracePeriodDays: 14, enforcedFrom: now.toISOString() },
|
|
});
|
|
});
|
|
|
|
it('preserves enforcedFrom when the policy stays enabled', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedEnabledActiveGrace,
|
|
update: { enabled: true, gracePeriodDays: 60 },
|
|
isLicensed: true,
|
|
actor: satisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toEqual({
|
|
allowed: true,
|
|
next: { enabled: true, gracePeriodDays: 60, enforcedFrom: storedEnabledActiveGrace.enforcedFrom },
|
|
});
|
|
});
|
|
|
|
it('preserves enforcedFrom on disable', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedEnabledActiveGrace,
|
|
update: { enabled: false, gracePeriodDays: storedEnabledActiveGrace.gracePeriodDays },
|
|
isLicensed: true,
|
|
actor: satisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toEqual({
|
|
allowed: true,
|
|
next: {
|
|
enabled: false,
|
|
gracePeriodDays: storedEnabledActiveGrace.gracePeriodDays,
|
|
enforcedFrom: storedEnabledActiveGrace.enforcedFrom,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('grace-reduction acknowledgement', () => {
|
|
it('rejects reducing an active grace period without acknowledgement', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedEnabledActiveGrace,
|
|
update: { enabled: true, gracePeriodDays: 1 },
|
|
isLicensed: true,
|
|
actor: satisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toEqual({ allowed: false, reason: 'GRACE_REDUCTION_NOT_ACKNOWLEDGED' });
|
|
});
|
|
|
|
it('allows reducing an active grace period with acknowledgement', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedEnabledActiveGrace,
|
|
update: { enabled: true, gracePeriodDays: 1, acknowledgeGracePeriodReduction: true },
|
|
isLicensed: true,
|
|
actor: satisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toEqual({
|
|
allowed: true,
|
|
next: { enabled: true, gracePeriodDays: 1, enforcedFrom: storedEnabledActiveGrace.enforcedFrom },
|
|
});
|
|
});
|
|
|
|
it('does not require acknowledgement when enabling for the first time', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: storedDisabled,
|
|
update: { enabled: true, gracePeriodDays: 0 },
|
|
isLicensed: true,
|
|
actor: satisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toMatchObject({ allowed: true });
|
|
});
|
|
|
|
it('does not require acknowledgement when tightening an already-expired window', () => {
|
|
const decision = evaluateInstanceTwoFactorEnforcementUpdate({
|
|
stored: {
|
|
enabled: true,
|
|
gracePeriodDays: 7,
|
|
enforcedFrom: '2026-01-01T00:00:00.000Z',
|
|
},
|
|
update: { enabled: true, gracePeriodDays: 0 },
|
|
isLicensed: true,
|
|
actor: satisfiedActor,
|
|
now,
|
|
});
|
|
|
|
expect(decision).toMatchObject({ allowed: true });
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('calculateTwoFactorDeadlineTimerDelay', () => {
|
|
const now = new Date('2026-06-01T00:00:00.000Z');
|
|
|
|
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 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: atMax, now })).toBe(MAX_TWO_FACTOR_DEADLINE_TIMER_DELAY_MS);
|
|
expect(calculateTwoFactorDeadlineTimerDelay({ deadline: pastMax, now })).toBeNull();
|
|
});
|
|
});
|