import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; import { CheckAuthenticationUseCase } from '../../../../packages/application/use-cases/CheckAuthenticationUseCase'; import { AuthenticationState } from '../../../../packages/domain/value-objects/AuthenticationState'; import { BrowserAuthenticationState } from '../../../../packages/domain/value-objects/BrowserAuthenticationState'; import { Result } from '../../../../packages/shared/result/Result'; import type { IAuthenticationService } from '../../../../packages/application/ports/IAuthenticationService'; interface ISessionValidator { validateSession(): Promise>; } describe('CheckAuthenticationUseCase', () => { let mockAuthService: { checkSession: Mock; initiateLogin: Mock; clearSession: Mock; getState: Mock; validateServerSide: Mock; refreshSession: Mock; getSessionExpiry: Mock; verifyPageAuthentication: Mock; }; let mockSessionValidator: { validateSession: Mock; }; beforeEach(() => { mockAuthService = { checkSession: vi.fn(), initiateLogin: vi.fn(), clearSession: vi.fn(), getState: vi.fn(), validateServerSide: vi.fn(), refreshSession: vi.fn(), getSessionExpiry: vi.fn(), verifyPageAuthentication: vi.fn(), }; mockSessionValidator = { validateSession: vi.fn(), }; }); describe('File-based validation only', () => { it('should return AUTHENTICATED when cookies are valid', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.AUTHENTICATED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() + 3600000)) ); const result = await useCase.execute(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBe(AuthenticationState.AUTHENTICATED); expect(mockAuthService.checkSession).toHaveBeenCalledTimes(1); }); it('should return EXPIRED when cookies are expired', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.EXPIRED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() - 3600000)) ); const result = await useCase.execute(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBe(AuthenticationState.EXPIRED); }); it('should return UNKNOWN when no session exists', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.UNKNOWN) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(null) ); const result = await useCase.execute(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBe(AuthenticationState.UNKNOWN); }); }); describe('Server-side validation enabled', () => { it('should confirm AUTHENTICATED when file and server both validate', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService, mockSessionValidator as unknown as ISessionValidator ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.AUTHENTICATED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() + 3600000)) ); mockSessionValidator.validateSession.mockResolvedValue( Result.ok(true) ); const result = await useCase.execute(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBe(AuthenticationState.AUTHENTICATED); expect(mockSessionValidator.validateSession).toHaveBeenCalledTimes(1); }); it('should return EXPIRED when file says valid but server rejects', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService, mockSessionValidator as unknown as ISessionValidator ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.AUTHENTICATED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() + 3600000)) ); mockSessionValidator.validateSession.mockResolvedValue( Result.ok(false) ); const result = await useCase.execute(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBe(AuthenticationState.EXPIRED); }); it('should work without ISessionValidator injected (optional dependency)', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.AUTHENTICATED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() + 3600000)) ); const result = await useCase.execute(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBe(AuthenticationState.AUTHENTICATED); }); }); describe('Error handling', () => { it('should not block file-based result if server validation fails', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService, mockSessionValidator as unknown as ISessionValidator ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.AUTHENTICATED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() + 3600000)) ); mockSessionValidator.validateSession.mockResolvedValue( Result.err('Network error') ); const result = await useCase.execute(); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBe(AuthenticationState.AUTHENTICATED); }); it('should handle authentication service errors gracefully', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.err('File read error') ); const result = await useCase.execute(); expect(result.isErr()).toBe(true); expect(result.unwrapErr()).toContain('File read error'); }); it('should handle session expiry check errors gracefully', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.AUTHENTICATED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.err('Invalid session format') ); const result = await useCase.execute(); // Should not block on expiry check errors, return file-based state expect(result.isOk()).toBe(true); expect(result.unwrap()).toBe(AuthenticationState.AUTHENTICATED); }); }); describe('Page content verification', () => { it('should call verifyPageAuthentication when verifyPageContent is true', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.AUTHENTICATED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() + 3600000)) ); (mockAuthService as any).verifyPageAuthentication = vi.fn().mockResolvedValue( Result.ok(new BrowserAuthenticationState(true, true)) ); await useCase.execute({ verifyPageContent: true }); expect((mockAuthService as any).verifyPageAuthentication).toHaveBeenCalledTimes(1); }); it('should return EXPIRED when cookies valid but page shows login UI', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.AUTHENTICATED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() + 3600000)) ); (mockAuthService as any).verifyPageAuthentication = vi.fn().mockResolvedValue( Result.ok(new BrowserAuthenticationState(true, false)) ); const result = await useCase.execute({ verifyPageContent: true }); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBe(AuthenticationState.EXPIRED); }); it('should return AUTHENTICATED when both cookies AND page authenticated', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.AUTHENTICATED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() + 3600000)) ); (mockAuthService as any).verifyPageAuthentication = vi.fn().mockResolvedValue( Result.ok(new BrowserAuthenticationState(true, true)) ); const result = await useCase.execute({ verifyPageContent: true }); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBe(AuthenticationState.AUTHENTICATED); }); it('should default verifyPageContent to false (backward compatible)', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.AUTHENTICATED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() + 3600000)) ); (mockAuthService as any).verifyPageAuthentication = vi.fn(); await useCase.execute(); expect((mockAuthService as any).verifyPageAuthentication).not.toHaveBeenCalled(); }); it('should handle verifyPageAuthentication errors gracefully', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.AUTHENTICATED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() + 3600000)) ); (mockAuthService as any).verifyPageAuthentication = vi.fn().mockResolvedValue( Result.err('Page navigation failed') ); const result = await useCase.execute({ verifyPageContent: true }); // Should not block on page verification errors, return cookie-based state expect(result.isOk()).toBe(true); expect(result.unwrap()).toBe(AuthenticationState.AUTHENTICATED); }); }); describe('BDD Scenarios', () => { it('Given valid session cookies, When checking auth, Then return AUTHENTICATED', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.AUTHENTICATED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() + 7200000)) ); const result = await useCase.execute(); expect(result.unwrap()).toBe(AuthenticationState.AUTHENTICATED); }); it('Given expired session cookies, When checking auth, Then return EXPIRED', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.EXPIRED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() - 1000)) ); const result = await useCase.execute(); expect(result.unwrap()).toBe(AuthenticationState.EXPIRED); }); it('Given no session file, When checking auth, Then return UNKNOWN', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.UNKNOWN) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(null) ); const result = await useCase.execute(); expect(result.unwrap()).toBe(AuthenticationState.UNKNOWN); }); it('Given valid cookies but page shows login, When verifying page content, Then return EXPIRED', async () => { const useCase = new CheckAuthenticationUseCase( mockAuthService as unknown as IAuthenticationService ); mockAuthService.checkSession.mockResolvedValue( Result.ok(AuthenticationState.AUTHENTICATED) ); mockAuthService.getSessionExpiry.mockResolvedValue( Result.ok(new Date(Date.now() + 3600000)) ); (mockAuthService as any).verifyPageAuthentication = vi.fn().mockResolvedValue( Result.ok(new BrowserAuthenticationState(true, false)) ); const result = await useCase.execute({ verifyPageContent: true }); expect(result.unwrap()).toBe(AuthenticationState.EXPIRED); }); }); });