142 lines
5.4 KiB
TypeScript
142 lines
5.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import type { Page, BrowserContext } from 'playwright';
|
|
import { PlaywrightAuthSessionService } from '../../../../core/automation/infrastructure//automation/auth/PlaywrightAuthSessionService';
|
|
import type { PlaywrightBrowserSession } from '../../../../core/automation/infrastructure//automation/core/PlaywrightBrowserSession';
|
|
import type { SessionCookieStore } from '../../../../core/automation/infrastructure//automation/auth/SessionCookieStore';
|
|
import type { IPlaywrightAuthFlow } from '../../../../core/automation/infrastructure//automation/auth/PlaywrightAuthFlow';
|
|
import type { LoggerPort as Logger } from '../../../../core/automation/application/ports/LoggerPort';
|
|
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
|
|
import { Result } from '../../../../core/shared/result/Result';
|
|
|
|
describe('PlaywrightAuthSessionService.initiateLogin browser mode behaviour', () => {
|
|
const originalEnv = { ...process.env };
|
|
|
|
let mockBrowserSession: PlaywrightBrowserSession;
|
|
let mockCookieStore: SessionCookieStore;
|
|
let mockAuthFlow: IPlaywrightAuthFlow;
|
|
let mockLogger: Logger;
|
|
let mockPage: Page;
|
|
|
|
beforeEach(() => {
|
|
process.env = { ...originalEnv };
|
|
|
|
mockLogger = {
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
fatal: vi.fn(),
|
|
child: vi.fn(),
|
|
flush: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
|
|
mockPage = {
|
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
url: vi.fn().mockReturnValue('https://members-ng.iracing.com/web/racing/hosted/browse-sessions'),
|
|
isClosed: vi.fn().mockReturnValue(false),
|
|
} as unknown as Page;
|
|
|
|
mockBrowserSession = {
|
|
connect: vi.fn().mockResolvedValue({ success: true }),
|
|
disconnect: vi.fn().mockResolvedValue(undefined),
|
|
getPersistentContext: vi.fn().mockReturnValue(null as unknown as BrowserContext | null),
|
|
getContext: vi.fn().mockReturnValue(null as unknown as BrowserContext | null),
|
|
getPage: vi.fn().mockReturnValue(mockPage),
|
|
getUserDataDir: vi.fn().mockReturnValue(''),
|
|
} as unknown as PlaywrightBrowserSession;
|
|
|
|
mockCookieStore = {
|
|
read: vi.fn().mockResolvedValue({
|
|
cookies: [],
|
|
origins: [],
|
|
}),
|
|
write: vi.fn().mockResolvedValue(undefined),
|
|
delete: vi.fn().mockResolvedValue(undefined),
|
|
validateCookies: vi.fn().mockReturnValue(AuthenticationState.UNKNOWN),
|
|
getSessionExpiry: vi.fn(),
|
|
getValidCookiesForUrl: vi.fn().mockReturnValue([]),
|
|
} as unknown as SessionCookieStore;
|
|
|
|
mockAuthFlow = {
|
|
getLoginUrl: vi.fn().mockReturnValue('https://members-ng.iracing.com/login'),
|
|
getPostLoginLandingUrl: vi.fn().mockReturnValue('https://members-ng.iracing.com/web/racing/hosted/browse-sessions'),
|
|
isLoginUrl: vi.fn().mockReturnValue(false),
|
|
isAuthenticatedUrl: vi.fn().mockReturnValue(true),
|
|
isLoginSuccessUrl: vi.fn().mockReturnValue(true),
|
|
detectAuthenticatedUi: vi.fn().mockResolvedValue(true),
|
|
detectLoginUi: vi.fn().mockResolvedValue(false),
|
|
navigateToAuthenticatedArea: vi.fn().mockResolvedValue(undefined),
|
|
waitForPostLoginRedirect: vi.fn().mockResolvedValue(true),
|
|
} as unknown as IPlaywrightAuthFlow;
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = { ...originalEnv };
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function createService() {
|
|
return new PlaywrightAuthSessionService(
|
|
mockBrowserSession,
|
|
mockCookieStore,
|
|
mockAuthFlow,
|
|
mockLogger,
|
|
{
|
|
navigationTimeoutMs: 1000,
|
|
loginWaitTimeoutMs: 1000,
|
|
},
|
|
);
|
|
}
|
|
|
|
it('always forces headed browser for login regardless of browser mode configuration', async () => {
|
|
const service = createService();
|
|
const result = await service.initiateLogin();
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
expect(mockBrowserSession.connect).toHaveBeenCalledWith(true);
|
|
});
|
|
|
|
it('navigates the headed page to the non-blank login URL', async () => {
|
|
const service = createService();
|
|
const result = await service.initiateLogin();
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
expect(mockAuthFlow.getLoginUrl).toHaveBeenCalledTimes(1);
|
|
|
|
expect(mockPage.goto).toHaveBeenCalledWith(
|
|
'https://members-ng.iracing.com/login',
|
|
expect.objectContaining({
|
|
waitUntil: 'domcontentloaded',
|
|
}),
|
|
);
|
|
|
|
const calledUrl = (mockPage.goto as unknown as ReturnType<typeof vi.fn>).mock.calls[0]![0] as string;
|
|
expect(calledUrl).not.toEqual('about:blank');
|
|
});
|
|
|
|
it('propagates connection failure from browserSession.connect', async () => {
|
|
(mockBrowserSession.connect as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
success: false,
|
|
error: 'boom',
|
|
});
|
|
|
|
const service = createService();
|
|
const result = await service.initiateLogin();
|
|
|
|
expect(result.isErr()).toBe(true);
|
|
const err = result.unwrapErr();
|
|
expect(err).toBeInstanceOf(Error);
|
|
expect(err.message).toContain('boom');
|
|
});
|
|
|
|
it('logs explicit headed login message for human companion flow', async () => {
|
|
const service = createService();
|
|
const result = await service.initiateLogin();
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
'Opening login in headed Playwright browser (forceHeaded=true)',
|
|
expect.objectContaining({ forceHeaded: true }),
|
|
);
|
|
});
|
|
}); |