Initial Commit
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import { createWasmSandbox } from "@suaveplan/api-sandbox-wasm";
|
||||
import type { PluginManifest } from "@suaveplan/api-types/manifest";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
SAND_WASM_BRIDGE_CODES,
|
||||
SandboxWasmBridgeError,
|
||||
} from "../../src/error-codes/error-codes.js";
|
||||
import { createSandboxedWasmPluginModule } from "../../src/index.js";
|
||||
import { buildFixtureWasm, WITNESS_OFFSET } from "../fixtures/build-wasm.js";
|
||||
|
||||
const MANIFEST: PluginManifest = {
|
||||
name: "full-flow",
|
||||
version: "0.1.0",
|
||||
apiHost: "@suaveplan/api-host@0",
|
||||
isolation: "sandboxed",
|
||||
trustLevel: "third-party",
|
||||
requiredCapabilities: [],
|
||||
};
|
||||
const ALLOW = { checkCapability: () => ({ allow: true as const }) };
|
||||
|
||||
async function buildInstall(
|
||||
fixture: Parameters<typeof buildFixtureWasm>[0] = {}
|
||||
) {
|
||||
const sandbox = await createWasmSandbox({
|
||||
pluginName: MANIFEST.name,
|
||||
policy: { fs: [], fetch: [], budgets: {} },
|
||||
capabilities: [],
|
||||
checker: ALLOW,
|
||||
wasmBytes: buildFixtureWasm(fixture),
|
||||
});
|
||||
return createSandboxedWasmPluginModule({ sandbox, manifest: MANIFEST });
|
||||
}
|
||||
|
||||
function ctx(method: string, path: string, body?: string): never {
|
||||
const url = `https://app.example.com${path}`;
|
||||
const request =
|
||||
body === undefined
|
||||
? new Request(url, { method })
|
||||
: new Request(url, {
|
||||
method,
|
||||
body,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
return {
|
||||
request,
|
||||
principal: null,
|
||||
tenant: null,
|
||||
capabilities: {},
|
||||
requestId: "rid",
|
||||
} as never;
|
||||
}
|
||||
|
||||
describe("full-flow integration", () => {
|
||||
it("WASM-exported routes register with the host", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const routesJson = JSON.stringify([
|
||||
{ method: "GET", path: "/items" },
|
||||
{ method: "POST", path: "/items" },
|
||||
]);
|
||||
const install = await buildInstall({ routesJson });
|
||||
try {
|
||||
expect(install.routes.map((r) => r.method)).toEqual([
|
||||
"GET",
|
||||
"POST",
|
||||
]);
|
||||
expect(install.routes.map((r) => r.path)).toEqual([
|
||||
"/items",
|
||||
"/items",
|
||||
]);
|
||||
expect(install.routes.every((r) => r.plugin === "full-flow")).toBe(
|
||||
true
|
||||
);
|
||||
} finally {
|
||||
install.module.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("1 KiB body round-trips through WASM bridge", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const install = await buildInstall();
|
||||
try {
|
||||
install.module.start();
|
||||
const route = install.routes[0];
|
||||
if (route === undefined) throw new Error("no route");
|
||||
// Build a 1 KiB body of ASCII bytes — easy to assert against.
|
||||
const body = "x".repeat(1024);
|
||||
const response = (await route.handler(
|
||||
ctx("POST", "/hello", body)
|
||||
)) as Response;
|
||||
expect(response.status).toBe(200);
|
||||
// Handler observed the bytes: witness range carries the
|
||||
// marshalled JSON which embeds the base64 body. We verify the
|
||||
// serialised JSON reached the guest by scanning for the URL.
|
||||
const memory = install.sandbox.instance.exports
|
||||
.memory as WebAssembly.Memory;
|
||||
const witness = new TextDecoder().decode(
|
||||
new Uint8Array(memory.buffer, WITNESS_OFFSET, 1024)
|
||||
);
|
||||
expect(witness).toContain('"url":"https://app.example.com/hello"');
|
||||
expect(witness).toContain('"method":"POST"');
|
||||
// Response body reached the caller intact.
|
||||
expect(await response.text()).toBe("ok");
|
||||
} finally {
|
||||
install.module.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("WASM unreachable instruction surfaces HANDLER_TRAPPED", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const install = await buildInstall({ handlerTraps: true });
|
||||
try {
|
||||
install.module.start();
|
||||
const route = install.routes[0];
|
||||
if (route === undefined) throw new Error("no route");
|
||||
await expect(
|
||||
route.handler(ctx("GET", "/hello"))
|
||||
).rejects.toMatchObject({
|
||||
code: SAND_WASM_BRIDGE_CODES.HANDLER_TRAPPED,
|
||||
context: { trapKind: "unreachable" },
|
||||
});
|
||||
} finally {
|
||||
install.module.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("denied capability surfaces SAND_PERM.DENIED through bridge", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const responseJson = JSON.stringify({
|
||||
status: 403,
|
||||
headers: [],
|
||||
bodyBase64: "",
|
||||
errorCode: "SAND_PERM.DENIED",
|
||||
errorMessage: "fs.write denied",
|
||||
});
|
||||
const install = await buildInstall({ responseJson });
|
||||
try {
|
||||
install.module.start();
|
||||
const route = install.routes[0];
|
||||
if (route === undefined) throw new Error("no route");
|
||||
await expect(
|
||||
route.handler(ctx("GET", "/hello"))
|
||||
).rejects.toMatchObject({
|
||||
code: "SAND_PERM.DENIED",
|
||||
});
|
||||
} finally {
|
||||
install.module.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("ABI validator rejects missing __suaveplan_handle_request at bridge-init", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
try {
|
||||
await buildInstall({ dropExports: ["__suaveplan_handle_request"] });
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(SandboxWasmBridgeError);
|
||||
const e = err as SandboxWasmBridgeError;
|
||||
expect(e.code).toBe(SAND_WASM_BRIDGE_CODES.INVALID_PLUGIN_SURFACE);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user