- Add auth redirect in root layout for unauthenticated users - Proxy OIDC routes (/login, /callback, /logout) through Vite dev server - Strip Secure flag from OIDC cookies in dev mode (HTTP localhost) - Disable retry on auth query to prevent stale cookie loops - Fix SQLite .get()/.all()/.run() calls in category and global-item services for PostgreSQL compatibility - Add userId scoping to category service functions - Add OIDC error logging in auth middleware - Apply linter auto-formatting across affected files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
196 lines
5.2 KiB
TypeScript
196 lines
5.2 KiB
TypeScript
import { beforeEach, describe, expect, it } from "bun:test";
|
|
import { globalItems, itemGlobalLinks, items } from "../../src/db/schema.ts";
|
|
import { seedGlobalItems } from "../../src/db/seed-global-items.ts";
|
|
import {
|
|
getGlobalItemWithOwnerCount,
|
|
linkItemToGlobal,
|
|
searchGlobalItems,
|
|
unlinkItemFromGlobal,
|
|
} from "../../src/server/services/global-item.service.ts";
|
|
import { createTestDb } from "../helpers/db.ts";
|
|
|
|
type TestDb = ReturnType<typeof createTestDb>;
|
|
|
|
function insertGlobalItem(
|
|
db: TestDb,
|
|
data: {
|
|
brand: string;
|
|
model: string;
|
|
category?: string;
|
|
weightGrams?: number;
|
|
priceCents?: number;
|
|
},
|
|
) {
|
|
return db
|
|
.insert(globalItems)
|
|
.values({
|
|
brand: data.brand,
|
|
model: data.model,
|
|
category: data.category ?? null,
|
|
weightGrams: data.weightGrams ?? null,
|
|
priceCents: data.priceCents ?? null,
|
|
})
|
|
.returning()
|
|
.get();
|
|
}
|
|
|
|
function insertItem(db: TestDb, name: string) {
|
|
return db.insert(items).values({ name, categoryId: 1 }).returning().get();
|
|
}
|
|
|
|
describe("Global Item Service", () => {
|
|
let db: TestDb;
|
|
|
|
beforeEach(() => {
|
|
db = createTestDb();
|
|
});
|
|
|
|
describe("searchGlobalItems", () => {
|
|
it("returns all global items when no query provided", () => {
|
|
insertGlobalItem(db, {
|
|
brand: "Revelate Designs",
|
|
model: "Terrapin System",
|
|
});
|
|
insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack" });
|
|
|
|
const results = searchGlobalItems(db);
|
|
expect(results).toHaveLength(2);
|
|
});
|
|
|
|
it("returns items matching brand (case-insensitive)", () => {
|
|
insertGlobalItem(db, {
|
|
brand: "Revelate Designs",
|
|
model: "Terrapin System",
|
|
});
|
|
insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack" });
|
|
|
|
const results = searchGlobalItems(db, "revelate");
|
|
expect(results).toHaveLength(1);
|
|
expect(results[0].brand).toBe("Revelate Designs");
|
|
});
|
|
|
|
it("returns items matching model (case-insensitive)", () => {
|
|
insertGlobalItem(db, {
|
|
brand: "Revelate Designs",
|
|
model: "Terrapin System",
|
|
});
|
|
insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack" });
|
|
|
|
const results = searchGlobalItems(db, "HANDLEBAR");
|
|
expect(results).toHaveLength(1);
|
|
expect(results[0].model).toBe("Handlebar Pack");
|
|
});
|
|
|
|
it("does not match everything with wildcard chars", () => {
|
|
insertGlobalItem(db, {
|
|
brand: "Revelate Designs",
|
|
model: "Terrapin System",
|
|
});
|
|
insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack" });
|
|
|
|
const results = searchGlobalItems(db, "100%");
|
|
expect(results).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("getGlobalItemWithOwnerCount", () => {
|
|
it("returns item with ownerCount 0 when no links", () => {
|
|
const gi = insertGlobalItem(db, {
|
|
brand: "MSR",
|
|
model: "PocketRocket 2",
|
|
});
|
|
|
|
const result = getGlobalItemWithOwnerCount(db, gi.id);
|
|
expect(result).not.toBeNull();
|
|
expect(result!.ownerCount).toBe(0);
|
|
expect(result!.brand).toBe("MSR");
|
|
});
|
|
|
|
it("returns ownerCount matching number of linked items", () => {
|
|
const gi = insertGlobalItem(db, {
|
|
brand: "MSR",
|
|
model: "PocketRocket 2",
|
|
});
|
|
const item1 = insertItem(db, "My Stove");
|
|
const item2 = insertItem(db, "Another Stove");
|
|
|
|
db.insert(itemGlobalLinks)
|
|
.values({ itemId: item1.id, globalItemId: gi.id })
|
|
.run();
|
|
db.insert(itemGlobalLinks)
|
|
.values({ itemId: item2.id, globalItemId: gi.id })
|
|
.run();
|
|
|
|
const result = getGlobalItemWithOwnerCount(db, gi.id);
|
|
expect(result).not.toBeNull();
|
|
expect(result!.ownerCount).toBe(2);
|
|
});
|
|
|
|
it("returns null for non-existent id", () => {
|
|
const result = getGlobalItemWithOwnerCount(db, 9999);
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("linkItemToGlobal", () => {
|
|
it("creates link and returns link row", () => {
|
|
const gi = insertGlobalItem(db, {
|
|
brand: "MSR",
|
|
model: "PocketRocket 2",
|
|
});
|
|
const item = insertItem(db, "My Stove");
|
|
|
|
const link = linkItemToGlobal(db, item.id, gi.id);
|
|
expect(link.itemId).toBe(item.id);
|
|
expect(link.globalItemId).toBe(gi.id);
|
|
});
|
|
|
|
it("throws when item already linked", () => {
|
|
const gi = insertGlobalItem(db, {
|
|
brand: "MSR",
|
|
model: "PocketRocket 2",
|
|
});
|
|
const item = insertItem(db, "My Stove");
|
|
|
|
linkItemToGlobal(db, item.id, gi.id);
|
|
expect(() => linkItemToGlobal(db, item.id, gi.id)).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("unlinkItemFromGlobal", () => {
|
|
it("removes the link", () => {
|
|
const gi = insertGlobalItem(db, {
|
|
brand: "MSR",
|
|
model: "PocketRocket 2",
|
|
});
|
|
const item = insertItem(db, "My Stove");
|
|
|
|
linkItemToGlobal(db, item.id, gi.id);
|
|
const deleted = unlinkItemFromGlobal(db, item.id);
|
|
expect(deleted).toBe(1);
|
|
|
|
// Verify link is gone
|
|
const result = getGlobalItemWithOwnerCount(db, gi.id);
|
|
expect(result!.ownerCount).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("seedGlobalItems", () => {
|
|
it("inserts seed data on first call", () => {
|
|
seedGlobalItems(db);
|
|
const all = db.select().from(globalItems).all();
|
|
expect(all.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("is idempotent on second call", () => {
|
|
seedGlobalItems(db);
|
|
const countAfterFirst = db.select().from(globalItems).all().length;
|
|
|
|
seedGlobalItems(db);
|
|
const countAfterSecond = db.select().from(globalItems).all().length;
|
|
|
|
expect(countAfterSecond).toBe(countAfterFirst);
|
|
});
|
|
});
|
|
});
|