300 lines
8.1 KiB
TypeScript
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);
|
|
}
|