diff --git a/apps/electron-backend-e2e/src/stalker-auth.e2e.ts b/apps/electron-backend-e2e/src/stalker-auth.e2e.ts index f61ba02c3..c18f3f80f 100644 --- a/apps/electron-backend-e2e/src/stalker-auth.e2e.ts +++ b/apps/electron-backend-e2e/src/stalker-auth.e2e.ts @@ -59,6 +59,181 @@ test.describe('Electron Stalker authenticated sessions', () => { }); }); + test('@stalker @electron keeps authenticated catalog and playback identity in main', async ({ + dataDir, + }) => { + const replay = await withStalkerReplayRun( + 'e2e/full-session-catalog-playback', + async (run) => { + const app = await launchElectronApp(dataDir); + + try { + const { ready } = await openStatus2Session( + app.mainWindow, + run, + 'e2e-catalog-playback' + ); + if ( + ready.kind !== 'ready' || + ready.recipe !== 'full-session' || + ready.connectionMode !== 'provisional' + ) { + throw new Error( + `expected-full-session:${JSON.stringify(ready)}` + ); + } + const committed = await app.mainWindow.evaluate( + async ({ attemptRef }) => { + const control = + window.electron?.stalkerSessionControl; + if (!control) { + throw new Error( + 'stalker-session-control-unavailable' + ); + } + + return control({ + action: 'commit', + attemptRef, + }); + }, + { attemptRef: ready.attemptRef } + ); + expect(committed).toMatchObject({ + action: 'commit', + kind: 'success', + }); + + const catalog = await app.mainWindow.evaluate( + async ({ leaseRef }) => { + const request = + window.electron?.stalkerSessionRequest; + if (!request) { + throw new Error( + 'stalker-session-request-unavailable' + ); + } + + return request({ + leaseRef, + operation: 'get-ordered-list', + parameters: { + category: '*', + contentType: 'itv', + page: 1, + }, + }); + }, + { leaseRef: ready.leaseRef } + ); + if ( + catalog.kind !== 'success' || + catalog.operation !== 'get-ordered-list' + ) { + throw new Error( + `expected-catalog-success:${JSON.stringify(catalog)}` + ); + } + expect(catalog).toMatchObject({ + kind: 'success', + operation: 'get-ordered-list', + payload: { + hasMore: false, + items: [ + { + contentType: 'itv', + id: 'channel-1', + title: 'Synthetic Channel', + }, + ], + page: 1, + pageSize: 1, + total: 1, + totalPages: 1, + }, + }); + const command = catalog.payload.items[0]?.command; + if (!command) { + throw new Error('expected-catalog-command'); + } + + const link = await app.mainWindow.evaluate( + async ({ command, leaseRef }) => { + const request = + window.electron?.stalkerSessionRequest; + if (!request) { + throw new Error( + 'stalker-session-request-unavailable' + ); + } + + return request({ + leaseRef, + operation: 'create-link', + parameters: { + command, + contentType: 'itv', + }, + }); + }, + { + command, + leaseRef: ready.leaseRef, + } + ); + expect(link).toMatchObject({ + kind: 'success', + operation: 'create-link', + payload: { + streamUrl: new URL( + '/stream/ready.m3u8', + requiredOrigin(run, 'portal') + ).toString(), + }, + }); + if ( + link.kind !== 'success' || + link.operation !== 'create-link' + ) { + throw new Error( + `expected-create-link-success:${JSON.stringify(link)}` + ); + } + expect(Object.keys(link.payload).sort()).toEqual([ + 'playbackContextRef', + 'streamUrl', + ]); + expect(link.payload.playbackContextRef).toEqual( + expect.any(String) + ); + expect(link.payload.playbackContextRef).not.toHaveLength(0); + expect( + JSON.stringify({ catalog, committed, link, ready }) + ).not.toMatch(/authorization|bearer|cookie|token/i); + } finally { + await closeElectronApp(app); + } + } + ); + + expect(replay.finalization).toMatchObject({ + ledger: { + mismatchCounts: {}, + operationCounts: { + 'anonymous-probe': 1, + 'catalog-page': 1, + 'create-link': 1, + 'do-auth': 1, + handshake: 1, + 'profile-first': 1, + 'profile-second': 1, + }, + terminalState: 'complete', + }, + ok: true, + }); + }); + test('@stalker @electron keeps concurrent authenticated attempts isolated by replay run', async ({ dataDir, }) => { @@ -160,7 +335,9 @@ async function openStatus2Session( } ); if (challenge.kind !== 'credentials-required') { - throw new Error('expected-credentials-challenge'); + throw new Error( + `expected-credentials-challenge:${JSON.stringify(challenge)}` + ); } const ready = await page.evaluate( diff --git a/apps/stalker-mock-server/fixtures/replay/authentication/status2-second-step.json b/apps/stalker-mock-server/fixtures/replay/authentication/status2-second-step.json index a86ab284d..59df1f65b 100644 --- a/apps/stalker-mock-server/fixtures/replay/authentication/status2-second-step.json +++ b/apps/stalker-mock-server/fixtures/replay/authentication/status2-second-step.json @@ -198,10 +198,6 @@ "exact": { "action": "get_profile", "auth_second_step": "0", - "random": { - "kind": "ref", - "symbol": "challenge" - }, "type": "stb" }, "present": [] @@ -356,10 +352,6 @@ "exact": { "action": "get_profile", "auth_second_step": "1", - "random": { - "kind": "ref", - "symbol": "challenge" - }, "type": "stb" }, "present": [] diff --git a/apps/stalker-mock-server/fixtures/replay/e2e/full-session-catalog-playback.json b/apps/stalker-mock-server/fixtures/replay/e2e/full-session-catalog-playback.json new file mode 100644 index 000000000..028432686 --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/e2e/full-session-catalog-playback.json @@ -0,0 +1,740 @@ +{ + "description": "Synthetic full session rotates server cookies across credential authentication, catalog paging, and playback link creation.", + "entry": { + "origin": "portal", + "path": "/c/" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "anonymous-probe", + "method": "GET", + "operation": "anonymous-probe", + "origin": "portal", + "path": "/c/", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "mac", + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "authorization", + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "device_id", + "device_id2", + "mac", + "password", + "signature", + "signature2", + "sn", + "token", + "username" + ], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": {}, + "status": 204 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "handshake", + "method": "GET", + "operation": "handshake", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + }, + "stb_lang": "en", + "timezone": "UTC" + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": { + "accept-language": "en-US", + "user-agent": "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250", + "x-user-agent": "Model: MAG250" + }, + "present": [ + "referer" + ] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "JsHttpRequest": "1-xml", + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "random": { + "kind": "ref", + "symbol": "challenge" + }, + "token": { + "kind": "ref", + "symbol": "session-token" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ], + "set-cookie": [ + { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "session=" + }, + { + "kind": "ref", + "symbol": "session-cookie-1" + }, + { + "kind": "literal", + "value": "; Path=/; HttpOnly" + } + ] + } + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "profile-first", + "method": "GET", + "operation": "profile-first", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + }, + "session": { + "kind": "ref", + "symbol": "session-cookie-1" + }, + "stb_lang": "en", + "timezone": "UTC" + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "accept-language": "en-US", + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + }, + "user-agent": "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250", + "x-user-agent": "Model: MAG250" + }, + "present": [ + "referer" + ] + }, + "query": { + "absent": [ + "device_id", + "device_id2", + "password", + "signature", + "signature2", + "sn", + "username" + ], + "exact": { + "JsHttpRequest": "1-xml", + "action": "get_profile", + "auth_second_step": "0", + "client_type": "STB", + "hd": "1", + "language": "en", + "not_valid_token": "0", + "stb_type": "MAG250", + "timezone": "UTC", + "type": "stb" + }, + "present": [ + "metrics" + ] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "status": 2 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "do-auth", + "method": "GET", + "operation": "do-auth", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + }, + "session": { + "kind": "ref", + "symbol": "session-cookie-1" + }, + "stb_lang": "en", + "timezone": "UTC" + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "accept-language": "en-US", + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + }, + "user-agent": "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250", + "x-user-agent": "Model: MAG250" + }, + "present": [ + "referer" + ] + }, + "query": { + "absent": [], + "exact": { + "JsHttpRequest": "1-xml", + "action": "do_auth", + "login": { + "kind": "ref", + "symbol": "username" + }, + "password": { + "kind": "ref", + "symbol": "password" + }, + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": true + } + }, + "headers": { + "content-type": [ + "application/json" + ], + "set-cookie": [ + { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "session=" + }, + { + "kind": "ref", + "symbol": "session-cookie-2" + }, + { + "kind": "literal", + "value": "; Path=/; HttpOnly" + } + ] + } + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "profile-second", + "method": "GET", + "operation": "profile-second", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + }, + "session": { + "kind": "ref", + "symbol": "session-cookie-2" + }, + "stb_lang": "en", + "timezone": "UTC" + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "accept-language": "en-US", + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + }, + "user-agent": "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250", + "x-user-agent": "Model: MAG250" + }, + "present": [ + "referer" + ] + }, + "query": { + "absent": [ + "device_id", + "device_id2", + "password", + "signature", + "signature2", + "sn", + "username" + ], + "exact": { + "JsHttpRequest": "1-xml", + "action": "get_profile", + "auth_second_step": "1", + "client_type": "STB", + "hd": "1", + "language": "en", + "not_valid_token": "0", + "stb_type": "MAG250", + "timezone": "UTC", + "type": "stb" + }, + "present": [ + "metrics" + ] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "status": 0 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "catalog-page", + "method": "GET", + "operation": "catalog-page", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + }, + "session": { + "kind": "ref", + "symbol": "session-cookie-2" + }, + "stb_lang": "en", + "timezone": "UTC" + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "accept-language": "en-US", + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + }, + "user-agent": "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250", + "x-user-agent": "Model: MAG250" + }, + "present": [ + "referer" + ] + }, + "query": { + "absent": [ + "mac", + "password", + "token", + "username" + ], + "exact": { + "JsHttpRequest": "1-xml", + "action": "get_ordered_list", + "category": "*", + "p": "1", + "type": "itv" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "cur_page": 1, + "data": [ + { + "cmd": "ffmpeg /stream/source.m3u8", + "id": "channel-1", + "name": "Synthetic Channel", + "number": 7, + "title": "Synthetic Channel" + } + ], + "max_page_items": 1, + "total_items": 1, + "total_pages": 1 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ], + "set-cookie": [ + { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "session=" + }, + { + "kind": "ref", + "symbol": "session-cookie-3" + }, + { + "kind": "literal", + "value": "; Path=/; HttpOnly" + } + ] + } + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "create-link", + "method": "GET", + "operation": "create-link", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + }, + "session": { + "kind": "ref", + "symbol": "session-cookie-3" + }, + "stb_lang": "en", + "timezone": "UTC" + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "accept-language": "en-US", + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + }, + "user-agent": "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250", + "x-user-agent": "Model: MAG250" + }, + "present": [ + "referer" + ] + }, + "query": { + "absent": [ + "mac", + "password", + "token", + "username" + ], + "exact": { + "JsHttpRequest": "1-xml", + "action": "create_link", + "cmd": "ffmpeg /stream/source.m3u8", + "disable_ad": "0", + "download": "0", + "type": "itv" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "cmd": "/stream/ready.m3u8" + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "full-session-catalog-playback", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "e2e-full-session-catalog-playback", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "mac", + "valueKind": "mac" + }, + { + "kind": "generate", + "symbol": "username", + "valueKind": "credential" + }, + { + "kind": "generate", + "symbol": "password", + "valueKind": "credential" + }, + { + "kind": "generate", + "symbol": "session-token", + "valueKind": "token" + }, + { + "kind": "generate", + "symbol": "challenge", + "valueKind": "random" + }, + { + "kind": "generate", + "symbol": "session-cookie-1", + "valueKind": "cookie" + }, + { + "kind": "generate", + "symbol": "session-cookie-2", + "valueKind": "cookie" + }, + { + "kind": "generate", + "symbol": "session-cookie-3", + "valueKind": "cookie" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/src/app/replay/replay-control-plane.spec.ts b/apps/stalker-mock-server/src/app/replay/replay-control-plane.spec.ts index 3056c7721..ff7ecc199 100644 --- a/apps/stalker-mock-server/src/app/replay/replay-control-plane.spec.ts +++ b/apps/stalker-mock-server/src/app/replay/replay-control-plane.spec.ts @@ -534,7 +534,7 @@ describe('replay control-plane lifecycle and public response', () => { ), }, inputs: { - mac: expect.stringMatching(/^02(?::[a-f0-9]{2}){5}$/), + mac: expect.stringMatching(/^02(?::[A-F0-9]{2}){5}$/), password: expect.stringMatching(/^test-credential-/), username: expect.stringMatching(/^test-credential-/), }, diff --git a/apps/stalker-mock-server/src/app/replay/replay-fixture-corpus.spec.ts b/apps/stalker-mock-server/src/app/replay/replay-fixture-corpus.spec.ts index 2af4ce0c7..15f6c130f 100644 --- a/apps/stalker-mock-server/src/app/replay/replay-fixture-corpus.spec.ts +++ b/apps/stalker-mock-server/src/app/replay/replay-fixture-corpus.spec.ts @@ -36,6 +36,7 @@ const EXPECTED_SCENARIOS = [ 'classifier-waf-403', 'cookies-managed-shadow', 'cookies-session-isolation', + 'e2e-full-session-catalog-playback', 'redirect-auth-query', 'redirect-identity-pause', 'redirect-landing-approval',