// CastTokenService is a small in-memory Map, same shape as AuthService - // exercised directly, no mocking needed. Each test uses its own // partyId/userId (never reused across `it` blocks) since the service is // a module-level singleton shared by every test in this file - see // musicQueueService.test.ts for the same convention. import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { CastTokenService } from '../src/services/CastTokenService'; import type { EmbySession } from '../src/types'; function fakeSession(userId: string): EmbySession { return { embyToken: `tok-${userId}`, userId, userName: userId, serverId: 'server1', createdUtc: Date.now() }; } describe('CastTokenService', () => { it('issue() then sessionFromToken() resolves back to the same session, for the issuing party only', () => { const session = fakeSession('u-alice'); const token = CastTokenService.issue('party1', session); assert.deepEqual(CastTokenService.sessionFromToken(token, 'party1'), session); assert.equal(CastTokenService.sessionFromToken(token, 'party2'), null, 'a token minted for one party must not resolve against another'); }); it('an unknown or empty token resolves to null', () => { assert.equal(CastTokenService.sessionFromToken('does-not-exist', 'party1'), null); assert.equal(CastTokenService.sessionFromToken('', 'party1'), null); }); it('issuing a second token for the same {partyId, userId} invalidates the first', () => { const session = fakeSession('u-bob'); const first = CastTokenService.issue('party2', session); const second = CastTokenService.issue('party2', session); assert.notEqual(first, second); assert.equal(CastTokenService.sessionFromToken(first, 'party2'), null, 'the old token must stop working once a new one is issued'); assert.deepEqual(CastTokenService.sessionFromToken(second, 'party2'), session); }); it('the same user casting two different parties gets independent tokens', () => { const session = fakeSession('u-carol'); const tokenA = CastTokenService.issue('partyA', session); const tokenB = CastTokenService.issue('partyB', session); assert.deepEqual(CastTokenService.sessionFromToken(tokenA, 'partyA'), session); assert.deepEqual(CastTokenService.sessionFromToken(tokenB, 'partyB'), session); }); it('revokeForMember() drops the token immediately', () => { const session = fakeSession('u-dave'); const token = CastTokenService.issue('party3', session); CastTokenService.revokeForMember('party3', 'u-dave'); assert.equal(CastTokenService.sessionFromToken(token, 'party3'), null); }); it('revokeForMember() only drops the named member - others casting the same party are untouched', () => { const alice = fakeSession('u-eve'); const bob = fakeSession('u-frank'); const tokenAlice = CastTokenService.issue('party4', alice); const tokenBob = CastTokenService.issue('party4', bob); CastTokenService.revokeForMember('party4', 'u-eve'); assert.equal(CastTokenService.sessionFromToken(tokenAlice, 'party4'), null); assert.deepEqual(CastTokenService.sessionFromToken(tokenBob, 'party4'), bob); }); it('revokeForMember() is a harmless no-op when nothing was ever cast', () => { assert.doesNotThrow(() => CastTokenService.revokeForMember('party-nope', 'u-nobody')); }); });