Initial Commit
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user