defmodule Mv.Membership.MemberRequiredCustomFieldsTest do @moduledoc """ Tests for required custom fields validation. Tests cover: - Member creation without required custom field → error - Member creation with empty required custom field (nil/empty string) → error - Member creation with valid required custom field → success - Member update: removing a required custom field value → error - Boolean required custom field: false is valid, nil is invalid """ use Mv.DataCase, async: true alias Mv.Membership setup do # Create required custom fields for different types {:ok, required_string_field} = Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "required_string", value_type: :string, required: true }) |> Ash.create() {:ok, required_integer_field} = Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "required_integer", value_type: :integer, required: true }) |> Ash.create() {:ok, required_boolean_field} = Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "required_boolean", value_type: :boolean, required: true }) |> Ash.create() {:ok, required_date_field} = Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "required_date", value_type: :date, required: true }) |> Ash.create() {:ok, required_email_field} = Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "required_email", value_type: :email, required: true }) |> Ash.create() {:ok, optional_field} = Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "optional_string", value_type: :string, required: false }) |> Ash.create() %{ required_string_field: required_string_field, required_integer_field: required_integer_field, required_boolean_field: required_boolean_field, required_date_field: required_date_field, required_email_field: required_email_field, optional_field: optional_field } end # Helper function to create all required custom fields with valid default values defp all_required_custom_fields_with_defaults(%{ required_string_field: string_field, required_integer_field: integer_field, required_boolean_field: boolean_field, required_date_field: date_field, required_email_field: email_field }) do [ %{ "custom_field_id" => string_field.id, "value" => %{"_union_type" => "string", "_union_value" => "default"} }, %{ "custom_field_id" => integer_field.id, "value" => %{"_union_type" => "integer", "_union_value" => 0} }, %{ "custom_field_id" => boolean_field.id, "value" => %{"_union_type" => "boolean", "_union_value" => false} }, %{ "custom_field_id" => date_field.id, "value" => %{"_union_type" => "date", "_union_value" => ~D[2020-01-01]} }, %{ "custom_field_id" => email_field.id, "value" => %{"_union_type" => "email", "_union_value" => "test@example.com"} } ] end describe "create_member with required custom fields" do @valid_attrs %{ first_name: "John", last_name: "Doe", email: "john@example.com" } test "fails when required custom field is missing", %{required_string_field: field} do attrs = Map.put(@valid_attrs, :custom_field_values, []) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "fails when required string custom field has nil value", %{ required_string_field: field } = context do # Start with all required fields having valid values custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "string", "_union_value" => nil}} else cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "fails when required string custom field has empty string value", %{ required_string_field: field } = context do # Start with all required fields having valid values custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "string", "_union_value" => ""}} else cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "fails when required string custom field has whitespace-only value", %{ required_string_field: field } = context do # Start with all required fields having valid values custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "string", "_union_value" => " "}} else cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "succeeds when required string custom field has valid value", %{ required_string_field: field } = context do # Start with all required fields having valid values, then update the string field custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "string", "_union_value" => "test value"}} else cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:ok, _member} = Membership.create_member(attrs) end test "fails when required integer custom field has nil value", %{ required_integer_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "integer", "_union_value" => nil}} else cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "fails when required integer custom field has empty string value", %{ required_integer_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "integer", "_union_value" => ""}} else cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "succeeds when required integer custom field has zero value", %{ required_integer_field: _field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:ok, _member} = Membership.create_member(attrs) end test "succeeds when required integer custom field has positive value", %{ required_integer_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "integer", "_union_value" => 42}} else cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:ok, _member} = Membership.create_member(attrs) end test "fails when required boolean custom field has nil value", %{ required_boolean_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "boolean", "_union_value" => nil}} else cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "succeeds when required boolean custom field has false value", %{ required_boolean_field: _field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:ok, _member} = Membership.create_member(attrs) end test "succeeds when required boolean custom field has true value", %{ required_boolean_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "boolean", "_union_value" => true}} else cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:ok, _member} = Membership.create_member(attrs) end test "fails when required date custom field has nil value", %{ required_date_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "date", "_union_value" => nil}} else cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "fails when required date custom field has empty string value", %{ required_date_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "date", "_union_value" => ""}} else cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "succeeds when required date custom field has valid date value", %{ required_date_field: _field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:ok, _member} = Membership.create_member(attrs) end test "fails when required email custom field has nil value", %{ required_email_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "email", "_union_value" => nil}} else cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "fails when required email custom field has empty string value", %{ required_email_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "email", "_union_value" => ""}} else cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "succeeds when required email custom field has valid email value", %{ required_email_field: _field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:ok, _member} = Membership.create_member(attrs) end test "succeeds when multiple required custom fields are provided", %{ required_string_field: string_field, required_integer_field: integer_field, required_boolean_field: boolean_field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> cond do cfv["custom_field_id"] == string_field.id -> %{cfv | "value" => %{"_union_type" => "string", "_union_value" => "test"}} cfv["custom_field_id"] == integer_field.id -> %{cfv | "value" => %{"_union_type" => "integer", "_union_value" => 42}} cfv["custom_field_id"] == boolean_field.id -> %{cfv | "value" => %{"_union_type" => "boolean", "_union_value" => true}} true -> cfv end end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:ok, _member} = Membership.create_member(attrs) end test "fails when one of multiple required custom fields is missing", %{ required_string_field: string_field, required_integer_field: integer_field } = context do # Provide only string field, missing integer, boolean, and date custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.filter(fn cfv -> cfv["custom_field_id"] == string_field.id end) |> Enum.map(fn cfv -> %{cfv | "value" => %{"_union_type" => "string", "_union_value" => "test"}} end) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ integer_field.name end test "succeeds when optional custom field is missing", %{} = context do # Provide all required fields, but no optional field custom_field_values = all_required_custom_fields_with_defaults(context) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:ok, _member} = Membership.create_member(attrs) end test "succeeds when optional custom field has nil value", %{optional_field: field} = context do # Provide all required fields plus optional field with nil custom_field_values = all_required_custom_fields_with_defaults(context) ++ [ %{ "custom_field_id" => field.id, "value" => %{"_union_type" => "string", "_union_value" => nil} } ] attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) assert {:ok, _member} = Membership.create_member(attrs) end end describe "update_member with required custom fields" do test "fails when removing a required custom field value", %{ required_string_field: field } = context do # Create member with all required custom fields custom_field_values = all_required_custom_fields_with_defaults(context) {:ok, member} = Membership.create_member(%{ first_name: "John", last_name: "Doe", email: "john@example.com", custom_field_values: custom_field_values }) # Try to update without the required custom field assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.update_member(member, %{custom_field_values: []}) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "fails when setting required custom field value to empty", %{ required_string_field: field } = context do # Create member with all required custom fields custom_field_values = all_required_custom_fields_with_defaults(context) {:ok, member} = Membership.create_member(%{ first_name: "John", last_name: "Doe", email: "john@example.com", custom_field_values: custom_field_values }) # Try to update with empty value for the string field updated_custom_field_values = all_required_custom_fields_with_defaults(context) |> Enum.map(fn cfv -> if cfv["custom_field_id"] == field.id do %{cfv | "value" => %{"_union_type" => "string", "_union_value" => ""}} else cfv end end) assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.update_member(member, %{ custom_field_values: updated_custom_field_values }) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "succeeds when updating required custom field to valid value", %{ required_string_field: field } = context do # Create member with all required custom fields custom_field_values = all_required_custom_fields_with_defaults(context) {:ok, member} = Membership.create_member(%{ first_name: "John", last_name: "Doe", email: "john@example.com", custom_field_values: custom_field_values }) # Load existing custom field values to get their IDs {:ok, member_with_cfvs} = Ash.load(member, :custom_field_values) # Update with new valid value for the string field, using existing IDs updated_custom_field_values = member_with_cfvs.custom_field_values |> Enum.map(fn cfv -> if cfv.custom_field_id == field.id do %{ "id" => cfv.id, "custom_field_id" => cfv.custom_field_id, "value" => %{"_union_type" => "string", "_union_value" => "new value"} } else # Keep other fields as they are value_type = Atom.to_string(cfv.value.type) actual_value = cfv.value.value %{ "id" => cfv.id, "custom_field_id" => cfv.custom_field_id, "value" => %{"_union_type" => value_type, "_union_value" => actual_value} } end end) assert {:ok, _updated_member} = Membership.update_member(member, %{ custom_field_values: updated_custom_field_values }) end end # Helper function for error evaluation defp error_message(errors, field) do errors |> Enum.filter(fn err -> Map.get(err, :field) == field end) |> Enum.map_join(" ", &Map.get(&1, :message, "")) end end