209 lines
5.6 KiB
Markdown
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
|