// What a consumer of mqtt-topic is granted (topics.ts): its own subtree unless it contributed // `topics`; a contributed list is granted exactly, refused whole when it is not topic filters; and // the role is brought to exactly the wanted ACLs — missing ones added, stale ones removed — read from // `mosquitto_ctrl dynsec getRole` as eclipse-mosquitto 2.1.2 prints it. import { test } from "node:test"; import assert from "node:assert/strict"; import { filterProblem, missingAcls, parseRoleAcls, staleAcls, topicFilters, wantedAcls } from "../topics.ts"; test("a consumer that contributed nothing gets its own subtree", () => { assert.deepEqual(topicFilters({}, "mesh_ace_hass"), { ok: true, filters: ["mesh_ace_hass/#"], own: true }); assert.deepEqual(topicFilters(undefined, "x"), { ok: true, filters: ["x/#"], own: true }); // Settings merge into every contribution: keys that are not `topics` change nothing. assert.deepEqual(topicFilters({ endpoints: { web: {} } }, "x"), { ok: true, filters: ["x/#"], own: true }); }); test("a contributed list is granted exactly, duplicates once", () => { assert.deepEqual(topicFilters({ topics: ["#"] }, "x"), { ok: true, filters: ["#"], own: false }); assert.deepEqual(topicFilters({ topics: ["stat/+/POWER", "tele/#", "tele/#", "/octoprint/x"] }, "x"), { ok: true, filters: ["stat/+/POWER", "tele/#", "/octoprint/x"], own: false, }); }); test("a list that is not topic filters is refused whole", () => { for (const topics of [[], "#", [""], ["a/#/b"], ["a#"], ["a/b+"], [42], ["a\u0000b"], {}]) { const out = topicFilters({ topics } as Record, "x"); assert.equal(out.ok, false, JSON.stringify(topics)); } assert.equal(filterProblem("+/+/#"), undefined); assert.equal(filterProblem("#"), undefined); }); const GET_ROLE = `Warning: You are running mosquitto_ctrl without encryption. This means all of the configuration changes you are making are visible on the network, including passwords. Rolename: u1 ACLs: publishClientSend : allow : # (priority: 0) subscribePattern : allow : u1/# (priority: 0) publishClientReceive : deny : secret topic/with space (priority: -1) `; test("getRole's ACL lines are read, the warning and headings are not", () => { assert.deepEqual(parseRoleAcls(GET_ROLE), [ { type: "publishClientSend", allow: true, topic: "#" }, { type: "subscribePattern", allow: true, topic: "u1/#" }, { type: "publishClientReceive", allow: false, topic: "secret topic/with space" }, ]); assert.deepEqual(parseRoleAcls("Rolename: empty\nACLs:\n"), []); }); test("the role is brought to exactly the wanted ACLs", () => { const current = parseRoleAcls(GET_ROLE); const wanted = wantedAcls(["u1/#"]); assert.deepEqual(wanted, [ { type: "publishClientSend", allow: true, topic: "u1/#" }, { type: "publishClientReceive", allow: true, topic: "u1/#" }, { type: "subscribePattern", allow: true, topic: "u1/#" }, ]); assert.deepEqual(missingAcls(current, wanted), [ { type: "publishClientSend", allow: true, topic: "u1/#" }, { type: "publishClientReceive", allow: true, topic: "u1/#" }, ]); assert.deepEqual(staleAcls(current, wanted), [ { type: "publishClientSend", allow: true, topic: "#" }, { type: "publishClientReceive", allow: false, topic: "secret topic/with space" }, ]); assert.deepEqual(staleAcls(wanted, wanted), []); assert.deepEqual(missingAcls(wanted, wanted), []); });