From 23d0ca8afda76fac614671f41f2f95022eeee94d Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 1 Aug 2026 19:32:17 +0200 Subject: [PATCH] test(stalker): force a real auth failure before asserting it stays hidden Review follow-up on #1324 (Codex 2xP2 + 1xP3), all valid: - The "never surfaces the plain-text auth failure" test only performed a successful import, so its negative body assertions were vacuous. It now imports with a MAC outside the Infomir OUI: the strict endpoint answers get_profile with a bare {status:1}, no token is ever adopted, and every content request keeps returning "Authorization failed." Unlike an invalidated session this cannot be repaired by the client retry, so the failure is genuinely observed (asserted directly against the proxy) and only then checked for not leaking into the UI. - docs/architecture/xtream-mock-server.md still documented the wildcard bind that 4b31f7167 replaced with a loopback default; it now states the new default and the HOST=0.0.0.0 opt-in needed for phone/STB/container. - Removed a dangling "Known app-side gap: the" fragment left in the stalker mock README. Co-Authored-By: Claude Fable 5 --- apps/stalker-mock-server/README.md | 2 - apps/web-e2e/src/stalker.e2e.ts | 56 +++++++++++++++++++++---- docs/architecture/xtream-mock-server.md | 10 +++-- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/apps/stalker-mock-server/README.md b/apps/stalker-mock-server/README.md index e78cf3e34..6bd9fd898 100644 --- a/apps/stalker-mock-server/README.md +++ b/apps/stalker-mock-server/README.md @@ -52,8 +52,6 @@ imported as a **full portal** (handshake + token + watchdog), anything else as a > upcoming fix that unifies those two predicates. Until then, import full > portals through a `/stalker_portal/...` URL. -> **Known app-side gap:** the - The strict endpoint reproduces the parts of Stalker 4.9.35 that a client can actually get wrong: diff --git a/apps/web-e2e/src/stalker.e2e.ts b/apps/web-e2e/src/stalker.e2e.ts index ad79ca98f..0600813de 100644 --- a/apps/web-e2e/src/stalker.e2e.ts +++ b/apps/web-e2e/src/stalker.e2e.ts @@ -64,8 +64,12 @@ const LEGACY_PAGINATION_MAC = '00:1A:79:00:00:06'; * The Infomir OUI matters: the strict endpoint validates the MAC format. */ const AUTH_FLOW_MAC = '00:1A:79:AD:00:01'; -const AUTH_TEXT_MAC = '00:1A:79:AD:00:02'; const AUTH_REAUTH_MAC = '00:1A:79:AD:00:03'; +/** + * Deliberately NOT an Infomir MAC: the strict endpoint rejects get_profile for + * it, so no token is ever adopted and content requests fail permanently. + */ +const AUTH_REJECTED_MAC = 'AA:BB:CC:DD:EE:01'; // --------------------------------------------------------------------------- // Helpers @@ -162,9 +166,9 @@ async function addStalkerPortal( */ async function addFullStalkerPortal( page: Page, - options: { name?: string; mac: string } + options: { name?: string; mac: string; expectContent?: boolean } ): Promise { - const { name = 'Full Stalker Portal', mac } = options; + const { name = 'Full Stalker Portal', mac, expectContent = true } = options; await page.getByRole('button', { name: 'Add playlist' }).click(); const dialog = page.locator('mat-dialog-container'); @@ -179,7 +183,10 @@ async function addFullStalkerPortal( await expect(addButton).toBeEnabled({ timeout: 10_000 }); await addButton.click(); await expect(dialog).toBeHidden(); - await page.waitForURL(/stalker.*vod/, { timeout: 30_000 }); + + if (expectContent) { + await page.waitForURL(/stalker.*vod/, { timeout: 30_000 }); + } } /** Portal actions the app sent, in order, with the token each carried. */ @@ -870,11 +877,46 @@ test.describe('@stalker full portal authentication', () => { test('never surfaces the portal plain-text auth failure as content', async ({ page, + request, }) => { - await addFullStalkerPortal(page, { mac: AUTH_TEXT_MAC }); + const requests = recordPortalRequests(page); - // A body of "Authorization failed." must never be rendered — if the - // token pipeline breaks, the app has to fail loudly instead. + // A MAC outside the Infomir OUI makes the strict endpoint answer + // get_profile with a bare {status:1}, so no token is ever adopted and + // every content request keeps returning the plain-text failure. Unlike + // an invalidated session this cannot be repaired by the app's retry, + // which is what makes the negative assertion below meaningful instead + // of vacuous. + const failureBody = await ( + await request.get( + `${BACKEND_PROXY}?url=${encodeURIComponent( + FULL_PORTAL_URL + )}&macAddress=${encodeURIComponent( + AUTH_REJECTED_MAC + )}&action=get_categories&type=vod` + ) + ).json(); + expect(failureBody.payload).toBe('Authorization failed.'); + + await addFullStalkerPortal(page, { + mac: AUTH_REJECTED_MAC, + expectContent: false, + }); + + // The app must have actually hit the failing portal... + await expect + .poll( + () => + requests.filter((entry) => + CONTENT_ACTIONS.includes(entry.action) + ).length, + { timeout: 30_000 } + ) + .toBeGreaterThan(0); + + // ...and must never render the raw portal response as content. A + // portal answers auth failures with HTTP 200 + plain text, so an app + // that trusts the status code would happily paint these strings. await expect(page.locator('body')).not.toContainText( 'Authorization failed.' ); diff --git a/docs/architecture/xtream-mock-server.md b/docs/architecture/xtream-mock-server.md index 8ca8141ba..8d9105db5 100644 --- a/docs/architecture/xtream-mock-server.md +++ b/docs/architecture/xtream-mock-server.md @@ -116,10 +116,12 @@ The control plane is absent by default. It is mounted only when `IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN` and a literal loopback `HOST` (`127.0.0.1` or `::1`). Every `/__control/*` request must carry that exact value in `x-iptvnator-performance-token`, including `OPTIONS` preflight requests. -Configuration is validated before the HTTP listener opens. Normal development -mode preserves the legacy wildcard bind when `HOST` is unset; control mode -instead defaults to `127.0.0.1` and rejects an explicitly configured -non-loopback host. The Nx serve targets do not pin `PORT`, so an explicit shell +Configuration is validated before the HTTP listener opens. Both modes now +default to `127.0.0.1` when `HOST` is unset — the fixture serves fabricated but +unauthenticated content, so it should not be reachable from other hosts by +accident. Set `HOST=0.0.0.0` explicitly to expose it, which is what you need +when driving the mock from a phone, an STB, a container, or another machine. +Control mode additionally *rejects* an explicitly configured non-loopback host. The Nx serve targets do not pin `PORT`, so an explicit shell value reaches the parser; its no-value default remains `3211`. Use a dedicated port rather than the normal `3211` E2E server: