fix(ui): send the key's organization only when the edit form changes it

An untouched save from the key edit form now leaves organization_id out
of the /key/update body, the same way the form already drops unchanged
gated fields. This stops a save from writing a team's fallback
organization onto a key that has none, and keeps the audit log free of
no-op organization updates. The organization tests now use typed inputs
This commit is contained in:
mateo-berri 2026-10-20 20:32:09 -07:00
parent d712b29914
commit b8e2192f4d
2 changed files with 48 additions and 25 deletions

View file

@ -1,6 +1,7 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { toast } from "@/lib/toast";
import type { KeyResponse } from "../key_team_helpers/key_list";
vi.mock("@/lib/toast", () => ({
toast: { error: vi.fn(), success: vi.fn() },
@ -657,40 +658,58 @@ describe("KeyInfoView handleKeyUpdate soft_budget", () => {
});
describe("KeyInfoView handleKeyUpdate organization", () => {
it("should show no organization after a save clears the one a key list row carried as org_id", async () => {
const adminAuth = {
accessToken: "access_abc",
userId: "user_1",
userRole: "Admin",
premiumUser: true,
token: "token_123",
userEmail: "test@example.com",
disabledPersonalKeyCreation: false,
showSSOBanner: false,
};
keyUpdateCallMock.mockResolvedValueOnce({ ...baseKeyData, organization_id: null });
const keyListRow = { ...baseKeyData, org_id: "org-1" } as KeyResponse;
const adminAuth = {
accessToken: "access_abc",
userId: "user_1",
userRole: "Admin",
premiumUser: true,
token: "token_123",
userEmail: "test@example.com",
disabledPersonalKeyCreation: false,
showSSOBanner: false,
};
const renderKeyListRowSettings = () => {
mockUseAuthorized.mockReturnValue(adminAuth);
render(
<KeyInfoView
keyId="tok_123"
onClose={() => {}}
keyData={{ ...baseKeyData, org_id: "org-1" } as any}
onKeyDataUpdate={() => {}}
teams={[]}
/>,
<KeyInfoView keyId="tok_123" onClose={() => {}} keyData={keyListRow} onKeyDataUpdate={() => {}} teams={[]} />,
);
fireEvent.click(screen.getByText("Settings"));
expect(screen.getByText("Organization", { selector: "p" }).nextElementSibling).toHaveTextContent("org-1");
};
const submitEditForm = (formValues: Record<string, unknown>) => {
fireEvent.click(screen.getByText("Edit Settings"));
(globalThis as any).__TEST_FORM_VALUES = { token: "tok_123", organization_id: null };
Object.assign(globalThis, { __TEST_FORM_VALUES: formValues });
fireEvent.click(screen.getByText("Mock Submit"));
};
const shownOrganization = () => screen.getByText("Organization", { selector: "p" }).nextElementSibling;
it("should show no organization after a save clears the one a key list row carried as org_id", async () => {
keyUpdateCallMock.mockResolvedValueOnce({ ...baseKeyData, organization_id: null });
renderKeyListRowSettings();
expect(shownOrganization()).toHaveTextContent("org-1");
submitEditForm({ token: "tok_123", organization_id: null });
await waitFor(() => {
expect(screen.getByText("Organization", { selector: "p" }).nextElementSibling).toHaveTextContent("Not Set");
expect(shownOrganization()).toHaveTextContent("Not Set");
});
expect(keyUpdateCallMock.mock.calls[0][1]).toHaveProperty("organization_id", null);
});
it("should leave organization_id out of a save that keeps the key's organization", async () => {
keyUpdateCallMock.mockResolvedValueOnce({ ...baseKeyData, organization_id: "org-1" });
renderKeyListRowSettings();
submitEditForm({ token: "tok_123", organization_id: "org-1" });
await waitFor(() => {
expect(keyUpdateCallMock).toHaveBeenCalledTimes(1);
});
expect(keyUpdateCallMock.mock.calls[0][1]).not.toHaveProperty("organization_id");
expect(shownOrganization()).toHaveTextContent("org-1");
});
});

View file

@ -240,6 +240,10 @@ export default function KeyInfoView({
delete formValues.disable_global_guardrails;
}
if ((formValues.organization_id || null) === keyOrganizationId(currentKeyData)) {
delete formValues.organization_id;
}
// Handle max budget empty string
formValues.max_budget = mapEmptyStringToNull(formValues.max_budget);