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

FileLinesCoverage
tests/api/auth.test.ts317Register, login, token validation, rate-limit headers
tests/api/security.test.ts133JWT forgery rejection, env var requirements, CSV injection, bcrypt
tests/api/structure.test.ts85Pure module-export smoke (no server needed)
tests/api/companies.test.ts472CRUD, pagination, filter by source, RoPA write
tests/api/leads.test.ts536CRUD, multi-filter list, POST /leads/:org_nr/validate, RoPA write
tests/api/projects.test.ts322CRUD, org-scoped list, RoPA write
tests/api/organizations.test.ts308CRUD + reject delete-with-users (409), RoPA write
tests/api/users.test.ts343CRUD, password update, duplicate-email rejection
tests/api/search.test.ts332/api/search?q=, /api/search/advanced, multi-source breakdown
tests/api/documents.test.ts468CRUD with 1536-dim pgvector embeddings, /similar endpoint
tests/api/index.test.ts655End-to-end happy-path across all resources + cleanup
tests/api/kundkort-enrich.test.ts88POST /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-Reset headers

Security (security.test.ts)

  • verifyTokenSignature (from src/api/auth.ts) rejects forged signatures, malformed tokens, tampered payloads, and expired tokens
  • sanitizeCSVValue (from src/api/export.ts) prefixes =, +, -, @ with ' to block formula injection
  • Production-only assertions check JWT_SECRET length ≥ 32, KEYCLOAK_CLIENT_SECRET set, HASH_SALT length ≥ 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_Log row 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/similar returns ranked neighbours
  • Leads add POST /api/leads/:org_nr/validate with a four-layer validationLayers payload (see Lead Scoring)

Search (search.test.ts)

  • Seeds 4 rows (2 companies + 2 leads) under source='search_test' in beforeAll
  • /api/search?q=...: 400 on missing/empty q, 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: filters name, city, sniCode, orgNr, postalCode, isActive, isValidated, minConfidence, q — returns echoed filters + paginated results

Structure (structure.test.ts)

  • No server needed. Imports companyHandlers, leadHandlers, searchHandlers from src/api/* and asserts methods exist
  • Imports validateCompany, validateLead, validateValidationResult, ValidationError from src/api/validation.ts and exercises happy/error paths

Kundkort enrich (kundkort-enrich.test.ts)

  • Tests POST /api/kundkort/${testOrgnr}/enrich with org_nr 5565672655 (Gordons Project AB)
  • 401 when unauthenticated unless KEYCLOAK_DEV_MODE=true (then 200)
  • 404 for 000000-0000, 400 for invalid orgnr 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' and LIKE '%Test%' to clean up. If you have non-test rows matching these patterns in enrichnodedb, they will be deleted by tests/api/index.test.ts:646–654.

See also

Test Strategy, Test Coverage Gaps, RoPA Log, Lead Scoring.

See also