test(csp): cover the csp-report collector's route logic + bound its invocation - #2709
Merged
Conversation
…ract The collector's subtlest logic lived inline in the route handler and had no tests: cap-before-de-duplicate ordering, intra-batch de-duplication, oldest- group eviction, the 1% duplicate sampling, content-type gating and the always-204 contract. Extract the per-batch selection into a pure selectReportsToForward() taking shouldForward as a parameter, so the loop's invariants are testable without moving the route's module-level seenGroups into a utils file. The route keeps that state and its sampling. Pins in particular that a group the per-request cap turned away is never recorded as seen — getting that backwards silently loses a group's first sighting and samples every later repeat away at 1%. Refs #2543
…F rule The endpoint is unauthenticated, so one POST can drive up to 20 outbound fetches on an invocation whose ceiling is a project default measured in minutes. maxDuration = 10 bounds that; the handler is one JSON parse plus those fetches, each already capped at 3s. The actual rate limit belongs in the Vercel WAF, which runs before the function and holds across instances — an in-memory limiter on a force-dynamic route resets on every cold start and misses any distributed source. That is a dashboard change, so the rule is recorded next to the code that needs it. Refs #2543
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Contributor
Code-analysis diffPainscore total: 7159.84 → 7159.4 (-0.44) 🆕 New findings (3)
✅ Resolved (4)
📈 Painscore deltas (top movers)
|
Contributor
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2543 — the two follow-ups deliberately left out of #2519.
1. Route-level test coverage
src/app/api/csp-report/route.tsheld the subtlest logic in the feature and had no tests. This adds 37, and extracts the part worth isolating.Extraction, as the issue described it. The per-batch selection loop moves out of the handler into a pure
selectReportsToForward(reports, shouldForward)incsp-report.utils.ts, taking the predicate as a parameter.seenGroupsand the 1% sampling stay in the route — no module-level mutable state moves into a utils file. The handler is now two lines where the loop was.12 unit tests for the loop:
MAX_FORWARDS_PER_REQUESTdistinct groups25 route tests for what only the route holds:
User-Agent/X-Forwarded-Forpass-through (and that empty ones are omitted, not forwarded blank)SEEN_GROUPS_MAX— asserting one group is dropped rather than all 500, which aclear()would otherwise pass unnoticedseenGroupsoutlives a request, so each test takes a fresh module viajest.isolateModules+require(the pattern fromauth-token.test.ts), withMath.randompinned so "forwarded" always means "first sighting".Mutation-tested rather than trusted green: moving the cap after
shouldForwardfails 5 tests, dropping the intra-batch de-duplication fails 1, replacing oldest-eviction withclear()fails 1.2. Rate limit
The control is a Vercel WAF rate-limit rule on
/api/csp-report— it runs before the function and holds across instances, where an in-memory limiter on aforce-dynamicroute resets every cold start and misses a distributed source entirely. That is a dashboard change, not a code one, so this PR records the rule (path, ~100 req/60s per IP, action deny) next to the code that needs it and does the code half:export const maxDuration = 10. The handler is one JSON parse plus at most 20 parallel fetches already capped at 3s each, so it can never legitimately need more.One correction to the issue's premise:
vercel.json'smaxDuration: 300does not currently apply to this route. Its glob isapp/api/**while this project's routes live undersrc/app/api/, which Vercel does not match — already documented atSERVER_FETCH_TIMEOUT_MSinsentry.utils.ts. The real ceiling is the unverifiable project default, which is the better reason to declare one explicitly.Verification
src/contentbeing uninitialized in a fresh worktree, unrelated)prettier --checkclean,eslintclean on all four files,tsc --noEmitadds no errorsNo behaviour change to what gets forwarded — the selection logic is moved verbatim.