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 nil value (optional field)", %{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" => nil} }) |> Ash.create() assert custom_field_value.value.value == nil end test "accepts empty string (becomes nil after trim)", %{ 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" => ""} }) |> Ash.create() # Empty string after trim should become nil assert custom_field_value.value.value == nil end 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