test(26-01): add failing tests for discovery service

- getPopularSetups: ordering, privacy filter, cursor pagination, creatorName
- getRecentGlobalItems: ordering, cursor pagination, second page deduplication
- getTrendingCategories: ordering by count desc, null category exclusion, empty state
This commit is contained in:
2026-04-10 14:53:09 +02:00
parent 2f88ead599
commit 06b6e935f2

View File

@@ -0,0 +1,246 @@
import { beforeEach, describe, expect, it } from "bun:test";
import { eq } from "drizzle-orm";
import {
globalItems,
items,
setups,
setupItems,
users,
} from "../../src/db/schema.ts";
import {
getPopularSetups,
getRecentGlobalItems,
getTrendingCategories,
} from "../../src/server/services/discovery.service.ts";
import { createTestDb } from "../helpers/db.ts";
type TestDb = Awaited<ReturnType<typeof createTestDb>>;
async function insertGlobalItem(
db: TestDb["db"],
data: { brand: string; model: string; category?: string },
) {
const [row] = await db
.insert(globalItems)
.values({
brand: data.brand,
model: data.model,
category: data.category ?? null,
})
.returning();
return row;
}
async function insertItem(
db: TestDb["db"],
userId: number,
categoryId = 1,
) {
const [row] = await db
.insert(items)
.values({ name: "Test Item", categoryId, userId })
.returning();
return row;
}
async function insertPublicSetup(
db: TestDb["db"],
userId: number,
name: string,
itemIds: number[],
) {
const [setup] = await db
.insert(setups)
.values({ name, userId, isPublic: true })
.returning();
for (const itemId of itemIds) {
await db.insert(setupItems).values({ setupId: setup.id, itemId });
}
return setup;
}
async function insertPrivateSetup(
db: TestDb["db"],
userId: number,
name: string,
) {
const [setup] = await db
.insert(setups)
.values({ name, userId, isPublic: false })
.returning();
return setup;
}
describe("Discovery Service", () => {
let db: TestDb["db"];
let userId: number;
beforeEach(async () => {
const testDb = await createTestDb();
db = testDb.db;
userId = testDb.userId;
});
describe("getPopularSetups", () => {
it("returns public setups ordered by item count descending", async () => {
const item1 = await insertItem(db, userId);
const item2 = await insertItem(db, userId);
const item3 = await insertItem(db, userId);
// Setup with 1 item
await insertPublicSetup(db, userId, "Solo Setup", [item1.id]);
// Setup with 2 items
await insertPublicSetup(db, userId, "Dual Setup", [item2.id, item3.id]);
const result = await getPopularSetups(db);
expect(result.items).toHaveLength(2);
expect(result.items[0].name).toBe("Dual Setup");
expect(result.items[0].itemCount).toBe(2);
expect(result.items[1].name).toBe("Solo Setup");
expect(result.items[1].itemCount).toBe(1);
});
it("excludes private setups", async () => {
const item1 = await insertItem(db, userId);
await insertPublicSetup(db, userId, "Public Setup", [item1.id]);
await insertPrivateSetup(db, userId, "Private Setup");
const result = await getPopularSetups(db);
expect(result.items).toHaveLength(1);
expect(result.items[0].name).toBe("Public Setup");
});
it("returns hasMore=true and nextCursor when more results exist", async () => {
const item1 = await insertItem(db, userId);
const item2 = await insertItem(db, userId);
const item3 = await insertItem(db, userId);
await insertPublicSetup(db, userId, "Setup A", [item1.id, item2.id, item3.id]);
await insertPublicSetup(db, userId, "Setup B", [item1.id, item2.id]);
await insertPublicSetup(db, userId, "Setup C", [item1.id]);
const result = await getPopularSetups(db, 1);
expect(result.hasMore).toBe(true);
expect(result.nextCursor).not.toBeNull();
});
it("returns second page without duplicates when cursor provided", async () => {
const item1 = await insertItem(db, userId);
const item2 = await insertItem(db, userId);
const item3 = await insertItem(db, userId);
await insertPublicSetup(db, userId, "Setup A", [item1.id, item2.id, item3.id]);
await insertPublicSetup(db, userId, "Setup B", [item1.id, item2.id]);
await insertPublicSetup(db, userId, "Setup C", [item1.id]);
const page1 = await getPopularSetups(db, 1);
expect(page1.items).toHaveLength(1);
expect(page1.nextCursor).not.toBeNull();
const page2 = await getPopularSetups(db, 1, page1.nextCursor!);
expect(page2.items).toHaveLength(1);
expect(page2.items[0].id).not.toBe(page1.items[0].id);
});
it("includes creatorName from users.displayName", async () => {
// Update user display name
await db.update(users)
.set({ displayName: "Jean-Luc" })
.where(eq(users.id, userId));
const item1 = await insertItem(db, userId);
await insertPublicSetup(db, userId, "My Setup", [item1.id]);
const result = await getPopularSetups(db);
expect(result.items).toHaveLength(1);
expect(result.items[0].creatorName).toBe("Jean-Luc");
});
});
describe("getRecentGlobalItems", () => {
it("returns items ordered by createdAt descending", async () => {
// Insert items with slight delay to get different timestamps
const item1 = await insertGlobalItem(db, { brand: "BrandA", model: "Model1" });
await new Promise((r) => setTimeout(r, 5));
const item2 = await insertGlobalItem(db, { brand: "BrandB", model: "Model2" });
await new Promise((r) => setTimeout(r, 5));
const item3 = await insertGlobalItem(db, { brand: "BrandC", model: "Model3" });
const result = await getRecentGlobalItems(db);
expect(result.items).toHaveLength(3);
// Most recent first
expect(result.items[0].id).toBe(item3.id);
expect(result.items[1].id).toBe(item2.id);
expect(result.items[2].id).toBe(item1.id);
});
it("returns hasMore=true and nextCursor when more results exist", async () => {
await insertGlobalItem(db, { brand: "BrandA", model: "Model1" });
await new Promise((r) => setTimeout(r, 5));
await insertGlobalItem(db, { brand: "BrandB", model: "Model2" });
await new Promise((r) => setTimeout(r, 5));
await insertGlobalItem(db, { brand: "BrandC", model: "Model3" });
const result = await getRecentGlobalItems(db, 2);
expect(result.hasMore).toBe(true);
expect(result.nextCursor).not.toBeNull();
});
it("returns second page without duplicates when cursor provided", async () => {
await insertGlobalItem(db, { brand: "BrandA", model: "Model1" });
await new Promise((r) => setTimeout(r, 5));
await insertGlobalItem(db, { brand: "BrandB", model: "Model2" });
await new Promise((r) => setTimeout(r, 5));
await insertGlobalItem(db, { brand: "BrandC", model: "Model3" });
const page1 = await getRecentGlobalItems(db, 2);
expect(page1.items).toHaveLength(2);
expect(page1.nextCursor).not.toBeNull();
const page2 = await getRecentGlobalItems(db, 2, page1.nextCursor!);
expect(page2.items).toHaveLength(1);
// Page 2 item should not appear in page 1
const page1Ids = page1.items.map((i) => i.id);
expect(page1Ids).not.toContain(page2.items[0].id);
});
});
describe("getTrendingCategories", () => {
it("returns categories ordered by item count descending", async () => {
// 3 items in Tents, 1 in Bags, 2 in Stoves
await insertGlobalItem(db, { brand: "BrandA", model: "Tent1", category: "Tents" });
await insertGlobalItem(db, { brand: "BrandB", model: "Tent2", category: "Tents" });
await insertGlobalItem(db, { brand: "BrandC", model: "Tent3", category: "Tents" });
await insertGlobalItem(db, { brand: "BrandD", model: "Bag1", category: "Bags" });
await insertGlobalItem(db, { brand: "BrandE", model: "Stove1", category: "Stoves" });
await insertGlobalItem(db, { brand: "BrandF", model: "Stove2", category: "Stoves" });
const result = await getTrendingCategories(db);
expect(result).toHaveLength(3);
expect(result[0].name).toBe("Tents");
expect(result[0].itemCount).toBe(3);
expect(result[1].name).toBe("Stoves");
expect(result[1].itemCount).toBe(2);
expect(result[2].name).toBe("Bags");
expect(result[2].itemCount).toBe(1);
});
it("excludes items with null category", async () => {
await insertGlobalItem(db, { brand: "BrandA", model: "Tent1", category: "Tents" });
// No category — should be excluded
await insertGlobalItem(db, { brand: "BrandB", model: "NoCategory" });
const result = await getTrendingCategories(db);
expect(result).toHaveLength(1);
expect(result[0].name).toBe("Tents");
});
it("returns empty array when no items have a category set", async () => {
await insertGlobalItem(db, { brand: "BrandA", model: "Model1" });
await insertGlobalItem(db, { brand: "BrandB", model: "Model2" });
const result = await getTrendingCategories(db);
expect(result).toHaveLength(0);
});
});
});