This commit is contained in:
2025-12-12 21:39:48 +01:00
parent ddbd99b747
commit cae81b1088
49 changed files with 777 additions and 269 deletions

View File

@@ -26,7 +26,7 @@ describe('OverlaySyncService (unit)', () => {
// create service wiring: pass emitter as dependency (constructor shape expected)
const svc = new OverlaySyncService({
lifecycleEmitter: emitter,
logger: console,
logger: console as any,
publisher: { publish: async () => {} },
})

View File

@@ -23,7 +23,7 @@ describe('OverlaySyncService timeout (unit)', () => {
const emitter = new MockLifecycleEmitter()
const svc = new OverlaySyncService({
lifecycleEmitter: emitter,
logger: console,
logger: console as any,
publisher: { publish: async () => {} },
})

View File

@@ -3,7 +3,7 @@ import { CheckAuthenticationUseCase } from '../../../../packages/automation/appl
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
import { Result } from '../../../../packages/shared/result/Result';
import type { IAuthenticationService } from '../../../../packages/automation/application/ports/IAuthenticationService';
import type { AuthenticationServicePort } from '../../../../packages/automation/application/ports/AuthenticationServicePort';
interface ISessionValidator {
validateSession(): Promise<Result<boolean>>;
@@ -44,7 +44,7 @@ describe('CheckAuthenticationUseCase', () => {
describe('File-based validation only', () => {
it('should return AUTHENTICATED when cookies are valid', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService
mockAuthService as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -63,7 +63,7 @@ describe('CheckAuthenticationUseCase', () => {
it('should return EXPIRED when cookies are expired', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService
mockAuthService as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -81,7 +81,7 @@ describe('CheckAuthenticationUseCase', () => {
it('should return UNKNOWN when no session exists', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService
mockAuthService as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -101,7 +101,7 @@ describe('CheckAuthenticationUseCase', () => {
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,
mockAuthService as unknown as AuthenticationServicePort,
mockSessionValidator as unknown as ISessionValidator
);
@@ -124,7 +124,7 @@ describe('CheckAuthenticationUseCase', () => {
it('should return EXPIRED when file says valid but server rejects', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService,
mockAuthService as unknown as AuthenticationServicePort,
mockSessionValidator as unknown as ISessionValidator
);
@@ -146,7 +146,7 @@ describe('CheckAuthenticationUseCase', () => {
it('should work without ISessionValidator injected (optional dependency)', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService
mockAuthService as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -166,7 +166,7 @@ describe('CheckAuthenticationUseCase', () => {
describe('Error handling', () => {
it('should not block file-based result if server validation fails', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService,
mockAuthService as unknown as AuthenticationServicePort,
mockSessionValidator as unknown as ISessionValidator
);
@@ -188,7 +188,7 @@ describe('CheckAuthenticationUseCase', () => {
it('should handle authentication service errors gracefully', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService
mockAuthService as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -203,7 +203,7 @@ describe('CheckAuthenticationUseCase', () => {
it('should handle session expiry check errors gracefully', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService
mockAuthService as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -224,7 +224,7 @@ describe('CheckAuthenticationUseCase', () => {
describe('Page content verification', () => {
it('should call verifyPageAuthentication when verifyPageContent is true', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService
mockAuthService as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -244,7 +244,7 @@ describe('CheckAuthenticationUseCase', () => {
it('should return EXPIRED when cookies valid but page shows login UI', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService
mockAuthService as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -265,7 +265,7 @@ describe('CheckAuthenticationUseCase', () => {
it('should return AUTHENTICATED when both cookies AND page authenticated', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService
mockAuthService as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -286,7 +286,7 @@ describe('CheckAuthenticationUseCase', () => {
it('should default verifyPageContent to false (backward compatible)', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService
mockAuthService as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -304,7 +304,7 @@ describe('CheckAuthenticationUseCase', () => {
it('should handle verifyPageAuthentication errors gracefully', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService
mockAuthService as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -328,7 +328,7 @@ describe('CheckAuthenticationUseCase', () => {
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 as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -345,7 +345,7 @@ describe('CheckAuthenticationUseCase', () => {
it('Given expired session cookies, When checking auth, Then return EXPIRED', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService
mockAuthService as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -362,7 +362,7 @@ describe('CheckAuthenticationUseCase', () => {
it('Given no session file, When checking auth, Then return UNKNOWN', async () => {
const useCase = new CheckAuthenticationUseCase(
mockAuthService as unknown as IAuthenticationService
mockAuthService as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(
@@ -379,7 +379,7 @@ describe('CheckAuthenticationUseCase', () => {
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 as unknown as AuthenticationServicePort
);
mockAuthService.checkSession.mockResolvedValue(

View File

@@ -3,11 +3,11 @@ import { CompleteRaceCreationUseCase } from '../../../../packages/automation/app
import { Result } from '../../../../packages/shared/result/Result';
import { RaceCreationResult } from '@gridpilot/automation/domain/value-objects/RaceCreationResult';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import type { ICheckoutService } from '../../../../packages/automation/application/ports/ICheckoutService';
import type { CheckoutServicePort } from '../../../../packages/automation/application/ports/CheckoutServicePort';
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
describe('CompleteRaceCreationUseCase', () => {
let mockCheckoutService: ICheckoutService;
let mockCheckoutService: CheckoutServicePort;
let useCase: CompleteRaceCreationUseCase;
beforeEach(() => {

View File

@@ -1,8 +1,9 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { Result } from '../../../../packages/shared/result/Result';
import { ConfirmCheckoutUseCase } from '../../../../packages/automation/application/use-cases/ConfirmCheckoutUseCase';
import { ICheckoutService, CheckoutInfo } from '../../../../packages/automation/application/ports/ICheckoutService';
import { ICheckoutConfirmationPort } from '../../../../packages/automation/application/ports/ICheckoutConfirmationPort';
import type { CheckoutServicePort } from '../../../../packages/automation/application/ports/CheckoutServicePort';
import type { CheckoutConfirmationPort } from '../../../../packages/automation/application/ports/CheckoutConfirmationPort';
import type { CheckoutInfoDTODTO } from '../../../../packages/automation/application/dto/CheckoutInfoDTODTO';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState, CheckoutStateEnum } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
@@ -44,8 +45,8 @@ describe('ConfirmCheckoutUseCase', () => {
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 as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -73,8 +74,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('should include price in confirmation message', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as ICheckoutService,
mockConfirmationPort as unknown as ICheckoutConfirmationPort
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -100,8 +101,8 @@ describe('ConfirmCheckoutUseCase', () => {
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 as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -124,8 +125,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('should not proceed with checkout after cancellation', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as ICheckoutService,
mockConfirmationPort as unknown as ICheckoutConfirmationPort
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -148,8 +149,8 @@ describe('ConfirmCheckoutUseCase', () => {
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 as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -170,8 +171,8 @@ describe('ConfirmCheckoutUseCase', () => {
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 as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -191,8 +192,8 @@ describe('ConfirmCheckoutUseCase', () => {
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 as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -213,8 +214,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('should return error when extraction service fails', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as ICheckoutService,
mockConfirmationPort as unknown as ICheckoutConfirmationPort
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -237,8 +238,8 @@ describe('ConfirmCheckoutUseCase', () => {
} as unknown as CheckoutPrice;
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as ICheckoutService,
mockConfirmationPort as unknown as ICheckoutConfirmationPort
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -270,8 +271,8 @@ describe('ConfirmCheckoutUseCase', () => {
} as unknown as CheckoutPrice;
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as ICheckoutService,
mockConfirmationPort as unknown as ICheckoutConfirmationPort
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -295,8 +296,8 @@ describe('ConfirmCheckoutUseCase', () => {
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 as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -323,8 +324,8 @@ describe('ConfirmCheckoutUseCase', () => {
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 as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -346,8 +347,8 @@ describe('ConfirmCheckoutUseCase', () => {
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 as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -369,8 +370,8 @@ describe('ConfirmCheckoutUseCase', () => {
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 as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -388,8 +389,8 @@ describe('ConfirmCheckoutUseCase', () => {
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 as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(

View File

@@ -17,7 +17,7 @@ describe('AutomationConfig', () => {
describe('getAutomationMode', () => {
describe('NODE_ENV-based mode detection', () => {
it('should return production mode when NODE_ENV=production', () => {
process.env.NODE_ENV = 'production';
(process.env as any).NODE_ENV = 'production';
delete process.env.AUTOMATION_MODE;
const mode = getAutomationMode();
@@ -26,7 +26,7 @@ describe('AutomationConfig', () => {
});
it('should return test mode when NODE_ENV=test', () => {
process.env.NODE_ENV = 'test';
(process.env as any).NODE_ENV = 'test';
delete process.env.AUTOMATION_MODE;
const mode = getAutomationMode();
@@ -35,7 +35,7 @@ describe('AutomationConfig', () => {
});
it('should return test mode when NODE_ENV is not set', () => {
delete process.env.NODE_ENV;
delete (process.env as any).NODE_ENV;
delete process.env.AUTOMATION_MODE;
const mode = getAutomationMode();
@@ -44,7 +44,7 @@ describe('AutomationConfig', () => {
});
it('should return test mode for unknown NODE_ENV values', () => {
process.env.NODE_ENV = 'staging';
(process.env as any).NODE_ENV = 'staging';
delete process.env.AUTOMATION_MODE;
const mode = getAutomationMode();
@@ -53,7 +53,7 @@ describe('AutomationConfig', () => {
});
it('should return development mode when NODE_ENV=development', () => {
process.env.NODE_ENV = 'development';
(process.env as any).NODE_ENV = 'development';
delete process.env.AUTOMATION_MODE;
const mode = getAutomationMode();
@@ -104,7 +104,7 @@ describe('AutomationConfig', () => {
it('should ignore invalid AUTOMATION_MODE and use NODE_ENV', () => {
process.env.AUTOMATION_MODE = 'invalid-mode';
process.env.NODE_ENV = 'production';
(process.env as any).NODE_ENV = 'production';
const mode = getAutomationMode();
@@ -116,7 +116,7 @@ describe('AutomationConfig', () => {
describe('loadAutomationConfig', () => {
describe('default configuration', () => {
it('should return test mode when NODE_ENV is not set', () => {
delete process.env.NODE_ENV;
delete (process.env as any).NODE_ENV;
delete process.env.AUTOMATION_MODE;
const config = loadAutomationConfig();
@@ -143,7 +143,7 @@ describe('AutomationConfig', () => {
describe('production mode configuration', () => {
it('should return production mode when NODE_ENV=production', () => {
process.env.NODE_ENV = 'production';
(process.env as any).NODE_ENV = 'production';
delete process.env.AUTOMATION_MODE;
const config = loadAutomationConfig();
@@ -228,7 +228,7 @@ describe('AutomationConfig', () => {
});
it('should fallback to test mode for invalid NODE_ENV', () => {
process.env.NODE_ENV = 'invalid-env';
(process.env as any).NODE_ENV = 'invalid-env';
delete process.env.AUTOMATION_MODE;
const config = loadAutomationConfig();
@@ -239,7 +239,7 @@ describe('AutomationConfig', () => {
describe('full configuration scenario', () => {
it('should load complete test environment configuration', () => {
process.env.NODE_ENV = 'test';
(process.env as any).NODE_ENV = 'test';
delete process.env.AUTOMATION_MODE;
const config = loadAutomationConfig();
@@ -249,7 +249,7 @@ describe('AutomationConfig', () => {
});
it('should load complete production environment configuration', () => {
process.env.NODE_ENV = 'production';
(process.env as any).NODE_ENV = 'production';
delete process.env.AUTOMATION_MODE;
const config = loadAutomationConfig();

View File

@@ -2,11 +2,13 @@ import { describe, test, expect, beforeEach } from 'vitest';
import { SessionCookieStore } from '@gridpilot/automation/infrastructure/adapters/automation/auth/SessionCookieStore';
import type { Cookie } from 'playwright';
const logger = console as any;
describe('SessionCookieStore - Cookie Validation', () => {
let cookieStore: SessionCookieStore;
beforeEach(() => {
cookieStore = new SessionCookieStore('test-user-data');
cookieStore = new SessionCookieStore('test-user-data', logger);
});
describe('validateCookieConfiguration()', () => {

View File

@@ -12,7 +12,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
beforeEach(() => {
process.env = { ...originalEnv };
delete process.env.NODE_ENV;
delete (process.env as any).NODE_ENV;
});
afterEach(() => {
@@ -21,7 +21,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
describe('Development Mode with Runtime Control', () => {
it('should default to headless in development mode', () => {
process.env.NODE_ENV = 'development';
(process.env as any).NODE_ENV = 'development';
const loader = new BrowserModeConfigLoader();
const config = loader.load();
@@ -31,7 +31,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
});
it('should allow runtime switch to headless mode in development', () => {
process.env.NODE_ENV = 'development';
(process.env as any).NODE_ENV = 'development';
const loader = new BrowserModeConfigLoader();
loader.setDevelopmentMode('headless');
@@ -42,7 +42,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
});
it('should allow runtime switch to headed mode in development', () => {
process.env.NODE_ENV = 'development';
(process.env as any).NODE_ENV = 'development';
const loader = new BrowserModeConfigLoader();
loader.setDevelopmentMode('headed');
@@ -53,7 +53,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
});
it('should persist runtime setting across multiple load() calls', () => {
process.env.NODE_ENV = 'development';
(process.env as any).NODE_ENV = 'development';
const loader = new BrowserModeConfigLoader();
loader.setDevelopmentMode('headless');
@@ -66,7 +66,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
});
it('should return current development mode via getter', () => {
process.env.NODE_ENV = 'development';
(process.env as any).NODE_ENV = 'development';
const loader = new BrowserModeConfigLoader();
expect(loader.getDevelopmentMode()).toBe('headless');
@@ -78,7 +78,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
describe('Production Mode', () => {
it('should use headless mode when NODE_ENV=production', () => {
process.env.NODE_ENV = 'production';
(process.env as any).NODE_ENV = 'production';
const loader = new BrowserModeConfigLoader();
const config = loader.load();
@@ -88,7 +88,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
});
it('should ignore setDevelopmentMode in production', () => {
process.env.NODE_ENV = 'production';
(process.env as any).NODE_ENV = 'production';
const loader = new BrowserModeConfigLoader();
loader.setDevelopmentMode('headed');
@@ -101,7 +101,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
describe('Test Mode', () => {
it('should use headless mode when NODE_ENV=test', () => {
process.env.NODE_ENV = 'test';
(process.env as any).NODE_ENV = 'test';
const loader = new BrowserModeConfigLoader();
const config = loader.load();
@@ -111,7 +111,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
});
it('should ignore setDevelopmentMode in test mode', () => {
process.env.NODE_ENV = 'test';
(process.env as any).NODE_ENV = 'test';
const loader = new BrowserModeConfigLoader();
loader.setDevelopmentMode('headed');
@@ -124,7 +124,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
describe('Default Mode', () => {
it('should default to headless mode when NODE_ENV is not set', () => {
delete process.env.NODE_ENV;
delete (process.env as any).NODE_ENV;
const loader = new BrowserModeConfigLoader();
const config = loader.load();
@@ -134,7 +134,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
});
it('should use headless mode for any non-development NODE_ENV value', () => {
process.env.NODE_ENV = 'staging';
(process.env as any).NODE_ENV = 'staging';
const loader = new BrowserModeConfigLoader();
const config = loader.load();
@@ -146,7 +146,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
describe('Source Tracking', () => {
it('should report GUI as source in development mode', () => {
process.env.NODE_ENV = 'development';
(process.env as any).NODE_ENV = 'development';
const loader = new BrowserModeConfigLoader();
const config = loader.load();
@@ -155,7 +155,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
});
it('should report NODE_ENV as source in production mode', () => {
process.env.NODE_ENV = 'production';
(process.env as any).NODE_ENV = 'production';
const loader = new BrowserModeConfigLoader();
const config = loader.load();
@@ -164,7 +164,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
});
it('should report NODE_ENV as source in test mode', () => {
process.env.NODE_ENV = 'test';
(process.env as any).NODE_ENV = 'test';
const loader = new BrowserModeConfigLoader();
const config = loader.load();
@@ -173,7 +173,7 @@ describe('BrowserModeConfig - GREEN Phase', () => {
});
it('should report NODE_ENV as source when NODE_ENV is not set', () => {
delete process.env.NODE_ENV;
delete (process.env as any).NODE_ENV;
const loader = new BrowserModeConfigLoader();
const config = loader.load();

View File

@@ -10,6 +10,10 @@ import type {
class FakeDashboardOverviewPresenter implements IDashboardOverviewPresenter {
viewModel: DashboardOverviewViewModel | null = null;
reset(): void {
this.viewModel = null;
}
present(viewModel: DashboardOverviewViewModel): void {
this.viewModel = viewModel;
}
@@ -201,11 +205,10 @@ describe('GetDashboardOverviewUseCase', () => {
socialRepository,
imageService,
getDriverStats,
presenter,
);
// When
await useCase.execute({ driverId });
await useCase.execute({ driverId }, presenter);
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
@@ -389,11 +392,10 @@ describe('GetDashboardOverviewUseCase', () => {
socialRepository,
imageService,
getDriverStats,
presenter,
);
// When
await useCase.execute({ driverId });
await useCase.execute({ driverId }, presenter);
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
@@ -496,11 +498,10 @@ describe('GetDashboardOverviewUseCase', () => {
socialRepository,
imageService,
getDriverStats,
presenter,
);
// When
await useCase.execute({ driverId });
await useCase.execute({ driverId }, presenter);
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();

View File

@@ -6,7 +6,6 @@ import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/ID
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository';
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
import type { LeagueMembership } from '@gridpilot/racing/domain/entities/LeagueMembership';
import type { DriverRatingProvider } from '@gridpilot/racing/application/ports/DriverRatingProvider';
import type { IImageServicePort } from '@gridpilot/racing/application/ports/IImageServicePort';
import type {
@@ -17,6 +16,8 @@ import type {
import { Race } from '@gridpilot/racing/domain/entities/Race';
import { League } from '@gridpilot/racing/domain/entities/League';
import { Result } from '@gridpilot/racing/domain/entities/Result';
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { LeagueMembership } from '@gridpilot/racing/domain/entities/LeagueMembership';
import { GetRaceDetailUseCase } from '@gridpilot/racing/application/use-cases/GetRaceDetailUseCase';
import { CancelRaceUseCase } from '@gridpilot/racing/application/use-cases/CancelRaceUseCase';
@@ -118,31 +119,39 @@ class InMemoryLeagueRepository implements ILeagueRepository {
async exists(id: string): Promise<boolean> {
return this.leagues.has(id);
}
async searchByName(): Promise<League[]> {
return [];
}
}
class InMemoryDriverRepository implements IDriverRepository {
private drivers = new Map<string, { id: string; name: string; country: string }>();
private drivers = new Map<string, Driver>();
constructor(drivers: Array<{ id: string; name: string; country: string }>) {
for (const driver of drivers) {
this.drivers.set(driver.id, {
...driver,
});
this.drivers.set(driver.id, Driver.create({
id: driver.id,
iracingId: `iracing-${driver.id}`,
name: driver.name,
country: driver.country,
joinedAt: new Date('2024-01-01'),
}));
}
}
async findById(id: string): Promise<{ id: string; name: string; country: string } | null> {
async findById(id: string): Promise<Driver | null> {
return this.drivers.get(id) ?? null;
}
async findAll(): Promise<Array<{ id: string; name: string; country: string }>> {
async findAll(): Promise<Driver[]> {
return [...this.drivers.values()];
}
async findByIds(ids: string[]): Promise<Array<{ id: string; name: string; country: string }>> {
async findByIds(ids: string[]): Promise<Driver[]> {
return ids
.map(id => this.drivers.get(id))
.filter((d): d is { id: string; name: string; country: string } => !!d);
.filter((d): d is Driver => !!d);
}
async create(): Promise<any> {
@@ -160,6 +169,14 @@ class InMemoryDriverRepository implements IDriverRepository {
async exists(): Promise<boolean> {
return false;
}
async findByIRacingId(): Promise<Driver | null> {
return null;
}
async existsByIRacingId(): Promise<boolean> {
return false;
}
}
class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepository {
@@ -365,6 +382,10 @@ class FakeRaceDetailPresenter implements IRaceDetailPresenter {
getViewModel(): RaceDetailViewModel | null {
return this.viewModel;
}
reset(): void {
this.viewModel = null;
}
}
describe('GetRaceDetailUseCase', () => {
@@ -405,13 +426,13 @@ describe('GetRaceDetailUseCase', () => {
const resultRepo = new InMemoryResultRepository([]);
const membershipRepo = new InMemoryLeagueMembershipRepository();
membershipRepo.seedMembership({
membershipRepo.seedMembership(LeagueMembership.create({
leagueId: league.id,
driverId,
role: 'member',
status: 'active',
joinedAt: new Date('2024-01-01'),
});
}));
const ratingProvider = new TestDriverRatingProvider();
ratingProvider.seed(driverId, 1500);
@@ -429,11 +450,10 @@ describe('GetRaceDetailUseCase', () => {
membershipRepo,
ratingProvider,
imageService,
presenter,
);
// When (execute the query for the current driver)
await useCase.execute({ raceId: race.id, driverId });
await useCase.execute({ raceId: race.id, driverId }, presenter);
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
@@ -502,13 +522,13 @@ describe('GetRaceDetailUseCase', () => {
const resultRepo = new InMemoryResultRepository([resultEntity]);
const membershipRepo = new InMemoryLeagueMembershipRepository();
membershipRepo.seedMembership({
membershipRepo.seedMembership(LeagueMembership.create({
leagueId: league.id,
driverId,
role: 'member',
status: 'active',
joinedAt: new Date('2024-01-01'),
});
}));
const ratingProvider = new TestDriverRatingProvider();
ratingProvider.seed(driverId, 2000);
@@ -525,11 +545,10 @@ describe('GetRaceDetailUseCase', () => {
membershipRepo,
ratingProvider,
imageService,
presenter,
);
// When (executing the query for the completed race)
await useCase.execute({ raceId: race.id, driverId });
await useCase.execute({ raceId: race.id, driverId }, presenter);
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
@@ -566,11 +585,10 @@ describe('GetRaceDetailUseCase', () => {
membershipRepo,
ratingProvider,
imageService,
presenter,
);
// When
await useCase.execute({ raceId: 'missing-race', driverId: 'driver-x' });
await useCase.execute({ raceId: 'missing-race', driverId: 'driver-x' }, presenter);
const viewModel = presenter.getViewModel();
// Then

View File

@@ -20,6 +20,10 @@ import type {
class FakeRaceResultsDetailPresenter implements IRaceResultsDetailPresenter {
viewModel: RaceResultsDetailViewModel | null = null;
reset(): void {
this.viewModel = null;
}
present(viewModel: RaceResultsDetailViewModel): RaceResultsDetailViewModel {
this.viewModel = viewModel;
return viewModel;
@@ -354,7 +358,7 @@ describe('GetRaceResultsDetailUseCase', () => {
);
// When executing the query
await useCase.execute({ raceId: race.id });
await useCase.execute({ raceId: race.id }, presenter);
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
@@ -464,7 +468,7 @@ describe('GetRaceResultsDetailUseCase', () => {
);
// When
await useCase.execute({ raceId: race.id });
await useCase.execute({ raceId: race.id }, presenter);
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
@@ -529,7 +533,7 @@ describe('GetRaceResultsDetailUseCase', () => {
);
// When
await useCase.execute({ raceId: 'missing-race' });
await useCase.execute({ raceId: 'missing-race' }, presenter);
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();

View File

@@ -56,6 +56,7 @@ import type {
DriverTeamResultDTO,
DriverTeamViewModel,
} from '@gridpilot/racing/application/presenters/IDriverTeamPresenter';
import type { RaceRegistrationsResultDTO } from '@gridpilot/racing/application/presenters/IRaceRegistrationsPresenter';
/**
* Simple in-memory fakes mirroring current alpha behavior.
@@ -179,16 +180,14 @@ class TestRaceRegistrationsPresenter implements IRaceRegistrationsPresenter {
raceId: string | null = null;
driverIds: string[] = [];
// Accepts either the legacy (raceId, driverIds) shape or the new (driverIds) shape
present(raceIdOrDriverIds: string | string[], driverIds?: string[]): void {
if (Array.isArray(raceIdOrDriverIds) && driverIds == null) {
this.raceId = null;
this.driverIds = raceIdOrDriverIds;
return;
}
reset(): void {
this.raceId = null;
this.driverIds = [];
}
this.raceId = raceIdOrDriverIds as string;
this.driverIds = driverIds ?? [];
present(input: RaceRegistrationsResultDTO): void {
this.driverIds = input.registeredDriverIds;
this.raceId = null;
}
}
@@ -318,6 +317,10 @@ class InMemoryTeamMembershipRepository implements ITeamMembershipRepository {
getAllJoinRequests(): TeamJoinRequest[] {
return [...this.joinRequests];
}
async countByTeamId(teamId: string): Promise<number> {
return this.memberships.filter((m) => m.teamId === teamId).length;
}
}
describe('Racing application use-cases - registrations', () => {
@@ -342,10 +345,7 @@ describe('Racing application use-cases - registrations', () => {
driverRegistrationPresenter,
);
raceRegistrationsPresenter = new TestRaceRegistrationsPresenter();
getRaceRegistrations = new GetRaceRegistrationsUseCase(
registrationRepo,
raceRegistrationsPresenter,
);
getRaceRegistrations = new GetRaceRegistrationsUseCase(registrationRepo);
});
it('registers an active league member for a race and tracks registration', async () => {
@@ -362,7 +362,7 @@ describe('Racing application use-cases - registrations', () => {
expect(driverRegistrationPresenter.raceId).toBe(raceId);
expect(driverRegistrationPresenter.driverId).toBe(driverId);
await getRaceRegistrations.execute({ raceId });
await getRaceRegistrations.execute({ raceId }, raceRegistrationsPresenter);
expect(raceRegistrationsPresenter.driverIds).toContain(driverId);
});
@@ -389,7 +389,7 @@ describe('Racing application use-cases - registrations', () => {
await isDriverRegistered.execute({ raceId, driverId });
expect(driverRegistrationPresenter.isRegistered).toBe(false);
await getRaceRegistrations.execute({ raceId });
await getRaceRegistrations.execute({ raceId }, raceRegistrationsPresenter);
expect(raceRegistrationsPresenter.driverIds).toEqual([]);
});
});
@@ -458,8 +458,16 @@ describe('Racing application use-cases - teams', () => {
class TestTeamDetailsPresenter implements ITeamDetailsPresenter {
viewModel: any = null;
present(team: any, membership: any, driverId: string): void {
this.viewModel = { team, membership, driverId };
reset(): void {
this.viewModel = null;
}
present(input: any): void {
this.viewModel = input;
}
getViewModel(): any {
return this.viewModel;
}
}
@@ -744,7 +752,7 @@ describe('Racing application use-cases - teams', () => {
updatedBy: ownerId,
});
await getTeamDetailsUseCase.execute(created.team.id, ownerId);
await getTeamDetailsUseCase.execute({ teamId: created.team.id, driverId: ownerId }, teamDetailsPresenter);
expect(teamDetailsPresenter.viewModel.team.name).toBe('Updated Name');
expect(teamDetailsPresenter.viewModel.team.description).toBe('Updated description');

View File

@@ -9,12 +9,12 @@ describe('getAppMode', () => {
beforeEach(() => {
process.env = { ...originalEnv };
process.env.NODE_ENV = 'production';
(process.env as any).NODE_ENV = 'production';
});
afterEach(() => {
process.env = originalEnv;
process.env.NODE_ENV = ORIGINAL_NODE_ENV;
(process.env as any).NODE_ENV = ORIGINAL_NODE_ENV;
});
it('returns "pre-launch" when NEXT_PUBLIC_GRIDPILOT_MODE is undefined', () => {
@@ -55,7 +55,7 @@ describe('getAppMode', () => {
});
it('throws in development when NEXT_PUBLIC_GRIDPILOT_MODE is invalid', () => {
process.env.NODE_ENV = 'development';
(process.env as any).NODE_ENV = 'development';
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'invalid-mode';
expect(() => getAppMode()).toThrowError(/Invalid NEXT_PUBLIC_GRIDPILOT_MODE/);

View File

@@ -77,7 +77,7 @@ describe('/api/signup POST', () => {
const data = (await response.json()) as { error: unknown };
expect(typeof data.error).toBe('string');
expect(data.error.toLowerCase()).toContain('email');
expect(typeof data.error === 'string' && data.error.toLowerCase()).toContain('email');
});
it('rejects disposable email domains with 400 and error message', async () => {
@@ -110,7 +110,7 @@ describe('/api/signup POST', () => {
const data = (await second.json()) as { error: unknown };
expect(typeof data.error).toBe('string');
expect(data.error.toLowerCase()).toContain('already');
expect(typeof data.error === 'string' && data.error.toLowerCase()).toContain('already');
});
it('returns 429 with retryAfter when rate limit is exceeded', async () => {