// EmbyClient.fetch itself (not mocked away, unlike catalogService.test.ts, // which replaces it wholesale to isolate CatalogService) - specifically // the error-logging path added after several rounds of not being able to // see what Emby actually said on a failure (see docs/architecture.md's // player-track-selection section). Mocks the GLOBAL fetch, since that's // what EmbyClient.fetch itself calls. import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { EmbyClient } from '../src/emby/EmbyClient'; describe('EmbyClient.fetch', () => { it('logs Emby\'s status and response body on a failure, without consuming the body the real caller still needs', async (t) => { const logged: string[] = []; t.mock.method(console, 'log', (line: string) => { logged.push(line); }); t.mock.method(globalThis, 'fetch', async () => new Response('Server.MediaEncoding.Api boom', { status: 500 })); const res = await EmbyClient.fetch('/Videos/91988/hls1/main/0.ts?PlaySessionId=abc', 'tok'); const embyLines = logged.filter((l) => l.includes('[watchparty:emby]')); assert.equal(embyLines.length, 1); assert.ok(embyLines[0]!.includes('500')); assert.ok(embyLines[0]!.includes('Server.MediaEncoding.Api boom'), 'the real body text should make it into the log line'); assert.ok(embyLines[0]!.includes('/Videos/91988/hls1/main/0.ts')); // The caller (proxyResponse/fetchJson) still needs to read this same // response's body itself - logErrorResponse must read a *clone*, not // the original, or this would come back empty/throw. assert.equal(await res.text(), 'Server.MediaEncoding.Api boom'); }); it('redacts api_key from the logged path - Emby\'s own TranscodingUrl can carry one', async (t) => { const logged: string[] = []; t.mock.method(console, 'log', (line: string) => { logged.push(line); }); t.mock.method(globalThis, 'fetch', async () => new Response('nope', { status: 502 })); await EmbyClient.fetch('/Videos/1/master.m3u8?api_key=super-secret&MediaSourceId=x', 'tok'); const line = logged.find((l) => l.includes('[watchparty:emby]'))!; assert.ok(!line.includes('super-secret'), 'the real key must never reach the log'); assert.ok(line.includes('api_key=REDACTED')); }); it('logs nothing extra on a successful response - only failures are interesting', async (t) => { const logged: string[] = []; t.mock.method(console, 'log', (line: string) => { logged.push(line); }); t.mock.method(globalThis, 'fetch', async () => new Response('{}', { status: 200 })); await EmbyClient.fetch('/Users/u1/Views', 'tok'); assert.equal(logged.filter((l) => l.includes('[watchparty:emby]')).length, 0); }); });