import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { promises as fs } from 'fs'; import * as path from 'path'; import { CheckAuthenticationUseCase } from '../../../packages/application/use-cases/CheckAuthenticationUseCase'; import { AuthenticationState } from '../../../packages/domain/value-objects/AuthenticationState'; import { Result } from '../../../packages/shared/result/Result'; import { PlaywrightAutomationAdapter } from 'packages/infrastructure/adapters/automation'; const TEST_USER_DATA_DIR = path.join(__dirname, '../../../test-browser-data'); const SESSION_FILE_PATH = path.join(TEST_USER_DATA_DIR, 'session-state.json'); interface SessionData { cookies: Array<{ name: string; value: string; domain: string; path: string; expires: number }>; expiry: string | null; } describe('Session Validation After Startup', () => { beforeEach(async () => { // Ensure test directory exists try { await fs.mkdir(TEST_USER_DATA_DIR, { recursive: true }); } catch { // Directory already exists } // Clean up session file if it exists try { await fs.unlink(SESSION_FILE_PATH); } catch { // File doesn't exist, that's fine } }); afterEach(async () => { try { await fs.unlink(SESSION_FILE_PATH); } catch { // Cleanup best effort } }); describe('Initial check on app startup', () => { it('should detect valid session on startup', async () => { const validSessionData: SessionData = { cookies: [ { name: 'irsso_membersv2', value: 'valid-token', domain: '.iracing.com', path: '/', expires: Date.now() + 3600000, }, ], expiry: new Date(Date.now() + 3600000).toISOString(), }; await fs.writeFile(SESSION_FILE_PATH, JSON.stringify(validSessionData, null, 2)); const authService = createRealAuthenticationService(); const useCase = new CheckAuthenticationUseCase(authService); const result = await useCase.execute(); expect(result.isOk()).toBe(true); expect(result.value).toBe(AuthenticationState.AUTHENTICATED); }); it('should detect expired session on startup', async () => { const expiredSessionData: SessionData = { cookies: [ { name: 'irsso_membersv2', value: 'expired-token', domain: '.iracing.com', path: '/', expires: Date.now() - 3600000, }, ], expiry: new Date(Date.now() - 3600000).toISOString(), }; await fs.writeFile(SESSION_FILE_PATH, JSON.stringify(expiredSessionData, null, 2)); const authService = createRealAuthenticationService(); const useCase = new CheckAuthenticationUseCase(authService); const result = await useCase.execute(); expect(result.isOk()).toBe(true); expect(result.value).toBe(AuthenticationState.EXPIRED); }); it('should handle missing session file on startup', async () => { const authService = createRealAuthenticationService(); const useCase = new CheckAuthenticationUseCase(authService); const result = await useCase.execute(); expect(result.isOk()).toBe(true); expect(result.value).toBe(AuthenticationState.UNKNOWN); }); }); describe('Session expiry during runtime', () => { it('should transition from AUTHENTICATED to EXPIRED after time passes', async () => { // Start with a session that expires in 10 minutes (beyond 5-minute buffer) const initialExpiry = Date.now() + (10 * 60 * 1000); const shortLivedSessionData: SessionData = { cookies: [ { name: 'irsso_membersv2', value: 'short-lived-token', domain: '.iracing.com', path: '/', expires: initialExpiry, }, ], expiry: new Date(initialExpiry).toISOString(), }; await fs.writeFile(SESSION_FILE_PATH, JSON.stringify(shortLivedSessionData, null, 2)); const authService = createRealAuthenticationService(); const useCase = new CheckAuthenticationUseCase(authService); const firstCheck = await useCase.execute(); expect(firstCheck.value).toBe(AuthenticationState.AUTHENTICATED); // Now update the session file to have an expiry in the past const expiredSessionData: SessionData = { cookies: [ { name: 'irsso_membersv2', value: 'short-lived-token', domain: '.iracing.com', path: '/', expires: Date.now() - 1000, }, ], expiry: new Date(Date.now() - 1000).toISOString(), }; await fs.writeFile(SESSION_FILE_PATH, JSON.stringify(expiredSessionData, null, 2)); const secondCheck = await useCase.execute(); expect(secondCheck.value).toBe(AuthenticationState.EXPIRED); }); it('should maintain AUTHENTICATED state when session is still valid', async () => { const longLivedSessionData: SessionData = { cookies: [ { name: 'irsso_membersv2', value: 'long-lived-token', domain: '.iracing.com', path: '/', expires: Date.now() + 3600000, }, ], expiry: new Date(Date.now() + 3600000).toISOString(), }; await fs.writeFile(SESSION_FILE_PATH, JSON.stringify(longLivedSessionData, null, 2)); const authService = createRealAuthenticationService(); const useCase = new CheckAuthenticationUseCase(authService); const firstCheck = await useCase.execute(); expect(firstCheck.value).toBe(AuthenticationState.AUTHENTICATED); await new Promise(resolve => setTimeout(resolve, 100)); const secondCheck = await useCase.execute(); expect(secondCheck.value).toBe(AuthenticationState.AUTHENTICATED); }); }); describe('Browser connection before auth check', () => { it('should establish browser connection then validate auth', async () => { const validSessionData: SessionData = { cookies: [ { name: 'irsso_membersv2', value: 'valid-token', domain: '.iracing.com', path: '/', expires: Date.now() + 3600000, }, ], expiry: new Date(Date.now() + 3600000).toISOString(), }; await fs.writeFile(SESSION_FILE_PATH, JSON.stringify(validSessionData, null, 2)); const browserAdapter = createMockBrowserAdapter(); await browserAdapter.initialize(); const authService = createRealAuthenticationService(); const useCase = new CheckAuthenticationUseCase(authService); const result = await useCase.execute(); expect(browserAdapter.isInitialized()).toBe(true); expect(result.value).toBe(AuthenticationState.AUTHENTICATED); }); it('should handle auth check when browser connection fails', async () => { const validSessionData: SessionData = { cookies: [ { name: 'irsso_membersv2', value: 'valid-token', domain: '.iracing.com', path: '/', expires: Date.now() + 3600000, }, ], expiry: new Date(Date.now() + 3600000).toISOString(), }; await fs.writeFile(SESSION_FILE_PATH, JSON.stringify(validSessionData, null, 2)); const browserAdapter = createMockBrowserAdapter(); browserAdapter.setConnectionFailure(true); const authService = createRealAuthenticationService(); const useCase = new CheckAuthenticationUseCase(authService); const result = await useCase.execute(); expect(result.value).toBe(AuthenticationState.AUTHENTICATED); }); }); describe('Authentication detection logic', () => { it('should consider page authenticated when both hasAuthUI=true AND hasLoginUI=true', async () => { // This tests the core bug: when authenticated UI is detected alongside login UI, // authentication should be considered VALID because authenticated UI takes precedence // Mock scenario: Dashboard visible (authenticated) but profile menu contains "Log in" text const mockAdapter = { page: { locator: vi.fn(), }, logger: undefined, }; // Setup: Both authenticated UI and login UI detected let callCount = 0; mockAdapter.page.locator.mockImplementation((selector: string) => { callCount++; // First call: checkForLoginUI - 'text="You are not logged in"' if (callCount === 1) { return { first: () => ({ isVisible: () => Promise.resolve(false), }), }; } // Second call: checkForLoginUI - 'button:has-text("Log in")' if (callCount === 2) { return { first: () => ({ isVisible: () => Promise.resolve(true), // FALSE POSITIVE from profile menu }), }; } // Third call: authenticated UI - 'button:has-text("Create a Race")' if (callCount === 3) { return { first: () => ({ isVisible: () => Promise.resolve(true), // Authenticated UI detected }), }; } return { first: () => ({ isVisible: () => Promise.resolve(false), }), }; }) as any; // Simulate the logic from PlaywrightAutomationAdapter.verifyPageAuthentication const hasLoginUI = true; // False positive from profile menu const hasAuthUI = true; // Real authenticated UI detected // CURRENT BUGGY LOGIC: const pageAuthenticated = !hasLoginUI && (hasAuthUI || !hasLoginUI); const currentLogic = !hasLoginUI && (hasAuthUI || !hasLoginUI); // EXPECTED CORRECT LOGIC: const pageAuthenticated = hasAuthUI || !hasLoginUI; const correctLogic = hasAuthUI || !hasLoginUI; expect(currentLogic).toBe(false); // Current buggy behavior expect(correctLogic).toBe(true); // Expected correct behavior }); it('should consider page authenticated when hasAuthUI=true even if hasLoginUI=true', async () => { // When authenticated UI is present, it should override any login UI detection const hasLoginUI = true; const hasAuthUI = true; // Buggy logic const pageAuthenticated = !hasLoginUI && (hasAuthUI || !hasLoginUI); // This fails: even though authenticated UI is detected, the result is false // because hasLoginUI=true makes the first condition fail expect(pageAuthenticated).toBe(false); // BUG: Should be true }); it('should consider page authenticated when hasAuthUI=true and hasLoginUI=false', async () => { // When authenticated UI is present and no login UI, clearly authenticated const hasLoginUI = false; const hasAuthUI = true; const pageAuthenticated = !hasLoginUI && (hasAuthUI || !hasLoginUI); expect(pageAuthenticated).toBe(true); // This works correctly }); it('should consider page authenticated when hasAuthUI=false and hasLoginUI=false', async () => { // No login UI and no explicit auth UI - assume authenticated (no login required) const hasLoginUI = false; const hasAuthUI = false; const pageAuthenticated = !hasLoginUI && (hasAuthUI || !hasLoginUI); expect(pageAuthenticated).toBe(true); // This works correctly }); it('should consider page unauthenticated when hasAuthUI=false and hasLoginUI=true', async () => { // Clear login UI with no authenticated UI - definitely not authenticated const hasLoginUI = true; const hasAuthUI = false; const pageAuthenticated = !hasLoginUI && (hasAuthUI || !hasLoginUI); expect(pageAuthenticated).toBe(false); // This works correctly }); }); describe('BDD Scenarios', () => { it('Scenario: App starts with valid session', async () => { const validSessionData: SessionData = { cookies: [ { name: 'irsso_membersv2', value: 'valid-session-token', domain: '.iracing.com', path: '/', expires: Date.now() + 7200000, }, ], expiry: new Date(Date.now() + 7200000).toISOString(), }; await fs.writeFile(SESSION_FILE_PATH, JSON.stringify(validSessionData, null, 2)); const authService = createRealAuthenticationService(); const useCase = new CheckAuthenticationUseCase(authService); const result = await useCase.execute(); expect(result.value).toBe(AuthenticationState.AUTHENTICATED); }); it('Scenario: App starts with expired session', async () => { const expiredSessionData: SessionData = { cookies: [ { name: 'irsso_membersv2', value: 'expired-session-token', domain: '.iracing.com', path: '/', expires: Date.now() - 7200000, }, ], expiry: new Date(Date.now() - 7200000).toISOString(), }; await fs.writeFile(SESSION_FILE_PATH, JSON.stringify(expiredSessionData, null, 2)); const authService = createRealAuthenticationService(); const useCase = new CheckAuthenticationUseCase(authService); const result = await useCase.execute(); expect(result.value).toBe(AuthenticationState.EXPIRED); }); it('Scenario: App starts without session', async () => { const authService = createRealAuthenticationService(); const useCase = new CheckAuthenticationUseCase(authService); const result = await useCase.execute(); expect(result.value).toBe(AuthenticationState.UNKNOWN); }); }); }); function createRealAuthenticationService() { // Create adapter with test-specific user data directory const adapter = new PlaywrightAutomationAdapter({ headless: true, timeout: 5000, mode: 'real', userDataDir: TEST_USER_DATA_DIR, }); return adapter; } function createMockBrowserAdapter() { // Simple mock that tracks initialization state let initialized = false; let shouldFailConnection = false; return { initialize: async () => { if (shouldFailConnection) { throw new Error('Mock connection failure'); } initialized = true; }, isInitialized: () => initialized, setConnectionFailure: (fail: boolean) => { shouldFailConnection = fail; }, }; }