87 lines
3.9 KiB
TypeScript
87 lines
3.9 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { join } from "node:path";
|
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
|
|
async function connectMcp(baseUrl: string) {
|
|
const env = { ...process.env, MCP_BACKEND_URL: baseUrl } as Record<string, string>;
|
|
delete env.ADMIN_USERNAME;
|
|
delete env.ADMIN_PASSWORD;
|
|
delete env.MCP_ADMIN_USERNAME;
|
|
delete env.MCP_ADMIN_PASSWORD;
|
|
env.MCP_ADMIN_USERNAME = "contract-test";
|
|
env.MCP_ADMIN_PASSWORD = "contract-test";
|
|
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [join(import.meta.dir, "index.ts")],
|
|
cwd: import.meta.dir,
|
|
env,
|
|
stderr: "pipe",
|
|
});
|
|
const client = new Client({ name: "mcp-contract-test", version: "1.0.0" });
|
|
await client.connect(transport);
|
|
return client;
|
|
}
|
|
|
|
test("settings writes send only supported fields and simple product fields are advertised", async () => {
|
|
const writes: unknown[] = [];
|
|
let getSettingsCount = 0;
|
|
const backend = Bun.serve({
|
|
port: 0,
|
|
fetch: async (request) => {
|
|
const path = new URL(request.url).pathname;
|
|
if (path === "/api/auth/login") return Response.json({ token: "contract-token" });
|
|
if (path === "/api/settings/footer-layout" && request.method === "GET") {
|
|
return Response.json({ version: "2026-10-11T00:00:00.000Z", layout: { columns: ["brand", "explore"] } });
|
|
}
|
|
if (path === "/api/settings" && request.method === "GET") {
|
|
getSettingsCount++;
|
|
return Response.json({ brandName: "Old", footerLayout: { columns: [] }, themeTokens: { brandAccent: "#123" } });
|
|
}
|
|
if (path === "/api/settings" && request.method === "PUT") {
|
|
const body = await request.json();
|
|
writes.push(body);
|
|
return Response.json({ ...(body as object), themeTokens: { brandAccent: "#123", ...((body as any).themeTokens ?? {}) } });
|
|
}
|
|
return new Response("Not found", { status: 404 });
|
|
},
|
|
});
|
|
let client: Client | undefined;
|
|
try {
|
|
client = await connectMcp(`http://localhost:${backend.port}/api`);
|
|
const tools = (await client.listTools()).tools;
|
|
expect(tools).toHaveLength(116);
|
|
expect(tools.find((item) => item.name === "get_footer_layout")?.annotations?.readOnlyHint).toBe(true);
|
|
expect(tools.find((item) => item.name === "delete_category_section")?.annotations?.destructiveHint).toBe(true);
|
|
const footerLayout = await client.callTool({ name: "get_footer_layout", arguments: {} });
|
|
expect(footerLayout.isError).toBeFalsy();
|
|
expect(footerLayout.structuredContent).toEqual({ version: "2026-10-11T00:00:00.000Z", layout: { columns: ["brand", "explore"] } });
|
|
for (const name of ["create_product", "bulk_create_products"]) {
|
|
const tool = tools.find((item) => item.name === name);
|
|
expect(tool).toBeDefined();
|
|
const properties = name === "create_product"
|
|
? tool!.inputSchema.properties
|
|
: (tool!.inputSchema.properties?.products as any).items.properties;
|
|
for (const field of ["price", "stock", "sku", "barcode"]) {
|
|
expect(properties?.[field]).toBeDefined();
|
|
}
|
|
}
|
|
|
|
expect((await client.callTool({ name: "update_theme_tokens", arguments: { brandPrimary: "#456" } })).isError).toBeFalsy();
|
|
expect((await client.callTool({ name: "update_brand_name", arguments: { brandName: "New" } })).isError).toBeFalsy();
|
|
expect((await client.callTool({ name: "update_footer_content", arguments: { brandDescription: "New footer", socialLinks: { facebookUrl: "https://example.com" } } })).isError).toBeFalsy();
|
|
|
|
expect(getSettingsCount).toBe(0);
|
|
expect(writes).toEqual([
|
|
{ themeTokens: { brandPrimary: "#456" } },
|
|
{ brandName: "New" },
|
|
{ footerDescription: "New footer", facebookUrl: "https://example.com" },
|
|
]);
|
|
} finally {
|
|
await client?.close();
|
|
backend.stop();
|
|
}
|
|
});
|
|
|