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>
3.9 KiB
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)
- Register a product integration (admin endpoint) and note the returned signing secret.
- Sign a token for that integration with
tenantId/userIdvalues inside itsallowedScope. - Send the inbound request with
Authorization: Bearer <token>and a body matching the contract. - Expected:
200-level response; aCustomerReferencerow exists for thetenantId/userId; anAuditLogrow recordsintegration.auth.success.
Scenario 2 — invalid/unregistered credential is rejected (User Story 1)
- Send the same request with a token signed by an arbitrary/wrong secret.
- Expected:
401 INVALID_INTEGRATION_CREDENTIAL; noCustomerReferencecreated; anAuditLogrow recordsintegration.auth.failurewithreason: invalid_credential. - Repeat with a
productIdthat has never been registered at all. - Expected: the exact same
401 INVALID_INTEGRATION_CREDENTIALresponse — 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)
- Set the seeded
ProductIntegration.status(orProduct.status) to suspended. - Send a request with an otherwise-valid token.
- Expected:
403 PRODUCT_INTEGRATION_SUSPENDED— distinguishable from Scenario 2's401.
Scenario 4 — unknown field rejects the whole request (User Story 1)
- Send an otherwise-valid request body with one extra, undefined field.
- 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)
- Rotate the seeded integration's credential (admin endpoint) — note both old and new secrets.
- Immediately send one request signed with the OLD secret and one with the NEW secret.
- Expected: both succeed (SC-003).
- Wait past the transition window (or adjust it down for the test), then retry with the OLD secret.
- Expected:
401 INVALID_INTEGRATION_CREDENTIAL— old credential now rejected.
Scenario 6 — revocation takes effect immediately (User Story 2)
- Revoke the seeded integration's credential (admin endpoint).
- Immediately send a request signed with that credential.
- Expected:
401 INVALID_INTEGRATION_CREDENTIALon the very next request (SC-002); anAuditLogrow records the revocation itself as an admin action.
Scenario 7 — audit trail is retrievable (User Story 2)
- After Scenarios 1-6 above, call the admin "get audit trail" endpoint for the seeded integration.
- 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)
- Seed two separate product integrations, A and B.
- Send requests from integration A past its configured
rateLimitPerMinute. - Expected: later requests from A in the burst receive
429 RATE_LIMIT_EXCEEDED; concurrent requests from integration B continue succeeding normally. - Within integration A, send requests as two different
userIds, one pastrateLimitPerUserPerMinuteand one under it. - 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.