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) { 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) { 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, asAdmin: ReturnType["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(); }); });