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:
134
convex/users.test.ts
Normal file
134
convex/users.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
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");
|
||||
|
||||
describe("users", () => {
|
||||
it("stores a new user on first login", async () => {
|
||||
const t = convexTest(schema, modules);
|
||||
const asSarah = t.withIdentity({
|
||||
name: "Sarah",
|
||||
email: "sarah@example.com",
|
||||
subject: "clerk_sarah_123",
|
||||
});
|
||||
const userId = await asSarah.mutation(api.users.store, {});
|
||||
expect(userId).toBeTruthy();
|
||||
|
||||
const user = await asSarah.query(api.users.current, {});
|
||||
expect(user).toMatchObject({
|
||||
name: "Sarah",
|
||||
email: "sarah@example.com",
|
||||
role: "customer",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns same user ID on subsequent logins", async () => {
|
||||
const t = convexTest(schema, modules);
|
||||
const asSarah = t.withIdentity({
|
||||
name: "Sarah",
|
||||
email: "sarah@example.com",
|
||||
subject: "clerk_sarah_123",
|
||||
});
|
||||
|
||||
const id1 = await asSarah.mutation(api.users.store, {});
|
||||
const id2 = await asSarah.mutation(api.users.store, {});
|
||||
expect(id1).toEqual(id2);
|
||||
});
|
||||
|
||||
it("updates name if it changed", async () => {
|
||||
const t = convexTest(schema, modules);
|
||||
const asSarah = t.withIdentity({
|
||||
name: "Sarah",
|
||||
email: "sarah@example.com",
|
||||
subject: "clerk_sarah_123",
|
||||
});
|
||||
await asSarah.mutation(api.users.store, {});
|
||||
|
||||
const asSarahRenamed = t.withIdentity({
|
||||
name: "Sarah Connor",
|
||||
email: "sarah@example.com",
|
||||
subject: "clerk_sarah_123",
|
||||
});
|
||||
await asSarahRenamed.mutation(api.users.store, {});
|
||||
|
||||
const user = await asSarahRenamed.query(api.users.current, {});
|
||||
expect(user?.name).toBe("Sarah Connor");
|
||||
});
|
||||
|
||||
it("returns null for unauthenticated user", async () => {
|
||||
const t = convexTest(schema, modules);
|
||||
const result = await t.query(api.users.current, {});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns user for authenticated user", async () => {
|
||||
const t = convexTest(schema, modules);
|
||||
const asSarah = t.withIdentity({
|
||||
name: "Sarah",
|
||||
email: "sarah@example.com",
|
||||
subject: "clerk_sarah_123",
|
||||
});
|
||||
await asSarah.mutation(api.users.store, {});
|
||||
|
||||
const user = await asSarah.query(api.users.current, {});
|
||||
expect(user).not.toBeNull();
|
||||
expect(user?.email).toBe("sarah@example.com");
|
||||
});
|
||||
|
||||
it("updateProfile updates only name, phone, avatarUrl for current user", async () => {
|
||||
const t = convexTest(schema, modules);
|
||||
const asSarah = t.withIdentity({
|
||||
name: "Sarah",
|
||||
email: "sarah@example.com",
|
||||
subject: "clerk_sarah_123",
|
||||
});
|
||||
await asSarah.mutation(api.users.store, {});
|
||||
|
||||
await asSarah.mutation(api.users.updateProfile, {
|
||||
name: "Sarah Jane",
|
||||
phone: "555-0000",
|
||||
avatarUrl: "https://example.com/avatar.png",
|
||||
});
|
||||
|
||||
const user = await asSarah.query(api.users.current, {});
|
||||
expect(user?.name).toBe("Sarah Jane");
|
||||
expect(user?.phone).toBe("555-0000");
|
||||
expect(user?.avatarUrl).toBe("https://example.com/avatar.png");
|
||||
expect(user?.email).toBe("sarah@example.com");
|
||||
});
|
||||
|
||||
it("listCustomers returns only customers and is admin-only", async () => {
|
||||
const t = convexTest(schema, modules);
|
||||
const asCustomer = t.withIdentity({
|
||||
name: "Customer",
|
||||
email: "customer@example.com",
|
||||
subject: "clerk_customer_789",
|
||||
});
|
||||
const asAdmin = t.withIdentity({
|
||||
name: "Admin",
|
||||
email: "admin@example.com",
|
||||
subject: "clerk_admin_456",
|
||||
});
|
||||
await asCustomer.mutation(api.users.store, {});
|
||||
const adminId = await asAdmin.mutation(api.users.store, {});
|
||||
await t.run(async (ctx) => {
|
||||
await ctx.db.patch(adminId, { role: "admin" });
|
||||
});
|
||||
|
||||
await expect(
|
||||
asCustomer.query(api.users.listCustomers, {
|
||||
paginationOpts: { numItems: 10, cursor: null },
|
||||
}),
|
||||
).rejects.toThrow(/Unauthorized|admin/);
|
||||
|
||||
const result = await asAdmin.query(api.users.listCustomers, {
|
||||
paginationOpts: { numItems: 10, cursor: null },
|
||||
});
|
||||
expect(result.page.length).toBeGreaterThanOrEqual(1);
|
||||
expect(result.page.every((u: { role: string }) => u.role === "customer")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user