mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
Category rows crossed the DB-worker IPC boundary with Drizzle's camelCase property names while the renderer contracts declare snake_case, so backup export dropped hidden-category IDs and restore degraded to a type-only match that hid every category. Project category ops to the declared wire shape, normalize restore state from untrusted sources (dropping entries without a numeric xtreamId), reject entries with missing user-state collections, and add full export→import round-trip coverage (unit manifest-equality + Electron e2e) plus regression tests. Closes #1017 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
import { normalizeXtreamPendingRestoreState } from './xtream-restore-state.util';
|
|
|
|
describe('normalizeXtreamPendingRestoreState', () => {
|
|
const emptyState = {
|
|
hiddenCategories: [],
|
|
favorites: [],
|
|
recentlyViewed: [],
|
|
playbackPositions: [],
|
|
};
|
|
|
|
it.each([null, undefined, 'text', 42, []])(
|
|
'returns an empty state for non-object input %p',
|
|
(value) => {
|
|
expect(normalizeXtreamPendingRestoreState(value)).toEqual(
|
|
emptyState
|
|
);
|
|
}
|
|
);
|
|
|
|
it('falls back to empty arrays for missing or non-array fields', () => {
|
|
expect(
|
|
normalizeXtreamPendingRestoreState({
|
|
hiddenCategories: 'broken',
|
|
favorites: null,
|
|
})
|
|
).toEqual(emptyState);
|
|
});
|
|
|
|
it('keeps hidden categories with a numeric xtream ID and drops the rest', () => {
|
|
const state = normalizeXtreamPendingRestoreState({
|
|
hiddenCategories: [
|
|
{ categoryType: 'live', xtreamId: 101 },
|
|
// Entries exported by builds affected by issue #1017 carry
|
|
// no ID at all and must not survive normalization.
|
|
{ categoryType: 'live' },
|
|
{ categoryType: 'movies', xtreamId: 'not-a-number' },
|
|
{ categoryType: 'unknown', xtreamId: 5 },
|
|
{ categoryType: 'series', xtreamId: '301' },
|
|
null,
|
|
],
|
|
});
|
|
|
|
expect(state.hiddenCategories).toEqual([
|
|
{ categoryType: 'live', xtreamId: 101 },
|
|
{ categoryType: 'series', xtreamId: 301 },
|
|
]);
|
|
});
|
|
|
|
it('drops favorites and recently viewed entries without a numeric xtream ID', () => {
|
|
const state = normalizeXtreamPendingRestoreState({
|
|
favorites: [
|
|
{
|
|
contentType: 'movie',
|
|
xtreamId: 7,
|
|
addedAt: '2026-07-01T00:00:00.000Z',
|
|
},
|
|
{ contentType: 'movie' },
|
|
],
|
|
recentlyViewed: [
|
|
{
|
|
contentType: 'live',
|
|
xtreamId: '9',
|
|
viewedAt: '2026-07-01T00:00:00.000Z',
|
|
},
|
|
{ contentType: 'live', xtreamId: Number.NaN },
|
|
],
|
|
});
|
|
|
|
expect(state.favorites).toEqual([
|
|
{
|
|
contentType: 'movie',
|
|
xtreamId: 7,
|
|
addedAt: '2026-07-01T00:00:00.000Z',
|
|
},
|
|
]);
|
|
expect(state.recentlyViewed).toEqual([
|
|
{
|
|
contentType: 'live',
|
|
xtreamId: 9,
|
|
viewedAt: '2026-07-01T00:00:00.000Z',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('keeps playback position objects and drops primitives', () => {
|
|
const position = {
|
|
contentXtreamId: 12,
|
|
contentType: 'vod',
|
|
positionSeconds: 30,
|
|
};
|
|
|
|
const state = normalizeXtreamPendingRestoreState({
|
|
playbackPositions: [position, 'broken', null],
|
|
});
|
|
|
|
expect(state.playbackPositions).toEqual([position]);
|
|
});
|
|
});
|