fix(backup): reject incomplete local Xtream credentials

This commit is contained in:
4gray committed 2026-07-27 12:25:52 +02:00
1 parent 9204f400bb
commit 1961952f48
2 files changed
+68 -9

No files matched your search

@@ -646,7 +646,9 @@ export class PlaylistBackupService {
playlist._id === entry.exportedId &&
this.getPlaylistPortalType(playlist) === 'xtream' &&
this.normalizeUrlIdentity(playlist.serverUrl ?? '') ===
this.normalizeUrlIdentity(entry.connection.serverUrl)
this.normalizeUrlIdentity(entry.connection.serverUrl) &&
this.normalizeIdentityValue(playlist.username ?? '') !== '' &&
this.normalizeIdentityValue(playlist.password ?? '') !== ''
);
if (exactLocalMatch) {
return entry;
@@ -8,8 +8,8 @@ import {
import { createPlaylistBackupService } from './playlist-backup.service.test-helpers';
describe('PlaylistBackupService redacted Xtream credential validation', () => {
it('does not persist the entry when the normal connection check rejects the supplied credentials', async () => {
const manifest: PlaylistBackupManifestV1 = {
function redactedManifest(): PlaylistBackupManifestV1 {
return {
kind: PLAYLIST_BACKUP_KIND,
version: PLAYLIST_BACKUP_VERSION,
exportedAt: '2026-07-27T00:00:00.000Z',
@@ -33,6 +33,9 @@ describe('PlaylistBackupService redacted Xtream credential validation', () => {
},
],
};
}
it('does not persist the entry when the normal connection check rejects the supplied credentials', async () => {
const playlistsService = {
addPlaylist: jest.fn((playlist: Playlist) => of(playlist)),
getAllData: jest.fn(() => of([])),
@@ -47,12 +50,15 @@ describe('PlaylistBackupService redacted Xtream credential validation', () => {
portalStatusService,
});
const summary = await service.importBackup(JSON.stringify(manifest), {
resolveXtreamCredentials: async () => ({
username: 'restored-user',
password: 'wrong-password',
}),
});
const summary = await service.importBackup(
JSON.stringify(redactedManifest()),
{
resolveXtreamCredentials: async () => ({
username: 'restored-user',
password: 'wrong-password',
}),
}
);
expect(summary).toEqual(
expect.objectContaining({ failed: 1, imported: 0 })
@@ -62,4 +68,55 @@ describe('PlaylistBackupService redacted Xtream credential validation', () => {
);
expect(playlistsService.addPlaylist).not.toHaveBeenCalled();
});
it.each([
['username', '', 'local-password'],
['password', 'local-user', ' '],
])(
'does not treat an exact local row with a blank %s as usable credentials',
async (_field, username, password) => {
const local = {
_id: 'xtream-redacted',
title: 'Incomplete local Xtream',
count: 1,
importDate: '2026-07-01T00:00:00.000Z',
lastUsage: '2026-07-01T00:00:00.000Z',
autoRefresh: false,
serverUrl: 'https://portal.test/base',
username,
password,
} as Playlist;
const playlistsService = {
addPlaylist: jest.fn((playlist: Playlist) => of(playlist)),
getAllData: jest.fn(() => of([local])),
getRawPlaylistById: jest.fn(() => of('#EXTM3U')),
handlePlaylistParsing: jest.fn(),
};
const resolveXtreamCredentials = jest
.fn()
.mockResolvedValue(null);
const service = createPlaylistBackupService({
playlistsService,
});
const summary = await service.importBackup(
JSON.stringify(redactedManifest()),
{ resolveXtreamCredentials }
);
expect(resolveXtreamCredentials).toHaveBeenCalledWith({
exportedId: 'xtream-redacted',
serverUrl: 'https://portal.test/base/',
title: 'Redacted Xtream',
});
expect(summary).toEqual({
imported: 0,
merged: 0,
skipped: 1,
failed: 0,
errors: [],
});
expect(playlistsService.addPlaylist).not.toHaveBeenCalled();
}
);
});