// Tests for the service_op consent chip's sanitize bounds (ui.js) — the client // whitelist gate every registry-op chip must pass. node test/serviceop.test.mjs import assert from "node:assert/strict"; import { sanitizeActions, CHIP_TYPES } from "../web/static/ui.js"; let failures = 0; async function test(name, fn) { try { await fn(); console.log(`PASS ${name}`); } catch (e) { failures++; console.log(`FAIL ${name} — ${e.message.split("\n")[0]}`); } } const valid = () => ({ type: "service_op", service: "dnsimple", op: "domain.check", access: "read", args: { account: "12345", domain: "blip.com" }, label: "Check blip.com", }); await test("service_op is a whitelisted chip type", () => { assert.ok(CHIP_TYPES.service_op); assert.equal(typeof CHIP_TYPES.service_op.sanitize, "function"); }); await test("a valid chip passes with service/op/access/args copied", () => { const [chip] = sanitizeActions([valid()]); assert.deepEqual(chip, { type: "service_op", service: "dnsimple", op: "domain.check", access: "read", args: { account: "12345", domain: "blip.com" }, label: "Check blip.com", }); }); await test("a write op passes and keeps access=write", () => { const a = { ...valid(), op: "domain.register", access: "write", args: { domain: "blip.com" } }; const [chip] = sanitizeActions([a]); assert.equal(chip.access, "write"); assert.equal(chip.op, "domain.register"); }); await test("missing args defaults to an empty object", () => { const a = valid(); delete a.args; const [chip] = sanitizeActions([a]); assert.deepEqual(chip.args, {}); }); await test("bad service slugs are rejected", () => { for (const service of ["DNSimple", "dns imple", "dns/../imple", "", 42, "a".repeat(65)]) { assert.deepEqual(sanitizeActions([{ ...valid(), service }]), [], `service=${JSON.stringify(service)}`); } }); await test("bad op ids are rejected", () => { for (const op of ["Domain.Check", ".check", "domain.", "domain..check", "domain check", "", 7, "a".repeat(65)]) { assert.deepEqual(sanitizeActions([{ ...valid(), op }]), [], `op=${JSON.stringify(op)}`); } }); await test("bad access is rejected", () => { for (const access of ["admin", "READ", "", undefined, null]) { assert.deepEqual(sanitizeActions([{ ...valid(), access }]), [], `access=${JSON.stringify(access)}`); } }); await test("non-string arg values are rejected", () => { for (const args of [{ n: 5 }, { ok: true }, { deep: { a: "b" } }, { fn: null }, ["a"], "str"]) { assert.deepEqual(sanitizeActions([{ ...valid(), args }]), [], `args=${JSON.stringify(args)}`); } }); await test("oversized args are rejected", () => { const many = Object.fromEntries(Array.from({ length: 17 }, (_, i) => [`k${i}`, "v"])); assert.deepEqual(sanitizeActions([{ ...valid(), args: many }]), [], "17 keys"); assert.deepEqual(sanitizeActions([{ ...valid(), args: { big: "x".repeat(5000) } }]), [], "stringified > 4096"); }); await test("label is copied, with room for a whole consent line (capped at 160)", () => { // A write's confirm sentence ("Register blip.com? This buys the domain and // costs real money.") is longer than a normal chip label — service_op gives // it room so consent text never cuts mid-warning, but still bounds it. const [chip] = sanitizeActions([{ ...valid(), label: " " + "L".repeat(200) }]); assert.equal(chip.label, "L".repeat(160)); const [bare] = sanitizeActions([{ ...valid(), label: undefined }]); assert.equal(bare.label, undefined); // shell.js falls back to "Run on " // Other chip types keep the tight 40-char cap. const [plain] = sanitizeActions([{ type: "open_profile", label: "T".repeat(80) }]); assert.equal(plain.label, "T".repeat(40)); }); console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`); process.exit(failures === 0 ? 0 : 1);