40227-vm/backend/docs/test-coverage.md
2026-06-12 06:55:35 +02:00

209 lines
5.6 KiB
Markdown

# 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<typeof createMockDbApi>;
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