148 lines
4.9 KiB
TypeScript
148 lines
4.9 KiB
TypeScript
import "./setup-env.js";
|
|
import { describe, expect, it, beforeAll, afterAll } from "vitest";
|
|
import { eq, inArray } from "drizzle-orm";
|
|
import { buildServer } from "../src/server.js";
|
|
import { loadConfig } from "../src/config.js";
|
|
import { createDatabase, closeDatabase, schema } from "@app/database";
|
|
|
|
describe("GET /v1/inventory/natural-search", () => {
|
|
const testDb = createDatabase(process.env.TEST_DATABASE_URL!);
|
|
const config = loadConfig();
|
|
let app: Awaited<ReturnType<typeof buildServer>>;
|
|
|
|
beforeAll(async () => {
|
|
app = await buildServer(config);
|
|
await app.ready();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await closeDatabase();
|
|
await app.close();
|
|
});
|
|
|
|
async function registerUser(email: string) {
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/v1/auth/register",
|
|
payload: { email, password: "Password123!", displayName: "Search Test" },
|
|
});
|
|
const body = JSON.parse(res.body) as { accessToken: string };
|
|
const token = body.accessToken;
|
|
const userId = (JSON.parse(atob(token.split(".")[1]!)) as { sub: string }).sub;
|
|
|
|
await app.inject({
|
|
method: "POST",
|
|
url: "/v1/onboarding/quick-start",
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: { goals: ["less_waste"], precisionMode: "simple" },
|
|
});
|
|
|
|
return { token, userId };
|
|
}
|
|
|
|
async function cleanupUser(email: string) {
|
|
const existing = await testDb.db
|
|
.select({ id: schema.users.id })
|
|
.from(schema.users)
|
|
.where(eq(schema.users.email, email));
|
|
for (const u of existing) {
|
|
const memberships = await testDb.db
|
|
.select({ householdId: schema.householdMembers.householdId })
|
|
.from(schema.householdMembers)
|
|
.where(eq(schema.householdMembers.userId, u.id));
|
|
const householdIds = memberships.map((m) => m.householdId);
|
|
if (householdIds.length > 0) {
|
|
await testDb.db
|
|
.delete(schema.inventoryItems)
|
|
.where(inArray(schema.inventoryItems.householdId, householdIds));
|
|
await testDb.db
|
|
.delete(schema.storageLocations)
|
|
.where(inArray(schema.storageLocations.householdId, householdIds));
|
|
await testDb.db
|
|
.delete(schema.householdMembers)
|
|
.where(inArray(schema.householdMembers.householdId, householdIds));
|
|
await testDb.db
|
|
.delete(schema.households)
|
|
.where(inArray(schema.households.id, householdIds));
|
|
}
|
|
await testDb.db.delete(schema.users).where(eq(schema.users.id, u.id));
|
|
}
|
|
}
|
|
|
|
it("returnerar ett mänskligt svar med plats och trust-hedge", async () => {
|
|
const email = "inventory-search-1@example.invalid";
|
|
await cleanupUser(email);
|
|
const { token, userId } = await registerUser(email);
|
|
|
|
const [household] = await testDb.db
|
|
.select({ id: schema.households.id })
|
|
.from(schema.households)
|
|
.innerJoin(
|
|
schema.householdMembers,
|
|
eq(schema.households.id, schema.householdMembers.householdId),
|
|
)
|
|
.where(eq(schema.householdMembers.userId, userId))
|
|
.limit(1);
|
|
|
|
const [fridge] = await testDb.db
|
|
.insert(schema.storageLocations)
|
|
.values({
|
|
householdId: household!.id,
|
|
type: "fridge",
|
|
name: "Kylen",
|
|
sublocations: ["mellanmålshyllan"],
|
|
})
|
|
.returning();
|
|
|
|
await testDb.db.insert(schema.inventoryItems).values({
|
|
householdId: household!.id,
|
|
displayName: "Mjölk",
|
|
quantity: 1,
|
|
unit: "LITER",
|
|
storageLocationId: fridge!.id,
|
|
sublocation: "mellanmålshyllan",
|
|
source: "manual_search",
|
|
confidence: 1,
|
|
verifiedByUser: true,
|
|
lastVerifiedAt: new Date(),
|
|
trustState: "trusted",
|
|
});
|
|
|
|
const res = await app.inject({
|
|
method: "GET",
|
|
url: "/v1/inventory/natural-search?q=mj%C3%B6lk&languageTag=sv-SE",
|
|
headers: { authorization: `Bearer ${token}` },
|
|
});
|
|
expect(res.statusCode).toBe(200);
|
|
|
|
const body = JSON.parse(res.body) as { response: string; items: unknown[] };
|
|
expect(body.response).toContain("Mjölk");
|
|
expect(body.response).toContain("Kylen");
|
|
expect(body.response).toContain("mellanmålshyllan");
|
|
expect(body.response).toContain("säker");
|
|
expect(body.items).toHaveLength(1);
|
|
|
|
await cleanupUser(email);
|
|
});
|
|
|
|
it("hanterar noll träffar på flera språk", async () => {
|
|
const email = "inventory-search-2@example.invalid";
|
|
await cleanupUser(email);
|
|
const { token } = await registerUser(email);
|
|
|
|
for (const languageTag of ["sv-SE", "en-GB", "de-DE"]) {
|
|
const res = await app.inject({
|
|
method: "GET",
|
|
url: `/v1/inventory/natural-search?q=unobtainium&languageTag=${encodeURIComponent(languageTag)}`,
|
|
headers: { authorization: `Bearer ${token}` },
|
|
});
|
|
expect(res.statusCode).toBe(200);
|
|
const body = JSON.parse(res.body) as { response: string; items: unknown[] };
|
|
expect(body.items).toHaveLength(0);
|
|
expect(body.response.length).toBeGreaterThan(10);
|
|
}
|
|
|
|
await cleanupUser(email);
|
|
});
|
|
});
|