This repository has been archived on 2025-11-09. You can view files and clone it, but cannot push or open issues or pull requests.
Files
trafficcue-server/test/main.test.ts
Jannik fae7308af8
All checks were successful
TrafficCue Server CI / check (push) Successful in 23s
style: run eslint and prettier
2025-08-30 10:31:03 +02:00

307 lines
8.4 KiB
TypeScript

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<string, unknown>;
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);
});
});