From c2d1bff89d896c32e75129adc4038583bc5338de Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 2 Aug 2026 07:49:39 +0200 Subject: [PATCH] fix(playback): apply header override to Stalker radio, redact mock cookie log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Codex review feedback on #1335: - The radio branch of the Stalker live layout renders the dedicated audio player, never WebPlayerViewComponent, so the resolved portal headers were built but never applied — an auth-gated radio stream still 403'd. The override sync is extracted into ElectronStreamHeadersService (single owner of the scoped override slot, with clear-only-while-owning semantics so a destroyed consumer cannot wipe a newer consumer's override), applied by WebPlayerViewComponent for video players and by the radio branch before the audio element gets its URL. The service feature-detects the bridge method so partial bridges behave like the PWA instead of throwing. - The gated-stream mock no longer logs the raw Cookie header on 403 — presence only, matching the Authorization logging. - The gated scenario now serves an audio fixture for radio create_link and the Electron e2e covers the radio path end-to-end (bare request 403s, built-in audio player advances past the gate). Co-Authored-By: Claude Fable 5 --- .../src/stalker-playback-headers.e2e.ts | 55 +++++++ .../src/app/handlers/create-link.handler.ts | 6 +- apps/stalker-mock-server/src/main.ts | 47 ++++-- docs/architecture/electron-security.md | 15 +- docs/architecture/stalker-portal.md | 10 +- ...alker-live-stream-layout.component.spec.ts | 41 +++++ .../stalker-live-stream-layout.component.ts | 19 +++ libs/ui/playback/src/index.ts | 1 + .../electron-stream-headers.service.spec.ts | 141 ++++++++++++++++++ .../electron-stream-headers.service.ts | 104 +++++++++++++ .../web-player-view.component.ts | 69 ++------- 11 files changed, 423 insertions(+), 85 deletions(-) create mode 100644 libs/ui/playback/src/lib/web-player-view/electron-stream-headers.service.spec.ts create mode 100644 libs/ui/playback/src/lib/web-player-view/electron-stream-headers.service.ts diff --git a/apps/electron-backend-e2e/src/stalker-playback-headers.e2e.ts b/apps/electron-backend-e2e/src/stalker-playback-headers.e2e.ts index 9884dc501..c8e7e8f1d 100644 --- a/apps/electron-backend-e2e/src/stalker-playback-headers.e2e.ts +++ b/apps/electron-backend-e2e/src/stalker-playback-headers.e2e.ts @@ -91,3 +91,58 @@ test('@electron @stalker built-in player plays an auth-gated portal stream', asy await closeElectronApp(app); } }); + +test('@electron @stalker built-in audio player plays an auth-gated radio stream', async ({ + dataDir, + request, +}) => { + await resetMockServers(request, ['stalker']); + + // The radio branch renders the dedicated audio player instead of + // WebPlayerViewComponent, so it exercises the Stalker live layout's own + // header wiring — a gap the ITV test above cannot catch. + const bareResponse = await request.get( + `${stalkerMockServer}/stream/gated/audio.mp4` + ); + expect(bareResponse.status()).toBe(403); + + const app = await launchElectronApp(dataDir); + + try { + await addStalkerPortal(app.mainWindow, { + macAddress: GATED_MAC, + portalUrl: FULL_PORTAL_URL, + }); + await waitForStalkerCatalog(app.mainWindow); + + await app.mainWindow.getByRole('link', { name: /radio/i }).click(); + await app.mainWindow.waitForURL(/stalker.*radio/); + + const categories = app.mainWindow.locator('.category-item'); + await expect(categories.first()).toBeVisible({ timeout: 10_000 }); + await categories.first().click(); + + const channels = app.mainWindow.locator( + '[data-test-id="channel-item"]' + ); + await expect(channels.first()).toBeVisible({ timeout: 20_000 }); + await channels.first().click(); + + // The bare