mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
Move DB health to management API (#1757)
* Move DB health to management API * Address DB health review feedback
This commit is contained in:
@@ -7,15 +7,30 @@ import { MCP_TOOL_MAP, dbHealthCheckInput } from "../schemas/tools.ts";
|
||||
const mockFetch = vi.fn();
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const mockRunManagedDbHealthCheck = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../audit.ts", () => ({
|
||||
logToolCall: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../../../src/lib/db/core.ts", () => ({
|
||||
runManagedDbHealthCheck: mockRunManagedDbHealthCheck,
|
||||
}));
|
||||
|
||||
describe("omniroute_db_health_check MCP tool", () => {
|
||||
let client: Client;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockFetch.mockReset();
|
||||
mockRunManagedDbHealthCheck.mockReset();
|
||||
mockRunManagedDbHealthCheck.mockReturnValue({
|
||||
isHealthy: false,
|
||||
issues: [{ type: "broken_reference", table: "combos", description: "broken", count: 1 }],
|
||||
repairedCount: 1,
|
||||
backupCreated: true,
|
||||
autoRepair: true,
|
||||
checkedAt: new Date().toISOString(),
|
||||
});
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
const server = createMcpServer();
|
||||
await server.connect(serverTransport);
|
||||
@@ -38,32 +53,19 @@ describe("omniroute_db_health_check MCP tool", () => {
|
||||
expect(dbHealthCheckInput.safeParse({ autoRepair: "yes" }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("dispatches to /api/v1/db/health using POST when autoRepair=true", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
isHealthy: false,
|
||||
issues: [{ type: "broken_reference", table: "combos", description: "broken", count: 1 }],
|
||||
repairedCount: 1,
|
||||
backupCreated: true,
|
||||
autoRepair: true,
|
||||
checkedAt: new Date().toISOString(),
|
||||
}),
|
||||
});
|
||||
|
||||
it("runs the database repair flow directly when autoRepair=true", async () => {
|
||||
const result = await client.callTool({
|
||||
name: "omniroute_db_health_check",
|
||||
arguments: { autoRepair: true },
|
||||
});
|
||||
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/api/v1/db/health"),
|
||||
expect.objectContaining({ method: "POST" })
|
||||
);
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
expect(mockRunManagedDbHealthCheck).toHaveBeenCalledWith({ autoRepair: true });
|
||||
|
||||
const content = result.content[0] as { type: string; text: string };
|
||||
const payload = JSON.parse(content.text);
|
||||
expect(payload.autoRepair).toBe(true);
|
||||
expect(payload.repairedCount).toBe(1);
|
||||
expect(payload.backupCreated).toBe(true);
|
||||
});
|
||||
|
||||
@@ -882,7 +882,7 @@ export const dbHealthCheckTool: McpToolDefinition<
|
||||
scopes: ["read:health", "write:resilience"],
|
||||
auditLevel: "full",
|
||||
phase: 2,
|
||||
sourceEndpoints: ["/api/v1/db/health"],
|
||||
sourceEndpoints: ["/api/db/health"],
|
||||
};
|
||||
|
||||
// --- Tool 19: omniroute_sync_pricing ---
|
||||
|
||||
@@ -893,11 +893,8 @@ export async function handleDbHealthCheck(args: { autoRepair?: boolean }) {
|
||||
const autoRepair = args.autoRepair === true;
|
||||
|
||||
try {
|
||||
const result = toRecord(
|
||||
await apiFetch("/api/v1/db/health", {
|
||||
method: autoRepair ? "POST" : "GET",
|
||||
})
|
||||
);
|
||||
const { runManagedDbHealthCheck } = await import("../../../src/lib/db/core.ts");
|
||||
const result = runManagedDbHealthCheck({ autoRepair });
|
||||
|
||||
await logToolCall(
|
||||
"omniroute_db_health_check",
|
||||
|
||||
@@ -81,7 +81,7 @@ export default function HealthPage() {
|
||||
|
||||
const fetchDbHealth = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/v1/db/health");
|
||||
const res = await fetch("/api/db/health");
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
setDbHealth(json);
|
||||
@@ -138,7 +138,7 @@ export default function HealthPage() {
|
||||
const handleRepairDb = async () => {
|
||||
setRepairingDb(true);
|
||||
try {
|
||||
const res = await fetch("/api/v1/db/health", { method: "POST" });
|
||||
const res = await fetch("/api/db/health", { method: "POST" });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
setDbHealth(json);
|
||||
|
||||
@@ -167,7 +167,7 @@ async function mockHealthPageApis(page: Page) {
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/db/health", async (route) => {
|
||||
await page.route("**/api/db/health", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
|
||||
@@ -153,6 +153,7 @@ const cases: Case[] = [
|
||||
expectedClass: "MANAGEMENT",
|
||||
},
|
||||
{ name: "/api/keys MANAGEMENT", path: "/api/keys", expectedClass: "MANAGEMENT" },
|
||||
{ name: "/api/db/health MANAGEMENT", path: "/api/db/health", expectedClass: "MANAGEMENT" },
|
||||
{ name: "/api/settings MANAGEMENT", path: "/api/settings", expectedClass: "MANAGEMENT" },
|
||||
{ name: "/api/audit MANAGEMENT", path: "/api/audit", expectedClass: "MANAGEMENT" },
|
||||
|
||||
|
||||
@@ -174,6 +174,20 @@ test("runAuthzPipeline allows dashboard sessions to read model catalog aliases",
|
||||
assert.equal(response.headers.get("x-omniroute-route-class"), "CLIENT_API");
|
||||
});
|
||||
|
||||
test("runAuthzPipeline allows dashboard sessions to reach DB health management API", async () => {
|
||||
await forceAuthRequired();
|
||||
|
||||
const response = await pipeline.runAuthzPipeline(
|
||||
request("http://localhost/api/db/health", {
|
||||
headers: { cookie: await dashboardCookie() },
|
||||
}),
|
||||
{ enforce: true }
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.headers.get("x-omniroute-route-class"), "MANAGEMENT");
|
||||
});
|
||||
|
||||
test("runAuthzPipeline refreshes dashboard JWTs near expiry", async () => {
|
||||
await forceAuthRequired();
|
||||
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
|
||||
|
||||
@@ -3,29 +3,44 @@ import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { SignJWT } from "jose";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-db-health-route-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "task-303-api-key-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
||||
const routeModule = await import("../../src/app/api/v1/db/health/route.ts");
|
||||
const routeModule = await import("../../src/app/api/db/health/route.ts");
|
||||
|
||||
const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET;
|
||||
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
|
||||
const TEST_JWT_SECRET = "db-health-route-jwt-secret";
|
||||
const TEST_INITIAL_PASSWORD = "db-health-route-password";
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
||||
process.env.INITIAL_PASSWORD = TEST_INITIAL_PASSWORD;
|
||||
}
|
||||
|
||||
function makeRequest(method, token) {
|
||||
return new Request("http://localhost/api/v1/db/health", {
|
||||
function makeRequest(method, cookie) {
|
||||
return new Request("http://localhost/api/db/health", {
|
||||
method,
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
headers: cookie ? { cookie } : {},
|
||||
});
|
||||
}
|
||||
|
||||
async function dashboardCookie() {
|
||||
const secret = new TextEncoder().encode(TEST_JWT_SECRET);
|
||||
const token = await new SignJWT({ authenticated: true })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setExpirationTime("1h")
|
||||
.sign(secret);
|
||||
return `auth_token=${token}`;
|
||||
}
|
||||
|
||||
function insertBrokenRows(db) {
|
||||
db.prepare(
|
||||
`INSERT INTO quota_snapshots
|
||||
@@ -43,35 +58,27 @@ test.beforeEach(async () => {
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
if (ORIGINAL_JWT_SECRET === undefined) delete process.env.JWT_SECRET;
|
||||
else process.env.JWT_SECRET = ORIGINAL_JWT_SECRET;
|
||||
if (ORIGINAL_INITIAL_PASSWORD === undefined) delete process.env.INITIAL_PASSWORD;
|
||||
else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
|
||||
});
|
||||
|
||||
test("GET /api/v1/db/health requires authentication", async () => {
|
||||
const previousInitialPassword = process.env.INITIAL_PASSWORD;
|
||||
process.env.INITIAL_PASSWORD = "route-health-auth";
|
||||
test("GET /api/db/health requires authentication", async () => {
|
||||
const response = await routeModule.GET(makeRequest("GET"));
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
try {
|
||||
const response = await routeModule.GET(makeRequest("GET"));
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
assert.equal(body.error.message, "Authentication required");
|
||||
} finally {
|
||||
if (previousInitialPassword === undefined) {
|
||||
delete process.env.INITIAL_PASSWORD;
|
||||
} else {
|
||||
process.env.INITIAL_PASSWORD = previousInitialPassword;
|
||||
}
|
||||
}
|
||||
assert.equal(response.status, 401);
|
||||
assert.equal(body.error.message, "Authentication required");
|
||||
});
|
||||
|
||||
test("GET /api/v1/db/health diagnoses without mutating database rows", async () => {
|
||||
const authKey = await apiKeysDb.createApiKey("Health Route", "machine-route-health");
|
||||
test("GET /api/db/health diagnoses without mutating database rows", async () => {
|
||||
const cookie = await dashboardCookie();
|
||||
const db = core.getDbInstance();
|
||||
insertBrokenRows(db);
|
||||
|
||||
const response = await routeModule.GET(makeRequest("GET", authKey.key));
|
||||
const response = await routeModule.GET(makeRequest("GET", cookie));
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
@@ -81,12 +88,12 @@ test("GET /api/v1/db/health diagnoses without mutating database rows", async ()
|
||||
assert.equal((db.prepare("SELECT COUNT(*) AS count FROM domain_budgets").get() as any).count, 1);
|
||||
});
|
||||
|
||||
test("POST /api/v1/db/health repairs broken rows for authenticated callers", async () => {
|
||||
const authKey = await apiKeysDb.createApiKey("Health Route", "machine-route-health");
|
||||
test("POST /api/db/health repairs broken rows for authenticated callers", async () => {
|
||||
const cookie = await dashboardCookie();
|
||||
const db = core.getDbInstance();
|
||||
insertBrokenRows(db);
|
||||
|
||||
const response = await routeModule.POST(makeRequest("POST", authKey.key));
|
||||
const response = await routeModule.POST(makeRequest("POST", cookie));
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
Reference in New Issue
Block a user