// What holds ombi's Plex step (plex/settings.ts): the entry for the server plex says it is — found // by machineIdentifier — is made to say what the mesh bound (host, port, TLS, token) and nothing // else it keeps is touched; an entry for another server is left alone; an ombi with no entry for it // gets one; nothing is written when nothing differs; and a token plex refuses (the mesh's own minted // value, before the operator accepts the server's token) is never written, with the `secret accept` // that fixes it named. // // ombi and plex are fakes answering the routes the step touches as the real ones do (checked against // lscr.io/linuxserver/ombi 4.53.10 and plexinc/pms-docker 1.43.4: plex answers 401 to an unknown // token from another network and 400 on one it trusts; ombi's /Tester/plex answers a bare boolean). // // Imports the compiled step, as keycloak's tests do: plex/settings.ts imports its sibling with the // `.js` specifier the build needs, which Node's type stripping does not resolve to a `.ts` file. import { test } from "node:test"; import assert from "node:assert/strict"; import { differingPlex, reconcilePlex, wantedPlex, withPlexServer } from "../dist/plex/settings.js"; import type { Binding, Http } from "../servarr/settings.ts"; const TOKEN = "the-servers-own-token"; const MACHINE = "5c47d9a165d10b622995d55b3ae1f168242f33bd"; const OMBI = { url: "http://127.0.0.1:3579", apiKey: "ombi-key" }; function binding(port = 32400, at = "ace.internal", scheme = "http"): Binding { return { binding: 1, provision: "plex-api", from: "ace", at, as: "mesh_ace_ombi", serves: { scheme, port } } as Binding; } interface Call { method: string; url: string; body?: unknown; } function fakes(plexSettings: Record, opts: { reachable?: boolean; trusted?: boolean } = {}) { const calls: Call[] = []; const store = { plex: plexSettings }; 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); // Other servers an entry may name: a friend's, and plex's own public name (the same server). if (u.hostname === "10.0.0.9") return reply(200, { MediaContainer: { machineIdentifier: "another-server" } }); if (u.hostname === "gone.example") throw new Error("getaddrinfo ENOTFOUND"); if (u.hostname === "plex.zurag.be") { if (u.pathname === "/identity") return reply(200, { MediaContainer: { machineIdentifier: MACHINE } }); return reply(401); } if (u.port === "32400" || u.hostname === "ace.internal") { if (opts.reachable === false) throw new Error("connect ECONNREFUSED"); if (u.pathname === "/identity") return reply(200, { MediaContainer: { machineIdentifier: MACHINE } }); const token = init?.headers?.["X-Plex-Token"]; if (token !== TOKEN) return reply(opts.trusted ? 400 : 401); return reply(200, { MediaContainer: { friendlyName: "ace", machineIdentifier: MACHINE } }); } if (init?.headers?.ApiKey !== "ombi-key") return reply(401); if (u.pathname === "/api/v1/Settings/Plex" && method === "GET") return reply(200, store.plex); if (u.pathname === "/api/v1/Settings/Plex" && method === "POST") { store.plex = body as Record; return reply(200, true); } if (u.pathname === "/api/v1/Tester/plex") { const tried = body as { plexAuthToken?: string; ip?: string }; return reply(200, tried.plexAuthToken === TOKEN && tried.ip === "ace.internal"); } return reply(404); }, }; return { http, calls, store }; } // What an operator's ombi holds: plex loaded through its public name, plus a friend's server. const operatorPlex = () => ({ enable: true, enableWatchlistImport: true, monitorAll: false, installId: "b358a2a2-2ab0-4025-a3f3-450313c3c418", servers: [ { name: "ace", plexAuthToken: TOKEN, machineIdentifier: MACHINE, episodeBatchSize: 150, serverHostname: "https://app.plex.tv", plexSelectedLibraries: [{ key: "1", title: "Films", enabled: true }], ssl: true, subDir: null, ip: "plex.zurag.be", port: 443, id: 1, }, { name: "a friend", plexAuthToken: "their-token", machineIdentifier: "another-server", episodeBatchSize: 150, plexSelectedLibraries: [], ssl: false, subDir: null, ip: "10.0.0.9", port: 32400, id: 2, }, ], id: 4, }); test("the server's own entry gets the bound connection; its libraries and every other setting stay", async () => { const f = fakes(operatorPlex()); const out = await reconcilePlex(f.http, OMBI, binding(), `${TOKEN}\n`); assert.deepEqual(out, { app: "plex", result: "written", fields: ["ip", "port", "ssl"] }); const want = operatorPlex(); Object.assign(want.servers[0], { ip: "ace.internal", port: 32400, ssl: false }); assert.deepEqual(f.store.plex, want); }); test("another server's entry is never touched", async () => { const f = fakes(operatorPlex()); await reconcilePlex(f.http, OMBI, binding(), TOKEN); const servers = f.store.plex.servers as Record[]; assert.deepEqual(servers[1], operatorPlex().servers[1]); }); test("nothing is written when ombi already says what the mesh says", async () => { const doc = operatorPlex(); Object.assign(doc.servers[0], { ip: "ace.internal", port: 32400, ssl: false }); const f = fakes(doc); const out = await reconcilePlex(f.http, OMBI, binding(), TOKEN); assert.deepEqual(out, { app: "plex", result: "unchanged" }); assert.equal(f.calls.filter((c) => c.method === "POST" && c.url.includes("/Settings/")).length, 0); }); test("an ombi with no entry for this server gets one, named as plex names itself", async () => { const fresh = { enable: false, enableWatchlistImport: false, monitorAll: false, installId: "x", servers: null, id: 0 }; const f = fakes(fresh); const out = await reconcilePlex(f.http, OMBI, binding(), TOKEN); assert.deepEqual(out, { app: "plex", result: "written", fields: ["server"] }); assert.deepEqual(f.store.plex, { ...fresh, servers: [{ name: "ace", machineIdentifier: MACHINE, ip: "ace.internal", port: 32400, ssl: false, subDir: null, plexAuthToken: TOKEN, episodeBatchSize: 150, plexSelectedLibraries: [], }], }); assert.equal(f.store.plex.enable, false, "whether plex is enabled in ombi is the operator's choice"); }); test("a token plex refuses is never written, and the accept that fixes it is named", async () => { for (const trusted of [false, true]) { const f = fakes(operatorPlex(), { trusted }); const out = await reconcilePlex(f.http, OMBI, binding(), "a-value-the-mesh-minted"); assert.equal(out.result, "refused"); const problem = (out as { problem: string }).problem; assert.match(problem, /secret accept ombi plex-api --provider ace/); assert.doesNotMatch(problem, /a-value-the-mesh-minted/); assert.deepEqual(f.store.plex, operatorPlex(), "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("a plex it cannot reach is reported, and ombi is left alone", async () => { const f = fakes(operatorPlex(), { reachable: false }); const out = await reconcilePlex(f.http, OMBI, binding(), TOKEN); assert.equal(out.result, "refused"); assert.match((out as { problem: string }).problem, /could not be asked.*ECONNREFUSED/); assert.deepEqual(f.store.plex, operatorPlex()); }); test("a loopback binding is refused: from ombi's container it is ombi itself", () => { const w = wantedPlex(binding(32400, "127.0.0.1"), TOKEN); assert.equal(w.ok, false); assert.match((w as { problem: string }).problem, /private network/); }); test("an https binding sets ombi's ssl flag; an empty subDir is none", () => { const w = wantedPlex(binding(32400, "ace.internal", "https"), TOKEN); assert.equal(w.ok && w.connection.ssl, true); assert.deepEqual( differingPlex({ ip: "h", port: 1, ssl: false, subDir: "", plexAuthToken: "k" }, { ip: "h", port: 1, ssl: false, subDir: null, plexAuthToken: "k" }), [], ); }); test("every entry naming the server is laid over, not only the first", () => { const doc = { servers: [{ machineIdentifier: MACHINE, ip: "a" }, { machineIdentifier: MACHINE, ip: "b" }] }; const want = { ip: "ace.internal", port: 32400, ssl: false, subDir: null, plexAuthToken: TOKEN }; const laid = withPlexServer(doc, MACHINE, want, "ace"); assert.equal(laid.added, false); assert.deepEqual((laid.next.servers as { ip: string }[]).map((s) => s.ip), ["ace.internal", "ace.internal"]); }); // ace's own ombi: its one entry was loaded from an older server (a stale identifier) and retyped to // plex's public name, so it IS this server, reached another way (read from ace, 2026-09-30). const acesOmbi = () => ({ enable: true, enableWatchlistImport: true, servers: [{ name: "Nami", plexAuthToken: TOKEN, machineIdentifier: "76562198623e708eef85b46aedb72c8f2fe671aa", episodeBatchSize: 0, plexSelectedLibraries: [1, 2, 3, 4, 5, 6].map((k) => ({ key: String(k), enabled: true })), ssl: true, subDir: null, ip: "plex.zurag.be", port: 443, id: 1, }], id: 4, }); test("an entry whose own address answers as this server is adopted: connection and identifier, nothing else", async () => { const f = fakes(acesOmbi()); const out = await reconcilePlex(f.http, OMBI, binding(), TOKEN); assert.deepEqual(out, { app: "plex", result: "written", fields: ["ip", "port", "ssl", "machineIdentifier"] }); const want = acesOmbi(); Object.assign(want.servers[0], { ip: "ace.internal", port: 32400, ssl: false, machineIdentifier: MACHINE }); assert.deepEqual(f.store.plex, want, "one entry, still named Nami, its six libraries kept; none added"); }); test("an entry answering as another server, or not at all, is not adopted; this server gets its own", async () => { const doc = { servers: [ { name: "a friend", machineIdentifier: "stale-1", ip: "10.0.0.9", port: 32400, ssl: false, plexAuthToken: "theirs" }, { name: "gone", machineIdentifier: "stale-2", ip: "gone.example", port: 32400, ssl: false, plexAuthToken: "old" }, ], }; const f = fakes(structuredClone(doc)); const out = await reconcilePlex(f.http, OMBI, binding(), TOKEN); assert.deepEqual(out, { app: "plex", result: "written", fields: ["server"] }); const servers = f.store.plex.servers as Record[]; assert.deepEqual(servers.slice(0, 2), doc.servers, "both left exactly as they were"); assert.equal(servers[2].machineIdentifier, MACHINE); }); test("no entry is probed once one carries the server's identifier", async () => { const f = fakes(operatorPlex()); await reconcilePlex(f.http, OMBI, binding(), TOKEN); assert.equal(f.calls.some((c) => c.url.startsWith("http://10.0.0.9")), false, "the friend's server was not asked"); });