Files
sofarr/tests/integration/emby.test.js
T
gronod 7d3e6e6a47
Build and Push Docker Image / build (push) Successful in 39s
Docs Check / Markdown lint (push) Successful in 45s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m12s
CI / Security audit (push) Successful in 1m30s
CI / Tests & coverage (push) Failing after 1m39s
Docs Check / Mermaid diagram parse check (push) Successful in 1m59s
test: add integration and unit tests for dashboard, emby, sonarr, radarr, sabnzbd routes
- tests/unit/dashboard.test.js: 58 unit tests covering all 12 pure helper
  functions in dashboard.js (sanitizeTagLabel, tagMatchesUser, getCoverArt,
  extractAllTags, extractUserTag, getImportIssues, getSonarrLink, getRadarrLink,
  canBlocklist, extractEpisode, gatherEpisodes, buildTagBadges)

- tests/integration/dashboard.test.js: 35 integration tests for
  /user-downloads (SAB+Sonarr, SAB+Radarr, qBit, showAll, paused queue,
  history matching, importIssues, wrong-user filtering), /status (admin guard,
  webhook check, failure handling), /webhook-metrics, /cover-art (all
  validation/proxy paths), /blocklist-search (guards, Sonarr, Radarr, failure)

- tests/integration/emby.test.js: 13 integration tests covering all 4 Emby
  routes (sessions, users, users/:id, session/:id/user) with auth guard,
  happy path, and upstream failure cases

- tests/integration/arrRoutes.test.js: 64 integration tests for Sonarr +
  Radarr (queue, history, series/movies, notifications CRUD, /test, /schema,
  /sofarr-webhook create+update+missing-config+failure) and SABnzbd (queue,
  history with custom params)

- vitest.config.js: raise global coverage thresholds (statements/functions/
  lines 20->55, branches 8->40) to reflect improved coverage
  (62.5% stmts, 42.6% branches, 64.1% funcs, 65.6% lines)

- tests/README.md: document new test files and update coverage table
2026-05-20 21:37:57 +01:00

261 lines
7.9 KiB
JavaScript

// 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);
});
});