Files
GearBox/tests/routes/profiles.test.ts
Jean-Luc Makiola 574a12e6fa fix: OIDC auth flow, Vite proxy, and PostgreSQL query compat
- Add auth redirect in root layout for unauthenticated users
- Proxy OIDC routes (/login, /callback, /logout) through Vite dev server
- Strip Secure flag from OIDC cookies in dev mode (HTTP localhost)
- Disable retry on auth query to prevent stale cookie loops
- Fix SQLite .get()/.all()/.run() calls in category and global-item
  services for PostgreSQL compatibility
- Add userId scoping to category service functions
- Add OIDC error logging in auth middleware
- Apply linter auto-formatting across affected files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:25:31 +02:00

251 lines
6.8 KiB
TypeScript

import { beforeEach, describe, expect, it } from "bun:test";
import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { Hono } from "hono";
import * as schema from "../../src/db/schema.ts";
import { parseId } from "../../src/server/lib/params.ts";
import { profileRoutes } from "../../src/server/routes/profiles.ts";
import { setupRoutes } from "../../src/server/routes/setups.ts";
import {
getPublicSetupWithItems,
updateProfile,
} from "../../src/server/services/profile.service.ts";
import { updateProfileSchema } from "../../src/shared/schemas.ts";
import { createTestDb } from "../helpers/db.ts";
type Db = Awaited<ReturnType<typeof createTestDb>>["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);
});
});
});