| 1 | // Tests for the service_op consent chip's sanitize bounds (ui.js) — the client |
| 2 | // whitelist gate every registry-op chip must pass. node test/serviceop.test.mjs |
| 3 | import assert from "node:assert/strict"; |
| 4 | import { sanitizeActions, CHIP_TYPES } from "../web/static/ui.js"; |
| 5 | |
| 6 | let failures = 0; |
| 7 | async function test(name, fn) { |
| 8 | try { |
| 9 | await fn(); |
| 10 | console.log(`PASS ${name}`); |
| 11 | } catch (e) { |
| 12 | failures++; |
| 13 | console.log(`FAIL ${name} — ${e.message.split("\n")[0]}`); |
| 14 | } |
| 15 | } |
| 16 | |
| 17 | const valid = () => ({ |
| 18 | type: "service_op", |
| 19 | service: "dnsimple", |
| 20 | op: "domain.check", |
| 21 | access: "read", |
| 22 | args: { account: "12345", domain: "blip.com" }, |
| 23 | label: "Check blip.com", |
| 24 | }); |
| 25 | |
| 26 | await test("service_op is a whitelisted chip type", () => { |
| 27 | assert.ok(CHIP_TYPES.service_op); |
| 28 | assert.equal(typeof CHIP_TYPES.service_op.sanitize, "function"); |
| 29 | }); |
| 30 | |
| 31 | await test("a valid chip passes with service/op/access/args copied", () => { |
| 32 | const [chip] = sanitizeActions([valid()]); |
| 33 | assert.deepEqual(chip, { |
| 34 | type: "service_op", |
| 35 | service: "dnsimple", |
| 36 | op: "domain.check", |
| 37 | access: "read", |
| 38 | args: { account: "12345", domain: "blip.com" }, |
| 39 | label: "Check blip.com", |
| 40 | }); |
| 41 | }); |
| 42 | |
| 43 | await test("a write op passes and keeps access=write", () => { |
| 44 | const a = { ...valid(), op: "domain.register", access: "write", args: { domain: "blip.com" } }; |
| 45 | const [chip] = sanitizeActions([a]); |
| 46 | assert.equal(chip.access, "write"); |
| 47 | assert.equal(chip.op, "domain.register"); |
| 48 | }); |
| 49 | |
| 50 | await test("missing args defaults to an empty object", () => { |
| 51 | const a = valid(); |
| 52 | delete a.args; |
| 53 | const [chip] = sanitizeActions([a]); |
| 54 | assert.deepEqual(chip.args, {}); |
| 55 | }); |
| 56 | |
| 57 | await test("bad service slugs are rejected", () => { |
| 58 | for (const service of ["DNSimple", "dns imple", "dns/../imple", "", 42, "a".repeat(65)]) { |
| 59 | assert.deepEqual(sanitizeActions([{ ...valid(), service }]), [], `service=${JSON.stringify(service)}`); |
| 60 | } |
| 61 | }); |
| 62 | |
| 63 | await test("bad op ids are rejected", () => { |
| 64 | for (const op of ["Domain.Check", ".check", "domain.", "domain..check", "domain check", "", 7, "a".repeat(65)]) { |
| 65 | assert.deepEqual(sanitizeActions([{ ...valid(), op }]), [], `op=${JSON.stringify(op)}`); |
| 66 | } |
| 67 | }); |
| 68 | |
| 69 | await test("bad access is rejected", () => { |
| 70 | for (const access of ["admin", "READ", "", undefined, null]) { |
| 71 | assert.deepEqual(sanitizeActions([{ ...valid(), access }]), [], `access=${JSON.stringify(access)}`); |
| 72 | } |
| 73 | }); |
| 74 | |
| 75 | await test("non-string arg values are rejected", () => { |
| 76 | for (const args of [{ n: 5 }, { ok: true }, { deep: { a: "b" } }, { fn: null }, ["a"], "str"]) { |
| 77 | assert.deepEqual(sanitizeActions([{ ...valid(), args }]), [], `args=${JSON.stringify(args)}`); |
| 78 | } |
| 79 | }); |
| 80 | |
| 81 | await test("oversized args are rejected", () => { |
| 82 | const many = Object.fromEntries(Array.from({ length: 17 }, (_, i) => [`k${i}`, "v"])); |
| 83 | assert.deepEqual(sanitizeActions([{ ...valid(), args: many }]), [], "17 keys"); |
| 84 | assert.deepEqual(sanitizeActions([{ ...valid(), args: { big: "x".repeat(5000) } }]), [], "stringified > 4096"); |
| 85 | }); |
| 86 | |
| 87 | await test("label is copied, with room for a whole consent line (capped at 160)", () => { |
| 88 | // A write's confirm sentence ("Register blip.com? This buys the domain and |
| 89 | // costs real money.") is longer than a normal chip label — service_op gives |
| 90 | // it room so consent text never cuts mid-warning, but still bounds it. |
| 91 | const [chip] = sanitizeActions([{ ...valid(), label: " " + "L".repeat(200) }]); |
| 92 | assert.equal(chip.label, "L".repeat(160)); |
| 93 | const [bare] = sanitizeActions([{ ...valid(), label: undefined }]); |
| 94 | assert.equal(bare.label, undefined); // shell.js falls back to "Run <op> on <service>" |
| 95 | // Other chip types keep the tight 40-char cap. |
| 96 | const [plain] = sanitizeActions([{ type: "open_profile", label: "T".repeat(80) }]); |
| 97 | assert.equal(plain.label, "T".repeat(40)); |
| 98 | }); |
| 99 | |
| 100 | console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`); |
| 101 | process.exit(failures === 0 ? 0 : 1); |
| 102 | |