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 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-01 19:32:17 +02:00
1 parent 4b31f71672
commit 23d0ca8afd
3 files changed
+55 -13

No files matched your search

-2
View File
@@ -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:
+49 -7
View File
@@ -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<void> {
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.'
);
+6 -4
View File
@@ -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: