feat(auth): centralize token storage
This commit is contained in:
103
utils/__tests__/token.test.ts
Normal file
103
utils/__tests__/token.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { tokenStorage } from '../token';
|
||||
import { logger } from '../../services/logging';
|
||||
|
||||
describe('tokenStorage', () => {
|
||||
const STORAGE_KEY = 'meds_auth_tokens';
|
||||
const originalLocalStorageDescriptor = Object.getOwnPropertyDescriptor(
|
||||
window,
|
||||
'localStorage'
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
tokenStorage.clear();
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (originalLocalStorageDescriptor) {
|
||||
Object.defineProperty(
|
||||
window,
|
||||
'localStorage',
|
||||
originalLocalStorageDescriptor
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('persists tokens with a single storage key', () => {
|
||||
const tokens = { accessToken: 'access-123', refreshToken: 'refresh-789' };
|
||||
|
||||
tokenStorage.save(tokens);
|
||||
|
||||
expect(window.localStorage.getItem(STORAGE_KEY)).toEqual(
|
||||
JSON.stringify({
|
||||
accessToken: 'access-123',
|
||||
refreshToken: 'refresh-789',
|
||||
})
|
||||
);
|
||||
expect(tokenStorage.getAccessToken()).toBe('access-123');
|
||||
});
|
||||
|
||||
it('clears tokens from storage and cache', () => {
|
||||
tokenStorage.save({
|
||||
accessToken: 'access-123',
|
||||
refreshToken: 'refresh-789',
|
||||
});
|
||||
|
||||
tokenStorage.clear();
|
||||
|
||||
expect(window.localStorage.getItem(STORAGE_KEY)).toBeNull();
|
||||
expect(tokenStorage.getTokens()).toBeNull();
|
||||
expect(tokenStorage.getAccessToken()).toBeNull();
|
||||
});
|
||||
|
||||
it('handles corrupted storage values by clearing and logging', () => {
|
||||
window.localStorage.setItem(STORAGE_KEY, 'not json');
|
||||
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {});
|
||||
|
||||
expect(tokenStorage.getTokens()).toBeNull();
|
||||
|
||||
expect(window.localStorage.getItem(STORAGE_KEY)).toBeNull();
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'Failed to parse stored tokens, clearing cache',
|
||||
'AUTH_TOKENS',
|
||||
expect.any(Error)
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to in-memory storage when localStorage is unavailable', () => {
|
||||
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {});
|
||||
const error = new Error('blocked');
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
|
||||
tokenStorage.clear();
|
||||
tokenStorage.save({ accessToken: 'access-only' });
|
||||
|
||||
expect(tokenStorage.getAccessToken()).toBe('access-only');
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'Token storage fallback to memory',
|
||||
'AUTH_TOKENS',
|
||||
error
|
||||
);
|
||||
|
||||
if (originalLocalStorageDescriptor) {
|
||||
Object.defineProperty(
|
||||
window,
|
||||
'localStorage',
|
||||
originalLocalStorageDescriptor
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('throws when attempting to save without an access token', () => {
|
||||
expect(() => tokenStorage.save({ accessToken: '' })).toThrow(
|
||||
'Token payload must include an access token'
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user