/**
* Authentication Manager Tests
* Tests for session management and token handling
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mockFetchSuccess, mockFetchError } from '../helpers/mock-fetch';
describe('AuthManager', () => {
const config = {
url: 'https://test.komodo.example.com',
apiKey: 'test_api_key',
apiSecret: 'test_api_secret',
};
beforeEach(() => {
vi.clearAllMocks();
});
describe('Session Management', () => {
it('should initialize without active session', () => {
const authManager = {
sessionToken: null,
isAuthenticated: false,
};
expect(authManager.sessionToken).toBeNull();
expect(authManager.isAuthenticated).toBe(false);
});
it('should store session token after login', async () => {
const mockToken = 'session_token_abc123';
const mockFetch = mockFetchSuccess({ token: mockToken });
global.fetch = mockFetch as any;
const response = await mockFetch(`${config.url}/auth/login`);
const data = await response.json();
const authManager = {
sessionToken: data.token,
isAuthenticated: true,
};
expect(authManager.sessionToken).toBe(mockToken);
expect(authManager.isAuthenticated).toBe(true);
});
it('should clear session token after logout', async () => {
const authManager = {
sessionToken: 'existing_token',
isAuthenticated: true,
};
const mockFetch = mockFetchSuccess({ success: true });
global.fetch = mockFetch as any;
await mockFetch(`${config.url}/auth/logout`);
authManager.sessionToken = null;
authManager.isAuthenticated = false;
expect(authManager.sessionToken).toBeNull();
expect(authManager.isAuthenticated).toBe(false);
});
it('should validate active session', async () => {
const mockFetch = mockFetchSuccess({ valid: true });
global.fetch = mockFetch as any;
const response = await mockFetch(`${config.url}/auth/validate`);
const data = await response.json();
expect(data.valid).toBe(true);
});
it('should detect invalid session', async () => {
const mockFetch = mockFetchError('Session expired', { status: 401 });
global.fetch = mockFetch as any;
const response = await mockFetch(`${config.url}/auth/validate`);
expect(response.ok).toBe(false);
expect(response.status).toBe(401);
});
});
describe('Token Refresh', () => {
it('should refresh expired token', async () => {
const newToken = 'refreshed_token_xyz789';
const mockFetch = mockFetchSuccess({ token: newToken });
global.fetch = mockFetch as any;
const response = await mockFetch(`${config.url}/auth/refresh`);
const data = await response.json();
const authManager = {
sessionToken: data.token,
isAuthenticated: true,
};
expect(authManager.sessionToken).toBe(newToken);
});
it('should handle refresh token expiration', async () => {
const mockFetch = mockFetchError('Refresh token expired', { status: 401 });
global.fetch = mockFetch as any;
const response = await mockFetch(`${config.url}/auth/refresh`);
expect(response.ok).toBe(false);
expect(response.status).toBe(401);
const authManager = {
sessionToken: null,
isAuthenticated: false,
};
expect(authManager.sessionToken).toBeNull();
});
it('should automatically refresh on 401 response', async () => {
let callCount = 0;
const mockFetch = vi.fn(async (url: string) => {
callCount++;
if (callCount === 1) {
// First call returns 401
return {
ok: false,
status: 401,
json: async () => ({ error: 'Unauthorized' }),
};
} else if (callCount === 2) {
// Second call (refresh) returns new token
return {
ok: true,
status: 200,
json: async () => ({ token: 'new_token' }),
};
} else {
// Third call (retry) succeeds
return {
ok: true,
status: 200,
json: async () => ({ data: 'success' }),
};
}
});
global.fetch = mockFetch as any;
// Simulate auto-refresh logic
const makeRequest = async () => {
let response = await mockFetch(`${config.url}/api/data`);
if (response.status === 401) {
// Refresh token
const refreshResponse = await mockFetch(`${config.url}/auth/refresh`);
const refreshData = await refreshResponse.json();
if (refreshData.token) {
// Retry original request
response = await mockFetch(`${config.url}/api/data`);
}
}
return response;
};
const finalResponse = await makeRequest();
const data = await finalResponse.json();
expect(callCount).toBe(3);
expect(data).toEqual({ data: 'success' });
});
});
describe('Authentication Headers', () => {
it('should include session token in authenticated requests', async () => {
const sessionToken = 'active_session_token';
const mockFetch = mockFetchSuccess({ data: 'protected' });
global.fetch = mockFetch as any;
await mockFetch(`${config.url}/api/protected`, {
headers: {
'Authorization': `Bearer ${sessionToken}`,
},
});
expect(mockFetch).toHaveBeenCalledWith(
`${config.url}/api/protected`,
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': `Bearer ${sessionToken}`,
}),
})
);
});
it('should not include token in public requests', async () => {
const mockFetch = mockFetchSuccess({ data: 'public' });
global.fetch = mockFetch as any;
await mockFetch(`${config.url}/api/public`, {
headers: {},
});
const call = mockFetch.mock.calls[0];
expect(call[1]?.headers?.['Authorization']).toBeUndefined();
});
});
describe('Error Handling', () => {
it('should handle invalid credentials on login', async () => {
const mockFetch = mockFetchError('Invalid credentials', { status: 401 });
global.fetch = mockFetch as any;
const response = await mockFetch(`${config.url}/auth/login`, {
method: 'POST',
body: JSON.stringify({
username: 'wrong',
password: 'wrong',
}),
});
expect(response.ok).toBe(false);
expect(response.status).toBe(401);
});
it('should handle network errors during login', async () => {
const mockFetch = vi.fn(async () => {
throw new Error('Network error');
});
global.fetch = mockFetch as any;
await expect(
mockFetch(`${config.url}/auth/login`)
).rejects.toThrow('Network error');
});
it('should handle server errors during token refresh', async () => {
const mockFetch = mockFetchError('Server error', { status: 500 });
global.fetch = mockFetch as any;
const response = await mockFetch(`${config.url}/auth/refresh`);
expect(response.ok).toBe(false);
expect(response.status).toBe(500);
});
});
describe('Session Persistence', () => {
it('should persist session token', () => {
const token = 'persistent_token';
const storage = { token: null as string | null };
storage.token = token;
expect(storage.token).toBe(token);
});
it('should restore session from storage', () => {
const storedToken = 'stored_token';
const storage = { token: storedToken };
const authManager = {
sessionToken: storage.token,
isAuthenticated: !!storage.token,
};
expect(authManager.sessionToken).toBe(storedToken);
expect(authManager.isAuthenticated).toBe(true);
});
it('should clear persisted session on logout', () => {
const storage = { token: 'token_to_clear' as string | null };
storage.token = null;
expect(storage.token).toBeNull();
});
});
});