- apps/web: test updates (Vitest/setup), playbackAnalyticsService, TrackGrid, serviceErrorHandler - veza-common: logging, metrics, traits, validation, random - veza-stream-server: audio pipeline, codecs, cache, monitoring, routes - apps/web/dist_verification: refresh build assets (content-hashed filenames) Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
3.3 KiB
TypeScript
107 lines
3.3 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { TokenStorage, _resetTokenMemory } from './tokenStorage';
|
|
|
|
// Mock localStorage
|
|
const localStorageMock = (() => {
|
|
let store: Record<string, string> = {};
|
|
|
|
return {
|
|
getItem: vi.fn((key: string) => store[key] || null),
|
|
setItem: vi.fn((key: string, value: string) => {
|
|
store[key] = value.toString();
|
|
}),
|
|
removeItem: vi.fn((key: string) => {
|
|
delete store[key];
|
|
}),
|
|
clear: vi.fn(() => {
|
|
store = {};
|
|
}),
|
|
};
|
|
})();
|
|
|
|
Object.defineProperty(window, 'localStorage', {
|
|
value: localStorageMock,
|
|
writable: true,
|
|
});
|
|
|
|
describe('TokenStorage', () => {
|
|
beforeEach(() => {
|
|
localStorageMock.clear();
|
|
vi.clearAllMocks();
|
|
_resetTokenMemory();
|
|
});
|
|
|
|
describe('setTokens', () => {
|
|
it('should clean up legacy localStorage tokens (no-op for httpOnly cookies)', () => {
|
|
// SECURITY: Action 5.1.1.2 - setTokens is now a no-op that only cleans legacy localStorage
|
|
TokenStorage.setTokens('access-token', 'refresh-token');
|
|
|
|
// Should remove legacy localStorage entries, not set them
|
|
expect(localStorageMock.removeItem).toHaveBeenCalledWith(
|
|
'veza_access_token',
|
|
);
|
|
expect(localStorageMock.removeItem).toHaveBeenCalledWith(
|
|
'veza_refresh_token',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getAccessToken', () => {
|
|
it('should return null (token is in httpOnly cookie, not accessible from JS)', () => {
|
|
const result = TokenStorage.getAccessToken();
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('getRefreshToken', () => {
|
|
it('should return null (token is in httpOnly cookie, not accessible from JS)', () => {
|
|
const result = TokenStorage.getRefreshToken();
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('clearTokens', () => {
|
|
it('should remove legacy localStorage tokens', () => {
|
|
TokenStorage.clearTokens();
|
|
|
|
expect(localStorageMock.removeItem).toHaveBeenCalledWith(
|
|
'veza_access_token',
|
|
);
|
|
expect(localStorageMock.removeItem).toHaveBeenCalledWith(
|
|
'veza_refresh_token',
|
|
);
|
|
});
|
|
|
|
it('should not throw error', () => {
|
|
expect(() => TokenStorage.clearTokens()).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('hasTokens', () => {
|
|
it('should return false (cannot check httpOnly cookies from JavaScript)', () => {
|
|
const result = TokenStorage.hasTokens();
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('integration', () => {
|
|
it('should work correctly with httpOnly cookie model', () => {
|
|
// With httpOnly cookies, JS cannot access tokens
|
|
expect(TokenStorage.hasTokens()).toBe(false);
|
|
expect(TokenStorage.getAccessToken()).toBeNull();
|
|
expect(TokenStorage.getRefreshToken()).toBeNull();
|
|
|
|
// setTokens is a no-op (just clears legacy localStorage)
|
|
TokenStorage.setTokens('access-123', 'refresh-456');
|
|
expect(TokenStorage.hasTokens()).toBe(false);
|
|
expect(TokenStorage.getAccessToken()).toBeNull();
|
|
expect(TokenStorage.getRefreshToken()).toBeNull();
|
|
|
|
// clearTokens cleans up legacy localStorage
|
|
TokenStorage.clearTokens();
|
|
expect(TokenStorage.hasTokens()).toBe(false);
|
|
expect(TokenStorage.getAccessToken()).toBeNull();
|
|
expect(TokenStorage.getRefreshToken()).toBeNull();
|
|
});
|
|
});
|
|
});
|