mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
fix(ui): omit blank expiry from key edit and regenerate payloads (#48634)
* fix(ui): omit blank expiry from key edit and regenerate payloads Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: credit #45048 author Co-authored-by: Thomas Scholtz <thomas@quantum-sicarius.za.net> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * style(ui): format regenerate key payload builder Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(ui): compare regenerate payloads by fields, not JSON order Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Thomas Scholtz <thomas@quantum-sicarius.za.net>
This commit is contained in:
parent
59b8c73930
commit
1b932332c1
6 changed files with 93 additions and 29 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders, screen, waitFor } from "../../../tests/test-utils";
|
||||
import { RegenerateKeyModal } from "./RegenerateKeyModal";
|
||||
|
|
@ -58,16 +59,22 @@ describe("RegenerateKeyModal submit payload", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("keeps the wire body's key order and drops nothing through JSON serialisation", async () => {
|
||||
it("submits the expected regenerated key fields", async () => {
|
||||
const user = userEvent.setup();
|
||||
const expectedPayload = {
|
||||
key_alias: "my-test-key",
|
||||
max_budget: 100,
|
||||
tpm_limit: 5000,
|
||||
rpm_limit: 500,
|
||||
grace_period: "",
|
||||
duration: "30d",
|
||||
};
|
||||
renderModal();
|
||||
|
||||
await regenerate(user);
|
||||
|
||||
await waitFor(() => expect(mockRegenerateKeyCall).toHaveBeenCalledOnce());
|
||||
expect(JSON.stringify(submittedPayload())).toBe(
|
||||
'{"key_alias":"my-test-key","max_budget":100,"tpm_limit":5000,"rpm_limit":500,"duration":"30d","grace_period":""}',
|
||||
);
|
||||
expect(submittedPayload()).toEqual(expectedPayload);
|
||||
});
|
||||
|
||||
it("rounds max_budget to two decimals and sends the other numbers unrounded", async () => {
|
||||
|
|
@ -138,7 +145,7 @@ describe("RegenerateKeyModal submit payload", () => {
|
|||
expect(submittedPayload()[key]).toBe(12.7);
|
||||
});
|
||||
|
||||
it("sends null for cleared numbers and an empty string for a cleared duration", async () => {
|
||||
it("omits a cleared duration while sending null for cleared numbers", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderModal();
|
||||
|
||||
|
|
@ -155,7 +162,6 @@ describe("RegenerateKeyModal submit payload", () => {
|
|||
max_budget: null,
|
||||
tpm_limit: null,
|
||||
rpm_limit: null,
|
||||
duration: "",
|
||||
grace_period: "",
|
||||
});
|
||||
});
|
||||
|
|
@ -180,10 +186,9 @@ describe("RegenerateKeyModal submit payload", () => {
|
|||
max_budget: undefined,
|
||||
tpm_limit: undefined,
|
||||
rpm_limit: undefined,
|
||||
duration: "",
|
||||
grace_period: "",
|
||||
});
|
||||
expect(JSON.stringify(submittedPayload())).toBe('{"duration":"","grace_period":""}');
|
||||
expect(JSON.parse(JSON.stringify(submittedPayload()))).toEqual({ grace_period: "" });
|
||||
});
|
||||
|
||||
it("regenerates a key whose fields the API returned as null", async () => {
|
||||
|
|
@ -206,11 +211,21 @@ describe("RegenerateKeyModal submit payload", () => {
|
|||
max_budget: null,
|
||||
tpm_limit: null,
|
||||
rpm_limit: null,
|
||||
duration: "",
|
||||
grace_period: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("sends a typed duration when the original key has no stored duration", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderModal(makeToken({ duration: undefined }));
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("e.g. 30s, 30h, 30d"), { target: { value: "30d" } });
|
||||
await regenerate(user);
|
||||
|
||||
await waitFor(() => expect(mockRegenerateKeyCall).toHaveBeenCalledOnce());
|
||||
expect(submittedPayload()).toHaveProperty("duration", "30d");
|
||||
});
|
||||
|
||||
it("targets the key by its token hash", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderModal();
|
||||
|
|
|
|||
|
|
@ -62,6 +62,13 @@ describe("buildRegenerateKeyPayload", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it.each(["", " "])("omits blank duration %j from the payload and JSON body", (duration) => {
|
||||
const payload = buildRegenerateKeyPayload(values({ duration }));
|
||||
|
||||
expect(payload).not.toHaveProperty("duration");
|
||||
expect(JSON.stringify(payload)).not.toContain('"duration"');
|
||||
});
|
||||
|
||||
it.each([
|
||||
["null", null],
|
||||
["undefined", undefined],
|
||||
|
|
@ -82,18 +89,10 @@ describe("buildRegenerateKeyPayload", () => {
|
|||
duration: "30d",
|
||||
grace_period: "",
|
||||
});
|
||||
expect(JSON.stringify(payload)).toBe('{"duration":"30d","grace_period":""}');
|
||||
});
|
||||
|
||||
it("preserves the field order the proxy request body is serialised in", () => {
|
||||
expect(Object.keys(buildRegenerateKeyPayload(values({ max_budget: 1.239 })))).toStrictEqual([
|
||||
"key_alias",
|
||||
"max_budget",
|
||||
"tpm_limit",
|
||||
"rpm_limit",
|
||||
"duration",
|
||||
"grace_period",
|
||||
]);
|
||||
expect(JSON.parse(JSON.stringify(payload))).toEqual({
|
||||
grace_period: "",
|
||||
duration: "30d",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not mutate the values it is handed", () => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ export interface RegenerateKeyFormValues {
|
|||
grace_period: string;
|
||||
}
|
||||
|
||||
export type RegenerateKeyPayload = Omit<RegenerateKeyFormValues, "duration"> & { duration?: string };
|
||||
|
||||
const shiftExponent = (value: number, places: number): number => {
|
||||
const [mantissa, exponent = "0"] = value.toExponential().split("e");
|
||||
return Number(`${mantissa}e${Number(exponent) + places}`);
|
||||
|
|
@ -21,10 +23,13 @@ export const roundToPrecision = (value: number, precision: number): number => {
|
|||
return value < 0 ? -rounded : rounded;
|
||||
};
|
||||
|
||||
export const buildRegenerateKeyPayload = (values: RegenerateKeyFormValues): RegenerateKeyFormValues => ({
|
||||
...values,
|
||||
max_budget:
|
||||
typeof values.max_budget === "number"
|
||||
? roundToPrecision(values.max_budget, MAX_BUDGET_PRECISION)
|
||||
: values.max_budget,
|
||||
});
|
||||
export const buildRegenerateKeyPayload = (values: RegenerateKeyFormValues): RegenerateKeyPayload => {
|
||||
const { duration, ...rest } = values;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
max_budget:
|
||||
typeof rest.max_budget === "number" ? roundToPrecision(rest.max_budget, MAX_BUDGET_PRECISION) : rest.max_budget,
|
||||
...(duration.trim() !== "" ? { duration } : {}),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -53,3 +53,17 @@ describe("keyEditFormSchema", () => {
|
|||
expect(parse({ default_estimated_output_tokens_per_model: '{"gpt-4": 4096}' }).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("duration submission", () => {
|
||||
const submit = (duration: string | null) =>
|
||||
toSubmittedValues({ duration }, { canViewPolicies: false, canViewPrompts: false });
|
||||
|
||||
it.each(["", " "])("omits a blank duration value %j", (duration) => {
|
||||
expect(submit(duration)).not.toHaveProperty("duration");
|
||||
});
|
||||
|
||||
it("preserves a typed duration and null", () => {
|
||||
expect(submit("30d")).toHaveProperty("duration", "30d");
|
||||
expect(submit(null)).toHaveProperty("duration", null);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ export const toSubmittedValues = (
|
|||
team_id: values.team_id,
|
||||
logging_settings: values.logging_settings,
|
||||
metadata: values.metadata,
|
||||
duration: values.duration,
|
||||
...(typeof values.duration === "string" && values.duration.trim() === "" ? {} : { duration: values.duration }),
|
||||
token: values.token,
|
||||
disabled_callbacks: values.disabled_callbacks,
|
||||
auto_rotate: values.auto_rotate,
|
||||
|
|
|
|||
|
|
@ -2542,6 +2542,37 @@ describe("KeyEditView", () => {
|
|||
expect(onSubmitMock.mock.calls[0][0]).toHaveProperty("duration", null);
|
||||
});
|
||||
|
||||
it("omits an untouched blank duration for an expiring key", async () => {
|
||||
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
|
||||
renderForPayload(onSubmitMock, {
|
||||
...MOCK_KEY_DATA,
|
||||
expires: "2026-12-31T00:00:00Z",
|
||||
duration: undefined,
|
||||
});
|
||||
await screen.findByRole("button", { name: /save changes/i });
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() => expect(onSubmitMock).toHaveBeenCalledTimes(1));
|
||||
expect(onSubmitMock.mock.calls[0][0]).not.toHaveProperty("duration");
|
||||
});
|
||||
|
||||
it("submits a typed expiry duration", async () => {
|
||||
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
|
||||
renderForPayload(onSubmitMock, {
|
||||
...MOCK_KEY_DATA,
|
||||
expires: "2026-12-31T00:00:00Z",
|
||||
duration: undefined,
|
||||
});
|
||||
await screen.findByRole("button", { name: /save changes/i });
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("e.g., 30d"), { target: { value: "30d" } });
|
||||
await userEvent.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() => expect(onSubmitMock).toHaveBeenCalledTimes(1));
|
||||
expect(onSubmitMock.mock.calls[0][0]).toHaveProperty("duration", "30d");
|
||||
});
|
||||
|
||||
it("carries a typed value from every free-text and numeric control into the payload", async () => {
|
||||
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
|
||||
renderForPayload(onSubmitMock);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue