import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { getDb, initDb } from "../src/db"; import { GenericContainer, Wait, type StartedTestContainer, } from "testcontainers"; import { createTestKey } from "./keys"; import { sign } from "jsonwebtoken"; import { getTokenData, getTokenUID, verifyToken } from "../src/auth"; import type { JWK } from "jose"; process.env.OIDC_ENABLED = "true"; process.env.OIDC_JWKS_URL = "http://oidc.example.com/.well-known/jwks.json"; process.env.OIDC_CLIENT_ID = "test-client"; process.env.OIDC_AUTH_URL = "http://oidc.example.com/auth"; process.env.OIDC_TOKEN_URL = "http://oidc.example.com/token"; process.env.NODE_ENV = "test"; process.env.REVIEWS_ENABLED = "true"; const app = await import("../src/main").then((m) => m.default); const PGSQL_IMAGE = "postgres:latest"; let postgresContainer: StartedTestContainer | null = null; let jwk: JWK; let privateKey: string; // Spin up a temporary Postgres container for testing beforeAll(async () => { if (!process.env.DATABASE_URL) { postgresContainer = await new GenericContainer(PGSQL_IMAGE) .withEnvironment({ POSTGRES_USER: "tc", POSTGRES_PASSWORD: "tc", POSTGRES_DB: "tc", }) .withExposedPorts(5432) .withHealthCheck({ test: ["CMD-SHELL", "pg_isready -U postgres"], interval: 1000, timeout: 3000, retries: 5, }) .withWaitStrategy(Wait.forHealthCheck()) .start(); process.env.DATABASE_URL = `postgres://tc:tc@localhost:${postgresContainer.getMappedPort(5432)}/tc`; } getDb(true); await initDb(); }); afterAll(async () => { // Nuke the entire database after tests const db = getDb(); await db.dropDatabase(); await db.destroy(); if (postgresContainer) await postgresContainer.stop(); }); // Override JWKS URL to prevent external calls during tests beforeAll(async () => { const key = await createTestKey(); jwk = key.jwk; privateKey = key.pem; // Mock the fetch function to return the test JWK // @ts-expect-error - we just need to override fetch for tests global.fetch = async () => ({ json: async () => ({ keys: [jwk] }), }); }); it("serves a GET request to /", async () => { const response = await app.fetch(new Request("http://localhost/")); const text = await response.text(); expect(response.status).toBe(200); expect(text).toBe("TrafficCue Server"); }); it("Serves a correct config", async () => { const res = await app.fetch(new Request("http://localhost/api/config")); expect(res.status).toBe(200); const json = await res.json(); expect(json).toBeDefined(); expect(json).toBeObject(); const config = json as Record; expect(config.name).toEqual("TrafficCue Server"); expect(config.version).toEqual("0"); expect(config.capabilities).toBeArray(); expect(config.capabilities).toContain("auth"); expect(config.capabilities).toContain("reviews"); expect(config.oidc).toEqual({ CLIENT_ID: "test-client", AUTH_URL: "http://oidc.example.com/auth", TOKEN_URL: "http://oidc.example.com/token", }); }); describe("Authentication", () => { it("verifies a valid token", async () => { const token = sign({ sub: "test" }, privateKey, { algorithm: "RS256", keyid: jwk.kid, expiresIn: "1h", }); expect(await verifyToken(token)).toBe(true); expect(getTokenUID(token)).toBe("test"); expect(getTokenData(token)).toMatchObject({ sub: "test" }); }); it("fails with invalid KID", async () => { const token = sign({ sub: "test" }, privateKey, { algorithm: "RS256", keyid: "invalid-kid", expiresIn: "1h", }); expect(await verifyToken(token)).toBe(false); expect(getTokenUID(token)).toBe("test"); expect(getTokenData(token)).toMatchObject({ sub: "test" }); }); it("fails on an invalid token", async () => { const token = "invalid.token.here"; expect(await verifyToken(token)).toBe(false); expect(getTokenUID(token)).toBe(null); expect(getTokenData(token)).toBe(null); }); it("fails on an expired token", async () => { const token = sign({ sub: "test" }, privateKey, { algorithm: "RS256", keyid: jwk.kid, expiresIn: "-1h", }); expect(await verifyToken(token)).toBe(false); expect(getTokenUID(token)).toBe("test"); expect(getTokenData(token)).toMatchObject({ sub: "test" }); }); }); let token: string; const uid = crypto.randomUUID(); describe("User Endpoints", () => { it("fails with invalid token", async () => { const res = await app.fetch( new Request("http://localhost/api/user", { method: "POST", body: JSON.stringify({ token: "invalid.token.here" }), }), ); expect(res.status).toBe(400); }); it("uploads a token", async () => { token = sign( { sub: uid, preferred_username: "testuser", }, privateKey, { algorithm: "RS256", keyid: jwk.kid, expiresIn: "1h", }, ); const res = await app.fetch( new Request("http://localhost/api/user", { method: "POST", body: JSON.stringify({ token }), }), ); expect(res.status).toBe(200); }); it("finds the user in search", async () => { const res = await app.fetch( new Request("http://localhost/api/user?name=testuser"), ); expect(res.status).toBe(200); const json = await res.json(); expect(json).toBeDefined(); expect(json).toBeArray(); const users = json as { username: string }[]; expect(users.length).toBeGreaterThan(0); expect(users[0]!.username).toBe("testuser"); }); }); describe("Reviews", () => { it("creates a review", async () => { const res = await app.fetch( new Request("http://localhost/api/review", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ rating: 4, comment: "This is a test review.", lat: 51.5074, lon: 7.4653, }), }), ); expect(res.status).toBe(200); const json = await res.json(); expect(json).toBeDefined(); expect(json).toBeObject(); const review = json as { success: boolean }; expect(review.success).toBe(true); }); it("lists reviews", async () => { const res = await app.fetch( new Request("http://localhost/api/reviews?lat=51.5074&lon=7.4653"), ); expect(res.status).toBe(200); const json = await res.json(); expect(json).toBeDefined(); expect(json).toBeArray(); const reviews = json as { rating: number; comment: string }[]; expect(reviews.length).toBeGreaterThan(0); expect(reviews[0]!.rating).toBe(4); expect(reviews[0]!.comment).toBe("This is a test review."); }); }); describe("Saved Routes", () => { it("saves a route", async () => { const res = await app.fetch( new Request("http://localhost/api/saved", { method: "PUT", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: "Home to Work", data: "This is some test route data.", }), }), ); expect(res.status).toBe(200); const json = await res.json(); expect(json).toBeDefined(); expect(json).toBeObject(); const result = json as { success: boolean }; expect(result.success).toBe(true); }); it("lists saved routes", async () => { const res = await app.fetch( new Request("http://localhost/api/saved", { method: "GET", headers: { Authorization: `Bearer ${token}`, }, }), ); expect(res.status).toBe(200); const json = await res.json(); expect(json).toBeDefined(); expect(json).toBeArray(); const routes = json as { name: string; data: string }[]; expect(routes.length).toBe(1); expect(routes[0]!.name).toBe("Home to Work"); expect(routes[0]!.data).toBe("This is some test route data."); }); it("deletes a saved route", async () => { const res = await app.fetch( new Request("http://localhost/api/saved", { method: "DELETE", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: "Home to Work", }), }), ); expect(res.status).toBe(200); const json = await res.json(); expect(json).toBeDefined(); expect(json).toBeObject(); const result = json as { success: boolean }; expect(result.success).toBe(true); }); it("no longer lists deleted routes", async () => { const res = await app.fetch( new Request("http://localhost/api/saved", { method: "GET", headers: { Authorization: `Bearer ${token}`, }, }), ); expect(res.status).toBe(200); const json = await res.json(); expect(json).toBeDefined(); expect(json).toBeArray(); const routes = json as { name: string; data: string }[]; expect(routes.length).toBe(0); }); });