test: close the async coverage gap (auth caching, lock, error mapping) - #7
Conversation
The async mirror duplicates the sync caching/refresh logic rather than sharing it, so the sync suite passing said nothing about the async one. Coverage showed the difference: _async_auth.py 76% vs auth.py 95%. Adds tests/unit/test_async_auth.py (mirrors test_auth.py): - token cache hit -- repeated get_token() hits the wire once - expiry-driven re-acquisition - refresh margin -- a token expiring inside the 60s margin is already stale - invalidate() drops the cache - concurrent get_token() collapses into a single request via asyncio.Lock - 401 -> AuthenticationError, 500 -> MyInvoisError, transport error wrapped Adds a status-mapping block to tests/unit/test_async_client.py. The async client has its own copy of the response-handling chain, so the equivalent sync assertions did not cover it: 400/401/403/404/429/500 -> the right exception type with status_code set, server error-message extraction, non-JSON error bodies, and 204 -> None. The concurrency test is verified to be able to fail: replacing AsyncTokenManager._lock with a no-op async context manager turns 1 token request into 10, so it genuinely pins the lock rather than passing vacuously. _async_auth.py 76% -> 92%, _async_client.py 84% -> 91%, total 86% -> 87%. 284 -> 302 tests. ruff, ruff format, mypy src/myinvois all clean. Tests only; no source changes. Note for a follow-up PR (deliberately not fixed here): TokenManager.is_valid() is a method but AsyncTokenManager.is_valid is a property, which breaks the one-for-one mirror contract AGENTS.md and the README both claim. Recorded in AGENTS.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAsync authentication coverage was added for token lifecycle, invalidation, refresh concurrency, and endpoint failures. Async client tests now cover HTTP status mapping, error payloads, transport responses, and ChangesAsync coverage expansion
Estimated code review effort: 3 (Moderate) | ~20 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/unit/test_async_auth.py`:
- Around line 72-81: Update test_async_token_manager_acquires_token to capture
the mocked token route and inspect its recorded request after mgr.get_token().
Assert the submitted form payload includes the expected grant_type, client
credentials, and scope values, while preserving the existing token response and
manager-state assertions.
- Around line 113-124: Update test_async_token_manager_refresh_margin to mock
two token responses, call mgr.get_token() twice, and verify the second call
returns the second token value. Also assert the respx route was called twice,
directly exercising reacquisition when the token is within the refresh margin.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 278b3103-f9fc-4ad7-8559-04daa296d6e0
📒 Files selected for processing (3)
AGENTS.mdtests/unit/test_async_auth.pytests/unit/test_async_client.py
Both CodeRabbit findings were valid. 1. test_async_token_manager_acquires_token now asserts the outgoing form payload (client_id, client_secret, grant_type, scope). Nothing anywhere asserted the grant body, so a typo in _build_form -- "client_credential" for instance -- would have passed the whole suite and only failed against the real LHDN token endpoint. The sync suite has the same hole. 2. test_async_token_manager_refresh_margin now drives the real path instead of only checking the is_valid flag: two mocked responses, two get_token() calls, asserting the second returns the fresh token and the route was hit twice. This matters because get_token does not consult the is_valid property -- it re-checks is_expired inline, twice (outer check, then again under the lock). A bug in that inline check would have slipped past the flag-only assertion. Verified sensitive: with refresh_margin=0 the second get_token returns the cached STALE token after a single request, failing the test. Also recorded in AGENTS.md, found while checking these against their sync counterparts and left for its own PR: tests/unit/test_auth.py::test_token_manager_refresh_margin is VACUOUS. It never calls get_token(), so is_valid() returns False only because no token was ever acquired. Re-running it with expires_in=3600 -- far outside the 60s margin -- still passes, with 0 calls to the token endpoint. It would pass with the refresh-margin logic deleted entirely. Tests and notes only; no source changes. 302 tests, ruff/format/mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. AGENTS.md overstated its own verification. It said both refresh-margin twins "are verified against refresh_margin=0", which reads as though a test in the suite uses that value. None does -- the committed tests use the default 60s margin, and refresh_margin=0 was a throwaway scratch mutation run that was deleted. Reworded to say what the tests actually do and to flag the mutation check as a manual step to repeat if the refresh-ahead logic changes. Worth fixing carefully: this file spends several sections warning against overstated verification claims, so it should not contain one. 2. The reviewer asked for outbound-request assertions on the refresh-margin test. The underlying gap is real -- the sync suite never asserted the grant payload, a hole noted when the async twin was fixed in #7 -- but the margin test is the wrong home for it. That test was just rewritten to do exactly one thing: prove a token expiring inside the margin is re-acquired. Loading it with request-shape assertions would blur that. Added instead to test_token_manager_acquires_token, mirroring test_async_auth.py::test_async_token_manager_acquires_token exactly, so both suites now pin the grant form in the same place. Verified it catches what it exists for: injecting the typo "client_credentials" -> "client_credential" into _build_form fails this test and ONLY this test, confirming nothing else covered the payload. Tests and docs only; no source changes. 303 tests, ruff/format/mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: make the sync refresh-margin test actually test the margin test_token_manager_refresh_margin never called get_token(), so is_valid was False merely because no token had ever been acquired -- the tok is None branch. The refresh margin was never exercised. It passed with expires_in=3600, far outside the 60s margin, having made 0 calls to the token endpoint, and would have passed with the refresh-ahead logic deleted entirely. auth.py reported 95% line coverage throughout, because coverage counts executed lines rather than meaningful assertions. Now mirrors the async twin fixed in #7: two mocked responses, two get_token() calls, asserting the second returns the fresh token and the route was hit twice. This drives the real path -- get_token does not consult the is_valid property, it re-checks is_expired inline, twice (outer check, then again under the lock). Verified sensitive: with refresh_margin=0 the second get_token returns the cached STALE token and the new assertions fail. The old assertion passed either way. Both managers' refresh-ahead behaviour is now covered symmetrically. Tests only; no source changes. 303 tests, ruff/format/mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: address review — correct the AGENTS.md claim, pin the grant form 1. AGENTS.md overstated its own verification. It said both refresh-margin twins "are verified against refresh_margin=0", which reads as though a test in the suite uses that value. None does -- the committed tests use the default 60s margin, and refresh_margin=0 was a throwaway scratch mutation run that was deleted. Reworded to say what the tests actually do and to flag the mutation check as a manual step to repeat if the refresh-ahead logic changes. Worth fixing carefully: this file spends several sections warning against overstated verification claims, so it should not contain one. 2. The reviewer asked for outbound-request assertions on the refresh-margin test. The underlying gap is real -- the sync suite never asserted the grant payload, a hole noted when the async twin was fixed in #7 -- but the margin test is the wrong home for it. That test was just rewritten to do exactly one thing: prove a token expiring inside the margin is re-acquired. Loading it with request-shape assertions would blur that. Added instead to test_token_manager_acquires_token, mirroring test_async_auth.py::test_async_token_manager_acquires_token exactly, so both suites now pin the grant form in the same place. Verified it catches what it exists for: injecting the typo "client_credentials" -> "client_credential" into _build_form fails this test and ONLY this test, confirming nothing else covered the payload. Tests and docs only; no source changes. 303 tests, ruff/format/mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tests only. No source changes.
Why
The async mirror duplicates the sync caching/refresh logic rather than sharing it (a deliberate non-DRY decision recorded in AGENTS.md), so the sync suite passing said nothing about the async one. Coverage showed the difference:
_async_auth.pyauth.py95%_async_client.pyclient.py88%What
tests/unit/test_async_auth.py(new, mirrorstest_auth.py):get_token()hits the wire onceinvalidate()drops the cacheget_token()collapses into a single request viaasyncio.LockAuthenticationError, 500 →MyInvoisError, transport error wrappedStatus-mapping block in
tests/unit/test_async_client.py— the async client has its own copy of the response-handling chain, so the equivalent sync assertions did not cover it: 400/401/403/404/429/500 → the right exception type withstatus_codeset, server error-message extraction, non-JSON error bodies, and 204 →None.The concurrency test is verified to be able to fail
A test that can't fail is worthless, so I checked this one rather than assuming. Replacing
AsyncTokenManager._lockwith a no-op async context manager turns 1 token request into 10 — it genuinely pins the lock rather than passing vacuously.Result
_async_auth.py76% → 92%,_async_client.py84% → 91%, total 86% → 87%. 284 → 302 tests.ruff check,ruff format --check,mypy src/myinvoisall clean.Found but deliberately not fixed here
TokenManager.is_valid()is a method butAsyncTokenManager.is_validis a property. That breaks the one-for-one mirror contract both AGENTS.md and the README claim — porting sync code to async hitsTypeError: 'bool' object is not callable. It's a public API change, so it belongs in its own PR; recorded in AGENTS.md in the meantime.🤖 Generated with Claude Code
Summary by CodeRabbit
204 No Contentbehavior.