test(auth-server): clean up test mocking leaked state#20657
Conversation
| @@ -236,6 +236,16 @@ No shared mutable state between tests. Each test owns its setup; use `beforeEach | |||
|
|
|||
| **Mid-test mock resets are a smell.** Calling `mockClear()`, `mockReset()`, `jest.clearAllMocks()`, or `jest.resetAllMocks()` inside an `it()` body is almost always a band-aid hiding state leakage from a prior test or from shared setup — fix the leak (scope the mock per-test, move it into `beforeEach`, stop sharing module-level mocks across describes) instead of clearing inline. The narrow legitimate case is asserting "no further calls happened after this point" in a multi-phase test; in that case, leave a comment explaining why. | |||
|
|
|||
| **TypeDI Container cleanup is conditional**, not mechanical. Pair `Container.set()` with cleanup *when leakage could change another test's observable behavior*: | |||
There was a problem hiding this comment.
While digging through these changes, Claude had flagged that the existing rules were too strict so it suggested expanding this. the short version is, calling Container.reset() or .remove() is more nuanced than just "always do it". Namely for our integration tests where we setup the container before integration server starts. If the server is a long running process then calling reset between tests doesn't do anything
There was a problem hiding this comment.
This was replaced by a ts version
| } | ||
| const { server, close } = await createServer(testConfig); | ||
| return wrapServer(server, close); | ||
| const wrappedClose = async () => { |
There was a problem hiding this comment.
Related to the changes to the testing GUIDELINES, if test stat sets the CapabilityService when starting up, we want to explicitly remove it when we're done.
|
|
||
| afterAll(async () => { | ||
| await server.stop(); | ||
| Container.remove(PlaySubscriptions); |
There was a problem hiding this comment.
This is more or less the largest change in the branch. We are using this surgical approach as a work around for the fact that container is a global process and we use the shared mocks. At some point it would be ideal to move toward always using Container.reset() which clears everything between tests but that is blocked by the mocks refactor. The long term correct change would be using scoped containers, but that's a significant refactor of production code to support that.
0a6932c to
76db480
Compare
There was a problem hiding this comment.
Pull request overview
This PR hardens fxa-auth-server’s Jest test suite against cross-test/module state leakage by standardizing module resets, pairing TypeDI Container.set() with teardown cleanup, and removing redundant mock resets (given clearMocks: true).
Changes:
- Replaces targeted
delete require.cache[...]patterns withjest.resetModules()for safer Jest-aware module reloading. - Adds/adjusts TypeDI
Container.remove(...)cleanup in integration tests and test server helpers to avoid leaked DI bindings. - Removes redundant
jest.clearAllMocks()calls, deletes legacytest/lib/server.js, and updates testing guidelines/docs to document the new patterns.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/fxa-auth-server/test/support/helpers/test-utils.ts | Uses jest.resetModules() before re-reading config for metrics context generation. |
| packages/fxa-auth-server/test/support/helpers/test-server.ts | Uses jest.resetModules() before reading base config for spawned/shared server helpers. |
| packages/fxa-auth-server/test/scripts/stripe-products-and-plans-converter.in.spec.ts | Removes redundant jest.clearAllMocks() given package clearMocks: true. |
| packages/fxa-auth-server/test/remote/totp.in.spec.ts | Adds TypeDI token cleanup in afterAll for IAP subscription tokens. |
| packages/fxa-auth-server/test/remote/subscription_tests.in.spec.ts | Restructures module loading around resetModules() to preserve token identity; adds explicit Container cleanup. |
| packages/fxa-auth-server/test/remote/payments/configuration/manager.in.spec.ts | Removes redundant jest.clearAllMocks() and relies on Container reset/clearMocks. |
| packages/fxa-auth-server/test/remote/passkeys.in.spec.ts | Removes redundant per-test jest.clearAllMocks() hook. |
| packages/fxa-auth-server/test/remote/oauth_token_route.in.spec.ts | Captures Container once and adds cleanup for a string token after tests. |
| packages/fxa-auth-server/test/remote/mfa_totp.in.spec.ts | Adds TypeDI token cleanup in afterAll for IAP subscription tokens. |
| packages/fxa-auth-server/test/lib/server.ts | Adds conditional TypeDI cleanup on server shutdown to avoid leaking CapabilityService. |
| packages/fxa-auth-server/test/lib/server.js | Removes the legacy JS helper (replaced by TS variant). |
| packages/fxa-auth-server/jest.oauth-api.config.js | Updates comment to reference test/lib/server.ts. |
| CLAUDE.md | Documents conditional TypeDI cleanup guidance and resetModules() Container identity gotchas. |
| .claude/skills/fxa-testing-shared/GUIDELINES.md | Expands Rule 7 with concrete guidance on when/how to clean up TypeDI tokens. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const wrappedClose = async () => { | ||
| await close(); | ||
| if (didSetCapabilityService) { | ||
| Container.remove(CapabilityService); | ||
| } | ||
| }; |
| afterAll(async () => { | ||
| if (server) { | ||
| await server.close(); | ||
| } | ||
| if (profileServer) { | ||
| await profileServer.close(); | ||
| } | ||
| Container.remove(AppConfig); | ||
| Container.remove(AuthLogger); | ||
| Container.remove(StripeHelper); | ||
| Container.remove(PlaySubscriptions); | ||
| Container.remove(AppStoreSubscriptions); | ||
| Container.remove(ProfileClient); | ||
| Container.remove(CapabilityManager); | ||
| Container.remove(BackupCodeManager); | ||
| Container.remove(RecoveryPhoneService); | ||
| Container.remove(PriceManager); | ||
| Container.remove(ProductConfigurationManager); | ||
| Container.remove(CapabilityService); | ||
| }); |
There was a problem hiding this comment.
I mean, valid, but also if server.close() fails we have a bigger problem
| afterAll(async () => { | ||
| await server.stop(); | ||
| Container.remove(PlaySubscriptions); | ||
| Container.remove(AppStoreSubscriptions); | ||
| }); |
| afterAll(async () => { | ||
| await server.stop(); | ||
| Container.remove(PlaySubscriptions); | ||
| Container.remove(AppStoreSubscriptions); | ||
| }); |
toufali
left a comment
There was a problem hiding this comment.
LGTM! Thanks for the cleanup and your inline comments were helpful.
Because: - Container.set() without paired cleanup and delete require.cache[...] leak state across tests and bypass Jest's module tracking, making the suite fragile under runner changes. This commit: - Pairs Container.set() in test/ with matching cleanup in the corresponding teardown hook. - Replaces 6 delete require.cache[...] with jest.resetModules(). - Restructures subscription_tests to require Container tokens inside beforeAll after resetModules, fixing latent identity drift between describe-scope captures and key_server's re-required deps. - Removes 3 redundant jest.clearAllMocks() calls (clearMocks: true already runs between tests). - Deletes Mocha-era test/lib/server.js duplicate. - Documents conditional cleanup nuance in .claude/rules/testing/base.md Rule 7. Closes: FXA-13443
76db480 to
9472eea
Compare
|
Latest force push was to fix merge conflicts in claude specific files |
Because:
This commit:
Closes: FXA-13443
Checklist
Put an
xin the boxes that applyHow to review (Optional)
Screenshots (Optional)
Please attach the screenshots of the changes made in case of change in user interface.
Other information (Optional)
Any other information that is important to this pull request.