All checks were successful
build-and-push / build (push) Successful in 14s
Rättelse per Björn: tredje förslags-typen ska vara genrer, inte sångare. Gemini föreslår genrer utifrån artisterna i gillat/spelat; varje genre får 5 toppartister (filtrerade mot biblioteket, Hämta-knapp i modalen) och 5 topplåtar (filtrerade mot gillalistan, YT-länkar). Genrekortet lånar första toppartistens omslag. Gamla sångar-batcher renderas fortfarande. DetailModal generaliserad. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01824ZrvG2mDYYrmwqypLNup
128 lines
5.1 KiB
JavaScript
128 lines
5.1 KiB
JavaScript
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()
|
|
})
|