x
This commit is contained in:
1 parent
70877882b0
commit
ce2f4cf0c8
93 files changed
+4200
-67
No files matched your search
@@ -6,7 +6,11 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove }: any) => {
|
||||
const childrenKey = type === 'root' ? 'columns' : 'links';
|
||||
const labelKey = type === 'column' ? 'title' : 'label';
|
||||
const hasUrl = type !== 'column';
|
||||
|
||||
|
||||
// Image field name by node type
|
||||
const imageFieldKey = type === 'root' ? 'rootImageUrl' : type === 'column' ? 'iconUrl' : 'imageUrl';
|
||||
const imageFieldLabel = type === 'root' ? 'Root Image URL (left panel, 1:1)' : type === 'column' ? 'Column Icon URL (1:1)' : 'Hover Image URL (right panel, 1:1)';
|
||||
|
||||
const children = node[childrenKey] || [];
|
||||
|
||||
const handleAddChild = () => {
|
||||
@@ -29,6 +33,7 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove }: any) => {
|
||||
|
||||
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
|
||||
const nodeLabel = node[labelKey] || {};
|
||||
const imageValue: string = node[imageFieldKey] || '';
|
||||
|
||||
return (
|
||||
<div className={`border border-white/10 p-4 rounded-xl mt-4 ${depth > 0 ? 'ml-6 border-l-cyan-500/30' : ''}`}>
|
||||
@@ -50,7 +55,29 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove }: any) => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image URL field */}
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Preview thumbnail */}
|
||||
<div className="w-14 h-14 shrink-0 rounded-lg overflow-hidden border border-white/10 bg-white/5 flex items-center justify-center">
|
||||
{imageValue ? (
|
||||
<img src={imageValue} alt="preview" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<span className="text-[9px] text-slate-600 text-center leading-tight px-1">No Image</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-[10px] uppercase font-bold text-slate-500">{imageFieldLabel}</label>
|
||||
<input
|
||||
className="admin-input w-full text-xs py-1 px-2"
|
||||
value={imageValue}
|
||||
placeholder="https://cdn.example.com/image.webp"
|
||||
onChange={e => onChange({ ...node, [imageFieldKey]: e.target.value || null })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasUrl && (
|
||||
<div className="w-48 space-y-1">
|
||||
<label className="text-[10px] uppercase font-bold text-slate-500">url</label>
|
||||
|
||||
@@ -7,6 +7,8 @@ export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
server: {
|
||||
host: true,
|
||||
port: 5174, // กำหนด port ชัดเจน
|
||||
|
||||
allowedHosts: true,
|
||||
},
|
||||
})
|
||||
@@ -31,6 +31,7 @@
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@types/pg": "^8.20.0",
|
||||
"axios": "^1.18.1",
|
||||
"prisma": "^7.6.0",
|
||||
"typescript": "^6.0.2"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import prisma from "../lib/prisma";
|
||||
import { adminOnly } from "../middleware/auth";
|
||||
import prisma from "../lib/prisma.js";
|
||||
import { adminOnly } from "../middleware/auth.js";
|
||||
|
||||
export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||
.get("/", async () => {
|
||||
@@ -29,27 +29,30 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||
return menu;
|
||||
})
|
||||
.use(adminOnly)
|
||||
.post("/", async ({ body, set }) => {
|
||||
.post("/", async ({ body, set }: { body: any; set: any }) => {
|
||||
try {
|
||||
const { name, rootImage, items } = body as {
|
||||
name: string;
|
||||
rootImage?: string | null;
|
||||
items?: { label: string; url: string; order: number; columnImage?: string | null; hoverImage?: string | null }[]
|
||||
};
|
||||
const data: any = {
|
||||
name,
|
||||
rootImage,
|
||||
};
|
||||
if (items) {
|
||||
data.items = {
|
||||
create: items.map(item => ({
|
||||
label: item.label,
|
||||
url: item.url,
|
||||
order: item.order,
|
||||
columnImage: item.columnImage,
|
||||
hoverImage: item.hoverImage
|
||||
}))
|
||||
};
|
||||
}
|
||||
const menu = await prisma.menu.create({
|
||||
data: {
|
||||
name,
|
||||
rootImage,
|
||||
items: items ? {
|
||||
create: items.map(item => ({
|
||||
label: item.label,
|
||||
url: item.url,
|
||||
order: item.order,
|
||||
columnImage: item.columnImage,
|
||||
hoverImage: item.hoverImage
|
||||
}))
|
||||
} : undefined
|
||||
},
|
||||
data,
|
||||
include: { items: true }
|
||||
});
|
||||
return menu;
|
||||
@@ -71,7 +74,7 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||
})))
|
||||
})
|
||||
})
|
||||
.put("/:name", async ({ params: { name }, body, set }) => {
|
||||
.put("/:name", async ({ params: { name }, body, set }: { params: { name: string }; body: any; set: any }) => {
|
||||
try {
|
||||
const { items, payload, isActive, rootImage } = body as {
|
||||
payload?: any;
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
import prisma from "./src/lib/prisma";
|
||||
import axios from "axios";
|
||||
import { handleMenuTool } from "../mcp-server/menu-toolkit";
|
||||
|
||||
const API_BASE = "http://localhost:3051/api";
|
||||
const TEST_MENU_NAME_API = "challenger-test-menu-api";
|
||||
const TEST_MENU_NAME_MCP = "challenger-test-menu-mcp";
|
||||
|
||||
async function loginAndGetToken() {
|
||||
const loginRes = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: "admin", password: "homaroma123" })
|
||||
});
|
||||
|
||||
if (!loginRes.ok) {
|
||||
throw new Error(`Failed to log in: ${loginRes.statusText}`);
|
||||
}
|
||||
|
||||
const data = await loginRes.json() as { success: boolean; token: string };
|
||||
if (!data.success || !data.token) {
|
||||
throw new Error(`Invalid login response: ${JSON.stringify(data)}`);
|
||||
}
|
||||
return data.token;
|
||||
}
|
||||
|
||||
async function cleanDatabase() {
|
||||
console.log("Cleaning up previous test data in the database...");
|
||||
await prisma.menu.deleteMany({
|
||||
where: {
|
||||
name: {
|
||||
in: [TEST_MENU_NAME_API, TEST_MENU_NAME_MCP]
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log("Cleanup finished.");
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const token = await loginAndGetToken();
|
||||
console.log("Obtained Admin Authorization Token successfully.");
|
||||
|
||||
await cleanDatabase();
|
||||
|
||||
console.log("\n=============================================");
|
||||
console.log("PART 1: Direct Elysia API Routes Verification");
|
||||
console.log("=============================================");
|
||||
|
||||
// 1. Post a new menu via API
|
||||
console.log("1.1 POST /api/menus - Creating a menu with rootImage and items with columnImage & hoverImage...");
|
||||
const createPayload = {
|
||||
name: TEST_MENU_NAME_API,
|
||||
rootImage: "https://example.com/api-root-original.png",
|
||||
items: [
|
||||
{
|
||||
label: "Link 1",
|
||||
url: "/link1",
|
||||
order: 1,
|
||||
columnImage: "https://example.com/api-col1-original.png",
|
||||
hoverImage: "https://example.com/api-hover1-original.png"
|
||||
},
|
||||
{
|
||||
label: "Link 2",
|
||||
url: "/link2",
|
||||
order: 2,
|
||||
columnImage: null,
|
||||
hoverImage: null
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const postRes = await fetch(`${API_BASE}/menus`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(createPayload)
|
||||
});
|
||||
|
||||
if (!postRes.ok) {
|
||||
throw new Error(`POST /menus failed: ${postRes.status} ${await postRes.text()}`);
|
||||
}
|
||||
const createdMenu = await postRes.json() as any;
|
||||
console.log("Successfully created menu via API:", JSON.stringify(createdMenu, null, 2));
|
||||
|
||||
// 2. Query the backend API and verify stored fields
|
||||
console.log("\n1.2 GET /api/menus/:name - Querying the backend API to verify fields...");
|
||||
const getRes = await fetch(`${API_BASE}/menus/${TEST_MENU_NAME_API}`);
|
||||
if (!getRes.ok) {
|
||||
throw new Error(`GET /menus/${TEST_MENU_NAME_API} failed: ${getRes.status}`);
|
||||
}
|
||||
const retrievedMenu = await getRes.json() as any;
|
||||
console.log("Retrieved menu from API:", JSON.stringify(retrievedMenu, null, 2));
|
||||
|
||||
if (retrievedMenu.rootImage !== createPayload.rootImage) {
|
||||
throw new Error(`API Verification Error: Menu rootImage mismatch. Expected '${createPayload.rootImage}', got '${retrievedMenu.rootImage}'`);
|
||||
}
|
||||
|
||||
const items = retrievedMenu.items || [];
|
||||
if (items.length !== 2) {
|
||||
throw new Error(`API Verification Error: Expected 2 items, got ${items.length}`);
|
||||
}
|
||||
|
||||
const item1 = items.find((i: any) => i.label === "Link 1");
|
||||
const item2 = items.find((i: any) => i.label === "Link 2");
|
||||
|
||||
if (!item1 || item1.columnImage !== "https://example.com/api-col1-original.png" || item1.hoverImage !== "https://example.com/api-hover1-original.png") {
|
||||
throw new Error(`API Verification Error: Item 1 image fields mismatch. Got: col='${item1?.columnImage}', hover='${item1?.hoverImage}'`);
|
||||
}
|
||||
if (!item2 || item2.columnImage !== null || item2.hoverImage !== null) {
|
||||
throw new Error(`API Verification Error: Item 2 image fields mismatch. Got: col='${item2?.columnImage}', hover='${item2?.hoverImage}'`);
|
||||
}
|
||||
console.log("✅ GET API fields are stored and retrieved correctly!");
|
||||
|
||||
// 3. Update the menu via PUT /api/menus/:name
|
||||
console.log("\n1.3 PUT /api/menus/:name - Updating the menu and items...");
|
||||
const updatePayload = {
|
||||
rootImage: "https://example.com/api-root-updated.png",
|
||||
items: [
|
||||
{
|
||||
id: item1.id,
|
||||
label: "Link 1 Updated",
|
||||
url: "/link1-updated",
|
||||
order: 10,
|
||||
columnImage: "https://example.com/api-col1-updated.png",
|
||||
hoverImage: "https://example.com/api-hover1-updated.png"
|
||||
},
|
||||
{
|
||||
label: "Link 3 Added",
|
||||
url: "/link3",
|
||||
order: 3,
|
||||
columnImage: "https://example.com/api-col3-new.png",
|
||||
hoverImage: null
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const putRes = await fetch(`${API_BASE}/menus/${TEST_MENU_NAME_API}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(updatePayload)
|
||||
});
|
||||
|
||||
if (!putRes.ok) {
|
||||
throw new Error(`PUT /menus failed: ${putRes.status} ${await putRes.text()}`);
|
||||
}
|
||||
const updatedMenu = await putRes.json() as any;
|
||||
console.log("Successfully updated menu via PUT:", JSON.stringify(updatedMenu, null, 2));
|
||||
|
||||
if (updatedMenu.rootImage !== updatePayload.rootImage) {
|
||||
throw new Error(`API Verification Error: Updated Menu rootImage mismatch. Expected '${updatePayload.rootImage}', got '${updatedMenu.rootImage}'`);
|
||||
}
|
||||
|
||||
const updatedItems = updatedMenu.items || [];
|
||||
const updatedItem1 = updatedItems.find((i: any) => i.id === item1.id);
|
||||
const addedItem3 = updatedItems.find((i: any) => i.label === "Link 3 Added");
|
||||
|
||||
if (!updatedItem1 || updatedItem1.columnImage !== "https://example.com/api-col1-updated.png" || updatedItem1.hoverImage !== "https://example.com/api-hover1-updated.png") {
|
||||
throw new Error(`API Verification Error: Updated Item 1 image fields mismatch.`);
|
||||
}
|
||||
if (!addedItem3 || addedItem3.columnImage !== "https://example.com/api-col3-new.png" || addedItem3.hoverImage !== null) {
|
||||
throw new Error(`API Verification Error: Added Item 3 image fields mismatch.`);
|
||||
}
|
||||
console.log("✅ PUT API fields update verified successfully!");
|
||||
|
||||
// 4. Verify validation errors for incorrect types in Elysia routes
|
||||
console.log("\n1.4 Verify Elysia Route Validation Errors for Incorrect Types...");
|
||||
const invalidPayloads = [
|
||||
{
|
||||
description: "Invalid rootImage type (number instead of string/null)",
|
||||
payload: {
|
||||
name: "challenger-invalid-1",
|
||||
rootImage: 12345,
|
||||
items: []
|
||||
}
|
||||
},
|
||||
{
|
||||
description: "Invalid columnImage type (boolean instead of string/null)",
|
||||
payload: {
|
||||
name: "challenger-invalid-2",
|
||||
rootImage: null,
|
||||
items: [
|
||||
{
|
||||
label: "Link",
|
||||
url: "/url",
|
||||
order: 1,
|
||||
columnImage: true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
description: "Invalid order type (string instead of number)",
|
||||
payload: {
|
||||
name: "challenger-invalid-3",
|
||||
rootImage: null,
|
||||
items: [
|
||||
{
|
||||
label: "Link",
|
||||
url: "/url",
|
||||
order: "first",
|
||||
columnImage: null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
for (const { description, payload } of invalidPayloads) {
|
||||
const invalidRes = await fetch(`${API_BASE}/menus`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
console.log(`- Testing: ${description}`);
|
||||
console.log(` Response Status: ${invalidRes.status} (Expected: 422)`);
|
||||
if (invalidRes.status !== 422) {
|
||||
throw new Error(`API Validation Verification Error: Expected 422 status, got ${invalidRes.status}`);
|
||||
}
|
||||
const errorBody = await invalidRes.json();
|
||||
console.log(` Validation Message: ${JSON.stringify((errorBody as any).summary || (errorBody as any).message || errorBody)}`);
|
||||
}
|
||||
console.log("✅ Elysia Route Validation Errors verification passed!");
|
||||
|
||||
console.log("\n=============================================");
|
||||
console.log("PART 2: Direct Database Schema Verification");
|
||||
console.log("=============================================");
|
||||
|
||||
console.log("Querying database directly using Prisma client...");
|
||||
const dbMenu = await prisma.menu.findUnique({
|
||||
where: { name: TEST_MENU_NAME_API },
|
||||
include: { items: true }
|
||||
});
|
||||
|
||||
if (!dbMenu) {
|
||||
throw new Error("DB Verification Error: Menu not found in database.");
|
||||
}
|
||||
console.log("Direct DB Menu Record:", JSON.stringify(dbMenu, null, 2));
|
||||
|
||||
if (dbMenu.rootImage !== "https://example.com/api-root-updated.png") {
|
||||
throw new Error(`DB Verification Error: Menu rootImage column mismatch. Expected 'https://example.com/api-root-updated.png', got '${dbMenu.rootImage}'`);
|
||||
}
|
||||
|
||||
const dbItem1 = dbMenu.items.find(i => i.url === "/link1-updated");
|
||||
if (!dbItem1 || dbItem1.columnImage !== "https://example.com/api-col1-updated.png" || dbItem1.hoverImage !== "https://example.com/api-hover1-updated.png") {
|
||||
throw new Error("DB Verification Error: Menu item 1 columns mismatch in PostgreSQL.");
|
||||
}
|
||||
|
||||
const dbItem3 = dbMenu.items.find(i => i.url === "/link3");
|
||||
if (!dbItem3 || dbItem3.columnImage !== "https://example.com/api-col3-new.png" || dbItem3.hoverImage !== null) {
|
||||
throw new Error("DB Verification Error: Menu item 3 columns mismatch in PostgreSQL.");
|
||||
}
|
||||
console.log("✅ PostgreSQL schema verification passed!");
|
||||
|
||||
console.log("\n=============================================");
|
||||
console.log("PART 3: MCP Tools Layer Verification");
|
||||
console.log("=============================================");
|
||||
|
||||
// Create an Axios instance mimicking the MCP server environment
|
||||
const apiAxios = axios.create({
|
||||
baseURL: API_BASE,
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
// 1. Test upsert_menu MCP Tool
|
||||
console.log("3.1 Testing upsert_menu MCP Tool...");
|
||||
const mcpPayload = {
|
||||
name: TEST_MENU_NAME_MCP,
|
||||
rootImage: "https://example.com/mcp-root.png",
|
||||
items: [
|
||||
{
|
||||
label: "MCP Link 1",
|
||||
url: "/mcp-link1",
|
||||
order: 1,
|
||||
columnImage: "https://example.com/mcp-col1.png",
|
||||
hoverImage: "https://example.com/mcp-hover1.png"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const upsertToolRes = await handleMenuTool(apiAxios, "upsert_menu", mcpPayload);
|
||||
console.log("upsert_menu MCP response:", JSON.stringify(upsertToolRes, null, 2));
|
||||
|
||||
if (upsertToolRes?.isError) {
|
||||
throw new Error(`MCP Tool Error: upsert_menu failed: ${JSON.stringify(upsertToolRes)}`);
|
||||
}
|
||||
|
||||
// Verify stored fields via direct API / DB
|
||||
const mcpDbMenu = await prisma.menu.findUnique({
|
||||
where: { name: TEST_MENU_NAME_MCP },
|
||||
include: { items: true }
|
||||
});
|
||||
|
||||
if (!mcpDbMenu) {
|
||||
throw new Error("MCP DB Verification Error: Menu created by MCP not found.");
|
||||
}
|
||||
if (mcpDbMenu.rootImage !== mcpPayload.rootImage) {
|
||||
throw new Error(`MCP DB Verification Error: rootImage mismatch. Got '${mcpDbMenu.rootImage}'`);
|
||||
}
|
||||
const mcpItem = mcpDbMenu.items[0];
|
||||
if (!mcpItem || mcpItem.columnImage !== "https://example.com/mcp-col1.png" || mcpItem.hoverImage !== "https://example.com/mcp-hover1.png") {
|
||||
throw new Error(`MCP DB Verification Error: Item columns mismatch. Got col='${mcpItem?.columnImage}', hover='${mcpItem?.hoverImage}'`);
|
||||
}
|
||||
console.log("✅ MCP upsert_menu successfully created & stored correct fields!");
|
||||
|
||||
// 2. Test get_menu MCP Tool
|
||||
console.log("\n3.2 Testing get_menu MCP Tool...");
|
||||
const getToolRes = await handleMenuTool(apiAxios, "get_menu", { name: TEST_MENU_NAME_MCP });
|
||||
console.log("get_menu MCP response:", JSON.stringify(getToolRes, null, 2));
|
||||
|
||||
if (getToolRes?.isError) {
|
||||
throw new Error(`MCP Tool Error: get_menu failed: ${JSON.stringify(getToolRes)}`);
|
||||
}
|
||||
|
||||
const mcpContentText = getToolRes?.content?.[0]?.text;
|
||||
if (!mcpContentText) {
|
||||
throw new Error("MCP Tool Error: No content returned by get_menu.");
|
||||
}
|
||||
const mcpMenuData = JSON.parse(mcpContentText);
|
||||
if (mcpMenuData.rootImage !== mcpPayload.rootImage) {
|
||||
throw new Error(`MCP Tool Verification Error: rootImage mismatch in get_menu response.`);
|
||||
}
|
||||
if (mcpMenuData.items?.[0]?.columnImage !== "https://example.com/mcp-col1.png") {
|
||||
throw new Error(`MCP Tool Verification Error: columnImage mismatch in get_menu response.`);
|
||||
}
|
||||
console.log("✅ MCP get_menu successfully retrieved correct fields!");
|
||||
|
||||
// 3. Verify validation errors in MCP Tools Layer
|
||||
console.log("\n3.3 Verify MCP Tool Validation Errors for Incorrect Types...");
|
||||
const invalidMcpPayloads = [
|
||||
{
|
||||
description: "Invalid items parameter type (not an array)",
|
||||
payload: {
|
||||
name: TEST_MENU_NAME_MCP,
|
||||
items: "not-an-array"
|
||||
}
|
||||
},
|
||||
{
|
||||
description: "Invalid columnImage parameter type (number instead of string/null)",
|
||||
payload: {
|
||||
name: TEST_MENU_NAME_MCP,
|
||||
items: [
|
||||
{
|
||||
label: "Invalid Item",
|
||||
url: "/invalid",
|
||||
order: 1,
|
||||
columnImage: 999
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
for (const { description, payload } of invalidMcpPayloads) {
|
||||
console.log(`- Testing MCP Tool: ${description}`);
|
||||
try {
|
||||
await handleMenuTool(apiAxios, "upsert_menu", payload);
|
||||
throw new Error(`Expected MCP Tool to throw validation error, but it succeeded.`);
|
||||
} catch (e: any) {
|
||||
console.log(` Caught expected validation error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
console.log("✅ MCP Tool validation error checking passed!");
|
||||
|
||||
console.log("\n=============================================");
|
||||
console.log("ALL TESTS COMPLETED SUCCESSFULLY!");
|
||||
console.log("=============================================");
|
||||
}
|
||||
|
||||
run()
|
||||
.catch((err) => {
|
||||
console.error("\n❌ VERIFICATION TEST FAILED!");
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => {
|
||||
prisma.$disconnect();
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Elysia } from "elysia";
|
||||
import prisma from "./src/lib/prisma.js";
|
||||
import { menusRoute } from "./src/routes/menus.js";
|
||||
import { authRoute } from "./src/routes/auth.js";
|
||||
|
||||
const TEST_MENU_NAME = "test-image-menu";
|
||||
|
||||
async function main() {
|
||||
console.log("🚀 Starting Menu Image Integration Test...");
|
||||
|
||||
// 1. Direct Prisma cleanup
|
||||
console.log("🧹 Cleaning up old test menu...");
|
||||
await prisma.menu.deleteMany({
|
||||
where: { name: TEST_MENU_NAME }
|
||||
});
|
||||
|
||||
// 2. Initialize Elysia mock server
|
||||
console.log("⚙️ Setting up Elysia test app...");
|
||||
const app = new Elysia().use(authRoute).use(menusRoute);
|
||||
|
||||
// 3. Login to get token
|
||||
console.log("🔑 Logging in as admin...");
|
||||
const loginRes = await app.handle(
|
||||
new Request("http://localhost/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: "admin", password: "homaroma123" })
|
||||
})
|
||||
);
|
||||
|
||||
if (loginRes.status !== 200) {
|
||||
const errText = await loginRes.text();
|
||||
throw new Error(`Login failed with status ${loginRes.status}: ${errText}`);
|
||||
}
|
||||
|
||||
const { success, token } = (await loginRes.json()) as { success: boolean; token: string };
|
||||
if (!success || !token) {
|
||||
throw new Error("Failed to retrieve authentication token.");
|
||||
}
|
||||
console.log("✅ Authenticated successfully.");
|
||||
|
||||
// 4. POST: Create menu with rootImage, columnImage, and hoverImage
|
||||
console.log("➕ Creating new menu with image fields via POST...");
|
||||
const postRes = await app.handle(
|
||||
new Request("http://localhost/menus", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: TEST_MENU_NAME,
|
||||
rootImage: "https://example.com/root.jpg",
|
||||
items: [
|
||||
{
|
||||
label: "Product 1",
|
||||
url: "/product1",
|
||||
order: 1,
|
||||
columnImage: "https://example.com/col1.jpg",
|
||||
hoverImage: "https://example.com/hov1.jpg"
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
if (postRes.status !== 200 && postRes.status !== 201) {
|
||||
const errText = await postRes.text();
|
||||
throw new Error(`POST /menus failed with status ${postRes.status}: ${errText}`);
|
||||
}
|
||||
|
||||
const createdMenu = (await postRes.json()) as any;
|
||||
console.log("Created menu payload:", JSON.stringify(createdMenu, null, 2));
|
||||
|
||||
// Assertions
|
||||
if (createdMenu.name !== TEST_MENU_NAME) throw new Error("Incorrect menu name");
|
||||
if (createdMenu.rootImage !== "https://example.com/root.jpg") throw new Error("Incorrect rootImage");
|
||||
if (!createdMenu.items || createdMenu.items.length !== 1) throw new Error("Expected 1 menu item");
|
||||
if (createdMenu.items[0].columnImage !== "https://example.com/col1.jpg") throw new Error("Incorrect columnImage");
|
||||
if (createdMenu.items[0].hoverImage !== "https://example.com/hov1.jpg") throw new Error("Incorrect hoverImage");
|
||||
console.log("✅ POST verification successful!");
|
||||
|
||||
// 5. PUT: Update menu with new images
|
||||
console.log("🔄 Updating menu with new images via PUT...");
|
||||
const putRes = await app.handle(
|
||||
new Request(`http://localhost/menus/${TEST_MENU_NAME}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
rootImage: "https://example.com/root-updated.jpg",
|
||||
items: [
|
||||
{
|
||||
label: "Product 1 Updated",
|
||||
url: "/product1-updated",
|
||||
order: 1,
|
||||
columnImage: "https://example.com/col1-updated.jpg",
|
||||
hoverImage: "https://example.com/hov1-updated.jpg"
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
if (putRes.status !== 200) {
|
||||
const errText = await putRes.text();
|
||||
throw new Error(`PUT /menus/:name failed with status ${putRes.status}: ${errText}`);
|
||||
}
|
||||
|
||||
const updatedMenu = (await putRes.json()) as any;
|
||||
console.log("Updated menu payload:", JSON.stringify(updatedMenu, null, 2));
|
||||
|
||||
// Assertions
|
||||
if (updatedMenu.rootImage !== "https://example.com/root-updated.jpg") throw new Error("Incorrect rootImage update");
|
||||
if (!updatedMenu.items || updatedMenu.items.length !== 1) throw new Error("Expected 1 menu item after update");
|
||||
if (updatedMenu.items[0].columnImage !== "https://example.com/col1-updated.jpg") throw new Error("Incorrect columnImage update");
|
||||
if (updatedMenu.items[0].hoverImage !== "https://example.com/hov1-updated.jpg") throw new Error("Incorrect hoverImage update");
|
||||
console.log("✅ PUT verification successful!");
|
||||
|
||||
// 6. GET: Fetch the menu publicly
|
||||
console.log("🔍 Fetching menu via public GET route...");
|
||||
const getRes = await app.handle(
|
||||
new Request(`http://localhost/menus/${TEST_MENU_NAME}`)
|
||||
);
|
||||
|
||||
if (getRes.status !== 200) {
|
||||
const errText = await getRes.text();
|
||||
throw new Error(`GET /menus/:name failed with status ${getRes.status}: ${errText}`);
|
||||
}
|
||||
|
||||
const fetchedMenu = (await getRes.json()) as any;
|
||||
if (fetchedMenu.rootImage !== "https://example.com/root-updated.jpg") throw new Error("Incorrect fetched rootImage");
|
||||
if (fetchedMenu.items[0].columnImage !== "https://example.com/col1-updated.jpg") throw new Error("Incorrect fetched columnImage");
|
||||
if (fetchedMenu.items[0].hoverImage !== "https://example.com/hov1-updated.jpg") throw new Error("Incorrect fetched hoverImage");
|
||||
console.log("✅ GET verification successful!");
|
||||
|
||||
// 7. Cleanup
|
||||
console.log("🧹 Performing final database cleanup...");
|
||||
await prisma.menu.deleteMany({
|
||||
where: { name: TEST_MENU_NAME }
|
||||
});
|
||||
console.log("✨ All Menu Image Integration Tests Passed Successfully!");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error("❌ Test failed:", err);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { spawn } from "child_process";
|
||||
|
||||
async function test() {
|
||||
console.log("Starting MCP server...");
|
||||
const mcp = spawn("bun", ["index.ts"], {
|
||||
cwd: "/home/ti/git/homaroma/apps/mcp-server",
|
||||
env: { ...process.env, MCP_BACKEND_URL: "http://localhost:3051/api" }
|
||||
});
|
||||
|
||||
mcp.stderr.on("data", (data) => {
|
||||
console.error(`[MCP STDERR] ${data}`);
|
||||
});
|
||||
|
||||
let responseData = "";
|
||||
mcp.stdout.on("data", (data) => {
|
||||
responseData += data.toString();
|
||||
});
|
||||
|
||||
const send = (msg: any) => {
|
||||
mcp.stdin.write(JSON.stringify(msg) + "\n");
|
||||
};
|
||||
|
||||
const waitResponse = async (ms: number) => {
|
||||
await new Promise(resolve => setTimeout(resolve, ms));
|
||||
const lines = responseData.trim().split("\n");
|
||||
const lastLine = lines[lines.length - 1];
|
||||
try {
|
||||
return JSON.parse(lastLine);
|
||||
} catch (e) {
|
||||
console.log("Failed to parse JSON line:", lastLine);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 1. Send initialize
|
||||
console.log("Sending initialize request...");
|
||||
send({
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "initialize",
|
||||
params: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "test-client", version: "1.0.0" }
|
||||
}
|
||||
});
|
||||
|
||||
await waitResponse(1000);
|
||||
|
||||
// 2. Send initialized notification
|
||||
console.log("Sending initialized notification...");
|
||||
send({
|
||||
jsonrpc: "2.0",
|
||||
method: "notifications/initialized"
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// 3. Try calling upsert_menu with invalid rootImage (number)
|
||||
console.log("\nSending tools/call for upsert_menu with invalid rootImage (number)...");
|
||||
responseData = "";
|
||||
send({
|
||||
jsonrpc: "2.0",
|
||||
id: 3,
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: "upsert_menu",
|
||||
arguments: {
|
||||
name: "mcp-test-invalid-root",
|
||||
rootImage: 99999,
|
||||
items: [
|
||||
{
|
||||
label: "MCP Item",
|
||||
url: "/item",
|
||||
order: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await waitResponse(2000);
|
||||
console.log("upsert_menu invalid rootImage response:", responseData);
|
||||
|
||||
mcp.kill();
|
||||
}
|
||||
|
||||
test().catch(err => {
|
||||
console.error("MCP Challenger Test failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -2,7 +2,15 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
/* config options here */
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "homaroma.shop",
|
||||
pathname: "/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Reference in new issue
Block a user