feat(portal): Support search_domain field in Account.Config (#8391)

Introduces a simple `search_domain` field embed into our existing
`Accounts.Account.Config` embedded schema. This will be sent to clients
to append to single-label DNS queries.

UI and API changes will come in subsequent PRs: this one adds field and
(lots of) validations only.

Related: #8365
This commit is contained in:
Jamil
2025-03-08 03:08:33 +00:00
committed by GitHub
parent d46ce9ab94
commit d723336c2a
3 changed files with 144 additions and 1 deletions

View File

@@ -3,6 +3,8 @@ defmodule Domain.Accounts.Config do
@primary_key false
embedded_schema do
field :search_domain, :string
embeds_many :clients_upstream_dns, ClientsUpstreamDNS,
primary_key: false,
on_replace: :delete do

View File

@@ -7,16 +7,45 @@ defmodule Domain.Accounts.Config.Changeset do
def changeset(config \\ %Config{}, attrs) do
config
|> cast(attrs, [])
|> cast(attrs, [:search_domain])
|> cast_embed(:clients_upstream_dns,
with: &client_upstream_dns_changeset/2,
sort_param: :clients_upstream_dns_sort,
drop_param: :clients_upstream_dns_drop
)
|> cast_embed(:notifications, with: &notifications_changeset/2)
|> validate_search_domain()
|> validate_unique_clients_upstream_dns()
end
defp validate_search_domain(changeset) do
changeset
|> validate_change(:search_domain, fn :search_domain, domain ->
cond do
domain == nil || domain == "" ->
[search_domain: "cannot be empty"]
String.length(domain) > 255 ->
[search_domain: "must not exceed 255 characters"]
String.starts_with?(domain, ".") || String.ends_with?(domain, ".") ->
[search_domain: "must not start or end with a dot"]
String.contains?(domain, "..") ->
[search_domain: "must not contain consecutive dots"]
!String.match?(domain, ~r/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i) ->
[search_domain: "must be a valid fully-qualified domain name"]
Enum.any?(String.split(domain, "."), &(String.length(&1) > 63)) ->
[search_domain: "each label must not exceed 63 characters"]
true ->
[]
end
end)
end
defp validate_unique_clients_upstream_dns(changeset) do
with false <- has_errors?(changeset, :clients_upstream_dns),
{_data_or_changes, client_upstream_dns} <- fetch_field(changeset, :clients_upstream_dns) do

View File

@@ -583,6 +583,118 @@ defmodule Domain.AccountsTest do
}
end
test "returns error when search_domain is invalid", %{account: account} do
attrs = %{
config: %{
search_domain: "invalid"
}
}
assert {:error, changeset} = update_account_by_id(account.id, attrs)
assert errors_on(changeset) == %{
config: %{
search_domain: ["must be a valid fully-qualified domain name"]
}
}
end
test "returns error when search_domain is too long", %{account: account} do
attrs = %{
config: %{
search_domain: String.duplicate("a", 256)
}
}
assert {:error, changeset} = update_account_by_id(account.id, attrs)
assert errors_on(changeset) == %{
config: %{
search_domain: ["must not exceed 255 characters"]
}
}
end
test "returns error when search_domain starts with a dot", %{account: account} do
attrs = %{
config: %{
search_domain: ".example.com"
}
}
assert {:error, changeset} = update_account_by_id(account.id, attrs)
assert errors_on(changeset) == %{
config: %{
search_domain: ["must not start or end with a dot"]
}
}
end
test "returns error when search_domain ends with a dot", %{account: account} do
attrs = %{
config: %{
search_domain: "example.com."
}
}
assert {:error, changeset} = update_account_by_id(account.id, attrs)
assert errors_on(changeset) == %{
config: %{
search_domain: ["must not start or end with a dot"]
}
}
end
test "returns error when search_domain contains consecutive dots", %{account: account} do
attrs = %{
config: %{
search_domain: "example..com"
}
}
assert {:error, changeset} = update_account_by_id(account.id, attrs)
assert errors_on(changeset) == %{
config: %{
search_domain: ["must not contain consecutive dots"]
}
}
end
test "returns error when search_domain labels exceed 63 characters", %{account: account} do
attrs = %{
config: %{
search_domain: "a" <> String.duplicate("a", 63) <> ".com"
}
}
assert {:error, changeset} = update_account_by_id(account.id, attrs)
assert errors_on(changeset) == %{
config: %{
search_domain: ["each label must not exceed 63 characters"]
}
}
end
test "returns error when search_domain contains invalid characters", %{account: account} do
attrs = %{
config: %{
search_domain: "example.com!"
}
}
assert {:error, changeset} = update_account_by_id(account.id, attrs)
assert errors_on(changeset) == %{
config: %{
search_domain: ["must be a valid fully-qualified domain name"]
}
}
end
test "updates account and broadcasts a message", %{account: account} do
Bypass.open()
|> Domain.Mocks.Stripe.mock_update_customer_endpoint(account)