feat: Add validation constraints and tests for CustomField and CustomFieldValue

This commit is contained in:
Moritz 2025-11-13 18:37:58 +01:00
parent 8400e727a7
commit e9290b7156
4 changed files with 523 additions and 9 deletions

View file

@ -0,0 +1,206 @@
defmodule Mv.Membership.CustomFieldValidationTest do
@moduledoc """
Tests for CustomField validation constraints.
Tests cover:
- Name length validation (max 100 characters)
- Name trimming
- Description length validation (max 500 characters)
- Description trimming
- Required vs optional fields
"""
use Mv.DataCase, async: true
alias Mv.Membership.CustomField
describe "name validation" do
test "accepts name with exactly 100 characters" do
name = String.duplicate("a", 100)
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: name,
value_type: :string
})
|> Ash.create()
assert custom_field.name == name
assert String.length(custom_field.name) == 100
end
test "rejects name with 101 characters" do
name = String.duplicate("a", 101)
assert {:error, changeset} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: name,
value_type: :string
})
|> Ash.create()
assert [%{field: :name, message: message}] = changeset.errors
assert message =~ "max" or message =~ "length" or message =~ "100"
end
test "trims whitespace from name" do
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: " test_field ",
value_type: :string
})
|> Ash.create()
assert custom_field.name == "test_field"
end
test "rejects empty name" do
assert {:error, changeset} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "",
value_type: :string
})
|> Ash.create()
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
end
test "rejects nil name" do
assert {:error, changeset} =
CustomField
|> Ash.Changeset.for_create(:create, %{
value_type: :string
})
|> Ash.create()
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
end
end
describe "description validation" do
test "accepts description with exactly 500 characters" do
description = String.duplicate("a", 500)
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field",
value_type: :string,
description: description
})
|> Ash.create()
assert custom_field.description == description
assert String.length(custom_field.description) == 500
end
test "rejects description with 501 characters" do
description = String.duplicate("a", 501)
assert {:error, changeset} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field",
value_type: :string,
description: description
})
|> Ash.create()
assert [%{field: :description, message: message}] = changeset.errors
assert message =~ "max" or message =~ "length" or message =~ "500"
end
test "trims whitespace from description" do
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field",
value_type: :string,
description: " A nice description "
})
|> Ash.create()
assert custom_field.description == "A nice description"
end
test "accepts nil description (optional field)" do
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field",
value_type: :string
})
|> Ash.create()
assert custom_field.description == nil
end
test "accepts empty description after trimming" do
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field",
value_type: :string,
description: " "
})
|> Ash.create()
# After trimming whitespace, becomes nil (empty strings are converted to nil)
assert custom_field.description == nil
end
end
describe "name uniqueness" do
test "rejects duplicate names" do
assert {:ok, _} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "unique_field",
value_type: :string
})
|> Ash.create()
assert {:error, changeset} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "unique_field",
value_type: :integer
})
|> Ash.create()
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
end
end
describe "value_type validation" do
test "accepts all valid value types" do
for value_type <- [:string, :integer, :boolean, :date, :email] do
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "field_#{value_type}",
value_type: value_type
})
|> Ash.create()
assert custom_field.value_type == value_type
end
end
test "rejects invalid value type" do
assert {:error, changeset} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "invalid_field",
value_type: :invalid_type
})
|> Ash.create()
assert [%{field: :value_type}] = changeset.errors
end
end
end

View file

@ -0,0 +1,276 @@
defmodule Mv.Membership.CustomFieldValueValidationTest do
@moduledoc """
Tests for CustomFieldValue validation constraints.
Tests cover:
- String value length validation (max 10,000 characters)
- String value trimming
- Email value validation (via Email type)
- Optional values (nil allowed)
"""
use Mv.DataCase, async: true
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
setup do
# Create a test member
{:ok, member} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Test",
last_name: "User",
email: "test.validation@example.com"
})
|> Ash.create()
# Create custom fields for different types
{:ok, string_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "string_field",
value_type: :string
})
|> Ash.create()
{:ok, integer_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "integer_field",
value_type: :integer
})
|> Ash.create()
{:ok, email_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "email_field",
value_type: :email
})
|> Ash.create()
%{
member: member,
string_field: string_field,
integer_field: integer_field,
email_field: email_field
}
end
describe "string value length validation" do
test "accepts string value with exactly 10,000 characters", %{
member: member,
string_field: string_field
} do
value_string = String.duplicate("a", 10_000)
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{
"_union_type" => "string",
"_union_value" => value_string
}
})
|> Ash.create()
assert custom_field_value.value.value == value_string
assert String.length(custom_field_value.value.value) == 10_000
end
test "rejects string value with 10,001 characters", %{
member: member,
string_field: string_field
} do
value_string = String.duplicate("a", 10_001)
assert {:error, changeset} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{"_union_type" => "string", "_union_value" => value_string}
})
|> Ash.create()
assert Enum.any?(changeset.errors, fn error ->
error.field == :value and (error.message =~ "max" or error.message =~ "length")
end)
end
test "trims whitespace from string value", %{member: member, string_field: string_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{"_union_type" => "string", "_union_value" => " test value "}
})
|> Ash.create()
assert custom_field_value.value.value == "test value"
end
test "accepts empty string value", %{member: member, string_field: string_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{"_union_type" => "string", "_union_value" => ""}
})
|> Ash.create()
# Empty strings after trimming become nil
assert custom_field_value.value.value == nil
end
test "accepts string with special characters", %{member: member, string_field: string_field} do
special_string = "Hello 世界! 🎉 @#$%^&*()"
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{"_union_type" => "string", "_union_value" => special_string}
})
|> Ash.create()
assert custom_field_value.value.value == special_string
end
end
describe "integer value validation" do
test "accepts valid integer value", %{member: member, integer_field: integer_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: integer_field.id,
value: %{"_union_type" => "integer", "_union_value" => 42}
})
|> Ash.create()
assert custom_field_value.value.value == 42
end
test "accepts negative integer", %{member: member, integer_field: integer_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: integer_field.id,
value: %{"_union_type" => "integer", "_union_value" => -100}
})
|> Ash.create()
assert custom_field_value.value.value == -100
end
test "accepts zero", %{member: member, integer_field: integer_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: integer_field.id,
value: %{"_union_type" => "integer", "_union_value" => 0}
})
|> Ash.create()
assert custom_field_value.value.value == 0
end
end
describe "email value validation" do
test "accepts valid email", %{member: member, email_field: email_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: email_field.id,
value: %{"_union_type" => "email", "_union_value" => "test@example.com"}
})
|> Ash.create()
assert custom_field_value.value.value == "test@example.com"
end
test "rejects invalid email format", %{member: member, email_field: email_field} do
assert {:error, changeset} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: email_field.id,
value: %{"_union_type" => "email", "_union_value" => "not-an-email"}
})
|> Ash.create()
assert Enum.any?(changeset.errors, fn error -> error.field == :value end)
end
test "rejects email longer than 254 characters", %{member: member, email_field: email_field} do
# Create an email with >254 chars (243 + 12 = 255)
long_email = String.duplicate("a", 243) <> "@example.com"
assert {:error, changeset} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: email_field.id,
value: %{"_union_type" => "email", "_union_value" => long_email}
})
|> Ash.create()
assert Enum.any?(changeset.errors, fn error -> error.field == :value end)
end
test "trims whitespace from email", %{member: member, email_field: email_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: email_field.id,
value: %{"_union_type" => "email", "_union_value" => " test@example.com "}
})
|> Ash.create()
assert custom_field_value.value.value == "test@example.com"
end
end
describe "uniqueness constraint" do
test "rejects duplicate custom_field_id per member", %{
member: member,
string_field: string_field
} do
# Create first custom field value
assert {:ok, _} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{"_union_type" => "string", "_union_value" => "first value"}
})
|> Ash.create()
# Try to create second custom field value with same custom_field_id for same member
assert {:error, changeset} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{"_union_type" => "string", "_union_value" => "second value"}
})
|> Ash.create()
# Should have uniqueness error
assert Enum.any?(changeset.errors, fn error ->
error.message =~ "unique" or error.message =~ "already exists" or
error.message =~ "has already been taken"
end)
end
end
end