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>
338 lines
10 KiB
TypeScript
338 lines
10 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");
|
|
|
|
const shippingAddress = {
|
|
type: "shipping" as const,
|
|
firstName: "Alice",
|
|
lastName: "Smith",
|
|
phone: "+447911123456",
|
|
addressLine1: "10 Downing Street",
|
|
city: "London",
|
|
postalCode: "SW1A 2AA",
|
|
country: "GB",
|
|
};
|
|
|
|
describe("addresses", () => {
|
|
it("add sets default correctly and second add unsets previous default", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asA = t.withIdentity({
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
subject: "clerk_alice_123",
|
|
});
|
|
await asA.mutation(api.users.store, {});
|
|
|
|
await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
});
|
|
const list1 = await asA.query(api.addresses.list, {});
|
|
expect(list1).toHaveLength(1);
|
|
expect(list1[0].isDefault).toBe(true);
|
|
|
|
await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
addressLine1: "221B Baker Street",
|
|
isDefault: true,
|
|
});
|
|
const list2 = await asA.query(api.addresses.list, {});
|
|
expect(list2).toHaveLength(2);
|
|
expect(list2[0].isDefault).toBe(true);
|
|
expect(list2[1].isDefault).toBe(false);
|
|
});
|
|
|
|
it("delete default address promotes next to default", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asA = t.withIdentity({
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
subject: "clerk_alice_123",
|
|
});
|
|
await asA.mutation(api.users.store, {});
|
|
|
|
await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
});
|
|
await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
addressLine1: "221B Baker Street",
|
|
isDefault: false,
|
|
});
|
|
const listBefore = await asA.query(api.addresses.list, {});
|
|
expect(listBefore).toHaveLength(2);
|
|
const defaultId = listBefore[0]._id;
|
|
|
|
await asA.mutation(api.addresses.remove, { id: defaultId });
|
|
|
|
const listAfter = await asA.query(api.addresses.list, {});
|
|
expect(listAfter).toHaveLength(1);
|
|
expect(listAfter[0].isDefault).toBe(true);
|
|
});
|
|
|
|
it("add derives fullName from firstName + lastName", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asA = t.withIdentity({
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
subject: "clerk_alice_123",
|
|
});
|
|
await asA.mutation(api.users.store, {});
|
|
|
|
await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
});
|
|
const list = await asA.query(api.addresses.list, {});
|
|
expect(list[0].fullName).toBe("Alice Smith");
|
|
});
|
|
|
|
it("add persists additionalInformation when provided", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asA = t.withIdentity({
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
subject: "clerk_alice_123",
|
|
});
|
|
await asA.mutation(api.users.store, {});
|
|
|
|
await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
additionalInformation: "Flat 4B",
|
|
isDefault: true,
|
|
});
|
|
const list = await asA.query(api.addresses.list, {});
|
|
expect(list[0].additionalInformation).toBe("Flat 4B");
|
|
});
|
|
|
|
it("add stores undefined for additionalInformation when not provided", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asA = t.withIdentity({
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
subject: "clerk_alice_123",
|
|
});
|
|
await asA.mutation(api.users.store, {});
|
|
|
|
await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
});
|
|
const list = await asA.query(api.addresses.list, {});
|
|
expect(list[0].additionalInformation).toBeUndefined();
|
|
});
|
|
|
|
it("address records do not have state or addressLine2 fields", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asA = t.withIdentity({
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
subject: "clerk_alice_123",
|
|
});
|
|
await asA.mutation(api.users.store, {});
|
|
|
|
await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
});
|
|
const list = await asA.query(api.addresses.list, {});
|
|
const addr = list[0] as Record<string, unknown>;
|
|
expect(addr).not.toHaveProperty("state");
|
|
expect(addr).not.toHaveProperty("addressLine2");
|
|
});
|
|
|
|
it("firstName and lastName are required and stored", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asA = t.withIdentity({
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
subject: "clerk_alice_123",
|
|
});
|
|
await asA.mutation(api.users.store, {});
|
|
|
|
await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
});
|
|
const list = await asA.query(api.addresses.list, {});
|
|
expect(list[0].firstName).toBe("Alice");
|
|
expect(list[0].lastName).toBe("Smith");
|
|
});
|
|
|
|
it("isValidated defaults to false when passed via add mutation", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asA = t.withIdentity({
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
subject: "clerk_alice_123",
|
|
});
|
|
await asA.mutation(api.users.store, {});
|
|
|
|
await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
});
|
|
const list = await asA.query(api.addresses.list, {});
|
|
expect(list).toHaveLength(1);
|
|
expect(list[0].isValidated).toBe(false);
|
|
});
|
|
|
|
it("add mutation persists isValidated: true when provided", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asA = t.withIdentity({
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
subject: "clerk_alice_123",
|
|
});
|
|
await asA.mutation(api.users.store, {});
|
|
|
|
await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
isValidated: true,
|
|
});
|
|
const list = await asA.query(api.addresses.list, {});
|
|
expect(list[0].isValidated).toBe(true);
|
|
});
|
|
|
|
it("update mutation patches isValidated", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asA = t.withIdentity({
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
subject: "clerk_alice_123",
|
|
});
|
|
await asA.mutation(api.users.store, {});
|
|
|
|
const id = await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
});
|
|
|
|
await asA.mutation(api.addresses.update, { id, isValidated: true });
|
|
const list1 = await asA.query(api.addresses.list, {});
|
|
expect(list1[0].isValidated).toBe(true);
|
|
|
|
await asA.mutation(api.addresses.update, { id, isValidated: false });
|
|
const list2 = await asA.query(api.addresses.list, {});
|
|
expect(list2[0].isValidated).toBe(false);
|
|
});
|
|
|
|
it("update mutation patches additionalInformation", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asA = t.withIdentity({
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
subject: "clerk_alice_123",
|
|
});
|
|
await asA.mutation(api.users.store, {});
|
|
|
|
const id = await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
});
|
|
|
|
await asA.mutation(api.addresses.update, { id, additionalInformation: "Floor 3" });
|
|
const list = await asA.query(api.addresses.list, {});
|
|
expect(list[0].additionalInformation).toBe("Floor 3");
|
|
});
|
|
|
|
it("markValidated sets isValidated to true", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asA = t.withIdentity({
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
subject: "clerk_alice_123",
|
|
});
|
|
await asA.mutation(api.users.store, {});
|
|
|
|
const id = await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
});
|
|
expect((await asA.query(api.addresses.list, {}))[0].isValidated).toBe(false);
|
|
|
|
await asA.mutation(api.addresses.markValidated, { id, isValidated: true });
|
|
const list = await asA.query(api.addresses.list, {});
|
|
expect(list[0].isValidated).toBe(true);
|
|
});
|
|
|
|
it("markValidated throws if user does not own address", async () => {
|
|
const t = convexTest(schema, modules);
|
|
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, {});
|
|
|
|
const id = await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
});
|
|
|
|
await expect(
|
|
asB.mutation(api.addresses.markValidated, { id, isValidated: true }),
|
|
).rejects.toThrow(/Unauthorized|does not belong/);
|
|
});
|
|
|
|
it("markValidated throws for non-existent address", async () => {
|
|
const t = convexTest(schema, modules);
|
|
const asA = t.withIdentity({
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
subject: "clerk_alice_123",
|
|
});
|
|
await asA.mutation(api.users.store, {});
|
|
|
|
const fakeId = await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
});
|
|
await asA.mutation(api.addresses.remove, { id: fakeId });
|
|
|
|
await expect(
|
|
asA.mutation(api.addresses.markValidated, { id: fakeId, isValidated: true }),
|
|
).rejects.toThrow(/not found/i);
|
|
});
|
|
|
|
it("update throws if user does not own the address", async () => {
|
|
const t = convexTest(schema, modules);
|
|
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, {});
|
|
|
|
const id = await asA.mutation(api.addresses.add, {
|
|
...shippingAddress,
|
|
isDefault: true,
|
|
});
|
|
|
|
await expect(
|
|
asB.mutation(api.addresses.update, { id, fullName: "Hacker" }),
|
|
).rejects.toThrow(/Unauthorized|does not belong/);
|
|
|
|
await asA.mutation(api.addresses.update, { id, fullName: "Alice Updated" });
|
|
const list = await asA.query(api.addresses.list, {});
|
|
expect(list[0].fullName).toBe("Alice Updated");
|
|
});
|
|
});
|