Skip to content

Implement skipped invoice-ingest integration tests #45

@studert

Description

@studert

Problem

tests/integration/invoices/ingest.test.ts contains four it.todo placeholders that have never been implemented. The invoice-ingest endpoint is the production entry point for automated PDF uploads (R2 + Drizzle + period matching + duplicate detection) — leaving its primary tests as todos means we have zero coverage of the auth, validation, happy path, and duplicate handling for one of the riskiest endpoints in the app.

Evidence

tests/integration/invoices/ingest.test.ts:1-9:

import { describe, it } from \"vitest\";

describe(\"POST /api/invoices/ingest\", () => {
  // These tests require a running server and DB
  it.todo(\"returns 401 with no auth header\");
  it.todo(\"returns 400 when no PDF provided\");
  it.todo(\"returns 200 with valid PDF and correct Bearer token\");
  it.todo(\"returns 409 when submitting same invoice twice\");
});

The endpoint under test is src/app/api/invoices/ingest/route.ts. It uses INVOICE_INGEST_SECRET for Bearer auth, validates content-type and file size, runs extractInvoiceFields, writes a row to invoices and (when matched) billed_costs, and logs to ingestion_log.

Proposed approach

Replace the it.todo calls with real it(...) tests. Use the same integration-test harness already used in tests/integration/sync/*.test.ts (Vitest + real Neon DB).

  1. Setup: per-test transaction or per-suite cleanup that truncates invoices, billed_costs, ingestion_log. See existing patterns in tests/integration/sync/.

  2. returns 401 with no auth header: POST with no Authorization header → expect 401, no DB writes.

  3. returns 401 with bad bearer: (added) POST with Authorization: Bearer wrong → expect 401.

  4. returns 400 when no PDF provided: POST with valid bearer but missing or non-PDF body → expect 400 with the expected error JSON shape ({ success: false, error: ... } per project convention).

  5. returns 200 with valid PDF and correct Bearer token:

    • Use a small fixture PDF in tests/fixtures/sample-invoice.pdf.
    • Stub getR2Client / extractInvoiceFields (or use a deterministic test extractor).
    • POST → expect 200, expect a row in invoices, expect a change_history row, expect an ingestion_log row with outcome = 'success'.
  6. returns 409 when submitting same invoice twice: Submit the same fixture twice → second call returns 409, no second invoices row, ingestion_log records the duplicate outcome.

  7. Wire the test file into pnpm test:integration (it should be picked up automatically by the integration glob).

  8. Document any fixture or env requirement in tests/integration/README.md (create if missing).

Acceptance criteria

  • Zero it.todo calls remain in tests/integration/invoices/ingest.test.ts.
  • At least 5 passing tests covering: missing auth, bad auth, missing/invalid file, happy path, duplicate.
  • Tests pass under pnpm test:integration against a Neon test branch.
  • Fixtures committed under tests/fixtures/ with a README.
  • pnpm lint && pnpm typecheck pass.

Verification

  1. pnpm test:integration tests/integration/invoices/ingest.test.ts runs green locally with DATABASE_URL pointing at a test branch.
  2. Manually break the auth check in the route (e.g. always return 200) → confirm the auth tests fail. Revert.
  3. Break the duplicate detection (e.g. comment out the existence check) → confirm the duplicate test fails. Revert.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:billingBudgets, invoices, costspriority:highShip soontestsTest coverage / disabled tests

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions