Skip to content

OUT-3586: integration tests for price.created with testcontainers - #221

Merged
SandipBajracharya merged 7 commits into
masterfrom
OUT-3586
Apr 24, 2026
Merged

OUT-3586: integration tests for price.created with testcontainers#221
SandipBajracharya merged 7 commits into
masterfrom
OUT-3586

Conversation

@SandipBajracharya

Copy link
Copy Markdown
Collaborator

Summary

  • Splits integration-test work out of OUT-3546 into its own scope so that ticket can land its unit-test pieces independently
  • Adds a reusable Vitest integration harness: ephemeral Postgres via testcontainers, real Drizzle migrations, real Next.js route via next-test-api-route-handler, mocked CopilotAPI/IntuitAPI/Sentry
  • Adds 6 price.created webhook scenarios: happy path, flag-off early return, idempotency, multi-price naming suffix, QB createItem failure + tx rollback, Copilot product 404

What's covered

Scenario Asserts
priceCreated.happyPath qb_product_sync row + SUCCESS log written
priceCreated.flagOff createNewProductFlag=false → no API calls, no rows
priceCreated.idempotency Duplicate POST → no new row, no QB calls
priceCreated.multiPrice 2nd price for same product → QB item name suffixed " (1)"
priceCreated.qbFailure createItem throws → tx rolled back, FAILED log outside the tx
priceCreated.copilotNotFound getProduct undefined → APIError(404) → FAILED log

All 28 tests green locally (yarn test).

Architecture notes

  • pool: 'forks' + fileParallelism: false + isolate: false so all integration tests share one container with deterministic order
  • globalSetup.ts sets DATABASE_URL from the container before any src/ import — workers inherit env via fork
  • Module-level mocks via vi.mock(..., factory) — explicit factories avoid loading copilot-node-sdk (which has a broken ESM directory import)
  • Helpers (installMockApis, seedHealthyPortal, truncateAllTestTables) are reusable for the upcoming invoice.created / payment.succeeded tests

Follow-ups (not in this PR — tracked in OUT-3586, to be split into their own tickets)

  • GitHub Actions workflow (.github/workflows/test.yml) running yarn test on every PR + push to master
  • Nightly QB sandbox smoke test — one real-everything test against QB sandbox + Copilot dev workspace, cron-scheduled, excluded from PR runs (catches external-API drift)
  • Additional webhook coverageinvoice.created, payment.succeeded, etc.
  • Outstanding gaps in price.created — income-account creation path, special-character handling, auth-token-expired branch, 429 retry behavior
  • Pre-existing production bug surfaced (not introduced) by these tests: ProductService#webhookPriceCreated calls unsetTransaction() inside the db.transaction callback; cleanup is skipped on any throw. Same pattern likely exists in other BaseService subclasses.

Test plan

  • yarn test --project unit — unit tests still pass without Docker
  • yarn test --project integration — all 6 scenarios green (requires Docker)
  • yarn test — both projects, sequential, all green

Linear

OUT-3586 (sub-issue of OUT-3546): https://linear.app/assemblycom/issue/OUT-3586

🤖 Generated with Claude Code

@linear

linear Bot commented Apr 13, 2026

Copy link
Copy Markdown

@vercel

vercel Bot commented Apr 13, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
quickbooks-sync Building Building Apr 24, 2026 10:44am
quickbooks-sync (dev) Ready Ready Preview, Comment Apr 24, 2026 10:44am

Request Review

@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.

I think we should use a factory helpers to seed the data. But the PR looks good to me.

Comment thread test/integration/globalSetup.ts
@greptile-apps

greptile-apps Bot commented Apr 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a reusable Vitest integration test harness for price.created webhooks, using ephemeral Postgres via testcontainers, real Drizzle migrations, and next-test-api-route-handler. It covers 6 scenarios (happy path, flag-off, idempotency, multi-price suffix, QB failure + rollback, Copilot 404) with well-structured helpers designed to extend to future webhook types.

Confidence Score: 5/5

Safe to merge — all findings are P2 style suggestions with no correctness or data-integrity impact

The harness architecture is sound: try/finally on migration client (addressing the previously flagged concern), deterministic test ordering via fileParallelism: false, clean per-test state via TRUNCATE + installMockApis, and well-typed mock factories. The two remaining comments are maintenance notes, not bugs. All 28 tests are green locally per the PR author.

No files require special attention — the hardcoded table list in testDb.ts and the isolate: false footgun are documented trade-offs, not blocking issues.

Important Files Changed

Filename Overview
test/integration/globalSetup.ts Starts testcontainer Postgres, loads .env.test, runs Drizzle migrations with try/finally cleanup, and exports teardown — solid implementation
test/integration/setup.ts Registers shared module mocks (CopilotAPI, IntuitAPI, Sentry) via vi.mock factory — necessary to avoid broken ESM imports in copilot-node-sdk
test/helpers/mocks.ts Well-typed mock factories for CopilotAPI and IntuitAPI with type-safe override keys; shared-instance caveat for multi-instantiation sites is documented
test/helpers/priceCreatedTestSetup.ts Centralises beforeEach/afterEach registration (truncate + installMockApis + clearAllMocks); optsFactory pattern correctly avoids stale vi.fn() references across tests
test/helpers/testDb.ts TRUNCATE with RESTART IDENTITY CASCADE is correct; hardcoded table list requires manual maintenance when schema grows
vitest.config.ts Two-project config (unit + integration) with correct sequencing; isolate: false + fileParallelism: false is intentional for container sharing but creates an implicit per-file contract
src/config/index.ts Skips dotenv.config() under NODE_ENV=test to prevent local .env credential bleed; unit tests now get only fallback values, confirmed passing but creates a latent risk for future unit tests
test/integration/quickbooks/priceCreated/happyPath.test.ts Asserts qb_product_sync row shape and SUCCESS sync log end-to-end; well-scoped, no issues
test/integration/quickbooks/priceCreated/qbFailure.test.ts Verifies tx rollback (no mapping row) and FAILED log written outside the tx; correctly documents the known unsetTransaction production bug without being blocked by it
test/integration/quickbooks/priceCreated/idempotency.test.ts Seeds existing qb_product_sync row, verifies duplicate POST triggers no QB calls and no new rows; early-return-skips-logging behavior is intentionally tested

Sequence Diagram

sequenceDiagram
    participant V as Vitest Runner
    participant GS as globalSetup.ts
    participant TC as testcontainers Postgres
    participant SF as setup.ts (setupFiles)
    participant T as Test File
    participant W as postWebhook helper
    participant R as Next.js Route (NTARH)
    participant DB as Test DB

    V->>GS: run globalSetup
    GS->>TC: start PostgreSqlContainer
    TC-->>GS: connection URI
    GS->>DB: drizzle migrate (try/finally)
    GS->>V: inject DATABASE_URL into process.env

    V->>SF: load setupFiles (vi.mock CopilotAPI, IntuitAPI, Sentry)
    V->>T: evaluate test file

    loop each test
        T->>DB: truncateAllTestTables (beforeEach)
        T->>T: installMockApis sets mockImplementation
        T->>DB: seed portal, settings, optional product sync
        T->>W: postWebhook(payload)
        W->>R: NTARH POST webhook route with auth stub
        R->>T: mock CopilotAPI.getTokenPayload returns portalId
        R->>DB: lookup portal connection + settings
        R->>T: mock CopilotAPI.getProduct / IntuitAPI calls
        R->>DB: insert qb_product_sync + qb_sync_logs or rollback
        W-->>T: Response
        T->>DB: assert rows in QBProductSync, QBSyncLog
        T->>T: vi.clearAllMocks afterEach resets counts not impl
    end

    V->>GS: teardown container.stop()
Loading

Reviews (5): Last reviewed commit: "fix(OUT-3586): seed tokenSetTime so test..." | Re-trigger Greptile

Comment thread test/integration/globalSetup.ts Outdated
Comment thread test/integration/globalSetup.ts Outdated
Comment thread test/helpers/testDb.ts
@SandipBajracharya
SandipBajracharya changed the base branch from OUT-3546 to master April 24, 2026 08:39
SandipBajracharya and others added 5 commits April 24, 2026 14:28
… Postgres

Adds a reusable integration-test harness that spins up an ephemeral Postgres
via testcontainers, applies Drizzle migrations, and wires it to the Next.js
route handlers through next-test-api-route-handler. The harness is split
from the existing unit project via Vitest `projects` so unit tests keep
running fast without Docker.

Key pieces:
- globalSetup.ts starts the container, sets DATABASE_URL, runs migrations
- setup.ts provides shared module mocks (CopilotAPI, IntuitAPI, Sentry)
  that avoid loading copilot-node-sdk's broken ESM directory import
- helpers/{testDb,seed,mocks}.ts expose truncate + seed + mock-install utilities
- .env.test holds non-secret stubs for src/config env vars
- Integration project runs single-fork, no file parallelism, shares the
  container across files (TRUNCATE between tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Integration tests were returning 400 because addSyncBreadcrumb in
src/utils/sentry.ts calls Sentry.addBreadcrumb, which wasn't stubbed in
the shared @sentry/nextjs mock.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the six price.created integration tests into a priceCreated/
subfolder and extract shared setup into test/helpers/webhook.ts and
test/helpers/priceCreatedTestSetup.ts so subsequent webhook suites can
reuse the same scaffolding without duplicating mock-wiring and request
boilerplate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/config/index.ts unconditionally loaded .env on import, which backfilled
any env var not stubbed in .env.test from the developer's local .env file
during test runs. Harmless for mocked integration tests but a real risk for
the upcoming nightly smoke tests (OUT-3649) that hit real QuickBooks and
Copilot APIs. Guard the dotenv call on NODE_ENV !== 'test' so tests only see
what globalSetup loads explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the stale singleFork=true reference with the actual mechanism —
pool: 'forks' + fileParallelism: false — so a future maintainer searching
the config for singleFork doesn't come up empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without tokenSetTime, isTokenFresh() returns false and getValidQbTokens
triggers a real HTTP call to Intuit's OAuth endpoint, which rejects the
stub INTUIT_CLIENT_ID with invalid_client and fails every price.created
integration test with a 400. Seeding tokenSetTime keeps the token in the
fresh window so the refresh path is skipped entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@SandipBajracharya
SandipBajracharya merged commit d897645 into master Apr 24, 2026
4 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