add forgiving API key validation with inline errors

Add KeyValidation module for format-checking API keys before
attempting connections. Auto-strips whitespace, detects common
mistakes (e.g. pasting a Stripe publishable key), and returns
helpful error messages.

Inline field errors across all three entry points:
- Setup wizard: provider + Stripe keys
- Admin provider form: simplified to single Connect button
- Email settings: per-field errors instead of flash toasts

Also: plain text inputs for all API keys (not password fields),
accessible error states (aria-invalid, role=alert, thick border,
bold text), inner_block slot declaration on error component.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-03-04 12:17:56 +00:00
parent e139a75b69
commit 76cff0494e
10 changed files with 557 additions and 216 deletions

View File

@@ -0,0 +1,194 @@
defmodule Berrypod.KeyValidationTest do
use ExUnit.Case, async: true
alias Berrypod.KeyValidation
describe "validate_stripe_key/1" do
test "accepts valid test key" do
key = "sk_test_51OUJ0abc123XYZdef456"
assert {:ok, ^key} = KeyValidation.validate_stripe_key(key)
end
test "accepts valid live key" do
key = "sk_live_51OUJ0abc123XYZdef456"
assert {:ok, ^key} = KeyValidation.validate_stripe_key(key)
end
test "strips whitespace" do
key = "sk_test_51OUJ0abc123XYZdef456"
assert {:ok, ^key} = KeyValidation.validate_stripe_key(" #{key} ")
end
test "strips newlines from paste" do
key = "sk_test_51OUJ0abc123XYZdef456"
assert {:ok, ^key} = KeyValidation.validate_stripe_key("#{key}\n")
end
test "rejects empty key" do
assert {:error, msg} = KeyValidation.validate_stripe_key("")
assert msg =~ "Please enter"
end
test "rejects nil" do
assert {:error, msg} = KeyValidation.validate_stripe_key(nil)
assert msg =~ "Please enter"
end
test "rejects whitespace-only" do
assert {:error, msg} = KeyValidation.validate_stripe_key(" ")
assert msg =~ "Please enter"
end
test "detects publishable key" do
assert {:error, msg} = KeyValidation.validate_stripe_key("pk_test_abc123")
assert msg =~ "publishable key"
assert msg =~ "secret key"
end
test "detects restricted key" do
assert {:error, msg} = KeyValidation.validate_stripe_key("rk_live_abc123")
assert msg =~ "restricted key"
end
test "rejects key without correct prefix" do
assert {:error, msg} = KeyValidation.validate_stripe_key("some_random_key_value")
assert msg =~ "sk_test_"
end
test "rejects too-short key with correct prefix" do
assert {:error, msg} = KeyValidation.validate_stripe_key("sk_test_x")
assert msg =~ "too short"
end
end
describe "validate_provider_key/2" do
test "accepts reasonable key" do
assert {:ok, "abcdef1234567890abcdef"} =
KeyValidation.validate_provider_key("abcdef1234567890abcdef", "printify")
end
test "strips whitespace" do
assert {:ok, "abcdef1234567890abcdef"} =
KeyValidation.validate_provider_key(" abcdef1234567890abcdef ", "printful")
end
test "rejects empty key" do
assert {:error, msg} = KeyValidation.validate_provider_key("", "printify")
assert msg =~ "Please enter"
end
test "rejects nil" do
assert {:error, msg} = KeyValidation.validate_provider_key(nil, "printify")
assert msg =~ "Please enter"
end
test "rejects too-short key" do
assert {:error, msg} = KeyValidation.validate_provider_key("short", "printify")
assert msg =~ "too short"
end
test "works without provider type" do
assert {:ok, "abcdef1234567890abcdef"} =
KeyValidation.validate_provider_key("abcdef1234567890abcdef")
end
end
describe "validate_email_key/3 - SendGrid" do
test "accepts valid SendGrid key" do
key = "SG.abcdefghijklmnopqrstuv.abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "sendgrid", "api_key")
end
test "rejects key without SG. prefix" do
assert {:error, msg} =
KeyValidation.validate_email_key("not_a_sendgrid_key", "sendgrid", "api_key")
assert msg =~ "SG."
end
test "strips whitespace" do
key = "SG.abcdefghijklmnopqrstuv.abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"
assert {:ok, ^key} = KeyValidation.validate_email_key(" #{key} ", "sendgrid", "api_key")
end
end
describe "validate_email_key/3 - Postmark" do
test "accepts valid UUID" do
key = "abc12345-abcd-1234-abcd-123456789abc"
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "postmark", "api_key")
end
test "accepts uppercase UUID" do
key = "ABC12345-ABCD-1234-ABCD-123456789ABC"
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "postmark", "api_key")
end
test "rejects non-UUID" do
assert {:error, msg} = KeyValidation.validate_email_key("not-a-uuid", "postmark", "api_key")
assert msg =~ "UUID"
end
end
describe "validate_email_key/3 - Resend" do
test "accepts valid Resend key" do
assert {:ok, "re_abc123xyz"} =
KeyValidation.validate_email_key("re_abc123xyz", "resend", "api_key")
end
test "rejects key without re_ prefix" do
assert {:error, msg} = KeyValidation.validate_email_key("not_resend", "resend", "api_key")
assert msg =~ "re_"
end
end
describe "validate_email_key/3 - Mailgun" do
test "accepts classic key- format" do
key = "key-abcdef1234567890abcdef1234567890"
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "mailgun", "api_key")
end
test "accepts newer long key without prefix" do
key = "abcdef1234567890abcdef1234567890abcdef1234"
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "mailgun", "api_key")
end
test "rejects short key without prefix" do
assert {:error, msg} = KeyValidation.validate_email_key("short", "mailgun", "api_key")
assert msg =~ "key-"
end
end
describe "validate_email_key/3 - Brevo" do
test "accepts valid Brevo key" do
key = "xkeysib-" <> String.duplicate("ab", 32)
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "brevo", "api_key")
end
test "rejects key without xkeysib- prefix" do
assert {:error, msg} = KeyValidation.validate_email_key("not_brevo_key", "brevo", "api_key")
assert msg =~ "xkeysib-"
end
end
describe "validate_email_key/3 - unknown providers" do
test "accepts reasonable key for unknown adapter" do
assert {:ok, "some_api_key_12345"} =
KeyValidation.validate_email_key("some_api_key_12345", "mailersend", "api_key")
end
test "accepts reasonable key for non-api_key fields" do
assert {:ok, "smtp.example.com"} =
KeyValidation.validate_email_key("smtp.example.com", "smtp", "relay")
end
test "rejects very short value" do
assert {:error, msg} = KeyValidation.validate_email_key("ab", "mailersend", "api_key")
assert msg =~ "too short"
end
test "rejects empty" do
assert {:error, msg} = KeyValidation.validate_email_key("", "mailersend", "api_key")
assert msg =~ "Please enter"
end
end
end

View File

@@ -82,11 +82,11 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|> form("form[phx-change=\"change_adapter\"]", %{email: %{adapter: "postmark"}})
|> render_change()
# Submit with an API key
# Submit with an API key (Postmark uses UUID format)
html =
view
|> form("form[phx-submit=\"save\"]", %{
email: %{adapter: "postmark", api_key: "pm_test_123"}
email: %{adapter: "postmark", api_key: "abc12345-abcd-1234-abcd-123456789abc"}
})
|> render_submit()
@@ -108,7 +108,7 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|> form("form[phx-submit=\"save\"]", %{email: %{adapter: "postmark", api_key: ""}})
|> render_submit()
assert html =~ "Missing required fields"
assert html =~ "API key is required"
end
test "disconnect clears email configuration", %{conn: conn} do
@@ -167,7 +167,7 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
view
|> form("form[phx-submit=\"save\"]", %{
email: %{adapter: "postmark", api_key: "pm_new_key"}
email: %{adapter: "postmark", api_key: "def12345-abcd-1234-abcd-123456789def"}
})
|> render_submit()

View File

@@ -167,14 +167,6 @@ defmodule BerrypodWeb.Admin.ProvidersTest do
assert html =~ "Connect to Printify"
end
test "test connection shows error when no api key", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/providers/new")
html = render_click(view, "test_connection")
assert html =~ "Please enter your API key"
end
test "saves new connection", %{conn: conn} do
expect(MockProvider, :test_connection, fn _conn ->
{:ok, %{shop_name: "My Printify Shop", shop_id: 12345}}
@@ -196,55 +188,6 @@ defmodule BerrypodWeb.Admin.ProvidersTest do
end
end
describe "form - test connection" do
setup %{conn: conn, user: user} do
Application.put_env(:berrypod, :provider_modules, %{
"printify" => MockProvider
})
on_exit(fn -> Application.delete_env(:berrypod, :provider_modules) end)
%{conn: log_in_user(conn, user)}
end
test "shows success when connection is valid", %{conn: conn} do
expect(MockProvider, :test_connection, fn _conn ->
{:ok, %{shop_name: "My Printify Shop", shop_id: 12345}}
end)
{:ok, view, _html} = live(conn, ~p"/admin/providers/new")
# Validate first to set pending_api_key
view
|> form("#provider-form", %{
"provider_connection" => %{"api_key" => "valid_key_123"}
})
|> render_change()
html = render_click(view, "test_connection")
assert html =~ "Connected to My Printify Shop"
end
test "shows error when connection fails", %{conn: conn} do
expect(MockProvider, :test_connection, fn _conn ->
{:error, :unauthorized}
end)
{:ok, view, _html} = live(conn, ~p"/admin/providers/new")
view
|> form("#provider-form", %{
"provider_connection" => %{"api_key" => "bad_key"}
})
|> render_change()
html = render_click(view, "test_connection")
assert html =~ "doesn&#39;t seem to be valid"
end
end
describe "form - edit" do
setup %{conn: conn, user: user} do
connection =