704 lines
24 KiB
TypeScript
704 lines
24 KiB
TypeScript
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
import {
|
|
createOpenAPIRegistry,
|
|
definePluginRoutes,
|
|
} from "@suaveplan/api-openapi";
|
|
import type { Principal } from "@suaveplan/api-plugin-auth";
|
|
import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { createFakeVaultService } from "../../tests/setup/fake-vault-service.js";
|
|
import { createMemoryDrive } from "../../tests/setup/memory-drive.js";
|
|
import { VAULT_PLUGIN_CODES } from "../error-codes/error-codes.js";
|
|
import { resetVaultPluginInstruments } from "../otel/instruments.js";
|
|
import { defaultUnlockExtractor } from "../plugin/unlock.js";
|
|
import type { VaultPluginConfig } from "../types/types.js";
|
|
import { buildVaultRoutes } from "./build-routes.js";
|
|
|
|
function makeApp(args: {
|
|
principal: Principal | null;
|
|
config: VaultPluginConfig;
|
|
fake?: ReturnType<typeof createFakeVaultService>;
|
|
}): { app: OpenAPIHono; service: ReturnType<typeof createFakeVaultService> } {
|
|
const service = args.fake ?? createFakeVaultService();
|
|
const app = new OpenAPIHono();
|
|
app.use("*", async (c, next) => {
|
|
c.set("user", args.principal);
|
|
await next();
|
|
});
|
|
const registry = createOpenAPIRegistry({
|
|
info: { title: "vault-test", version: "0.1.0" },
|
|
});
|
|
const entries = buildVaultRoutes(service, args.config);
|
|
definePluginRoutes(entries, { app, registry, pluginName: "vault" });
|
|
app.onError((err, c) => {
|
|
const ctx = (err as { context?: { statusCode?: number } }).context;
|
|
const status = ctx?.statusCode ?? 500;
|
|
const code = (err as { code?: string }).code ?? "UNKNOWN";
|
|
return c.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code,
|
|
message: (err as Error).message ?? "error",
|
|
timestamp: new Date().toISOString(),
|
|
},
|
|
},
|
|
status as 400 | 401 | 403 | 404 | 413 | 500
|
|
);
|
|
});
|
|
return { app, service };
|
|
}
|
|
|
|
const baseConfig = (): VaultPluginConfig => ({
|
|
storage: createMemoryDrive(),
|
|
routePrefix: "/vault",
|
|
maxItemSizeBytes: 10 * 1024 * 1024,
|
|
applySchemaOnInit: true,
|
|
unlockExtractor: defaultUnlockExtractor,
|
|
serverPepper: "test-pepper",
|
|
});
|
|
|
|
const principal: Principal = {
|
|
id: "user:alice",
|
|
emailVerified: true,
|
|
roles: [],
|
|
};
|
|
|
|
beforeEach(() => {
|
|
resetVaultPluginInstruments();
|
|
});
|
|
|
|
describe("routes/build-routes", () => {
|
|
it("registers exactly 5 routes", { timeout: 10_000 }, () => {
|
|
const service = createFakeVaultService();
|
|
const entries = buildVaultRoutes(service, baseConfig());
|
|
expect(entries.length).toBe(5);
|
|
expect(
|
|
entries.map(
|
|
(e) => `${e.route.method.toUpperCase()} ${e.route.path}`
|
|
)
|
|
).toEqual([
|
|
"GET /vault/items",
|
|
"POST /vault/items",
|
|
"GET /vault/items/{id}",
|
|
"PATCH /vault/items/{id}",
|
|
"DELETE /vault/items/{id}",
|
|
]);
|
|
});
|
|
|
|
it("requires auth — anonymous principal returns 401", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const { app } = makeApp({ principal: null, config: baseConfig() });
|
|
const res = await app.request("/vault/items", {
|
|
headers: { "X-Vault-Unlock": "p" },
|
|
});
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it("missing unlock header raises UNLOCK_HEADER_MISSING (400)", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const { app } = makeApp({ principal, config: baseConfig() });
|
|
const res = await app.request("/vault/items");
|
|
expect(res.status).toBe(400);
|
|
const json = (await res.json()) as { error: { code: string } };
|
|
expect(json.error.code).toBe(VAULT_PLUGIN_CODES.UNLOCK_HEADER_MISSING);
|
|
});
|
|
|
|
it("POST /vault/items creates and returns the item", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const { app } = makeApp({ principal, config: baseConfig() });
|
|
const res = await app.request("/vault/items", {
|
|
method: "POST",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
title: "API key",
|
|
kind: "credential",
|
|
body: "k",
|
|
}),
|
|
});
|
|
expect(res.status).toBe(201);
|
|
const json = (await res.json()) as {
|
|
data: { id: string; title: string; ownerId: string };
|
|
};
|
|
expect(json.data.title).toBe("API key");
|
|
expect(json.data.ownerId).toBe("user:alice");
|
|
});
|
|
|
|
it("POST rejects invalid body (400)", { timeout: 10_000 }, async () => {
|
|
const { app } = makeApp({ principal, config: baseConfig() });
|
|
const res = await app.request("/vault/items", {
|
|
method: "POST",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({}),
|
|
});
|
|
// Zod auto-validation at the @hono/zod-openapi boundary returns
|
|
// 400 before our handler runs.
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("POST rejects oversize bodies with ITEM_TOO_LARGE (413)", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const config: VaultPluginConfig = {
|
|
...baseConfig(),
|
|
maxItemSizeBytes: 16,
|
|
};
|
|
const { app } = makeApp({ principal, config });
|
|
const res = await app.request("/vault/items", {
|
|
method: "POST",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
title: "t",
|
|
kind: "k",
|
|
body: "x".repeat(64),
|
|
}),
|
|
});
|
|
expect(res.status).toBe(413);
|
|
const json = (await res.json()) as { error: { code: string } };
|
|
expect(json.error.code).toBe(VAULT_PLUGIN_CODES.ITEM_TOO_LARGE);
|
|
});
|
|
|
|
it("returns only principal's items in GET /vault/items", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService();
|
|
const { app } = makeApp({
|
|
principal,
|
|
config: baseConfig(),
|
|
fake: service,
|
|
});
|
|
// Seed three items for Alice + two for Bob.
|
|
const bob: Principal = {
|
|
id: "user:bob",
|
|
emailVerified: true,
|
|
roles: [],
|
|
};
|
|
const aliceHandle = await service.openForPrincipal(principal, "p");
|
|
await service.createItem(aliceHandle, {
|
|
title: "a1",
|
|
kind: "k",
|
|
body: "1",
|
|
});
|
|
await service.createItem(aliceHandle, {
|
|
title: "a2",
|
|
kind: "k",
|
|
body: "2",
|
|
});
|
|
await service.createItem(aliceHandle, {
|
|
title: "a3",
|
|
kind: "k",
|
|
body: "3",
|
|
});
|
|
await service.close(aliceHandle);
|
|
const bobHandle = await service.openForPrincipal(bob, "p");
|
|
await service.createItem(bobHandle, {
|
|
title: "b1",
|
|
kind: "k",
|
|
body: "1",
|
|
});
|
|
await service.createItem(bobHandle, {
|
|
title: "b2",
|
|
kind: "k",
|
|
body: "2",
|
|
});
|
|
await service.close(bobHandle);
|
|
const res = await app.request("/vault/items", {
|
|
headers: { "X-Vault-Unlock": "secret" },
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const json = (await res.json()) as {
|
|
data: Array<{ ownerId: string }>;
|
|
meta: { total: number };
|
|
};
|
|
expect(json.meta.total).toBe(3);
|
|
for (const it of json.data) expect(it.ownerId).toBe("user:alice");
|
|
});
|
|
|
|
it("GET /vault/items/:id of foreign item returns 404 (ITEM_NOT_FOUND)", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService();
|
|
const { app } = makeApp({
|
|
principal,
|
|
config: baseConfig(),
|
|
fake: service,
|
|
});
|
|
const bob: Principal = {
|
|
id: "user:bob",
|
|
emailVerified: true,
|
|
roles: [],
|
|
};
|
|
const bobHandle = await service.openForPrincipal(bob, "p");
|
|
const item = await service.createItem(bobHandle, {
|
|
title: "secret",
|
|
kind: "credential",
|
|
body: "k",
|
|
});
|
|
await service.close(bobHandle);
|
|
const res = await app.request(`/vault/items/${item.id}`, {
|
|
headers: { "X-Vault-Unlock": "secret" },
|
|
});
|
|
expect(res.status).toBe(404);
|
|
const json = (await res.json()) as { error: { code: string } };
|
|
expect(json.error.code).toBe(VAULT_PLUGIN_CODES.ITEM_NOT_FOUND);
|
|
});
|
|
|
|
it("GET /vault/items/:id returns the item when owned", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService();
|
|
const { app } = makeApp({
|
|
principal,
|
|
config: baseConfig(),
|
|
fake: service,
|
|
});
|
|
const handle = await service.openForPrincipal(principal, "p");
|
|
const created = await service.createItem(handle, {
|
|
title: "t",
|
|
kind: "k",
|
|
body: "body-value",
|
|
});
|
|
await service.close(handle);
|
|
const res = await app.request(`/vault/items/${created.id}`, {
|
|
headers: { "X-Vault-Unlock": "secret" },
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const json = (await res.json()) as { data: { body: string } };
|
|
expect(json.data.body).toBe("body-value");
|
|
});
|
|
|
|
it("PATCH /vault/items/:id rejects unknown fields (strict mode)", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService();
|
|
const { app } = makeApp({
|
|
principal,
|
|
config: baseConfig(),
|
|
fake: service,
|
|
});
|
|
const handle = await service.openForPrincipal(principal, "p");
|
|
const created = await service.createItem(handle, {
|
|
title: "t",
|
|
kind: "k",
|
|
body: "b",
|
|
});
|
|
await service.close(handle);
|
|
const res = await app.request(`/vault/items/${created.id}`, {
|
|
method: "PATCH",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ title: "X", evil: "Y" }),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("PATCH /vault/items/:id returns 404 for unknown id", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const { app } = makeApp({ principal, config: baseConfig() });
|
|
const res = await app.request("/vault/items/nope", {
|
|
method: "PATCH",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ title: "X" }),
|
|
});
|
|
expect(res.status).toBe(404);
|
|
const json = (await res.json()) as { error: { code: string } };
|
|
expect(json.error.code).toBe(VAULT_PLUGIN_CODES.ITEM_NOT_FOUND);
|
|
});
|
|
|
|
it("PATCH /vault/items/:id rejects oversize body", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService();
|
|
const config: VaultPluginConfig = {
|
|
...baseConfig(),
|
|
maxItemSizeBytes: 8,
|
|
};
|
|
const { app } = makeApp({ principal, config, fake: service });
|
|
const handle = await service.openForPrincipal(principal, "p");
|
|
const created = await service.createItem(handle, {
|
|
title: "t",
|
|
kind: "k",
|
|
body: "b",
|
|
});
|
|
await service.close(handle);
|
|
const res = await app.request(`/vault/items/${created.id}`, {
|
|
method: "PATCH",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ body: "x".repeat(64) }),
|
|
});
|
|
expect(res.status).toBe(413);
|
|
});
|
|
|
|
it("PATCH /vault/items/:id updates the item and returns it", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService();
|
|
const { app } = makeApp({
|
|
principal,
|
|
config: baseConfig(),
|
|
fake: service,
|
|
});
|
|
const handle = await service.openForPrincipal(principal, "p");
|
|
const created = await service.createItem(handle, {
|
|
title: "old",
|
|
kind: "k",
|
|
body: "b",
|
|
});
|
|
await service.close(handle);
|
|
const res = await app.request(`/vault/items/${created.id}`, {
|
|
method: "PATCH",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ title: "new" }),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const json = (await res.json()) as { data: { title: string } };
|
|
expect(json.data.title).toBe("new");
|
|
});
|
|
|
|
it("DELETE /vault/items/:id of foreign item returns 404 (not 403)", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService();
|
|
const { app } = makeApp({
|
|
principal,
|
|
config: baseConfig(),
|
|
fake: service,
|
|
});
|
|
const bob: Principal = {
|
|
id: "user:bob",
|
|
emailVerified: true,
|
|
roles: [],
|
|
};
|
|
const bobHandle = await service.openForPrincipal(bob, "p");
|
|
const item = await service.createItem(bobHandle, {
|
|
title: "b",
|
|
kind: "k",
|
|
body: "b",
|
|
});
|
|
await service.close(bobHandle);
|
|
const res = await app.request(`/vault/items/${item.id}`, {
|
|
method: "DELETE",
|
|
headers: { "X-Vault-Unlock": "secret" },
|
|
});
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("DELETE /vault/items/:id deletes and returns { deleted: true }", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService();
|
|
const { app } = makeApp({
|
|
principal,
|
|
config: baseConfig(),
|
|
fake: service,
|
|
});
|
|
const handle = await service.openForPrincipal(principal, "p");
|
|
const created = await service.createItem(handle, {
|
|
title: "t",
|
|
kind: "k",
|
|
body: "b",
|
|
});
|
|
await service.close(handle);
|
|
const res = await app.request(`/vault/items/${created.id}`, {
|
|
method: "DELETE",
|
|
headers: { "X-Vault-Unlock": "secret" },
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const json = (await res.json()) as { data: { deleted: boolean } };
|
|
expect(json.data.deleted).toBe(true);
|
|
});
|
|
|
|
it("UNLOCK_FAILED surfaces 401 when service rejects the password", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService({
|
|
passwords: { "user:alice": "correct" },
|
|
});
|
|
const { app } = makeApp({
|
|
principal,
|
|
config: baseConfig(),
|
|
fake: service,
|
|
});
|
|
const res = await app.request("/vault/items", {
|
|
headers: { "X-Vault-Unlock": "wrong" },
|
|
});
|
|
expect(res.status).toBe(401);
|
|
const json = (await res.json()) as { error: { code: string } };
|
|
expect(json.error.code).toBe(VAULT_PLUGIN_CODES.UNLOCK_FAILED);
|
|
});
|
|
|
|
it("POST with explicit metadata roundtrips", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const { app } = makeApp({ principal, config: baseConfig() });
|
|
const res = await app.request("/vault/items", {
|
|
method: "POST",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
title: "with-meta",
|
|
kind: "note",
|
|
body: "x",
|
|
metadata: { v: 1 },
|
|
}),
|
|
});
|
|
expect(res.status).toBe(201);
|
|
const json = (await res.json()) as {
|
|
data: { metadata: { v: number } };
|
|
};
|
|
expect(json.data.metadata.v).toBe(1);
|
|
});
|
|
|
|
it("PATCH supports kind / body / metadata permutations", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService();
|
|
const { app } = makeApp({
|
|
principal,
|
|
config: baseConfig(),
|
|
fake: service,
|
|
});
|
|
const handle = await service.openForPrincipal(principal, "p");
|
|
const created = await service.createItem(handle, {
|
|
title: "t",
|
|
kind: "k",
|
|
body: "b",
|
|
});
|
|
await service.close(handle);
|
|
// metadata only
|
|
const r1 = await app.request(`/vault/items/${created.id}`, {
|
|
method: "PATCH",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ metadata: { x: 1 } }),
|
|
});
|
|
expect(r1.status).toBe(200);
|
|
// kind only
|
|
const r2 = await app.request(`/vault/items/${created.id}`, {
|
|
method: "PATCH",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ kind: "new-kind" }),
|
|
});
|
|
expect(r2.status).toBe(200);
|
|
// body only
|
|
const r3 = await app.request(`/vault/items/${created.id}`, {
|
|
method: "PATCH",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ body: "new-body" }),
|
|
});
|
|
expect(r3.status).toBe(200);
|
|
});
|
|
|
|
it("withOpenVault failure path on LIST forwards non-VaultPluginError", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService();
|
|
(service as { listItems: unknown }).listItems = async () => {
|
|
throw new Error("list-failure");
|
|
};
|
|
const { app } = makeApp({
|
|
principal,
|
|
config: baseConfig(),
|
|
fake: service,
|
|
});
|
|
const res = await app.request("/vault/items", {
|
|
headers: { "X-Vault-Unlock": "secret" },
|
|
});
|
|
expect(res.status).toBeGreaterThanOrEqual(500);
|
|
});
|
|
|
|
it("withOpenVault failure path on read forwards instrument increment for non-VaultPluginError", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService();
|
|
// Override readItem to throw a plain Error so the catch block
|
|
// exercises the non-VaultPluginError branch.
|
|
(service as { readItem: unknown }).readItem = async () => {
|
|
throw new Error("non-vault-plugin-error");
|
|
};
|
|
const { app } = makeApp({
|
|
principal,
|
|
config: baseConfig(),
|
|
fake: service,
|
|
});
|
|
const res = await app.request("/vault/items/something", {
|
|
headers: { "X-Vault-Unlock": "secret" },
|
|
});
|
|
expect(res.status).toBeGreaterThanOrEqual(500);
|
|
});
|
|
|
|
it("invalid POST body is caught by handler safeParse when bypassing auto-validator", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
// Mount routes on a plain Hono — handlers run with explicit
|
|
// safeParse path which throws VAULT_PLUGIN.INVALID_INPUT.
|
|
const { Hono } = await import("hono");
|
|
const service = createFakeVaultService();
|
|
const config = baseConfig();
|
|
const app = new Hono();
|
|
app.use("*", async (c, next) => {
|
|
c.set("user", principal);
|
|
await next();
|
|
});
|
|
const entries = buildVaultRoutes(service, config);
|
|
for (const e of entries) {
|
|
const method = e.route.method as
|
|
| "get"
|
|
| "post"
|
|
| "patch"
|
|
| "delete";
|
|
const honoPath = e.route.path.replace(/\{(\w+)\}/gu, ":$1");
|
|
app[method](honoPath, async (c) => e.handler(c) as never);
|
|
}
|
|
app.onError((err, c) => {
|
|
return c.json(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: (err as { code?: string }).code ?? "X",
|
|
},
|
|
},
|
|
400
|
|
);
|
|
});
|
|
const res = await app.request("/vault/items", {
|
|
method: "POST",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({}),
|
|
});
|
|
const json = (await res.json()) as { error: { code: string } };
|
|
expect(json.error.code).toBe(VAULT_PLUGIN_CODES.INVALID_INPUT);
|
|
});
|
|
|
|
it("invalid PATCH body via plain Hono throws INVALID_INPUT", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const { Hono } = await import("hono");
|
|
const service = createFakeVaultService();
|
|
const config = baseConfig();
|
|
const h = await service.openForPrincipal(principal, "p");
|
|
const created = await service.createItem(h, {
|
|
title: "t",
|
|
kind: "k",
|
|
body: "b",
|
|
});
|
|
await service.close(h);
|
|
const app = new Hono();
|
|
app.use("*", async (c, next) => {
|
|
c.set("user", principal);
|
|
await next();
|
|
});
|
|
const entries = buildVaultRoutes(service, config);
|
|
for (const e of entries) {
|
|
const method = e.route.method as
|
|
| "get"
|
|
| "post"
|
|
| "patch"
|
|
| "delete";
|
|
const honoPath = e.route.path.replace(/\{(\w+)\}/gu, ":$1");
|
|
app[method](honoPath, async (c) => e.handler(c) as never);
|
|
}
|
|
app.onError((err, c) =>
|
|
c.json(
|
|
{
|
|
success: false,
|
|
error: { code: (err as { code?: string }).code ?? "X" },
|
|
},
|
|
400
|
|
)
|
|
);
|
|
const res = await app.request(`/vault/items/${created.id}`, {
|
|
method: "PATCH",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ title: 0 }),
|
|
});
|
|
const json = (await res.json()) as { error: { code: string } };
|
|
expect(json.error.code).toBe(VAULT_PLUGIN_CODES.INVALID_INPUT);
|
|
});
|
|
|
|
it("PATCH /vault/items/:id with body field records size histogram", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService();
|
|
const { app } = makeApp({
|
|
principal,
|
|
config: baseConfig(),
|
|
fake: service,
|
|
});
|
|
const handle = await service.openForPrincipal(principal, "p");
|
|
const created = await service.createItem(handle, {
|
|
title: "t",
|
|
kind: "k",
|
|
body: "b",
|
|
});
|
|
await service.close(handle);
|
|
const res = await app.request(`/vault/items/${created.id}`, {
|
|
method: "PATCH",
|
|
headers: {
|
|
"X-Vault-Unlock": "secret",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ body: "new-payload" }),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it("GET /vault/items/:id returns 500 if a non-VaultPluginError leaks from the service", {
|
|
timeout: 10_000,
|
|
}, async () => {
|
|
const service = createFakeVaultService();
|
|
// Override readItem to throw a plain Error.
|
|
(service as { readItem: unknown }).readItem = async () => {
|
|
throw new Error("boom");
|
|
};
|
|
const { app } = makeApp({
|
|
principal,
|
|
config: baseConfig(),
|
|
fake: service,
|
|
});
|
|
const res = await app.request("/vault/items/something", {
|
|
headers: { "X-Vault-Unlock": "secret" },
|
|
});
|
|
expect(res.status).toBeGreaterThanOrEqual(500);
|
|
});
|
|
});
|