Compare commits

...

3 Commits

Author SHA1 Message Date
Amin Ghadersohi
81742eacc7 test(logout): fix Cache API mock global typing for tsc
The intersected global type made `caches` non-optional, so `delete` failed
type-checking (TS2790). Cast to a fresh optional-typed shape so the mock can be
both assigned and deleted without using `any`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-24 01:30:38 -07:00
Amin Ghadersohi
0be9698141 test(logout): use a typed global and restore the Cache API mock in finally
Address review feedback: replace the `any` casts on `global` with a narrowed
typed reference, and restore the original `caches` value in a `finally` block so
an early assertion failure cannot leak the mock into other tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-24 01:12:50 -07:00
Amin Ghadersohi
67d861ae7b fix(logout): purge the namespaced Cache API store on logout
GET responses with an ETag were persisted into the @SUPERSET-UI/CONNECTION
Cache API namespace and retained across sessions, with no purge on logout. The
logout handler already cleared the persisted redux localStorage and session
storage; extend it to also delete the namespaced Cache API store so cached
responses are not retained on the device after the session ends.

Best-effort (the promise is not awaited since logout navigates away). Extends
the existing RightMenu logout test to assert the cache namespace is purged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-22 17:47:12 -07:00
2 changed files with 36 additions and 9 deletions

View File

@@ -401,17 +401,35 @@ test('Logs out and clears local storage item redux', async () => {
expect(localStorage.getItem('redux')).not.toBeNull();
expect(sessionStorage.getItem('login_attempted')).not.toBeNull();
await userEvent.hover(await screen.findByText(/Settings/i));
// Mock the Cache API so we can assert the namespaced store is purged.
const cacheGlobal = global as unknown as { caches?: CacheStorage };
const priorCaches = cacheGlobal.caches;
const deleteMock = jest.fn().mockResolvedValue(true);
cacheGlobal.caches = { delete: deleteMock } as unknown as CacheStorage;
// Simulate user clicking the logout button
const logoutButton = await screen.findByText('Logout');
await userEvent.click(logoutButton);
try {
await userEvent.hover(await screen.findByText(/Settings/i));
// Wait for local and session storage to be cleared
await waitFor(() => {
expect(localStorage.getItem('redux')).toBeNull();
expect(sessionStorage.getItem('login_attempted')).toBeNull();
});
// Simulate user clicking the logout button
const logoutButton = await screen.findByText('Logout');
await userEvent.click(logoutButton);
// Wait for local and session storage to be cleared
await waitFor(() => {
expect(localStorage.getItem('redux')).toBeNull();
expect(sessionStorage.getItem('login_attempted')).toBeNull();
});
// The namespaced Cache API store is purged on logout.
expect(deleteMock).toHaveBeenCalledWith('@SUPERSET-UI/CONNECTION');
} finally {
// Restore the global so an early assertion failure cannot leak the mock
// into other tests.
if (priorCaches === undefined) {
delete cacheGlobal.caches;
} else {
cacheGlobal.caches = priorCaches;
}
}
});
test('shows logout button when not embedded', async () => {

View File

@@ -28,6 +28,7 @@ import {
getExtensionsRegistry,
isFeatureEnabled,
FeatureFlag,
CACHE_KEY,
} from '@superset-ui/core';
import {
styled,
@@ -353,6 +354,14 @@ const RightMenu = ({
try {
window.localStorage.removeItem('redux');
window.sessionStorage.removeItem('login_attempted');
// Purge the namespaced Cache API store so cached GET responses are not
// retained on the device after the session ends. Best-effort: the
// returned promise is not awaited since logout navigates away.
if (typeof caches !== 'undefined') {
caches.delete(CACHE_KEY).catch(() => {
/* best-effort: ignore cache deletion failures */
});
}
} catch (error) {
console.warn('Failed to clear storage on logout:', error);
}