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 - Value type immutability (cannot be changed after creation) """ use Mv.DataCase, async: true alias Mv.Membership.CustomField setup do system_actor = Mv.Helpers.SystemActor.get_system_actor() %{actor: system_actor} end describe "name validation" do test "accepts name with exactly 100 characters", %{actor: actor} do name = String.duplicate("a", 100) assert {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: name, value_type: :string }) |> Ash.create(actor: actor) assert custom_field.name == name assert String.length(custom_field.name) == 100 end test "rejects name with 101 characters", %{actor: actor} do name = String.duplicate("a", 101) assert {:error, changeset} = CustomField |> Ash.Changeset.for_create(:create, %{ name: name, value_type: :string }) |> Ash.create(actor: actor) assert [%{field: :name, message: message}] = changeset.errors assert message =~ "max" or message =~ "length" or message =~ "100" end test "trims whitespace from name", %{actor: actor} do assert {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: " test_field ", value_type: :string }) |> Ash.create(actor: actor) assert custom_field.name == "test_field" end test "rejects empty name", %{actor: actor} do assert {:error, changeset} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "", value_type: :string }) |> Ash.create(actor: actor) assert Enum.any?(changeset.errors, fn error -> error.field == :name end) end test "rejects nil name", %{actor: actor} do assert {:error, changeset} = CustomField |> Ash.Changeset.for_create(:create, %{ value_type: :string }) |> Ash.create(actor: actor) assert Enum.any?(changeset.errors, fn error -> error.field == :name end) end end describe "description validation" do test "accepts description with exactly 500 characters", %{actor: actor} 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(actor: actor) assert custom_field.description == description assert String.length(custom_field.description) == 500 end test "rejects description with 501 characters", %{actor: actor} 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(actor: actor) assert [%{field: :description, message: message}] = changeset.errors assert message =~ "max" or message =~ "length" or message =~ "500" end test "trims whitespace from description", %{actor: actor} do assert {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "test_field", value_type: :string, description: " A nice description " }) |> Ash.create(actor: actor) assert custom_field.description == "A nice description" end test "accepts nil description (optional field)", %{actor: actor} do assert {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "test_field", value_type: :string }) |> Ash.create(actor: actor) assert custom_field.description == nil end test "accepts empty description after trimming", %{actor: actor} do assert {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "test_field", value_type: :string, description: " " }) |> Ash.create(actor: actor) # 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", %{actor: actor} do assert {:ok, _} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "unique_field", value_type: :string }) |> Ash.create(actor: actor) assert {:error, changeset} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "unique_field", value_type: :integer }) |> Ash.create(actor: actor) assert Enum.any?(changeset.errors, fn error -> error.field == :name end) end end describe "value_type validation" do test "accepts all valid value types", %{actor: actor} 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(actor: actor) assert custom_field.value_type == value_type end end test "rejects invalid value type", %{actor: actor} do assert {:error, changeset} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "invalid_field", value_type: :invalid_type }) |> Ash.create(actor: actor) assert [%{field: :value_type}] = changeset.errors end end describe "value_type immutability" do test "rejects attempt to change value_type after creation", %{actor: actor} do # Create custom field with value_type :string {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "test_field", value_type: :string }) |> Ash.create(actor: actor) original_value_type = custom_field.value_type assert original_value_type == :string # Attempt to update value_type to :integer assert {:error, %Ash.Error.Invalid{} = error} = custom_field |> Ash.Changeset.for_update(:update, %{ value_type: :integer }) |> Ash.update(actor: actor) # Verify error message contains expected text error_message = Exception.message(error) assert error_message =~ "cannot be changed" or error_message =~ "value_type" # Reload and verify value_type remained unchanged reloaded = Ash.get!(CustomField, custom_field.id, actor: actor) assert reloaded.value_type == original_value_type assert reloaded.value_type == :string end test "allows updating other fields while value_type remains unchanged", %{actor: actor} do # Create custom field with value_type :string {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "test_field", value_type: :string, description: "Original description" }) |> Ash.create(actor: actor) original_value_type = custom_field.value_type assert original_value_type == :string # Update other fields (name, description) without touching value_type {:ok, updated_custom_field} = custom_field |> Ash.Changeset.for_update(:update, %{ name: "updated_name", description: "Updated description" }) |> Ash.update(actor: actor) # Verify value_type remained unchanged assert updated_custom_field.value_type == original_value_type assert updated_custom_field.value_type == :string # Verify other fields were updated assert updated_custom_field.name == "updated_name" assert updated_custom_field.description == "Updated description" end test "rejects value_type change even when other fields are updated", %{actor: actor} do # Create custom field with value_type :boolean {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "test_field", value_type: :boolean }) |> Ash.create(actor: actor) original_value_type = custom_field.value_type assert original_value_type == :boolean # Attempt to update both name and value_type assert {:error, %Ash.Error.Invalid{} = error} = custom_field |> Ash.Changeset.for_update(:update, %{ name: "updated_name", value_type: :date }) |> Ash.update(actor: actor) # Verify error message error_message = Exception.message(error) assert error_message =~ "cannot be changed" or error_message =~ "value_type" # Reload and verify value_type remained unchanged, but name was not updated either reloaded = Ash.get!(CustomField, custom_field.id, actor: actor) assert reloaded.value_type == original_value_type assert reloaded.value_type == :boolean assert reloaded.name == "test_field" end end end