import { TwitchAuthService, TwitchJwtPayload } from './twitch-auth.service'; function makeJwt(payload: Partial = {}): string { const full: TwitchJwtPayload = { exp: Math.floor(Date.now() / 1000) + 3600, opaque_user_id: 'U12345678', user_id: '12345678', channel_id: '987654321', role: 'viewer', ...payload, }; const encoded = btoa(JSON.stringify(full)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); return `eyJhbGciOiJIUzI1NiJ9.${encoded}.sig`; } function makeTwitchAuth(payload: Partial = {}): TwitchAuth { return { channelId: payload.channel_id ?? '987654321', clientId: 'test_client', token: makeJwt(payload), userId: payload.opaque_user_id ?? 'U12345678', }; } describe('TwitchAuthService', () => { let service: TwitchAuthService; let extCallbacks: { onAuthorized?: (auth: TwitchAuth) => void; onContext?: (ctx: TwitchContext, changed: (keyof TwitchContext)[]) => void; onVisibilityChanged?: (visible: boolean, ctx: TwitchContext) => void; }; beforeEach(() => { extCallbacks = {}; (window as Window & { Twitch?: unknown }).Twitch = { ext: { onAuthorized: (cb: (auth: TwitchAuth) => void) => { extCallbacks.onAuthorized = cb; }, onContext: (cb: (ctx: TwitchContext, changed: (keyof TwitchContext)[]) => void) => { extCallbacks.onContext = cb; }, onVisibilityChanged: (cb: (visible: boolean, ctx: TwitchContext) => void) => { extCallbacks.onVisibilityChanged = cb; }, listen: () => undefined, unlisten: () => undefined, send: () => undefined, }, }; service = new TwitchAuthService(); service.init(); }); afterEach(() => { delete (window as Window & { Twitch?: unknown }).Twitch; }); it('exposes null auth before onAuthorized fires', () => { expect(service.auth()).toBeNull(); expect(service.jwtPayload()).toBeNull(); expect(service.isLoggedIn()).toBe(false); expect(service.channelId()).toBeNull(); }); it('sets auth and decoded payload when onAuthorized fires', () => { extCallbacks.onAuthorized!(makeTwitchAuth()); expect(service.auth()).not.toBeNull(); expect(service.jwtPayload()?.opaque_user_id).toBe('U12345678'); expect(service.jwtPayload()?.channel_id).toBe('987654321'); expect(service.channelId()).toBe('987654321'); }); it('reports isLoggedIn true for U-prefixed opaque_user_id', () => { extCallbacks.onAuthorized!(makeTwitchAuth({ opaque_user_id: 'U99999999' })); expect(service.isLoggedIn()).toBe(true); }); it('reports isLoggedIn false for A-prefixed opaque_user_id', () => { extCallbacks.onAuthorized!(makeTwitchAuth({ opaque_user_id: 'Aanonymous' })); expect(service.isLoggedIn()).toBe(false); }); it('defaults isVisible to true', () => { expect(service.isVisible()).toBe(true); }); it('updates isVisible when onVisibilityChanged fires', () => { const ctx = {} as TwitchContext; extCallbacks.onVisibilityChanged!(false, ctx); expect(service.isVisible()).toBe(false); extCallbacks.onVisibilityChanged!(true, ctx); expect(service.isVisible()).toBe(true); }); it('updates context when onContext fires', () => { const ctx = { game: 'test-game' } as TwitchContext; extCallbacks.onContext!(ctx, ['game']); expect(service.context()?.game).toBe('test-game'); }); it('updates context from onVisibilityChanged', () => { const ctx = { isFullScreen: true } as TwitchContext; extCallbacks.onVisibilityChanged!(false, ctx); expect(service.context()?.isFullScreen).toBe(true); }); });