Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
## Running tests (ALJ)

The file `backend/src/tests/tests-ALJ.test.ts` contains both unit and integration suites.

- **Unit tests** are fully mocked (no Docker, no DB):
```bash
cd backend && npm run test:unit
```
- **Integration tests** spin up a disposable PostgreSQL via Testcontainers and run real Prisma migrations against it. Docker must be running:
```bash
cd backend && npm run test:integration
```
- **Everything**:
```bash
cd backend && npm run test:all
```

First integration run pulls the `postgres:latest` image (~671 MB). Subsequent runs reuse the cached image; container boot is ~1 s and a full `test:all` finishes in ~3 s on a warm cache.

### macOS / Docker Desktop note

The integration suite sets `process.env.TESTCONTAINERS_RYUK_DISABLED = 'true'` in its `beforeAll`. Ryuk is Testcontainers' cleanup sidecar and fails to reach the Docker daemon socket on some Docker Desktop configurations on macOS. Container cleanup is still deterministic via the suite's `afterAll → container.stop()`. If Jest is killed mid-run (rare), a leftover postgres container can be cleaned up with `docker container prune`.

### Schema bootstrap: `db push` vs `migrate deploy`

The integration suite uses `npx prisma db push --skip-generate --accept-data-loss` rather than `migrate deploy`. `db push` applies `prisma/schema.prisma` directly to the disposable container, bypassing the `prisma/migrations/` folder. This means the test suite works even if the migrations folder is empty or out of sync with the schema (which is the case if you've ever run `prisma migrate reset` without re-creating an init migration). If you regenerate migrations with `prisma migrate dev`, the test suite remains unaffected.

### Why `app.listen` doesn't bind a port during tests

`src/index.ts` calls `app.listen(3010)` unconditionally. Both test suites stub `express.application.listen` to a no-op inside their `jest.isolateModules` blocks before requiring the app. This prevents the test process from competing with the dev server (or stale prior test runs) on port `3010`. Supertest is unaffected because it uses `http.createServer(app).listen(0)` directly, not the express prototype method.

### Test inventory

**30 tests total — 22 unit + 8 integration.**

#### `UNIT — mocked, no database`

**Validator (`validateCandidateData`) — 7**

1. accepts a fully valid candidate payload
2. rejects missing `firstName`
3. rejects missing `lastName`
4. rejects invalid email format
5. rejects invalid phone format (must match Spanish 6/7/9 + 8 digits)
6. rejects malformed `educations[].startDate`
7. rejects `cv` missing `fileType`

**Domain models — 4**

8. `Candidate.save()` (no id) calls `prisma.candidate.create` with flat fields and no nested writes
9. `Education.save()` (new) calls `prisma.education.create` with the `candidateId` linkage
10. `WorkExperience.save()` (new) calls `prisma.workExperience.create` with the `candidateId` linkage
11. `Resume.save()` persists `filePath`, `fileType`, `candidateId`, and a generated `uploadDate`

**Service (`addCandidate`) — 4**

12. happy path: validates, then saves Candidate + each Education + WorkExperience + Resume
13. validator failure short-circuits: throws and never calls Prisma
14. Prisma `P2002` (unique-email) is mapped to "email already exists" domain error
15. empty `educations` and `workExperiences` arrays do not trigger nested creates

**Routes — `POST /candidates` via supertest (also covers controller-equivalent behaviour) — 7**

16. valid body → 201, body equals service's return value
17. valid body invokes the service exactly once with the request body
18. service throws `Error("Invalid email")` → 400 with the error message
19. service throws "email already exists" Error → 400 with that message
20. service throws a non-Error (string) → 500 with "unexpected" message
21. `GET /unknown-route` → 404
22. `GET /` (root) → 200 with the LTI greeting (sanity check that the app booted)

#### `INTEGRATION — Testcontainers + Prisma + supertest` — 8

23. `POST /candidates` creates a candidate with educations, workExperiences, and resume
24. `POST /candidates` twice with the same email → second is rejected, only one row in DB
25. `POST /candidates` with invalid email → 400 and zero rows persisted
26. `POST /candidates` with empty `educations` and `workExperiences` arrays → 201, candidate row exists, zero child rows
27. repository: `Candidate.save()` against real Prisma — row appears, ids autoincrement
28. referential integrity: deleting a candidate with children fails (schema is `onDelete: RESTRICT`)
29. date handling: ISO date strings in the request body are stored as `DateTime` columns and roundtrip
30. CV persistence: payload's `cv` produces a linked resume row (no real disk write)
7 changes: 6 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,24 @@
"swagger-ui-express": "^5.0.0"
},
"devDependencies": {
"@testcontainers/postgresql": "^11.14.0",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.9",
"@types/jest": "^29.5.13",
"@types/multer": "^1.4.11",
"@types/node": "^20.12.10",
"@types/supertest": "^7.2.0",
"eslint": "^9.2.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"jest-mock-extended": "^4.0.1",
"prettier": "^3.2.5",
"prisma": "^5.13.0",
"supertest": "^7.2.2",
"testcontainers": "^11.14.0",
"ts-jest": "^29.2.5",
"ts-node": "^9.1.1",
"ts-node": "^10.9.2",
"ts-node-dev": "^1.1.6",
"typescript": "^4.9.5"
}
Expand Down
Loading