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>
This commit is contained in:
166
convex/wishlists.test.ts
Normal file
166
convex/wishlists.test.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user