// What holds ombi's Servarr step (servarr/settings.ts): the connection ombi keeps for each app is // made to say what the mesh bound — host, port, TLS, base path, key — and nothing else it keeps is // touched; nothing is written when nothing differs; Radarr's 4K instance is left alone; and a key the // app refuses (the mesh's own minted value, before the operator accepts the app's key) is never // written, with the `secret accept` that fixes it named. // // ombi and the apps are fakes: the routes the step touches, answering as the real ones do (checked // against lscr.io/linuxserver/ombi 4.53.10 and the catalogue's pinned sonarr/radarr/lidarr). import { test } from "node:test"; import assert from "node:assert/strict"; import { APPS, differing, reconcileApp, subDirOf, wanted, type Binding, type Http, type ServarrApp } from "../servarr/settings.ts"; const SONARR = APPS.find((a) => a.app === "sonarr") as ServarrApp; const RADARR = APPS.find((a) => a.app === "radarr") as ServarrApp; const LIDARR = APPS.find((a) => a.app === "lidarr") as ServarrApp; const THE_KEY = "the-apps-own-key"; function binding(provision: string, port: number, at = "ace.internal"): Binding { return { binding: 1, provision, from: "ace", at, as: "mesh_ace_ombi", serves: { scheme: "http", port, "url-base": "" } } as Binding; } interface Call { method: string; url: string; body?: unknown; } /** ombi's settings store and the apps' key check, behind one fetch. */ function fakes(settings: Record, opts: { appKey?: string; reachable?: boolean } = {}) { const calls: Call[] = []; const appKey = opts.appKey ?? THE_KEY; const http: Http = { async fetch(url, init) { const method = init?.method ?? "GET"; const body = init?.body ? (JSON.parse(init.body) as unknown) : undefined; calls.push({ method, url, body }); const reply = (status: number, value?: unknown) => ({ status, text: async () => (value === undefined ? "" : JSON.stringify(value)), }); const u = new URL(url); if (u.pathname.endsWith("/system/status")) { if (opts.reachable === false) throw new Error("connect ECONNREFUSED"); return init?.headers?.["X-Api-Key"] === appKey ? reply(200, { version: "4" }) : reply(401); } if (init?.headers?.ApiKey !== "ombi-key") return reply(401); const m = u.pathname.match(/^\/api\/v1\/(Settings|Tester)\/(\w+)$/); if (!m) return reply(404); const [, kind, app] = m; if (kind === "Settings" && method === "GET") return reply(200, settings[app]); if (kind === "Settings" && method === "POST") { settings[app] = body; return reply(200, true); } const tried = body as { apiKey?: string }; return reply(200, { isValid: tried.apiKey === appKey, expectedSubDir: null }); }, }; return { http, calls, settings }; } const OMBI = { url: "http://127.0.0.1:3579", apiKey: "ombi-key" }; const operatorSonarr = () => ({ enabled: true, apiKey: "old-key", qualityProfile: "3", seasonFolders: true, rootPath: "10", qualityProfileAnime: "7", rootPathAnime: "9", languageProfile: 1, ssl: false, subDir: null, ip: "sonarr", port: 8989, id: 5, }); test("it writes the connection the mesh bound, and keeps every other setting ombi had", async () => { const f = fakes({ sonarr: operatorSonarr() }); const out = await reconcileApp(f.http, OMBI, SONARR, binding("sonarr-api", 20101), `${THE_KEY}\n`); assert.deepEqual(out, { app: "sonarr", result: "written", fields: ["ip", "port", "apiKey"] }); assert.deepEqual(f.settings.sonarr, { ...operatorSonarr(), ip: "ace.internal", port: 20101, apiKey: THE_KEY, ssl: false, subDir: null, }); // Checked against the app itself, at the bound address, before anything was written. assert.equal(f.calls[0].url, "http://ace.internal:20101/api/v3/system/status"); }); test("nothing is written when ombi already says what the mesh says", async () => { const f = fakes({ sonarr: { ...operatorSonarr(), ip: "ace.internal", port: 20101, apiKey: THE_KEY } }); const out = await reconcileApp(f.http, OMBI, SONARR, binding("sonarr-api", 20101), THE_KEY); assert.deepEqual(out, { app: "sonarr", result: "unchanged" }); assert.equal(f.calls.filter((c) => c.method === "POST" && c.url.includes("/Settings/")).length, 0); }); test("a key the app refuses is never written, and the accept that fixes it is named", async () => { const f = fakes({ sonarr: operatorSonarr() }); const out = await reconcileApp(f.http, OMBI, SONARR, binding("sonarr-api", 20101), "a-value-the-mesh-minted"); assert.equal(out.result, "refused"); assert.match((out as { problem: string }).problem, /secret accept ombi sonarr-api --provider ace/); assert.doesNotMatch((out as { problem: string }).problem, /a-value-the-mesh-minted/); assert.deepEqual(f.settings.sonarr, operatorSonarr(), "ombi's working settings were left alone"); assert.equal(f.calls.some((c) => c.url.includes("/api/v1/")), false, "ombi was not even asked"); }); test("an app it cannot reach is reported, and ombi is left alone", async () => { const f = fakes({ sonarr: operatorSonarr() }, { reachable: false }); const out = await reconcileApp(f.http, OMBI, SONARR, binding("sonarr-api", 20101), THE_KEY); assert.equal(out.result, "refused"); assert.match((out as { problem: string }).problem, /could not be asked.*ECONNREFUSED/); assert.deepEqual(f.settings.sonarr, operatorSonarr()); }); test("radarr's connection is written inside its combined document, and the 4K instance is untouched", async () => { const fourK = { enabled: true, apiKey: "4k-key", ip: "radarr4k", port: 7879, defaultQualityProfile: "9", id: 7 }; const f = fakes({ radarr: { radarr: { enabled: true, apiKey: "old", ip: "radarr", port: 7878, defaultRootPath: "/movies", id: 6 }, radarr4K: fourK } }); const out = await reconcileApp(f.http, OMBI, RADARR, binding("radarr-api", 20102), THE_KEY); assert.equal(out.result, "written"); const doc = f.settings.radarr as { radarr: Record; radarr4K: unknown }; assert.deepEqual(doc.radarr4K, fourK); assert.equal(doc.radarr.ip, "ace.internal"); assert.equal(doc.radarr.port, 20102); assert.equal(doc.radarr.defaultRootPath, "/movies"); }); test("lidarr is checked on its own API version", async () => { const f = fakes({ lidarr: { enabled: true, apiKey: null, ip: null, port: 0, id: 0 } }); const out = await reconcileApp(f.http, OMBI, LIDARR, binding("lidarr-api", 20103), THE_KEY); assert.equal(out.result, "written"); assert.equal(f.calls[0].url, "http://ace.internal:20103/api/v1/system/status"); }); test("a loopback binding is refused: from ombi's container it is ombi itself", () => { const w = wanted(SONARR, binding("sonarr-api", 20101, "127.0.0.1"), THE_KEY); assert.equal(w.ok, false); assert.match((w as { problem: string }).problem, /private network/); }); test("the base path is ombi's subDir, slashes trimmed; empty is none", () => { assert.equal(subDirOf(""), null); assert.equal(subDirOf("/sonarr/"), "sonarr"); assert.deepEqual( differing({ ip: "h", port: 1, ssl: false, subDir: "", apiKey: "k" }, { ip: "h", port: 1, ssl: false, subDir: null, apiKey: "k" }), [], ); }); test("an https binding sets ombi's ssl flag", () => { const b = binding("sonarr-api", 443); (b.serves as Record).scheme = "https"; const w = wanted(SONARR, b, THE_KEY); assert.equal(w.ok && w.connection.ssl, true); });