Replaces the no-op fastify.authenticate stub and the never-implemented
identity/auth login with real bcrypt password verification, JWT session
issuance/verification (reusing the existing JWT_SECRET), and a Redis-backed
revocation denylist for logout. Adds requireRole('ADMIN') to admin-only
configuration writes across 002-009 that previously relied on a decorator
that never actually checked anything. Adds self-identity (GET /auth/me,
re-validated against live account state) and admin-provisioned accounts
(POST /admin/users).
Making the auth check genuinely reject invalid/missing tokens exposed that
~18 pre-existing integration test files called already-gated routes with no
Authorization header (safe against the old no-op stub, broken against a real
one) — fixed via a shared tests/helpers/auth.ts (loginAs/authHeader) and a
file-by-file pass, plus two related SLA-run cleanup races exposed once admin
setup calls in those files' own beforeAll blocks started actually succeeding.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
103 lines
4.8 KiB
TypeScript
103 lines
4.8 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
addBusinessMinutes,
|
|
isWithinWorkingHours,
|
|
} from '@/modules/platform/business-calendars/calculators/business-hours.calculator';
|
|
|
|
const MON_FRI_9_TO_5 = {
|
|
timezone: 'America/New_York',
|
|
workingHours: {
|
|
mon: { start: '09:00', end: '17:00' },
|
|
tue: { start: '09:00', end: '17:00' },
|
|
wed: { start: '09:00', end: '17:00' },
|
|
thu: { start: '09:00', end: '17:00' },
|
|
fri: { start: '09:00', end: '17:00' },
|
|
},
|
|
};
|
|
|
|
// America/New_York is UTC-5 (EST) outside DST, UTC-4 (EDT) during DST — every Date.UTC(...)
|
|
// below is written as the equivalent UTC instant for the Eastern wall-clock time noted in the
|
|
// comment, since luxon's own DST handling is exactly what's under test here.
|
|
|
|
describe('addBusinessMinutes', () => {
|
|
it('stays within the same working day when enough time remains', () => {
|
|
// Mon 2026-01-05 10:00 EST (UTC-5) -> 15:00 UTC
|
|
const start = new Date(Date.UTC(2026, 0, 5, 15, 0));
|
|
const due = addBusinessMinutes(start, 120, MON_FRI_9_TO_5, []);
|
|
// +120 min -> 12:00 EST -> 17:00 UTC
|
|
expect(due.toISOString()).toBe(new Date(Date.UTC(2026, 0, 5, 17, 0)).toISOString());
|
|
});
|
|
|
|
it('rolls over a weekend, never counting Saturday/Sunday as available time', () => {
|
|
// Fri 2026-01-02 16:00 EST (1 hour left in the window) -> 21:00 UTC
|
|
const start = new Date(Date.UTC(2026, 0, 2, 21, 0));
|
|
// 1 hour left Friday + need 3 more hours -> Monday 12:00 EST
|
|
const due = addBusinessMinutes(start, 240, MON_FRI_9_TO_5, []);
|
|
expect(due.toISOString()).toBe(new Date(Date.UTC(2026, 0, 5, 17, 0)).toISOString()); // Mon 12:00 EST
|
|
});
|
|
|
|
it('excludes a holiday entirely, rolling to the next working day', () => {
|
|
// Fri 2026-01-02 16:00 EST, Monday Jan 5 is a holiday
|
|
const start = new Date(Date.UTC(2026, 0, 2, 21, 0));
|
|
const holiday = new Date(Date.UTC(2026, 0, 5, 17, 0)); // Mon 12:00 EST — safely mid-Monday
|
|
const due = addBusinessMinutes(start, 240, MON_FRI_9_TO_5, [holiday]);
|
|
// 1h Friday + Monday excluded + 3h into Tuesday -> Tue 12:00 EST
|
|
expect(due.toISOString()).toBe(new Date(Date.UTC(2026, 0, 6, 17, 0)).toISOString());
|
|
});
|
|
|
|
it('a weekday with no configured window contributes zero available time', () => {
|
|
const noWednesday = {
|
|
timezone: 'America/New_York',
|
|
workingHours: {
|
|
tue: { start: '09:00', end: '17:00' },
|
|
thu: { start: '09:00', end: '17:00' },
|
|
},
|
|
};
|
|
// Tue 2026-01-06 16:00 EST (1h left)
|
|
const start = new Date(Date.UTC(2026, 0, 6, 21, 0));
|
|
const due = addBusinessMinutes(start, 300, noWednesday, []);
|
|
// 1h Tue + (Wed skipped, zero hours) + 4h into Thu -> Thu 13:00 EST
|
|
expect(due.toISOString()).toBe(new Date(Date.UTC(2026, 0, 8, 18, 0)).toISOString());
|
|
});
|
|
|
|
it('clips to the start time on the first day, never counting time before it', () => {
|
|
// Mon 10:00 EST: 7h remain in the window (to 17:00) + 1 more hour -> Tue 10:00 EST
|
|
const start = new Date(Date.UTC(2026, 0, 5, 15, 0));
|
|
const due = addBusinessMinutes(start, 420 + 60, MON_FRI_9_TO_5, []);
|
|
expect(due.toISOString()).toBe(new Date(Date.UTC(2026, 0, 6, 15, 0)).toISOString()); // Tue 10:00 EST
|
|
});
|
|
|
|
it('returns the start time unchanged when minutes is zero or negative', () => {
|
|
const start = new Date(Date.UTC(2026, 0, 5, 15, 0));
|
|
expect(addBusinessMinutes(start, 0, MON_FRI_9_TO_5, []).toISOString()).toBe(
|
|
start.toISOString(),
|
|
);
|
|
});
|
|
|
|
it('is correct across a DST transition (US spring-forward, March 2026)', () => {
|
|
// 2026-03-08 is the US DST transition (02:00 -> 03:00). Fri 2026-03-06 16:00 EST (1h left).
|
|
const start = new Date(Date.UTC(2026, 2, 6, 21, 0));
|
|
// 1h Friday + weekend skipped + 3h into Monday 2026-03-09 (now EDT, UTC-4) -> 12:00 EDT -> 16:00 UTC
|
|
const due = addBusinessMinutes(start, 240, MON_FRI_9_TO_5, []);
|
|
expect(due.toISOString()).toBe(new Date(Date.UTC(2026, 2, 9, 16, 0)).toISOString());
|
|
});
|
|
});
|
|
|
|
describe('isWithinWorkingHours', () => {
|
|
it('is true inside a configured window and false outside it', () => {
|
|
const insideWindow = new Date(Date.UTC(2026, 0, 5, 16, 0)); // Mon 11:00 EST
|
|
const beforeWindow = new Date(Date.UTC(2026, 0, 5, 13, 0)); // Mon 08:00 EST
|
|
expect(isWithinWorkingHours(insideWindow, MON_FRI_9_TO_5, [])).toBe(true);
|
|
expect(isWithinWorkingHours(beforeWindow, MON_FRI_9_TO_5, [])).toBe(false);
|
|
});
|
|
|
|
it('is false on a weekend and on a holiday', () => {
|
|
const saturday = new Date(Date.UTC(2026, 0, 3, 16, 0));
|
|
expect(isWithinWorkingHours(saturday, MON_FRI_9_TO_5, [])).toBe(false);
|
|
|
|
const mondayNoon = new Date(Date.UTC(2026, 0, 5, 16, 0)); // Mon 11:00 EST
|
|
const holiday = new Date(Date.UTC(2026, 0, 5, 17, 0)); // Mon 12:00 EST — safely mid-Monday
|
|
expect(isWithinWorkingHours(mondayNoon, MON_FRI_9_TO_5, [holiday])).toBe(false);
|
|
});
|
|
});
|