Files
doormilxpress_astryx/tests/utils/session.test.js
2026-08-27 13:40:48 +05:30

172 lines
6.0 KiB
JavaScript

import {
ACTIVITY_STORAGE_KEY,
SESSION_START_STORAGE_KEY,
AUTH_PRESENCE_KEY,
INACTIVITY_TIMEOUT_MS,
ABSOLUTE_SESSION_TIMEOUT_MS,
markActivity,
markSessionStart,
isSessionActive,
performSessionLogout
} from '@/utils/session';
/**
* The shared logout contract. Every logout path — the manual button and the
* inactivity timer — goes through here so they cannot clear different things.
*
* The security-relevant property is that local state is wiped and the page
* HARD-navigates even when a caller-supplied teardown throws: a half-completed
* logout that leaves an authenticated page rendered is the failure mode this
* module exists to prevent.
*/
describe('session', () => {
let replace;
beforeEach(() => {
replace = jest.fn();
Object.defineProperty(window, 'location', {
writable: true,
configurable: true,
value: { href: 'http://console.local/deliveries', replace }
});
});
describe('timeout policy', () => {
it('should expire an idle session after one hour', () => {
expect(INACTIVITY_TIMEOUT_MS).toBe(60 * 60 * 1000);
});
it('should cap a session at one hour since login regardless of activity', () => {
expect(ABSOLUTE_SESSION_TIMEOUT_MS).toBe(60 * 60 * 1000);
});
});
describe('markActivity', () => {
it('should record the current time as a millisecond string', () => {
jest.useFakeTimers().setSystemTime(new Date('2026-08-27T10:00:00Z'));
markActivity();
expect(localStorage.getItem(ACTIVITY_STORAGE_KEY)).toBe(String(Date.now()));
jest.useRealTimers();
});
it('should overwrite the previous activity stamp', () => {
jest.useFakeTimers().setSystemTime(new Date('2026-08-27T10:00:00Z'));
markActivity();
const first = localStorage.getItem(ACTIVITY_STORAGE_KEY);
jest.setSystemTime(new Date('2026-08-27T10:05:00Z'));
markActivity();
expect(localStorage.getItem(ACTIVITY_STORAGE_KEY)).not.toBe(first);
expect(Number(localStorage.getItem(ACTIVITY_STORAGE_KEY))).toBeGreaterThan(Number(first));
jest.useRealTimers();
});
});
describe('markSessionStart', () => {
it('should start the absolute-lifetime clock separately from the activity clock', () => {
jest.useFakeTimers().setSystemTime(new Date('2026-08-27T10:00:00Z'));
markSessionStart();
expect(localStorage.getItem(SESSION_START_STORAGE_KEY)).toBe(String(Date.now()));
expect(localStorage.getItem(ACTIVITY_STORAGE_KEY)).toBeNull();
jest.useRealTimers();
});
});
describe('isSessionActive', () => {
it('should report an active session when the auth presence key is set', () => {
localStorage.setItem(AUTH_PRESENCE_KEY, 'ops@doormile.com');
expect(isSessionActive()).toBe(true);
});
it('should report no session when the key is absent', () => {
expect(isSessionActive()).toBe(false);
});
it('should report no session for an empty presence value', () => {
localStorage.setItem(AUTH_PRESENCE_KEY, '');
expect(isSessionActive()).toBe(false);
});
it('should return a boolean, not the stored string', () => {
localStorage.setItem(AUTH_PRESENCE_KEY, 'ops@doormile.com');
expect(typeof isSessionActive()).toBe('boolean');
});
});
describe('performSessionLogout', () => {
it('should wipe all local state and hard-navigate to login', () => {
localStorage.setItem('doormileToken', 'jwt-abc');
localStorage.setItem(AUTH_PRESENCE_KEY, 'ops@doormile.com');
localStorage.setItem('anything-else', 'x');
performSessionLogout();
expect(localStorage.length).toBe(0);
expect(replace).toHaveBeenCalledWith('/login');
});
it('should clear the react-query cache so no fetched data survives', () => {
const queryClient = { clear: jest.fn() };
performSessionLogout({ queryClient });
expect(queryClient.clear).toHaveBeenCalledTimes(1);
});
it('should dispatch the supplied teardown actions', () => {
const dispatch = jest.fn();
const clearFcmToken = jest.fn(() => ({ type: 'fcm/clear' }));
const logoutUser = jest.fn(() => ({ type: 'auth/logout' }));
performSessionLogout({ dispatch, clearFcmToken, logoutUser });
expect(dispatch).toHaveBeenCalledWith({ type: 'fcm/clear' });
expect(dispatch).toHaveBeenCalledWith({ type: 'auth/logout' });
});
it('should skip the redux teardown when no dispatch is supplied', () => {
const clearFcmToken = jest.fn();
expect(() => performSessionLogout({ clearFcmToken })).not.toThrow();
expect(clearFcmToken).not.toHaveBeenCalled();
});
it('should work with no arguments at all', () => {
expect(() => performSessionLogout()).not.toThrow();
expect(replace).toHaveBeenCalledWith('/login');
});
it('should still wipe storage and redirect when the cache teardown throws', () => {
// The security-critical guarantee: a broken teardown must not leave an
// authenticated page rendered over live local state.
localStorage.setItem('doormileToken', 'jwt-abc');
const queryClient = {
clear: jest.fn(() => {
throw new Error('cache blew up');
})
};
expect(() => performSessionLogout({ queryClient })).toThrow('cache blew up');
expect(localStorage.length).toBe(0);
expect(replace).toHaveBeenCalledWith('/login');
});
it('should still wipe storage and redirect when a dispatched action throws', () => {
localStorage.setItem('doormileToken', 'jwt-abc');
const dispatch = jest.fn(() => {
throw new Error('reducer blew up');
});
expect(() =>
performSessionLogout({ dispatch, logoutUser: () => ({ type: 'auth/logout' }) })
).toThrow('reducer blew up');
expect(localStorage.length).toBe(0);
expect(replace).toHaveBeenCalledWith('/login');
});
it('should navigate rather than route, so the back button cannot resurrect the console', () => {
performSessionLogout();
expect(replace).toHaveBeenCalledTimes(1);
expect(replace).toHaveBeenCalledWith('/login');
});
});
});