Skip to main content

Integration Testing (Live TerminusDB)

The integration suite in backend/tests/integration-live/** exercises route → service → real TerminusDB with the real Fastify server and the full production schema. It has its own Vitest config (backend/vitest.integration.config.ts) and is excluded from the default npm run test:run, which still passes with no services available.

Why It Exists

Mocked unit tests cannot catch entire classes of regression that only appear against a live database:

  • GraphQL field/schema mismatches — e.g. Person(id:) does not match subclass documents (Faculty/...); the correct form is Person(filter: { _id }). A mocked client happily returns whatever the test stubs.
  • TerminusDB Set-ordering behaviorSet fields come back unordered; code that assumes insertion order passes against mocks and fails live.
  • Schema drift — queries or writes referencing fields that no longer exist in pbs-schema.json.

The suite loads the full production schema and runs the real workflow/approval flows end to end, so these break loudly before deploy instead of in production.

Running Locally

From backend/:

npm run test:integration:db-up    # disposable terminusdb/terminusdb-server:v12.0.5 on 127.0.0.1:6364
npm run test:integration # creates pbs_itest DB, loads full pbs-schema.json, runs suites
npm run test:integration:db-down # removes the container (all test data discarded)

The database container is throwaway — every run starts from a clean pbs_itest database and teardown drops it.

Safety Guards (Do Not Weaken)

The suite is engineered so it cannot touch the production pbs-terminusdb container:

  1. Loopback-only target. tests/integration-live/env.ts (assertSafeTestTarget) refuses to run unless TERMINUSDB_URL is exactly the loopback test instance (http://127.0.0.1:6364 by default — never port 6363) and TERMINUSDB_DB starts with pbs_itest.
  2. Triple-gated test auth. Authentication uses an x-test-user header bypass that requires NODE_ENV=test and TEST_AUTH_BYPASS=1 and buildServer({ testAuth: true }). It is inert in production; tests/unit/plugins/testAuthBypass.test.ts asserts every gate.
  3. No real side-channel services. Redis/BullMQ are vi.mocked (setup.ts); enqueued jobs are captured in queueCapture.ts so tests can assert on notifications without sending anything. Firebase is absent (the plugin skips init under NODE_ENV=test); email templates fall back to embedded defaults.

Layout

backend/tests/integration-live/
├── env.ts # assertSafeTestTarget — loopback + pbs_itest guard
├── global-setup.ts # idempotent DB lifecycle: delete → create pbs_itest →
│ # load FULL src/schema/pbs-schema.json via
│ # /api/document?graph_type=schema; teardown drops the DB
├── setup.ts # per-suite mocks (Redis/BullMQ)
├── queueCapture.ts # captured queue jobs for notification assertions
├── helpers.ts # fixtures and server/query helpers (below)
├── workflows/ # award-nomination, fellowship, thesis, independent-research,
│ # major-plan flow suites + authz.test.ts
├── admin/ migrations/ sync/ # additional suites

Key helpers (helpers.ts)

  • buildTestServer() — boots the real buildServer exported from backend/src/index.ts (plugins + routes + boot registrations, no listen). Health routes register before plugins on purpose — /api/health must stay reachable without auth (it is the deploy gate).
  • TestFixtures — typed, test_-prefixed persons/groups created via the Document API, with auto-cleanup including the workflow-artifact cascade (WorkflowInstance / StageInstance / StageApproval).
  • asUser(actor) — builds the x-test-user headers for the triple-gated bypass, so suites can act as a specific student, advisor, or committee member.
  • gql(query, variables), getDocument / replaceDocument, getWorkflowStateForEntity(entityUri) — direct query and workflow-state assertion helpers.
  • testId(type) / fullUri / shortId — id construction utilities (terminusdb:///data/ prefix handling).

A typical workflow suite creates fixtures, submits an entity through the real bridge endpoint as a student, approves it as the resolved approver, and asserts both the unified StageApproval state and the captured notification jobs.

CI: This Suite Gates Production Deploys

The integration-tests job in .github/workflows/ci.yml runs npm run test:integration against a terminusdb/terminusdb-server:v12.0.5 service container (runner port 6364). The deploy job — which is what ships main to production — depends on integration-tests (alongside build, security-audit, and ferpa-compliance). A failing integration test blocks the deploy.

Practical implications:

  • If you change pbs-schema.json, a query, or a workflow flow, run the suite locally before pushing to mainmain deploys automatically on green CI.
  • New approval flows should ship with a flow suite under workflows/ (copy an existing one); these caught real bugs on day one, including the Person(id:) subclass mismatch.
  • Never point the suite at port 6363 or weaken assertSafeTestTarget to "make it run" — fix the environment instead.

When to Add an Integration Test (vs a Unit Test)

Use a live integration test when the behavior under test depends on what TerminusDB actually does:

  • A new or changed GraphQL query/filter shape (especially anything involving abstract types like Person, Set fields, or _id filtering)
  • A new approval/workflow flow or stage-completion hook — assert end state via getWorkflowStateForEntity
  • Authorization on routes that read or mutate real documents (see workflows/authz.test.ts for the pattern)
  • Document writes where field preservation matters (partial updates, schema-added fields)

Keep using mocked unit tests for pure logic: status mapping, validation, formatting, and error branches. Integration tests are slower (full schema load per run), so test the database-shaped behavior there and the combinatorics in unit tests.