wip
This commit is contained in:
@@ -105,4 +105,55 @@ describeMaybe('Real-site hosted session smoke – login and wizard entry (member
|
||||
},
|
||||
300_000,
|
||||
);
|
||||
|
||||
it(
|
||||
'detects login guard and does not attempt Create a Race when not authenticated',
|
||||
async () => {
|
||||
const step1Result = await adapter.executeStep(StepId.create(1), {});
|
||||
expect(step1Result.success).toBe(true);
|
||||
|
||||
const page = adapter.getPage();
|
||||
expect(page).not.toBeNull();
|
||||
|
||||
const currentUrl = page!.url();
|
||||
expect(currentUrl).not.toEqual('about:blank');
|
||||
expect(currentUrl.toLowerCase()).toContain('iracing');
|
||||
expect(currentUrl.toLowerCase()).toSatisfy((u: string) =>
|
||||
u.includes('oauth.iracing.com') ||
|
||||
u.includes('members.iracing.com') ||
|
||||
u.includes('/login'),
|
||||
);
|
||||
|
||||
const emailInput = page!
|
||||
.locator(IRACING_SELECTORS.login.emailInput)
|
||||
.first();
|
||||
const passwordInput = page!
|
||||
.locator(IRACING_SELECTORS.login.passwordInput)
|
||||
.first();
|
||||
|
||||
const hasEmail = (await emailInput.count()) > 0;
|
||||
const hasPassword = (await passwordInput.count()) > 0;
|
||||
|
||||
if (!hasEmail && !hasPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
await emailInput.waitFor({
|
||||
state: 'visible',
|
||||
timeout: IRACING_TIMEOUTS.elementWait,
|
||||
});
|
||||
await passwordInput.waitFor({
|
||||
state: 'visible',
|
||||
timeout: IRACING_TIMEOUTS.elementWait,
|
||||
});
|
||||
|
||||
const createRaceButton = page!
|
||||
.locator(IRACING_SELECTORS.hostedRacing.createRaceButton)
|
||||
.first();
|
||||
const createRaceCount = await createRaceButton.count();
|
||||
|
||||
expect(createRaceCount).toBe(0);
|
||||
},
|
||||
300_000,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import type { Page, BrowserContext } from 'playwright';
|
||||
import { PlaywrightAuthSessionService } from '../../../packages/infrastructure/adapters/automation/auth/PlaywrightAuthSessionService';
|
||||
import type { PlaywrightBrowserSession } from '../../../packages/infrastructure/adapters/automation/core/PlaywrightBrowserSession';
|
||||
import type { SessionCookieStore } from '../../../packages/infrastructure/adapters/automation/auth/SessionCookieStore';
|
||||
import type { IPlaywrightAuthFlow } from '../../../packages/infrastructure/adapters/automation/auth/PlaywrightAuthFlow';
|
||||
import type { ILogger } from '../../../packages/application/ports/ILogger';
|
||||
import { AuthenticationState } from '../../../packages/domain/value-objects/AuthenticationState';
|
||||
import { Result } from '../../../packages/shared/result/Result';
|
||||
|
||||
describe('PlaywrightAuthSessionService.initiateLogin browser mode behaviour', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
let mockBrowserSession: PlaywrightBrowserSession;
|
||||
let mockCookieStore: SessionCookieStore;
|
||||
let mockAuthFlow: IPlaywrightAuthFlow;
|
||||
let mockLogger: ILogger;
|
||||
let mockPage: Page;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
|
||||
mockLogger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
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(2);
|
||||
|
||||
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 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import type { Page, Locator } from 'playwright';
|
||||
import { PlaywrightAuthSessionService } from '../../../packages/infrastructure/adapters/automation/auth/PlaywrightAuthSessionService';
|
||||
import { AuthenticationState } from '../../../packages/domain/value-objects/AuthenticationState';
|
||||
import { BrowserAuthenticationState } from '../../../packages/domain/value-objects/BrowserAuthenticationState';
|
||||
import type { ILogger } from '../../../packages/application/ports/ILogger';
|
||||
import type { Result } from '../../../packages/shared/result/Result';
|
||||
import type { PlaywrightBrowserSession } from '../../../packages/infrastructure/adapters/automation/core/PlaywrightBrowserSession';
|
||||
import type { SessionCookieStore } from '../../../packages/infrastructure/adapters/automation/auth/SessionCookieStore';
|
||||
import type { IPlaywrightAuthFlow } from '../../../packages/infrastructure/adapters/automation/auth/PlaywrightAuthFlow';
|
||||
|
||||
describe('PlaywrightAuthSessionService.verifyPageAuthentication', () => {
|
||||
function createService(deps: {
|
||||
pageUrl: string;
|
||||
hasLoginUi: boolean;
|
||||
hasAuthUi: boolean;
|
||||
cookieState: AuthenticationState;
|
||||
}) {
|
||||
const mockLogger: ILogger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
const mockLocator: Locator = {
|
||||
first: vi.fn().mockReturnThis(),
|
||||
isVisible: vi.fn().mockImplementation(async () => deps.hasLoginUi),
|
||||
} as unknown as Locator;
|
||||
|
||||
const mockPage: Page = {
|
||||
url: vi.fn().mockReturnValue(deps.pageUrl),
|
||||
locator: vi.fn().mockReturnValue(mockLocator),
|
||||
} as unknown as Page;
|
||||
|
||||
const mockBrowserSession: PlaywrightBrowserSession = {
|
||||
getPersistentContext: vi.fn().mockReturnValue(null),
|
||||
getContext: vi.fn().mockReturnValue(null),
|
||||
getPage: vi.fn().mockReturnValue(mockPage),
|
||||
} as unknown as PlaywrightBrowserSession;
|
||||
|
||||
const mockCookieStore: SessionCookieStore = {
|
||||
read: vi.fn().mockResolvedValue({
|
||||
cookies: [{ name: 'XSESSIONID', value: 'abc', domain: 'members-ng.iracing.com', path: '/', expires: -1 }],
|
||||
origins: [],
|
||||
}),
|
||||
validateCookies: vi.fn().mockReturnValue(deps.cookieState),
|
||||
getSessionExpiry: vi.fn(),
|
||||
write: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
} as unknown as SessionCookieStore;
|
||||
|
||||
const mockAuthFlow: IPlaywrightAuthFlow = {
|
||||
getLoginUrl: () => 'https://members-ng.iracing.com/login',
|
||||
getPostLoginLandingUrl: () => 'https://members-ng.iracing.com/web/racing/hosted/browse-sessions',
|
||||
isLoginUrl: (url: string) => url.includes('/login'),
|
||||
isAuthenticatedUrl: (url: string) => url.includes('/web/racing/hosted'),
|
||||
isLoginSuccessUrl: (url: string) => url.includes('/web/racing/hosted'),
|
||||
detectAuthenticatedUi: vi.fn().mockResolvedValue(deps.hasAuthUi),
|
||||
detectLoginUi: vi.fn(),
|
||||
navigateToAuthenticatedArea: vi.fn(),
|
||||
waitForPostLoginRedirect: vi.fn(),
|
||||
} as unknown as IPlaywrightAuthFlow;
|
||||
|
||||
const service = new PlaywrightAuthSessionService(
|
||||
mockBrowserSession,
|
||||
mockCookieStore,
|
||||
mockAuthFlow,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
return { service, mockCookieStore, mockAuthFlow, mockPage };
|
||||
}
|
||||
|
||||
it('treats cookies-valid + login UI as EXPIRED (page wins over cookies)', async () => {
|
||||
const { service } = createService({
|
||||
pageUrl: 'https://members-ng.iracing.com/web/racing/hosted/browse-sessions',
|
||||
hasLoginUi: true,
|
||||
hasAuthUi: false,
|
||||
cookieState: AuthenticationState.AUTHENTICATED,
|
||||
});
|
||||
|
||||
const result: Result<BrowserAuthenticationState> = await service.verifyPageAuthentication();
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
const browserState = result.unwrap();
|
||||
expect(browserState.getCookieValidity()).toBe(true);
|
||||
expect(browserState.getPageAuthenticationStatus()).toBe(false);
|
||||
expect(browserState.getAuthenticationState()).toBe(AuthenticationState.EXPIRED);
|
||||
expect(browserState.requiresReauthentication()).toBe(true);
|
||||
});
|
||||
|
||||
it('treats cookies-valid + authenticated UI without login UI as AUTHENTICATED', async () => {
|
||||
const { service } = createService({
|
||||
pageUrl: 'https://members-ng.iracing.com/web/racing/hosted/browse-sessions',
|
||||
hasLoginUi: false,
|
||||
hasAuthUi: true,
|
||||
cookieState: AuthenticationState.AUTHENTICATED,
|
||||
});
|
||||
|
||||
const result: Result<BrowserAuthenticationState> = await service.verifyPageAuthentication();
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
const browserState = result.unwrap();
|
||||
expect(browserState.getCookieValidity()).toBe(true);
|
||||
expect(browserState.getPageAuthenticationStatus()).toBe(true);
|
||||
expect(browserState.getAuthenticationState()).toBe(AuthenticationState.AUTHENTICATED);
|
||||
expect(browserState.requiresReauthentication()).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user