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>
841 lines
25 KiB
TypeScript
841 lines
25 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: any;
|
|
await t.run(async (ctx) => {
|
|
categoryId = await ctx.db.insert("categories", {
|
|
name: "Pet Food",
|
|
slug: "pet-food",
|
|
});
|
|
});
|
|
return categoryId;
|
|
}
|
|
|
|
describe("products", () => {
|
|
it("list returns empty page when no products", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const result = await t.query(api.products.list, {
|
|
paginationOpts: { numItems: 10, cursor: null },
|
|
});
|
|
expect(result.page).toHaveLength(0);
|
|
});
|
|
|
|
it("list returns only active products when filtered by status", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Active Product",
|
|
slug: "active-product",
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Draft Product",
|
|
slug: "draft-product",
|
|
status: "draft",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
|
|
const result = await t.query(api.products.list, {
|
|
paginationOpts: { numItems: 10, cursor: null },
|
|
status: "active",
|
|
});
|
|
expect(result.page).toHaveLength(1);
|
|
expect(result.page[0].name).toBe("Active Product");
|
|
});
|
|
|
|
it("getBySlug returns product for valid slug", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Dog Treats",
|
|
slug: "dog-treats",
|
|
status: "active",
|
|
categoryId,
|
|
tags: ["dogs"],
|
|
});
|
|
|
|
const product = await t.query(api.products.getBySlug, {
|
|
slug: "dog-treats",
|
|
});
|
|
expect(product).not.toBeNull();
|
|
expect(product?.name).toBe("Dog Treats");
|
|
});
|
|
|
|
it("getBySlug returns null for non-existent slug", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const product = await t.query(api.products.getBySlug, {
|
|
slug: "does-not-exist",
|
|
});
|
|
expect(product).toBeNull();
|
|
});
|
|
|
|
it("create throws for non-admin users", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
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.products.create, {
|
|
name: "Illegal Product",
|
|
slug: "illegal-product",
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
}),
|
|
).rejects.toThrow("Unauthorized: admin access required");
|
|
});
|
|
|
|
it("create succeeds for admin users", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
const productId = await asAdmin.mutation(api.products.create, {
|
|
name: "Cat Toy",
|
|
slug: "cat-toy",
|
|
status: "draft",
|
|
categoryId,
|
|
tags: ["cats"],
|
|
});
|
|
expect(productId).toBeTruthy();
|
|
});
|
|
|
|
it("archive changes product status to archived", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
const productId = await asAdmin.mutation(api.products.create, {
|
|
name: "Old Product",
|
|
slug: "old-product",
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
|
|
await asAdmin.mutation(api.products.archive, { id: productId });
|
|
|
|
const product = await t.query(api.products.getBySlug, {
|
|
slug: "old-product",
|
|
});
|
|
expect(product).toBeNull();
|
|
});
|
|
|
|
it("addVariant creates variant linked to product", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
const productId = await asAdmin.mutation(api.products.create, {
|
|
name: "Variant Product",
|
|
slug: "variant-product",
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
|
|
const variantId = await asAdmin.mutation(api.products.addVariant, {
|
|
productId,
|
|
name: "Small",
|
|
sku: "VAR-SM-001",
|
|
price: 999,
|
|
stockQuantity: 10,
|
|
isActive: true,
|
|
});
|
|
expect(variantId).toBeTruthy();
|
|
|
|
const product = await t.query(api.products.getBySlug, {
|
|
slug: "variant-product",
|
|
});
|
|
expect(product).not.toBeNull();
|
|
expect(product?.variants).toHaveLength(1);
|
|
expect(product?.variants[0].name).toBe("Small");
|
|
expect(product?.variants[0].sku).toBe("VAR-SM-001");
|
|
expect(product?.variants[0].price).toBe(999);
|
|
});
|
|
|
|
it("updateVariant changes variant price", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
const productId = await asAdmin.mutation(api.products.create, {
|
|
name: "Update Variant Product",
|
|
slug: "update-variant-product",
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
const variantId = await asAdmin.mutation(api.products.addVariant, {
|
|
productId,
|
|
name: "Medium",
|
|
sku: "VAR-MD-001",
|
|
price: 1999,
|
|
stockQuantity: 5,
|
|
isActive: true,
|
|
});
|
|
|
|
await asAdmin.mutation(api.products.updateVariant, {
|
|
id: variantId,
|
|
price: 1499,
|
|
});
|
|
|
|
const product = await t.query(api.products.getBySlug, {
|
|
slug: "update-variant-product",
|
|
});
|
|
expect(product?.variants[0].price).toBe(1499);
|
|
});
|
|
|
|
it("addImage inserts image with correct position", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
const productId = await asAdmin.mutation(api.products.create, {
|
|
name: "Image Product",
|
|
slug: "image-product",
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
|
|
await asAdmin.mutation(api.products.addImage, {
|
|
productId,
|
|
url: "https://example.com/first.jpg",
|
|
alt: "First image",
|
|
position: 1,
|
|
});
|
|
await asAdmin.mutation(api.products.addImage, {
|
|
productId,
|
|
url: "https://example.com/second.jpg",
|
|
alt: "Second image",
|
|
position: 0,
|
|
});
|
|
|
|
const product = await t.query(api.products.getBySlug, {
|
|
slug: "image-product",
|
|
});
|
|
expect(product?.images).toHaveLength(2);
|
|
expect(product?.images[0].position).toBe(0);
|
|
expect(product?.images[0].url).toBe("https://example.com/second.jpg");
|
|
expect(product?.images[1].position).toBe(1);
|
|
expect(product?.images[1].url).toBe("https://example.com/first.jpg");
|
|
});
|
|
|
|
it("reorderImages updates position values", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
const productId = await asAdmin.mutation(api.products.create, {
|
|
name: "Reorder Product",
|
|
slug: "reorder-product",
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
|
|
const id1 = await asAdmin.mutation(api.products.addImage, {
|
|
productId,
|
|
url: "https://example.com/a.jpg",
|
|
position: 0,
|
|
});
|
|
const id2 = await asAdmin.mutation(api.products.addImage, {
|
|
productId,
|
|
url: "https://example.com/b.jpg",
|
|
position: 1,
|
|
});
|
|
|
|
await asAdmin.mutation(api.products.reorderImages, {
|
|
updates: [
|
|
{ id: id1, position: 1 },
|
|
{ id: id2, position: 0 },
|
|
],
|
|
});
|
|
|
|
const product = await t.query(api.products.getBySlug, {
|
|
slug: "reorder-product",
|
|
});
|
|
expect(product?.images).toHaveLength(2);
|
|
expect(product?.images[0].position).toBe(0);
|
|
expect(product?.images[0].url).toBe("https://example.com/b.jpg");
|
|
expect(product?.images[1].position).toBe(1);
|
|
expect(product?.images[1].url).toBe("https://example.com/a.jpg");
|
|
});
|
|
|
|
it("search returns products matching query string", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Dog Treats",
|
|
slug: "dog-treats",
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Cat Food",
|
|
slug: "cat-food",
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
|
|
const result = await t.query(api.products.search, { query: "Dog" });
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].name).toContain("Dog");
|
|
});
|
|
|
|
it("search respects status filter", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Active Treats",
|
|
slug: "active-treats",
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Draft Treats",
|
|
slug: "draft-treats",
|
|
status: "draft",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
|
|
const result = await t.query(api.products.search, {
|
|
query: "Treats",
|
|
status: "active",
|
|
});
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].name).toBe("Active Treats");
|
|
});
|
|
|
|
it("search with no matches returns empty array", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const result = await t.query(api.products.search, {
|
|
query: "xyznonexistent",
|
|
});
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
describe("searchTypeahead", () => {
|
|
async function setupHierarchicalCategories(t: ReturnType<typeof convexTest>) {
|
|
let dogsChildCategoryId: any;
|
|
let catsChildCategoryId: any;
|
|
await t.run(async (ctx) => {
|
|
const dogsId = await ctx.db.insert("categories", {
|
|
name: "Dogs",
|
|
slug: "dogs",
|
|
});
|
|
dogsChildCategoryId = await ctx.db.insert("categories", {
|
|
name: "Dog Food",
|
|
slug: "dog-food",
|
|
parentId: dogsId,
|
|
});
|
|
const catsId = await ctx.db.insert("categories", {
|
|
name: "Cats",
|
|
slug: "cats",
|
|
});
|
|
catsChildCategoryId = await ctx.db.insert("categories", {
|
|
name: "Cat Food",
|
|
slug: "cat-food",
|
|
parentId: catsId,
|
|
});
|
|
});
|
|
return { dogsChildCategoryId, catsChildCategoryId };
|
|
}
|
|
|
|
it("returns empty array for query shorter than 3 characters", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const result = await t.query(api.products.searchTypeahead, { query: "do" });
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("returns empty array for query of exactly 2 characters", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const result = await t.query(api.products.searchTypeahead, { query: "ab" });
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("returns empty array when no products match the query", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const result = await t.query(api.products.searchTypeahead, {
|
|
query: "xyznonexistent",
|
|
});
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("returns active products matching the query", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const { dogsChildCategoryId } = await setupHierarchicalCategories(t);
|
|
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Premium Dog Kibble",
|
|
slug: "premium-dog-kibble",
|
|
status: "active",
|
|
categoryId: dogsChildCategoryId,
|
|
tags: [],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Cat Litter Deluxe",
|
|
slug: "cat-litter-deluxe",
|
|
status: "active",
|
|
categoryId: dogsChildCategoryId,
|
|
tags: [],
|
|
});
|
|
|
|
const result = await t.query(api.products.searchTypeahead, {
|
|
query: "Kibble",
|
|
});
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].name).toBe("Premium Dog Kibble");
|
|
});
|
|
|
|
it("does not return draft or archived products", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const { dogsChildCategoryId } = await setupHierarchicalCategories(t);
|
|
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Active Bone Treat",
|
|
slug: "active-bone-treat",
|
|
status: "active",
|
|
categoryId: dogsChildCategoryId,
|
|
tags: [],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Draft Bone Treat",
|
|
slug: "draft-bone-treat",
|
|
status: "draft",
|
|
categoryId: dogsChildCategoryId,
|
|
tags: [],
|
|
});
|
|
|
|
const result = await t.query(api.products.searchTypeahead, {
|
|
query: "Bone",
|
|
});
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].name).toBe("Active Bone Treat");
|
|
});
|
|
|
|
it("filters by parentCategorySlug when provided", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const { dogsChildCategoryId, catsChildCategoryId } =
|
|
await setupHierarchicalCategories(t);
|
|
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Royal Canin Dog Food",
|
|
slug: "royal-canin-dog-food",
|
|
status: "active",
|
|
categoryId: dogsChildCategoryId,
|
|
tags: [],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Royal Canin Cat Food",
|
|
slug: "royal-canin-cat-food",
|
|
status: "active",
|
|
categoryId: catsChildCategoryId,
|
|
tags: [],
|
|
});
|
|
|
|
const result = await t.query(api.products.searchTypeahead, {
|
|
query: "Royal Canin",
|
|
parentCategorySlug: "cats",
|
|
});
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].name).toBe("Royal Canin Cat Food");
|
|
expect(result[0].parentCategorySlug).toBe("cats");
|
|
});
|
|
|
|
it("returns all matching products when no category filter is provided", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const { dogsChildCategoryId, catsChildCategoryId } =
|
|
await setupHierarchicalCategories(t);
|
|
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Pro Plan Dog Food",
|
|
slug: "pro-plan-dog-food",
|
|
status: "active",
|
|
categoryId: dogsChildCategoryId,
|
|
tags: [],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Pro Plan Cat Food",
|
|
slug: "pro-plan-cat-food",
|
|
status: "active",
|
|
categoryId: catsChildCategoryId,
|
|
tags: [],
|
|
});
|
|
|
|
const result = await t.query(api.products.searchTypeahead, {
|
|
query: "Pro Plan",
|
|
});
|
|
expect(result).toHaveLength(2);
|
|
});
|
|
|
|
it("returns enriched data: imageUrl, minPrice, slug, and category slugs", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const { dogsChildCategoryId } = await setupHierarchicalCategories(t);
|
|
|
|
const productId = await asAdmin.mutation(api.products.create, {
|
|
name: "Enriched Dog Treat",
|
|
slug: "enriched-dog-treat",
|
|
status: "active",
|
|
categoryId: dogsChildCategoryId,
|
|
tags: [],
|
|
});
|
|
await asAdmin.mutation(api.products.addImage, {
|
|
productId,
|
|
url: "https://example.com/treat.jpg",
|
|
alt: "Dog Treat",
|
|
position: 0,
|
|
});
|
|
await asAdmin.mutation(api.products.addVariant, {
|
|
productId,
|
|
name: "100g",
|
|
sku: "TREAT-100G",
|
|
price: 599,
|
|
stockQuantity: 50,
|
|
isActive: true,
|
|
});
|
|
|
|
const result = await t.query(api.products.searchTypeahead, {
|
|
query: "Enriched",
|
|
});
|
|
expect(result).toHaveLength(1);
|
|
const item = result[0];
|
|
expect(item.slug).toBe("enriched-dog-treat");
|
|
expect(item.parentCategorySlug).toBe("dogs");
|
|
expect(item.childCategorySlug).toBe("dog-food");
|
|
expect(item.imageUrl).toBe("https://example.com/treat.jpg");
|
|
expect(item.imageAlt).toBe("Dog Treat");
|
|
expect(item.minPrice).toBe(599);
|
|
});
|
|
|
|
it("returns minPrice=0 when product has no active variants", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const { dogsChildCategoryId } = await setupHierarchicalCategories(t);
|
|
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Variantless Collar",
|
|
slug: "variantless-collar",
|
|
status: "active",
|
|
categoryId: dogsChildCategoryId,
|
|
tags: [],
|
|
});
|
|
|
|
const result = await t.query(api.products.searchTypeahead, {
|
|
query: "Variantless",
|
|
});
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].minPrice).toBe(0);
|
|
});
|
|
|
|
it("returns imageUrl as undefined when product has no images", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const { dogsChildCategoryId } = await setupHierarchicalCategories(t);
|
|
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Imageless Dog Toy",
|
|
slug: "imageless-dog-toy",
|
|
status: "active",
|
|
categoryId: dogsChildCategoryId,
|
|
tags: [],
|
|
});
|
|
|
|
const result = await t.query(api.products.searchTypeahead, {
|
|
query: "Imageless",
|
|
});
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].imageUrl).toBeUndefined();
|
|
});
|
|
|
|
it("picks the lowest priced active variant as minPrice", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const { dogsChildCategoryId } = await setupHierarchicalCategories(t);
|
|
|
|
const productId = await asAdmin.mutation(api.products.create, {
|
|
name: "Multi-Variant Dog Food",
|
|
slug: "multi-variant-dog-food",
|
|
status: "active",
|
|
categoryId: dogsChildCategoryId,
|
|
tags: [],
|
|
});
|
|
await asAdmin.mutation(api.products.addVariant, {
|
|
productId,
|
|
name: "1kg",
|
|
sku: "MV-1KG",
|
|
price: 1200,
|
|
stockQuantity: 10,
|
|
isActive: true,
|
|
});
|
|
await asAdmin.mutation(api.products.addVariant, {
|
|
productId,
|
|
name: "500g",
|
|
sku: "MV-500G",
|
|
price: 799,
|
|
stockQuantity: 20,
|
|
isActive: true,
|
|
});
|
|
// Inactive variant with a lower price — should be ignored
|
|
await asAdmin.mutation(api.products.addVariant, {
|
|
productId,
|
|
name: "2kg",
|
|
sku: "MV-2KG",
|
|
price: 199,
|
|
stockQuantity: 5,
|
|
isActive: false,
|
|
});
|
|
|
|
const result = await t.query(api.products.searchTypeahead, {
|
|
query: "Multi-Variant",
|
|
});
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].minPrice).toBe(799);
|
|
});
|
|
|
|
it("respects the limit parameter", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const { dogsChildCategoryId } = await setupHierarchicalCategories(t);
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: `Dog Snack ${i}`,
|
|
slug: `dog-snack-${i}`,
|
|
status: "active",
|
|
categoryId: dogsChildCategoryId,
|
|
tags: [],
|
|
});
|
|
}
|
|
|
|
const result = await t.query(api.products.searchTypeahead, {
|
|
query: "Dog Snack",
|
|
limit: 3,
|
|
});
|
|
expect(result).toHaveLength(3);
|
|
});
|
|
});
|
|
|
|
describe("listByTag", () => {
|
|
it("returns only active products with the given tag", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Sale Product",
|
|
slug: "sale-product",
|
|
status: "active",
|
|
categoryId,
|
|
tags: ["sale", "dogs"],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Top Pick",
|
|
slug: "top-pick",
|
|
status: "active",
|
|
categoryId,
|
|
tags: ["top-picks"],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Regular Product",
|
|
slug: "regular-product",
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Draft Sale",
|
|
slug: "draft-sale",
|
|
status: "draft",
|
|
categoryId,
|
|
tags: ["sale"],
|
|
});
|
|
|
|
const result = await t.query(api.products.listByTag, { tag: "sale" });
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].name).toBe("Sale Product");
|
|
});
|
|
|
|
it("returns empty array when no products match the tag", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Regular Product",
|
|
slug: "regular-product",
|
|
status: "active",
|
|
categoryId,
|
|
tags: ["dogs"],
|
|
});
|
|
|
|
const result = await t.query(api.products.listByTag, { tag: "sale" });
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
|
|
it("respects the limit arg", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: `Sale Product ${i}`,
|
|
slug: `sale-product-${i}`,
|
|
status: "active",
|
|
categoryId,
|
|
tags: ["sale"],
|
|
});
|
|
}
|
|
|
|
const result = await t.query(api.products.listByTag, {
|
|
tag: "sale",
|
|
limit: 3,
|
|
});
|
|
expect(result).toHaveLength(3);
|
|
});
|
|
|
|
it("returns products matching top-picks tag", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Top Pick A",
|
|
slug: "top-pick-a",
|
|
status: "active",
|
|
categoryId,
|
|
tags: ["top-picks"],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Top Pick B",
|
|
slug: "top-pick-b",
|
|
status: "active",
|
|
categoryId,
|
|
tags: ["top-picks", "sale"],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "No Pick",
|
|
slug: "no-pick",
|
|
status: "active",
|
|
categoryId,
|
|
tags: ["sale"],
|
|
});
|
|
|
|
const result = await t.query(api.products.listByTag, { tag: "top-picks" });
|
|
expect(result).toHaveLength(2);
|
|
const names = result.map((p: any) => p.name).sort();
|
|
expect(names).toEqual(["Top Pick A", "Top Pick B"]);
|
|
});
|
|
});
|
|
|
|
describe("listRecentlyAdded", () => {
|
|
it("returns active products created within the last 30 days", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
// These will have _creationTime = now (within 30 days)
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "New Product A",
|
|
slug: "new-product-a",
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "New Product B",
|
|
slug: "new-product-b",
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: "Draft New",
|
|
slug: "draft-new",
|
|
status: "draft",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
|
|
const result = await t.query(api.products.listRecentlyAdded, {});
|
|
// Only the two active products should appear; draft is excluded
|
|
expect(result.length).toBeGreaterThanOrEqual(2);
|
|
const names = result.map((p: any) => p.name);
|
|
expect(names).toContain("New Product A");
|
|
expect(names).toContain("New Product B");
|
|
expect(names).not.toContain("Draft New");
|
|
});
|
|
|
|
it("respects the limit arg", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asAdmin = await setupAdminUser(t);
|
|
const categoryId = await setupCategory(t);
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
await asAdmin.mutation(api.products.create, {
|
|
name: `Product ${i}`,
|
|
slug: `product-${i}`,
|
|
status: "active",
|
|
categoryId,
|
|
tags: [],
|
|
});
|
|
}
|
|
|
|
const result = await t.query(api.products.listRecentlyAdded, { limit: 3 });
|
|
expect(result).toHaveLength(3);
|
|
});
|
|
|
|
it("returns empty array when no active products exist", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const result = await t.query(api.products.listRecentlyAdded, {});
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
});
|
|
});
|