Files
ubi-dan-itam/tests/bot-api.test.ts
toanhoangandClaude Sonnet 5 84e6db7406 test: add Area, InventoryScan and bot API tests
369/369 tests pass (35 new), tsc --noEmit clean, next lint clean. Not yet run through the repos code-reviewer subagent gate (this session operated via SSH, cwd was not the project dir so .claude/agents/ subagents were not recognized) -- see CLAUDE.md note.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:07:32 -04:00

215 lines
7.4 KiB
TypeScript

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);
});
});