refactor(stalker): gate portal sqlite sync by runtime capability

This commit is contained in:
4gray committed 2026-05-22 13:34:16 +03:00
1 parent 790dbb7062
commit 465b1b5559
2 files changed
+142 -7

No files matched your search

@@ -0,0 +1,131 @@
import { TestBed } from '@angular/core/testing';
import { signalStore } from '@ngrx/signals';
import { DataService, RuntimeCapabilitiesService } from '@iptvnator/services';
import { PlaylistMeta, STALKER_REQUEST } from '@iptvnator/shared/interfaces';
import { StalkerSessionService } from '../../stalker-session.service';
import { withStalkerPortal } from './with-stalker-portal.feature';
jest.mock('@iptvnator/portal/shared/util', () => ({
createLogger: () => ({
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
}),
}));
const TestPortalStore = signalStore(withStalkerPortal());
const PLAYLIST = {
_id: 'stalker-1',
title: 'Demo Stalker',
count: 0,
autoRefresh: false,
importDate: '2026-05-22T00:00:00.000Z',
portalUrl: 'http://demo.example/stalker_portal/server/load.php',
macAddress: '00:1A:79:00:00:01',
isFullStalkerPortal: false,
} as PlaylistMeta;
describe('withStalkerPortal', () => {
let store: InstanceType<typeof TestPortalStore>;
let dbCreatePlaylist: jest.Mock;
let dbGetPlaylist: jest.Mock;
let runtime: {
hasElectronMethod: jest.Mock<boolean, [string]>;
};
let stalkerSession: {
ensureToken: jest.Mock;
setActiveWatchdogPlaylist: jest.Mock;
};
beforeEach(() => {
dbCreatePlaylist = jest.fn().mockResolvedValue(undefined);
dbGetPlaylist = jest.fn().mockResolvedValue(null);
Object.defineProperty(window, 'electron', {
value: {
dbCreatePlaylist,
dbGetPlaylist,
} as Window['electron'],
configurable: true,
});
runtime = {
hasElectronMethod: jest.fn(() => true),
};
stalkerSession = {
ensureToken: jest.fn(),
setActiveWatchdogPlaylist: jest.fn(),
};
TestBed.configureTestingModule({
providers: [
TestPortalStore,
{
provide: DataService,
useValue: {
sendIpcEvent: jest.fn(),
},
},
{
provide: RuntimeCapabilitiesService,
useValue: runtime,
},
{
provide: StalkerSessionService,
useValue: stalkerSession,
},
],
});
store = TestBed.inject(TestPortalStore);
});
it('creates the SQLite playlist row when the runtime exposes the required bridge methods', async () => {
await store.setCurrentPlaylist(PLAYLIST);
expect(dbGetPlaylist).toHaveBeenCalledWith('stalker-1');
expect(dbCreatePlaylist).toHaveBeenCalledWith({
id: 'stalker-1',
name: 'Demo Stalker',
macAddress: '00:1A:79:00:00:01',
url: 'http://demo.example/stalker_portal/server/load.php',
type: 'stalker',
});
expect(stalkerSession.setActiveWatchdogPlaylist).toHaveBeenCalledWith(
expect.objectContaining({
macAddress: '00:1A:79:00:00:01',
portalUrl: 'http://demo.example/stalker_portal/server/load.php',
})
);
});
it('does not touch SQLite when the Electron bridge is partial', async () => {
runtime.hasElectronMethod.mockImplementation(
(methodName) => methodName === 'dbGetPlaylist'
);
await store.setCurrentPlaylist(PLAYLIST);
expect(dbGetPlaylist).not.toHaveBeenCalled();
expect(dbCreatePlaylist).not.toHaveBeenCalled();
});
it('sends Stalker requests through DataService without requiring the SQLite bridge', async () => {
const dataService = TestBed.inject(DataService) as unknown as {
sendIpcEvent: jest.Mock;
};
dataService.sendIpcEvent.mockResolvedValue({ js: { data: [] } });
await store.makeStalkerRequest(PLAYLIST, { action: 'get_profile' });
expect(dataService.sendIpcEvent).toHaveBeenCalledWith(
STALKER_REQUEST,
expect.objectContaining({
macAddress: '00:1A:79:00:00:01',
params: { action: 'get_profile' },
url: 'http://demo.example/stalker_portal/server/load.php',
})
);
});
});
@@ -8,20 +8,20 @@ import {
} from '@ngrx/signals';
import { PlaylistMeta, STALKER_REQUEST } from '@iptvnator/shared/interfaces';
import { createLogger } from '@iptvnator/portal/shared/util';
import { DataService } from '@iptvnator/services';
import { DataService, RuntimeCapabilitiesService } from '@iptvnator/services';
import { StalkerSessionService } from '../../stalker-session.service';
import { toStalkerSessionPlaylist } from '../utils';
type StalkerPortalWindow = Window & {
electron?: {
dbCreatePlaylist: (playlist: {
dbCreatePlaylist?: (playlist: {
id: string;
name: string;
macAddress: string;
url: string;
type: 'stalker';
}) => Promise<unknown>;
dbGetPlaylist: (playlistId: string) => Promise<unknown>;
dbGetPlaylist?: (playlistId: string) => Promise<unknown>;
};
};
@@ -81,8 +81,8 @@ export function withStalkerPortal() {
withMethods(
(
store,
dataService = inject(DataService),
stalkerSession = inject(StalkerSessionService)
stalkerSession = inject(StalkerSessionService),
runtime = inject(RuntimeCapabilitiesService)
) => ({
async setCurrentPlaylist(playlist: PlaylistMeta | undefined) {
stalkerSession.setActiveWatchdogPlaylist(
@@ -96,7 +96,8 @@ export function withStalkerPortal() {
// Only sync if this is actually a Stalker playlist (has macAddress and portalUrl)
if (
playlist &&
dataService.isElectron &&
runtime.hasElectronMethod('dbGetPlaylist') &&
runtime.hasElectronMethod('dbCreatePlaylist') &&
playlist._id &&
playlist.macAddress &&
playlist.portalUrl
@@ -104,7 +105,10 @@ export function withStalkerPortal() {
try {
const electronApi = (window as StalkerPortalWindow)
.electron;
if (!electronApi) {
if (
!electronApi?.dbGetPlaylist ||
!electronApi.dbCreatePlaylist
) {
return;
}