This commit is contained in:
13
test/keys.ts
Normal file
13
test/keys.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { exportJWK, exportPKCS8, generateKeyPair } from "jose";
|
||||
|
||||
export async function createTestKey() {
|
||||
const { publicKey, privateKey } = await generateKeyPair("RS256", {
|
||||
extractable: true
|
||||
});
|
||||
const jwk = await exportJWK(publicKey);
|
||||
jwk.kid = "test-key";
|
||||
jwk.alg = "RS256";
|
||||
|
||||
const pem = await exportPKCS8(privateKey);
|
||||
return { jwk, pem };
|
||||
}
|
||||
272
test/main.test.ts
Normal file
272
test/main.test.ts
Normal file
@ -0,0 +1,272 @@
|
||||
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;
|
||||
let jwk: JWK;
|
||||
let privateKey: string;
|
||||
|
||||
// Spin up a temporary Postgres container for testing
|
||||
beforeAll(async () => {
|
||||
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 () => {
|
||||
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();
|
||||
console.log(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();
|
||||
console.log(json);
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user