mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 04:26:16 -08:00
test(stalker): prove content actually reloads after re-authentication
Review follow-up on #1324 (Codex P2, valid — the "passes for the wrong reason" class): the re-auth test only polled for a fresh handshake and a negative body-text assertion, both of which pass even if the original content request is never replayed or stays unauthorized. Capture the content token from the initial import, then assert a post-invalidation CONTENT request goes out under a DIFFERENT token and that the ITV categories actually render — the mock only answers content for an adopted token, so this proves the new token round-tripped through get_profile. Verified against a live mock that the token genuinely rotates (old token -> "Authorization failed.", new token -> content). Also documents the second Codex P2: the mock is deliberately strict on /server/load.php (a real portal enforces auth there); the import dialog vs session predicate divergence is a separate app bug the strict endpoint will let a later PR cover. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
149df18c32
commit
ad27c06396
2 files changed
+77
-4
No files matched your search
@@ -43,6 +43,17 @@ imported as a **full portal** (handshake + token + watchdog), anything else as a
|
||||
| `/stalker_portal/server/load.php` | Strict. Enforces the token and the MAC format exactly like the real middleware. |
|
||||
| `/server/load.php` | Strict. The second full-portal URL shape the app recognizes; enforced identically. |
|
||||
|
||||
> **Known app inconsistency:** `StalkerSessionService.isFullStalkerPortal`
|
||||
> classifies `/server/load.php` as a full portal, but the import dialog's
|
||||
> `isFullStalkerPortalUrl` checks only for `/stalker_portal`, so importing a bare
|
||||
> `…/server/load.php` URL persists `isFullStalkerPortal: false` and the app skips
|
||||
> the handshake. The mock is deliberately faithful to a **real** portal here
|
||||
> (that path enforces auth), which makes it the right fixture to drive the
|
||||
> upcoming fix that unifies those two predicates. Until then, import full
|
||||
> portals through a `/stalker_portal/...` URL.
|
||||
|
||||
> **Known app-side gap:** the
|
||||
|
||||
The strict endpoint reproduces the parts of Stalker 4.9.35 that a client can
|
||||
actually get wrong:
|
||||
|
||||
|
||||
@@ -114,6 +114,34 @@ function recordPortalActions(page: Page): {
|
||||
return { actions, tokensByAction };
|
||||
}
|
||||
|
||||
const CONTENT_ACTIONS = [
|
||||
'get_categories',
|
||||
'get_genres',
|
||||
'get_ordered_list',
|
||||
'get_all_channels',
|
||||
];
|
||||
|
||||
/** Every portal request in order, with the token it carried. */
|
||||
function recordPortalRequests(
|
||||
page: Page
|
||||
): Array<{ action: string; token: string | null }> {
|
||||
const requests: Array<{ action: string; token: string | null }> = [];
|
||||
|
||||
page.on('request', (request) => {
|
||||
const url = new URL(request.url());
|
||||
if (!url.pathname.endsWith('/stalker')) {
|
||||
return;
|
||||
}
|
||||
const action = url.searchParams.get('action');
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
requests.push({ action, token: url.searchParams.get('token') });
|
||||
});
|
||||
|
||||
return requests;
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, request }) => {
|
||||
// Importing a full portal costs a handshake, a profile call and the first
|
||||
// content load — on a cold dev server that alone approaches Playwright's
|
||||
@@ -177,9 +205,16 @@ test.describe('@stalker full portal authentication', () => {
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
const requests = recordPortalRequests(page);
|
||||
|
||||
await addFullStalkerPortal(page);
|
||||
|
||||
const { actions } = recordPortalActions(page);
|
||||
// The token the initial import authenticated with — recovery must end
|
||||
// up on a DIFFERENT one, or nothing was actually re-negotiated.
|
||||
const tokenBeforeInvalidation = requests.find(
|
||||
(entry) => CONTENT_ACTIONS.includes(entry.action) && entry.token
|
||||
)?.token;
|
||||
expect(tokenBeforeInvalidation).toBeTruthy();
|
||||
|
||||
// Server-side session loss is what a real expired/replaced token looks
|
||||
// like: the next request gets "Authorization failed." with HTTP 200.
|
||||
@@ -190,15 +225,42 @@ test.describe('@stalker full portal authentication', () => {
|
||||
);
|
||||
expect(invalidated.ok()).toBe(true);
|
||||
|
||||
const requestCountBeforeNavigation = requests.length;
|
||||
|
||||
// Navigating to another content type forces a fresh portal request.
|
||||
await page.getByRole('link', { name: /live|itv/i }).click();
|
||||
|
||||
// Recovery is only proven end to end when a CONTENT request goes out
|
||||
// under a freshly negotiated token — a re-handshake alone could still
|
||||
// leave the original request unreplayed or unauthorized. The mock only
|
||||
// answers content for an adopted token, so this doubles as proof the
|
||||
// new token was adopted via get_profile.
|
||||
await expect
|
||||
.poll(() => actions.filter((a) => a === 'handshake').length, {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.poll(
|
||||
() =>
|
||||
requests
|
||||
.slice(requestCountBeforeNavigation)
|
||||
.filter(
|
||||
(entry) =>
|
||||
CONTENT_ACTIONS.includes(entry.action) &&
|
||||
entry.token &&
|
||||
entry.token !== tokenBeforeInvalidation
|
||||
).length,
|
||||
{ timeout: 30_000 }
|
||||
)
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
const recovered = requests.slice(requestCountBeforeNavigation);
|
||||
expect(
|
||||
recovered.filter((entry) => entry.action === 'handshake').length
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// And the recovered session must actually render: the ITV categories
|
||||
// can only come from an authorized request against the new token.
|
||||
await expect(page.locator('.category-item').first()).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
await expect(page.locator('body')).not.toContainText(
|
||||
'Authorization failed.'
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user