test(OUT-3708): unit + integration tests for invoice.created webhook - #244
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Canary test verifying the shared infra (seeders, fixture, mocks, setup helper) wires up correctly. Asserts QB customer + invoice creation, mapping rows persisted, and a SUCCESS sync log written. Mock defaults required no adjustment — flow is greener than expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The per-file vi.mock factory in expiringSweep.test.ts didn't survive
Vitest's shared module registry under pool:forks + fileParallelism:false +
isolate:false. Once any earlier test transitively loaded @/utils/intuit
(via auth.service.ts), the per-file mock no longer applied.
Moves the module mock to test/integration/setup.ts so it's installed
before any test-file imports run, matching the existing pattern for
CopilotAPI/IntuitAPI/Sentry. expiringSweep.test.ts wires its per-test
behavior via vi.mocked(Intuit.getInstance) inside beforeEach.
The mock singleton is pinned on globalThis. Vitest's setupFiles get
evaluated more than once per run when separate test files spin up
fresh module-graph contexts under this config — a naive
`vi.mock(..., () => ({ default: { getInstance: vi.fn() } }))` produces
a different vi.fn() per factory invocation, so the test-file wiring
ends up on a different mock than the one tokenRefresh.ts closed over.
Storing the singleton on globalThis (single process, since
fileParallelism is false) makes every factory invocation return the
same getInstance mock, so beforeEach wiring is what the runtime sees.
Verified: 10 consecutive full integration-suite runs pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Exists comment - happyPath: default getAnItem(undefined, id, true) mock returned undefined, collapsing the mapped-product path into the Assembly Service fallback. Both branches happened to yield Id '999' via createItem default. The ItemRef assertion was passing via the wrong path. Override getAnItem to return a real item when queried by id and pin the mapped-product branch via two new assertions: createItem is invoked exactly once for 'Assembly Service' (handleServiceItem still runs because seedHealthyPortal does not set serviceItemRef) and the line Description comes from copilot.getProduct, which only happens on the mapped path. - invoiceAlreadyExists: corrected the comment claiming the existence check runs after customer resolution. It runs first, before any service call. Added an explicit qb_customers count = 0 assertion to make the invariant load-bearing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d tests - Integration test for the QBO DocNumber collision flow: pre-check detects the duplicate, and a 6240 race after pre-check triggers a single re-walk. - Unit test for resolveAvailableDocNumber covering base, suffix walk, and exhaustion (Sentry capture + rethrow). - Default findInvoicesByDocNumberPrefix in the shared IntuitAPI mock so existing invoice.created tests no longer fall through undefined. - Rewrite describe/it text across all invoice.created tests in plain English and strip ticket/internal-symbol references from comments. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4581220 to
17dc954
Compare
Greptile SummaryAdds comprehensive test coverage (9 integration + 2 unit files) for the
Confidence Score: 5/5Safe to merge — all changes are test code and configuration; no production logic is modified. The PR adds tests and fixes a flake; no production paths are touched. The globalThis singleton for the @/utils/intuit mock is unconventional but well-documented and addresses a real shared-module-registry constraint. All test logic is structurally sound: Once-queue mocks are correctly scoped inside optsFactory, the optsFactory pattern ensures fresh vi.fn() instances per beforeEach, and the idempotency/invoiceAlreadyExists/happyPath branches each target distinct code paths without overlapping DB state. No files require special attention. Important Files Changed
Reviews (2): Last reviewed commit: "test(OUT-3708): rebuild DocNumber-race m..." | Re-trigger Greptile |
…mports - Exclude test/ from the root tsconfig so the IDE stops suggesting test helpers when editing source files. - Add test/tsconfig.json that extends root and re-includes test/** so test files still get full type-checking and the @/* + @test/* path aliases. - Add a src/**-scoped ESLint no-restricted-imports rule that errors on any @test/* or relative ../test/** import from source code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mments Move the findInvoicesByDocNumberPrefix and createInvoice mocks for the 6240-race block inside optsFactory so each beforeEach gets fresh vi.fn()s with fresh Once-queues. clearAllMocks does not refill consumed Once queues, so a watch-mode re-run or a second it in the block would otherwise see undefined returns. Also strip restatement-style comments that just narrate the next assertion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
priosshrsth
left a comment
There was a problem hiding this comment.
This pr is a very good start. Well done.
…safety
- Replace test/fixtures/{invoiceCreated,priceCreated}.webhook.json with
TypeScript fixtures typed against z.input<typeof Schema> so DTO drift
surfaces as a compile-time error.
- Drop wire-only extras the schemas don't define; reference InvoiceStatus
enum directly.
- Update 16 test files to import from the new .ts path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
invoice.createdwebhook handler.priceCreated/integration suite shape (testcontainers + module-mocked Copilot/Intuit).expiringSweep.test.tssurfaced by the higher integration-file count.Linear: OUT-3708
What's covered
Integration (
test/integration/quickbooks/invoiceCreated/):happyPath— OPEN status, mapped product, new customer (asserts mapped-product path viaDescriptionfromcopilot.getProduct)draftSkip— DRAFT gate sits before claimidempotency— pre-existing CREATED log blocks second deliveryinvoiceAlreadyExists—getInvoiceByNumbershort-circuits at the top ofwebhookInvoiceCreated, before customer resolutionqbCreateInvoiceFails— claim row flipped to FAILED witherrorMessagestatusPaidCreatesPayment— PAID branch creates Payment withLinkedTxnlinked to the just-created invoiceoneOffLineItem— line item without productId/priceId routes through the Assembly Service refcreateNewProductFlagOff— flag-off + unmapped line item falls back to one-off (noqb_product_syncrow)useCompanyNameFlag— company-only payload + flag-on creates customer withcustomerType='company'Unit (
test/unit/api/quickbooks/webhook/handleInvoiceCreated.test.ts):Side fix
expiringSweep.test.tswas using a per-filevi.mock('@/utils/intuit', …)factory. Under integration'spool:forks + fileParallelism:false + isolate:falseconfig, that mock didn't survive across files once any earlier test transitively loaded the real@/utils/intuit(every webhook test does, viaauth.service.ts). With master's 7 integration files this never landed in the bad slot; with this branch's 16, it landed ~50% of the time.Fix: moved the module mock to
test/integration/setup.tsand pinned the singleton onglobalThis(Vitest re-evaluates the setupFiles factory more than once per session under these flags — a naive shared mock still produces multiplevi.fn()instances). Verified with 10 consecutive full-suite runs all green. Full rationale in commit2664c98and in the inline comment insetup.ts.Out of scope (separate tickets)
delayMs)Test plan
test.yml+lint.yml)yarn test— 22 files / 101 tests passing (verified 10/10 consecutive runs locally)yarn lint:check— cleanyarn prettier:check— cleanglobalThismock pin intest/integration/setup.tsis intelligible🤖 Generated with Claude Code