From db19d7d9f4adb80913e7a5f771b0b4fac970568d Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 15 May 2026 19:22:37 +0200 Subject: [PATCH] test(web-e2e): mock provider target registration --- apps/web-e2e/src/provider-target-route.ts | 80 +++++++++++++++++++++++ apps/web-e2e/src/stalker.e2e.ts | 23 ++++++- apps/web-e2e/src/xtream.e2e.ts | 39 ++++++++--- 3 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 apps/web-e2e/src/provider-target-route.ts diff --git a/apps/web-e2e/src/provider-target-route.ts b/apps/web-e2e/src/provider-target-route.ts new file mode 100644 index 000000000..50ebd11ef --- /dev/null +++ b/apps/web-e2e/src/provider-target-route.ts @@ -0,0 +1,80 @@ +import { type Page } from '@playwright/test'; + +interface ProviderTargetPayload { + readonly url: string; +} + +const BACKEND_ORIGIN = 'http://localhost:3000'; +const CORS_HEADERS = { + 'access-control-allow-headers': 'content-type', + 'access-control-allow-methods': 'POST, OPTIONS', + 'access-control-allow-origin': 'http://localhost:4200', +} as const; + +export async function interceptProviderTargetRegistration( + page: Page +): Promise> { + const providerTargets = new Map(); + + await page.route(`${BACKEND_ORIGIN}/provider-targets**`, async (route) => { + if (route.request().method() === 'OPTIONS') { + await route.fulfill({ + body: '', + headers: CORS_HEADERS, + status: 200, + }); + return; + } + + const providerUrl = getProviderUrl(route.request().postDataJSON()); + if (!providerUrl) { + await route.fulfill({ + body: JSON.stringify({ + message: 'Missing url', + status: 400, + }), + contentType: 'application/json', + headers: CORS_HEADERS, + status: 400, + }); + return; + } + + const targetId = Buffer.from(providerUrl).toString('base64url'); + providerTargets.set(targetId, providerUrl); + + await route.fulfill({ + body: JSON.stringify({ targetId }), + contentType: 'application/json', + headers: CORS_HEADERS, + status: 200, + }); + }); + + return providerTargets; +} + +export function getRegisteredProviderUrl( + url: URL, + providerTargets: ReadonlyMap +): string | null { + const targetId = url.searchParams.get('targetId'); + if (!targetId) { + return url.searchParams.get('url'); + } + + return providerTargets.get(targetId) ?? null; +} + +function getProviderUrl(value: unknown): string | null { + if ( + value && + typeof value === 'object' && + 'url' in value && + typeof (value as Partial).url === 'string' + ) { + return (value as ProviderTargetPayload).url; + } + + return null; +} diff --git a/apps/web-e2e/src/stalker.e2e.ts b/apps/web-e2e/src/stalker.e2e.ts index 799b4cbe8..aa8e8bc5e 100644 --- a/apps/web-e2e/src/stalker.e2e.ts +++ b/apps/web-e2e/src/stalker.e2e.ts @@ -5,6 +5,10 @@ import { type Page, test, } from '@playwright/test'; +import { + getRegisteredProviderUrl, + interceptProviderTargetRegistration, +} from './provider-target-route'; /** * Stalker Portal E2E Tests @@ -42,11 +46,25 @@ const MINIMAL_MAC = '00:1A:79:00:00:03'; * any app environment configuration. */ async function interceptStalkerRequests(page: Page): Promise { + const providerTargets = await interceptProviderTargetRegistration(page); + await page.route('**/localhost:3000/stalker**', async (route) => { const originalUrl = new URL(route.request().url()); const mockUrl = new URL(BACKEND_PROXY); - // Forward all query params unchanged + const providerUrl = getRegisteredProviderUrl( + originalUrl, + providerTargets + ); + + if (providerUrl) { + mockUrl.searchParams.set('url', providerUrl); + } + originalUrl.searchParams.forEach((value, key) => { + if (key === 'targetId') { + return; + } + mockUrl.searchParams.set(key, value); }); await route.continue({ url: mockUrl.toString() }); @@ -183,9 +201,8 @@ test('@stalker VOD — content list loads after selecting a category', async ({ const contentItems = page.locator( '.content-card, [data-test-id="channel-item"], mat-card' ); + await expect(contentItems).not.toHaveCount(0, { timeout: 10_000 }); await expect(contentItems.first()).toBeVisible({ timeout: 10_000 }); - const itemCount = await contentItems.count(); - expect(itemCount).toBeGreaterThan(0); }); test('@stalker minimal scenario — correct item counts', async ({ page }) => { diff --git a/apps/web-e2e/src/xtream.e2e.ts b/apps/web-e2e/src/xtream.e2e.ts index 71df92d8c..f1c54b529 100644 --- a/apps/web-e2e/src/xtream.e2e.ts +++ b/apps/web-e2e/src/xtream.e2e.ts @@ -1,4 +1,8 @@ import { APIRequestContext, test, expect, Page } from '@playwright/test'; +import { + getRegisteredProviderUrl, + interceptProviderTargetRegistration, +} from './provider-target-route'; /** * Xtream Codes E2E Tests @@ -33,10 +37,25 @@ const EPG_PASSWORD = 'epg'; * to the mock server. This avoids any real backend requirement. */ async function interceptXtreamRequests(page: Page): Promise { + const providerTargets = await interceptProviderTargetRegistration(page); + await page.route('**/localhost:3000/xtream**', async (route) => { const originalUrl = new URL(route.request().url()); const mockUrl = new URL(`${MOCK_SERVER}/xtream`); + const providerUrl = getRegisteredProviderUrl( + originalUrl, + providerTargets + ); + + if (providerUrl) { + mockUrl.searchParams.set('url', providerUrl); + } + originalUrl.searchParams.forEach((value, key) => { + if (key === 'targetId') { + return; + } + mockUrl.searchParams.set(key, value); }); await route.continue({ url: mockUrl.toString() }); @@ -289,21 +308,19 @@ test('@xtream epg fixture — short epg starts at the current program, respects expect(decodeXtreamText(nextListing.title)).toBe('Market Wrap'); const now = Math.floor(Date.now() / 1000); - expect(Number.parseInt(currentListing.start_timestamp, 10)).toBeLessThanOrEqual( - now - ); - expect(Number.parseInt(currentListing.stop_timestamp, 10)).toBeGreaterThanOrEqual( - now - ); + expect( + Number.parseInt(currentListing.start_timestamp, 10) + ).toBeLessThanOrEqual(now); + expect( + Number.parseInt(currentListing.stop_timestamp, 10) + ).toBeGreaterThanOrEqual(now); expect(currentListing.start).not.toBe( formatXtreamDateTime( Number.parseInt(currentListing.start_timestamp, 10) ) ); expect(currentListing.end).not.toBe( - formatXtreamDateTime( - Number.parseInt(currentListing.stop_timestamp, 10) - ) + formatXtreamDateTime(Number.parseInt(currentListing.stop_timestamp, 10)) ); }); @@ -344,7 +361,9 @@ test('@xtream epg fixture — full epg and legacy alias return the same ordered decodeXtreamText(listing.title) === 'Late Edition' ); if (!boundaryListing) { - throw new Error('Expected the full EPG fixture to include Late Edition.'); + throw new Error( + 'Expected the full EPG fixture to include Late Edition.' + ); } const boundaryStart = new Date(