test(25-01): add failing tests for upsertGlobalItem and bulkUpsertGlobalItems

- Import upsertGlobalItem and bulkUpsertGlobalItems (not yet exported)
- Tests cover: create, conflict update, attribution fields, tag sync
- Tests cover: empty tags clear, tags omitted leaves untouched
- Tests cover: bulk upsert counts (created vs updated)
This commit is contained in:
2026-04-10 10:56:54 +02:00
parent 39ef9cc433
commit 9093a2c8f6

View File

@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it } from "bun:test";
import { eq } from "drizzle-orm";
import {
globalItems,
globalItemTags,
@@ -7,8 +8,10 @@ import {
} from "../../src/db/schema.ts";
import { seedGlobalItems } from "../../src/db/seed-global-items.ts";
import {
bulkUpsertGlobalItems,
getGlobalItemWithOwnerCount,
searchGlobalItems,
upsertGlobalItem,
} from "../../src/server/services/global-item.service.ts";
import { createTestDb } from "../helpers/db.ts";
@@ -263,4 +266,153 @@ describe("Global Item Service", () => {
expect(countAfterSecond).toBe(countAfterFirst);
});
});
describe("upsert operations", () => {
it("upsertGlobalItem creates new item and returns { item, created: true }", async () => {
const result = await upsertGlobalItem(db, {
brand: "Revelate Designs",
model: "Terrapin System",
category: "Bags",
weightGrams: 210,
});
expect(result.created).toBe(true);
expect(result.item.id).toBeDefined();
expect(result.item.brand).toBe("Revelate Designs");
expect(result.item.model).toBe("Terrapin System");
});
it("upsertGlobalItem updates existing item on (brand, model) conflict and returns { item, created: false }", async () => {
await upsertGlobalItem(db, {
brand: "MSR",
model: "PocketRocket 2",
weightGrams: 83,
});
const second = await upsertGlobalItem(db, {
brand: "MSR",
model: "PocketRocket 2",
weightGrams: 90,
});
expect(second.created).toBe(false);
expect(second.item.weightGrams).toBe(90);
// Only one row should exist
const all = await db.select().from(globalItems);
expect(all).toHaveLength(1);
});
it("upsertGlobalItem persists sourceUrl, imageCredit, imageSourceUrl", async () => {
const result = await upsertGlobalItem(db, {
brand: "Apidura",
model: "Handlebar Pack",
sourceUrl: "https://apidura.com/shop/handlebar-pack/",
imageCredit: "Apidura Ltd",
imageSourceUrl: "https://apidura.com/images/handlebar-pack.jpg",
});
expect(result.item.sourceUrl).toBe("https://apidura.com/shop/handlebar-pack/");
expect(result.item.imageCredit).toBe("Apidura Ltd");
expect(result.item.imageSourceUrl).toBe("https://apidura.com/images/handlebar-pack.jpg");
});
it("upsertGlobalItem with tags creates tags and links them", async () => {
const result = await upsertGlobalItem(db, {
brand: "Therm-a-Rest",
model: "NeoAir XLite",
tags: ["sleeping-pad", "ultralight"],
});
expect(result.created).toBe(true);
const linkedTags = await db
.select({ name: tags.name })
.from(globalItemTags)
.innerJoin(tags, eq(globalItemTags.tagId, tags.id))
.where(eq(globalItemTags.globalItemId, result.item.id));
expect(linkedTags).toHaveLength(2);
const tagNames = linkedTags.map((t) => t.name).sort();
expect(tagNames).toEqual(["sleeping-pad", "ultralight"]);
});
it("upsertGlobalItem without tags leaves existing tags untouched", async () => {
// Create item with tags
const first = await upsertGlobalItem(db, {
brand: "Sea to Summit",
model: "Spark III",
tags: ["sleeping-bag"],
});
// Upsert without tags
await upsertGlobalItem(db, {
brand: "Sea to Summit",
model: "Spark III",
weightGrams: 450,
});
// Tags should remain
const linkedTags = await db
.select()
.from(globalItemTags)
.where(eq(globalItemTags.globalItemId, first.item.id));
expect(linkedTags).toHaveLength(1);
});
it("upsertGlobalItem with empty tags array clears existing tags", async () => {
// Create item with tags
const first = await upsertGlobalItem(db, {
brand: "Big Agnes",
model: "Copper Spur HV UL2",
tags: ["tent", "ultralight"],
});
// Upsert with empty tags
await upsertGlobalItem(db, {
brand: "Big Agnes",
model: "Copper Spur HV UL2",
tags: [],
});
// Tags should be cleared
const linkedTags = await db
.select()
.from(globalItemTags)
.where(eq(globalItemTags.globalItemId, first.item.id));
expect(linkedTags).toHaveLength(0);
});
it("bulkUpsertGlobalItems processes array and returns correct created/updated counts", async () => {
const result = await bulkUpsertGlobalItems(db, [
{ brand: "Petzl", model: "Actik Core", weightGrams: 87 },
{ brand: "Black Diamond", model: "Spot 400", weightGrams: 95 },
{ brand: "Black Diamond", model: "Spot 350", weightGrams: 90 },
]);
expect(result.created).toBe(3);
expect(result.updated).toBe(0);
expect(result.items).toHaveLength(3);
});
it("bulkUpsertGlobalItems handles mix of new and existing items", async () => {
// Pre-insert one item
await upsertGlobalItem(db, {
brand: "Petzl",
model: "Actik Core",
weightGrams: 87,
});
const result = await bulkUpsertGlobalItems(db, [
{ brand: "Petzl", model: "Actik Core", weightGrams: 90 }, // existing
{ brand: "Black Diamond", model: "Spot 400", weightGrams: 95 }, // new
]);
expect(result.created).toBe(1);
expect(result.updated).toBe(1);
expect(result.items).toHaveLength(2);
});
});
});