Compare commits
2 commits
ed9259d35e
...
1f354e8b5c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f354e8b5c | ||
|
|
66dc2b2065 |
3 changed files with 206 additions and 15 deletions
|
|
@ -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
140
src/server.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,24 +1,73 @@
|
||||||
import Fastify from 'fastify';
|
import Fastify, { type FastifyError } from "fastify";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
const app = Fastify({
|
const echoSchema = z.object({
|
||||||
logger: true,
|
message: z.string().min(1).max(500),
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/health', async () => ({
|
export const buildApp = () => {
|
||||||
status: 'ok',
|
const app = Fastify({
|
||||||
}));
|
logger: true,
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/ready', async () => ({
|
app.setErrorHandler((error: FastifyError, request, reply) => {
|
||||||
status: 'ready',
|
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 () => ({
|
||||||
|
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 () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void start();
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
void start();
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue