mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
fix(xtream): apply an account-info answer only to the panel it came from
Review follow-up (Codex): an in-place edit keeps the playlist id while moving the source, so an answer already on the wire for the OLD panel passed the id-only guard and patched the new panel's status and clock into the store. One `answersFor(candidate, credentials)` predicate now gates the store patch, the error path and the persisted-row transform alike. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
1c446cd5be
commit
e634cdea6e
2 files changed
+68
-12
No files matched your search
@@ -252,6 +252,37 @@ describe('withPortal', () => {
|
||||
expect(storedPlaylist.serverTimezone).toBe('Europe/London');
|
||||
});
|
||||
|
||||
it('does not patch the store with a late answer when the playlist was edited in place meanwhile', async () => {
|
||||
let answer!: (value: unknown) => void;
|
||||
apiService.getAccountInfo.mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
answer = resolve;
|
||||
})
|
||||
);
|
||||
|
||||
const pending = store.checkPortalStatus();
|
||||
// Same id, new panel: the in-place edit flow.
|
||||
const moved: XtreamPlaylistData = {
|
||||
...PLAYLIST,
|
||||
serverUrl: 'https://moved.example.com',
|
||||
};
|
||||
store.setCurrentPlaylist(moved);
|
||||
storedPlaylist = {
|
||||
...storedPlaylist,
|
||||
serverUrl: moved.serverUrl,
|
||||
serverTimezone: undefined,
|
||||
};
|
||||
answer({
|
||||
user_info: { auth: 1, exp_date: '0', status: 'Active' },
|
||||
server_info: { timezone: 'Europe/London' },
|
||||
});
|
||||
|
||||
await expect(pending).resolves.toBe('active');
|
||||
expect(store.currentPlaylist()).toEqual(moved);
|
||||
expect(store.portalStatus()).toBe('unavailable');
|
||||
expect(storedPlaylist.serverTimezone).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not persist a late answer onto a row whose connection was edited meanwhile', async () => {
|
||||
let answer!: (value: unknown) => void;
|
||||
apiService.getAccountInfo.mockReturnValue(
|
||||
|
||||
@@ -58,6 +58,26 @@ export function withPortal() {
|
||||
const dataSource = inject(XTREAM_DATA_SOURCE);
|
||||
const playlistsService = inject(PlaylistsService);
|
||||
|
||||
/**
|
||||
* Whether a playlist (the store's current one, or the stored
|
||||
* row) still points at the panel an account-info answer came
|
||||
* from. The playlist id alone is not enough: an in-place edit
|
||||
* keeps the id while moving the source, and an answer already on
|
||||
* the wire for the OLD panel must not describe the new one.
|
||||
*/
|
||||
const answersFor = (
|
||||
candidate: {
|
||||
serverUrl?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
} | null,
|
||||
credentials: XtreamCredentials
|
||||
): boolean =>
|
||||
!!candidate &&
|
||||
candidate.serverUrl === credentials.serverUrl &&
|
||||
candidate.username === credentials.username &&
|
||||
candidate.password === credentials.password;
|
||||
|
||||
/**
|
||||
* The Favorites / Recent catch-up resolver reads the STORED
|
||||
* playlist row, not this store, so a timezone learned here has
|
||||
@@ -79,9 +99,7 @@ export function withPortal() {
|
||||
playlistsService.transformPlaylistMeta(
|
||||
playlistId,
|
||||
(playlist) =>
|
||||
playlist.serverUrl !== credentials.serverUrl ||
|
||||
playlist.username !== credentials.username ||
|
||||
playlist.password !== credentials.password ||
|
||||
!answersFor(playlist, credentials) ||
|
||||
playlist.serverTimezone === serverTimezone
|
||||
? null
|
||||
: { ...playlist, serverTimezone }
|
||||
@@ -173,15 +191,18 @@ export function withPortal() {
|
||||
.map((format) => format.trim())
|
||||
.filter(Boolean)
|
||||
: undefined;
|
||||
// The answer belongs to the playlist whose
|
||||
// credentials were sent: its timezone is persisted
|
||||
// under THAT id regardless, while the store is
|
||||
// patched only if that playlist is still the
|
||||
// selected one — a source switch during the request
|
||||
// must not hand playlist A's status or clock to
|
||||
// playlist B.
|
||||
// The answer belongs to the panel whose credentials
|
||||
// were sent: its timezone is offered to THAT row
|
||||
// regardless, while the store is patched only if the
|
||||
// selected playlist is still that panel — neither a
|
||||
// source switch nor an in-place edit during the
|
||||
// request may hand the old panel's status or clock
|
||||
// to the new one.
|
||||
const current = store.currentPlaylist();
|
||||
if (current?.id === playlist.id) {
|
||||
if (
|
||||
current?.id === playlist.id &&
|
||||
answersFor(current, credentials)
|
||||
) {
|
||||
patchState(store, {
|
||||
portalStatus,
|
||||
currentPlaylist: {
|
||||
@@ -208,7 +229,11 @@ export function withPortal() {
|
||||
return portalStatus;
|
||||
} catch (error) {
|
||||
logger.error('Error checking portal status', error);
|
||||
if (store.currentPlaylist()?.id === playlist.id) {
|
||||
const current = store.currentPlaylist();
|
||||
if (
|
||||
current?.id === playlist.id &&
|
||||
answersFor(current, credentials)
|
||||
) {
|
||||
patchState(store, { portalStatus: 'unavailable' });
|
||||
}
|
||||
return 'unavailable';
|
||||
|
||||
Reference in new issue
Block a user