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:
194
test/berrypod/key_validation_test.exs
Normal file
194
test/berrypod/key_validation_test.exs
Normal 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
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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't seem to be valid"
|
||||
end
|
||||
end
|
||||
|
||||
describe "form - edit" do
|
||||
setup %{conn: conn, user: user} do
|
||||
connection =
|
||||
|
||||
Reference in New Issue
Block a user