mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
* fix: redact sensitive log data * fix(ci): keep logging preload self-contained * fix(logging): preserve shared diagnostics * fix(logging): close trace redaction gaps * fix(logging): redact Xtream path credentials * fix(logging): close credential redaction gaps * fix(logging): suppress external player arguments * fix(logging): harden URL and date redaction * fix(logging): redact map keys and URL fragments * fix(logging): redact sensitive map values * fix(logging): redact credentials in diagnostic text * fix(logging): close remaining credential leaks
194 lines
6.1 KiB
TypeScript
194 lines
6.1 KiB
TypeScript
import { TestBed } from '@angular/core/testing';
|
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
import { Store } from '@ngrx/store';
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
import { of } from 'rxjs';
|
|
import { DialogService } from '@iptvnator/ui/components';
|
|
import { SettingsStore } from '@iptvnator/services';
|
|
import {
|
|
ELECTRON_BRIDGE_SECURITY_ERROR_CODES,
|
|
PLAYLIST_PARSE_BY_URL,
|
|
SECURITY_ERROR_PREFIX,
|
|
} from '@iptvnator/shared/interfaces';
|
|
import { ElectronService } from './electron.service';
|
|
|
|
describe('ElectronService', () => {
|
|
const session = { id: 'session-1' };
|
|
let electronBridge: {
|
|
fetchPlaylistByUrl: jest.Mock;
|
|
onPlayerError: jest.Mock;
|
|
openInMpv: jest.Mock;
|
|
openInVlc: jest.Mock;
|
|
};
|
|
let snackBar: { open: jest.Mock };
|
|
let service: ElectronService;
|
|
|
|
beforeEach(() => {
|
|
jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
|
|
electronBridge = {
|
|
fetchPlaylistByUrl: jest.fn(),
|
|
onPlayerError: jest.fn(),
|
|
openInMpv: jest.fn().mockResolvedValue(session),
|
|
openInVlc: jest.fn().mockResolvedValue(session),
|
|
};
|
|
snackBar = {
|
|
open: jest.fn(() => ({
|
|
onAction: () => of(undefined),
|
|
})),
|
|
};
|
|
|
|
Object.defineProperty(window, 'electron', {
|
|
configurable: true,
|
|
value: electronBridge,
|
|
});
|
|
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
ElectronService,
|
|
{
|
|
provide: MatSnackBar,
|
|
useValue: snackBar,
|
|
},
|
|
{
|
|
provide: DialogService,
|
|
useValue: {
|
|
openConfirmDialog: jest.fn(),
|
|
},
|
|
},
|
|
{
|
|
provide: SettingsStore,
|
|
useValue: {
|
|
getTrustOptions: jest.fn(() => ({
|
|
trustedPrivateNetworkEpgUrls: [],
|
|
trustedInsecureTlsHosts: [],
|
|
})),
|
|
getSettings: jest.fn(() => ({
|
|
trustedInsecureTlsHosts: [],
|
|
})),
|
|
updateSettings: jest.fn().mockResolvedValue(undefined),
|
|
},
|
|
},
|
|
{
|
|
provide: Store,
|
|
useValue: {
|
|
dispatch: jest.fn(),
|
|
},
|
|
},
|
|
{
|
|
provide: TranslateService,
|
|
useValue: {
|
|
instant: jest.fn((key: string) => key),
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
service = TestBed.inject(ElectronService);
|
|
});
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(window, 'electron', {
|
|
configurable: true,
|
|
value: undefined,
|
|
});
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
it('ignores URL imports without a payload instead of calling the Electron bridge', async () => {
|
|
await service.sendIpcEvent(PLAYLIST_PARSE_BY_URL);
|
|
|
|
expect(electronBridge.fetchPlaylistByUrl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('redacts credentials from backend player errors before logging', () => {
|
|
const secret = 'player-error-token-secret';
|
|
const listener = electronBridge.onPlayerError.mock.calls[0][0];
|
|
|
|
listener({
|
|
player: 'MPV',
|
|
error: 'Playback failed',
|
|
originalError: `Request failed: token=${secret}&channel=news`,
|
|
});
|
|
|
|
const output = JSON.stringify(
|
|
(console.error as jest.Mock).mock.calls
|
|
);
|
|
expect(output).not.toContain(secret);
|
|
expect(output).toContain('channel=news');
|
|
});
|
|
|
|
it('shows the trust-host action for Electron-wrapped security errors', async () => {
|
|
const securityPayload = {
|
|
code: ELECTRON_BRIDGE_SECURITY_ERROR_CODES.InvalidTlsCertificate,
|
|
host: 'playlist.local',
|
|
message: 'Certificate for this playlist host is invalid.',
|
|
};
|
|
electronBridge.fetchPlaylistByUrl.mockRejectedValue(
|
|
new Error(
|
|
`Error invoking remote method 'FETCH_PLAYLIST_BY_URL': Error: ${SECURITY_ERROR_PREFIX}${JSON.stringify(
|
|
securityPayload
|
|
)}`
|
|
)
|
|
);
|
|
|
|
await service.sendIpcEvent(PLAYLIST_PARSE_BY_URL, {
|
|
url: 'https://playlist.local/list.m3u',
|
|
});
|
|
await Promise.resolve();
|
|
|
|
expect(snackBar.open).toHaveBeenCalledWith(
|
|
'Certificate for this playlist host is invalid.',
|
|
'Trust host',
|
|
{ duration: 10000 }
|
|
);
|
|
});
|
|
|
|
it('preserves an absent MPV user-agent so backend fallback headers can apply', async () => {
|
|
await service.sendIpcEvent('OPEN_MPV_PLAYER', {
|
|
url: 'https://example.test/live.m3u8',
|
|
headers: {
|
|
'User-Agent': 'FallbackAgent/1.0',
|
|
},
|
|
});
|
|
|
|
expect(electronBridge.openInMpv).toHaveBeenCalledWith(
|
|
'https://example.test/live.m3u8',
|
|
'',
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
{
|
|
'User-Agent': 'FallbackAgent/1.0',
|
|
}
|
|
);
|
|
});
|
|
|
|
it('preserves an absent VLC user-agent so backend fallback headers can apply', async () => {
|
|
await service.sendIpcEvent('OPEN_VLC_PLAYER', {
|
|
url: 'https://example.test/live.m3u8',
|
|
headers: {
|
|
'User-Agent': 'FallbackAgent/1.0',
|
|
},
|
|
});
|
|
|
|
expect(electronBridge.openInVlc).toHaveBeenCalledWith(
|
|
'https://example.test/live.m3u8',
|
|
'',
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
{
|
|
'User-Agent': 'FallbackAgent/1.0',
|
|
}
|
|
);
|
|
});
|
|
});
|