0348dd1eab
Deploy Dashboard (Main) / Backend Tests (push) Successful in 2m32s
Deploy Dashboard (Main) / Frontend Tests (push) Successful in 6s
Deploy Dashboard (Main) / Build Main Image (push) Successful in 4m11s
Deploy Dashboard (Main) / Deploy to Production (push) Successful in 55s
391 lines
9.1 KiB
JavaScript
391 lines
9.1 KiB
JavaScript
/**
|
|
* Unit tests for API client (api.js)
|
|
*/
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import {
|
|
setToken,
|
|
getToken,
|
|
setCurrentUser,
|
|
currentUser,
|
|
get,
|
|
post,
|
|
del,
|
|
put,
|
|
login,
|
|
checkAuth,
|
|
logout,
|
|
tryRefreshSession,
|
|
} from '../../src/lib/api.js';
|
|
|
|
describe('API Client - Token Management', () => {
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
setToken('');
|
|
});
|
|
|
|
it('should set and get token', () => {
|
|
setToken('test-token-123');
|
|
expect(getToken()).toBe('test-token-123');
|
|
});
|
|
|
|
it('should store token in localStorage', () => {
|
|
setToken('test-token-123');
|
|
expect(localStorage.setItem).toHaveBeenCalledWith('token', 'test-token-123');
|
|
});
|
|
|
|
it('should remove token from localStorage when cleared', () => {
|
|
setToken('test-token-123');
|
|
setToken('');
|
|
expect(localStorage.removeItem).toHaveBeenCalledWith('token');
|
|
});
|
|
|
|
it('should set current user', () => {
|
|
const user = { username: 'testuser', role: 'admin', pages: ['dashboard'] };
|
|
setCurrentUser(user);
|
|
expect(currentUser).toEqual(user);
|
|
});
|
|
});
|
|
|
|
describe('API Client - GET Requests', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
setToken('test-token');
|
|
});
|
|
|
|
it('should make GET request with auth header', async () => {
|
|
const mockData = { result: 'success' };
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockData,
|
|
});
|
|
|
|
const result = await get('/test/endpoint');
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
'/api/test/endpoint',
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
Authorization: 'Bearer test-token',
|
|
}),
|
|
})
|
|
);
|
|
expect(result).toEqual(mockData);
|
|
});
|
|
|
|
it('should make GET request without auth header when no token', async () => {
|
|
setToken('');
|
|
const mockData = { result: 'success' };
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockData,
|
|
});
|
|
|
|
await get('/test/endpoint');
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
'/api/test/endpoint',
|
|
expect.objectContaining({
|
|
headers: expect.not.objectContaining({
|
|
Authorization: expect.anything(),
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should throw error on 404', async () => {
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 404,
|
|
json: async () => ({ error: 'Not found' }),
|
|
});
|
|
|
|
await expect(get('/test/endpoint')).rejects.toThrow('HTTP 404');
|
|
});
|
|
});
|
|
|
|
describe('API Client - POST Requests', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
setToken('test-token');
|
|
});
|
|
|
|
it('should make POST request with JSON body', async () => {
|
|
const mockData = { result: 'created' };
|
|
const postData = { name: 'test', value: 123 };
|
|
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockData,
|
|
});
|
|
|
|
const result = await post('/test/endpoint', postData);
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
'/api/test/endpoint',
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
headers: expect.objectContaining({
|
|
'Content-Type': 'application/json',
|
|
Authorization: 'Bearer test-token',
|
|
}),
|
|
body: JSON.stringify(postData),
|
|
})
|
|
);
|
|
expect(result).toEqual(mockData);
|
|
});
|
|
});
|
|
|
|
describe('API Client - DELETE Requests', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
setToken('test-token');
|
|
});
|
|
|
|
it('should make DELETE request', async () => {
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ message: 'deleted' }),
|
|
});
|
|
|
|
await del('/test/endpoint');
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
'/api/test/endpoint',
|
|
expect.objectContaining({
|
|
method: 'DELETE',
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('API Client - PUT Requests', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
setToken('test-token');
|
|
});
|
|
|
|
it('should make PUT request with JSON body', async () => {
|
|
const putData = { name: 'updated' };
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ result: 'updated' }),
|
|
});
|
|
|
|
await put('/test/endpoint', putData);
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
'/api/test/endpoint',
|
|
expect.objectContaining({
|
|
method: 'PUT',
|
|
headers: expect.objectContaining({
|
|
'Content-Type': 'application/json',
|
|
}),
|
|
body: JSON.stringify(putData),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('API Client - Authentication Methods', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should login with credentials', async () => {
|
|
const mockResponse = {
|
|
access_token: 'new-token',
|
|
user: 'testuser',
|
|
role: 'admin',
|
|
};
|
|
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockResponse,
|
|
});
|
|
|
|
const result = await login({ username: 'testuser', password: 'pass123' });
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
'/api/auth/login',
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
})
|
|
);
|
|
expect(result).toEqual(mockResponse);
|
|
});
|
|
|
|
it('should check auth status', async () => {
|
|
setToken('test-token');
|
|
const mockUser = { username: 'testuser', role: 'admin' };
|
|
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockUser,
|
|
});
|
|
|
|
const result = await checkAuth();
|
|
|
|
expect(fetch).toHaveBeenCalledWith('/api/auth/me', expect.anything());
|
|
expect(result).toEqual(mockUser);
|
|
});
|
|
|
|
it('should logout', async () => {
|
|
setToken('test-token');
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ message: 'logged out' }),
|
|
});
|
|
|
|
await logout();
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
'/api/auth/logout',
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('API Client - Token Refresh', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
setToken('');
|
|
localStorage.clear();
|
|
});
|
|
|
|
it('should refresh token from cookie', async () => {
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ access_token: 'new-token' }),
|
|
});
|
|
|
|
const result = await tryRefreshSession();
|
|
|
|
expect(result).toBe(true);
|
|
expect(getToken()).toBe('new-token');
|
|
});
|
|
|
|
it('should return false if cookie refresh fails (no legacy fallback)', async () => {
|
|
localStorage.getItem.mockReturnValue('legacy-refresh-token');
|
|
|
|
// Cookie refresh fails
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
});
|
|
|
|
const result = await tryRefreshSession();
|
|
|
|
expect(result).toBe(false);
|
|
expect(getToken()).toBe('');
|
|
});
|
|
|
|
it('should return false if all refresh attempts fail', async () => {
|
|
global.fetch.mockResolvedValue({
|
|
ok: false,
|
|
status: 401,
|
|
});
|
|
|
|
const result = await tryRefreshSession();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should auto-refresh on 401 response', async () => {
|
|
setToken('expired-token');
|
|
|
|
// First request returns 401
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
});
|
|
|
|
// Refresh succeeds
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ access_token: 'refreshed-token' }),
|
|
});
|
|
|
|
// Retry original request succeeds
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ data: 'success' }),
|
|
});
|
|
|
|
const result = await get('/test/endpoint');
|
|
|
|
expect(result).toEqual({ data: 'success' });
|
|
expect(getToken()).toBe('refreshed-token');
|
|
});
|
|
|
|
it('should clear token on final 401', async () => {
|
|
setToken('expired-token');
|
|
|
|
// First request returns 401
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
});
|
|
|
|
// Refresh fails
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
});
|
|
|
|
// Retry original request still 401
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
});
|
|
|
|
await expect(get('/test/endpoint')).rejects.toThrow('Unauthorized');
|
|
expect(getToken()).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('API Client - Error Handling', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
setToken('test-token');
|
|
});
|
|
|
|
it('should handle network errors', async () => {
|
|
global.fetch.mockReset();
|
|
global.fetch.mockRejectedValueOnce(new Error('Network error'));
|
|
|
|
await expect(get('/test/endpoint')).rejects.toThrow('Network error');
|
|
});
|
|
|
|
it('should include error body in thrown error', async () => {
|
|
const errorBody = { error: 'Bad request', details: 'Invalid input' };
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 400,
|
|
json: async () => errorBody,
|
|
});
|
|
|
|
try {
|
|
await get('/test/endpoint');
|
|
expect.fail('Should have thrown');
|
|
} catch (error) {
|
|
expect(error.status).toBe(400);
|
|
expect(error.body).toEqual(errorBody);
|
|
}
|
|
});
|
|
});
|