40227-vm/backend/src/api/controllers/campus_attendance.controller.test.ts
2026-06-12 06:55:35 +02:00

300 lines
8.1 KiB
TypeScript

/**
* 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<typeof createTestUser>;
interface MockRequest {
body: Record<string, unknown>;
query: Record<string, unknown>;
params: Record<string, unknown>;
currentUser?: CurrentUser;
}
function createMockRequest(overrides: Partial<MockRequest> = {}): 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<TReturn> {
callCount: number;
lastUser: CurrentUser | undefined;
returnValue: TReturn;
call: (user: CurrentUser | undefined) => Promise<TReturn>;
}
function createTypedMock<TReturn>(defaultReturn: TReturn): TypedMock<TReturn> {
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<ConfigRow>;
listSummaries: TypedMock<{ rows: SummaryRow[]; count: number }>;
upsertSummary: TypedMock<SummaryRow>;
}
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);
}