Files
GearBox/tests/routes/discovery.test.ts
Jean-Luc Makiola 6e3ce4a31f
All checks were successful
CI / ci (push) Successful in 1m8s
CI / e2e (push) Has been skipped
CI / deploy (push) Successful in 14s
fix: resolve biome lint errors in discovery files
Remove unused functions and imports from route tests, fix array index key
warnings in skeleton components, apply biome formatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:15:58 +02:00

221 lines
6.5 KiB
TypeScript

import { beforeEach, describe, expect, it } from "bun:test";
import { Hono } from "hono";
import { globalItems, setups } from "../../src/db/schema.ts";
import { discoveryRoutes } from "../../src/server/routes/discovery.ts";
import { createTestDb } from "../helpers/db.ts";
type TestDb = Awaited<ReturnType<typeof createTestDb>>;
async function createTestApp() {
const { db, userId } = await createTestDb();
const app = new Hono();
app.use("*", async (c, next) => {
c.set("db", db);
// Note: NO userId set — discovery endpoints don't need auth
await next();
});
app.route("/api/discovery", discoveryRoutes);
return { app, db, userId };
}
async function insertGlobalItem(
db: TestDb["db"],
brand: string,
model: string,
category?: string,
) {
const [row] = await db
.insert(globalItems)
.values({ brand, model, category: category ?? "bags" })
.returning();
return row;
}
async function insertPublicSetup(
db: TestDb["db"],
userId: number,
name: string,
) {
const [row] = await db
.insert(setups)
.values({ name, userId, isPublic: true })
.returning();
return row;
}
describe("Discovery Routes", () => {
let app: Hono;
let db: TestDb["db"];
let userId: number;
beforeEach(async () => {
const testApp = await createTestApp();
app = testApp.app;
db = testApp.db;
userId = testApp.userId;
});
describe("GET /api/discovery/setups", () => {
it("returns 200 with { items, nextCursor, hasMore } shape", async () => {
await insertPublicSetup(db, userId, "My Bikepacking Setup");
const res = await app.request("/api/discovery/setups");
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toHaveProperty("items");
expect(body).toHaveProperty("nextCursor");
expect(body).toHaveProperty("hasMore");
expect(Array.isArray(body.items)).toBe(true);
});
it("returns only public setups", async () => {
await insertPublicSetup(db, userId, "Public Setup");
// Insert a private setup
await db
.insert(setups)
.values({ name: "Private Setup", userId, isPublic: false });
const res = await app.request("/api/discovery/setups");
expect(res.status).toBe(200);
const body = await res.json();
expect(body.items).toHaveLength(1);
expect(body.items[0].name).toBe("Public Setup");
});
it("respects limit query param", async () => {
for (let i = 0; i < 5; i++) {
await insertPublicSetup(db, userId, `Setup ${i}`);
}
const res = await app.request("/api/discovery/setups?limit=2");
expect(res.status).toBe(200);
const body = await res.json();
expect(body.items).toHaveLength(2);
expect(body.hasMore).toBe(true);
});
});
describe("GET /api/discovery/items", () => {
it("returns 200 with { items, nextCursor, hasMore } shape", async () => {
await insertGlobalItem(db, "MSR", "PocketRocket 2");
const res = await app.request("/api/discovery/items");
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toHaveProperty("items");
expect(body).toHaveProperty("nextCursor");
expect(body).toHaveProperty("hasMore");
expect(Array.isArray(body.items)).toBe(true);
});
it("returns 200 with empty items when catalog is empty", async () => {
const res = await app.request("/api/discovery/items");
expect(res.status).toBe(200);
const body = await res.json();
expect(body.items).toHaveLength(0);
expect(body.hasMore).toBe(false);
expect(body.nextCursor).toBeNull();
});
it("respects limit query param", async () => {
for (let i = 0; i < 5; i++) {
await insertGlobalItem(db, `Brand${i}`, `Model${i}`);
}
const res = await app.request("/api/discovery/items?limit=2");
expect(res.status).toBe(200);
const body = await res.json();
expect(body.items).toHaveLength(2);
expect(body.hasMore).toBe(true);
});
it("pagination with cursor returns items before cursor timestamp", async () => {
// Insert items with explicit timestamps so they are definitively ordered
const olderTime = new Date("2024-01-01T00:00:00Z");
const newerTime = new Date("2024-06-01T00:00:00Z");
await db.insert(globalItems).values({
brand: "Brand A",
model: "Model A",
category: "bags",
createdAt: olderTime,
});
await db.insert(globalItems).values({
brand: "Brand B",
model: "Model B",
category: "bags",
createdAt: newerTime,
});
// First page (newest first: Brand B)
const res1 = await app.request("/api/discovery/items?limit=1");
expect(res1.status).toBe(200);
const page1 = await res1.json();
expect(page1.items).toHaveLength(1);
expect(page1.items[0].brand).toBe("Brand B");
expect(page1.hasMore).toBe(true);
expect(page1.nextCursor).not.toBeNull();
// Second page using cursor (should return Brand A)
const cursor = encodeURIComponent(page1.nextCursor);
const res2 = await app.request(
`/api/discovery/items?limit=1&cursor=${cursor}`,
);
expect(res2.status).toBe(200);
const page2 = await res2.json();
expect(page2.items).toHaveLength(1);
expect(page2.items[0].brand).toBe("Brand A");
});
});
describe("GET /api/discovery/categories", () => {
it("returns 200 with array shape", async () => {
await insertGlobalItem(db, "MSR", "PocketRocket", "cooking");
await insertGlobalItem(db, "Revelate", "Terrapin", "bags");
const res = await app.request("/api/discovery/categories");
expect(res.status).toBe(200);
const body = await res.json();
expect(Array.isArray(body)).toBe(true);
});
it("returns categories with name and itemCount fields", async () => {
await insertGlobalItem(db, "MSR", "PocketRocket", "cooking");
await insertGlobalItem(db, "Jetboil", "Flash", "cooking");
await insertGlobalItem(db, "Revelate", "Terrapin", "bags");
const res = await app.request("/api/discovery/categories");
expect(res.status).toBe(200);
const body = await res.json();
expect(body.length).toBeGreaterThan(0);
expect(body[0]).toHaveProperty("name");
expect(body[0]).toHaveProperty("itemCount");
// cooking has most items (2), should be first
expect(body[0].name).toBe("cooking");
expect(body[0].itemCount).toBe(2);
});
it("respects limit query param", async () => {
await insertGlobalItem(db, "Brand A", "Model A", "category-a");
await insertGlobalItem(db, "Brand B", "Model B", "category-b");
await insertGlobalItem(db, "Brand C", "Model C", "category-c");
const res = await app.request("/api/discovery/categories?limit=2");
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toHaveLength(2);
});
});
});