mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-23 05:34:41 +00:00
fix(ui): omit blank expiry from key payloads
The key edit form and the Regenerate Key dialog sent `duration: ""` when the expiry field was left blank. With `upperbound_key_generate_params.duration` set, the proxy's upperbound check parses it and rejects the request with "Invalid duration format". Drop `duration` from both payload builders when it is blank, so the request follows the update API contract: omitted leaves the expiry unchanged, null never expires, a value sets it
This commit is contained in:
parent
736ff14f11
commit
a4ca2e1f2a
6 changed files with 52 additions and 8 deletions
|
|
@ -138,7 +138,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("sends null for cleared numbers and omits a cleared duration", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderModal();
|
||||
|
||||
|
|
@ -155,7 +155,6 @@ describe("RegenerateKeyModal submit payload", () => {
|
|||
max_budget: null,
|
||||
tpm_limit: null,
|
||||
rpm_limit: null,
|
||||
duration: "",
|
||||
grace_period: "",
|
||||
});
|
||||
});
|
||||
|
|
@ -180,10 +179,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.stringify(submittedPayload())).toBe('{"grace_period":""}');
|
||||
});
|
||||
|
||||
it("regenerates a key whose fields the API returned as null", async () => {
|
||||
|
|
@ -206,7 +204,6 @@ describe("RegenerateKeyModal submit payload", () => {
|
|||
max_budget: null,
|
||||
tpm_limit: null,
|
||||
rpm_limit: null,
|
||||
duration: "",
|
||||
grace_period: "",
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -96,6 +96,17 @@ describe("buildRegenerateKeyPayload", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("omits a blank duration so the proxy leaves the expiry unchanged", () => {
|
||||
const payload = buildRegenerateKeyPayload(values({ duration: "" }));
|
||||
|
||||
expect(payload).not.toHaveProperty("duration");
|
||||
expect(JSON.stringify(payload)).not.toContain("duration");
|
||||
});
|
||||
|
||||
it("keeps a typed duration", () => {
|
||||
expect(buildRegenerateKeyPayload(values({ duration: "30d" })).duration).toBe("30d");
|
||||
});
|
||||
|
||||
it("does not mutate the values it is handed", () => {
|
||||
const original = values({ max_budget: 42.567 });
|
||||
|
||||
|
|
|
|||
|
|
@ -21,10 +21,16 @@ export const roundToPrecision = (value: number, precision: number): number => {
|
|||
return value < 0 ? -rounded : rounded;
|
||||
};
|
||||
|
||||
export const buildRegenerateKeyPayload = (values: RegenerateKeyFormValues): RegenerateKeyFormValues => ({
|
||||
...values,
|
||||
export type RegenerateKeyPayload = Omit<RegenerateKeyFormValues, "duration"> & { duration?: string };
|
||||
|
||||
export const buildRegenerateKeyPayload = (values: RegenerateKeyFormValues): RegenerateKeyPayload => ({
|
||||
key_alias: values.key_alias,
|
||||
max_budget:
|
||||
typeof values.max_budget === "number"
|
||||
? roundToPrecision(values.max_budget, MAX_BUDGET_PRECISION)
|
||||
: values.max_budget,
|
||||
tpm_limit: values.tpm_limit,
|
||||
rpm_limit: values.rpm_limit,
|
||||
...(values.duration === "" ? {} : { duration: values.duration }),
|
||||
grace_period: values.grace_period,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,22 @@ describe("tpd_limit round trip", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("duration in the submitted payload", () => {
|
||||
const submit = (duration: string | null) =>
|
||||
toSubmittedValues(
|
||||
{ ...toKeyEditFormValues({ token: "tok", models: [] } as unknown as KeyResponse), duration },
|
||||
{ canViewPolicies: true, canViewPrompts: true },
|
||||
);
|
||||
|
||||
it("omits a blank duration so the proxy leaves the expiry unchanged", () => {
|
||||
expect(submit("")).not.toHaveProperty("duration");
|
||||
});
|
||||
|
||||
it.each([["30d"], [null]])("keeps duration %s", (duration) => {
|
||||
expect(submit(duration)).toHaveProperty("duration", duration);
|
||||
});
|
||||
});
|
||||
|
||||
describe("keyEditFormSchema", () => {
|
||||
it("accepts an empty form", () => {
|
||||
expect(parse({}).success).toBe(true);
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ export const toSubmittedValues = (
|
|||
team_id: values.team_id,
|
||||
logging_settings: values.logging_settings,
|
||||
metadata: values.metadata,
|
||||
duration: values.duration,
|
||||
...(values.duration === "" ? {} : { duration: values.duration }),
|
||||
token: values.token,
|
||||
disabled_callbacks: values.disabled_callbacks,
|
||||
auto_rotate: values.auto_rotate,
|
||||
|
|
|
|||
|
|
@ -2218,6 +2218,20 @@ describe("KeyEditView", () => {
|
|||
expect(onSubmitMock.mock.calls[0][0]).toHaveProperty("duration", null);
|
||||
});
|
||||
|
||||
it("untouched expiry sends no duration key", async () => {
|
||||
const { duration: _storedDuration, ...keyWithoutDuration } = MOCK_KEY_DATA;
|
||||
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
|
||||
renderForPayload(onSubmitMock, { ...keyWithoutDuration, expires: "2030-01-01T00:00:00Z" } as KeyResponse);
|
||||
await screen.findByRole("button", { name: /save changes/i });
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmitMock).toHaveBeenCalled();
|
||||
});
|
||||
expect(onSubmitMock.mock.calls[0][0]).not.toHaveProperty("duration");
|
||||
});
|
||||
|
||||
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