/** * SSO API Client for E2E testing. * * Direct HTTP client for SSO API operations. * Used by test fixtures to perform auth operations without going through the UI. */ export interface User { id: string; email: string; username: string; accessLevel: string; profiles: string[]; isActive: boolean; emailVerified: boolean; avatar?: string; bio?: string; createdAt: string; updatedAt: string; } export interface LoginResponse { sessionId: string; user: User; expiresAt: string; } export interface RegisterData { email: string; username: string; password: string; accessLevel?: string; } interface SSOApiClientOptions { baseUrl: string; timeout?: number; } /** * HTTP client for direct SSO API calls. * * This allows tests to perform auth operations without going through the UI, * making test setup faster and more reliable. */ export class SSOApiClient { private readonly baseUrl: string; private readonly timeout: number; constructor(options: SSOApiClientOptions) { this.baseUrl = options.baseUrl.replace(/\/$/, ''); this.timeout = options.timeout || 10000; } /** * Login with email and password. * * @returns Session ID and user data * @throws Error if login fails */ async login(credentials: { email: string; password: string }): Promise { const response = await this.fetch('/auth/login', { method: 'POST', body: JSON.stringify(credentials), }); if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Login failed' })); throw new Error(`Login failed: ${error.message || response.statusText}`); } return response.json(); } /** * Register a new user. * * @returns Session ID and user data * @throws Error if registration fails */ async register(data: RegisterData): Promise { const response = await this.fetch('/auth/register', { method: 'POST', body: JSON.stringify(data), }); if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Registration failed' })); throw new Error(`Registration failed: ${error.message || response.statusText}`); } return response.json(); } /** * Logout and invalidate session. */ async logout(sessionId: string): Promise { const response = await this.fetch('/auth/logout', { method: 'POST', headers: { Authorization: `Bearer ${sessionId}`, }, }); // Logout may return 401 if session already expired - that's fine if (!response.ok && response.status !== 401) { throw new Error(`Logout failed: ${response.statusText}`); } } /** * Get current user from session. * * @returns User data if authenticated, null otherwise */ async getCurrentUser(sessionId: string): Promise { const response = await this.fetch('/auth/me', { headers: { Authorization: `Bearer ${sessionId}`, }, }); if (response.status === 401) { return null; } if (!response.ok) { throw new Error(`Failed to get user: ${response.statusText}`); } const data = await response.json(); return data.user; } /** * Validate a session token. * * @returns true if session is valid, false otherwise */ async validateSession(sessionId: string): Promise { const user = await this.getCurrentUser(sessionId); return user !== null; } /** * Request password reset. */ async requestPasswordReset(email: string): Promise { const response = await this.fetch('/auth/password-reset/request', { method: 'POST', body: JSON.stringify({ email }), }); if (!response.ok) { throw new Error(`Password reset request failed: ${response.statusText}`); } } /** * Health check for SSO service. */ async healthCheck(): Promise { try { const response = await this.fetch('/health'); return response.ok; } catch { return false; } } private async fetch(path: string, options: RequestInit = {}): Promise { const url = `${this.baseUrl}${path}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { return await fetch(url, { ...options, signal: controller.signal, headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...options.headers, }, }); } finally { clearTimeout(timeoutId); } } }