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 } from '../src/settings.js' import { ntfySettings, ntfyNotifier } from '../src/ntfy.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' }, ntfy: { url: 'https://ntfy.env-fallback', topic: '' }, } async function loggedIn(app) { const login = await app.inject({ method: 'POST', url: '/api/login', payload: { password: 'testlösen' } }) return login.headers['set-cookie'] } test('ntfySettings: sparat har företräde, env är fallback, enabled kräver explicit true', () => { const def = ntfySettings(baseConfig, {}) assert.equal(def.enabled, false) assert.equal(def.url, 'https://ntfy.env-fallback') const s = ntfySettings(baseConfig, { ntfy: { url: 'https://ntfy.ui/', topic: 'media-hamtningar', enabled: true } }) assert.equal(s.enabled, true) assert.equal(s.url, 'https://ntfy.ui', 'avslutande / ska trimmas') assert.equal(s.topic, 'media-hamtningar') const utanConfig = ntfySettings({}, {}) assert.equal(utanConfig.url, '', 'config utan ntfy-sektion får inte krascha') }) test('publish skickar bara när påslagen och komplett', async () => { const store = settingsStore(tmpDataDir()) const sent = [] const fetchImpl = async (url, opts) => { sent.push({ url, body: JSON.parse(opts.body) }) return { ok: true } } const notifier = ntfyNotifier({ config: baseConfig, store, fetchImpl }) assert.equal(await notifier.publish('t', 'm'), false, 'avstängd => inget skickas') assert.equal(sent.length, 0) store.save({ ntfy: { url: 'https://ntfy.test', topic: 'media-hamtningar', enabled: true } }) assert.equal(await notifier.publish('Tillagt', 'Powerwolf – Interludium'), true) assert.equal(sent.length, 1) assert.deepEqual(sent[0].body, { topic: 'media-hamtningar', title: 'Tillagt', message: 'Powerwolf – Interludium', tags: ['musical_note'], }) }) test('POST /api/add notifierar vid nytt tillägg men inte vid dubblett', async () => { const hit = { album: { foreignAlbumId: 'mbid-album-1', title: 'Interludium', artist: { artistName: 'Powerwolf', foreignArtistId: 'mbid-artist-1' }, }, } const lidarr = { search: async () => [hit], addAlbum: async () => ({}), } const published = [] const ntfy = { settings: () => ({ enabled: true, url: 'x', topic: 'y' }), publish: async (title, message) => { published.push({ title, message }) return true }, } const app = buildApp(baseConfig, { lidarr, musicbrainz: {}, store: settingsStore(tmpDataDir()), ntfy }) const cookie = await loggedIn(app) const res = await app.inject({ method: 'POST', url: '/api/add', headers: { cookie }, payload: { kind: 'album', mbid: 'mbid-album-1' }, }) assert.equal(res.statusCode, 200) assert.deepEqual(res.json(), { ok: true, already: false }) // fire-and-forget: ge event-loopen ett varv await new Promise((r) => setImmediate(r)) assert.equal(published.length, 1) assert.equal(published[0].title, 'Tillagt för nedladdning') assert.equal(published[0].message, 'Powerwolf – Interludium') hit.album.id = 42 // finns redan i Lidarr const dup = await app.inject({ method: 'POST', url: '/api/add', headers: { cookie }, payload: { kind: 'album', mbid: 'mbid-album-1' }, }) assert.deepEqual(dup.json(), { ok: true, already: true }) await new Promise((r) => setImmediate(r)) assert.equal(published.length, 1, 'dubblett ska inte notifiera') await app.close() }) test('misslyckad notis fäller inte add-svaret', async () => { const hit = { artist: { foreignArtistId: 'mbid-artist-1', artistName: 'Powerwolf' }, } const lidarr = { search: async () => [hit], addArtist: async () => ({}) } const ntfy = { settings: () => ({ enabled: true, url: 'x', topic: 'y' }), publish: async () => { throw new Error('ntfy nere') }, } const app = buildApp(baseConfig, { lidarr, musicbrainz: {}, store: settingsStore(tmpDataDir()), ntfy }) const cookie = await loggedIn(app) const res = await app.inject({ method: 'POST', url: '/api/add', headers: { cookie }, payload: { kind: 'artist', mbid: 'mbid-artist-1' }, }) assert.equal(res.statusCode, 200) assert.deepEqual(res.json(), { ok: true, already: false }) await app.close() }) test('PUT + GET /api/settings hanterar ntfy-sektionen, enabled=false består', async () => { const store = settingsStore(tmpDataDir()) const app = buildApp(baseConfig, { lidarr: {}, musicbrainz: {}, store }) const cookie = await loggedIn(app) await app.inject({ method: 'PUT', url: '/api/settings', headers: { cookie }, payload: { ntfy: { url: 'https://ntfy.test', topic: 'media-hamtningar', enabled: true } }, }) let body = (await app.inject({ method: 'GET', url: '/api/settings', headers: { cookie } })).json() assert.deepEqual(body.ntfy, { enabled: true, url: 'https://ntfy.test', topic: 'media-hamtningar' }) await app.inject({ method: 'PUT', url: '/api/settings', headers: { cookie }, payload: { ntfy: { enabled: false } }, }) body = (await app.inject({ method: 'GET', url: '/api/settings', headers: { cookie } })).json() assert.equal(body.ntfy.enabled, false, 'false ska sparas, inte ignoreras') assert.equal(body.ntfy.topic, 'media-hamtningar', 'övriga fält ska behållas') await app.close() }) test('ntfy-testern skickar testnotis och skiljer på saknad konfig', async () => { const store = settingsStore(tmpDataDir()) const sent = [] const fetchImpl = async (url, opts) => { sent.push(JSON.parse(opts.body)) return { ok: true } } const testers = makeTesters({ config: { ...baseConfig, ntfy: { url: '', topic: '' } }, store, fetchImpl }) assert.equal((await testers.ntfy()).ok, false, 'utan konfig => fel') store.save({ ntfy: { url: 'https://ntfy.test', topic: 'media-hamtningar' } }) const out = await testers.ntfy() assert.equal(out.ok, true) assert.match(out.message, /avstängda/, 'ska påminna om att notiser inte är påslagna') assert.equal(sent.length, 1) assert.equal(sent[0].topic, 'media-hamtningar') })