Skip to content

OUT-3933: set up integration test harness with testcontainers - #64

Merged
SandipBajracharya merged 4 commits into
mainfrom
OUT-3933
Jul 1, 2026
Merged

OUT-3933: set up integration test harness with testcontainers#64
SandipBajracharya merged 4 commits into
mainfrom
OUT-3933

Conversation

@SandipBajracharya

Copy link
Copy Markdown
Collaborator

Summary

Reusable Vitest integration test harness for the Xero integration, modeled on the quickbooks-sync setup. Infra only — a smoke test verifies the harness boots; per-flow tests (e.g. product.created) come next.

What it provides

  • Ephemeral Postgres via testcontainers — Drizzle migrations applied in globalSetup before any worker starts; DATABASE_URL set dynamically and inherited by the test fork.
  • Single shared container/fork (pool: forks, maxWorkers: 1, fileParallelism: false, isolate: false); truncateAllTestTables (schema-derived) resets state in beforeEach.
  • Module mocks for CopilotAPI, XeroAPI, Sentry, and sleep, pinned on globalThis for the shared-state run mode. clearMocks: true keeps call-history clean between tests.
  • Helpersseed (typed row seeders + constants), webhook (drives /api/webhook via next-test-api-route-handler), mocks (mock API factories).
  • server-only aliased to an empty stub (it throws at import outside an RSC context).

Notes

  • .env.test holds non-secret stubs only and is un-ignored (!.env.test) so the harness works on clone.
  • test/ is excluded from the root tsconfig; pnpm typecheck and pre-push now also typecheck the test/ project so the typed mocks/seeders are gated.

Verification

  • pnpm typecheck — clean (src + test)
  • biome check — clean
  • pnpm test — 3/3 passing (container boots, migrations apply, seed/truncate work)

🤖 Generated with Claude Code

Add a reusable Vitest integration harness modeled on the quickbooks-sync
pattern: an ephemeral Postgres via testcontainers, Drizzle migrations in
globalSetup, globalThis-pinned module mocks for CopilotAPI/XeroAPI/Sentry,
and seed/testDb/webhook helpers. Wire test typechecking into typecheck and
pre-push since test/ is excluded from the root tsconfig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jun 30, 2026

Copy link
Copy Markdown

OUT-3933

@supabase

supabase Bot commented Jun 30, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project pkdwtcdqcefmlgxmcwmc because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
xero-integration Ready Ready Preview, Comment Jun 30, 2026 3:10pm

Request Review

@SandipBajracharya SandipBajracharya changed the title test(OUT-3933): set up integration test harness with testcontainers OUT-3933: set up integration test harness with testcontainers Jun 30, 2026
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR establishes an integration test harness for the Xero integration, modeled on the quickbooks-sync setup — no application logic changes are included. The infrastructure is well-structured: a single ephemeral Postgres container managed by testcontainers, Drizzle migrations applied once in globalSetup, and a shared-module-state Vitest config that prevents container URL drift across workers.

  • Ephemeral Postgres via testcontainersglobalSetup boots a postgres:16-alpine container, runs all Drizzle migrations, injects DATABASE_URL, and returns the teardown function; truncateAllTestTables (schema-derived, self-healing) resets state in beforeEach.
  • Typed helpersseed (typed Drizzle inserters + test constants), webhook (wraps next-test-api-route-handler), and mocks (factory functions with MockMethodOverrides for compile-time method-name safety) form a clean fixture API for future flow tests.
  • Config & CI gatesserver-only is stubbed via Vitest alias, test/ is excluded from the root tsconfig but covered by a separate typecheck:test script that is now part of both pnpm typecheck and pre-push.

Confidence Score: 5/5

Safe to merge — changes are entirely additive test infrastructure with no modifications to application code paths.

All production source files are untouched except the schema barrel addition of two already-existing tables. The harness is well-isolated: a dedicated testcontainer, a separate tsconfig, and explicit alias stubbing for server-only. The only notable design trade-off is clearMocks:true leaving mock implementations alive between tests, which is a footgun for future test authors but harmless for the smoke test included here.

vitest.config.ts — the clearMocks vs resetMocks choice will affect every future integration test that exercises mock APIs.

Important Files Changed

Filename Overview
test/integration/globalSetup.ts Boots the PostgreSQL testcontainer, injects DATABASE_URL, runs Drizzle migrations, and registers teardown. Logic is correct; migration client is properly closed in the finally block.
test/integration/setup.ts Registers module mocks for CopilotAPI, XeroAPI, Sentry, and sleep. GlobalThis pinning is a sound technique to prevent duplicate vi.fn() construction with isolate:false.
vitest.config.ts Integration config wires the container, single-worker fork pool, and shared module state correctly. clearMocks:true leaves mock implementations alive between tests — a footgun for future authors who omit installMockApis.
test/helpers/testDb.ts Derives the table list from the schema barrel and issues a single TRUNCATE … RESTART IDENTITY CASCADE; self-healing because new tables added to the barrel are picked up automatically.
test/helpers/seed.ts Typed row seeders for xeroConnections and settings; expires_at is now computed per seed call, so it stays fresh under isolate:false long runs.
test/helpers/mocks.ts Factory helpers for typed CopilotAPI / XeroAPI mock instances; MockMethodOverrides restricts override keys to real method names for compile-time typo detection.
test/helpers/webhook.ts Thin wrapper around next-test-api-route-handler for posting webhook payloads; correctly throws if the handler callback is never invoked.
src/db/schema/index.ts Added syncLogs and failedSyncs to the schema barrel so truncateAllTestTables is now self-healing — no manual table list to maintain.
test/tsconfig.json Extends root tsconfig and covers test/**/*.ts; combined with the typecheck:test script ensures the typed mocks/seeders are checked on pre-push.
test/integration/harness.smoke.test.ts Three-test smoke suite verifying container boot, migration, seed, and truncation; intentionally minimal and marked for deletion once real flow tests land.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant VS as Vitest Runner
    participant GS as globalSetup.ts
    participant TC as PostgreSQL Testcontainer
    participant DZ as Drizzle Migrator
    participant SF as setup.ts (setupFiles)
    participant T as Test File

    VS->>GS: execute globalSetup()
    GS->>GS: dotenv.config(.env.test, override:true)
    GS->>TC: PostgreSqlContainer.start()
    TC-->>GS: container (connection URI)
    GS->>GS: "process.env.DATABASE_URL = uri"
    GS->>DZ: migrate(db, migrationsFolder)
    DZ-->>GS: migrations applied
    GS->>GS: migrationClient.end()
    GS-->>VS: teardown fn registered

    VS->>SF: load setupFiles (once, isolate:false)
    SF->>SF: vi.mock CopilotAPI / XeroAPI / Sentry / sleep
    SF->>SF: pin vi.fn() constructors on globalThis

    loop each test
        VS->>T: beforeEach → truncateAllTestTables()
        T->>T: installMockApis() [recommended]
        T->>T: run test assertions
    end

    VS->>GS: teardown()
    GS->>TC: container.stop()
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant VS as Vitest Runner
    participant GS as globalSetup.ts
    participant TC as PostgreSQL Testcontainer
    participant DZ as Drizzle Migrator
    participant SF as setup.ts (setupFiles)
    participant T as Test File

    VS->>GS: execute globalSetup()
    GS->>GS: dotenv.config(.env.test, override:true)
    GS->>TC: PostgreSqlContainer.start()
    TC-->>GS: container (connection URI)
    GS->>GS: "process.env.DATABASE_URL = uri"
    GS->>DZ: migrate(db, migrationsFolder)
    DZ-->>GS: migrations applied
    GS->>GS: migrationClient.end()
    GS-->>VS: teardown fn registered

    VS->>SF: load setupFiles (once, isolate:false)
    SF->>SF: vi.mock CopilotAPI / XeroAPI / Sentry / sleep
    SF->>SF: pin vi.fn() constructors on globalThis

    loop each test
        VS->>T: beforeEach → truncateAllTestTables()
        T->>T: installMockApis() [recommended]
        T->>T: run test assertions
    end

    VS->>GS: teardown()
    GS->>TC: container.stop()
Loading

Reviews (2): Last reviewed commit: "chore(OUT-3933): add trailing new line" | Re-trigger Greptile

Comment thread test/helpers/seed.ts Outdated
Comment thread .husky/pre-push Outdated
Comment thread test/helpers/testDb.ts Outdated
SandipBajracharya and others added 2 commits June 30, 2026 20:45
Under isolate:false the seed module loads once for the whole run, so a
module-level expires_at could drift into the past on a long suite and flip
the seeded token to expired. Build it inside the seeder instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add syncLogs and failedSyncs to the schema barrel so it's complete, then
derive truncateAllTestTables from Object.values(schema). New tables are now
truncated automatically instead of needing a second manual update.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@SandipBajracharya

Copy link
Copy Markdown
Collaborator Author

@greptileai

Comment thread .husky/pre-push
Comment thread .husky/pre-push

@priosshrsth priosshrsth left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@SandipBajracharya
SandipBajracharya merged commit 05235da into main Jul 1, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants