import { convexTest } from "convex-test"; import { describe, it, expect } from "vitest"; import { api, internal } from "./_generated/api"; import schema from "./schema"; import type { Id } from "./_generated/dataModel"; const modules = import.meta.glob("./**/*.ts"); const identity = { name: "Alice", email: "alice@example.com", subject: "clerk_alice_123", }; const baseAddress = { firstName: "Alice", lastName: "Smith", phone: "+447911123456", addressLine1: "10 Downing Street", city: "London", postalCode: "SW1A 2AA", country: "GB", }; describe("checkout.getShippingAddresses", () => { it("returns only shipping addresses (excludes billing)", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); await asA.mutation(api.users.store, {}); await asA.mutation(api.addresses.add, { ...baseAddress, type: "shipping", isDefault: true, }); await asA.mutation(api.addresses.add, { ...baseAddress, type: "billing", addressLine1: "1 Treasury Place", isDefault: false, }); await asA.mutation(api.addresses.add, { ...baseAddress, type: "shipping", addressLine1: "221B Baker Street", isDefault: false, }); const result = await asA.query(api.checkout.getShippingAddresses, {}); expect(result).toHaveLength(2); expect(result.every((a) => a.type === "shipping")).toBe(true); }); it("sorts default address first, then by recency descending", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); await asA.mutation(api.users.store, {}); await asA.mutation(api.addresses.add, { ...baseAddress, type: "shipping", addressLine1: "First added", isDefault: false, }); await asA.mutation(api.addresses.add, { ...baseAddress, type: "shipping", addressLine1: "Second added (default)", isDefault: true, }); await asA.mutation(api.addresses.add, { ...baseAddress, type: "shipping", addressLine1: "Third added", isDefault: false, }); const result = await asA.query(api.checkout.getShippingAddresses, {}); expect(result).toHaveLength(3); expect(result[0].addressLine1).toBe("Second added (default)"); expect(result[0].isDefault).toBe(true); expect(result[1].addressLine1).toBe("Third added"); expect(result[2].addressLine1).toBe("First added"); }); it("returns empty array when user has no shipping addresses", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); await asA.mutation(api.users.store, {}); const result = await asA.query(api.checkout.getShippingAddresses, {}); expect(result).toEqual([]); }); it("returns empty array when user only has billing addresses", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); await asA.mutation(api.users.store, {}); await asA.mutation(api.addresses.add, { ...baseAddress, type: "billing", isDefault: true, }); const result = await asA.query(api.checkout.getShippingAddresses, {}); expect(result).toEqual([]); }); it("includes isValidated field on returned addresses", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); await asA.mutation(api.users.store, {}); await asA.mutation(api.addresses.add, { ...baseAddress, type: "shipping", isDefault: true, isValidated: true, }); await asA.mutation(api.addresses.add, { ...baseAddress, type: "shipping", addressLine1: "221B Baker Street", isDefault: false, }); const result = await asA.query(api.checkout.getShippingAddresses, {}); expect(result).toHaveLength(2); expect(result[0].isValidated).toBe(true); expect(result[1].isValidated).toBe(false); }); it("includes additionalInformation when present", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); await asA.mutation(api.users.store, {}); await asA.mutation(api.addresses.add, { ...baseAddress, type: "shipping", additionalInformation: "Flat 4B", isDefault: true, }); const result = await asA.query(api.checkout.getShippingAddresses, {}); expect(result).toHaveLength(1); expect(result[0].additionalInformation).toBe("Flat 4B"); }); it("returned addresses have no state or addressLine2 fields", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); await asA.mutation(api.users.store, {}); await asA.mutation(api.addresses.add, { ...baseAddress, type: "shipping", isDefault: true, }); const result = await asA.query(api.checkout.getShippingAddresses, {}); const addr = result[0] as Record; expect(addr).not.toHaveProperty("state"); expect(addr).not.toHaveProperty("addressLine2"); }); it("throws for unauthenticated users", async () => { const t = convexTest(schema, modules); await expect( t.query(api.checkout.getShippingAddresses, {}), ).rejects.toThrow(/Unauthenticated/i); }); it("does not return addresses belonging to other users", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); 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.addresses.add, { ...baseAddress, type: "shipping", isDefault: true, }); await asB.mutation(api.addresses.add, { ...baseAddress, type: "shipping", addressLine1: "Bob's address", isDefault: true, }); const resultA = await asA.query(api.checkout.getShippingAddresses, {}); expect(resultA).toHaveLength(1); expect(resultA[0].addressLine1).toBe("10 Downing Street"); const resultB = await asB.query(api.checkout.getShippingAddresses, {}); expect(resultB).toHaveLength(1); expect(resultB[0].addressLine1).toBe("Bob's address"); }); }); // ─── getAddressById (internal query) ───────────────────────────────────────── describe("checkout.getAddressById", () => { it("returns the address document when it exists", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); await asA.mutation(api.users.store, {}); const addressId = await asA.mutation(api.addresses.add, { ...baseAddress, type: "shipping", isDefault: true, }); const result = await t.query(internal.checkout.getAddressById, { addressId, }); expect(result.addressLine1).toBe("10 Downing Street"); expect(result.fullName).toBe("Alice Smith"); expect(result.city).toBe("London"); expect(result.postalCode).toBe("SW1A 2AA"); expect(result.country).toBe("GB"); }); it("throws when the address does not exist", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); await asA.mutation(api.users.store, {}); const addressId = await asA.mutation(api.addresses.add, { ...baseAddress, type: "shipping", isDefault: true, }); await asA.mutation(api.addresses.remove, { id: addressId }); await expect( t.query(internal.checkout.getAddressById, { addressId }), ).rejects.toThrow(/Address not found/i); }); it("returns additionalInformation when present on address", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); await asA.mutation(api.users.store, {}); const addressId = await asA.mutation(api.addresses.add, { ...baseAddress, type: "shipping", additionalInformation: "Flat 4B", isDefault: true, }); const result = await t.query(internal.checkout.getAddressById, { addressId, }); expect(result.additionalInformation).toBe("Flat 4B"); }); }); // ─── getCurrentUserId (internal query) ─────────────────────────────────────── describe("checkout.getCurrentUserId", () => { it("returns the user ID for an authenticated user", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); const userId = await asA.mutation(api.users.store, {}); const result = await asA.query(internal.checkout.getCurrentUserId); expect(result).toBe(userId); }); it("throws for unauthenticated requests", async () => { const t = convexTest(schema, modules); await expect( t.query(internal.checkout.getCurrentUserId), ).rejects.toThrow(/Unauthenticated/i); }); }); // ─── validateCartInternal (internal query) ─────────────────────────────────── describe("checkout.validateCartInternal", () => { async function setupProductAndVariant( t: ReturnType, overrides?: { stockQuantity?: number; price?: number; isActive?: boolean }, ) { let productId: Id<"products">; let variantId: Id<"productVariants">; let categoryId: Id<"categories">; await t.run(async (ctx) => { categoryId = await ctx.db.insert("categories", { name: "Dog Food", slug: "dog-food", }); productId = await ctx.db.insert("products", { name: "Premium Kibble", slug: "premium-kibble", status: "active", categoryId, tags: [], parentCategorySlug: "dogs", childCategorySlug: "dog-food", }); variantId = await ctx.db.insert("productVariants", { productId, name: "1kg Bag", sku: "PK-001", price: overrides?.price ?? 2499, stockQuantity: overrides?.stockQuantity ?? 50, isActive: overrides?.isActive ?? true, weight: 1000, weightUnit: "g", length: 30, width: 20, height: 10, dimensionUnit: "cm", }); await ctx.db.insert("productImages", { productId, url: "https://example.com/kibble.jpg", position: 0, }); }); return { productId: productId!, variantId: variantId!, categoryId: categoryId! }; } it("returns null when the user has no cart", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); const userId = await asA.mutation(api.users.store, {}); const result = await t.query(internal.checkout.validateCartInternal, { userId, }); expect(result).toBeNull(); }); it("returns null when the cart has no items", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); const userId = await asA.mutation(api.users.store, {}); await t.run(async (ctx) => { await ctx.db.insert("carts", { userId, items: [], createdAt: Date.now(), updatedAt: Date.now(), expiresAt: Date.now() + 86400000, }); }); const result = await t.query(internal.checkout.validateCartInternal, { userId, }); expect(result).toBeNull(); }); it("returns a valid cart validation result with enriched items", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); const userId = await asA.mutation(api.users.store, {}); const { productId, variantId } = await setupProductAndVariant(t); await t.run(async (ctx) => { await ctx.db.insert("carts", { userId, items: [{ productId, variantId, quantity: 2, price: 2499 }], createdAt: Date.now(), updatedAt: Date.now(), expiresAt: Date.now() + 86400000, }); }); const result = await t.query(internal.checkout.validateCartInternal, { userId, }); expect(result).not.toBeNull(); expect(result!.valid).toBe(true); expect(result!.items).toHaveLength(1); expect(result!.items[0].productName).toBe("Premium Kibble"); expect(result!.items[0].variantName).toBe("1kg Bag"); expect(result!.items[0].quantity).toBe(2); expect(result!.items[0].weight).toBe(1000); expect(result!.items[0].weightUnit).toBe("g"); expect(result!.subtotal).toBe(2499 * 2); }); it("returns issues for out-of-stock items", async () => { const t = convexTest(schema, modules); const asA = t.withIdentity(identity); const userId = await asA.mutation(api.users.store, {}); const { productId, variantId } = await setupProductAndVariant(t, { stockQuantity: 0, }); await t.run(async (ctx) => { await ctx.db.insert("carts", { userId, items: [{ productId, variantId, quantity: 1, price: 2499 }], createdAt: Date.now(), updatedAt: Date.now(), expiresAt: Date.now() + 86400000, }); }); const result = await t.query(internal.checkout.validateCartInternal, { userId, }); expect(result).not.toBeNull(); expect(result!.valid).toBe(false); expect(result!.issues).toHaveLength(1); expect(result!.issues[0].type).toBe("out_of_stock"); }); it("resolves cart by sessionId when userId not provided", async () => { const t = convexTest(schema, modules); const { productId, variantId } = await setupProductAndVariant(t); await t.run(async (ctx) => { await ctx.db.insert("carts", { sessionId: "sess_abc123", items: [{ productId, variantId, quantity: 1, price: 2499 }], createdAt: Date.now(), updatedAt: Date.now(), expiresAt: Date.now() + 86400000, }); }); const result = await t.query(internal.checkout.validateCartInternal, { sessionId: "sess_abc123", }); expect(result).not.toBeNull(); expect(result!.valid).toBe(true); expect(result!.items).toHaveLength(1); }); });