230 lines
7.5 KiB
TypeScript
230 lines
7.5 KiB
TypeScript
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { MANIFEST_CODES } from "../error-codes/error-codes.js";
|
|
import type { PluginManifest } from "../schema/plugin-manifest.js";
|
|
import { MINIMAL_FIXTURE } from "../testing/conformance.js";
|
|
import { validateErrorStatusMapShape } from "./validate-error-status-map-shape.js";
|
|
import { validateOpenApiRequiresGraphqlOrRoutes } from "./validate-openapi-requires-graphql-or-routes.js";
|
|
import { validatePermissionsWhenSandboxed } from "./validate-permissions-when-sandboxed.js";
|
|
import { validateSagaOwnership } from "./validate-saga-ownership.js";
|
|
|
|
describe("validateSagaOwnership", () => {
|
|
it("rejects step referencing undeclared dependency", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const m: PluginManifest = {
|
|
...MINIMAL_FIXTURE,
|
|
sagas: {
|
|
owns: [
|
|
{
|
|
name: "s1",
|
|
steps: [
|
|
{
|
|
plugin: "@acme/unknown",
|
|
action: "a",
|
|
compensate: "c",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const r = validateSagaOwnership(m);
|
|
expect(r.ok).toBe(false);
|
|
if (!r.ok) {
|
|
expect(r.error.code).toBe(MANIFEST_CODES.INVALID_SAGA_OWNERSHIP);
|
|
}
|
|
});
|
|
|
|
it("accepts step naming the manifest itself", { timeout: 5000 }, () => {
|
|
const m: PluginManifest = {
|
|
...MINIMAL_FIXTURE,
|
|
sagas: {
|
|
owns: [
|
|
{
|
|
name: "s1",
|
|
steps: [
|
|
{
|
|
plugin: MINIMAL_FIXTURE.name,
|
|
action: "a",
|
|
compensate: "c",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
expect(validateSagaOwnership(m).ok).toBe(true);
|
|
});
|
|
|
|
it("accepts step naming a declared dependency", { timeout: 5000 }, () => {
|
|
const m: PluginManifest = {
|
|
...MINIMAL_FIXTURE,
|
|
dependsOn: { "@acme/dep": "^1" },
|
|
sagas: {
|
|
owns: [
|
|
{
|
|
name: "s1",
|
|
steps: [
|
|
{
|
|
plugin: "@acme/dep",
|
|
action: "a",
|
|
compensate: "c",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
expect(validateSagaOwnership(m).ok).toBe(true);
|
|
});
|
|
|
|
it("no-op when sagas absent", { timeout: 5000 }, () => {
|
|
expect(validateSagaOwnership(MINIMAL_FIXTURE).ok).toBe(true);
|
|
});
|
|
|
|
it("no-op when owns is empty", { timeout: 5000 }, () => {
|
|
const m: PluginManifest = {
|
|
...MINIMAL_FIXTURE,
|
|
sagas: { owns: [] },
|
|
};
|
|
expect(validateSagaOwnership(m).ok).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("validatePermissionsWhenSandboxed", () => {
|
|
it("sandboxed isolation requires permissions.budgets", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const m: PluginManifest = {
|
|
...MINIMAL_FIXTURE,
|
|
isolation: "sandboxed",
|
|
};
|
|
const r = validatePermissionsWhenSandboxed(m);
|
|
expect(r.ok).toBe(false);
|
|
if (!r.ok) {
|
|
expect(r.error.code).toBe(MANIFEST_CODES.SANDBOX_REQUIRES_BUDGETS);
|
|
}
|
|
});
|
|
|
|
it("ok when sandboxed with budgets", { timeout: 5000 }, () => {
|
|
const m: PluginManifest = {
|
|
...MINIMAL_FIXTURE,
|
|
isolation: "sandboxed",
|
|
permissions: { budgets: { cpuPercent: 25 } },
|
|
};
|
|
expect(validatePermissionsWhenSandboxed(m).ok).toBe(true);
|
|
});
|
|
|
|
it("no-op for non-sandboxed tiers", { timeout: 5000 }, () => {
|
|
expect(validatePermissionsWhenSandboxed(MINIMAL_FIXTURE).ok).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("validateOpenApiRequiresGraphqlOrRoutes", () => {
|
|
it("ok when not contributing", { timeout: 5000 }, () => {
|
|
expect(validateOpenApiRequiresGraphqlOrRoutes(MINIMAL_FIXTURE).ok).toBe(
|
|
true
|
|
);
|
|
});
|
|
|
|
it("fails when contributing without routes or graphql", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const m: PluginManifest = {
|
|
...MINIMAL_FIXTURE,
|
|
openapi: { contributes: true },
|
|
};
|
|
const r = validateOpenApiRequiresGraphqlOrRoutes(m);
|
|
expect(r.ok).toBe(false);
|
|
if (!r.ok) {
|
|
expect(r.error.code).toBe(
|
|
MANIFEST_CODES.OPENAPI_REQUIRES_GRAPHQL_OR_ROUTES
|
|
);
|
|
}
|
|
});
|
|
|
|
it("ok with routes", { timeout: 5000 }, () => {
|
|
const m: PluginManifest = {
|
|
...MINIMAL_FIXTURE,
|
|
openapi: { contributes: true },
|
|
routes: { prefix: "/x" },
|
|
};
|
|
expect(validateOpenApiRequiresGraphqlOrRoutes(m).ok).toBe(true);
|
|
});
|
|
|
|
it("ok with graphql.contributes=true", { timeout: 5000 }, () => {
|
|
const m: PluginManifest = {
|
|
...MINIMAL_FIXTURE,
|
|
openapi: { contributes: true },
|
|
graphql: { contributes: true },
|
|
};
|
|
expect(validateOpenApiRequiresGraphqlOrRoutes(m).ok).toBe(true);
|
|
});
|
|
|
|
it("fails when openapi contributes and graphql.contributes=false", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const m: PluginManifest = {
|
|
...MINIMAL_FIXTURE,
|
|
openapi: { contributes: true },
|
|
graphql: { contributes: false },
|
|
};
|
|
expect(validateOpenApiRequiresGraphqlOrRoutes(m).ok).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("validateErrorStatusMapShape", () => {
|
|
it("no-op when statusMap absent", { timeout: 5000 }, () => {
|
|
expect(validateErrorStatusMapShape(MINIMAL_FIXTURE).ok).toBe(true);
|
|
});
|
|
|
|
it("ok when every entry is well-formed", { timeout: 5000 }, () => {
|
|
const m: PluginManifest = {
|
|
...MINIMAL_FIXTURE,
|
|
errors: { statusMap: { "PLUGIN.A.B": 409 } },
|
|
};
|
|
expect(validateErrorStatusMapShape(m).ok).toBe(true);
|
|
});
|
|
|
|
it("collects code-shape offenders", { timeout: 5000 }, () => {
|
|
// bypass Zod by passing a manually built object
|
|
const bad = {
|
|
...MINIMAL_FIXTURE,
|
|
errors: {
|
|
statusMap: {
|
|
"lower.case.code": 409,
|
|
},
|
|
},
|
|
} as unknown as PluginManifest;
|
|
const r = validateErrorStatusMapShape(bad);
|
|
expect(r.ok).toBe(false);
|
|
if (!r.ok) {
|
|
expect(r.error.code).toBe(MANIFEST_CODES.INVALID_ERROR_STATUS_MAP);
|
|
const off = r.error.context?.offenders as unknown[];
|
|
expect(off.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it("collects status-range offenders", { timeout: 5000 }, () => {
|
|
const bad = {
|
|
...MINIMAL_FIXTURE,
|
|
errors: {
|
|
statusMap: { "PLUGIN.A.B": 200 },
|
|
},
|
|
} as unknown as PluginManifest;
|
|
const r = validateErrorStatusMapShape(bad);
|
|
expect(r.ok).toBe(false);
|
|
});
|
|
|
|
it("rejects non-integer status", { timeout: 5000 }, () => {
|
|
const bad = {
|
|
...MINIMAL_FIXTURE,
|
|
errors: {
|
|
statusMap: { "PLUGIN.A.B": 4.5 as unknown as number },
|
|
},
|
|
} as unknown as PluginManifest;
|
|
const r = validateErrorStatusMapShape(bad);
|
|
expect(r.ok).toBe(false);
|
|
});
|
|
});
|