mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 18:44:05 +02:00
# Description of Changes Ages ago I made #4835 to try and fix all the `any` type usage in the system but never got it finished, and there were just too many to review and ensure it still worked. There's even more now. My new tactic is to fix folder by folder. This fixes the `any` typing in the `saas/` folder, and also enables `no-unnecessary-type-assertion`, which really helps reduce pointless `as` casts that AI generates when the type is already known. I hope to expand both of these to the rest of the folders soon, but one folder is better than none.
234 lines
7.2 KiB
TypeScript
234 lines
7.2 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import type { Session, AuthError } from '@supabase/supabase-js';
|
|
import { supabase } from '@app/auth/supabase';
|
|
|
|
// Mock supabase
|
|
vi.mock('@app/auth/supabase', () => ({
|
|
supabase: {
|
|
auth: {
|
|
getSession: vi.fn(),
|
|
refreshSession: vi.fn(),
|
|
},
|
|
},
|
|
}));
|
|
|
|
describe('apiClient', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Reset modules to get fresh instance of apiClient
|
|
vi.resetModules();
|
|
});
|
|
|
|
it('should add JWT token to request headers when session exists', async () => {
|
|
const mockToken = 'test-jwt-token-12345';
|
|
const mockSession = {
|
|
access_token: mockToken,
|
|
refresh_token: 'refresh-token',
|
|
expires_in: 3600,
|
|
token_type: 'bearer',
|
|
user: { id: 'user-123' },
|
|
} as unknown as Session;
|
|
|
|
// Mock getSession to return a session with token
|
|
vi.mocked(supabase.auth.getSession).mockResolvedValue({
|
|
data: { session: mockSession },
|
|
error: null,
|
|
});
|
|
|
|
// Import apiClient after mocking
|
|
const { default: apiClient } = await import('@app/services/apiClient');
|
|
|
|
// Create a mock adapter to intercept the request
|
|
const mockAdapter = vi.fn((config) => {
|
|
// Verify the Authorization header is set correctly
|
|
expect(config.headers.Authorization).toBe(`Bearer ${mockToken}`);
|
|
return Promise.resolve({
|
|
data: { success: true },
|
|
status: 200,
|
|
statusText: 'OK',
|
|
headers: {},
|
|
config,
|
|
});
|
|
});
|
|
|
|
// Replace the adapter
|
|
apiClient.defaults.adapter = mockAdapter;
|
|
|
|
// Make a test request
|
|
await apiClient.get('/api/v1/test');
|
|
|
|
// Verify the request was made with the token
|
|
expect(mockAdapter).toHaveBeenCalled();
|
|
expect(supabase.auth.getSession).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle requests when no session exists', async () => {
|
|
// Mock getSession to return no session
|
|
vi.mocked(supabase.auth.getSession).mockResolvedValue({
|
|
data: { session: null },
|
|
error: null,
|
|
});
|
|
|
|
// Import apiClient after mocking
|
|
const { default: apiClient } = await import('@app/services/apiClient');
|
|
|
|
// Create a mock adapter to intercept the request
|
|
const mockAdapter = vi.fn((config) => {
|
|
// Verify no Authorization header is set
|
|
expect(config.headers.Authorization).toBeUndefined();
|
|
return Promise.resolve({
|
|
data: { success: true },
|
|
status: 200,
|
|
statusText: 'OK',
|
|
headers: {},
|
|
config,
|
|
});
|
|
});
|
|
|
|
// Replace the adapter
|
|
apiClient.defaults.adapter = mockAdapter;
|
|
|
|
// Make a test request
|
|
await apiClient.get('/api/v1/test');
|
|
|
|
// Verify the request was made without a token
|
|
expect(mockAdapter).toHaveBeenCalled();
|
|
expect(supabase.auth.getSession).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should refresh token on 401 response', async () => {
|
|
const oldToken = 'old-token';
|
|
const newToken = 'new-refreshed-token';
|
|
|
|
const oldSession = {
|
|
access_token: oldToken,
|
|
refresh_token: 'refresh-token',
|
|
expires_in: 3600,
|
|
token_type: 'bearer',
|
|
user: { id: 'user-123' },
|
|
} as unknown as Session;
|
|
|
|
const newSession = {
|
|
access_token: newToken,
|
|
refresh_token: 'new-refresh-token',
|
|
expires_in: 3600,
|
|
token_type: 'bearer',
|
|
user: { id: 'user-123' },
|
|
} as unknown as Session;
|
|
|
|
// Mock initial session for first request
|
|
let getSessionCallCount = 0;
|
|
vi.mocked(supabase.auth.getSession).mockImplementation(async () => {
|
|
getSessionCallCount++;
|
|
// First call returns old session, subsequent calls return new session
|
|
if (getSessionCallCount === 1) {
|
|
return { data: { session: oldSession }, error: null };
|
|
}
|
|
return { data: { session: newSession }, error: null };
|
|
});
|
|
|
|
// Mock refresh to return new session
|
|
vi.mocked(supabase.auth.refreshSession).mockResolvedValue({
|
|
data: { user: null, session: newSession },
|
|
error: null,
|
|
});
|
|
|
|
// Import apiClient after mocking
|
|
const { default: apiClient } = await import('@app/services/apiClient');
|
|
|
|
let requestCount = 0;
|
|
const mockAdapter = vi.fn((config) => {
|
|
requestCount++;
|
|
|
|
// First request returns 401
|
|
if (requestCount === 1) {
|
|
// Verify first request has old token
|
|
expect(config.headers.Authorization).toBe(`Bearer ${oldToken}`);
|
|
const error = Object.assign(new Error('Unauthorized'), {
|
|
response: { status: 401, data: { error: 'Unauthorized' } },
|
|
config,
|
|
});
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
// Second request (after refresh) should have new token
|
|
// The interceptor will call getSession again, which now returns the new session
|
|
expect(config.headers.Authorization).toBe(`Bearer ${newToken}`);
|
|
return Promise.resolve({
|
|
data: { success: true },
|
|
status: 200,
|
|
statusText: 'OK',
|
|
headers: {},
|
|
config,
|
|
});
|
|
});
|
|
|
|
// Replace the adapter
|
|
apiClient.defaults.adapter = mockAdapter;
|
|
|
|
// Make a test request that will trigger 401 and retry
|
|
const response = await apiClient.get('/api/v1/test');
|
|
|
|
// Verify the token was refreshed and request retried
|
|
expect(response.data).toEqual({ success: true });
|
|
expect(supabase.auth.refreshSession).toHaveBeenCalled();
|
|
expect(mockAdapter).toHaveBeenCalledTimes(2);
|
|
expect(getSessionCallCount).toBe(3); // Called for initial request, for checking if refresh is possible, and for retry
|
|
});
|
|
|
|
it('should handle refresh token failure', async () => {
|
|
const oldToken = 'old-token';
|
|
|
|
const oldSession = {
|
|
access_token: oldToken,
|
|
user: { id: 'user-123' },
|
|
};
|
|
|
|
// Mock initial session
|
|
vi.mocked(supabase.auth.getSession).mockResolvedValue({
|
|
data: { session: oldSession as unknown as Session },
|
|
error: null,
|
|
});
|
|
vi.mocked(supabase.auth.getSession).mockResolvedValue({
|
|
data: { session: oldSession as unknown as Session },
|
|
error: null,
|
|
});
|
|
|
|
// Mock refresh to fail
|
|
vi.mocked(supabase.auth.refreshSession).mockResolvedValue({
|
|
data: { user: null, session: null },
|
|
error: { name: 'AuthError', message: 'Refresh failed', status: 400, code: 'auth_error' } as unknown as AuthError,
|
|
});
|
|
|
|
// Import apiClient after mocking
|
|
const { default: apiClient } = await import('@app/services/apiClient');
|
|
|
|
// Mock window.location for redirect test
|
|
Object.defineProperty(window, 'location', { writable: true, value: { href: '' } });
|
|
|
|
const mockAdapter = vi.fn((config) => {
|
|
// Always return 401 to trigger refresh
|
|
const error = Object.assign(new Error('Unauthorized'), {
|
|
response: { status: 401, data: { error: 'Unauthorized' } },
|
|
config,
|
|
});
|
|
return Promise.reject(error);
|
|
});
|
|
|
|
// Replace the adapter
|
|
apiClient.defaults.adapter = mockAdapter;
|
|
|
|
// Make a test request that will trigger 401
|
|
try {
|
|
await apiClient.get('/api/v1/test');
|
|
// Should not reach here
|
|
expect(true).toBe(false);
|
|
} catch (_) {
|
|
// Verify refresh was attempted
|
|
expect(supabase.auth.refreshSession).toHaveBeenCalled();
|
|
// Verify redirect to login
|
|
expect(window.location.href).toBe('/login');
|
|
}
|
|
});
|
|
});
|