Files
iptvnator/libs/shared/interfaces/src/lib/parental-lock.util.spec.ts
T
4grayandClaude Fable 5.1 02f5b09201 fix(settings): withhold the Xtream catalog at relock time, keep exact M3U titles in backups, roll back a failed relock-timeout save
- A relock now fails closed immediately: the selected detail is stepped
  off against the lock store, the catalog lists and stored search results
  are emptied, and the filtered reloads publish only while the captured
  lock version is still current.
- Backups carry M3U lock titles verbatim (exact dedup), since the locks
  match group titles exactly.
- A relock-timeout write that fails reverts the in-memory value and shows
  the settings save-failure snackbar.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 06:21:18 +02:00

159 lines
6.0 KiB
TypeScript

import {
isParentalLockPlaylistLocksEmpty,
normalizeParentalLockGroupTitles,
isWellFormedParentalLockStore,
lockedStalkerCategoryIds,
lockedXtreamCategoryIds,
normalizeParentalLockPlaylistLocks,
normalizeParentalLockStore,
toParentalLockStalkerCategoryType,
toParentalLockXtreamCategoryType,
} from './parental-lock.util';
describe('parental-lock.util', () => {
describe('normalizeParentalLockPlaylistLocks', () => {
it('keeps well-formed entries and drops junk, duplicates and the All pseudo-genre', () => {
const locks = normalizeParentalLockPlaylistLocks({
xtream: [
{ categoryType: 'live', xtreamId: 7 },
{ categoryType: 'live', xtreamId: '7' },
{ categoryType: 'movies', xtreamId: 'abc' },
{ categoryType: 'unknown', xtreamId: 1 },
null,
],
stalker: [
{ categoryType: 'itv', categoryId: 12 },
{ categoryType: 'itv', categoryId: '12' },
{ categoryType: 'vod', categoryId: '*' },
{ categoryType: 'radio', categoryId: '' },
],
m3u: ['Adult', 'Adult', 42, 'News'],
});
expect(locks).toEqual({
xtream: [{ categoryType: 'live', xtreamId: 7 }],
stalker: [{ categoryType: 'itv', categoryId: '12' }],
m3u: ['Adult', 'News'],
});
});
it('returns empty locks for non-objects', () => {
expect(normalizeParentalLockPlaylistLocks(null)).toEqual({
xtream: [],
stalker: [],
m3u: [],
});
expect(normalizeParentalLockPlaylistLocks('x')).toEqual({
xtream: [],
stalker: [],
m3u: [],
});
});
});
describe('normalizeParentalLockStore', () => {
it('drops playlists that lock nothing', () => {
const store = normalizeParentalLockStore({
'p-1': { m3u: ['XXX'] },
'p-2': { xtream: [], stalker: [], m3u: [] },
'': { m3u: ['ignored'] },
'p-3': 'junk',
});
expect(Object.keys(store)).toEqual(['p-1']);
expect(isParentalLockPlaylistLocksEmpty(store['p-1'])).toBe(false);
});
it('returns an empty store for anything that is not an object', () => {
expect(normalizeParentalLockStore(undefined)).toEqual({});
expect(normalizeParentalLockStore([])).toEqual({});
});
});
describe('id lookups', () => {
const locks = normalizeParentalLockPlaylistLocks({
xtream: [
{ categoryType: 'live', xtreamId: 1 },
{ categoryType: 'movies', xtreamId: 2 },
],
stalker: [
{ categoryType: 'itv', categoryId: '10' },
{ categoryType: 'series', categoryId: '11' },
],
});
it('filters Xtream ids by category type', () => {
expect(lockedXtreamCategoryIds(locks, 'live')).toEqual([1]);
expect(lockedXtreamCategoryIds(locks, 'series')).toEqual([]);
expect(lockedXtreamCategoryIds(undefined, 'live')).toEqual([]);
});
it('filters Stalker ids by section', () => {
expect(lockedStalkerCategoryIds(locks, 'itv')).toEqual(['10']);
expect(lockedStalkerCategoryIds(locks, 'vod')).toEqual([]);
});
});
describe('section mapping', () => {
it('maps Xtream route sections to DB category types', () => {
expect(toParentalLockXtreamCategoryType('live')).toBe('live');
expect(toParentalLockXtreamCategoryType('vod')).toBe('movies');
expect(toParentalLockXtreamCategoryType('movies')).toBe('movies');
expect(toParentalLockXtreamCategoryType('series')).toBe('series');
expect(toParentalLockXtreamCategoryType('search')).toBeNull();
});
it('accepts only Stalker sections with a genre list', () => {
expect(toParentalLockStalkerCategoryType('itv')).toBe('itv');
expect(toParentalLockStalkerCategoryType('radio')).toBe('radio');
expect(toParentalLockStalkerCategoryType('favorites')).toBeNull();
expect(toParentalLockStalkerCategoryType(undefined)).toBeNull();
});
});
});
describe('isWellFormedParentalLockStore', () => {
it('accepts what writeLocks produces, lists omitted or empty', () => {
expect(isWellFormedParentalLockStore({})).toBe(true);
expect(
isWellFormedParentalLockStore({
p: {
xtream: [{ categoryType: 'live', xtreamId: 7 }],
stalker: [{ categoryType: 'itv', categoryId: '9' }],
m3u: ['Adult'],
},
q: { m3u: [] },
})
).toBe(true);
});
it.each([
['a non-object root', []],
['a non-object playlist entry', { p: 'corrupt' }],
['a non-list lock field', { p: { xtream: 'corrupt' } }],
['a non-string group title', { p: { m3u: [1] } }],
[
'an unknown category type',
{ p: { xtream: [{ categoryType: 'x', xtreamId: 1 }] } },
],
[
'a non-numeric xtream id',
{ p: { xtream: [{ categoryType: 'live', xtreamId: 'nope' }] } },
],
[
'the Stalker "all" pseudo id',
{ p: { stalker: [{ categoryType: 'itv', categoryId: '*' }] } },
],
])('rejects %s', (_, value) => {
expect(isWellFormedParentalLockStore(value)).toBe(false);
});
});
describe('normalizeParentalLockGroupTitles', () => {
it('deduplicates exactly, keeping surrounding whitespace', () => {
expect(
normalizeParentalLockGroupTitles([' Adult ', ' Adult ', 'Adult', 7])
).toEqual([' Adult ', 'Adult']);
});
});