import { beforeEach, describe, expect, it } from "vitest"; // Covers every /api/bot/* route — these are authed by a shared secret // header (src/lib/bot-auth.ts), not an Auth.js session, since the caller is // the inventory-capture Telegram bot service, not a browser. No @/auth // mock needed here (unlike tests/employees.test.ts and friends). process.env.BOT_INGEST_SECRET = "test-bot-secret"; import { GET as getCatalog } from "@/app/api/bot/catalog/route"; import { GET as getBotAreas, POST as postBotAreas } from "@/app/api/bot/areas/route"; import { GET as getBotEmployees, POST as postBotEmployees } from "@/app/api/bot/employees/route"; import { POST as postBotAssetModel } from "@/app/api/bot/asset-models/route"; import { POST as postBotInventoryScan } from "@/app/api/bot/inventory-scans/route"; import { prisma } from "@/lib/prisma"; const AUTH_HEADER = { authorization: "Bearer test-bot-secret" }; function req(url: string, opts: { method?: string; body?: unknown; auth?: boolean } = {}): Request { return new Request(`http://localhost${url}`, { method: opts.method ?? "GET", headers: { "content-type": "application/json", ...(opts.auth === false ? {} : AUTH_HEADER), }, ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}), }); } async function seedCategoryAndModel() { const category = await prisma.assetCategory.create({ data: { name: "Workstation", valueGroup: "high_value" }, }); await prisma.assetCategoryAttributeDef.create({ data: { categoryId: category.id, key: "cpu", label: "CPU", dataType: "string", required: false, sortOrder: 1 }, }); const model = await prisma.assetModel.create({ data: { categoryId: category.id, manufacturer: "HP", name: "Z4 G4", specs: {} }, }); return { category, model }; } beforeEach(async () => { await prisma.inventoryScan.deleteMany(); await prisma.assetCategoryAttributeDef.deleteMany(); await prisma.assetModel.deleteMany(); await prisma.assetCategory.deleteMany(); await prisma.area.deleteMany(); await prisma.employee.deleteMany(); }); describe("GET /api/bot/catalog", () => { it("returns categories with nested models and attribute defs", async () => { await seedCategoryAndModel(); const res = await getCatalog(req("/api/bot/catalog")); expect(res.status).toBe(200); const body = await res.json(); expect(body.categories).toHaveLength(1); expect(body.categories[0]).toMatchObject({ name: "Workstation", models: [{ manufacturer: "HP", name: "Z4 G4" }], attribute_defs: [{ key: "cpu", label: "CPU", data_type: "string" }], }); }); it("returns 401 with a wrong secret", async () => { const res = await getCatalog( new Request("http://localhost/api/bot/catalog", { headers: { authorization: "Bearer wrong-secret" }, }) ); expect(res.status).toBe(401); }); it("returns 401 with no auth header", async () => { const res = await getCatalog(req("/api/bot/catalog", { auth: false })); expect(res.status).toBe(401); }); }); describe("/api/bot/areas", () => { it("POST creates an area, and re-posting the same name returns the same row (idempotent)", async () => { const first = await postBotAreas(req("/api/bot/areas", { method: "POST", body: { name: "Seat 12" } })); expect(first.status).toBe(201); const firstBody = await first.json(); const second = await postBotAreas(req("/api/bot/areas", { method: "POST", body: { name: "Seat 12" } })); const secondBody = await second.json(); expect(secondBody.id).toBe(firstBody.id); }); it("GET lists areas", async () => { await postBotAreas(req("/api/bot/areas", { method: "POST", body: { name: "Seat 12" } })); const res = await getBotAreas(req("/api/bot/areas")); const body = await res.json(); expect(body.areas).toHaveLength(1); }); it("POST returns 401 without the bot secret", async () => { const res = await postBotAreas( req("/api/bot/areas", { method: "POST", body: { name: "Seat 12" }, auth: false }) ); expect(res.status).toBe(401); }); }); describe("/api/bot/employees", () => { it("POST creates an employee, defaulting department when omitted", async () => { const res = await postBotEmployees( req("/api/bot/employees", { method: "POST", body: { name: "Toan Hoang" } }) ); expect(res.status).toBe(201); const body = await res.json(); expect(body.name).toBe("Toan Hoang"); expect(body.department).toBeTruthy(); }); it("GET lists employees", async () => { await postBotEmployees(req("/api/bot/employees", { method: "POST", body: { name: "Toan Hoang" } })); const res = await getBotEmployees(req("/api/bot/employees")); const body = await res.json(); expect(body.employees).toHaveLength(1); }); it("POST returns 401 without the bot secret", async () => { const res = await postBotEmployees( req("/api/bot/employees", { method: "POST", body: { name: "Toan" }, auth: false }) ); expect(res.status).toBe(401); }); }); describe("POST /api/bot/asset-models", () => { it("creates a model within an existing high_value category", async () => { const { category } = await seedCategoryAndModel(); const res = await postBotAssetModel( req("/api/bot/asset-models", { method: "POST", body: { categoryId: category.id, manufacturer: "Lenovo", name: "P620" }, }) ); expect(res.status).toBe(201); const body = await res.json(); expect(body).toMatchObject({ manufacturer: "Lenovo", name: "P620" }); }); it("returns 401 without the bot secret", async () => { const { category } = await seedCategoryAndModel(); const res = await postBotAssetModel( req("/api/bot/asset-models", { method: "POST", body: { categoryId: category.id, manufacturer: "Lenovo", name: "P620" }, auth: false, }) ); expect(res.status).toBe(401); }); }); describe("POST /api/bot/inventory-scans", () => { it("creates a scan with specs, asset tag and serial number", async () => { const { category, model } = await seedCategoryAndModel(); const res = await postBotInventoryScan( req("/api/bot/inventory-scans", { method: "POST", body: { categoryId: category.id, modelId: model.id, assetTag: "TAG-1", serialNumber: "SN-1", specs: { cpu: "i7" }, telegramChatId: "12345", }, }) ); expect(res.status).toBe(201); const body = await res.json(); expect(body).toMatchObject({ category_name: "Workstation", model_name: "Z4 G4", asset_tag: "TAG-1", serial_number: "SN-1", specs: { cpu: "i7" }, }); }); it("rejects a categoryId/modelId that doesn't reference an existing row", async () => { const res = await postBotInventoryScan( req("/api/bot/inventory-scans", { method: "POST", body: { categoryId: "00000000-0000-0000-0000-000000000000", modelId: "00000000-0000-0000-0000-000000000000", }, }) ); expect(res.status).toBe(400); }); it("returns 401 without the bot secret", async () => { const { category, model } = await seedCategoryAndModel(); const res = await postBotInventoryScan( req("/api/bot/inventory-scans", { method: "POST", body: { categoryId: category.id, modelId: model.id }, auth: false, }) ); expect(res.status).toBe(401); expect(await prisma.inventoryScan.findMany()).toHaveLength(0); }); });