test(ui): unit-test key edit route payload normalization

Extract route payload construction into a pure helper and move payload-shape assertions out of the rendered component suite.
This commit is contained in:
Patrick Ribbsaeter 2026-08-12 06:34:23 +02:00
parent 485ceda428
commit 599290aa81
4 changed files with 84 additions and 260 deletions

View file

@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { keyTypeFromRoutes, normalizeKeyEditRoutePayload } from "./keyEditFieldNormalizers";
describe("key edit route payload normalization", () => {
it("parses comma-separated custom routes", () => {
expect(normalizeKeyEditRoutePayload({ allowed_routes: "route1, route2, route3" }, [])).toEqual({
allowed_routes: ["route1", "route2", "route3"],
});
});
it("submits the default preset without derived routes", () => {
expect(normalizeKeyEditRoutePayload({ allowed_routes: "" }, ["llm_api_routes"])).toEqual({
key_type: "default",
});
});
it("submits the AI APIs preset without derived routes", () => {
expect(normalizeKeyEditRoutePayload({ allowed_routes: "llm_api_routes" }, [])).toEqual({
key_type: "llm_api",
});
});
it("preserves mixed allowlists without applying a preset", () => {
expect(normalizeKeyEditRoutePayload({ allowed_routes: "llm_api_routes, /custom" }, [])).toEqual({
allowed_routes: ["llm_api_routes", "/custom"],
});
});
it.each([
{ original: ["llm_api_routes"], submitted: "llm_api_routes" },
{ original: null, submitted: "" },
{ original: ["beta_routes", "alpha_routes"], submitted: "alpha_routes, beta_routes" },
])("omits unchanged routes: $original", ({ original, submitted }) => {
expect(normalizeKeyEditRoutePayload({ allowed_routes: submitted }, original)).toEqual({});
});
it("does not label a mixed allowlist as Full Access", () => {
expect(keyTypeFromRoutes(["llm_api_routes", "/custom"])).toBeUndefined();
});
});

View file

@ -50,3 +50,45 @@ export const currentValuePlaceholder = (
if (!premiumUser) return premiumHint;
return Array.isArray(current) && current.length > 0 ? `Current: ${current.join(", ")}` : emptyHint;
};
export type KeyEditRoutePayload = {
allowed_routes?: string[] | string | null;
key_type?: string;
[key: string]: unknown;
};
export const normalizeKeyEditRoutePayload = (
values: KeyEditRoutePayload,
originalAllowedRoutes: string[] | null | undefined,
): KeyEditRoutePayload => {
const normalized = { ...values };
if (typeof normalized.allowed_routes === "string") {
normalized.allowed_routes = normalized.allowed_routes
.split(",")
.map((route) => route.trim())
.filter(Boolean);
}
const originalRoutes = new Set(originalAllowedRoutes ?? []);
const submittedRoutes = new Set(Array.isArray(normalized.allowed_routes) ? normalized.allowed_routes : []);
const routesUnchanged =
originalRoutes.size === submittedRoutes.size && [...submittedRoutes].every((route) => originalRoutes.has(route));
if (routesUnchanged) {
delete normalized.allowed_routes;
return normalized;
}
const preset = exactKeyTypePresetFromRoutes(
Array.isArray(normalized.allowed_routes) ? normalized.allowed_routes : undefined,
);
if (preset) {
normalized.key_type = preset;
delete normalized.allowed_routes;
} else {
delete normalized.key_type;
}
return normalized;
};

View file

@ -11,7 +11,6 @@ import {
modelAvailableCall,
vectorStoreListCall,
} from "../networking";
import { keyTypeFromRoutes } from "./keyEditFieldNormalizers";
import { KeyEditView } from "./key_edit_view";
const can = vi.fn();
@ -135,13 +134,6 @@ vi.mock("../agent_management/AgentSelector", () => ({
</button>
),
}));
describe("keyTypeFromRoutes", () => {
it("does not label a mixed allowlist as Full Access", () => {
expect(keyTypeFromRoutes(["llm_api_routes", "/custom"])).toBeUndefined();
});
});
vi.mock("../common_components/AccessGroupSelector", () => ({
default: ({ value = [], onChange }: { value?: string[]; onChange?: (v: string[]) => void }) => (
<input
@ -690,222 +682,6 @@ describe("KeyEditView", () => {
});
});
it("should parse comma-separated allowed routes on submit", async () => {
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => {}}
onSubmit={onSubmitMock}
accessToken={"test-token"}
userID={"test-user"}
userRole={"admin"}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByLabelText(/allowed routes/i)).toBeInTheDocument();
});
const allowedRoutesInput = screen.getByLabelText(/allowed routes/i);
await userEvent.clear(allowedRoutesInput);
await userEvent.type(allowedRoutesInput, "route1, route2, route3");
const submitButton = screen.getByRole("button", { name: /save changes/i });
await userEvent.click(submitButton);
await waitFor(() => {
expect(onSubmitMock).toHaveBeenCalled();
const callArgs = onSubmitMock.mock.calls[0][0];
expect(Array.isArray(callArgs.allowed_routes)).toBe(true);
expect(callArgs.allowed_routes).toEqual(["route1", "route2", "route3"]);
});
});
it("should submit the default preset without derived allowed routes", async () => {
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
const keyDataWithRoutes = {
...MOCK_KEY_DATA,
allowed_routes: ["llm_api_routes"],
};
renderWithProviders(
<KeyEditView
keyData={keyDataWithRoutes}
onCancel={() => {}}
onSubmit={onSubmitMock}
accessToken={"test-token"}
userID={"test-user"}
userRole={"admin"}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByLabelText(/allowed routes/i)).toBeInTheDocument();
});
const allowedRoutesInput = screen.getByLabelText(/allowed routes/i);
await userEvent.clear(allowedRoutesInput);
const submitButton = screen.getByRole("button", { name: /save changes/i });
await userEvent.click(submitButton);
await waitFor(() => {
expect(onSubmitMock).toHaveBeenCalled();
const callArgs = onSubmitMock.mock.calls[0][0];
expect(callArgs.allowed_routes).toBeUndefined();
expect(callArgs.key_type).toBe("default");
});
});
it("should submit the AI APIs preset without derived allowed routes", async () => {
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => {}}
onSubmit={onSubmitMock}
accessToken={"test-token"}
userID={"test-user"}
userRole={"admin"}
premiumUser={false}
/>,
);
const allowedRoutesInput = await screen.findByLabelText(/allowed routes/i);
await userEvent.type(allowedRoutesInput, "llm_api_routes");
await userEvent.click(screen.getByRole("button", { name: /save changes/i }));
await waitFor(() => {
const callArgs = onSubmitMock.mock.calls[0][0];
expect(callArgs.allowed_routes).toBeUndefined();
expect(callArgs.key_type).toBe("llm_api");
});
});
it("should preserve mixed route allowlists without applying a preset", async () => {
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => {}}
onSubmit={onSubmitMock}
accessToken={"test-token"}
userID={"test-user"}
userRole={"admin"}
premiumUser={false}
/>,
);
const allowedRoutesInput = await screen.findByLabelText(/allowed routes/i);
await userEvent.clear(allowedRoutesInput);
await userEvent.type(allowedRoutesInput, "llm_api_routes, /custom");
await userEvent.click(screen.getByRole("button", { name: /save changes/i }));
await waitFor(() => {
const callArgs = onSubmitMock.mock.calls[0][0];
expect(callArgs.allowed_routes).toEqual(["llm_api_routes", "/custom"]);
expect(callArgs.key_type).toBeUndefined();
});
});
it("should omit allowed_routes from submit when value is unchanged", async () => {
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
const aiApisKeyData = {
...MOCK_KEY_DATA,
allowed_routes: ["llm_api_routes"],
};
renderWithProviders(
<KeyEditView
keyData={aiApisKeyData}
onCancel={() => {}}
onSubmit={onSubmitMock}
accessToken={"test-token"}
userID={"test-user"}
userRole={"admin"}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByRole("button", { name: /save changes/i })).toBeInTheDocument();
});
const submitButton = screen.getByRole("button", { name: /save changes/i });
await userEvent.click(submitButton);
await waitFor(() => {
expect(onSubmitMock).toHaveBeenCalled();
const callArgs = onSubmitMock.mock.calls[0][0];
expect("allowed_routes" in callArgs).toBe(false);
});
});
it("should omit allowed_routes from submit when keyData.allowed_routes is null and form is untouched", async () => {
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
const keyDataNullRoutes = {
...MOCK_KEY_DATA,
allowed_routes: null as unknown as string[],
};
renderWithProviders(
<KeyEditView
keyData={keyDataNullRoutes}
onCancel={() => {}}
onSubmit={onSubmitMock}
accessToken={"test-token"}
userID={"test-user"}
userRole={"admin"}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByRole("button", { name: /save changes/i })).toBeInTheDocument();
});
const submitButton = screen.getByRole("button", { name: /save changes/i });
await userEvent.click(submitButton);
await waitFor(() => {
expect(onSubmitMock).toHaveBeenCalled();
const callArgs = onSubmitMock.mock.calls[0][0];
expect("allowed_routes" in callArgs).toBe(false);
});
});
it("should omit allowed_routes from submit when server returned routes in a different order", async () => {
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
const keyDataReordered = {
...MOCK_KEY_DATA,
allowed_routes: ["beta_routes", "alpha_routes"],
};
renderWithProviders(
<KeyEditView
keyData={keyDataReordered}
onCancel={() => {}}
onSubmit={onSubmitMock}
accessToken={"test-token"}
userID={"test-user"}
userRole={"admin"}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByRole("button", { name: /save changes/i })).toBeInTheDocument();
});
const submitButton = screen.getByRole("button", { name: /save changes/i });
await userEvent.click(submitButton);
await waitFor(() => {
expect(onSubmitMock).toHaveBeenCalled();
const callArgs = onSubmitMock.mock.calls[0][0];
expect("allowed_routes" in callArgs).toBe(false);
});
});
it("should pass access_group_ids to onSubmit when saving key with access groups", async () => {
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
const keyDataWithAccessGroups = {

View file

@ -27,11 +27,10 @@ import RouterSettingsAccordion, { RouterSettingsAccordionRef } from "../common_c
import { routerSettingsEditorValue, routerSettingsUpdate } from "../common_components/routerSettingsPayload";
import { estimateTooltips, withNormalizedEstimates } from "./estimatedOutputTokens";
import {
canonicalBudgetDuration,
currentValuePlaceholder,
exactKeyTypePresetFromRoutes,
keyTypeFromRoutes,
modelSentinelOptions,
normalizeKeyEditRoutePayload,
parseAllowedRoutes,
} from "./keyEditFieldNormalizers";
import { KeyTypeSelect, labelWithHint } from "./KeyEditViewControls";
@ -215,40 +214,7 @@ export function KeyEditView({
try {
setIsKeySaving(true);
// Parse allowed_routes from comma-separated string to array
if (typeof values.allowed_routes === "string") {
const trimmedInput = values.allowed_routes.trim();
if (trimmedInput === "") {
values.allowed_routes = [];
} else {
values.allowed_routes = trimmedInput
.split(",")
.map((route: string) => route.trim())
.filter((route: string) => route.length > 0);
}
}
// If it's already an array (shouldn't happen, but handle it), keep as is
// Backend rejects non-empty allowed_routes from non-admins, so re-sending
// an unchanged value 403s a team admin. Set compare tolerates reorder.
const originalRoutesSet = new Set<string>(Array.isArray(keyData.allowed_routes) ? keyData.allowed_routes : []);
const submittedRoutesSet = new Set<string>(Array.isArray(values.allowed_routes) ? values.allowed_routes : []);
const allowedRoutesUnchanged =
originalRoutesSet.size === submittedRoutesSet.size &&
[...submittedRoutesSet].every((r) => originalRoutesSet.has(r));
if (allowedRoutesUnchanged) {
delete values.allowed_routes;
} else {
const exactKeyTypePreset = exactKeyTypePresetFromRoutes(values.allowed_routes);
if (exactKeyTypePreset) {
values.key_type = exactKeyTypePreset;
// The backend derives allowed_routes from key_type. Omitting the
// derived routes also lets authorized non-admins use safe presets.
delete values.allowed_routes;
} else {
delete values.key_type;
}
}
values = normalizeKeyEditRoutePayload(values, keyData.allowed_routes);
if (neverExpire) {
values.duration = null;