Step 5.2: add server time endpoint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
af849feea7
commit
c809e10d7e
|
|
@ -207,3 +207,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
- **Config** comes from env vars validated with Zod in `loadConfig`, which reports every problem at once. `NODE_ENV`, `PORT` and `LOG_LEVEL` have defaults (development, 3000, info). The entry point loads the repo-root `.env` with `process.loadEnvFile` when the file exists; deployments set real env vars.
|
||||
- **The health endpoint is `GET /api/health`**, under `/api` so it is reachable through the same-domain proxy (decision 0.5.20). It is infrastructure, so it is not in the route catalog.
|
||||
- **`buildApp(config)` does not listen,** so tests start it on port 0. Logs are pino JSON; tests use `LOG_LEVEL=silent`.
|
||||
|
||||
## Step 5.2
|
||||
|
||||
- **The injectable clock arrives now, not in Step 7.2.** `buildApp(config, { clock })` takes a `Clock` (`() => RealTime`) and defaults to `Date.now`. Every endpoint will read "now" through it (decision 0.5.18). Step 7.2 still adds the test-only clock-offset endpoint.
|
||||
- **Game time uses the default `content.config`**, via sim `realToGame`.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
import { content } from "@deadhead/content";
|
||||
import { routes, type ServerTime } from "@deadhead/schema";
|
||||
import { realToGame } from "@deadhead/sim";
|
||||
import Fastify, { type FastifyInstance } from "fastify";
|
||||
import { type Clock, systemClock } from "./clock";
|
||||
import type { ServerConfig } from "./config";
|
||||
|
||||
export interface AppDeps {
|
||||
clock?: Clock;
|
||||
}
|
||||
|
||||
/** Builds the Fastify app without listening, so tests can start it on any port. */
|
||||
export function buildApp(config: ServerConfig): FastifyInstance {
|
||||
export function buildApp(config: ServerConfig, deps: AppDeps = {}): FastifyInstance {
|
||||
const clock = deps.clock ?? systemClock;
|
||||
const app = Fastify({
|
||||
// Structured JSON logs (pino); silenced in tests unless LOG_LEVEL says otherwise.
|
||||
logger: { level: config.logLevel },
|
||||
|
|
@ -11,5 +20,10 @@ export function buildApp(config: ServerConfig): FastifyInstance {
|
|||
// Under /api so it is reachable through the same-domain proxy (decision 0.5.20).
|
||||
app.get("/api/health", async () => ({ status: "ok" }));
|
||||
|
||||
app.get(routes.getServerTime.path, async (): Promise<ServerTime> => {
|
||||
const realTime = clock();
|
||||
return { realTime, gameTime: realToGame(realTime, content.config) };
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
import type { RealTime } from "@deadhead/sim";
|
||||
|
||||
/** The server's only source of "now" (decision 0.5.18), so tests can control time. */
|
||||
export type Clock = () => RealTime;
|
||||
|
||||
export const systemClock: Clock = () => Date.now();
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export { buildApp } from "./app";
|
||||
export { buildApp, type AppDeps } from "./app";
|
||||
export { systemClock, type Clock } from "./clock";
|
||||
export { loadConfig, type ServerConfig } from "./config";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import { content } from "@deadhead/content";
|
||||
import { serverTimeSchema } from "@deadhead/schema";
|
||||
import { realToGame } from "@deadhead/sim";
|
||||
import { afterAll, describe, expect, it } from "vitest";
|
||||
import { buildApp } from "../src/app";
|
||||
import { testConfig } from "./helpers";
|
||||
|
||||
describe("server time endpoint", () => {
|
||||
const app = buildApp(testConfig());
|
||||
afterAll(() => app.close());
|
||||
|
||||
it("returns the current real time and the matching game time", async () => {
|
||||
const response = await app.inject({ method: "GET", url: "/api/time" });
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = serverTimeSchema.parse(response.json());
|
||||
expect(Math.abs(body.realTime - Date.now())).toBeLessThan(2000);
|
||||
expect(body.gameTime).toBe(realToGame(body.realTime, content.config));
|
||||
});
|
||||
|
||||
it("reads time from the injected clock", async () => {
|
||||
const fixed = Date.parse("2100-06-01T12:00:00Z");
|
||||
const fixedApp = buildApp(testConfig(), { clock: () => fixed });
|
||||
const body = (await fixedApp.inject({ method: "GET", url: "/api/time" })).json();
|
||||
expect(body).toEqual({ realTime: fixed, gameTime: realToGame(fixed, content.config) });
|
||||
await fixedApp.close();
|
||||
});
|
||||
});
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
## Phase 5 — Server Foundation
|
||||
|
||||
- [x] **Step 5.1 — Fastify skeleton.** `apps/server`: Zod-validated env config (`src/config.ts`), `buildApp` with pino JSON logging and `GET /api/health` (`src/app.ts`), entry point `src/main.ts` (loads repo-root `.env` if present). `pnpm --filter @deadhead/server dev|start` run it with tsx. Integration test starts the app on an ephemeral port and calls health over HTTP.
|
||||
- [ ] Step 5.2 — Server time endpoint
|
||||
- [x] **Step 5.2 — Server time endpoint.** `GET /api/time` returns `{ realTime, gameTime }` from an injectable `Clock` (`src/clock.ts`, passed via `buildApp(config, { clock })`).
|
||||
- [ ] Step 5.3 — Database schema and migrations
|
||||
- [ ] Step 5.4 — Test database harness
|
||||
- [ ] Step 5.5 — Guest accounts
|
||||
|
|
|
|||
Loading…
Reference in New Issue