wip
This commit is contained in:
180
tests/unit/application/ports/ICheckoutConfirmationPort.test.ts
Normal file
180
tests/unit/application/ports/ICheckoutConfirmationPort.test.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Result } from '@/packages/shared/result/Result';
|
||||
import { CheckoutConfirmation } from '@/packages/domain/value-objects/CheckoutConfirmation';
|
||||
import { CheckoutPrice } from '@/packages/domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '@/packages/domain/value-objects/CheckoutState';
|
||||
|
||||
/**
|
||||
* Contract tests for ICheckoutConfirmationPort
|
||||
*
|
||||
* Any implementation must:
|
||||
* 1. Accept CheckoutConfirmationRequest with price, state, sessionMetadata, timeoutMs
|
||||
* 2. Return Result<CheckoutConfirmation> with decision: confirmed, cancelled, or timeout
|
||||
* 3. Handle timeout gracefully by returning timeout decision
|
||||
* 4. Validate request parameters before processing
|
||||
*/
|
||||
|
||||
export interface CheckoutConfirmationRequest {
|
||||
price: CheckoutPrice;
|
||||
state: CheckoutState;
|
||||
sessionMetadata: {
|
||||
sessionName: string;
|
||||
trackId: string;
|
||||
carIds: string[];
|
||||
};
|
||||
timeoutMs: number;
|
||||
}
|
||||
|
||||
export interface ICheckoutConfirmationPort {
|
||||
requestCheckoutConfirmation(
|
||||
request: CheckoutConfirmationRequest
|
||||
): Promise<Result<CheckoutConfirmation>>;
|
||||
}
|
||||
|
||||
describe('ICheckoutConfirmationPort contract', () => {
|
||||
it('should define the required interface structure', () => {
|
||||
// This test verifies the port interface contract exists
|
||||
const mockPort: ICheckoutConfirmationPort = {
|
||||
requestCheckoutConfirmation: async (_request: CheckoutConfirmationRequest) => {
|
||||
return Result.ok(CheckoutConfirmation.create('confirmed'));
|
||||
},
|
||||
};
|
||||
|
||||
expect(mockPort.requestCheckoutConfirmation).toBeDefined();
|
||||
expect(typeof mockPort.requestCheckoutConfirmation).toBe('function');
|
||||
});
|
||||
|
||||
it('should accept valid CheckoutConfirmationRequest', async () => {
|
||||
const mockPort: ICheckoutConfirmationPort = {
|
||||
requestCheckoutConfirmation: async (request: CheckoutConfirmationRequest) => {
|
||||
expect(request.price).toBeInstanceOf(CheckoutPrice);
|
||||
expect(request.state).toBeInstanceOf(CheckoutState);
|
||||
expect(request.sessionMetadata).toBeDefined();
|
||||
expect(request.sessionMetadata.sessionName).toBeTruthy();
|
||||
expect(request.sessionMetadata.trackId).toBeTruthy();
|
||||
expect(Array.isArray(request.sessionMetadata.carIds)).toBe(true);
|
||||
expect(request.timeoutMs).toBeGreaterThan(0);
|
||||
return Result.ok(CheckoutConfirmation.create('confirmed'));
|
||||
},
|
||||
};
|
||||
|
||||
const request: CheckoutConfirmationRequest = {
|
||||
price: CheckoutPrice.fromString('$10.00'),
|
||||
state: CheckoutState.ready(),
|
||||
sessionMetadata: {
|
||||
sessionName: 'Test Session',
|
||||
trackId: 'spa',
|
||||
carIds: ['car1', 'car2'],
|
||||
},
|
||||
timeoutMs: 30000,
|
||||
};
|
||||
|
||||
const result = await mockPort.requestCheckoutConfirmation(request);
|
||||
expect(result.isOk()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return Result with CheckoutConfirmation on success', async () => {
|
||||
const mockPort: ICheckoutConfirmationPort = {
|
||||
requestCheckoutConfirmation: async () => {
|
||||
return Result.ok(CheckoutConfirmation.create('confirmed'));
|
||||
},
|
||||
};
|
||||
|
||||
const request: CheckoutConfirmationRequest = {
|
||||
price: CheckoutPrice.fromString('$10.00'),
|
||||
state: CheckoutState.ready(),
|
||||
sessionMetadata: {
|
||||
sessionName: 'Test Session',
|
||||
trackId: 'spa',
|
||||
carIds: ['car1'],
|
||||
},
|
||||
timeoutMs: 30000,
|
||||
};
|
||||
|
||||
const result = await mockPort.requestCheckoutConfirmation(request);
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
const confirmation = result.unwrap();
|
||||
expect(confirmation).toBeInstanceOf(CheckoutConfirmation);
|
||||
expect(confirmation.isConfirmed()).toBe(true);
|
||||
});
|
||||
|
||||
it('should support cancelled decision', async () => {
|
||||
const mockPort: ICheckoutConfirmationPort = {
|
||||
requestCheckoutConfirmation: async () => {
|
||||
return Result.ok(CheckoutConfirmation.create('cancelled'));
|
||||
},
|
||||
};
|
||||
|
||||
const request: CheckoutConfirmationRequest = {
|
||||
price: CheckoutPrice.fromString('$10.00'),
|
||||
state: CheckoutState.ready(),
|
||||
sessionMetadata: {
|
||||
sessionName: 'Test Session',
|
||||
trackId: 'spa',
|
||||
carIds: ['car1'],
|
||||
},
|
||||
timeoutMs: 30000,
|
||||
};
|
||||
|
||||
const result = await mockPort.requestCheckoutConfirmation(request);
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
const confirmation = result.unwrap();
|
||||
expect(confirmation.isCancelled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should support timeout decision', async () => {
|
||||
const mockPort: ICheckoutConfirmationPort = {
|
||||
requestCheckoutConfirmation: async () => {
|
||||
return Result.ok(CheckoutConfirmation.create('timeout'));
|
||||
},
|
||||
};
|
||||
|
||||
const request: CheckoutConfirmationRequest = {
|
||||
price: CheckoutPrice.fromString('$10.00'),
|
||||
state: CheckoutState.ready(),
|
||||
sessionMetadata: {
|
||||
sessionName: 'Test Session',
|
||||
trackId: 'spa',
|
||||
carIds: ['car1'],
|
||||
},
|
||||
timeoutMs: 1000,
|
||||
};
|
||||
|
||||
const result = await mockPort.requestCheckoutConfirmation(request);
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
const confirmation = result.unwrap();
|
||||
expect(confirmation.isTimeout()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return error Result for invalid requests', async () => {
|
||||
const mockPort: ICheckoutConfirmationPort = {
|
||||
requestCheckoutConfirmation: async (request: CheckoutConfirmationRequest) => {
|
||||
if (request.timeoutMs <= 0) {
|
||||
return Result.err(new Error('Timeout must be positive'));
|
||||
}
|
||||
if (!request.sessionMetadata.sessionName) {
|
||||
return Result.err(new Error('Session name is required'));
|
||||
}
|
||||
return Result.ok(CheckoutConfirmation.create('confirmed'));
|
||||
},
|
||||
};
|
||||
|
||||
const invalidRequest: CheckoutConfirmationRequest = {
|
||||
price: CheckoutPrice.fromString('$10.00'),
|
||||
state: CheckoutState.ready(),
|
||||
sessionMetadata: {
|
||||
sessionName: '',
|
||||
trackId: 'spa',
|
||||
carIds: ['car1'],
|
||||
},
|
||||
timeoutMs: 30000,
|
||||
};
|
||||
|
||||
const result = await mockPort.requestCheckoutConfirmation(invalidRequest);
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('Session name');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,407 @@
|
||||
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';
|
||||
|
||||
interface IAuthenticationService {
|
||||
checkSession(): Promise<Result<AuthenticationState>>;
|
||||
initiateLogin(): Promise<Result<void>>;
|
||||
clearSession(): Promise<Result<void>>;
|
||||
getState(): AuthenticationState;
|
||||
validateServerSide(): Promise<Result<boolean>>;
|
||||
refreshSession(): Promise<Result<void>>;
|
||||
getSessionExpiry(): Promise<Result<Date | null>>;
|
||||
}
|
||||
|
||||
interface ISessionValidator {
|
||||
validateSession(): Promise<Result<boolean>>;
|
||||
}
|
||||
|
||||
describe('CheckAuthenticationUseCase', () => {
|
||||
let mockAuthService: {
|
||||
checkSession: Mock;
|
||||
initiateLogin: Mock;
|
||||
clearSession: Mock;
|
||||
getState: Mock;
|
||||
validateServerSide: Mock;
|
||||
refreshSession: Mock;
|
||||
getSessionExpiry: 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(),
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { CompleteRaceCreationUseCase } from '@/packages/application/use-cases/CompleteRaceCreationUseCase';
|
||||
import { Result } from '@/packages/shared/result/Result';
|
||||
import { RaceCreationResult } from '@/packages/domain/value-objects/RaceCreationResult';
|
||||
import { CheckoutPrice } from '@/packages/domain/value-objects/CheckoutPrice';
|
||||
import type { ICheckoutService } from '@/packages/application/ports/ICheckoutService';
|
||||
import { CheckoutState } from '@/packages/domain/value-objects/CheckoutState';
|
||||
|
||||
describe('CompleteRaceCreationUseCase', () => {
|
||||
let mockCheckoutService: ICheckoutService;
|
||||
let useCase: CompleteRaceCreationUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCheckoutService = {
|
||||
extractCheckoutInfo: vi.fn(),
|
||||
proceedWithCheckout: vi.fn(),
|
||||
};
|
||||
|
||||
useCase = new CompleteRaceCreationUseCase(mockCheckoutService);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should extract checkout price and create RaceCreationResult', async () => {
|
||||
const price = CheckoutPrice.fromString('$25.50');
|
||||
const state = CheckoutState.ready();
|
||||
const sessionId = 'test-session-123';
|
||||
|
||||
vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue(
|
||||
Result.ok({ price, state })
|
||||
);
|
||||
|
||||
const result = await useCase.execute(sessionId);
|
||||
|
||||
expect(mockCheckoutService.extractCheckoutInfo).toHaveBeenCalled();
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
const raceCreationResult = result.unwrap();
|
||||
expect(raceCreationResult).toBeInstanceOf(RaceCreationResult);
|
||||
expect(raceCreationResult.sessionId).toBe(sessionId);
|
||||
expect(raceCreationResult.price).toBe('$25.50');
|
||||
expect(raceCreationResult.timestamp).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should return error if checkout info extraction fails', async () => {
|
||||
vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue(
|
||||
Result.err(new Error('Failed to extract checkout info'))
|
||||
);
|
||||
|
||||
const result = await useCase.execute('test-session-123');
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('Failed to extract checkout info');
|
||||
});
|
||||
|
||||
it('should return error if price is missing', async () => {
|
||||
const state = CheckoutState.ready();
|
||||
|
||||
vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue(
|
||||
Result.ok({ price: undefined as any, state })
|
||||
);
|
||||
|
||||
const result = await useCase.execute('test-session-123');
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('Could not extract price');
|
||||
});
|
||||
|
||||
it('should validate session ID is provided', async () => {
|
||||
const result = await useCase.execute('');
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('Session ID is required');
|
||||
});
|
||||
|
||||
it('should format different price values correctly', async () => {
|
||||
const testCases = [
|
||||
{ input: '$10.00', expected: '$10.00' },
|
||||
{ input: '$100.50', expected: '$100.50' },
|
||||
{ input: '$0.99', expected: '$0.99' },
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const price = CheckoutPrice.fromString(testCase.input);
|
||||
const state = CheckoutState.ready();
|
||||
|
||||
vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue(
|
||||
Result.ok({ price, state })
|
||||
);
|
||||
|
||||
const result = await useCase.execute('test-session');
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
const raceCreationResult = result.unwrap();
|
||||
expect(raceCreationResult.price).toBe(testCase.expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('should capture current timestamp when creating result', async () => {
|
||||
const price = CheckoutPrice.fromString('$25.50');
|
||||
const state = CheckoutState.ready();
|
||||
const beforeExecution = new Date();
|
||||
|
||||
vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue(
|
||||
Result.ok({ price, state })
|
||||
);
|
||||
|
||||
const result = await useCase.execute('test-session');
|
||||
const afterExecution = new Date();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const raceCreationResult = result.unwrap();
|
||||
|
||||
expect(raceCreationResult.timestamp.getTime()).toBeGreaterThanOrEqual(
|
||||
beforeExecution.getTime()
|
||||
);
|
||||
expect(raceCreationResult.timestamp.getTime()).toBeLessThanOrEqual(
|
||||
afterExecution.getTime()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ConfirmCheckoutUseCase } from '@/packages/application/use-cases/ConfirmCheckoutUseCase';
|
||||
import { Result } from '@/packages/shared/result/Result';
|
||||
import { CheckoutPrice } from '@/packages/domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '@/packages/domain/value-objects/CheckoutState';
|
||||
import { CheckoutConfirmation } from '@/packages/domain/value-objects/CheckoutConfirmation';
|
||||
import type { ICheckoutService } from '@/packages/application/ports/ICheckoutService';
|
||||
import type { ICheckoutConfirmationPort } from '@/packages/application/ports/ICheckoutConfirmationPort';
|
||||
|
||||
describe('ConfirmCheckoutUseCase - Enhanced with Confirmation Port', () => {
|
||||
let mockCheckoutService: ICheckoutService;
|
||||
let mockConfirmationPort: ICheckoutConfirmationPort;
|
||||
let useCase: ConfirmCheckoutUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCheckoutService = {
|
||||
extractCheckoutInfo: vi.fn(),
|
||||
proceedWithCheckout: vi.fn(),
|
||||
};
|
||||
|
||||
mockConfirmationPort = {
|
||||
requestCheckoutConfirmation: vi.fn(),
|
||||
};
|
||||
|
||||
useCase = new ConfirmCheckoutUseCase(mockCheckoutService, mockConfirmationPort);
|
||||
});
|
||||
|
||||
describe('with new confirmation flow', () => {
|
||||
it('should extract price, request confirmation via port, then proceed', async () => {
|
||||
const price = CheckoutPrice.fromString('$25.50');
|
||||
const state = CheckoutState.ready();
|
||||
|
||||
vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue(
|
||||
Result.ok({ price, state })
|
||||
);
|
||||
|
||||
vi.mocked(mockConfirmationPort.requestCheckoutConfirmation).mockResolvedValue(
|
||||
Result.ok(CheckoutConfirmation.create('confirmed'))
|
||||
);
|
||||
|
||||
vi.mocked(mockCheckoutService.proceedWithCheckout).mockResolvedValue(
|
||||
Result.ok(undefined)
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(mockCheckoutService.extractCheckoutInfo).toHaveBeenCalled();
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
price: expect.any(CheckoutPrice),
|
||||
state: expect.any(CheckoutState),
|
||||
})
|
||||
);
|
||||
expect(mockCheckoutService.proceedWithCheckout).toHaveBeenCalled();
|
||||
expect(result.isOk()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not proceed if user cancels confirmation', async () => {
|
||||
const price = CheckoutPrice.fromString('$10.00');
|
||||
const state = CheckoutState.ready();
|
||||
|
||||
vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue(
|
||||
Result.ok({ price, state })
|
||||
);
|
||||
|
||||
vi.mocked(mockConfirmationPort.requestCheckoutConfirmation).mockResolvedValue(
|
||||
Result.ok(CheckoutConfirmation.create('cancelled'))
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).toHaveBeenCalled();
|
||||
expect(mockCheckoutService.proceedWithCheckout).not.toHaveBeenCalled();
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('cancelled');
|
||||
});
|
||||
|
||||
it('should not proceed if confirmation times out', async () => {
|
||||
const price = CheckoutPrice.fromString('$10.00');
|
||||
const state = CheckoutState.ready();
|
||||
|
||||
vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue(
|
||||
Result.ok({ price, state })
|
||||
);
|
||||
|
||||
vi.mocked(mockConfirmationPort.requestCheckoutConfirmation).mockResolvedValue(
|
||||
Result.ok(CheckoutConfirmation.create('timeout'))
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).toHaveBeenCalled();
|
||||
expect(mockCheckoutService.proceedWithCheckout).not.toHaveBeenCalled();
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('timeout');
|
||||
});
|
||||
|
||||
it('should fail if confirmation port returns error', async () => {
|
||||
const price = CheckoutPrice.fromString('$10.00');
|
||||
const state = CheckoutState.ready();
|
||||
|
||||
vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue(
|
||||
Result.ok({ price, state })
|
||||
);
|
||||
|
||||
vi.mocked(mockConfirmationPort.requestCheckoutConfirmation).mockResolvedValue(
|
||||
Result.err(new Error('IPC communication failed'))
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('IPC communication failed');
|
||||
});
|
||||
|
||||
it('should still reject insufficient funds before confirmation', async () => {
|
||||
const price = CheckoutPrice.fromString('$10.00');
|
||||
const state = CheckoutState.insufficientFunds();
|
||||
|
||||
vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue(
|
||||
Result.ok({ price, state })
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).not.toHaveBeenCalled();
|
||||
expect(mockCheckoutService.proceedWithCheckout).not.toHaveBeenCalled();
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toContain('Insufficient funds');
|
||||
});
|
||||
|
||||
it('should pass session metadata to confirmation port', async () => {
|
||||
const price = CheckoutPrice.fromString('$25.50');
|
||||
const state = CheckoutState.ready();
|
||||
const sessionMetadata = {
|
||||
sessionName: 'Test Race',
|
||||
trackId: 'spa',
|
||||
carIds: ['car1', 'car2'],
|
||||
};
|
||||
|
||||
vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue(
|
||||
Result.ok({ price, state })
|
||||
);
|
||||
|
||||
vi.mocked(mockConfirmationPort.requestCheckoutConfirmation).mockResolvedValue(
|
||||
Result.ok(CheckoutConfirmation.create('confirmed'))
|
||||
);
|
||||
|
||||
vi.mocked(mockCheckoutService.proceedWithCheckout).mockResolvedValue(
|
||||
Result.ok(undefined)
|
||||
);
|
||||
|
||||
const result = await useCase.execute(sessionMetadata);
|
||||
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionMetadata,
|
||||
timeoutMs: expect.any(Number),
|
||||
})
|
||||
);
|
||||
expect(result.isOk()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
404
tests/unit/application/use-cases/ConfirmCheckoutUseCase.test.ts
Normal file
404
tests/unit/application/use-cases/ConfirmCheckoutUseCase.test.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { Result } from '../../../../packages/shared/result/Result';
|
||||
import { ConfirmCheckoutUseCase } from '../../../../packages/application/use-cases/ConfirmCheckoutUseCase';
|
||||
import { ICheckoutService, CheckoutInfo } from '../../../../packages/application/ports/ICheckoutService';
|
||||
import { ICheckoutConfirmationPort } from '../../../../packages/application/ports/ICheckoutConfirmationPort';
|
||||
import { CheckoutPrice } from '../../../../packages/domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState, CheckoutStateEnum } from '../../../../packages/domain/value-objects/CheckoutState';
|
||||
import { CheckoutConfirmation } from '../../../../packages/domain/value-objects/CheckoutConfirmation';
|
||||
|
||||
/**
|
||||
* ConfirmCheckoutUseCase - GREEN PHASE
|
||||
*
|
||||
* Tests for checkout confirmation flow including price extraction,
|
||||
* insufficient funds detection, and user confirmation.
|
||||
*/
|
||||
|
||||
describe('ConfirmCheckoutUseCase', () => {
|
||||
let mockCheckoutService: {
|
||||
extractCheckoutInfo: Mock;
|
||||
proceedWithCheckout: Mock;
|
||||
};
|
||||
let mockConfirmationPort: {
|
||||
requestCheckoutConfirmation: Mock;
|
||||
};
|
||||
let mockPrice: CheckoutPrice;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCheckoutService = {
|
||||
extractCheckoutInfo: vi.fn(),
|
||||
proceedWithCheckout: vi.fn(),
|
||||
};
|
||||
|
||||
mockConfirmationPort = {
|
||||
requestCheckoutConfirmation: vi.fn(),
|
||||
};
|
||||
|
||||
mockPrice = {
|
||||
getAmount: vi.fn(() => 0.50),
|
||||
toDisplayString: vi.fn(() => '$0.50'),
|
||||
isZero: vi.fn(() => false),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Success flow', () => {
|
||||
it('should extract price, get user confirmation, and proceed with checkout', async () => {
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.ok({
|
||||
price: mockPrice,
|
||||
state: CheckoutState.ready(),
|
||||
buttonHtml: '<a class="btn btn-success"><span>$0.50</span></a>',
|
||||
})
|
||||
);
|
||||
mockConfirmationPort.requestCheckoutConfirmation.mockResolvedValue(
|
||||
Result.ok(CheckoutConfirmation.create('confirmed'))
|
||||
);
|
||||
mockCheckoutService.proceedWithCheckout.mockResolvedValue(Result.ok(undefined));
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(mockCheckoutService.extractCheckoutInfo).toHaveBeenCalledTimes(1);
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).toHaveBeenCalledTimes(1);
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ price: mockPrice })
|
||||
);
|
||||
expect(mockCheckoutService.proceedWithCheckout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should include price in confirmation message', async () => {
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.ok({
|
||||
price: mockPrice,
|
||||
state: CheckoutState.ready(),
|
||||
buttonHtml: '<a class="btn btn-success"><span>$0.50</span></a>',
|
||||
})
|
||||
);
|
||||
mockConfirmationPort.requestCheckoutConfirmation.mockResolvedValue(
|
||||
Result.ok(CheckoutConfirmation.create('confirmed'))
|
||||
);
|
||||
mockCheckoutService.proceedWithCheckout.mockResolvedValue(Result.ok(undefined));
|
||||
|
||||
await useCase.execute();
|
||||
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ price: mockPrice })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('User cancellation', () => {
|
||||
it('should abort checkout when user cancels confirmation', async () => {
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.ok({
|
||||
price: mockPrice,
|
||||
state: CheckoutState.ready(),
|
||||
buttonHtml: '<a class="btn btn-success"><span>$0.50</span></a>',
|
||||
})
|
||||
);
|
||||
mockConfirmationPort.requestCheckoutConfirmation.mockResolvedValue(
|
||||
Result.ok(CheckoutConfirmation.create('cancelled'))
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toMatch(/cancel/i);
|
||||
expect(mockCheckoutService.proceedWithCheckout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not proceed with checkout after cancellation', async () => {
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.ok({
|
||||
price: mockPrice,
|
||||
state: CheckoutState.ready(),
|
||||
buttonHtml: '<a class="btn btn-success"><span>$0.50</span></a>',
|
||||
})
|
||||
);
|
||||
mockConfirmationPort.requestCheckoutConfirmation.mockResolvedValue(
|
||||
Result.ok(CheckoutConfirmation.create('cancelled'))
|
||||
);
|
||||
|
||||
await useCase.execute();
|
||||
|
||||
expect(mockCheckoutService.proceedWithCheckout).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Insufficient funds detection', () => {
|
||||
it('should return error when checkout state is INSUFFICIENT_FUNDS', async () => {
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.ok({
|
||||
price: mockPrice,
|
||||
state: CheckoutState.insufficientFunds(),
|
||||
buttonHtml: '<a class="btn btn-default"><span>$0.50</span></a>',
|
||||
})
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toMatch(/insufficient.*funds/i);
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).not.toHaveBeenCalled();
|
||||
expect(mockCheckoutService.proceedWithCheckout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not ask for confirmation when funds are insufficient', async () => {
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.ok({
|
||||
price: mockPrice,
|
||||
state: CheckoutState.insufficientFunds(),
|
||||
buttonHtml: '<a class="btn btn-default"><span>$0.50</span></a>',
|
||||
})
|
||||
);
|
||||
|
||||
await useCase.execute();
|
||||
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Price extraction failure', () => {
|
||||
it('should return error when price cannot be extracted', async () => {
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.ok({
|
||||
price: null,
|
||||
state: CheckoutState.unknown(),
|
||||
buttonHtml: '',
|
||||
})
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toMatch(/extract|price|not found/i);
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).not.toHaveBeenCalled();
|
||||
expect(mockCheckoutService.proceedWithCheckout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return error when extraction service fails', async () => {
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.err('Button not found')
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Zero price warning', () => {
|
||||
it('should still require confirmation for $0.00 price', async () => {
|
||||
const zeroPriceMock: CheckoutPrice = {
|
||||
getAmount: vi.fn(() => 0.00),
|
||||
toDisplayString: vi.fn(() => '$0.00'),
|
||||
isZero: vi.fn(() => true),
|
||||
};
|
||||
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.ok({
|
||||
price: zeroPriceMock,
|
||||
state: CheckoutState.ready(),
|
||||
buttonHtml: '<a class="btn btn-success"><span>$0.00</span></a>',
|
||||
})
|
||||
);
|
||||
mockConfirmationPort.requestCheckoutConfirmation.mockResolvedValue(
|
||||
Result.ok(CheckoutConfirmation.create('confirmed'))
|
||||
);
|
||||
mockCheckoutService.proceedWithCheckout.mockResolvedValue(Result.ok(undefined));
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).toHaveBeenCalledTimes(1);
|
||||
expect(mockConfirmationPort.requestCheckoutConfirmation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ price: zeroPriceMock })
|
||||
);
|
||||
});
|
||||
|
||||
it('should proceed with checkout for zero price after confirmation', async () => {
|
||||
const zeroPriceMock: CheckoutPrice = {
|
||||
getAmount: vi.fn(() => 0.00),
|
||||
toDisplayString: vi.fn(() => '$0.00'),
|
||||
isZero: vi.fn(() => true),
|
||||
};
|
||||
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.ok({
|
||||
price: zeroPriceMock,
|
||||
state: CheckoutState.ready(),
|
||||
buttonHtml: '<a class="btn btn-success"><span>$0.00</span></a>',
|
||||
})
|
||||
);
|
||||
mockConfirmationPort.requestCheckoutConfirmation.mockResolvedValue(
|
||||
Result.ok(CheckoutConfirmation.create('confirmed'))
|
||||
);
|
||||
mockCheckoutService.proceedWithCheckout.mockResolvedValue(Result.ok(undefined));
|
||||
|
||||
await useCase.execute();
|
||||
|
||||
expect(mockCheckoutService.proceedWithCheckout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Checkout execution failure', () => {
|
||||
it('should return error when proceedWithCheckout fails', async () => {
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.ok({
|
||||
price: mockPrice,
|
||||
state: CheckoutState.ready(),
|
||||
buttonHtml: '<a class="btn btn-success"><span>$0.50</span></a>',
|
||||
})
|
||||
);
|
||||
mockConfirmationPort.requestCheckoutConfirmation.mockResolvedValue(
|
||||
Result.ok(CheckoutConfirmation.create('confirmed'))
|
||||
);
|
||||
mockCheckoutService.proceedWithCheckout.mockResolvedValue(
|
||||
Result.err('Network error')
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toContain('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BDD Scenarios', () => {
|
||||
it('Given checkout price $0.50 and READY state, When user confirms, Then checkout proceeds', async () => {
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.ok({
|
||||
price: mockPrice,
|
||||
state: CheckoutState.ready(),
|
||||
buttonHtml: '<a class="btn btn-success"><span>$0.50</span></a>',
|
||||
})
|
||||
);
|
||||
mockConfirmationPort.requestCheckoutConfirmation.mockResolvedValue(
|
||||
Result.ok(CheckoutConfirmation.create('confirmed'))
|
||||
);
|
||||
mockCheckoutService.proceedWithCheckout.mockResolvedValue(Result.ok(undefined));
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
});
|
||||
|
||||
it('Given checkout price $0.50, When user cancels, Then checkout is aborted', async () => {
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.ok({
|
||||
price: mockPrice,
|
||||
state: CheckoutState.ready(),
|
||||
buttonHtml: '<a class="btn btn-success"><span>$0.50</span></a>',
|
||||
})
|
||||
);
|
||||
mockConfirmationPort.requestCheckoutConfirmation.mockResolvedValue(
|
||||
Result.ok(CheckoutConfirmation.create('cancelled'))
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(mockCheckoutService.proceedWithCheckout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Given INSUFFICIENT_FUNDS state, When executing, Then error is returned', async () => {
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.ok({
|
||||
price: mockPrice,
|
||||
state: CheckoutState.insufficientFunds(),
|
||||
buttonHtml: '<a class="btn btn-default"><span>$0.50</span></a>',
|
||||
})
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
});
|
||||
|
||||
it('Given price extraction failure, When executing, Then error is returned', async () => {
|
||||
const useCase = new ConfirmCheckoutUseCase(
|
||||
mockCheckoutService as unknown as ICheckoutService,
|
||||
mockConfirmationPort as unknown as ICheckoutConfirmationPort
|
||||
);
|
||||
|
||||
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
|
||||
Result.err('Button not found')
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -289,4 +289,21 @@ describe('StartAutomationSessionUseCase', () => {
|
||||
expect(result.config.sessionName).toBe('Test & Race #1 (2025)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute - step count verification', () => {
|
||||
it('should verify automation flow has exactly 17 steps (not 18)', async () => {
|
||||
// This test verifies that step 17 "Race Options" has been completely removed
|
||||
// Step 17 "Race Options" does not exist in real iRacing and must not be in the code
|
||||
// The old step 18 (Track Conditions) is now the new step 17 (final step)
|
||||
|
||||
// Import the adapter to check its totalSteps property
|
||||
const { PlaywrightAutomationAdapter } = await import('../../../../packages/infrastructure/adapters/automation/PlaywrightAutomationAdapter');
|
||||
|
||||
// Create a temporary adapter instance to check totalSteps
|
||||
const adapter = new PlaywrightAutomationAdapter({ mode: 'mock' });
|
||||
|
||||
// Verify totalSteps is 17 (not 18)
|
||||
expect((adapter as any).totalSteps).toBe(17);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { VerifyAuthenticatedPageUseCase } from '../../../../packages/application/use-cases/VerifyAuthenticatedPageUseCase';
|
||||
import { IAuthenticationService } from '../../../../packages/application/ports/IAuthenticationService';
|
||||
import { Result } from '../../../../packages/shared/result/Result';
|
||||
import { BrowserAuthenticationState } from '../../../../packages/domain/value-objects/BrowserAuthenticationState';
|
||||
import { AuthenticationState } from '../../../../packages/domain/value-objects/AuthenticationState';
|
||||
|
||||
describe('VerifyAuthenticatedPageUseCase', () => {
|
||||
let useCase: VerifyAuthenticatedPageUseCase;
|
||||
let mockAuthService: {
|
||||
checkSession: ReturnType<typeof vi.fn>;
|
||||
verifyPageAuthentication: ReturnType<typeof vi.fn>;
|
||||
initiateLogin: ReturnType<typeof vi.fn>;
|
||||
clearSession: ReturnType<typeof vi.fn>;
|
||||
getState: ReturnType<typeof vi.fn>;
|
||||
validateServerSide: ReturnType<typeof vi.fn>;
|
||||
refreshSession: ReturnType<typeof vi.fn>;
|
||||
getSessionExpiry: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockAuthService = {
|
||||
checkSession: vi.fn(),
|
||||
verifyPageAuthentication: vi.fn(),
|
||||
initiateLogin: vi.fn(),
|
||||
clearSession: vi.fn(),
|
||||
getState: vi.fn(),
|
||||
validateServerSide: vi.fn(),
|
||||
refreshSession: vi.fn(),
|
||||
getSessionExpiry: vi.fn(),
|
||||
};
|
||||
useCase = new VerifyAuthenticatedPageUseCase(
|
||||
mockAuthService as unknown as IAuthenticationService
|
||||
);
|
||||
});
|
||||
|
||||
it('should return fully authenticated browser state', async () => {
|
||||
const mockBrowserState = new BrowserAuthenticationState(true, true);
|
||||
mockAuthService.verifyPageAuthentication.mockResolvedValue(
|
||||
Result.ok(mockBrowserState)
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const browserState = result.unwrap();
|
||||
expect(browserState.isFullyAuthenticated()).toBe(true);
|
||||
expect(browserState.getAuthenticationState()).toBe(AuthenticationState.AUTHENTICATED);
|
||||
});
|
||||
|
||||
it('should return unauthenticated state when page not authenticated', async () => {
|
||||
const mockBrowserState = new BrowserAuthenticationState(true, false);
|
||||
mockAuthService.verifyPageAuthentication.mockResolvedValue(
|
||||
Result.ok(mockBrowserState)
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const browserState = result.unwrap();
|
||||
expect(browserState.isFullyAuthenticated()).toBe(false);
|
||||
expect(browserState.getAuthenticationState()).toBe(AuthenticationState.EXPIRED);
|
||||
});
|
||||
|
||||
it('should return requires reauth state when cookies invalid', async () => {
|
||||
const mockBrowserState = new BrowserAuthenticationState(false, false);
|
||||
mockAuthService.verifyPageAuthentication.mockResolvedValue(
|
||||
Result.ok(mockBrowserState)
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const browserState = result.unwrap();
|
||||
expect(browserState.requiresReauthentication()).toBe(true);
|
||||
expect(browserState.getAuthenticationState()).toBe(AuthenticationState.UNKNOWN);
|
||||
});
|
||||
|
||||
it('should propagate errors from verifyPageAuthentication', async () => {
|
||||
const error = new Error('Verification failed');
|
||||
mockAuthService.verifyPageAuthentication.mockResolvedValue(
|
||||
Result.err(error)
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.error.message).toBe('Verification failed');
|
||||
});
|
||||
|
||||
it('should handle unexpected errors', async () => {
|
||||
mockAuthService.verifyPageAuthentication.mockRejectedValue(
|
||||
new Error('Unexpected error')
|
||||
);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.error.message).toBe('Page verification failed: Unexpected error');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user