Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/health-check-platform-endpoints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@openmapx/extension-contracts": minor
"@openmapx/extension-cli": patch
---

Accept `${endpoint:<id>}` in integration health-check URL templates and headers: the instance of a platform backend that the platform's own client queries (the self-hosted service, then the operator's URL, then the public default). `HEALTH_CHECK_ENDPOINT_IDS` lists the ids, currently `overpass`. Validation rejects any other id, and an endpoint anywhere but as the whole URL prefix of `urlTemplate`, including in headers. Rebuild the CLI against the updated portable contracts.
152 changes: 151 additions & 1 deletion apps/api/src/services/__tests__/integration-health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const coreServerMocks = vi.hoisted(() => ({
safeFetchStatus: vi.fn(),
}));
const dnsMocks = vi.hoisted(() => ({ lookup: vi.fn() }));
const serviceUrls = vi.hoisted(() => new Map<string, string>());
const netMocks = vi.hoisted(() => ({ createConnection: vi.fn() }));

vi.mock("@openmapx/core/server", () => coreServerMocks);
Expand All @@ -23,7 +24,7 @@ vi.mock("node:dns/promises", () => dnsMocks);
vi.mock("../service-registry", () => ({
serviceUrl: (id: string) => {
if (id === "valhalla") return "http://valhalla:8002";
return undefined;
return serviceUrls.get(id);
},
}));

Expand Down Expand Up @@ -553,6 +554,155 @@ describe("integration-health placeholder substitution", () => {
});
});

// biome-ignore lint/suspicious/noTemplateCurlyInString: manifest template syntax
describe("integration-health ${endpoint:...} placeholders", () => {
let fetchSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(null, { status: 200 }));
});

afterEach(() => {
serviceUrls.clear();
vi.unstubAllEnvs();
vi.restoreAllMocks();
});

function manifestIntegration(id: string): LoadedIntegration {
const manifest = JSON.parse(
readFileSync(resolve(REPO_ROOT, `integrations/${id}/manifest.json`), "utf8"),
);
return { ...makeIntegration(manifest.healthCheck), id, manifest, isBuiltIn: true };
}

it.each([
[
"elsewhere in the URL",
// biome-ignore lint/suspicious/noTemplateCurlyInString: manifest template syntax
{ type: "http", urlTemplate: "https://collector.example/?u=${endpoint:overpass}" },
],
[
"as a host prefix",
// biome-ignore lint/suspicious/noTemplateCurlyInString: manifest template syntax
{ type: "http", urlTemplate: "${endpoint:overpass}@collector.example/status" },
],
[
"in a header",
{
type: "http",
url: "https://collector.example/status",
// biome-ignore lint/suspicious/noTemplateCurlyInString: manifest template syntax
headers: { "X-Upstream": "${endpoint:overpass}" },
},
],
])("never sends the operator's endpoint %s", async (_where, healthCheck) => {
vi.stubEnv("OVERPASS_URL", "https://mapper:s3cret-pass@overpass.example.org");

const [status] = await executeIntegrationHealthCheck({
...makeIntegration(healthCheck),
isBuiltIn: true,
});

expect(status).toMatchObject({ status: "down", url: "health check misconfigured" });
expect(coreServerMocks.safeFetchStatus).not.toHaveBeenCalled();
expect(JSON.stringify(status)).not.toMatch(/s3cret-pass|mapper/);
});

it("does not resolve an endpoint for a community integration", async () => {
serviceUrls.set("overpass", "http://overpass:80");

const [status] = await executeIntegrationHealthCheck(
// biome-ignore lint/suspicious/noTemplateCurlyInString: manifest template syntax
makeIntegration({ type: "http", urlTemplate: "${endpoint:overpass}/api/status" }),
);

expect(status).toMatchObject({
status: "down",
error: "endpoint placeholders are reserved for built-in integrations",
});
expect(coreServerMocks.safeFetchStatus).not.toHaveBeenCalled();
});

it.each([
["poi-overpass", "poi-overpass"],
["transit-overpass", "transit-overpass"],
["overlay-hiking", "overlay-hiking:OpenStreetMap Overpass"],
])("%s probes the self-hosted Overpass when it is enabled", async (id, statusId) => {
vi.stubEnv("OVERPASS_URL", "https://overpass.example.org");
serviceUrls.set("overpass", "http://overpass:80");

const statuses = await executeIntegrationHealthCheck(manifestIntegration(id));

expect(statuses.find((status) => status.id === statusId)).toMatchObject({
status: "up",
url: "http://overpass:80/api/status",
});
expect(coreServerMocks.safeFetchStatus).toHaveBeenCalledWith(
"http://overpass:80/api/status",
expect.objectContaining({ allowPrivateHosts: ["overpass"] }),
);
expect(fetchSpy.mock.calls.map((call: unknown[]) => String(call[0]))).not.toContain(
"https://overpass-api.de/api/status",
);
});

it("probes an operator's private instance, sending its URL credentials as Basic auth", async () => {
vi.stubEnv("OVERPASS_URL", "http://mapper:s3cret-pass@overpass.lan:8080/api/interpreter");

const [status] = await executeIntegrationHealthCheck(manifestIntegration("poi-overpass"));

expect(status).toMatchObject({ status: "up", url: "http://overpass.lan:8080/api/status" });
expect(coreServerMocks.safeFetchStatus).toHaveBeenCalledWith(
"http://overpass.lan:8080/api/status",
expect.objectContaining({
allowPrivateHosts: ["overpass.lan"],
allowedRedirectOrigin: "http://overpass.lan:8080",
headers: expect.objectContaining({
Authorization: `Basic ${Buffer.from("mapper:s3cret-pass").toString("base64")}`,
}),
}),
);
});

it("never shows URL credentials, even in an error message that repeats them", async () => {
vi.stubEnv("OVERPASS_URL", "https://mapper:s3cret-pass@overpass.example.org");
coreServerMocks.safeFetchStatus.mockRejectedValue(
new Error("request to https://mapper:s3cret-pass@overpass.example.org failed"),
);

const [status] = await executeIntegrationHealthCheck(manifestIntegration("poi-overpass"));

expect(status?.status).toBe("down");
expect(JSON.stringify(status)).not.toMatch(/s3cret-pass|mapper/);
});

it("probes the public default, as a public host, when nothing is configured", async () => {
vi.stubEnv("OVERPASS_URL", "");

const [status] = await executeIntegrationHealthCheck(manifestIntegration("transit-overpass"));

expect(status?.url).toBe("https://overpass-api.de/api/status");
expect(coreServerMocks.safeFetchStatus).toHaveBeenCalledWith(
"https://overpass-api.de/api/status",
expect.objectContaining({ allowPrivateHosts: [] }),
);
});

it("reports an endpoint id it does not know as unconfigured, without a request", async () => {
const [status] = await executeIntegrationHealthCheck({
// biome-ignore lint/suspicious/noTemplateCurlyInString: manifest template syntax
...makeIntegration({ type: "http", urlTemplate: "${endpoint:nominatim}/status" }),
isBuiltIn: true,
});

expect(status).toMatchObject({
status: "unconfigured",
url: "nominatim endpoint not configured",
});
expect(coreServerMocks.safeFetchStatus).not.toHaveBeenCalled();
});
});

describe("integration-health secret redaction", () => {
let fetchSpy: ReturnType<typeof vi.spyOn>;

Expand Down
Loading
Loading