// Exercises EmbySessionService directly against a mocked EmbyClient - no // real Emby server involved. Covers the request shape each of the three // Sessions-API calls sends and that a failure (a non-ok response, or a // thrown error) is swallowed rather than propagated - see the class's // own comment for why none of this may ever be allowed to disrupt real // playback over an admin-dashboard nicety. import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { EmbySessionService } from '../src/services/EmbySessionService'; import { EmbyClient } from '../src/emby/EmbyClient'; function okResponse(): Response { return new Response(null, { status: 204 }) as unknown as Response; } describe('EmbySessionService', () => { it('start() POSTs to /Sessions/Playing with the negotiated ids and the given position/pause state', async (t) => { let capturedPath = ''; let capturedToken = ''; let capturedBody: unknown = null; t.mock.method(EmbyClient, 'fetch', async (path: string, token: string, options: { body?: unknown }) => { capturedPath = path; capturedToken = token; capturedBody = options.body; return okResponse(); }); await EmbySessionService.start('tok1', 'item1', 'src1', 'play1', 12_345, false); assert.equal(capturedPath, '/Sessions/Playing'); assert.equal(capturedToken, 'tok1'); assert.deepEqual(capturedBody, { ItemId: 'item1', MediaSourceId: 'src1', PlaySessionId: 'play1', PositionTicks: 12_345, IsPaused: false, PlayMethod: 'Transcode', CanSeek: true }); }); it('progress() POSTs to /Sessions/Playing/Progress', async (t) => { let capturedPath = ''; t.mock.method(EmbyClient, 'fetch', async (path: string) => { capturedPath = path; return okResponse(); }); await EmbySessionService.progress('tok1', 'item1', 'src1', 'play1', 999, true); assert.equal(capturedPath, '/Sessions/Playing/Progress'); }); it('stop() POSTs to /Sessions/Playing/Stopped', async (t) => { let capturedPath = ''; let capturedBody: unknown = null; t.mock.method(EmbyClient, 'fetch', async (path: string, _token: string, options: { body?: unknown }) => { capturedPath = path; capturedBody = options.body; return okResponse(); }); await EmbySessionService.stop('tok1', 'item1', 'src1', 'play1', 42); assert.equal(capturedPath, '/Sessions/Playing/Stopped'); assert.deepEqual(capturedBody, { ItemId: 'item1', MediaSourceId: 'src1', PlaySessionId: 'play1', PositionTicks: 42 }); }); it('rounds a fractional PositionTicks rather than sending a non-integer', async (t) => { let capturedBody: { PositionTicks?: number } = {}; t.mock.method(EmbyClient, 'fetch', async (_path: string, _token: string, options: { body?: unknown }) => { capturedBody = options.body as { PositionTicks?: number }; return okResponse(); }); await EmbySessionService.progress('tok1', 'item1', 'src1', 'play1', 12_345.6, false); assert.equal(capturedBody.PositionTicks, 12_346); }); it('a non-ok response is logged, not thrown', async (t) => { t.mock.method(EmbyClient, 'fetch', async () => new Response(null, { status: 401 }) as unknown as Response); await assert.doesNotReject(() => EmbySessionService.start('tok1', 'item1', 'src1', 'play1', 0, false)); }); it('a thrown network error is logged, not propagated', async (t) => { t.mock.method(EmbyClient, 'fetch', async () => { throw new Error('network down'); }); await assert.doesNotReject(() => EmbySessionService.stop('tok1', 'item1', 'src1', 'play1', 0)); }); });