diff --git a/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.spec.ts b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.spec.ts new file mode 100644 index 000000000..b35b5fc81 --- /dev/null +++ b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.spec.ts @@ -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; + let dbCreatePlaylist: jest.Mock; + let dbGetPlaylist: jest.Mock; + let runtime: { + hasElectronMethod: jest.Mock; + }; + 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', + }) + ); + }); +}); diff --git a/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.ts b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.ts index 9a42485d2..e77e61460 100644 --- a/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.ts +++ b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-portal.feature.ts @@ -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; - dbGetPlaylist: (playlistId: string) => Promise; + dbGetPlaylist?: (playlistId: string) => Promise; }; }; @@ -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; }