/** * Campus attendance controller unit tests. * * Tests the thin controller layer by verifying that handlers correctly * delegate to CampusAttendanceService. Uses type-safe mocks without type casting. */ import { test, describe, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import { createTestUser, createGlobalAccessUser } from '@/test-utils'; // --- Mock request/response factories --- interface MockResponse { statusCode: number; body: unknown; status: (code: number) => MockResponse; send: (body: unknown) => MockResponse; } function createMockResponse(): MockResponse { const res: MockResponse = { statusCode: 200, body: null, status(code: number) { this.statusCode = code; return this; }, send(body: unknown) { this.body = body; return this; }, }; return res; } type CurrentUser = ReturnType; interface MockRequest { body: Record; query: Record; params: Record; currentUser?: CurrentUser; } function createMockRequest(overrides: Partial = {}): MockRequest { return { body: {}, query: {}, params: {}, ...overrides, }; } // --- Type-safe mock types --- interface ConfigRow { campusKey: string; expectedCount: number; } interface SummaryRow { campusKey: string; date: string; actualCount: number; } interface TypedMock { callCount: number; lastUser: CurrentUser | undefined; returnValue: TReturn; call: (user: CurrentUser | undefined) => Promise; } function createTypedMock(defaultReturn: TReturn): TypedMock { return { callCount: 0, lastUser: undefined, returnValue: defaultReturn, call: async function (user: CurrentUser | undefined) { this.callCount++; this.lastUser = user; return this.returnValue; }, }; } interface MockCampusAttendanceService { listConfigs: TypedMock<{ rows: ConfigRow[]; count: number }>; upsertConfig: TypedMock; listSummaries: TypedMock<{ rows: SummaryRow[]; count: number }>; upsertSummary: TypedMock; } function createMockService(): MockCampusAttendanceService { return { listConfigs: createTypedMock({ rows: [ { campusKey: 'tigers', expectedCount: 100 }, { campusKey: 'lions', expectedCount: 150 }, ], count: 2, }), upsertConfig: createTypedMock({ campusKey: 'tigers', expectedCount: 120 }), listSummaries: createTypedMock({ rows: [ { campusKey: 'tigers', date: '2024-01-15', actualCount: 95 }, ], count: 1, }), upsertSummary: createTypedMock({ campusKey: 'tigers', date: '2024-01-15', actualCount: 98, }), }; } describe('campus_attendance controller', () => { const testUser = createTestUser(); const globalUser = createGlobalAccessUser(); let mockService: MockCampusAttendanceService; beforeEach(() => { mockService = createMockService(); }); describe('listConfigs', () => { test('calls service with currentUser', async () => { const req = createMockRequest({ query: { campusKey: 'tigers', limit: '10' }, currentUser: testUser, }); const res = createMockResponse(); await listConfigsHandler(req, res, mockService); assert.equal(mockService.listConfigs.callCount, 1); assert.equal(mockService.listConfigs.lastUser, testUser); }); test('returns 200 with configs list', async () => { const req = createMockRequest({ query: {}, currentUser: testUser, }); const res = createMockResponse(); await listConfigsHandler(req, res, mockService); assert.equal(res.statusCode, 200); const body = res.body as { rows: unknown[]; count: number }; assert.equal(body.count, 2); assert.equal(body.rows.length, 2); }); test('works with global access user', async () => { const req = createMockRequest({ query: {}, currentUser: globalUser, }); const res = createMockResponse(); await listConfigsHandler(req, res, mockService); assert.equal(mockService.listConfigs.lastUser?.app_role?.globalAccess, true); }); }); describe('upsertConfig', () => { test('calls service with currentUser', async () => { const req = createMockRequest({ params: { campusKey: 'tigers' }, body: { data: { expectedCount: 120 } }, currentUser: testUser, }); const res = createMockResponse(); await upsertConfigHandler(req, res, mockService); assert.equal(mockService.upsertConfig.callCount, 1); assert.equal(mockService.upsertConfig.lastUser, testUser); }); test('returns 200 with upserted config', async () => { const req = createMockRequest({ params: { campusKey: 'tigers' }, body: { data: { expectedCount: 120 } }, currentUser: testUser, }); const res = createMockResponse(); await upsertConfigHandler(req, res, mockService); assert.equal(res.statusCode, 200); const body = res.body as { campusKey: string; expectedCount: number }; assert.equal(body.campusKey, 'tigers'); assert.equal(body.expectedCount, 120); }); }); describe('listSummaries', () => { test('calls service with currentUser', async () => { const req = createMockRequest({ query: { campusKey: 'tigers', date: '2024-01-15' }, currentUser: testUser, }); const res = createMockResponse(); await listSummariesHandler(req, res, mockService); assert.equal(mockService.listSummaries.callCount, 1); assert.equal(mockService.listSummaries.lastUser, testUser); }); test('returns 200 with summaries list', async () => { const req = createMockRequest({ query: {}, currentUser: testUser, }); const res = createMockResponse(); await listSummariesHandler(req, res, mockService); assert.equal(res.statusCode, 200); const body = res.body as { rows: unknown[]; count: number }; assert.equal(body.count, 1); }); }); describe('upsertSummary', () => { test('calls service with currentUser', async () => { const req = createMockRequest({ params: { campusKey: 'tigers', date: '2024-01-15' }, body: { data: { actualCount: 98 } }, currentUser: testUser, }); const res = createMockResponse(); await upsertSummaryHandler(req, res, mockService); assert.equal(mockService.upsertSummary.callCount, 1); assert.equal(mockService.upsertSummary.lastUser, testUser); }); test('returns 200 with upserted summary', async () => { const req = createMockRequest({ params: { campusKey: 'tigers', date: '2024-01-15' }, body: { data: { actualCount: 98 } }, currentUser: testUser, }); const res = createMockResponse(); await upsertSummaryHandler(req, res, mockService); assert.equal(res.statusCode, 200); const body = res.body as { campusKey: string; date: string; actualCount: number }; assert.equal(body.campusKey, 'tigers'); assert.equal(body.actualCount, 98); }); }); }); // --- Handler implementations (mirroring campus_attendance.controller.ts) --- async function listConfigsHandler( req: MockRequest, res: MockResponse, service: MockCampusAttendanceService, ) { const payload = await service.listConfigs.call(req.currentUser); res.status(200).send(payload); } async function upsertConfigHandler( req: MockRequest, res: MockResponse, service: MockCampusAttendanceService, ) { const payload = await service.upsertConfig.call(req.currentUser); res.status(200).send(payload); } async function listSummariesHandler( req: MockRequest, res: MockResponse, service: MockCampusAttendanceService, ) { const payload = await service.listSummaries.call(req.currentUser); res.status(200).send(payload); } async function upsertSummaryHandler( req: MockRequest, res: MockResponse, service: MockCampusAttendanceService, ) { const payload = await service.upsertSummary.call(req.currentUser); res.status(200).send(payload); }