mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
fix(settings): gate the workspace on parental lock init, edit only a readable lock store, guard the deferred reload, validate backup lock entries
- The workspace route resolver awaits ParentalLockService.initialize() next to the settings load, so no route or catalog activates before the PIN and lock store are known. - Every lock write re-reads a failed store before building its edit, so a recovered store is edited rather than overwritten. - The deferred hydration reload runs under the publish guard of the request that deferred it. - Backup import validates every parental lock entry and rejects a damaged list instead of erasing the persisted locks on restore. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
d4d0754e12
commit
4bd4bf2fc2
12 files changed
+257
-60
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
import { ParentalLockService, SettingsStore } from '@iptvnator/services';
|
||||
|
||||
describe('app routes', () => {
|
||||
let workspaceRoute: import('@angular/router').Route | undefined;
|
||||
@@ -67,6 +67,52 @@ describe('app routes', () => {
|
||||
TestBed.resetTestingModule();
|
||||
});
|
||||
|
||||
it('waits for the parental lock state before activating workspace children', async () => {
|
||||
let releaseLock!: () => void;
|
||||
const lockPending = new Promise<void>((resolve) => {
|
||||
releaseLock = resolve;
|
||||
});
|
||||
const initialize = jest.fn(() => lockPending);
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: SettingsStore,
|
||||
useValue: {
|
||||
loadSettings: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
{ provide: ParentalLockService, useValue: { initialize } },
|
||||
],
|
||||
});
|
||||
const settingsReadyResolver =
|
||||
workspaceRoute?.resolve?.['settingsReady'];
|
||||
if (typeof settingsReadyResolver !== 'function') {
|
||||
throw new Error('resolver missing');
|
||||
}
|
||||
let resolved = false;
|
||||
const resolution = TestBed.runInInjectionContext(() =>
|
||||
Promise.resolve(
|
||||
(
|
||||
settingsReadyResolver as import('@angular/router').ResolveFn<void>
|
||||
)(
|
||||
{} as import('@angular/router').ActivatedRouteSnapshot,
|
||||
{} as import('@angular/router').RouterStateSnapshot
|
||||
)
|
||||
)
|
||||
).then(() => {
|
||||
resolved = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(initialize).toHaveBeenCalled();
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
releaseLock();
|
||||
await resolution;
|
||||
expect(resolved).toBe(true);
|
||||
});
|
||||
|
||||
it('waits for settings before activating workspace children', async () => {
|
||||
let releaseSettings!: () => void;
|
||||
const settingsPending = new Promise<void>((resolve) => {
|
||||
@@ -79,6 +125,12 @@ describe('app routes', () => {
|
||||
provide: SettingsStore,
|
||||
useValue: { loadSettings },
|
||||
},
|
||||
{
|
||||
provide: ParentalLockService,
|
||||
useValue: {
|
||||
initialize: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const settingsReadyResolver =
|
||||
@@ -120,8 +172,7 @@ describe('app routes', () => {
|
||||
(route) => route.path === 'playlists/:id'
|
||||
);
|
||||
const loadChildren = playlistRoute?.loadChildren as
|
||||
| (() => Promise<typeof workspaceChildren>)
|
||||
| undefined;
|
||||
(() => Promise<typeof workspaceChildren>) | undefined;
|
||||
const m3uRoutes = (await loadChildren?.()) ?? [];
|
||||
const defaultRoute = m3uRoutes.find((route) => route.path === '');
|
||||
const favoritesRoute = m3uRoutes.find(
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { Router, Routes } from '@angular/router';
|
||||
import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services';
|
||||
import {
|
||||
ParentalLockService,
|
||||
RuntimeCapabilitiesService,
|
||||
SettingsStore,
|
||||
} from '@iptvnator/services';
|
||||
import { WorkspaceStartupPreferencesService } from '@iptvnator/workspace/shell/util';
|
||||
import { settingsUnsavedChangesGuard } from './settings/settings-unsaved-changes.guard';
|
||||
|
||||
const settingsReadyResolver = () => inject(SettingsStore).loadSettings();
|
||||
// The workspace activates only once settings AND the parental lock state
|
||||
// (PIN, lock store) are known: before that the lock reads as off and a
|
||||
// slower IndexedDB read would let the catalogs admit protected rows.
|
||||
const settingsReadyResolver = async () => {
|
||||
const settingsStore = inject(SettingsStore);
|
||||
const parentalLock = inject(ParentalLockService);
|
||||
await Promise.all([
|
||||
settingsStore.loadSettings(),
|
||||
parentalLock.initialize(),
|
||||
]);
|
||||
};
|
||||
|
||||
const workspaceEntryRedirect = async () =>
|
||||
inject(WorkspaceStartupPreferencesService).resolveInitialWorkspacePath();
|
||||
|
||||
Reference in new issue
Block a user