diff --git a/apps/electron-backend-e2e/src/stalker-discovery.e2e.ts b/apps/electron-backend-e2e/src/stalker-discovery.e2e.ts new file mode 100644 index 000000000..6caf03578 --- /dev/null +++ b/apps/electron-backend-e2e/src/stalker-discovery.e2e.ts @@ -0,0 +1,124 @@ +import { + closeElectronApp, + expect, + launchElectronApp, + test, +} from './electron-test-fixtures'; +import { + StalkerReplayRun, + withStalkerReplayRun, +} from './support/stalker-replay'; + +test.describe('Electron Stalker endpoint discovery', () => { + test('@stalker @electron discovers a full session from a custom-prefix source without a learned endpoint', async ({ + dataDir, + }) => { + const replay = await withStalkerReplayRun( + 'resolver/custom-prefix-full', + async (run) => { + const app = await launchElectronApp(dataDir); + + try { + const ready = await app.mainWindow.evaluate( + async ({ macAddress, sourceUrl }) => { + const open = window.electron?.stalkerSessionOpen; + if (!open) { + throw new Error( + 'stalker-session-open-unavailable' + ); + } + return open({ + descriptor: { + connectionMode: 'provisional', + macAddress, + playlistRef: 'e2e-custom-prefix-discovery', + profilePreset: { + id: 'mag250-public-5_1-minimal-v1', + version: 1, + }, + provisionalReason: 'migration', + sourceUrl, + }, + }); + }, + { + macAddress: requiredInput(run, 'mac'), + sourceUrl: run.entryUrl, + } + ); + + expect(ready.kind, JSON.stringify(ready)).toBe('ready'); + expect(ready).toMatchObject({ + connectionMode: 'provisional', + endpoint: `${requiredOrigin( + run, + 'portal' + )}/tenant/server/load.php`, + kind: 'ready', + recipe: 'full-session', + }); + if ( + ready.kind !== 'ready' || + ready.connectionMode !== 'provisional' + ) { + throw new Error( + `expected-provisional-ready:${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, + }); + }, + ready.attemptRef + ); + expect(committed).toMatchObject({ + action: 'commit', + kind: 'success', + }); + } finally { + await closeElectronApp(app); + } + } + ); + + expect(replay.finalization).toMatchObject({ + ledger: { + mismatchCounts: {}, + operationCounts: { + 'custom-handshake': 1, + 'custom-landing': 1, + 'custom-profile': 1, + }, + terminalState: 'complete', + }, + ok: true, + }); + }); +}); + +function requiredInput(run: StalkerReplayRun, name: string): string { + const value = run.inputs[name]; + if (!value) { + throw new Error(`stalker-replay-input-missing:${name}`); + } + return value; +} + +function requiredOrigin(run: StalkerReplayRun, name: string): string { + const value = run.origins[name]; + if (!value) { + throw new Error(`stalker-replay-origin-missing:${name}`); + } + return value; +} diff --git a/apps/stalker-mock-server/fixtures/replay/resolver/custom-prefix-full.json b/apps/stalker-mock-server/fixtures/replay/resolver/custom-prefix-full.json new file mode 100644 index 000000000..bbeed993f --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/resolver/custom-prefix-full.json @@ -0,0 +1,244 @@ +{ + "description": "Synthetic /tenant/c/ source discovers and authenticates its sibling custom-prefix full-session endpoint without a learned hint.", + "entry": { + "origin": "portal", + "path": "/tenant/c/" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/tenant/server/load.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "custom-landing", + "method": "GET", + "operation": "custom-landing", + "origin": "portal", + "path": "/tenant/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 + } + } + ], + "mode": "ordered", + "name": "custom-landing", + "nextState": "full-session", + "state": "start" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "custom-handshake", + "method": "GET", + "operation": "custom-handshake", + "origin": "portal", + "path": "/tenant/server/load.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "token": { + "kind": "generate", + "symbol": "custom-token", + "valueKind": "token" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "custom-profile", + "method": "GET", + "operation": "custom-profile", + "origin": "portal", + "path": "/tenant/server/load.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "custom-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "0", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "id": 1, + "status": 0 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "full-session", + "nextState": "complete", + "state": "full-session" + } + ], + "scenarioId": "resolver-custom-prefix-full", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "mac", + "valueKind": "mac" + } + ], + "terminalState": "complete" +} 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 7fd8c07ca..62a98a64a 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 @@ -47,6 +47,7 @@ const EXPECTED_SCENARIOS = [ 'redirect-repeated-loop', 'refresh-concurrent-rejections', 'refresh-exhausted-retry', + 'resolver-custom-prefix-full', 'resolver-direct-load-jsonp', 'resolver-direct-portal-statusless', 'resolver-learned-rediscovery',