diff --git a/apps/stalker-mock-server/README.md b/apps/stalker-mock-server/README.md index cefba4fb6..b77100e1e 100644 --- a/apps/stalker-mock-server/README.md +++ b/apps/stalker-mock-server/README.md @@ -86,7 +86,7 @@ actually get wrong: | `00:1A:79:00:00:06` | **legacy-pagination** | No `get_all_channels` support — tests the paginated `get_ordered_list` crawl fallback for the full ITV channel list | | `00:1A:79:00:00:07` | **marketing-demo** | 35 original poster movies with the newest 20 first — safe for screenshots and marketing | | `00:1A:79:00:00:08` | **login-required** | `get_profile` answers `status: 2` until the client completes `do_auth` with non-empty credentials. The app drives this end to end: enter a username and password in the Stalker import dialog and it runs `do_auth`, then retries `get_profile` with `auth_second_step=1`. Covered by `stalker.e2e.ts` (`@stalker full portal authentication`) | -| `00:1A:79:AE::02` | **login-required** | Worker-scoped alias range used by parallel auth E2E runs; it has the same login contract as `00:1A:79:00:00:08` but independent per-MAC state | +| `00:1A:79:AE::02` | **login-required** | Parallel-slot-scoped alias range used by concurrent auth E2E runs; it has the same login contract as `00:1A:79:00:00:08` but independent per-MAC state | | `00:1A:79:00:00:09` | **gated-stream** | `create_link` returns a local `/stream/gated/…` URL that answers 403 without the mac cookie and the MAC's current Bearer token — proves a player's media requests really carry the portal credentials | | `00:1A:79:00:00:0A` | **static-channel-cmd** | ITV rows carry a directly playable `cmd` with `use_http_tmp_link`/`use_load_balancing` both `'0'` — a client honouring the flags must play them without calling `create_link` | | `` | **auto** | MAC bytes used as seed → deterministic unique dataset | @@ -180,9 +180,10 @@ everything. The sibling specs that talk to this server (`self-hosted.e2e.ts`, the `sources-pwa` helpers) own a disjoint `00:1A:79:5F:*` range for the same reason, and `stalker.e2e.ts` runs `mode: 'serial'` because its tests deliberately share scenario MACs. Its stateful authentication tests derive a -separate `00:1A:79:AE::*` range from Playwright's worker index and add -only that range to the scoped reset, so browser projects and repeat workers do -not clear one another's sessions or pinned device identity. Full contract: +separate `00:1A:79:AE::*` range from Playwright's bounded +`parallelIndex` and add only that range to the scoped reset, so concurrent +browser projects do not clear one another's sessions or pinned device +identity; restarted workers retain their slot. Full contract: [`docs/architecture/stalker-mock-server.md`](../../docs/architecture/stalker-mock-server.md#test-isolation). ## EPG Behavior diff --git a/apps/stalker-mock-server/src/app/handlers/auth-handlers.spec.ts b/apps/stalker-mock-server/src/app/handlers/auth-handlers.spec.ts index ceb82279c..ea2affbcd 100644 --- a/apps/stalker-mock-server/src/app/handlers/auth-handlers.spec.ts +++ b/apps/stalker-mock-server/src/app/handlers/auth-handlers.spec.ts @@ -126,7 +126,7 @@ describe('stalker mock authentication handlers', () => { ).toBe(null); }); - it('applies the login-required scenario to worker-scoped MACs', () => { + it('applies the login-required scenario to parallel-slot MACs', () => { const token = invoke(handleHandshake, { action: 'handshake' }, { mac: WORKER_LOGIN_REQUIRED_MAC, })['token'] as string; diff --git a/apps/web-e2e/src/stalker.e2e.ts b/apps/web-e2e/src/stalker.e2e.ts index ca33704f2..e7539d9d0 100644 --- a/apps/web-e2e/src/stalker.e2e.ts +++ b/apps/web-e2e/src/stalker.e2e.ts @@ -40,9 +40,10 @@ import { * test; serializing one file is the cheaper trade. * * - ACROSS BROWSER PROJECTS/WORKERS: state-sensitive auth tests use one MAC - * range per Playwright worker. Their `beforeEach` adds only that worker's - * MACs to the scoped reset, so concurrent projects or repeat workers cannot - * clear one another's token, invalidated session or pinned device id. + * range per Playwright parallel slot. Their `beforeEach` adds only that + * slot's MACs to the scoped reset, so concurrent projects cannot clear one + * another's token, invalidated session or pinned device id. A restarted + * worker retains its bounded `parallelIndex`. */ test.describe.configure({ mode: 'serial' }); @@ -80,8 +81,8 @@ const STATIC_CMD_MAC = '00:1A:79:00:00:0A'; /** * These tests assert state transitions within one portal session, so a reset * from a concurrent browser project or repeat worker would invalidate the - * assertion itself. Giving every worker its own MAC range preserves browser - * parallelism and also keeps `--repeat-each` runs isolated. + * assertion itself. Giving every concurrent worker slot its own MAC range + * preserves browser parallelism and also keeps `--repeat-each` runs isolated. */ interface StatefulAuthMacs { authenticatedFlow: string; @@ -91,16 +92,25 @@ interface StatefulAuthMacs { reauthentication: string; } -function getStatefulAuthMacs(workerIndex: number): StatefulAuthMacs { +function getStatefulAuthMacs({ + parallelIndex, +}: { + parallelIndex: number; +}): StatefulAuthMacs { if ( - !Number.isSafeInteger(workerIndex) || - workerIndex < 0 || - workerIndex > 255 + !Number.isSafeInteger(parallelIndex) || + parallelIndex < 0 || + parallelIndex > 255 ) { - throw new Error(`Unsupported Playwright worker index: ${workerIndex}`); + throw new Error( + `Unsupported Playwright parallel index: ${parallelIndex}` + ); } - const workerOctet = workerIndex.toString(16).padStart(2, '0').toUpperCase(); + const workerOctet = parallelIndex + .toString(16) + .padStart(2, '0') + .toUpperCase(); const workerPrefix = `00:1A:79:AE:${workerOctet}`; return { @@ -311,7 +321,7 @@ function recordPortalRequests( test.beforeEach(async ({ page, request }, testInfo) => { // Reset mock server state (clears in-memory favorites and cache) - await resetMockServer(request, getStatefulAuthMacs(testInfo.workerIndex)); + await resetMockServer(request, getStatefulAuthMacs(testInfo)); // Playwright creates a fresh browser context per test, so extra // IndexedDB cleanup here only risks racing with app-managed DB handles. @@ -332,6 +342,14 @@ test('@stalker health check — mock server is running', async ({ request }) => expect(body.status).toBe('ok'); }); +test('@stalker auth MAC allocation survives worker process restarts', () => { + const restartedWorker = { parallelIndex: 7, workerIndex: 256 }; + + expect(getStatefulAuthMacs(restartedWorker).loginRequired).toBe( + '00:1A:79:AE:07:02' + ); +}); + test('@stalker add a Stalker portal and see it in the playlist list', async ({ page, }) => { @@ -980,7 +998,7 @@ test.describe('@stalker full portal authentication', () => { }, testInfo) => { const requests = recordPortalRequests(page); const actionsInOrder = () => requests.map((entry) => entry.action); - const mac = getStatefulAuthMacs(testInfo.workerIndex).authenticatedFlow; + const mac = getStatefulAuthMacs(testInfo).authenticatedFlow; await addFullStalkerPortal(page, { mac }); @@ -1038,7 +1056,7 @@ test.describe('@stalker full portal authentication', () => { page, }, testInfo) => { const requests = recordPortalRequests(page); - const mac = getStatefulAuthMacs(testInfo.workerIndex).loginRequired; + const mac = getStatefulAuthMacs(testInfo).loginRequired; // The second auth step must only be claimed on the retry AFTER // do_auth — a client that always sends auth_second_step=1 works // against the mock but violates the documented protocol. @@ -1093,7 +1111,7 @@ test.describe('@stalker full portal authentication', () => { test('explains a login-required portal and recovers once credentials are entered', async ({ page, }, testInfo) => { - const mac = getStatefulAuthMacs(testInfo.workerIndex).loginRequired; + const mac = getStatefulAuthMacs(testInfo).loginRequired; // First attempt without credentials: the import must fail with the // portal's actual reason, not a generic "check URL and MAC" error. await page.getByRole('button', { name: 'Add playlist' }).click(); @@ -1135,7 +1153,7 @@ test.describe('@stalker full portal authentication', () => { page, }, testInfo) => { const requests = recordPortalRequests(page); - const mac = getStatefulAuthMacs(testInfo.workerIndex).tokenReuse; + const mac = getStatefulAuthMacs(testInfo).tokenReuse; await addFullStalkerPortal(page, { mac }); @@ -1258,7 +1276,7 @@ test.describe('@stalker full portal authentication', () => { page, request, }, testInfo) => { - const mac = getStatefulAuthMacs(testInfo.workerIndex).deviceConflict; + const mac = getStatefulAuthMacs(testInfo).deviceConflict; // Pin a device id the way another client (StbEmu, a set-top box) // would have: the stock server binds the first non-empty `device_id` // it sees to the MAC and refuses every different one afterwards. @@ -1315,7 +1333,7 @@ test.describe('@stalker full portal authentication', () => { request, }, testInfo) => { const requests = recordPortalRequests(page); - const mac = getStatefulAuthMacs(testInfo.workerIndex).reauthentication; + const mac = getStatefulAuthMacs(testInfo).reauthentication; await addFullStalkerPortal(page, { mac }); diff --git a/docs/architecture/stalker-mock-server.md b/docs/architecture/stalker-mock-server.md index ecd3d80a6..09a22f847 100644 --- a/docs/architecture/stalker-mock-server.md +++ b/docs/architecture/stalker-mock-server.md @@ -319,9 +319,9 @@ supporting scenarios, `get_all_channels` (`get-all-channels.handler.ts`, (adult) genres. The login-required scenario also recognizes -`00:1A:79:AE::02`. This test-only alias range preserves the same -`status: 2`/`do_auth` contract while giving concurrent Playwright workers -independent per-MAC auth state. +`00:1A:79:AE::02`. This test-only alias range preserves the same +`status: 2`/`do_auth` contract while giving concurrent Playwright parallel +slots independent per-MAC auth state. The `marketing-demo` scenario (`00:1A:79:00:00:07`) replaces faker-generated VOD with the 35-movie provider-neutral showcase catalog from @@ -401,10 +401,12 @@ file running in parallel workers, so isolation is per-MAC rather than global: (their fixture shapes are what the assertions are written against), so the file uses `test.describe.configure({ mode: 'serial' })`. - Authentication tests whose assertions span multiple requests derive a - disjoint `00:1A:79:AE::*` range from Playwright's `workerIndex`. - Their `beforeEach` adds only the current worker's range to the batched reset, - so browser projects and `--repeat-each` workers cannot clear one another's - token, login completion, invalidated session or pinned device identity. + disjoint `00:1A:79:AE::*` range from Playwright's bounded + `parallelIndex`. Their `beforeEach` adds only the current parallel slot's + range to the batched reset, so browser projects cannot clear one another's + token, login completion, invalidated session or pinned device identity; a + restarted worker retains the same slot instead of consuming a wider MAC + value. A reset drops the generated content cache, favorites (`data-store.ts`), the auth/session record (`auth-store.ts`, including any pinned device identity) and