Merge branch 'feat/phase-1-api-foundation'

This commit is contained in:
Brandon Wolfgang 2026-10-21 21:36:16 -05:00
commit 1f354e8b5c
3 changed files with 206 additions and 15 deletions

View file

@ -4,7 +4,7 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "tsx --test src/**/*.test.ts",
"dev": "tsx watch src/server.ts", "dev": "tsx watch src/server.ts",
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"start": "node dist/server.js" "start": "node dist/server.js"

140
src/server.test.ts Normal file
View file

@ -0,0 +1,140 @@
import assert from "node:assert";
import { after, describe, it } from "node:test";
import { buildApp } from "./server.js";
import { setServers } from "node:dns";
describe("API endpoints", () => {
const app = buildApp();
after(async () => {
await app.close();
});
it("GET /health returns 200 and status ok", async () => {
const response = await app.inject({
method: "GET",
url: "/health",
});
assert.equal(response.statusCode, 200);
assert.deepEqual(response.json(), {
status: "ok",
});
});
it("GET /ready returns 200 and dependency status", async () => {
const response = await app.inject({
method: "GET",
url: "/ready",
});
assert.equal(response.statusCode, 200);
assert.deepEqual(response.json(), {
status: "ready",
dependencies: {
ollama: "not-integrated",
},
});
});
it("POST /echo returns the validated message", async () => {
const response = await app.inject({
method: "POST",
url: "/echo",
payload: {
message: "Hello, world!",
},
});
assert.equal(response.statusCode, 200);
assert.deepEqual(response.json(), {
message: "Hello, world!",
});
});
it("POST /echo accepts a 500-character message", async () => {
const message = "a".repeat(500);
const response = await app.inject({
method: "POST",
url: "/echo",
payload: { message },
});
assert.equal(response.statusCode, 200);
assert.deepEqual(response.json(), { message });
});
it("POST /echo rejects an empty message", async () => {
const response = await app.inject({
method: "POST",
url: "/echo",
payload: {
message: "",
},
});
assert.equal(response.statusCode, 400);
assert.deepEqual(response.json(), {
error: "Invalid request body",
});
});
it("POST /echo rejects a message longer than 500 characters", async () => {
const message = "a".repeat(501);
const response = await app.inject({
method: "POST",
url: "/echo",
payload: { message },
});
assert.equal(response.statusCode, 400);
assert.deepEqual(response.json(), {
error: "Invalid request body",
});
});
it("POST /echo rejects a non-string message", async () => {
const message = "a".repeat(501);
const response = await app.inject({
method: "POST",
url: "/echo",
payload: {
message: 123,
},
});
assert.equal(response.statusCode, 400);
assert.deepEqual(response.json(), {
error: "Invalid request body",
});
});
it("returns 404 for an unknown endpoint", async () => {
const response = await app.inject({
method: "GET",
url: "/does-not-exist",
});
assert.equal(response.statusCode, 404);
assert.deepEqual(response.json(), {
error: "Not Found",
});
});
it("returns a generic 500 response for unexpected error", async () => {
const response = await app.inject({
method: "GET",
url: "/test-error",
});
assert.equal(response.statusCode, 500);
assert.deepEqual(response.json(), {
error: "Internal Server Error",
});
assert.equal(response.body.includes("Sensitive internal detail"), false);
});
});

View file

@ -1,24 +1,73 @@
import Fastify from 'fastify'; import Fastify, { type FastifyError } from "fastify";
import { z } from "zod";
const echoSchema = z.object({
message: z.string().min(1).max(500),
});
export const buildApp = () => {
const app = Fastify({ const app = Fastify({
logger: true, logger: true,
}); });
app.get('/health', async () => ({ app.setErrorHandler((error: FastifyError, request, reply) => {
status: 'ok', request.log.error(error);
const candidateStatusCode = error.statusCode ?? 500;
const statusCode =
candidateStatusCode >= 400 && candidateStatusCode < 500
? candidateStatusCode
: 500;
return reply.code(statusCode).send({
error: statusCode === 500 ? "Internal Server Error" : "Request Error",
});
});
app.setNotFoundHandler((_request, reply) => {
return reply.code(404).send({
error: "Not Found",
});
});
app.get("/health", async () => ({
status: "ok",
})); }));
app.get('/ready', async () => ({ app.get("/ready", async () => ({
status: 'ready', status: "ready",
dependencies: { dependencies: {
ollama: 'not-integrated', ollama: "not-integrated",
}, },
})); }));
app.post("/echo", async (request, reply) => {
const result = echoSchema.safeParse(request.body);
if (!result.success) {
return reply.code(400).send({
error: "Invalid request body",
});
}
return {
message: result.data.message,
};
});
app.get("/test-error", async () => {
throw new Error("Sensitive internal detail");
});
return app;
};
const start = async () => { const start = async () => {
const app = buildApp();
try { try {
await app.listen({ await app.listen({
host: '127.0.0.1', host: "127.0.0.1",
port: 3000, port: 3000,
}); });
} catch (error) { } catch (error) {
@ -27,4 +76,6 @@ const start = async () => {
} }
}; };
if (import.meta.url === `file://${process.argv[1]}`) {
void start(); void start();
}