import { beforeEach, describe, expect, it } from "bun:test"; import { eq } from "drizzle-orm"; import { Hono } from "hono"; import * as schema from "../../src/db/schema.ts"; import { updateProfileSchema } from "../../src/shared/schemas.ts"; import { profileRoutes } from "../../src/server/routes/profiles.ts"; import { setupRoutes } from "../../src/server/routes/setups.ts"; import { getPublicSetupWithItems } from "../../src/server/services/profile.service.ts"; import { updateProfile } from "../../src/server/services/profile.service.ts"; import { createTestDb } from "../helpers/db.ts"; import { zValidator } from "@hono/zod-validator"; import { parseId } from "../../src/server/lib/params.ts"; type Db = Awaited>["db"]; /** * Creates a test app with authenticated routes. * Auth middleware is simulated by always injecting userId. */ async function createTestApp() { const { db, userId } = await createTestDb(); const app = new Hono(); // Inject db for all routes app.use("*", async (c, next) => { c.set("db", db); c.set("userId", userId); await next(); }); // Public routes app.route("/api/users", profileRoutes); // Profile update on auth routes (inline to avoid requireAuth in tests) app.put( "/api/auth/profile", zValidator("json", updateProfileSchema), async (c) => { const testDb = c.get("db"); const uid = c.get("userId")!; const data = c.req.valid("json"); const updated = await updateProfile(testDb, uid, data); if (!updated) return c.json({ error: "User not found" }, 404); return c.json(updated); }, ); // Setup routes including /:id/public app.route("/api/setups", setupRoutes); return { app, db, userId }; } /** * Creates a test app WITHOUT auth — no userId injected. * Public routes should still work; protected routes should fail. */ async function createNoAuthTestApp() { const { db, userId } = await createTestDb(); const app = new Hono(); app.use("*", async (c, next) => { c.set("db", db); // No userId set — simulates unauthenticated request await next(); }); // Public routes (work without auth) app.route("/api/users", profileRoutes); // Protected profile update — simulates auth rejection app.put("/api/auth/profile", async (c) => { const uid = c.get("userId"); if (!uid) return c.json({ error: "Authentication required" }, 401); return c.json({ error: "Unexpected" }, 500); }); app.route("/api/setups", setupRoutes); return { app, db, userId }; } describe("Profile Routes", () => { let app: Hono; let db: Db; let userId: number; beforeEach(async () => { const testData = await createTestApp(); app = testData.app; db = testData.db; userId = testData.userId; }); describe("GET /api/users/:id/profile", () => { it("returns 200 with profile data without auth", async () => { // Set up profile data await db .update(schema.users) .set({ displayName: "Alice", bio: "Bikepacker" }) .where(eq(schema.users.id, userId)); const res = await app.request(`/api/users/${userId}/profile`); expect(res.status).toBe(200); const body = await res.json(); expect(body.id).toBe(userId); expect(body.displayName).toBe("Alice"); expect(body.bio).toBe("Bikepacker"); expect(body.setups).toEqual([]); }); it("includes only public setups", async () => { // Create public and private setups await db .insert(schema.setups) .values([ { name: "Public Setup", userId, isPublic: true }, { name: "Private Setup", userId, isPublic: false }, ]); const res = await app.request(`/api/users/${userId}/profile`); expect(res.status).toBe(200); const body = await res.json(); expect(body.setups).toHaveLength(1); expect(body.setups[0].name).toBe("Public Setup"); }); it("returns 404 for non-existent user", async () => { const res = await app.request("/api/users/99999/profile"); expect(res.status).toBe(404); }); it("returns 400 for invalid user ID", async () => { const res = await app.request("/api/users/abc/profile"); expect(res.status).toBe(400); }); }); describe("PUT /api/auth/profile", () => { it("returns 200 with updated fields when authenticated", async () => { const res = await app.request("/api/auth/profile", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ displayName: "Alice", bio: "Loves bikepacking", }), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.displayName).toBe("Alice"); expect(body.bio).toBe("Loves bikepacking"); }); it("returns 401 without auth", async () => { const { app: noAuthApp } = await createNoAuthTestApp(); const res = await noAuthApp.request("/api/auth/profile", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ displayName: "Alice" }), }); expect(res.status).toBe(401); }); }); }); describe("Public Setup Routes", () => { let app: Hono; let db: Db; let userId: number; beforeEach(async () => { const testData = await createTestApp(); app = testData.app; db = testData.db; userId = testData.userId; }); describe("GET /api/setups/:id/public", () => { it("returns 200 for public setup without auth", async () => { const [setup] = await db .insert(schema.setups) .values({ name: "My Public Setup", userId, isPublic: true }) .returning(); const res = await app.request(`/api/setups/${setup.id}/public`); expect(res.status).toBe(200); const body = await res.json(); expect(body.name).toBe("My Public Setup"); expect(body.isPublic).toBe(true); expect(body.items).toBeDefined(); }); it("returns 200 for public setup with items", async () => { const [setup] = await db .insert(schema.setups) .values({ name: "Loaded Setup", userId, isPublic: true }) .returning(); const [cat] = await db .select() .from(schema.categories) .where(eq(schema.categories.userId, userId)); const [item] = await db .insert(schema.items) .values({ name: "Tent", categoryId: cat.id, userId, weightGrams: 1200, priceCents: 30000, }) .returning(); await db.insert(schema.setupItems).values({ setupId: setup.id, itemId: item.id, }); const res = await app.request(`/api/setups/${setup.id}/public`); expect(res.status).toBe(200); const body = await res.json(); expect(body.items).toHaveLength(1); expect(body.items[0].name).toBe("Tent"); }); it("returns 404 for private setup", async () => { const [setup] = await db .insert(schema.setups) .values({ name: "Private Setup", userId, isPublic: false }) .returning(); const res = await app.request(`/api/setups/${setup.id}/public`); expect(res.status).toBe(404); }); it("returns 404 for non-existent setup", async () => { const res = await app.request("/api/setups/99999/public"); expect(res.status).toBe(404); }); }); });