import test from 'node:test' import assert from 'node:assert/strict' import { mkdtempSync } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' import { buildApp } from '../src/app.js' import { settingsStore, effectiveConfig, suggestConfig } from '../src/settings.js' import { makeTesters } from '../src/testers.js' function tmpDataDir() { return mkdtempSync(path.join(tmpdir(), 'lyssnarr-test-')) } const baseConfig = { appPassword: 'testlösen', sessionSecret: 'x'.repeat(32), dataDir: '/tmp', lidarr: { url: 'http://stub', apiKey: 'k', qualityProfileId: 3, metadataProfileId: 1, rootFolder: '/music' }, jellyfin: { url: 'http://jellyfin:8096', apiKey: '' }, gemini: { apiKey: '', model: 'gemini-2.5-flash' }, youtubarr: { url: 'http://youtubarr', dbPath: '/tmp/finns-inte.sqlite3' }, } async function loggedIn(app) { const login = await app.inject({ method: 'POST', url: '/api/login', payload: { password: 'testlösen' } }) return login.headers['set-cookie'] } test('settingsStore sparar, rensar och behåller fält', () => { const store = settingsStore(tmpDataDir()) store.save({ gemini: { apiKey: 'hemlig', model: 'gemini-2.5-flash' } }) assert.equal(store.load().gemini.apiKey, 'hemlig') store.save({ gemini: { apiKey: undefined, model: 'gemini-2.5-pro' } }) assert.equal(store.load().gemini.apiKey, 'hemlig', 'undefined ska behålla') assert.equal(store.load().gemini.model, 'gemini-2.5-pro') store.save({ gemini: { apiKey: '' } }) assert.equal(store.load().gemini.apiKey, undefined, 'tom sträng ska rensa') }) test('effectiveConfig: sparat har företräde, env är fallback', () => { const eff = effectiveConfig(baseConfig, { jellyfin: { apiKey: 'ui-nyckel' } }) assert.equal(eff.jellyfin.apiKey, 'ui-nyckel') assert.equal(eff.jellyfin.url, 'http://jellyfin:8096') assert.equal(eff.gemini.model, 'gemini-2.5-flash') }) test('suggestConfig: defaults, migrering av gamla nycklar och clamping', () => { const def = suggestConfig({}) assert.equal(def.mix.every, 24) assert.equal(def.tracks.unit, 'days') assert.equal(def.genres.unit, 'months') const legacy = suggestConfig({ suggest: { intervalHours: 12, trackIntervalDays: 3 } }) assert.equal(legacy.mix.every, 12) assert.equal(legacy.mix.unit, 'hours') assert.equal(legacy.tracks.every, 3) const clamped = suggestConfig({ suggest: { mix: { count: 99, unit: 'veckor' } } }) assert.equal(clamped.mix.count, 25) assert.equal(clamped.mix.unit, 'hours') }) test('PUT + GET /api/settings maskerar hemligheter', async () => { const store = settingsStore(tmpDataDir()) const app = buildApp(baseConfig, { lidarr: {}, musicbrainz: {}, store }) const cookie = await loggedIn(app) const put = await app.inject({ method: 'PUT', url: '/api/settings', headers: { cookie }, payload: { gemini: { apiKey: 'super-hemlig' }, jellyfin: { userId: 'u1', userName: 'brasse' } }, }) assert.equal(put.statusCode, 200) const get = await app.inject({ method: 'GET', url: '/api/settings', headers: { cookie } }) const body = get.json() assert.equal(body.gemini.apiKeySet, true) assert.ok(!JSON.stringify(body).includes('super-hemlig'), 'nyckeln får inte läcka ut') assert.equal(body.jellyfin.userName, 'brasse') await app.close() }) test('jellyfin-testern returnerar användarlista', async () => { const store = settingsStore(tmpDataDir()) store.save({ jellyfin: { apiKey: 'jf-nyckel' } }) const fetchImpl = async (url) => { if (url.endsWith('/System/Info')) { return { ok: true, json: async () => ({ ServerName: 'brasse-pi5', Version: '10.10.0' }) } } if (url.endsWith('/Users')) { return { ok: true, json: async () => [{ Id: 'u1', Name: 'brasse' }] } } throw new Error('oväntad url ' + url) } const testers = makeTesters({ config: baseConfig, store, fetchImpl }) const out = await testers.jellyfin() assert.equal(out.ok, true) assert.match(out.message, /brasse-pi5/) assert.deepEqual(out.users, [{ id: 'u1', name: 'brasse' }]) }) test('gemini-testern skiljer på saknad nyckel och fel modell', async () => { const store = settingsStore(tmpDataDir()) const testers = makeTesters({ config: baseConfig, store, fetchImpl: async () => ({ ok: true }) }) assert.equal((await testers.gemini()).ok, false) store.save({ gemini: { apiKey: 'nyckel' } }) const testers404 = makeTesters({ config: baseConfig, store, fetchImpl: async () => ({ ok: false, status: 404 }) }) const out = await testers404.gemini() assert.equal(out.ok, false) assert.match(out.message, /modellen/) const testersOk = makeTesters({ config: baseConfig, store, fetchImpl: async () => ({ ok: true, status: 200 }) }) assert.equal((await testersOk.gemini()).ok, true) }) test('POST /api/settings/test avvisar okänd tjänst', async () => { const app = buildApp(baseConfig, { lidarr: {}, musicbrainz: {}, store: settingsStore(tmpDataDir()) }) const cookie = await loggedIn(app) const res = await app.inject({ method: 'POST', url: '/api/settings/test', headers: { cookie }, payload: { service: 'skum-tjänst' }, }) assert.equal(res.statusCode, 400) await app.close() })