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