Files
Termix/scripts/validate-plugin-manifest.test.ts
T

98 lines
3.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { execFileSync } from "node:child_process";
import path from "node:path";
const VALIDATOR = path.join(__dirname, "validate-plugin-manifest.cjs");
const FIXTURES = path.join(__dirname, "__fixtures__", "manifests");
function runValidator(fixture?: string): { status: number; output: string } {
const args = fixture
? [VALIDATOR, path.join(FIXTURES, fixture)]
: [VALIDATOR];
try {
const output = execFileSync("node", args, { encoding: "utf8" });
return { status: 0, output };
} catch (err) {
const error = err as { status: number; stdout: string; stderr: string };
return { status: error.status, output: error.stdout + error.stderr };
}
}
describe("validate-plugin-manifest.cjs", () => {
it("accepts a valid manifest", () => {
const { status, output } = runValidator("valid.json");
expect(status).toBe(0);
expect(output).toContain("ok");
});
it("accepts a manifest declaring services", () => {
expect(runValidator("valid-services.json").status).toBe(0);
});
it("accepts a manifest declaring actions", () => {
expect(runValidator("valid-actions.json").status).toBe(0);
});
it("rejects a manifest missing a required field", () => {
const { status, output } = runValidator("invalid-missing-field.json");
expect(status).toBe(1);
expect(output).toContain("id");
});
it("rejects an id that is not a lowercase slug", () => {
const { status, output } = runValidator("invalid-bad-id.json");
expect(status).toBe(1);
expect(output).toContain("id");
});
it("rejects an unknown category", () => {
expect(runValidator("invalid-unknown-category.json").status).toBe(1);
});
// The dotted spelling was the v1 form. Capabilities are colon-separated now
// so they can never be confused with RBAC permissions.
it("rejects a dotted capability name", () => {
const { status, output } = runValidator("invalid-unknown-capability.json");
expect(status).toBe(1);
expect(output).toContain("hosts.read");
});
it("rejects a service permission the plugin does not declare", () => {
expect(runValidator("invalid-service-permission.json").status).toBe(1);
});
it("rejects an action permission the plugin does not declare", () => {
expect(runValidator("invalid-action-permission.json").status).toBe(1);
});
it("rejects an unknown top-level field", () => {
const { status, output } = runValidator("invalid-unknown-field.json");
expect(status).toBe(1);
expect(output).toContain("sudoEverything");
});
it("rejects an unknown field nested inside contributes", () => {
const { status, output } = runValidator(
"invalid-nested-unknown-field.json",
);
expect(status).toBe(1);
expect(output).toContain("extra");
});
// A manifest must not be able to name a core namespace, which is how a
// plugin could otherwise gate a route on admin authority it never had.
it("rejects a permission name outside the plugin's own namespace", () => {
const { status, output } = runValidator(
"invalid-role-default-escalation.json",
);
expect(status).toBe(1);
expect(output).toContain("admin.users.manage");
});
it("validates every bundled manifest when given no argument", () => {
const { status, output } = runValidator();
expect(status).toBe(0);
expect(output).toContain("ssh-terminal");
});
});