Files
support_backend/specs/002-saas-integration/quickstart.md
T
saqib mirandClaude Sonnet 5 55253287b3 feat: per-integration and per-user rate limiting (US3) + polish
Implements tasks T026-T032 from specs/002-saas-integration/tasks.md
(User Story 3, P3 - the final piece of this feature) plus Polish.

- New bespoke Redis fixed-window counter (checkRateLimit,
  src/infrastructure/cache/rate-limiter.ts) rather than
  @fastify/rate-limit's default onRequest-stage hook -- that hook
  runs before this feature's preHandler-based auth resolves the
  integration/user identity the limit needs to key on. A second
  preHandler (checkIntegrationRateLimit) runs after
  authenticateProductIntegration on the inbound route, checking the
  integration-level limit then the per-user limit independently,
  each throwing the existing RateLimitError (429
  RATE_LIMIT_EXCEEDED) on breach.
- New integration test (inbound-rate-limit.test.ts) verifies both
  limits are enforced independently against a real Postgres/Redis:
  a throttled user doesn't affect others, and the integration cap
  throttles even when no individual user has hit their own limit.
- Docs: contracts/quickstart updated from the placeholder
  "RATE_LIMITED" code to the actual reused RATE_LIMIT_EXCEEDED code;
  cleaned up a duplicated paragraph in the admin endpoints section;
  added a "SaaS Integration" section to README.md documenting the
  inbound contract, admin routes (and their known auth-stub
  limitation), and how rate limits are configured.

All 32 tasks in tasks.md are now complete -- all three user stories
(P1 trust boundary, P2 admin lifecycle, P3 rate limiting) are
implemented and covered by integration tests verified against a
live database, in addition to unit tests for the crypto/token
primitives. Full quality gate (typecheck/lint/format/architecture/
unit tests) passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 14:42:20 +05:30

3.9 KiB

Quickstart: Validating SaaS Product Integration & Inbound Request Trust

Prerequisites: local dev environment running (docker:up:dev or equivalent), Prisma migrated with this feature's schema changes applied, one Product + ProductIntegration seeded (or created via the admin endpoints below).

Scenario 1 — a valid, in-scope request is accepted (User Story 1)

  1. Register a product integration (admin endpoint) and note the returned signing secret.
  2. Sign a token for that integration with tenantId/userId values inside its allowedScope.
  3. Send the inbound request with Authorization: Bearer <token> and a body matching the contract.
  4. Expected: 200-level response; a CustomerReference row exists for the tenantId/ userId; an AuditLog row records integration.auth.success.

Scenario 2 — invalid/unregistered credential is rejected (User Story 1)

  1. Send the same request with a token signed by an arbitrary/wrong secret.
  2. Expected: 401 INVALID_INTEGRATION_CREDENTIAL; no CustomerReference created; an AuditLog row records integration.auth.failure with reason: invalid_credential.
  3. Repeat with a productId that has never been registered at all.
  4. Expected: the exact same 401 INVALID_INTEGRATION_CREDENTIAL response — confirm the two failure modes are indistinguishable from the response alone (FR-010).

Scenario 3 — suspended product is distinguishably rejected (User Story 1, Edge Cases)

  1. Set the seeded ProductIntegration.status (or Product.status) to suspended.
  2. Send a request with an otherwise-valid token.
  3. Expected: 403 PRODUCT_INTEGRATION_SUSPENDED — distinguishable from Scenario 2's 401.

Scenario 4 — unknown field rejects the whole request (User Story 1)

  1. Send an otherwise-valid request body with one extra, undefined field.
  2. Expected: 400 VALIDATION_ERROR — the request is rejected outright, not partially processed with the extra field ignored.

Scenario 5 — credential rotation is zero-downtime (User Story 2)

  1. Rotate the seeded integration's credential (admin endpoint) — note both old and new secrets.
  2. Immediately send one request signed with the OLD secret and one with the NEW secret.
  3. Expected: both succeed (SC-003).
  4. Wait past the transition window (or adjust it down for the test), then retry with the OLD secret.
  5. Expected: 401 INVALID_INTEGRATION_CREDENTIAL — old credential now rejected.

Scenario 6 — revocation takes effect immediately (User Story 2)

  1. Revoke the seeded integration's credential (admin endpoint).
  2. Immediately send a request signed with that credential.
  3. Expected: 401 INVALID_INTEGRATION_CREDENTIAL on the very next request (SC-002); an AuditLog row records the revocation itself as an admin action.

Scenario 7 — audit trail is retrievable (User Story 2)

  1. After Scenarios 1-6 above, call the admin "get audit trail" endpoint for the seeded integration.
  2. Expected: a chronological list including the success from Scenario 1 and the failures from Scenarios 2-4, each without any raw credential value present anywhere in the response.

Scenario 8 — rate limiting throttles one integration/user without affecting others (User Story 3)

  1. Seed two separate product integrations, A and B.
  2. Send requests from integration A past its configured rateLimitPerMinute.
  3. Expected: later requests from A in the burst receive 429 RATE_LIMIT_EXCEEDED; concurrent requests from integration B continue succeeding normally.
  4. Within integration A, send requests as two different userIds, one past rateLimitPerUserPerMinute and one under it.
  5. Expected: the over-limit user is throttled; the other user's requests continue succeeding.

What "done" looks like

All eight scenarios pass, and together they demonstrate every functional requirement and success criterion in spec.md without needing to read the plugin/module implementation to know what "correct" means.