# Backend Test Coverage ## Test Runner The backend uses Node.js built-in test runner (`node:test`) with `tsx` for TypeScript execution. ```bash npm run test # Run all tests npm run verify # Typecheck + lint + tests ``` ## Test Structure Tests are colocated with source files using the `.test.ts` suffix: ``` src/ ├── services/ │ ├── auth.ts │ ├── auth.test.ts # Auth service tests │ └── shared/ │ ├── crud-service.ts │ ├── crud-service.test.ts │ └── role-policy.test.ts ├── api/controllers/ │ ├── auth.controller.ts │ └── auth.controller.test.ts ├── middlewares/ │ ├── error-handler.ts │ └── error-handler.test.ts ├── db/api/shared/ │ ├── repository.ts │ └── repository.test.ts └── test-utils/ └── index.ts # Shared test utilities ``` ## Test Utilities Located in `src/test-utils/index.ts`: ### Test Data Builders ```typescript import { createTestUser, createGlobalAccessUser } from '@/test-utils'; // Create a standard test user const user = createTestUser(); // Create user with global access const admin = createGlobalAccessUser(); // Override specific properties const customUser = createTestUser({ organizationId: 'custom-org', app_role: { name: 'director', globalAccess: false }, }); ``` ### Mock DB API Factory ```typescript import { createMockDbApi } from '@/test-utils'; // Create a mock with default behavior const mockDbApi = createMockDbApi(); // Customize responses const mockDbApi = createMockDbApi({ findBy: async (where) => where.id === 'exists' ? { id: 'exists' } : null, }); // Check calls expect(mockDbApi.calls.create.length).toBe(1); mockDbApi.reset(); // Clear call history ``` ### Mock Request/Response ```typescript import { createMockRequest } from '@/test-utils'; const req = createMockRequest({ body: { email: 'test@example.com' }, currentUser: createTestUser(), }); ``` ## Current Coverage ### Services | File | Description | Tests | |------|-------------|-------| | `services/auth.test.ts` | Auth helpers and service methods | ~40 | | `services/shared/crud-service.test.ts` | CRUD factory | ~20 | | `services/shared/role-policy.test.ts` | Role constraints | ~10 | | `services/shared/audio-access.test.ts` | Audio-library visibility/management rules | ~12 | | `services/refresh-token-maintenance.test.ts` | Refresh-token retention cutoff + cleanup orchestration (mocked DB API) | ~4 | ### Domain constants / pure rules | File | Description | Tests | |------|-------------|-------| | `shared/constants/audio-files.test.ts` | `file`/`url`/`recipe` kinds + `isAudioFileKind` | ~2 | | `shared/constants/policy-documents.test.ts` | category validation + version-bump re-acknowledgment rule | ~several | | `shared/constants/users.test.ts` | honorific name-prefix formatting (`formatPersonName`) | ~several | ### Controllers | File | Description | Tests | |------|-------------|-------| | `api/controllers/auth.controller.test.ts` | Auth endpoints | ~20 | | `api/controllers/campus_attendance.controller.test.ts` | Attendance endpoints | ~10 | ### Infrastructure | File | Description | Tests | |------|-------------|-------| | `middlewares/error-handler.test.ts` | Error normalization | ~10 | | `db/api/shared/repository.test.ts` | Repository base | ~10 | | `shared/architecture/import-boundaries.test.ts` | Architecture validation | ~5 | ## Testing Patterns ### Pure Function Tests Test pure functions directly without mocking: ```typescript import { test } from 'node:test'; import assert from 'node:assert/strict'; test('formats user display name', () => { const result = formatDisplayName('John', 'Doe'); assert.equal(result, 'John Doe'); }); ``` ### Service Tests with Mocked DB APIs Mock the data layer to test service logic: ```typescript import { test, describe, mock, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; describe('UserService', () => { let mockDbApi: ReturnType; beforeEach(() => { mockDbApi = createMockDbApi(); }); test('creates user with hashed password', async () => { mockDbApi.create.mock.mockImplementation(async (data) => ({ id: 'new-user', ...data, })); await createUser({ email: 'new@example.com', password: 'secret' }); assert.equal(mockDbApi.calls.create.length, 1); const [data] = mockDbApi.calls.create[0]; assert.notEqual(data.password, 'secret'); // Should be hashed }); }); ``` ### Controller Tests Mock services and test request/response handling: ```typescript import { test, describe, mock } from 'node:test'; import assert from 'node:assert/strict'; describe('auth controller', () => { test('returns user profile on successful signin', async () => { const req = createMockRequest({ body: { email: 'test@example.com', password: 'password' }, }); const res = createMockResponse(); await signinHandler(req, res, mockAuthService); assert.equal(res.statusCode, 200); assert.equal(res.body.email, 'test@example.com'); }); }); ``` ## Adding New Tests 1. Create a `.test.ts` file next to the source file 2. Import from `node:test` and `node:assert/strict` 3. Use `@/test-utils` for common setup 4. Follow the describe/test structure 5. Run `npm run test` to verify ## Best Practices - Test behavior, not implementation details - Use descriptive test names that explain the expected behavior - Keep tests focused - one assertion per test when possible - Mock at the boundary (DB APIs, external services) - Use `beforeEach` to reset mocks between tests - Prefer `assert.deepEqual` for objects, `assert.equal` for primitives