mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
fix(mock): scope /reset by MAC so parallel specs stop wiping each other
The re-authentication test passed locally but failed all three CI
attempts: no request carried a token, because self-hosted.e2e.ts issues
a GLOBAL `POST /reset` against the same mock from a parallel Playwright
worker, destroying the session mid-import. Running only stalker.e2e.ts
locally never triggered it.
Serializing within one file (4b31f7167) could not fix this — the
interference is between files. Mock state is per-MAC, so `/reset` now
accepts `?macAddress=` and clears only that MAC's data, favorites,
session and watchdog counters; the unscoped form is kept for callers
that own the whole server. Both spec files now reset only the MACs they
own, so no worker can disturb another.
Verified: a scoped reset of one MAC leaves another MAC's session intact
(and its own dies), and stalker.e2e.ts + self-hosted.e2e.ts run together
23/23 green — the combination that reproduced the CI failure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
23d0ca8afd
commit
3a93fef0f3
6 files changed
+89
-13
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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=<mac>` 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(),
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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('/');
|
||||
});
|
||||
|
||||
@@ -106,12 +106,38 @@ async function interceptStalkerRequests(page: Page): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
/** 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<void> {
|
||||
await Promise.all(OWNED_MACS.map((mac) => resetMockServerMac(request, mac)));
|
||||
}
|
||||
|
||||
async function resetMockServerMac(
|
||||
request: APIRequestContext,
|
||||
mac: string
|
||||
): Promise<void> {
|
||||
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(
|
||||
|
||||
Reference in new issue
Block a user