test: add integration tests for reconcile HTTP endpoint
This commit is contained in:
parent
880cb2de4b
commit
cd6e8ea120
225
src/integration.test.ts
Normal file
225
src/integration.test.ts
Normal file
@ -0,0 +1,225 @@
|
||||
import { assertEquals, assertExists } from "@std/assert";
|
||||
import { createHandler } from "./server.ts";
|
||||
import { NetbirdClient } from "./netbird/client.ts";
|
||||
import type { GiteaClient } from "./gitea/client.ts";
|
||||
import type { Config } from "./config.ts";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Mock NetBird API
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
interface ApiCall {
|
||||
method: string;
|
||||
path: string;
|
||||
body?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const MOCK_BASE = "https://nb.test/api";
|
||||
const TEST_KEY_VALUE = "NBSK-test-key-value-abc123";
|
||||
|
||||
function createMockFetch(calls: ApiCall[]) {
|
||||
let groupCounter = 0;
|
||||
|
||||
return async (
|
||||
input: string | URL | Request,
|
||||
init?: RequestInit,
|
||||
): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input.toString();
|
||||
const method = init?.method ?? "GET";
|
||||
const path = url.replace(MOCK_BASE, "");
|
||||
const body = init?.body ? JSON.parse(init.body as string) : undefined;
|
||||
|
||||
calls.push({ method, path, body });
|
||||
|
||||
// Route GET list endpoints — return empty arrays
|
||||
if (method === "GET") {
|
||||
return Response.json([]);
|
||||
}
|
||||
|
||||
// POST /groups — return a created group
|
||||
if (method === "POST" && path === "/groups") {
|
||||
groupCounter++;
|
||||
return Response.json({
|
||||
id: `mock-group-${groupCounter}`,
|
||||
name: body.name,
|
||||
peers_count: 0,
|
||||
peers: [],
|
||||
issued: "api",
|
||||
});
|
||||
}
|
||||
|
||||
// POST /setup-keys — return a created key
|
||||
if (method === "POST" && path === "/setup-keys") {
|
||||
return Response.json({
|
||||
id: 1,
|
||||
name: body.name,
|
||||
type: body.type,
|
||||
key: TEST_KEY_VALUE,
|
||||
expires: "2027-01-01T00:00:00Z",
|
||||
valid: true,
|
||||
revoked: false,
|
||||
used_times: 0,
|
||||
state: "valid",
|
||||
auto_groups: body.auto_groups ?? [],
|
||||
usage_limit: body.usage_limit ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
// POST /policies — return a created policy
|
||||
if (method === "POST" && path === "/policies") {
|
||||
return Response.json({
|
||||
id: "mock-policy-1",
|
||||
name: body.name,
|
||||
description: body.description ?? "",
|
||||
enabled: body.enabled ?? true,
|
||||
rules: body.rules ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE — 204 No Content
|
||||
if (method === "DELETE") {
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
return Response.json({ error: "mock: unhandled route" }, { status: 500 });
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test fixtures
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const MOCK_CONFIG: Config = {
|
||||
netbirdApiUrl: MOCK_BASE,
|
||||
netbirdApiToken: "nb-test-token",
|
||||
giteaUrl: "https://gitea.test",
|
||||
giteaToken: "gitea-test-token",
|
||||
giteaRepo: "org/repo",
|
||||
reconcilerToken: "secret",
|
||||
pollIntervalSeconds: 30,
|
||||
port: 8080,
|
||||
dataDir: "/data",
|
||||
};
|
||||
|
||||
/** Desired state with one group and one setup key referencing it. */
|
||||
const DESIRED_STATE = {
|
||||
groups: {
|
||||
pilots: { peers: [] },
|
||||
},
|
||||
setup_keys: {
|
||||
"Pilot-hawk-72": {
|
||||
type: "one-off",
|
||||
expires_in: 604800,
|
||||
usage_limit: 1,
|
||||
auto_groups: ["pilots"],
|
||||
enrolled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function buildHandler(calls: ApiCall[]) {
|
||||
const mockFetch = createMockFetch(calls);
|
||||
const netbird = new NetbirdClient(MOCK_BASE, "nb-test-token", mockFetch);
|
||||
|
||||
// The GiteaClient is not exercised in reconcile tests — stub it out.
|
||||
const gitea = {} as GiteaClient;
|
||||
|
||||
return createHandler({
|
||||
config: MOCK_CONFIG,
|
||||
netbird,
|
||||
gitea,
|
||||
reconcileInProgress: { value: false },
|
||||
});
|
||||
}
|
||||
|
||||
function authedRequest(path: string, body?: unknown): Request {
|
||||
return new Request(`http://localhost:8080${path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": "Bearer secret",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Tests
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
Deno.test("POST /reconcile?dry_run=true returns planned operations", async () => {
|
||||
const calls: ApiCall[] = [];
|
||||
const handler = buildHandler(calls);
|
||||
|
||||
const resp = await handler(
|
||||
authedRequest("/reconcile?dry_run=true", DESIRED_STATE),
|
||||
);
|
||||
assertEquals(resp.status, 200);
|
||||
|
||||
const json = await resp.json();
|
||||
assertEquals(json.status, "planned");
|
||||
|
||||
const opTypes = json.operations.map((op: { type: string }) => op.type);
|
||||
assertEquals(opTypes.includes("create_group"), true);
|
||||
assertEquals(opTypes.includes("create_setup_key"), true);
|
||||
});
|
||||
|
||||
Deno.test("POST /reconcile apply creates resources and returns keys", async () => {
|
||||
const calls: ApiCall[] = [];
|
||||
const handler = buildHandler(calls);
|
||||
|
||||
const resp = await handler(authedRequest("/reconcile", DESIRED_STATE));
|
||||
assertEquals(resp.status, 200);
|
||||
|
||||
const json = await resp.json();
|
||||
assertEquals(json.status, "applied");
|
||||
|
||||
// The created setup key's raw value should be in the response
|
||||
assertExists(json.created_keys["Pilot-hawk-72"]);
|
||||
assertEquals(json.created_keys["Pilot-hawk-72"], TEST_KEY_VALUE);
|
||||
|
||||
// Verify mock API received the expected POST calls
|
||||
const postGroups = calls.filter(
|
||||
(c) => c.method === "POST" && c.path === "/groups",
|
||||
);
|
||||
assertEquals(postGroups.length, 1);
|
||||
assertEquals(postGroups[0].body?.name, "pilots");
|
||||
|
||||
const postKeys = calls.filter(
|
||||
(c) => c.method === "POST" && c.path === "/setup-keys",
|
||||
);
|
||||
assertEquals(postKeys.length, 1);
|
||||
assertEquals(postKeys[0].body?.name, "Pilot-hawk-72");
|
||||
});
|
||||
|
||||
Deno.test("POST /reconcile rejects unauthorized requests", async () => {
|
||||
const calls: ApiCall[] = [];
|
||||
const handler = buildHandler(calls);
|
||||
|
||||
const req = new Request("http://localhost:8080/reconcile", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": "Bearer wrong-token",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(DESIRED_STATE),
|
||||
});
|
||||
|
||||
const resp = await handler(req);
|
||||
assertEquals(resp.status, 401);
|
||||
|
||||
const json = await resp.json();
|
||||
assertEquals(json.error, "unauthorized");
|
||||
});
|
||||
|
||||
Deno.test("GET /health returns ok", async () => {
|
||||
const calls: ApiCall[] = [];
|
||||
const handler = buildHandler(calls);
|
||||
|
||||
const req = new Request("http://localhost:8080/health", { method: "GET" });
|
||||
const resp = await handler(req);
|
||||
assertEquals(resp.status, 200);
|
||||
|
||||
const json = await resp.json();
|
||||
assertEquals(json.status, "ok");
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user