diff --git a/ui/src/pages/secrets/ImportFromVaultDialog.test.tsx b/ui/src/pages/secrets/ImportFromVaultDialog.test.tsx index 3dab9f4442b..ea869eab375 100644 --- a/ui/src/pages/secrets/ImportFromVaultDialog.test.tsx +++ b/ui/src/pages/secrets/ImportFromVaultDialog.test.tsx @@ -357,6 +357,77 @@ describe("ImportFromVaultDialog", () => { }); }); + it("requires lowercase operator-entered keys during review", async () => { + const externalRef = "arn:aws:secretsmanager:us-east-1:1:secret:prod/openai-XYZ"; + mockSecretsApi.remoteImportPreview.mockResolvedValueOnce( + makePreview([ + makeCandidate({ + externalRef, + remoteName: "prod/openai", + name: "OpenAI API key", + key: "openai-api-key", + }), + ]), + ); + + const { queryClient } = makeWrapper(); + const root = createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); + await flush(); + await flush(); + + const row = document.querySelector( + `[data-testid="vault-row-${externalRef}"]`, + ) as HTMLElement | null; + await act(async () => { + row?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flush(); + + const continueBtn = Array.from(document.querySelectorAll("button")).find( + (btn) => btn.textContent?.includes("Continue"), + ); + await act(async () => { + continueBtn!.click(); + }); + await flush(); + + const keyInput = document.querySelector( + `[data-testid="review-key-${externalRef}"]`, + ) as HTMLInputElement | null; + const valueSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + "value", + )?.set; + await act(async () => { + valueSetter?.call(keyInput, "MY_KEY"); + keyInput!.dispatchEvent(new Event("input", { bubbles: true })); + }); + await flush(); + + expect(document.body.textContent).toContain("lowercase letters"); + const importBtn = Array.from(document.querySelectorAll("button")).find( + (btn) => btn.textContent?.startsWith("Import "), + ) as HTMLButtonElement | undefined; + expect(importBtn?.disabled).toBe(true); + + await act(async () => { + root.unmount(); + }); + }); + it("submits the operator-entered review description", async () => { const externalRef = "arn:aws:secretsmanager:us-east-1:1:secret:prod/openai-XYZ"; mockSecretsApi.remoteImportPreview.mockResolvedValueOnce( diff --git a/ui/src/pages/secrets/ImportFromVaultDialog.tsx b/ui/src/pages/secrets/ImportFromVaultDialog.tsx index 5bfe6e6c56c..c29424769ac 100644 --- a/ui/src/pages/secrets/ImportFromVaultDialog.tsx +++ b/ui/src/pages/secrets/ImportFromVaultDialog.tsx @@ -71,7 +71,7 @@ interface DraftSelection { description: string; } -const KEY_PATTERN = /^[a-zA-Z0-9_.-]+$/; +const KEY_PATTERN = /^[a-z0-9_.-]+$/; const PAGE_SIZE = 50; function isAwsSelectable(config: CompanySecretProviderConfig) { @@ -267,7 +267,7 @@ function validateDraftRow( if (draft.name.length > 160) return "Name must be 160 characters or fewer."; if (!draft.key.trim()) return "Key is required."; if (!KEY_PATTERN.test(draft.key)) { - return "Key may only contain letters, numbers, dot, underscore, or hyphen."; + return "Key may only contain lowercase letters, numbers, dot, underscore, or hyphen."; } if (draft.key.length > 120) return "Key must be 120 characters or fewer."; if (draft.description.length > 500) return "Description must be 500 characters or fewer.";