Thanks to visit codestin.com
Credit goes to github.com

Skip to content

test: property-check the auth rate limiter against a sliding-window oracle - #593

Merged
ndycode merged 3 commits into
mainfrom
claude/audit-74-auth-rate-limit-property
Jun 19, 2026
Merged

ndycode merged 3 commits into
mainfrom
claude/audit-74-auth-rate-limit-property

Conversation

@ndycode

@ndycode ndycode commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a fast-check property suite for lib/auth-rate-limit.ts — the sliding-window limiter that gates OAuth attempts per account. The natural companion to test: property-check the circuit breaker's availability contract #592's circuit-breaker suite: this module is the L3 convention's canonical module-state example, and its oracle (timestamps-in-window) is trivial enough for a clean model-based test.

What Changed

New test/property/auth-rate-limit.property.test.ts (3 properties, real module under vi.useFakeTimers() with a 3-attempt / 1-second config so sequences cross the expiry boundary):

  1. Model-based equivalence — for any interleaving of record/reset/advance events across three accounts, with every call going through a decorated id spelling (uppercase, leading/trailing whitespace, tabs), getAttemptsRemaining and canAttemptAuth match a trivial timestamps-in-window oracle keyed by canonical id. One property pins both the sliding window arithmetic and the trim+lowercase bucket mapping.
  2. Gate fidelitycheckAuthRateLimit throws AuthRateLimitError exactly when canAttemptAuth is false, and the error's payload is live: the canonical accountId, attemptsRemaining === 0, and resetAfterMs agreeing with getTimeUntilReset (and strictly positive).
  3. No wedged bucketsrecordAuthAttempt doesn't cap at maxAttempts, so a burst can stack more timestamps than the limit; the property proves any burst (up to 4× the limit, arbitrary intra-burst gaps) still unblocks completely after one quiet window, with getTimeUntilReset bounded by windowMs throughout.

Validation

  • npm test -- test/property/auth-rate-limit.property.test.ts test/auth-rate-limit.test.ts — 25/25 (new 3 + existing 22 untouched)
  • npm run typecheck (also via pre-commit hook)
  • npx eslint test/property/auth-rate-limit.property.test.ts --max-warnings=0
  • Module-state hygiene: config restored to the documented defaults and buckets cleared in afterEach; ids namespaced per fc iteration so no run can observe another's buckets

Docs and Governance Checklist

  • Test-only; no behavior or docs surface changed

Risk and Rollback

  • Risk level: minimal — additive test file; conventions match the existing test/property/ suites (explicit vitest imports, plain fc.assert, fake timers scoped with try/finally).
  • Rollback plan: revert the single commit.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB


Generated by Claude Code

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

adds test/property/auth-rate-limit.property.test.ts, a three-property fast-check suite for the sliding-window oauth rate limiter, covering model-based oracle equivalence, gate-throw fidelity, and burst-then-expiry unblocking.

  • property 1 runs record/reset/advance interleavings across three accounts with five decorated id spellings and checks getAttemptsRemaining/canAttemptAuth against a timestamp-filter oracle after every step.
  • property 2 asserts checkAuthRateLimit throws AuthRateLimitError exactly when canAttemptAuth is false and that the error fields match live module state — but only exercises a pre-canonical id, leaving gateError.accountId unverified for decorated inputs.
  • property 3 proves any over-burst (up to 4× MAX_ATTEMPTS) unblocks completely after one quiet window, with getTimeUntilReset bounded throughout.

Confidence Score: 4/5

safe to merge as-is, but property 2 contains a coverage gap that lets a real module behaviour (raw accountId echoed in AuthRateLimitError) go undetected

the suite is additive and the first and third properties are logically sound. the second property's gateError.accountId assertion uses a pre-canonical id, so it cannot catch the module storing the raw caller-supplied string in the error. this is a live gap between what the PR description claims is tested and what is actually exercised — downstream consumers of AuthRateLimitError.accountId who expect the canonical form could receive a decorated string in production

test/property/auth-rate-limit.property.test.ts — property 2 needs a decorated-spelling path through checkAuthRateLimit to validate gateError.accountId

Important Files Changed

Filename Overview
test/property/auth-rate-limit.property.test.ts adds three fast-check property tests for the sliding-window rate limiter; property 2 has a coverage gap where checkAuthRateLimit is never called with a decorated spelling, so the error's raw accountId field is untested against the canonical form

Fix All in Codex

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
test/property/auth-rate-limit.property.test.ts:145
**`gateError.accountId` assertion is vacuously canonical**

`id` is already lowercase and trimmed (`gate-${runCounter++}@example.com`), so `expect(gateError.accountId).toBe(id)` passes regardless of whether the module canonicalizes the id or echoes the raw input. `checkAuthRateLimit` stores the raw caller-supplied string directly in the error (`throw new AuthRateLimitError(accountId, ...)`) — calling it with a decorated spelling like `" [email protected] "` would produce `gateError.accountId === " [email protected] "`, not the canonical `"[email protected]"`. Property 1 applies `arbDecoration` to every module call except `checkAuthRateLimit`, leaving this path uncovered and the PR description's claim of "the canonical accountId" unverified.

### Issue 2 of 2
test/property/auth-rate-limit.property.test.ts:178-184
**`recordAuthAttempt` is misindented inside the burst loop**

`recordAuthAttempt(id)` sits at the same tab depth as the `for` statement itself rather than one level deeper, making it look like it lives outside the loop. The closing `}` is also at the for-statement level, so the braces confirm it is inside — but a reader scanning the indentation could easily conclude the record call runs only once (after the loop), which would invalidate the burst-count premise of the property. since this is a rate-limit test where the recorded-count is the invariant under test, the misleading indentation is worth fixing for future maintainers.

Reviews (3): Last reviewed commit: "Merge branch 'main' into claude/audit-74..." | Re-trigger Greptile

…racle

Three fast-check properties over the real module under fake timers
(maxAttempts 3 / 1s window so sequences cross the expiry boundary):

- model-based: for any record/reset/advance interleaving across three
  accounts with decorated id spellings (case/whitespace variants),
  getAttemptsRemaining and canAttemptAuth match a trivial
  timestamps-in-window oracle keyed by canonical id - pinning both the
  sliding window and the trim+lowercase bucket mapping at once
- checkAuthRateLimit throws AuthRateLimitError exactly when blocked,
  carrying the canonical accountId, zero attemptsRemaining, and a
  resetAfterMs that agrees with the live getTimeUntilReset
- over-recording past maxAttempts can never wedge a bucket: any burst
  unblocks fully after one quiet window, with getTimeUntilReset
  bounded by windowMs throughout

Config and registry are module state, so each property restores the
documented defaults and clears buckets, and ids are namespaced per
iteration. Companion to #574/#575/#579/#592; same conventions.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

This pull request introduces a comprehensive property-based test suite for the auth rate limiter module using Vitest and fast-check. The addition is test-only (no production code changes) and strengthens security validation by covering edge cases across interleaved rate-limit operations, decorated account ID variants, and boundary conditions. All existing tests remain unchanged, ensuring regression coverage is maintained.

The suite validates three core properties: (1) a sliding-window oracle that tracks attempt timestamps and verifies both getAttemptsRemaining and canAttemptAuth across arbitrary record/reset/time-advance sequences with canonicalized account IDs, (2) gate fidelity ensuring checkAuthRateLimit throws AuthRateLimitError precisely when blocked with correct error payloads, and (3) no wedged buckets confirming that bursts exceeding the limit fully unblock after a quiet window. Tests use a 3-attempt/1-second window configuration with fake timers, proper state isolation between iterations, and exhaustive event narrowing to guard against silent modeling errors. The commit sequence demonstrates iterative refinement addressing code review feedback on timer hygiene and maintainability (replacing hardcoded values with derived constants).

Walkthrough

adds a single property-based test file at test/property/auth-rate-limit.property.test.ts with three fast-check properties: a sliding-window oracle check, checkAuthRateLimit throw-semantics validation, and a quiet-window unblocking assertion. uses fake timers and per-run unique account ids to isolate state across runs.

Changes

Auth Rate Limit Property Tests

Layer / File(s) Summary
Test harness setup
test/property/auth-rate-limit.property.test.ts:1-63
defines WINDOW_MS, MAX_ATTEMPTS, per-run account id counter, and beforeEach/afterEach hooks that call configureAuthRateLimit, resetAllAuthRateLimits, and toggle fake/real timers.
Sliding-window oracle property
test/property/auth-rate-limit.property.test.ts:65-111
generates random record/reset/advance steps and whitespace/case-variant account id decorations; maintains an oracle tracking raw timestamps per account and asserts getAttemptsRemaining and canAttemptAuth match oracle after every step.
checkAuthRateLimit throw-semantics property
test/property/auth-rate-limit.property.test.ts:113-156
for the same randomized sequences, asserts checkAuthRateLimit throws AuthRateLimitError with correct accountId, attemptsRemaining (=0), and resetAfterMs == getTimeUntilReset when blocked, and does not throw when allowed.
Unblocking-after-quiet-window property
test/property/auth-rate-limit.property.test.ts:158-200
generates over-burst attempt counts and gap durations; verifies account stays blocked immediately after exceeding maxAttempts, then confirms unblocking and reset of getAttemptsRemaining to MAX_ATTEMPTS and getTimeUntilReset to 0 after advancing WINDOW_MS + 1.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


a few things to flag explicitly:

missing edge cases at test/property/auth-rate-limit.property.test.ts:

  • test/...:65-111 — the oracle only tracks three canonical account ids. there's no property covering concurrent interleaving of multiple distinct accounts advancing time simultaneously. if the underlying implementation (lib/auth-rate-limit) holds a single shared timestamp store, you're not testing cross-account isolation under concurrent access patterns.

  • test/...:1-63WINDOW_MS and MAX_ATTEMPTS are set to small test values but there's no property that exercises the boundary at exactly maxAttempts (i.e., the nth attempt that is still allowed vs. the n+1th that blocks). the oracle path at line 65–111 could miss an off-by-one in the production sliding window if the generated sequences never land on that exact boundary count.

  • test/...:158-200 — the unblocking property only advances by exactly WINDOW_MS + 1. there's no case for advancing by less than WINDOW_MS with some but not all attempts having expired. that's the core sliding-window behavior; without it, a fixed-window implementation could pass these tests incorrectly.

  • test/...:113-156resetAfterMs is asserted equal to getTimeUntilReset at the same instant, but there's no test for what happens when time is advanced between the throw and reading getTimeUntilReset. minor, but if those calls aren't atomic, this assertion could flake under real timers.

no regression tests flagged for windows-style environments: if lib/auth-rate-limit uses Date.now() internally, fake timer behavior across platforms should be verified — test/...:1-63 uses vi.useFakeTimers() but doesn't explicitly set an initial clock value, which could cause the oracle timestamps to start at 0 vs. the real epoch in some environments.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning title exceeds 72 character limit at 74 chars; conventional commit format and type are correct but summary length violates requirements. shorten summary to ≤72 chars, e.g. 'test: property-check auth rate limiter against sliding-window oracle' (67 chars).
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed description comprehensively covers all required template sections: summary, what changed, validation checklist (all marked complete), governance checklist, and risk/rollback details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/audit-74-auth-rate-limit-property
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/audit-74-auth-rate-limit-property

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

test/property/auth-rate-limit.property.test.ts

Oops! Something went wrong! :(

ESLint: 10.0.0

Error: The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it.
at /node_modules/eslint/lib/config/config-loader.js:145:10
at async loadTypeScriptConfigFileWithJiti (/node_modules/eslint/lib/config/config-loader.js:144:3)
at async loadConfigFile (/node_modules/eslint/lib/config/config-loader.js:265:11)
at async ConfigLoader.calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:588:23)
at async #calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:369:19)
at async Promise.all (index 0)
at async findFiles (/node_modules/eslint/lib/eslint/eslint-helpers.js:635:25)
at async ESLint.lintFiles (/node_modules/eslint/lib/eslint/eslint.js:1014:21)
at async Object.execute (/node_modules/eslint/lib/cli.js:386:14)
at async main (/node_modules/eslint/bin/eslint.js:175:19)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread test/property/auth-rate-limit.property.test.ts
Comment thread test/property/auth-rate-limit.property.test.ts Outdated
Greptile flagged the bare else branches (a future event kind would
silently model as a reset) and the literal 11 coupled to
MAX_ATTEMPTS * 4 - 1.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
@ndycode
ndycode merged commit 54a0d94 into main Jun 19, 2026
1 of 2 checks passed
@ndycode
ndycode deleted the claude/audit-74-auth-rate-limit-property branch June 19, 2026 06:04
ndycode added a commit that referenced this pull request Jun 19, 2026
…contracts (#594)

* test: property-check SessionAffinityStore TTL, eviction, and reindex contracts

Five fast-check properties over the real store using its injectable
now parameters (1s TTL floor so sequences cross expiry often):

- model-based TTL/upsert equivalence: for any remember/update/forget/
  advance interleaving through whitespace-decorated key spellings,
  getPreferredAccountIndex and getLastResponseId match a trivial
  TTL map (remember preserves the continuation id, response-id writes
  refresh expiry and never create entries)
- capacity: size() never exceeds maxEntries, and LRU eviction can
  never evict the entry just written
- write-version conflicts: a stale version loses to a live entry on
  both the index and response-id channels, but may rebind once the
  entry expires
- forgetAccount + reindexAfterRemoval mirror an account-array splice,
  with both return counts pinned against the model
- prune removes exactly the expired entries; lazily-reaped sessions
  (touched while expired) correctly do not count as prunable

The prune model initially missed that updateLastResponseId deletes an
expired entry outright; fast-check found the 8-event counterexample
and the model now mirrors the lazy reap.

Companion to #574/#575/#579/#592/#593; same conventions.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

* test: fix remember model after lazy reap; cover clearAll; decorate prune keys

Greptile P1: the model carried a responseId across expiry, but the
assertion block's reads lazily reap expired entries from the store, so
a remember after expiry finds no existing entry and the id is gone -
the model now inherits the id only from a live entry (verified at
FAST_CHECK_NUM_RUNS=1000, where the original 4-event counterexample
sequence reproduces without the fix).

P2s: a sixth property pins clearAll (#474 invalidation) - size drops
to zero, every read goes null, and the store stays usable - and the
prune property now routes remember/forget/updateLastResponseId
through decorated key spellings like the model property does.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

---------

Co-authored-by: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants