Files
cfw-gateway/tests/gateway-worker.test.ts

189 lines
6.5 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import worker from "../src/index";
import type { Env, FetcherLike } from "../src/env";
function createJsonService(status: number, body: unknown): FetcherLike {
return {
fetch: vi.fn(async () =>
new Response(JSON.stringify(body), {
status,
headers: {
"content-type": "application/json",
},
}),
),
};
}
function createEnv(overrides: Partial<Env> = {}): Env {
return {
AUTH: createJsonService(401, { authenticated: false }),
OPS: createJsonService(404, { service: "ops" }),
SCHEDULER: createJsonService(404, { service: "scheduler" }),
...overrides,
};
}
describe("cfw-gateway worker", () => {
it("returns health status", async () => {
const env = createEnv();
const response = await worker.fetch(new Request("http://gateway.local/healthz"), env);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
ok: true,
service: "cfw-gateway",
});
});
it("proxies Better Auth routes to the auth service", async () => {
const auth = createJsonService(200, { proxied: true });
const env = createEnv({
AUTH: auth,
});
const response = await worker.fetch(new Request("http://gateway.local/api/auth/session"), env);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ proxied: true });
expect(auth.fetch).toHaveBeenCalledOnce();
});
it("proxies ops and scheduler routes with gateway prefixes removed", async () => {
const ops = createJsonService(200, { proxied: "ops" });
const scheduler = createJsonService(200, { proxied: "scheduler" });
const env = createEnv({
OPS: ops,
SCHEDULER: scheduler,
});
const opsResponse = await worker.fetch(
new Request("http://gateway.local/api/ops/observations?limit=10"),
env,
);
const schedulerResponse = await worker.fetch(
new Request("http://gateway.local/api/scheduler/jobs"),
env,
);
expect(opsResponse.status).toBe(200);
expect(schedulerResponse.status).toBe(200);
const opsRequest = vi.mocked(ops.fetch).mock.calls[0]?.[0];
const schedulerRequest = vi.mocked(scheduler.fetch).mock.calls[0]?.[0];
expect(new URL(opsRequest.url).pathname).toBe("/observations");
expect(new URL(opsRequest.url).search).toBe("?limit=10");
expect(new URL(schedulerRequest.url).pathname).toBe("/jobs");
});
it("serves Scalar API docs for the aggregated schema", async () => {
const response = await worker.fetch(new Request("http://gateway.local/docs"), createEnv());
expect(response.status).toBe(200);
const html = await response.text();
expect(html).toContain("Scalar API Reference");
expect(html).toContain('"url": "/openapi.json"');
});
it("serves the aggregated OpenAPI schema", async () => {
const env = createEnv({
AUTH: createJsonService(200, {
openapi: "3.1.0",
info: { title: "auth", version: "1.0.0" },
paths: { "/api/auth/sign-in/email": { post: { responses: { "200": { description: "ok" } } } } },
}),
OPS: createJsonService(200, {
openapi: "3.1.0",
info: { title: "ops", version: "1.0.0" },
paths: { "/observations": { post: { responses: { "202": { description: "accepted" } } } } },
}),
SCHEDULER: createJsonService(200, {
openapi: "3.1.0",
info: { title: "scheduler", version: "1.0.0" },
paths: { "/jobs": { get: { responses: { "200": { description: "ok" } } } } },
}),
});
const response = await worker.fetch(new Request("http://gateway.local/openapi.json"), env);
expect(response.status).toBe(200);
const schema = await response.json() as { info: { title: string }; paths: Record<string, unknown> };
expect(schema.info.title).toBe("cfw-gateway API");
expect(schema.paths["/api/auth/sign-in/email"]).toBeTruthy();
expect(schema.paths["/api/ops/observations"]).toBeTruthy();
expect(schema.paths["/api/scheduler/jobs"]).toBeTruthy();
});
it("serves the gateway-owned plugin manifest without consulting services", async () => {
const auth = createJsonService(401, { authenticated: false });
const ops = createJsonService(404, { service: "ops" });
const scheduler = createJsonService(404, { service: "scheduler" });
const env = createEnv({
AUTH: auth,
OPS: ops,
SCHEDULER: scheduler,
});
const response = await worker.fetch(new Request("http://gateway.local/mf-manifest.json"), env);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain("application/json");
expect(response.headers.get("cache-control")).toBe("public, max-age=60, stale-while-revalidate=300");
const manifest = await response.json() as {
schemaVersion: number;
kind: string;
plugins: Array<{
name: string;
entry: string;
routes: string[];
status: string;
}>;
};
expect(manifest.schemaVersion).toBe(1);
expect(manifest.kind).toBe("cfw-plugin-registry");
expect(manifest.plugins).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "account-center",
entry: "account_center@/apps/account-center/mf-manifest.json",
routes: ["/account-center/*"],
status: "enabled",
}),
]),
);
expect(auth.fetch).not.toHaveBeenCalled();
expect(ops.fetch).not.toHaveBeenCalled();
expect(scheduler.fetch).not.toHaveBeenCalled();
});
it("returns a diagnostic error when aggregated OpenAPI fetch fails", async () => {
const env = createEnv({
AUTH: {
fetch: vi.fn(async () => new Response("auth unavailable", { status: 503 })),
},
});
const response = await worker.fetch(new Request("http://gateway.local/openapi.json"), env);
expect(response.status).toBe(502);
await expect(response.json()).resolves.toEqual({
ok: false,
error: "openapi_aggregation_failed",
message: "Failed to fetch auth OpenAPI schema: 503",
});
});
it("returns not found for unknown routes without consulting auth service", async () => {
const auth = createJsonService(401, { authenticated: false });
const env = createEnv({
AUTH: auth,
});
const response = await worker.fetch(new Request("http://gateway.local/app"), env);
expect(response.status).toBe(404);
await expect(response.json()).resolves.toEqual({
ok: false,
error: "not_found",
});
expect(auth.fetch).not.toHaveBeenCalled();
});
});