Files
the-pet-loft/convex/wishlists.test.ts
ianshaloom cc15338ad9 feat: initial commit — storefront, convex backend, and shared packages
Completes the first milestone of The Pet Loft ecommerce platform:
- apps/storefront: full customer-facing Next.js app with HeroUI (cart,
  checkout, orders, wishlist, product detail, shop, search, auth)
- convex/: serverless backend with schema, queries, mutations, actions,
  HTTP routes, Stripe/Shippo integrations, and co-located tests
- packages/types, packages/utils, packages/convex: shared workspace packages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:31:18 +03:00

167 lines
5.2 KiB
TypeScript

import { convexTest } from "convex-test";
import { describe, it, expect } from "vitest";
import { api } from "./_generated/api";
import schema from "./schema";
const modules = import.meta.glob("./**/*.ts");
async function setupAdminUser(t: ReturnType<typeof convexTest>) {
const asAdmin = t.withIdentity({
name: "Admin",
email: "admin@example.com",
subject: "clerk_admin_123",
});
const userId = await asAdmin.mutation(api.users.store, {});
await t.run(async (ctx) => {
await ctx.db.patch(userId, { role: "admin" });
});
return asAdmin;
}
async function setupCategory(t: ReturnType<typeof convexTest>) {
let categoryId: import("./_generated/dataModel").Id<"categories">;
await t.run(async (ctx) => {
categoryId = await ctx.db.insert("categories", {
name: "Pet Food",
slug: "pet-food",
});
});
return categoryId!;
}
async function setupProduct(
t: ReturnType<typeof convexTest>,
asAdmin: ReturnType<ReturnType<typeof convexTest>["withIdentity"]>,
) {
const categoryId = await setupCategory(t);
return await asAdmin.mutation(api.products.create, {
name: "Wishlist Product",
slug: "wishlist-product",
status: "active",
categoryId,
tags: [],
});
}
describe("wishlists", () => {
it("toggle adds then removes and list is empty after remove", async () => {
const t = convexTest(schema, modules);
const asAdmin = await setupAdminUser(t);
const productId = await setupProduct(t, asAdmin);
const asA = t.withIdentity({
name: "Alice",
email: "alice@example.com",
subject: "clerk_alice_123",
});
await asA.mutation(api.users.store, {});
const addResult = await asA.mutation(api.wishlists.toggle, { productId });
expect(addResult).toEqual({ added: true, id: expect.anything() });
const listAfterAdd = await asA.query(api.wishlists.list, {});
expect(listAfterAdd).toHaveLength(1);
const removeResult = await asA.mutation(api.wishlists.toggle, { productId });
expect(removeResult).toEqual({ removed: true });
const listAfterRemove = await asA.query(api.wishlists.list, {});
expect(listAfterRemove).toHaveLength(0);
});
it("isWishlisted is true after add and false after remove", async () => {
const t = convexTest(schema, modules);
const asAdmin = await setupAdminUser(t);
const productId = await setupProduct(t, asAdmin);
const asA = t.withIdentity({
name: "Alice",
email: "alice@example.com",
subject: "clerk_alice_123",
});
await asA.mutation(api.users.store, {});
expect(await asA.query(api.wishlists.isWishlisted, { productId })).toBe(
false,
);
await asA.mutation(api.wishlists.toggle, { productId });
expect(await asA.query(api.wishlists.isWishlisted, { productId })).toBe(
true,
);
await asA.mutation(api.wishlists.toggle, { productId });
expect(await asA.query(api.wishlists.isWishlisted, { productId })).toBe(
false,
);
});
it("count returns 0 for unauthenticated user", async () => {
const t = convexTest(schema, modules);
const count = await t.query(api.wishlists.count, {});
expect(count).toBe(0);
});
it("count returns correct number after add and toggle", async () => {
const t = convexTest(schema, modules);
const asAdmin = await setupAdminUser(t);
const productId1 = await setupProduct(t, asAdmin);
const categoryId = await setupCategory(t);
const productId2 = await asAdmin.mutation(api.products.create, {
name: "Wishlist Product 2",
slug: "wishlist-product-2",
status: "active",
categoryId,
tags: [],
});
const asA = t.withIdentity({
name: "Alice",
email: "alice@example.com",
subject: "clerk_alice_123",
});
await asA.mutation(api.users.store, {});
expect(await asA.query(api.wishlists.count, {})).toBe(0);
await asA.mutation(api.wishlists.toggle, { productId: productId1 });
await asA.mutation(api.wishlists.toggle, { productId: productId2 });
expect(await asA.query(api.wishlists.count, {})).toBe(2);
await asA.mutation(api.wishlists.toggle, { productId: productId1 });
expect(await asA.query(api.wishlists.count, {})).toBe(1);
});
it("remove throws if user does not own the wishlist item", async () => {
const t = convexTest(schema, modules);
const asAdmin = await setupAdminUser(t);
const productId = await setupProduct(t, asAdmin);
const asA = t.withIdentity({
name: "Alice",
email: "alice@example.com",
subject: "clerk_alice_123",
});
const asB = t.withIdentity({
name: "Bob",
email: "bob@example.com",
subject: "clerk_bob_456",
});
await asA.mutation(api.users.store, {});
await asB.mutation(api.users.store, {});
await asA.mutation(api.wishlists.add, { productId });
const listA = await asA.query(api.wishlists.list, {});
expect(listA).toHaveLength(1);
const wishlistId = listA[0]._id;
await expect(
asB.mutation(api.wishlists.remove, { id: wishlistId }),
).rejects.toThrow(/Unauthorized|does not belong/);
await asA.mutation(api.wishlists.remove, { id: wishlistId });
const listAfter = await asA.query(api.wishlists.list, {});
expect(listAfter).toHaveLength(0);
});
});