test(stalker): cover legacy route reopen

This commit is contained in:
4gray committed 2026-07-27 13:15:02 +02:00
1 parent 09469afbae
commit 63ab2837a7
3 files changed
+869

No files matched your search

@@ -0,0 +1,260 @@
import type { Page } from '@playwright/test';
import {
closeElectronApp,
expect,
launchElectronApp,
restartElectronApp,
test,
} from './electron-test-fixtures';
import {
StalkerReplayRun,
withStalkerReplayRun,
} from './support/stalker-replay';
test.describe('Electron Stalker route authentication', () => {
test('@stalker @electron migrates a legacy route through credentials, atomic persistence, and credential-free reopen', async ({
dataDir,
}) => {
const replay = await withStalkerReplayRun(
'e2e/legacy-first-open-reopen',
async (run) => {
const playlistId = 'e2e-legacy-route-auth';
const playlistTitle = 'Legacy authenticated Stalker';
const endpoint = `${requiredOrigin(run, 'portal')}/portal.php`;
const app = await launchElectronApp(dataDir);
try {
await seedLegacyPlaylist(app.mainWindow, {
endpoint,
macAddress: requiredInput(run, 'mac'),
playlistId,
playlistTitle,
sourceUrl: run.entryUrl,
});
await expect(
readPersistedStalkerPlaylist(
app.mainWindow,
playlistId
)
).resolves.toMatchObject({
_id: playlistId,
title: playlistTitle,
});
const seededRestart = await restartElectronApp(
app,
dataDir
);
app.electronApp = seededRestart.electronApp;
app.mainWindow = seededRestart.mainWindow;
await navigateToStalkerRoute(
app.mainWindow,
playlistId
);
await submitCredentials(app.mainWindow, run);
await expect
.poll(
() =>
readPersistedStalkerPlaylist(
app.mainWindow,
playlistId
),
{ timeout: 15000 }
)
.toMatchObject({
isFullStalkerPortal: true,
password: requiredInput(run, 'password'),
portalUrl: endpoint,
stalkerRecipeClassifierVersion: 1,
stalkerRequestRecipe: 'full-session',
stalkerSourceUrl: run.entryUrl,
username: requiredInput(run, 'username'),
});
const firstPersisted = await readPersistedStalkerPlaylist(
app.mainWindow,
playlistId
);
expect(firstPersisted).not.toHaveProperty('stalkerToken');
expect(firstPersisted.stalkerLastVerifiedAt).toEqual(
expect.any(String)
);
await waitForEmptyStalkerCategories(app.mainWindow);
const restarted = await restartElectronApp(app, dataDir);
app.electronApp = restarted.electronApp;
app.mainWindow = restarted.mainWindow;
await expect(
readPersistedStalkerPlaylist(
app.mainWindow,
playlistId
)
).resolves.toMatchObject({
_id: playlistId,
stalkerRequestRecipe: 'full-session',
title: playlistTitle,
});
await navigateToStalkerRoute(
app.mainWindow,
playlistId
);
await expect
.poll(
async () =>
(
await readPersistedStalkerPlaylist(
app.mainWindow,
playlistId
)
).stalkerLastVerifiedAt,
{ timeout: 15000 }
)
.not.toBe(firstPersisted.stalkerLastVerifiedAt);
await expect(
app.mainWindow.locator(
'app-stalker-credentials-dialog'
)
).toHaveCount(0);
await waitForEmptyStalkerCategories(app.mainWindow);
} finally {
await closeElectronApp(app);
}
}
);
expect(replay.finalization).toMatchObject({
ledger: {
mismatchCounts: {},
operationCounts: {
'anonymous-probe': 2,
'catalog-categories': 2,
'do-auth': 2,
handshake: 2,
'profile-first': 2,
'profile-second': 2,
},
terminalState: 'complete',
},
ok: true,
});
});
});
async function seedLegacyPlaylist(
page: Page,
input: {
endpoint: string;
macAddress: string;
playlistId: string;
playlistTitle: string;
sourceUrl: string;
}
): Promise<void> {
await page.evaluate(
async ({
endpoint,
macAddress,
playlistId,
playlistTitle,
sourceUrl,
}) => {
const upsert = window.electron?.dbUpsertAppPlaylist;
if (!upsert) {
throw new Error('playlist-upsert-unavailable');
}
const now = new Date().toISOString();
await upsert({
_id: playlistId,
autoRefresh: false,
count: 0,
importDate: now,
isFullStalkerPortal: true,
lastUsage: now,
macAddress,
portalUrl: endpoint,
stalkerSourceUrl: sourceUrl,
stalkerToken: 'legacy-renderer-token',
title: playlistTitle,
});
},
input
);
}
async function navigateToStalkerRoute(
page: Page,
playlistId: string
): Promise<void> {
const route = `/workspace/stalker/${playlistId}/vod`;
await page.evaluate((nextRoute) => {
window.history.pushState({}, '', nextRoute);
window.dispatchEvent(new PopStateEvent('popstate'));
}, route);
await page.waitForURL(
new RegExp(`/workspace/stalker/${escapeRegExp(playlistId)}/`)
);
}
async function submitCredentials(
page: Page,
run: StalkerReplayRun
): Promise<void> {
const dialog = page.locator('app-stalker-credentials-dialog').last();
await expect(dialog).toBeVisible({ timeout: 15000 });
await dialog
.locator('input[autocomplete="username"]')
.fill(requiredInput(run, 'username'));
await dialog
.locator('input[autocomplete="current-password"]')
.fill(requiredInput(run, 'password'));
await dialog.locator('button[type="submit"]').click();
await dialog.waitFor({ state: 'detached' });
}
async function readPersistedStalkerPlaylist(
page: Page,
playlistId: string
): Promise<Record<string, unknown>> {
const playlist = await page.evaluate(async (id) => {
const read = window.electron?.dbGetAppPlaylist;
if (!read) {
throw new Error('playlist-read-unavailable');
}
return read(id);
}, playlistId);
if (playlist === null || typeof playlist !== 'object') {
throw new Error('persisted-stalker-playlist-missing');
}
return playlist as Record<string, unknown>;
}
async function waitForEmptyStalkerCategories(page: Page): Promise<void> {
await expect(
page.locator('app-workspace-context-panel .category-empty-state')
).toBeVisible({ timeout: 15000 });
await expect(
page.locator('app-workspace-context-panel .context-skeleton')
).toHaveCount(0);
}
function requiredInput(run: StalkerReplayRun, name: string): string {
const value = run.inputs[name];
if (!value) {
throw new Error(`stalker-replay-input-missing:${name}`);
}
return value;
}
function requiredOrigin(run: StalkerReplayRun, name: string): string {
const value = run.origins[name];
if (!value) {
throw new Error(`stalker-replay-origin-missing:${name}`);
}
return value;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
@@ -0,0 +1,608 @@
{
"description": "A legacy full portal is classified through the route UI, persists accepted credentials and its learned recipe, then reopens without asking for credentials again.",
"entry": {
"origin": "portal",
"path": "/c/"
},
"expectedEndpoint": {
"origin": "portal",
"path": "/portal.php"
},
"failOnUnexpectedRequest": true,
"initialState": "start",
"origins": {
"portal": {}
},
"phases": [
{
"expectations": [
{
"cardinality": {
"max": 2,
"min": 2
},
"id": "anonymous-probe",
"method": "GET",
"operation": "anonymous-probe",
"origin": "portal",
"path": "/c/",
"request": {
"body": {
"kind": "absent"
},
"cookies": {
"absent": [
"mac",
"session"
],
"attributes": {},
"exact": {},
"present": []
},
"headers": {
"absent": [
"authorization",
"cookie"
],
"exact": {},
"present": []
},
"query": {
"absent": [
"device_id",
"device_id2",
"mac",
"password",
"signature",
"signature2",
"sn",
"token",
"username"
],
"exact": {},
"present": []
}
},
"response": {
"body": {
"kind": "empty"
},
"headers": {},
"status": 204
}
},
{
"cardinality": {
"max": 2,
"min": 2
},
"id": "handshake",
"method": "GET",
"operation": "handshake",
"origin": "portal",
"path": "/portal.php",
"request": {
"body": {
"kind": "absent"
},
"cookies": {
"absent": [
"session"
],
"attributes": {},
"exact": {
"mac": {
"kind": "ref",
"symbol": "mac"
},
"stb_lang": "en",
"timezone": "UTC"
},
"present": []
},
"headers": {
"absent": [
"authorization"
],
"exact": {
"accept-language": "en-US",
"user-agent": "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250",
"x-user-agent": "Model: MAG250"
},
"present": [
"referer"
]
},
"query": {
"absent": [
"password",
"token",
"username"
],
"exact": {
"JsHttpRequest": "1-xml",
"action": "handshake",
"type": "stb"
},
"present": []
}
},
"response": {
"body": {
"kind": "json",
"value": {
"js": {
"random": {
"kind": "ref",
"symbol": "challenge"
},
"token": {
"kind": "ref",
"symbol": "session-token"
}
}
}
},
"headers": {
"content-type": [
"application/json"
],
"set-cookie": [
{
"kind": "parts",
"parts": [
{
"kind": "literal",
"value": "session="
},
{
"kind": "ref",
"symbol": "session-cookie-1"
},
{
"kind": "literal",
"value": "; Path=/; HttpOnly"
}
]
}
]
},
"status": 200
}
},
{
"cardinality": {
"max": 2,
"min": 2
},
"id": "profile-first",
"method": "GET",
"operation": "profile-first",
"origin": "portal",
"path": "/portal.php",
"request": {
"body": {
"kind": "absent"
},
"cookies": {
"absent": [],
"attributes": {},
"exact": {
"mac": {
"kind": "ref",
"symbol": "mac"
},
"session": {
"kind": "ref",
"symbol": "session-cookie-1"
},
"stb_lang": "en",
"timezone": "UTC"
},
"present": []
},
"headers": {
"absent": [],
"exact": {
"accept-language": "en-US",
"authorization": {
"kind": "parts",
"parts": [
{
"kind": "literal",
"value": "Bearer "
},
{
"kind": "ref",
"symbol": "session-token"
}
]
},
"user-agent": "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250",
"x-user-agent": "Model: MAG250"
},
"present": [
"referer"
]
},
"query": {
"absent": [
"device_id",
"device_id2",
"password",
"signature",
"signature2",
"sn",
"username"
],
"exact": {
"JsHttpRequest": "1-xml",
"action": "get_profile",
"auth_second_step": "0",
"client_type": "STB",
"hd": "1",
"language": "en",
"not_valid_token": "0",
"stb_type": "MAG250",
"timezone": "UTC",
"type": "stb"
},
"present": [
"metrics"
]
}
},
"response": {
"body": {
"kind": "json",
"value": {
"js": {
"status": 2
}
}
},
"headers": {
"content-type": [
"application/json"
]
},
"status": 200
}
},
{
"cardinality": {
"max": 2,
"min": 2
},
"id": "do-auth",
"method": "GET",
"operation": "do-auth",
"origin": "portal",
"path": "/portal.php",
"request": {
"body": {
"kind": "absent"
},
"cookies": {
"absent": [],
"attributes": {},
"exact": {
"mac": {
"kind": "ref",
"symbol": "mac"
},
"session": {
"kind": "ref",
"symbol": "session-cookie-1"
},
"stb_lang": "en",
"timezone": "UTC"
},
"present": []
},
"headers": {
"absent": [],
"exact": {
"accept-language": "en-US",
"authorization": {
"kind": "parts",
"parts": [
{
"kind": "literal",
"value": "Bearer "
},
{
"kind": "ref",
"symbol": "session-token"
}
]
},
"user-agent": "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250",
"x-user-agent": "Model: MAG250"
},
"present": [
"referer"
]
},
"query": {
"absent": [],
"exact": {
"JsHttpRequest": "1-xml",
"action": "do_auth",
"login": {
"kind": "ref",
"symbol": "username"
},
"password": {
"kind": "ref",
"symbol": "password"
},
"type": "stb"
},
"present": []
}
},
"response": {
"body": {
"kind": "json",
"value": {
"js": true
}
},
"headers": {
"content-type": [
"application/json"
],
"set-cookie": [
{
"kind": "parts",
"parts": [
{
"kind": "literal",
"value": "session="
},
{
"kind": "ref",
"symbol": "session-cookie-2"
},
{
"kind": "literal",
"value": "; Path=/; HttpOnly"
}
]
}
]
},
"status": 200
}
},
{
"cardinality": {
"max": 2,
"min": 2
},
"id": "profile-second",
"method": "GET",
"operation": "profile-second",
"origin": "portal",
"path": "/portal.php",
"request": {
"body": {
"kind": "absent"
},
"cookies": {
"absent": [],
"attributes": {},
"exact": {
"mac": {
"kind": "ref",
"symbol": "mac"
},
"session": {
"kind": "ref",
"symbol": "session-cookie-2"
},
"stb_lang": "en",
"timezone": "UTC"
},
"present": []
},
"headers": {
"absent": [],
"exact": {
"accept-language": "en-US",
"authorization": {
"kind": "parts",
"parts": [
{
"kind": "literal",
"value": "Bearer "
},
{
"kind": "ref",
"symbol": "session-token"
}
]
},
"user-agent": "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250",
"x-user-agent": "Model: MAG250"
},
"present": [
"referer"
]
},
"query": {
"absent": [
"device_id",
"device_id2",
"password",
"signature",
"signature2",
"sn",
"username"
],
"exact": {
"JsHttpRequest": "1-xml",
"action": "get_profile",
"auth_second_step": "1",
"client_type": "STB",
"hd": "1",
"language": "en",
"not_valid_token": "0",
"stb_type": "MAG250",
"timezone": "UTC",
"type": "stb"
},
"present": [
"metrics"
]
}
},
"response": {
"body": {
"kind": "json",
"value": {
"js": {
"status": 0
}
}
},
"headers": {
"content-type": [
"application/json"
]
},
"status": 200
}
},
{
"cardinality": {
"max": 2,
"min": 2
},
"id": "vod-categories",
"method": "GET",
"operation": "catalog-categories",
"origin": "portal",
"path": "/portal.php",
"request": {
"body": {
"kind": "absent"
},
"cookies": {
"absent": [],
"attributes": {},
"exact": {
"mac": {
"kind": "ref",
"symbol": "mac"
},
"session": {
"kind": "ref",
"symbol": "session-cookie-2"
},
"stb_lang": "en",
"timezone": "UTC"
},
"present": []
},
"headers": {
"absent": [],
"exact": {
"accept-language": "en-US",
"authorization": {
"kind": "parts",
"parts": [
{
"kind": "literal",
"value": "Bearer "
},
{
"kind": "ref",
"symbol": "session-token"
}
]
},
"user-agent": "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG250",
"x-user-agent": "Model: MAG250"
},
"present": [
"referer"
]
},
"query": {
"absent": [
"password",
"username"
],
"exact": {
"JsHttpRequest": "1-xml",
"action": "get_categories",
"type": "vod"
},
"present": []
}
},
"response": {
"body": {
"kind": "json",
"value": {
"js": []
}
},
"headers": {
"content-type": [
"application/json"
]
},
"status": 200
}
}
],
"mode": "unordered",
"name": "legacy-first-open-and-reopen",
"nextState": "complete",
"state": "start"
}
],
"scenarioId": "e2e-legacy-first-open-reopen",
"schemaVersion": 1,
"symbols": [
{
"kind": "generate",
"symbol": "mac",
"valueKind": "mac"
},
{
"kind": "generate",
"symbol": "username",
"valueKind": "credential"
},
{
"kind": "generate",
"symbol": "password",
"valueKind": "credential"
},
{
"kind": "generate",
"symbol": "challenge",
"valueKind": "random"
},
{
"kind": "generate",
"symbol": "session-token",
"valueKind": "token"
},
{
"kind": "generate",
"symbol": "session-cookie-1",
"valueKind": "cookie"
},
{
"kind": "generate",
"symbol": "session-cookie-2",
"valueKind": "cookie"
}
],
"terminalState": "complete"
}
@@ -51,6 +51,7 @@ const EXPECTED_SCENARIOS = [
'cookies-session-isolation',
'e2e-concurrent-catalog-refresh',
'e2e-full-session-catalog-playback',
'e2e-legacy-first-open-reopen',
'redirect-auth-query',
'redirect-identity-pause',
'redirect-landing-approval',