// Copyright (c) 2026 Gordon Bolton. MIT License. /** * Integration tests for server/routes/emby.js * * All four endpoints are covered: * GET /api/emby/sessions * GET /api/emby/users * GET /api/emby/users/:id * GET /api/emby/session/:sessionId/user * * For each: auth guard (401), happy path, and upstream failure (500). * No CSRF token is needed — all routes are read-only GETs. */ import request from 'supertest'; import nock from 'nock'; import { createApp } from '../../server/app.js'; const EMBY_BASE = 'https://emby.test'; // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- const EMBY_AUTH = { AccessToken: 'tok', User: { Id: 'uid1', Name: 'alice' } }; const EMBY_USER = { Id: 'uid1', Name: 'alice', Policy: { IsAdministrator: false } }; const EMBY_SESSIONS = [ { Id: 'sess-001', UserId: 'uid1', UserName: 'alice', Client: 'Emby Web', DeviceName: 'Chrome' }, { Id: 'sess-002', UserId: 'uid2', UserName: 'bob', Client: 'Emby iOS', DeviceName: 'iPhone' } ]; const EMBY_USERS_LIST = [ { Id: 'uid1', Name: 'alice', Policy: { IsAdministrator: false } }, { Id: 'uid2', Name: 'bob', Policy: { IsAdministrator: false } } ]; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function interceptLogin() { nock(EMBY_BASE).post('/Users/authenticatebyname').reply(200, EMBY_AUTH); nock(EMBY_BASE).get(/\/Users\//).reply(200, EMBY_USER); } async function loginAs(app) { interceptLogin(); const res = await request(app) .post('/api/auth/login') .send({ username: 'alice', password: 'pw' }); return res.headers['set-cookie']; } // --------------------------------------------------------------------------- // Environment // --------------------------------------------------------------------------- beforeAll(() => { process.env.EMBY_URL = EMBY_BASE; process.env.EMBY_API_KEY = 'emby-api-key'; }); afterAll(() => { delete process.env.EMBY_URL; delete process.env.EMBY_API_KEY; }); afterEach(() => { nock.cleanAll(); }); // --------------------------------------------------------------------------- // GET /api/emby/sessions // --------------------------------------------------------------------------- describe('GET /api/emby/sessions', () => { it('returns 401 when not authenticated', async () => { const app = createApp({ skipRateLimits: true }); const res = await request(app).get('/api/emby/sessions'); expect(res.status).toBe(401); }); it('proxies Emby sessions list', async () => { const app = createApp({ skipRateLimits: true }); const cookies = await loginAs(app); nock(EMBY_BASE) .get('/Sessions') .reply(200, EMBY_SESSIONS); const res = await request(app) .get('/api/emby/sessions') .set('Cookie', cookies); expect(res.status).toBe(200); expect(Array.isArray(res.body)).toBe(true); expect(res.body.length).toBe(2); expect(res.body[0].Id).toBe('sess-001'); }); it('returns 500 when Emby is unreachable', async () => { const app = createApp({ skipRateLimits: true }); const cookies = await loginAs(app); nock(EMBY_BASE) .get('/Sessions') .replyWithError('ECONNREFUSED'); const res = await request(app) .get('/api/emby/sessions') .set('Cookie', cookies); expect(res.status).toBe(500); expect(res.body.error).toMatch(/sessions/i); }); }); // --------------------------------------------------------------------------- // GET /api/emby/users // --------------------------------------------------------------------------- describe('GET /api/emby/users', () => { it('returns 401 when not authenticated', async () => { const app = createApp({ skipRateLimits: true }); const res = await request(app).get('/api/emby/users'); expect(res.status).toBe(401); }); it('proxies Emby users list', async () => { const app = createApp({ skipRateLimits: true }); const cookies = await loginAs(app); nock(EMBY_BASE) .get('/Users') .reply(200, EMBY_USERS_LIST); const res = await request(app) .get('/api/emby/users') .set('Cookie', cookies); expect(res.status).toBe(200); expect(Array.isArray(res.body)).toBe(true); expect(res.body[0].Name).toBe('alice'); }); it('returns 500 when Emby is unreachable', async () => { const app = createApp({ skipRateLimits: true }); const cookies = await loginAs(app); nock(EMBY_BASE) .get('/Users') .replyWithError('ECONNREFUSED'); const res = await request(app) .get('/api/emby/users') .set('Cookie', cookies); expect(res.status).toBe(500); expect(res.body.error).toMatch(/users/i); }); }); // --------------------------------------------------------------------------- // GET /api/emby/users/:id // --------------------------------------------------------------------------- describe('GET /api/emby/users/:id', () => { it('returns 401 when not authenticated', async () => { const app = createApp({ skipRateLimits: true }); const res = await request(app).get('/api/emby/users/uid1'); expect(res.status).toBe(401); }); it('proxies individual user details', async () => { const app = createApp({ skipRateLimits: true }); const cookies = await loginAs(app); nock(EMBY_BASE) .get('/Users/uid1') .reply(200, EMBY_USER); const res = await request(app) .get('/api/emby/users/uid1') .set('Cookie', cookies); expect(res.status).toBe(200); expect(res.body.Id).toBe('uid1'); expect(res.body.Name).toBe('alice'); }); it('returns 500 when Emby returns an error', async () => { const app = createApp({ skipRateLimits: true }); const cookies = await loginAs(app); nock(EMBY_BASE) .get('/Users/uid-unknown') .reply(404, { error: 'Not found' }); const res = await request(app) .get('/api/emby/users/uid-unknown') .set('Cookie', cookies); expect(res.status).toBe(500); }); }); // --------------------------------------------------------------------------- // GET /api/emby/session/:sessionId/user // --------------------------------------------------------------------------- describe('GET /api/emby/session/:sessionId/user', () => { it('returns 401 when not authenticated', async () => { const app = createApp({ skipRateLimits: true }); const res = await request(app).get('/api/emby/session/sess-001/user'); expect(res.status).toBe(401); }); it('returns the user associated with a session', async () => { const app = createApp({ skipRateLimits: true }); const cookies = await loginAs(app); nock(EMBY_BASE) .get('/Sessions') .reply(200, EMBY_SESSIONS); nock(EMBY_BASE) .get('/Users/uid1') .reply(200, EMBY_USER); const res = await request(app) .get('/api/emby/session/sess-001/user') .set('Cookie', cookies); expect(res.status).toBe(200); expect(res.body.Name).toBe('alice'); }); it('returns 404 when session ID is not found', async () => { const app = createApp({ skipRateLimits: true }); const cookies = await loginAs(app); nock(EMBY_BASE) .get('/Sessions') .reply(200, EMBY_SESSIONS); const res = await request(app) .get('/api/emby/session/sess-nonexistent/user') .set('Cookie', cookies); expect(res.status).toBe(404); expect(res.body.error).toMatch(/session not found/i); }); it('returns 500 when Emby sessions fetch fails', async () => { const app = createApp({ skipRateLimits: true }); const cookies = await loginAs(app); nock(EMBY_BASE) .get('/Sessions') .replyWithError('ECONNREFUSED'); const res = await request(app) .get('/api/emby/session/sess-001/user') .set('Cookie', cookies); expect(res.status).toBe(500); expect(res.body.error).toMatch(/session/i); }); });