Files
the-pet-loft/convex/reviews.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

292 lines
8.5 KiB
TypeScript

import { convexTest } from "convex-test";
import { describe, it, expect } from "vitest";
import { api } from "./_generated/api";
import schema from "./schema";
import type { Doc } from "./_generated/dataModel";
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: "Review Product",
slug: "review-product",
status: "active",
categoryId,
tags: [],
});
}
describe("reviews", () => {
it("create adds review with isApproved false and duplicate throws", async () => {
const t = convexTest(schema, modules);
const asAdmin = await setupAdminUser(t);
const productId = await setupProduct(t, asAdmin);
const asCustomer = t.withIdentity({
name: "Customer",
email: "customer@example.com",
subject: "clerk_customer_123",
});
await asCustomer.mutation(api.users.store, {});
const reviewId = await asCustomer.mutation(api.reviews.create, {
productId,
rating: 5,
title: "Great product",
content: "Really liked it.",
});
expect(reviewId).toBeTruthy();
const byProduct = await t.query(api.reviews.listByProduct, {
productId,
limit: 10,
offset: 0,
});
expect(byProduct.page).toHaveLength(0);
await expect(
asCustomer.mutation(api.reviews.create, {
productId,
rating: 4,
title: "Second",
content: "Duplicate.",
}),
).rejects.toThrow(/already reviewed/);
});
it("approve updates product averageRating and reviewCount", async () => {
const t = convexTest(schema, modules);
const asAdmin = await setupAdminUser(t);
const productId = await setupProduct(t, asAdmin);
const asCustomer = t.withIdentity({
name: "Customer",
email: "customer@example.com",
subject: "clerk_customer_123",
});
await asCustomer.mutation(api.users.store, {});
const reviewId = await asCustomer.mutation(api.reviews.create, {
productId,
rating: 5,
title: "Great",
content: "Content here.",
});
const productBefore = (await t.run(async (ctx) => ctx.db.get(productId))) as Doc<"products"> | null;
expect(productBefore?.averageRating).toBeUndefined();
expect(productBefore?.reviewCount).toBeUndefined();
await asAdmin.mutation(api.reviews.approve, { id: reviewId });
const productAfter = (await t.run(async (ctx) => ctx.db.get(productId))) as Doc<"products"> | null;
expect(productAfter?.averageRating).toBe(5);
expect(productAfter?.reviewCount).toBe(1);
const byProduct = await t.query(api.reviews.listByProduct, {
productId,
limit: 10,
offset: 0,
});
expect(byProduct.page).toHaveLength(1);
expect(byProduct.page[0].rating).toBe(5);
});
it("create throws when rating out of range", async () => {
const t = convexTest(schema, modules);
const asAdmin = await setupAdminUser(t);
const productId = await setupProduct(t, asAdmin);
const asCustomer = t.withIdentity({
name: "Customer",
email: "customer@example.com",
subject: "clerk_customer_123",
});
await asCustomer.mutation(api.users.store, {});
await expect(
asCustomer.mutation(api.reviews.create, {
productId,
rating: 0,
title: "Bad",
content: "Content.",
}),
).rejects.toThrow(/1 and 5/);
await expect(
asCustomer.mutation(api.reviews.create, {
productId,
rating: 6,
title: "Bad",
content: "Content.",
}),
).rejects.toThrow(/1 and 5/);
});
it("listForAdmin and approve and deleteReview require admin", async () => {
const t = convexTest(schema, modules);
const asAdmin = await setupAdminUser(t);
const productId = await setupProduct(t, asAdmin);
const asCustomer = t.withIdentity({
name: "Customer",
email: "customer@example.com",
subject: "clerk_customer_123",
});
await asCustomer.mutation(api.users.store, {});
const reviewId = await asCustomer.mutation(api.reviews.create, {
productId,
rating: 3,
title: "OK",
content: "Content.",
});
await expect(
asCustomer.query(api.reviews.listForAdmin, { limit: 10, offset: 0 }),
).rejects.toThrow(/admin|Unauthorized/);
await expect(
asCustomer.mutation(api.reviews.approve, { id: reviewId }),
).rejects.toThrow(/admin|Unauthorized/);
await expect(
asCustomer.mutation(api.reviews.deleteReview, { id: reviewId }),
).rejects.toThrow(/admin|Unauthorized/);
await asAdmin.mutation(api.reviews.approve, { id: reviewId });
const adminList = await asAdmin.query(api.reviews.listForAdmin, {
limit: 10,
offset: 0,
});
expect(adminList.page.length).toBeGreaterThanOrEqual(1);
});
it("listByProductSorted sorts correctly", async () => {
const t = convexTest(schema, modules);
const asAdmin = await setupAdminUser(t);
const productId = await setupProduct(t, asAdmin);
const asCustomer1 = t.withIdentity({
name: "Customer 1",
email: "customer1@example.com",
subject: "clerk_c1",
});
await asCustomer1.mutation(api.users.store, {});
const r1 = await asCustomer1.mutation(api.reviews.create, {
productId,
rating: 3,
title: "C1",
content: "C1",
});
const asCustomer2 = t.withIdentity({
name: "Customer 2",
email: "customer2@example.com",
subject: "clerk_c2",
});
await asCustomer2.mutation(api.users.store, {});
const r2 = await asCustomer2.mutation(api.reviews.create, {
productId,
rating: 5,
title: "C2",
content: "C2",
});
await asAdmin.mutation(api.reviews.approve, { id: r1 });
await asAdmin.mutation(api.reviews.approve, { id: r2 });
const highest = await t.query(api.reviews.listByProductSorted, {
productId,
sortBy: "highest",
});
expect(highest.page.length).toBe(2);
expect(highest.page[0].rating).toBe(5);
const lowest = await t.query(api.reviews.listByProductSorted, {
productId,
sortBy: "lowest",
});
expect(lowest.page[0].rating).toBe(3);
});
it("hasUserReviewed works", async () => {
const t = convexTest(schema, modules);
const asAdmin = await setupAdminUser(t);
const productId = await setupProduct(t, asAdmin);
const asCustomer = t.withIdentity({
name: "Customer",
email: "cust@example.com",
subject: "clerk_c_has",
});
await asCustomer.mutation(api.users.store, {});
const hasBefore = await asCustomer.query(api.reviews.hasUserReviewed, { productId });
expect(hasBefore).toBe(false);
await asCustomer.mutation(api.reviews.create, {
productId,
rating: 4,
title: "Has",
content: "Has content",
});
const hasAfter = await asCustomer.query(api.reviews.hasUserReviewed, { productId });
expect(hasAfter).toBe(true);
});
it("submitAndRecalculate works", async () => {
const t = convexTest(schema, modules);
const asAdmin = await setupAdminUser(t);
const productId = await setupProduct(t, asAdmin);
const asCustomer = t.withIdentity({
name: "Customer",
email: "cust_submit@example.com",
subject: "clerk_c_submit",
});
await asCustomer.mutation(api.users.store, {});
const reviewId = await asCustomer.action(api.reviews.submitAndRecalculate, {
productId,
rating: 4,
title: "Title",
content: "Content",
});
expect(reviewId).toBeTruthy();
const product = (await t.run(async (ctx) => ctx.db.get(productId))) as Doc<"products"> | null;
// Since isApproved is false initially, the stats are actually undefined (or unchanged)
expect(product?.averageRating).toBeUndefined();
expect(product?.reviewCount).toBeUndefined();
});
});