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>
This commit is contained in:
337
convex/addresses.test.ts
Normal file
337
convex/addresses.test.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user