257 lines
7.6 KiB
TypeScript
257 lines
7.6 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import * as classifyPublicApi from "../../../src/server/authz/classify.ts";
|
|
import { classifyRoute } from "../../../src/server/authz/classify.ts";
|
|
import type { RouteClass } from "../../../src/server/authz/types.ts";
|
|
|
|
interface Case {
|
|
name: string;
|
|
path: string;
|
|
method?: string;
|
|
expectedClass: RouteClass;
|
|
expectedNormalized?: string;
|
|
}
|
|
|
|
const cases: Case[] = [
|
|
{ name: "root /", path: "/", expectedClass: "MANAGEMENT", expectedNormalized: "/" },
|
|
{ name: "dashboard root", path: "/dashboard", expectedClass: "MANAGEMENT" },
|
|
{ name: "dashboard nested", path: "/dashboard/settings", expectedClass: "MANAGEMENT" },
|
|
{ name: "dashboard onboarding", path: "/dashboard/onboarding", expectedClass: "PUBLIC" },
|
|
|
|
{
|
|
name: "/api/v1 base",
|
|
path: "/api/v1",
|
|
expectedClass: "CLIENT_API",
|
|
expectedNormalized: "/api/v1",
|
|
},
|
|
{
|
|
name: "/api/v1/chat/completions",
|
|
path: "/api/v1/chat/completions",
|
|
expectedClass: "CLIENT_API",
|
|
},
|
|
{ name: "/api/v1/responses", path: "/api/v1/responses", expectedClass: "CLIENT_API" },
|
|
{ name: "/api/v1/models", path: "/api/v1/models", expectedClass: "CLIENT_API" },
|
|
{ name: "/api/v1/embeddings", path: "/api/v1/embeddings", expectedClass: "CLIENT_API" },
|
|
{ name: "/api/v1/files", path: "/api/v1/files", expectedClass: "CLIENT_API" },
|
|
{ name: "/api/v1/batches", path: "/api/v1/batches", expectedClass: "CLIENT_API" },
|
|
{ name: "/api/v1/ws", path: "/api/v1/ws", expectedClass: "CLIENT_API" },
|
|
{ name: "/api/mcp/* stays management", path: "/api/mcp/status", expectedClass: "MANAGEMENT" },
|
|
|
|
{
|
|
name: "/v1 alias",
|
|
path: "/v1",
|
|
expectedClass: "CLIENT_API",
|
|
expectedNormalized: "/api/v1",
|
|
},
|
|
{
|
|
name: "/v1/chat/completions alias",
|
|
path: "/v1/chat/completions",
|
|
expectedClass: "CLIENT_API",
|
|
expectedNormalized: "/api/v1/chat/completions",
|
|
},
|
|
{
|
|
name: "/v1beta alias",
|
|
path: "/v1beta",
|
|
expectedClass: "CLIENT_API",
|
|
expectedNormalized: "/api/v1beta",
|
|
},
|
|
{
|
|
name: "/v1beta generateContent alias",
|
|
path: "/v1beta/models/gemini-pro:generateContent",
|
|
expectedClass: "CLIENT_API",
|
|
expectedNormalized: "/api/v1beta/models/gemini-pro:generateContent",
|
|
},
|
|
{
|
|
name: "/api/v1beta generateContent",
|
|
path: "/api/v1beta/models/gemini-pro:generateContent",
|
|
expectedClass: "CLIENT_API",
|
|
},
|
|
{
|
|
name: "/v1/v1 double-prefix",
|
|
path: "/v1/v1",
|
|
expectedClass: "CLIENT_API",
|
|
expectedNormalized: "/api/v1",
|
|
},
|
|
{
|
|
name: "/v1/v1/embeddings double-prefix",
|
|
path: "/v1/v1/embeddings",
|
|
expectedClass: "CLIENT_API",
|
|
expectedNormalized: "/api/v1/embeddings",
|
|
},
|
|
{
|
|
name: "/chat/completions alias",
|
|
path: "/chat/completions",
|
|
expectedClass: "CLIENT_API",
|
|
expectedNormalized: "/api/v1/chat/completions",
|
|
},
|
|
{
|
|
name: "/responses alias",
|
|
path: "/responses",
|
|
expectedClass: "CLIENT_API",
|
|
expectedNormalized: "/api/v1/responses",
|
|
},
|
|
{
|
|
name: "/responses/abc alias",
|
|
path: "/responses/abc",
|
|
expectedClass: "CLIENT_API",
|
|
expectedNormalized: "/api/v1/responses/abc",
|
|
},
|
|
{
|
|
name: "/codex/* alias collapses to /api/v1/responses",
|
|
path: "/codex/anything",
|
|
expectedClass: "CLIENT_API",
|
|
expectedNormalized: "/api/v1/responses",
|
|
},
|
|
{
|
|
name: "/models alias",
|
|
path: "/models",
|
|
expectedClass: "CLIENT_API",
|
|
expectedNormalized: "/api/v1/models",
|
|
},
|
|
|
|
{
|
|
name: "/api/auth/login is PUBLIC",
|
|
path: "/api/auth/login",
|
|
method: "POST",
|
|
expectedClass: "PUBLIC",
|
|
},
|
|
{
|
|
name: "/api/auth/logout is PUBLIC",
|
|
path: "/api/auth/logout",
|
|
method: "POST",
|
|
expectedClass: "PUBLIC",
|
|
},
|
|
{
|
|
name: "/api/auth/status is PUBLIC",
|
|
path: "/api/auth/status",
|
|
method: "GET",
|
|
expectedClass: "PUBLIC",
|
|
},
|
|
{ name: "/api/init is PUBLIC", path: "/api/init", method: "POST", expectedClass: "PUBLIC" },
|
|
{
|
|
name: "/api/monitoring/health is PUBLIC",
|
|
path: "/api/monitoring/health",
|
|
method: "GET",
|
|
expectedClass: "PUBLIC",
|
|
},
|
|
{
|
|
name: "/api/cloud/auth POST is PUBLIC",
|
|
path: "/api/cloud/auth",
|
|
method: "POST",
|
|
expectedClass: "PUBLIC",
|
|
},
|
|
{
|
|
name: "/api/cloud/model/resolve POST is PUBLIC",
|
|
path: "/api/cloud/model/resolve",
|
|
method: "POST",
|
|
expectedClass: "PUBLIC",
|
|
},
|
|
{
|
|
name: "/api/cloud/models/alias GET is PUBLIC",
|
|
path: "/api/cloud/models/alias",
|
|
method: "GET",
|
|
expectedClass: "PUBLIC",
|
|
},
|
|
{
|
|
name: "/api/cloud/credentials/update PUT is MANAGEMENT",
|
|
path: "/api/cloud/credentials/update",
|
|
method: "PUT",
|
|
expectedClass: "MANAGEMENT",
|
|
},
|
|
{
|
|
name: "/api/cloud/models/alias PUT is MANAGEMENT",
|
|
path: "/api/cloud/models/alias",
|
|
method: "PUT",
|
|
expectedClass: "MANAGEMENT",
|
|
},
|
|
{
|
|
name: "/api/cloud/unknown GET is MANAGEMENT",
|
|
path: "/api/cloud/unknown",
|
|
method: "GET",
|
|
expectedClass: "MANAGEMENT",
|
|
},
|
|
{
|
|
name: "/api/oauth/* is PUBLIC",
|
|
path: "/api/oauth/callback",
|
|
method: "GET",
|
|
expectedClass: "PUBLIC",
|
|
},
|
|
{
|
|
name: "/api/sync/bundle is PUBLIC",
|
|
path: "/api/sync/bundle",
|
|
method: "POST",
|
|
expectedClass: "PUBLIC",
|
|
},
|
|
{
|
|
name: "/api/settings/require-login GET is PUBLIC readonly",
|
|
path: "/api/settings/require-login",
|
|
method: "GET",
|
|
expectedClass: "PUBLIC",
|
|
},
|
|
{
|
|
name: "/api/settings/require-login POST is MANAGEMENT",
|
|
path: "/api/settings/require-login",
|
|
method: "POST",
|
|
expectedClass: "MANAGEMENT",
|
|
},
|
|
|
|
{
|
|
name: "/api/providers/* MUST stay MANAGEMENT",
|
|
path: "/api/providers/openai",
|
|
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" },
|
|
|
|
{
|
|
name: "/api/usage/om-usage is PUBLIC (handler enforces its own API key auth)",
|
|
path: "/api/usage/om-usage",
|
|
method: "GET",
|
|
expectedClass: "PUBLIC",
|
|
},
|
|
|
|
{
|
|
name: "Unknown top-level path defaults MANAGEMENT (fail-closed)",
|
|
path: "/totally-unknown",
|
|
expectedClass: "MANAGEMENT",
|
|
},
|
|
];
|
|
|
|
for (const c of cases) {
|
|
test(`classifyRoute: ${c.name}`, () => {
|
|
const r = classifyRoute(c.path, c.method ?? "GET");
|
|
assert.equal(r.routeClass, c.expectedClass, `routeClass for ${c.path}`);
|
|
if (c.expectedNormalized) {
|
|
assert.equal(r.normalizedPath, c.expectedNormalized, `normalizedPath for ${c.path}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
test("classifyRoute returns deterministic result for trailing slash", () => {
|
|
const a = classifyRoute("/api/v1/chat/completions", "POST");
|
|
const b = classifyRoute("/api/v1/chat/completions/", "POST");
|
|
assert.equal(a.routeClass, b.routeClass);
|
|
assert.equal(a.normalizedPath, b.normalizedPath);
|
|
});
|
|
|
|
test("classifyRoute strips trailing slash on root only when not '/'", () => {
|
|
assert.equal(classifyRoute("/").normalizedPath, "/");
|
|
});
|
|
|
|
test("classifyRoute treats /api/v1 prefix exactly", () => {
|
|
assert.equal(classifyRoute("/api/v1abc").routeClass, "MANAGEMENT");
|
|
assert.equal(classifyRoute("/api/v1/x").routeClass, "CLIENT_API");
|
|
assert.equal(classifyRoute("/api/v1betamax").routeClass, "MANAGEMENT");
|
|
assert.equal(classifyRoute("/api/v1beta/models").routeClass, "CLIENT_API");
|
|
});
|
|
|
|
test("classify module public surface only exposes route classification", () => {
|
|
assert.equal("classifyRoute" in classifyPublicApi, true);
|
|
assert.equal("isClientApi" in classifyPublicApi, false);
|
|
assert.equal("isManagement" in classifyPublicApi, false);
|
|
assert.equal("isPublic" in classifyPublicApi, false);
|
|
});
|