mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 21:56:18 -08:00
fix(settings): withhold Electron Xtream reads while locks are unknown, persist the switch when settings are unreadable, re-stamp after a recovered read
- ElectronXtreamDataSource serves no categories, content or search hits while the lock store withholds everything; its SQLite index may still carry a stale stamp. - setupPin decides whether to persist the switch from the settings value before the PIN is stored, since enabled follows hasPin while the switch is unknown. - A lock store recovered by a later read marks its playlists stale so the index is re-derived, a persisted entry must carry all three lists, and a stale Stalker search page is dropped before touching the withheld-id bookkeeping. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
4f931030e1
commit
bfdfe18a9c
11 files changed
+163
-27
No files matched your search
@@ -80,6 +80,18 @@ describe('ParentalLockLockStore', () => {
|
||||
expect(store.readable()).toBe(true);
|
||||
});
|
||||
|
||||
it('re-derives the index from a store recovered after a failed read', async () => {
|
||||
storage.readLocks.mockResolvedValueOnce(null);
|
||||
await store.load();
|
||||
expect(store.readable()).toBe(false);
|
||||
expect(setCategoryLocks).not.toHaveBeenCalled();
|
||||
|
||||
await expect(store.ensureReadable()).resolves.toBe(true);
|
||||
|
||||
expect(setCategoryLocks).toHaveBeenCalledWith('pl-1', 'live', [7]);
|
||||
expect(store.readable()).toBe(true);
|
||||
});
|
||||
|
||||
it('re-derives the SQLite index from the store on load', async () => {
|
||||
await store.load();
|
||||
// ensureReadable awaits the reconcile that load() started.
|
||||
|
||||
@@ -116,6 +116,12 @@ export class ParentalLockLockStore {
|
||||
}
|
||||
this.locks.set(locks);
|
||||
this.unreadable.set(false);
|
||||
// The index may carry stamps from a write that failed before
|
||||
// the read did; re-derive it from the recovered store as load()
|
||||
// does.
|
||||
for (const playlistId of Object.keys(locks)) {
|
||||
this.markIndexStale(playlistId);
|
||||
}
|
||||
this.revisionState.update((value) => value + 1);
|
||||
}
|
||||
await this.reconcileXtreamIndex();
|
||||
|
||||
@@ -35,7 +35,9 @@ describe('ParentalLockStorageService.readLocks', () => {
|
||||
it('normalizes a stored store', async () => {
|
||||
localStorage.setItem(
|
||||
PARENTAL_LOCK_STORE_KEY,
|
||||
JSON.stringify({ p: { m3u: ['Adult'], junk: 1 } })
|
||||
JSON.stringify({
|
||||
p: { xtream: [], stalker: [], m3u: ['Adult'], junk: 1 },
|
||||
})
|
||||
);
|
||||
await expect(service.readLocks()).resolves.toEqual({
|
||||
p: { xtream: [], stalker: [], m3u: ['Adult'] },
|
||||
@@ -46,11 +48,18 @@ describe('ParentalLockStorageService.readLocks', () => {
|
||||
['truncated JSON', '{"p":{"m3u":["Adu'],
|
||||
['an array payload', '[]'],
|
||||
['a scalar payload', '"locks"'],
|
||||
['a corrupt nested list', '{"p":{"xtream":"corrupt"}}'],
|
||||
['a corrupt nested entry', '{"p":{"m3u":[1]}}'],
|
||||
[
|
||||
'a corrupt nested list',
|
||||
'{"p":{"xtream":"corrupt","stalker":[],"m3u":[]}}',
|
||||
],
|
||||
[
|
||||
'a corrupt nested entry',
|
||||
'{"p":{"xtream":[],"stalker":[],"m3u":[1]}}',
|
||||
],
|
||||
['a playlist entry missing a list', '{"p":{}}'],
|
||||
[
|
||||
'an entry the normalizer would drop',
|
||||
'{"p":{"xtream":[{"categoryType":"live","xtreamId":"nope"}]}}',
|
||||
'{"p":{"xtream":[{"categoryType":"live","xtreamId":"nope"}],"stalker":[],"m3u":[]}}',
|
||||
],
|
||||
])('treats %s as a failed read, not an empty store', async (_, raw) => {
|
||||
localStorage.setItem(PARENTAL_LOCK_STORE_KEY, raw);
|
||||
|
||||
@@ -226,6 +226,21 @@ describe('ParentalLockService', () => {
|
||||
expect(updateBridgeSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('still persists the switch when settings could not be read', async () => {
|
||||
storageFailure.set('load');
|
||||
prompt.requestPin.mockResolvedValue('1234');
|
||||
const service = await createService();
|
||||
|
||||
await expect(service.setupPin()).resolves.toBe(true);
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledWith({
|
||||
parentalLockEnabled: true,
|
||||
});
|
||||
expect(updateBridgeSettings).toHaveBeenCalledWith({
|
||||
parentalLockEnabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the session locked on a failed PIN read and retries before unlocking', async () => {
|
||||
storage.pinHash = await hashParentalLockPin('1234');
|
||||
parentalLockEnabled.set(true);
|
||||
|
||||
@@ -238,6 +238,12 @@ export class ParentalLockService {
|
||||
if (!this.prompt) {
|
||||
return false;
|
||||
}
|
||||
// Decided BEFORE the PIN is stored: with the settings switch
|
||||
// unknown, `enabled` follows `hasPin`, and would read as on the
|
||||
// moment the PIN lands — skipping the persistence the next launch
|
||||
// depends on.
|
||||
const needsPersist =
|
||||
this.settingsStore.parentalLockEnabled?.() !== true;
|
||||
const pin = await this.prompt.requestPin({ mode: 'set' });
|
||||
if (pin === null) {
|
||||
return false;
|
||||
@@ -246,7 +252,7 @@ export class ParentalLockService {
|
||||
return false;
|
||||
}
|
||||
this.unlockedState.set(true);
|
||||
if (!this.enabled() && !(await this.persistEnabled(true))) {
|
||||
if (needsPersist && !(await this.persistEnabled(true))) {
|
||||
this.unlockedState.set(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user