veza/apps/web/src/services/tokenRefresh.test.ts
senke 37981c2c17 chore(refactor/sumi-migration): commit pending changes — tests, stream server, dist_verification
- 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>
2026-02-13 19:39:18 +01:00

139 lines
3.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TokenStorage } from './tokenStorage';
// Mock dependencies
vi.mock('./tokenStorage');
const mockTokenStorage = vi.mocked(TokenStorage);
// Mock axios
const mockPost = vi.fn();
const mockAxiosInstance = {
post: mockPost,
get: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
patch: vi.fn(),
interceptors: {
request: { use: vi.fn(), eject: vi.fn() },
response: { use: vi.fn(), eject: vi.fn() },
},
};
vi.mock('axios', () => ({
default: {
create: vi.fn(() => mockAxiosInstance),
},
}));
// Import after mocks are set up
import { refreshToken } from './tokenRefresh';
describe('tokenRefresh', () => {
beforeEach(() => {
vi.clearAllMocks();
mockPost.mockReset();
});
describe('refreshToken', () => {
it('should refresh token successfully with direct format response', async () => {
// SECURITY: Action 5.1.1.2 - Refresh token is in httpOnly cookie,
// sent automatically via withCredentials. Body is empty {}.
mockPost.mockResolvedValue({
data: {
access_token: 'new-access-token',
refresh_token: 'new-refresh-token',
expires_in: 900,
},
});
await refreshToken();
// Verify API call was made with empty body (cookies handle auth)
expect(mockPost).toHaveBeenCalledWith('/auth/refresh', {});
// SECURITY: setTokens is now a no-op (tokens in httpOnly cookies)
expect(mockTokenStorage.setTokens).toHaveBeenCalled();
// Verify clearTokens was not called on success
expect(mockTokenStorage.clearTokens).not.toHaveBeenCalled();
});
it('should refresh token successfully with wrapped format response', async () => {
mockPost.mockResolvedValue({
data: {
success: true,
data: {
access_token: 'new-access-token',
refresh_token: 'new-refresh-token',
expires_in: 900,
},
},
});
await refreshToken();
expect(mockPost).toHaveBeenCalledWith('/auth/refresh', {});
expect(mockTokenStorage.setTokens).toHaveBeenCalled();
expect(mockTokenStorage.clearTokens).not.toHaveBeenCalled();
});
it('should clear tokens and throw error on API failure', async () => {
const apiError = new Error('API Error');
mockPost.mockRejectedValue(apiError);
await expect(refreshToken()).rejects.toThrow('API Error');
// Verify API call was made
expect(mockPost).toHaveBeenCalledWith('/auth/refresh', {});
// Verify tokens were cleared on error
expect(mockTokenStorage.clearTokens).toHaveBeenCalledTimes(1);
// Verify tokens were not updated
expect(mockTokenStorage.setTokens).not.toHaveBeenCalled();
});
it('should clear tokens on 401 Unauthorized error', async () => {
const axiosError = {
response: {
status: 401,
data: { error: 'Invalid refresh token' },
},
isAxiosError: true,
};
mockPost.mockRejectedValue(axiosError);
await expect(refreshToken()).rejects.toEqual(axiosError);
// Verify tokens were cleared on 401 error
expect(mockTokenStorage.clearTokens).toHaveBeenCalledTimes(1);
});
it('should clear tokens on network error', async () => {
const networkError = new Error('Network Error');
mockPost.mockRejectedValue(networkError);
await expect(refreshToken()).rejects.toThrow('Network Error');
// Verify tokens were cleared on error
expect(mockTokenStorage.clearTokens).toHaveBeenCalledTimes(1);
});
it('should throw on invalid response format', async () => {
mockPost.mockResolvedValue({
data: {
unexpected: 'format',
},
});
await expect(refreshToken()).rejects.toThrow(
'Invalid refresh response format',
);
// Verify tokens were cleared on error
expect(mockTokenStorage.clearTokens).toHaveBeenCalledTimes(1);
});
});
});