diff --git a/apps/stalker-mock-server/src/app/auth-store.ts b/apps/stalker-mock-server/src/app/auth-store.ts index a69b5d7db..c32486f5f 100644 --- a/apps/stalker-mock-server/src/app/auth-store.ts +++ b/apps/stalker-mock-server/src/app/auth-store.ts @@ -201,7 +201,11 @@ export function invalidateSession(mac: string): void { session.pendingToken = undefined; } -export function resetAuthState(): void { +export function resetAuthState(mac?: string): void { + if (mac) { + sessions.delete(mac.toLowerCase()); + return; + } sessions.clear(); tokenCounter = 0; } diff --git a/apps/stalker-mock-server/src/app/data-store.ts b/apps/stalker-mock-server/src/app/data-store.ts index 52a16a756..4aca56f4f 100644 --- a/apps/stalker-mock-server/src/app/data-store.ts +++ b/apps/stalker-mock-server/src/app/data-store.ts @@ -44,6 +44,17 @@ export function resetFavorites(mac: string): void { favoritesStore.delete(mac.toLowerCase()); } +/** + * Reset everything cached for one MAC. Preferred over `resetAll()` in tests: + * mock state is per-MAC, so a scoped reset cannot disturb a spec that runs + * concurrently against a different MAC. + */ +export function resetMac(mac: string): void { + const key = mac.toLowerCase(); + portalCache.delete(key); + favoritesStore.delete(key); +} + /** Reset all cached data (exposed via /reset endpoint). */ export function resetAll(): void { portalCache.clear(); diff --git a/apps/stalker-mock-server/src/app/handlers/get-events.handler.ts b/apps/stalker-mock-server/src/app/handlers/get-events.handler.ts index 9132ab64a..32cc4c37a 100644 --- a/apps/stalker-mock-server/src/app/handlers/get-events.handler.ts +++ b/apps/stalker-mock-server/src/app/handlers/get-events.handler.ts @@ -35,6 +35,10 @@ export function getWatchdogPings( return watchdogPings.get(mac.toLowerCase()); } -export function resetWatchdogPings(): void { +export function resetWatchdogPings(mac?: string): void { + if (mac) { + watchdogPings.delete(mac.toLowerCase()); + return; + } watchdogPings.clear(); } diff --git a/apps/stalker-mock-server/src/main.ts b/apps/stalker-mock-server/src/main.ts index 93b2876dc..121e26354 100644 --- a/apps/stalker-mock-server/src/main.ts +++ b/apps/stalker-mock-server/src/main.ts @@ -6,7 +6,7 @@ import portalRouter, { createPortalRouter } from './app/routes/portal.route.js'; import dispatchPortalAction from './app/routes/dispatch.js'; import { invalidateSession, resetAuthState } from './app/auth-store.js'; import { resetWatchdogPings } from './app/handlers/get-events.handler.js'; -import { resetAll } from './app/data-store.js'; +import { resetAll, resetMac } from './app/data-store.js'; import { SCENARIOS } from './app/scenarios.js'; import { buildRequestOrigin, @@ -173,12 +173,34 @@ app.get('/health', (_req: Request, res: Response) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); -// Reset all in-memory data (useful between Playwright test runs) -app.post('/reset', (_req: Request, res: Response) => { - resetAll(); - resetAuthState(); - resetWatchdogPings(); - res.json({ status: 'reset', timestamp: new Date().toISOString() }); +/** + * Reset in-memory state between test runs. + * + * `?macAddress=` scopes the reset to that MAC and is what specs should + * use: mock state is per-MAC, so a scoped reset cannot wipe the session of a + * spec file running concurrently in another Playwright worker. Without the + * parameter everything is cleared, which is only safe when nothing else is + * talking to this server. + */ +app.post('/reset', (req: Request, res: Response) => { + const macParam = req.query['macAddress']; + const mac = typeof macParam === 'string' ? macParam : undefined; + + if (mac) { + resetMac(mac); + resetAuthState(mac); + resetWatchdogPings(mac); + } else { + resetAll(); + resetAuthState(); + resetWatchdogPings(); + } + + res.json({ + status: 'reset', + ...(mac ? { mac } : {}), + timestamp: new Date().toISOString(), + }); }); /** diff --git a/apps/web-e2e/src/self-hosted.e2e.ts b/apps/web-e2e/src/self-hosted.e2e.ts index b00a0b5f1..782ef4ce7 100644 --- a/apps/web-e2e/src/self-hosted.e2e.ts +++ b/apps/web-e2e/src/self-hosted.e2e.ts @@ -102,7 +102,13 @@ function expectRequestsUseTargetId(requests: string[], path: string): void { test.beforeEach(async ({ page, request }) => { await request.post(`${XTREAM_MOCK_SERVER}/reset`); - await request.post(`${STALKER_MOCK_SERVER}/reset`); + // Scope the Stalker reset to the MAC this file uses: a global reset would + // wipe the sessions of stalker.e2e.ts running in a parallel worker. + await request.post( + `${STALKER_MOCK_SERVER}/reset?macAddress=${encodeURIComponent( + DEFAULT_MAC + )}` + ); await installRuntimeConfig(page); await page.goto('/'); }); diff --git a/apps/web-e2e/src/stalker.e2e.ts b/apps/web-e2e/src/stalker.e2e.ts index 0600813de..2fbb6196d 100644 --- a/apps/web-e2e/src/stalker.e2e.ts +++ b/apps/web-e2e/src/stalker.e2e.ts @@ -106,12 +106,38 @@ async function interceptStalkerRequests(page: Page): Promise { }); } +/** Every MAC this file touches; each is reset individually (see below). */ +const OWNED_MACS = [ + DEFAULT_MAC, + MINIMAL_MAC, + EMBEDDED_SERIES_MAC, + LEGACY_PAGINATION_MAC, + AUTH_FLOW_MAC, + AUTH_REAUTH_MAC, + AUTH_REJECTED_MAC, +]; + +/** + * Reset only the MACs this file owns. Mock state is per-MAC and other spec + * files (self-hosted.e2e.ts) talk to the same server from a parallel worker, + * so a global reset here would wipe their sessions mid-test — and theirs + * would wipe ours. + */ async function resetMockServer(request: APIRequestContext): Promise { + await Promise.all(OWNED_MACS.map((mac) => resetMockServerMac(request, mac))); +} + +async function resetMockServerMac( + request: APIRequestContext, + mac: string +): Promise { let lastError: unknown; for (let attempt = 0; attempt < 3; attempt += 1) { try { - const response = await request.post(`${MOCK_SERVER}/reset`); + const response = await request.post( + `${MOCK_SERVER}/reset?macAddress=${encodeURIComponent(mac)}` + ); if (response.ok()) { return; } @@ -630,9 +656,12 @@ test('@stalker mock server reset clears cached state', async ({ request }) => { ); expect(before.ok()).toBeTruthy(); - // Reset - const reset = await request.post(`${MOCK_SERVER}/reset`); + // Scoped reset — a global one would clear MACs owned by parallel specs. + const reset = await request.post( + `${MOCK_SERVER}/reset?macAddress=${encodeURIComponent(DEFAULT_MAC)}` + ); expect(reset.ok()).toBeTruthy(); + expect((await reset.json()).mac).toBe(DEFAULT_MAC); // Data is regenerated identically (deterministic seed) const after = await request.get(