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>
167 lines
5.2 KiB
TypeScript
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);
|
|
});
|
|
});
|