mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
fix(stalker): prefer the live playlist row over a stale favorite snapshot
Codex P1 on #1364, and mine. `resolveStalker` reads its portal coordinates as `item.stalkerPortalUrl ?? playlist?.portalUrl` — item first. The create_link branch quietly corrected for that afterwards by re-reading `applyOverride(playlist).portalUrl`, so the row won wherever it existed, which is what the comment right above it already promised: "when the row exists it wins over the item's snapshot of the portal URL (a repaired endpoint must beat a stale favorite)". The static branch I added returns before that correction, so it shipped the stale snapshot. Consequences after a playlist edit: a same-host static URL matching the OLD host gets the newly negotiated token and identity headers sent to the previous portal, and a MAC-only edit pairs the new token with the old MAC cookie — precisely the pairing `stalkerIdentityFingerprint` exists to prevent. Both branches now derive the coordinates once, row-first with the repair override applied, and fall back to the item's snapshot only for a playlist that no longer exists — which is the role `buildStalkerPlayback` already documents for it. Mutation-checked: restoring item-first precedence fails the new test alone. Also documents a local-only e2e hazard found while re-running the suite: `mode: 'serial'` orders tests within one project, but chromium/firefox/webkit run the file concurrently against the same mock server, so one project's beforeEach reset can drop a session another is mid-test on — which is what a lone auth-spec failure that passes on rerun actually is. CI never sees it; the Web E2E job runs --project=chromium alone, and that command is clean (22/22). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
041abeb4d7
commit
9afdad0279
3 files changed
+68
-2
No files matched your search
@@ -38,6 +38,16 @@ import {
|
||||
*
|
||||
* Giving each test its own MAC instead would mean inventing a scenario per
|
||||
* test; serializing one file is the cheaper trade.
|
||||
*
|
||||
* ACROSS BROWSER PROJECTS this does NOT hold, and it is a local-run hazard
|
||||
* only. `mode: 'serial'` orders tests within one project; chromium, firefox
|
||||
* and webkit still run the file concurrently against the SAME mock server, so
|
||||
* one project's `beforeEach` reset can drop a session another project is
|
||||
* mid-test on — the auth specs below are the ones that notice, failing as if
|
||||
* the portal had dropped them. CI never sees it: the Web E2E job runs
|
||||
* `--project=chromium` alone (`.github/workflows/e2e-tests.yaml`). If a local
|
||||
* all-project run shows a lone auth failure that passes on rerun, this is why;
|
||||
* `--project=chromium` reproduces CI exactly.
|
||||
*/
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
@@ -934,6 +934,49 @@ describe('StreamResolverService', () => {
|
||||
expect(playback.headers?.['Authorization']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('prefers the edited playlist coordinates over a stale favorite snapshot', async () => {
|
||||
// A favorite persists the portal URL and MAC it was saved with. After
|
||||
// the playlist is edited, the session token is negotiated for the NEW
|
||||
// identity — sending it with the old MAC cookie (or to the old host)
|
||||
// is the mismatch the identity fingerprint exists to prevent.
|
||||
playlistsService.getPlaylistById.mockReturnValue(
|
||||
of({
|
||||
_id: 'stalker-1',
|
||||
portalUrl: 'https://new.example.com/portal.php',
|
||||
macAddress: 'AA:BB:CC:00:00:99',
|
||||
isFullStalkerPortal: false,
|
||||
} satisfies Partial<Playlist>)
|
||||
);
|
||||
stalkerSession.getCachedToken.mockReturnValue('TOKEN-NEW');
|
||||
|
||||
const playback = await service.resolvePlayback({
|
||||
uid: 'stalker::stalker-1::93',
|
||||
name: 'Edited Portal Channel',
|
||||
contentType: 'live',
|
||||
sourceType: 'stalker',
|
||||
playlistId: 'stalker-1',
|
||||
playlistName: 'Stalker',
|
||||
stalkerId: '93',
|
||||
stalkerCmd: 'ffrt3 https://new.example.com/live/93.m3u8',
|
||||
// Stale snapshot from before the edit.
|
||||
stalkerPortalUrl: 'https://old.example.com/portal.php',
|
||||
stalkerMacAddress: '00:11:22:33:44:55',
|
||||
stalkerItem: {
|
||||
id: '93',
|
||||
cmd: 'ffrt3 https://new.example.com/live/93.m3u8',
|
||||
use_http_tmp_link: '0',
|
||||
},
|
||||
} as UnifiedCollectionItem);
|
||||
|
||||
expect(playback.headers?.['Cookie']).toContain('mac=AA:BB:CC:00:00:99');
|
||||
expect(playback.headers?.['Cookie']).not.toContain(
|
||||
'mac=00:11:22:33:44:55'
|
||||
);
|
||||
// Same host as the edited row ⇒ portal-owned ⇒ credentials attached.
|
||||
expect(playback.headers?.['Authorization']).toBe('Bearer TOKEN-NEW');
|
||||
expect(playback.origin).toBe('https://new.example.com');
|
||||
});
|
||||
|
||||
it('mints a link for a flagged Stalker favorite', async () => {
|
||||
playlistsService.getPlaylistById.mockReturnValue(
|
||||
of({
|
||||
|
||||
@@ -336,9 +336,22 @@ export class StreamResolverService {
|
||||
const playlist = (await firstValueFrom(
|
||||
this.playlistsService.getPlaylistById(item.playlistId)
|
||||
)) as Playlist | undefined;
|
||||
// The playlist row wins whenever it exists: a repaired endpoint or an
|
||||
// edited MAC must beat the snapshot a favorite persisted, and the
|
||||
// session token is negotiated for the ROW's identity — pairing a fresh
|
||||
// token with a stale MAC cookie is exactly the mismatch the identity
|
||||
// fingerprint exists to prevent. The item's own coordinates are the
|
||||
// fallback for a playlist that no longer exists.
|
||||
const currentPlaylist = playlist
|
||||
? this.portalRepair.applyOverride(playlist)
|
||||
: undefined;
|
||||
const portalUrl =
|
||||
item.stalkerPortalUrl ?? playlist?.portalUrl ?? playlist?.url ?? '';
|
||||
const macAddress = item.stalkerMacAddress ?? playlist?.macAddress ?? '';
|
||||
currentPlaylist?.portalUrl ??
|
||||
currentPlaylist?.url ??
|
||||
item.stalkerPortalUrl ??
|
||||
'';
|
||||
const macAddress =
|
||||
currentPlaylist?.macAddress ?? item.stalkerMacAddress ?? '';
|
||||
// Favorites and Recently Viewed persist the raw catalog row, so the
|
||||
// temporary-link flags travel with the item and this route makes the
|
||||
// same decision the portal views make: an unflagged, directly playable
|
||||
|
||||
Reference in new issue
Block a user