test(stalker): bound auth e2e worker slots

This commit is contained in:
4gray committed 2026-08-07 21:43:42 +02:00
1 parent f962438a04
commit 1f3d684ee6
4 files changed
+51 -30

No files matched your search

+5 -4
View File
@@ -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:<worker>: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:<slot>: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` |
| `<any other MAC>` | **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:<worker>:*` 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:<slot>:*` 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
@@ -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;
+36 -18
View File
@@ -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 });
+9 -7
View File
@@ -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:<worker>: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:<slot>: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:<worker>:*` 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:<slot>:*` 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