189 lines
6.5 KiB
TypeScript
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();
|
|
});
|
|
});
|