REST integration tests under tests/api/. 12 files, 4,059 lines. All require a running API server at http://localhost:${API_PORT ?? 3000} and a Postgres at localhost:5433. Each suite opens its own pg.Pool (max 5) and registers a fresh test user.
Files
| File | Lines | Coverage |
|---|---|---|
tests/api/auth.test.ts | 317 | Register, login, token validation, rate-limit headers |
tests/api/security.test.ts | 133 | JWT forgery rejection, env var requirements, CSV injection, bcrypt |
tests/api/structure.test.ts | 85 | Pure module-export smoke (no server needed) |
tests/api/companies.test.ts | 472 | CRUD, pagination, filter by source, RoPA write |
tests/api/leads.test.ts | 536 | CRUD, multi-filter list, POST /leads/:org_nr/validate, RoPA write |
tests/api/projects.test.ts | 322 | CRUD, org-scoped list, RoPA write |
tests/api/organizations.test.ts | 308 | CRUD + reject delete-with-users (409), RoPA write |
tests/api/users.test.ts | 343 | CRUD, password update, duplicate-email rejection |
tests/api/search.test.ts | 332 | /api/search?q=, /api/search/advanced, multi-source breakdown |
tests/api/documents.test.ts | 468 | CRUD with 1536-dim pgvector embeddings, /similar endpoint |
tests/api/index.test.ts | 655 | End-to-end happy-path across all resources + cleanup |
tests/api/kundkort-enrich.test.ts | 88 | POST /api/kundkort/:orgNr/enrich — calls real enrichment |
Authentication pattern
Every CRUD suite follows the same shape:
beforeAll: POST /api/auth/register → store token (fall back to login if user exists)
beforeEach (sometimes): DELETE FROM <table> WHERE source = '<suite>_test'
afterAll: cleanup + pool.end()Helper authApiRequest(path, options) injects Authorization: Bearer ${token}. Defined per file (no shared util) — minor duplication.
Key assertions
Auth (auth.test.ts)
- 201 on register, 409 on duplicate, 400 on bad email/short password/missing fields
- 200 on valid login, 401 on wrong password / unknown user
- Token works against
/api/companies; malformed tokens get 401 - Auth endpoints expose
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Resetheaders
Security (security.test.ts)
verifyTokenSignature(fromsrc/api/auth.ts) rejects forged signatures, malformed tokens, tampered payloads, and expired tokenssanitizeCSVValue(fromsrc/api/export.ts) prefixes=,+,-,@with'to block formula injection- Production-only assertions check
JWT_SECRETlength ≥ 32,KEYCLOAK_CLIENT_SECRETset,HASH_SALTlength ≥ 32 - Bcrypt cost test only verifies modules load — does not actually inspect the cost value
Note
The bcrypt-cost test (
security.test.ts:111) is a placeholder — it asserts the module imports, nothing more.
Companies / Leads / Projects / Documents
- Standard CRUD: list with pagination (default 20, capped at 100), get by id/org_nr (404 on miss), POST with required fields (400 on missing, 409 on duplicate orgNr), PUT partial update (400 on empty body), DELETE
- Each resource verifies a
RoPA_Logrow gets written for INSERT / UPDATE / VALIDATE / UPSERT operations — see RoPA Log - Documents test 1536-dimension pgvector embeddings (
Array(1536).fill(0).map(() => Math.random())); rejects wrong dimensions with 400;POST /api/documents/:id/similarreturns ranked neighbours - Leads add
POST /api/leads/:org_nr/validatewith a four-layervalidationLayerspayload (see Lead Scoring)
Search (search.test.ts)
- Seeds 4 rows (2 companies + 2 leads) under
source='search_test'inbeforeAll /api/search?q=...: 400 on missing/emptyq, returns{query, results, pagination, breakdown:{companies, leads, bolagsverket, scb}}- Result type is unified:
{type, id, org_nr, name, address, source, confidence_score} - Sort: leads with confidence_score first
/api/search/advanced: filtersname,city,sniCode,orgNr,postalCode,isActive,isValidated,minConfidence,q— returns echoed filters + paginated results
Structure (structure.test.ts)
- No server needed. Imports
companyHandlers,leadHandlers,searchHandlersfromsrc/api/*and asserts methods exist - Imports
validateCompany,validateLead,validateValidationResult,ValidationErrorfromsrc/api/validation.tsand exercises happy/error paths
Kundkort enrich (kundkort-enrich.test.ts)
- Tests
POST /api/kundkort/${testOrgnr}/enrichwith org_nr5565672655(Gordons Project AB) - 401 when unauthenticated unless
KEYCLOAK_DEV_MODE=true(then 200) - 404 for
000000-0000, 400 forinvalidorgnr format - Asserts response shape
{success, orgNr, validationResult: {isHeltValiderad, leadScore, validatedAt}} - Will hit live enrichment pipeline (network, may be flaky)
Cleanup hazards
Warning
Suites use
LIKE '%@example.com'andLIKE '%Test%'to clean up. If you have non-test rows matching these patterns inenrichnodedb, they will be deleted bytests/api/index.test.ts:646–654.
See also
Test Strategy, Test Coverage Gaps, RoPA Log, Lead Scoring.