Skip to content

test(frontend/auth): cover cross-tab logout sync (closes #493) - #535

Merged
cristim merged 4 commits into
feat/multicloud-web-frontendfrom
test/issue-493-crosstab-logout-v2
Jun 9, 2026
Merged

test(frontend/auth): cover cross-tab logout sync (closes #493)#535
cristim merged 4 commits into
feat/multicloud-web-frontendfrom
test/issue-493-crosstab-logout-v2

Conversation

@cristim

@cristim cristim commented May 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds frontend/src/__tests__/crosstab-session.test.ts which fires a StorageEvent against the token key and asserts the second-tab listener clears in-memory auth and triggers window.location.reload().
  • Patches window.location.reload via Object.defineProperty rather than direct assignment, matching jsdom's constraint that location is non-configurable (CodeRabbit nit addressed in second commit).

This test is the regression guard for installStorageListener() added in PR #487, which is the compensating control for widening the XSS exposure window from tab-close to explicit-logout when tokens moved off sessionStorage.

Closes #493.

Test plan

  • npx jest --testPathPattern="crosstab" passes (1 suite, 4 assertions).
  • Pre-existing settings-purchasing-grace failure is in origin/feat/multicloud-web-frontend and is unrelated to this PR.

Summary by CodeRabbit

  • Tests
    • Added cross-tab logout synchronization tests: verify clearing authentication tokens in another tab logs out the app and triggers a single reload.
    • Ensure the sync ignores unrelated storage keys and partial updates (token-rotation/refresh events).
    • Confirm the storage-change listener is registered only once across repeated initialization calls (idempotent behavior).

cristim added 2 commits May 20, 2026 02:01
PR #487 added installStorageListener() in api/client.ts as the
compensating control for moving tokens off sessionStorage. The
behaviour is implemented but had no automated regression test; if the
listener's key filter, newValue check, or idempotency guard ever
regresses, the SECURITY claim in client.ts becomes silently false.

Add crosstab-session.test.ts covering:
- cleared auth keys (authToken, apiKey, csrfToken) zero in-memory state
  and call window.location.reload
- non-auth keys are ignored
- partial updates (newValue !== null, e.g. token refresh) are ignored
- initAuth() is idempotent: storage listener installs exactly once

Each test loads api/client in jest.isolateModules() so the
storageListenerInstalled guard is fresh per test.
Capture the original window.location reference before tests and restore
it in an afterEach, so each test starts from a clean Location object.
This implements CR's teardown recommendation while keeping the
Object.defineProperty(window, 'location', ...) approach required by jsdom
(window.location.reload is non-configurable and cannot be patched directly).
@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy effort/xs Trivial / one-liner type/chore Maintenance / non-user-visible labels May 20, 2026
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@cristim, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 7 minutes. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 488f1db2-64da-4b3a-99d2-54cec9e54c8b

📥 Commits

Reviewing files that changed from the base of the PR and between b1ea4b1 and 7b19b4e.

📒 Files selected for processing (1)
  • frontend/src/__tests__/crosstab-session.test.ts
📝 Walkthrough

Walkthrough

Adds a Jest test file that captures the storage event listener installed by api.initAuth(), simulates StorageEvent notifications for auth key changes, and asserts in-memory auth invalidation, single reload, ignore conditions, and listener idempotency.

Changes

Cross-tab logout sync test coverage

Layer / File(s) Summary
Test infrastructure and helpers
frontend/src/__tests__/crosstab-session.test.ts
Setup/teardown restores window.location and clears localStorage; loadAuth() spies on window.addEventListener to capture the storage handler installed by api.initAuth(), stubs window.location.reload, and fails if no listener was found; includes an evt factory for jsdom-compatible StorageEvent objects.
Cross-tab logout sync test suite
frontend/src/__tests__/crosstab-session.test.ts
Parameterized tests ensure clearing authToken, apiKey, or csrfToken via StorageEvent with newValue: null makes api.isAuthenticated() false and calls reload() exactly once; additional tests assert unrelated keys are ignored, partial updates (newValue !== null) are ignored, and initAuth() installs the storage listener only once (idempotency).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • LeanerCloud/CUDly#495: Adds/extends the same Jest suite validating cross-tab logout synchronization with mocked storage events and reload assertions.
  • LeanerCloud/CUDly#487: Introduced installStorageListener() and the related cross-tab logout behavior in frontend/src/api/client.ts, which these tests exercise.

Suggested labels

severity/low, urgency/this-quarter, impact/internal

Poem

🐰 I spy the handler tucked in test-time light,
I mock a reload and fire storage at night,
When auth keys vanish, other tabs awake —
One reload, one truth, no sleepy mistake.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding test coverage for cross-tab logout synchronization, and references the related issue #493.
Linked Issues check ✅ Passed All coding requirements from issue #493 are met: logout propagation for all auth keys (authToken, apiKey, csrfToken), non-auth key ignoring, partial update ignoring, and idempotent listener installation with proper mocking.
Out of Scope Changes check ✅ Passed The pull request contains only test file additions directly aligned with issue #493 objectives; no unrelated or out-of-scope changes are present.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/issue-493-crosstab-logout-v2

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

@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@cristim

cristim commented May 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@cristim

cristim commented May 30, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Rate Limit Exceeded

@cristim have exceeded the limit for the number of chat messages per hour. Please wait 54 minutes and 11 seconds before sending another message.

@cristim

cristim commented Jun 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/src/__tests__/crosstab-session.test.ts (1)

13-27: ⚡ Quick win

Consider adding localStorage cleanup for test isolation.

Each test uses jest.isolateModules to get fresh module state, which is excellent. However, localStorage is global and persists across tests. Since api.initAuth() reads from localStorage (per the relevant snippet), stale values from previous tests could potentially be loaded.

While the tests currently pass because they explicitly set auth state before assertions, adding explicit cleanup in afterEach would strengthen test isolation:

afterEach(() => {
  Object.defineProperty(window, 'location', { configurable: true, value: originalLocation });
  localStorage.clear();
});
🤖 Prompt for 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.

In `@frontend/src/__tests__/crosstab-session.test.ts` around lines 13 - 27, Add
explicit test cleanup to ensure isolation by clearing localStorage and restoring
any mocked window.location after each test; specifically, in the test file
containing loadAuth and calls to api.initAuth(), add an afterEach that calls
localStorage.clear() and restores the original window.location (the value saved
before overriding in loadAuth) so api.initAuth() cannot read stale values
between tests and the mocked reload is removed; ensure the restoration
references the same originalLocation used when mocking window.location and also
restores/clears the jest spy addSpy if needed.
🤖 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.

Nitpick comments:
In `@frontend/src/__tests__/crosstab-session.test.ts`:
- Around line 13-27: Add explicit test cleanup to ensure isolation by clearing
localStorage and restoring any mocked window.location after each test;
specifically, in the test file containing loadAuth and calls to api.initAuth(),
add an afterEach that calls localStorage.clear() and restores the original
window.location (the value saved before overriding in loadAuth) so
api.initAuth() cannot read stale values between tests and the mocked reload is
removed; ensure the restoration references the same originalLocation used when
mocking window.location and also restores/clears the jest spy addSpy if needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 18c2ff47-b1b4-4864-b62a-fedb0a99a0ee

📥 Commits

Reviewing files that changed from the base of the PR and between b1ea4b1 and 8d42715.

📒 Files selected for processing (1)
  • frontend/src/__tests__/crosstab-session.test.ts

…s (CR nit)

initAuth() seeds in-memory auth from localStorage, so without clearing it
between tests a stale token from one case could leak into the next. Extend
the existing afterEach to call localStorage.clear() for proper isolation.
@cristim

cristim commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

Addressed the test-isolation nitpick: the existing afterEach now also calls localStorage.clear(), since initAuth() seeds in-memory auth from localStorage and a stale token from one case could otherwise leak into the next. crosstab-session tests (6) pass, tsc and build are clean.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

@cristim Great fix — clearing localStorage in afterEach is exactly the right call. Since initAuth() reads from localStorage to seed in-memory auth state, leaving stale entries between tests would make the isolation incomplete (the window.addEventListener mock alone wasn't enough). The 6-test pass + clean tsc/build confirms the teardown is working as intended.

Triggering a full review now.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
frontend/src/__tests__/crosstab-session.test.ts (2)

16-30: 💤 Low value

Helper correctly captures the storage listener.

The loadAuth() function properly uses jest.isolateModules to obtain a fresh module instance and spies on window.addEventListener to capture the storage handler installed by initAuth().

♻️ Optional: Replace non-null assertion with explicit check

Line 29 uses a non-null assertion (out!). While safe, an explicit check would be clearer:

 function loadAuth(): { api: typeof import('../api'); handler: (e: StorageEvent) => void; reload: jest.Mock; addSpy: jest.SpyInstance } {
   let out: ReturnType<typeof loadAuth> | undefined;
   jest.isolateModules(() => {
     const addSpy = jest.spyOn(window, 'addEventListener');
     const reload = jest.fn();
     Object.defineProperty(window, 'location', { configurable: true, value: { ...window.location, reload } });
     // eslint-disable-next-line `@typescript-eslint/no-require-imports`
     const api = require('../api') as typeof import('../api');
     api.initAuth();
     const call = addSpy.mock.calls.find(c => c[0] === 'storage');
     if (!call) throw new Error('initAuth() did not install a storage listener');
     out = { api, handler: call[1] as (e: StorageEvent) => void, reload, addSpy };
   });
-  return out!;
+  if (!out) throw new Error('isolateModules did not execute');
+  return out;
🤖 Prompt for 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.

In `@frontend/src/__tests__/crosstab-session.test.ts` around lines 16 - 30, The
helper loadAuth currently returns out! using a non-null assertion; replace this
with an explicit check to avoid the assertion. After jest.isolateModules
finishes, verify that out is defined (e.g. if (!out) throw new Error('loadAuth
failed to capture storage listener')) and then return out (without using !).
Update the loadAuth signature or runtime check as needed so callers get a
concrete value or a clear error; reference the loadAuth function, the out local,
and the addSpy/call check to guide where the validation should be added.

46-60: 💤 Low value

Tests correctly verify guard conditions.

Both tests validate that the storage listener properly filters events: non-auth keys and partial updates (newValue !== null) are ignored, matching the implementation in client.ts.

♻️ Optional: Add test case for null key

The storage listener in client.ts also guards against !e.key. Consider adding a test to confirm that evt(null, null) is ignored:

test('ignores events with null key (e.g., localStorage.clear())', () => {
  const { api, handler, reload } = loadAuth();
  api.setAuthToken('t');
  handler(evt(null, null));
  expect(api.isAuthenticated()).toBe(true);
  expect(reload).not.toHaveBeenCalled();
});
🤖 Prompt for 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.

In `@frontend/src/__tests__/crosstab-session.test.ts` around lines 46 - 60, Add a
test that verifies the storage listener ignores events with a null key by
reusing the existing test pattern: call loadAuth() to get { api, handler, reload
}, set the token via api.setAuthToken('t'), invoke the listener with
handler(evt(null, null)), and assert api.isAuthenticated() remains true and
reload was not called; place the test alongside the existing 'ignores non-auth
keys' and 'ignores partial updates' tests and name it e.g. 'ignores events with
null key (e.g., localStorage.clear())' so it covers the !e.key guard in
client.ts.
🤖 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.

Nitpick comments:
In `@frontend/src/__tests__/crosstab-session.test.ts`:
- Around line 16-30: The helper loadAuth currently returns out! using a non-null
assertion; replace this with an explicit check to avoid the assertion. After
jest.isolateModules finishes, verify that out is defined (e.g. if (!out) throw
new Error('loadAuth failed to capture storage listener')) and then return out
(without using !). Update the loadAuth signature or runtime check as needed so
callers get a concrete value or a clear error; reference the loadAuth function,
the out local, and the addSpy/call check to guide where the validation should be
added.
- Around line 46-60: Add a test that verifies the storage listener ignores
events with a null key by reusing the existing test pattern: call loadAuth() to
get { api, handler, reload }, set the token via api.setAuthToken('t'), invoke
the listener with handler(evt(null, null)), and assert api.isAuthenticated()
remains true and reload was not called; place the test alongside the existing
'ignores non-auth keys' and 'ignores partial updates' tests and name it e.g.
'ignores events with null key (e.g., localStorage.clear())' so it covers the
!e.key guard in client.ts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 153855e7-97ba-4f2d-bcdc-230bbc20496a

📥 Commits

Reviewing files that changed from the base of the PR and between b1ea4b1 and 8446e77.

📒 Files selected for processing (1)
  • frontend/src/__tests__/crosstab-session.test.ts

- replace the out! non-null assertion in loadAuth() with an explicit
  guard that throws a clear error if isolateModules never ran
- add a regression case for the !e.key guard in installStorageListener:
  a null-key storage event (localStorage.clear()) must not clear
  in-memory auth or reload
@cristim

cristim commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

Addressed both nitpicks from the latest review:

  • Replaced the out! non-null assertion in loadAuth() with an explicit if (!out) throw guard that surfaces a clear error if isolateModules never executed.
  • Added a regression test for the !e.key guard in installStorageListener(): a null-key storage event (as fired by localStorage.clear()) must not clear in-memory auth or trigger a reload.

npm run build and npm test (TZ=UTC) are green: crosstab-session now 7 tests, full suite passes (one unrelated, pre-existing failure in settings-purchasing-grace.test.ts predates this PR and is out of scope).

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

@cristim Both fixes look solid:

  • The if (!out) throw guard in loadAuth() is a strict improvement over out! — it surfaces a meaningful error at the point of failure rather than an opaque TypeError deeper in the call stack if isolateModules silently skipped execution.
  • The null-key regression test closes an important gap: localStorage.clear() dispatches StorageEvent with key === null in real browsers, and verifying the listener ignores it (no auth clear, no reload) ensures the guard in installStorageListener() is actually exercised by the test suite.

7 passing tests + clean build looks good. Triggering a full review now.

✅ Action performed

Full review finished.

@cristim
cristim merged commit eeaa1a6 into feat/multicloud-web-frontend Jun 9, 2026
5 checks passed
@cristim
cristim deleted the test/issue-493-crosstab-logout-v2 branch July 27, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/xs Trivial / one-liner priority/p2 Backlog-worthy triaged Item has been triaged type/chore Maintenance / non-user-visible

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant