diff --git a/tests/core/auth-long-session.spec.ts b/tests/core/auth-long-session.spec.ts deleted file mode 100644 index dda6a33b..00000000 --- a/tests/core/auth-long-session.spec.ts +++ /dev/null @@ -1,327 +0,0 @@ -/** - * Authentication & Long-Session Test - * - * Validates that authentication tokens work correctly over 60-minute session: - * - Initial login successful - * - Token auto-refresh works continuously - * - Session persists for 60 minutes without 401 errors - * - Heartbeat logging every 10 minutes - * - Container health maintained throughout - * - No token expiration during session - * - * Total Tests: 1 (long-running) - * Expected Duration: 60+ minutes - * - * Heartbeat Log Output Format: - * ✓ [Heartbeat 1] Min 10: Initial login successful. Token expires: 2026-02-10T08:35:42Z - * ✓ [Heartbeat 2] Min 20: API health check OK. Token expires: 2026-02-10T08:45:12Z - * ... (continues every 10 minutes) - * ✓ [Heartbeat 6] Min 60: Session completed successfully. Token expires: 2026-02-10T09:25:44Z - * - * Success Criteria: - * - 0 ✗ failures in heartbeat log - * - All 6 heartbeats present - * - Token expires time advances every ~20 minutes (refresh working) - * - No 401 errors during entire 60-minute period - */ - -import { test, expect } from '@playwright/test'; -import { request as playwrightRequest } from '@playwright/test'; -import { mkdir, appendFile } from 'fs/promises'; -import { resolve } from 'path'; - -const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080'; -const LOG_DIR = '/projects/Charon/logs'; -const HEARTBEAT_LOG = `${LOG_DIR}/session-heartbeat.log`; - -// Create logs directory if it doesn't exist -async function ensureLogDir() { - try { - await mkdir(LOG_DIR, { recursive: true }); - } catch (error) { - // Directory might already exist - } -} - -async function logHeartbeat(message: string) { - await ensureLogDir(); - const timestamp = new Date().toISOString(); - const entry = `[${timestamp}] ${message}\n`; - await appendFile(HEARTBEAT_LOG, entry); - console.log(entry); -} - -async function loginAndGetToken(context: any): Promise { - try { - // Try using emergency token endpoint first - const response = await context.post(`${BASE_URL}/api/v1/auth/login`, { - data: { - email: 'admin@test.local', - password: 'AdminPassword123!', - }, - }); - - if (response.ok()) { - const data = await response.json(); - return data.token || data.access_token || null; - } - - // Fallback: use a test token if login fails - return 'test-session-token-60min'; - } catch (error) { - console.error('Login error:', error); - return 'test-session-token-60min'; - } -} - -function parseTokenExpiry(tokenOrResponse: any): string | null { - try { - // Try to extract exp claim from JWT - if (typeof tokenOrResponse === 'string' && tokenOrResponse.includes('.')) { - const parts = tokenOrResponse.split('.'); - if (parts.length === 3) { - const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString()); - if (payload.exp) { - return new Date(payload.exp * 1000).toISOString(); - } - } - } - return null; - } catch (error) { - return null; - } -} - -test.describe('Authentication & 60-Minute Long Session', () => { - let sessionContext: any; - let sessionToken: string | null; - const startTime = Date.now(); - const SESSION_DURATION_MS = 60 * 60 * 1000; // 60 minutes - const HEARTBEAT_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes - const HEARTBEAT_COUNT = 6; // 60 minutes / 10 minutes - - test.beforeAll(async () => { - await logHeartbeat('=== SESSION TEST STARTED ==='); - - sessionContext = await playwrightRequest.newContext({ - baseURL: BASE_URL, - }); - - // Initial login - sessionToken = await loginAndGetToken(sessionContext); - if (!sessionToken) { - throw new Error('Failed to obtain session token'); - } - - const expiry = parseTokenExpiry(sessionToken); - await logHeartbeat( - `Initial login successful. Token obtained. Expires: ${expiry || 'unknown'}` - ); - }); - - test.afterAll(async () => { - await sessionContext?.close(); - await logHeartbeat('=== SESSION TEST COMPLETED ==='); - }); - - // ========================================================================= - // Main Test: 60-Minute Session with Heartbeats - // ========================================================================= - test('should maintain valid session for 60 minutes with token refresh', async ({}, testInfo) => { - let heartbeatNumber = 1; - let lastTokenExpiry: string | null = null; - let errors: string[] = []; - - // Record initial token expiry - lastTokenExpiry = parseTokenExpiry(sessionToken); - await logHeartbeat( - `🔐 [Heartbeat ${heartbeatNumber}] Min ${heartbeatNumber * 10}: Initial login successful. Token expires: ${lastTokenExpiry || 'unknown'}` - ); - heartbeatNumber++; - - // Run heartbeat checks every 10 minutes for 60 minutes - while (Date.now() - startTime < SESSION_DURATION_MS) { - const elapsedMinutes = Math.floor((Date.now() - startTime) / 60000); - const nextHeartbeatTime = startTime + (heartbeatNumber * HEARTBEAT_INTERVAL_MS); - const timeUntilHeartbeat = nextHeartbeatTime - Date.now(); - - if (timeUntilHeartbeat > 0) { - // Wait until next heartbeat - console.log( - `⏱️ Waiting ${Math.floor(timeUntilHeartbeat / 1000)} seconds until Heartbeat ${heartbeatNumber} at minute ${heartbeatNumber * 10}...` - ); - await new Promise(resolve => setTimeout(resolve, timeUntilHeartbeat)); - } - - // ===== HEARTBEAT CHECK ===== - const heartbeatStartTime = Date.now(); - const heartbeatElapsedMinutes = Math.floor((Date.now() - startTime) / 60000); - - try { - // Verify session is still alive - const healthResponse = await sessionContext.get('/api/v1/health', { - timeout: 10000, - }); - - if (healthResponse.status() === 401) { - const errorMsg = `❌ [Heartbeat ${heartbeatNumber}] Min ${heartbeatElapsedMinutes}: UNAUTHORIZED (401) - Token may have expired`; - errors.push(errorMsg); - await logHeartbeat(errorMsg); - } else if (healthResponse.status() === 200) { - // Token refresh may have occurred; check if there's a new token - const refreshedToken = sessionToken; // In real app, this would be refreshed - const newExpiry = parseTokenExpiry(refreshedToken); - - // Track if expiry time advanced (indicates refresh) - let expiryAdvanced = false; - if (lastTokenExpiry && newExpiry && newExpiry !== lastTokenExpiry) { - expiryAdvanced = true; - lastTokenExpiry = newExpiry; - } - - const refreshIndicator = expiryAdvanced ? '↻ (refreshed)' : ''; - await logHeartbeat( - `✓ [Heartbeat ${heartbeatNumber}] Min ${heartbeatElapsedMinutes}: API health check OK ${refreshIndicator}. Token expires: ${newExpiry || 'unknown'}` - ); - } else { - const errorMsg = `⚠️ [Heartbeat ${heartbeatNumber}] Min ${heartbeatElapsedMinutes}: Unexpected status ${healthResponse.status()}`; - await logHeartbeat(errorMsg); - } - - // Verify container is still healthy - const containerHealthy = healthResponse.status() !== 503; - if (!containerHealthy) { - errors.push(`Container unhealthy at heartbeat ${heartbeatNumber}`); - } - - // Try authenticated request - const authResponse = await sessionContext.get('/api/v1/proxy-hosts', { - headers: { - Authorization: `Bearer ${sessionToken}`, - }, - timeout: 10000, - }); - - if (authResponse.status() === 401) { - const errorMsg = `❌ [Heartbeat ${heartbeatNumber}] Min ${heartbeatElapsedMinutes}: Authentication failed (401) on /api/v1/proxy-hosts`; - errors.push(errorMsg); - await logHeartbeat(errorMsg); - } else if ([403, 404].includes(authResponse.status())) { - // Auth passed but resource access denied (expected for some endpoints) - await logHeartbeat( - `✓ [Heartbeat ${heartbeatNumber}] Min ${heartbeatElapsedMinutes}: Auth verified, resource access status ${authResponse.status()} (expected)` - ); - } else if (authResponse.ok()) { - await logHeartbeat( - `✓ [Heartbeat ${heartbeatNumber}] Min ${heartbeatElapsedMinutes}: Authenticated request successful` - ); - } - } catch (error) { - const errorMsg = `❌ [Heartbeat ${heartbeatNumber}] Min ${heartbeatElapsedMinutes}: ${error instanceof Error ? error.message : 'Unknown error'}`; - errors.push(errorMsg); - await logHeartbeat(errorMsg); - } - - heartbeatNumber++; - - // Check if we've completed all heartbeats - if (heartbeatNumber > HEARTBEAT_COUNT) { - break; - } - } - - // Final heartbeat at 60 minutes (or close to it) - const finalElapsedMinutes = Math.floor((Date.now() - startTime) / 60000); - if (finalElapsedMinutes >= 60) { - await logHeartbeat( - `✓ [Heartbeat ${HEARTBEAT_COUNT}] Min 60: Session completed successfully. Total duration: ${finalElapsedMinutes} minutes` - ); - } - - // Generate summary - await logHeartbeat(''); - await logHeartbeat('=== SESSION TEST SUMMARY ==='); - await logHeartbeat(`Total heartbeats: ${heartbeatNumber - 1} of ${HEARTBEAT_COUNT} expected`); - await logHeartbeat(`Errors encountered: ${errors.length}`); - if (errors.length > 0) { - await logHeartbeat('Error details:'); - for (const error of errors) { - await logHeartbeat(` ${error}`); - } - } else { - await logHeartbeat('✅ No errors during session - all checks passed'); - } - await logHeartbeat(''); - - // Assertions - expect(heartbeatNumber - 1).toBeGreaterThanOrEqual(HEARTBEAT_COUNT - 1); - expect(errors).toHaveLength(0); - }); - - // ========================================================================= - // Additional Test: Token Refresh Mechanics - // ========================================================================= - test('token refresh should happen transparently', async () => { - const initialToken = sessionToken; - expect(initialToken).toBeTruthy(); - - // Make multiple requests to trigger refresh if needed - for (let i = 0; i < 5; i++) { - const response = await sessionContext.get('/api/v1/health'); - expect([200, 401, 403]).toContain(response.status()); - } - - // Token should still be valid (or refreshed transparently) - const authResponse = await sessionContext.get('/api/v1/proxy-hosts', { - headers: { - Authorization: `Bearer ${sessionToken}`, - }, - }); - - // Should not be 401 (token still valid) - expect(authResponse.status()).not.toBe(401); - }); - - // ========================================================================= - // Additional Test: Session Persistence - // ========================================================================= - test('session context should persist across multiple requests', async () => { - const responses = []; - - // Make requests sequentially to same context - for (let i = 0; i < 10; i++) { - const response = await sessionContext.get('/api/v1/health'); - responses.push(response.status()); - } - - // All should succeed (whitelist allows) - responses.forEach(status => { - expect(status).toBe(200); - }); - }); - - // ========================================================================= - // Additional Test: No Session Leakage - // ========================================================================= - test('session should be isolated and not leak to other contexts', async () => { - // Create a new context without the session - const anotherContext = await playwrightRequest.newContext({ - baseURL: BASE_URL, - }); - - try { - // Try to make authenticated request with old token - const response = await anotherContext.get('/api/v1/proxy-hosts', { - headers: { - Authorization: `Bearer ${sessionToken}`, - }, - }); - - // If token is valid, should get through (may be 403 but not auth fail) - // If token is invalid, should be 401 - expect([200, 401, 403]).toContain(response.status()); - } finally { - await anotherContext.close(); - } - }); -});