mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -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;
|
session.pendingToken = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resetAuthState(): void {
|
export function resetAuthState(mac?: string): void {
|
||||||
|
if (mac) {
|
||||||
|
sessions.delete(mac.toLowerCase());
|
||||||
|
return;
|
||||||
|
}
|
||||||
sessions.clear();
|
sessions.clear();
|
||||||
tokenCounter = 0;
|
tokenCounter = 0;
|
||||||
}
|
}
|
||||||
@@ -44,6 +44,17 @@ export function resetFavorites(mac: string): void {
|
|||||||
favoritesStore.delete(mac.toLowerCase());
|
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). */
|
/** Reset all cached data (exposed via /reset endpoint). */
|
||||||
export function resetAll(): void {
|
export function resetAll(): void {
|
||||||
portalCache.clear();
|
portalCache.clear();
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ export function getWatchdogPings(
|
|||||||
return watchdogPings.get(mac.toLowerCase());
|
return watchdogPings.get(mac.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resetWatchdogPings(): void {
|
export function resetWatchdogPings(mac?: string): void {
|
||||||
|
if (mac) {
|
||||||
|
watchdogPings.delete(mac.toLowerCase());
|
||||||
|
return;
|
||||||
|
}
|
||||||
watchdogPings.clear();
|
watchdogPings.clear();
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ import portalRouter, { createPortalRouter } from './app/routes/portal.route.js';
|
|||||||
import dispatchPortalAction from './app/routes/dispatch.js';
|
import dispatchPortalAction from './app/routes/dispatch.js';
|
||||||
import { invalidateSession, resetAuthState } from './app/auth-store.js';
|
import { invalidateSession, resetAuthState } from './app/auth-store.js';
|
||||||
import { resetWatchdogPings } from './app/handlers/get-events.handler.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 { SCENARIOS } from './app/scenarios.js';
|
||||||
import {
|
import {
|
||||||
buildRequestOrigin,
|
buildRequestOrigin,
|
||||||
@@ -173,12 +173,34 @@ app.get('/health', (_req: Request, res: Response) => {
|
|||||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
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) => {
|
* Reset in-memory state between test runs.
|
||||||
resetAll();
|
*
|
||||||
resetAuthState();
|
* `?macAddress=<mac>` scopes the reset to that MAC and is what specs should
|
||||||
resetWatchdogPings();
|
* use: mock state is per-MAC, so a scoped reset cannot wipe the session of a
|
||||||
res.json({ status: 'reset', timestamp: new Date().toISOString() });
|
* 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 }) => {
|
test.beforeEach(async ({ page, request }) => {
|
||||||
await request.post(`${XTREAM_MOCK_SERVER}/reset`);
|
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 installRuntimeConfig(page);
|
||||||
await page.goto('/');
|
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> {
|
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;
|
let lastError: unknown;
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
const response = await request.post(`${MOCK_SERVER}/reset`);
|
const response = await request.post(
|
||||||
|
`${MOCK_SERVER}/reset?macAddress=${encodeURIComponent(mac)}`
|
||||||
|
);
|
||||||
if (response.ok()) {
|
if (response.ok()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -630,9 +656,12 @@ test('@stalker mock server reset clears cached state', async ({ request }) => {
|
|||||||
);
|
);
|
||||||
expect(before.ok()).toBeTruthy();
|
expect(before.ok()).toBeTruthy();
|
||||||
|
|
||||||
// Reset
|
// Scoped reset — a global one would clear MACs owned by parallel specs.
|
||||||
const reset = await request.post(`${MOCK_SERVER}/reset`);
|
const reset = await request.post(
|
||||||
|
`${MOCK_SERVER}/reset?macAddress=${encodeURIComponent(DEFAULT_MAC)}`
|
||||||
|
);
|
||||||
expect(reset.ok()).toBeTruthy();
|
expect(reset.ok()).toBeTruthy();
|
||||||
|
expect((await reset.json()).mac).toBe(DEFAULT_MAC);
|
||||||
|
|
||||||
// Data is regenerated identically (deterministic seed)
|
// Data is regenerated identically (deterministic seed)
|
||||||
const after = await request.get(
|
const after = await request.get(
|
||||||
|
|||||||
Reference in new issue
Block a user