Implements validation for required custom fields closes #274 #301

Open
carla wants to merge 7 commits from bugfix/274_required_custom_fields into main
2 changed files with 278 additions and 192 deletions
Showing only changes of commit 6084827c73 - Show all commits

View file

@ -354,7 +354,15 @@ defmodule Mv.Membership.Member do
Enum.reduce(custom_field_values_arg, %{}, fn cfv, acc ->
custom_field_id = Map.get(cfv, "custom_field_id")
value_map = Map.get(cfv, "value", %{})
actual_value = Map.get(value_map, "value")
# Support both "value" and "_union_value" keys, without using || to preserve false values
actual_value =
cond do
Map.has_key?(value_map, "value") -> Map.get(value_map, "value")
Map.has_key?(value_map, "_union_value") -> Map.get(value_map, "_union_value")
true -> nil
end
Map.put(acc, custom_field_id, actual_value)
end)
end

View file

@ -69,6 +69,33 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
}
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
}) 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]}
}
]
end
describe "create_member with required custom fields" do
@valid_attrs %{
first_name: "John",
@ -84,15 +111,20 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
assert error_message(errors, :custom_field_values) =~ field.name
end
test "fails when required string custom field has nil value", %{
required_string_field: field
} do
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "string", "value" => nil}
}
]
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)
@ -101,15 +133,20 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
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
} do
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "string", "value" => ""}
}
]
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)
@ -118,15 +155,20 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
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
} do
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "string", "value" => " "}
}
]
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)
@ -135,30 +177,39 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
assert error_message(errors, :custom_field_values) =~ field.name
end
test "succeeds when required string custom field has valid value", %{
required_string_field: field
} do
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "string", "value" => "test value"}
}
]
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
} do
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "integer", "value" => nil}
}
]
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)
@ -167,45 +218,49 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
assert error_message(errors, :custom_field_values) =~ field.name
end
test "succeeds when required integer custom field has zero value", %{
required_integer_field: field
} do
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "integer", "value" => 0}
}
]
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
} do
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "integer", "value" => 42}
}
]
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
} do
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "boolean", "value" => nil}
}
]
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)
@ -214,45 +269,49 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
assert error_message(errors, :custom_field_values) =~ field.name
end
test "succeeds when required boolean custom field has false value", %{
required_boolean_field: field
} do
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "boolean", "value" => false}
}
]
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
} do
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "boolean", "value" => true}
}
]
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
} do
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "date", "value" => nil}
}
]
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)
@ -261,57 +320,60 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
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
} do
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "date", "value" => ~D[2020-01-01]}
}
]
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 "succeeds when multiple required custom fields are provided", %{
required_string_field: string_field,
required_integer_field: integer_field,
required_boolean_field: boolean_field
} do
custom_field_values = [
%{
"custom_field_id" => string_field.id,
"value" => %{"type" => "string", "value" => "test"}
},
%{
"custom_field_id" => integer_field.id,
"value" => %{"type" => "integer", "value" => 42}
},
%{
"custom_field_id" => boolean_field.id,
"value" => %{"type" => "boolean", "value" => true}
}
]
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
} do
custom_field_values = [
%{
"custom_field_id" => string_field.id,
"value" => %{"type" => "string", "value" => "test"}
}
# Missing required_integer_field
]
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)
@ -320,19 +382,26 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
assert error_message(errors, :custom_field_values) =~ integer_field.name
end
test "succeeds when optional custom field is missing", %{optional_field: field} do
attrs = Map.put(@valid_attrs, :custom_field_values, [])
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} do
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "string", "value" => nil}
}
]
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)
@ -341,16 +410,12 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
end
describe "update_member with required custom fields" do
test "fails when removing a required custom field value", %{
required_string_field: field
} do
# Create member with required custom field
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "string", "value" => "test"}
}
]
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(%{
@ -368,16 +433,12 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
assert error_message(errors, :custom_field_values) =~ field.name
end
test "fails when setting required custom field value to empty", %{
required_string_field: field
} do
# Create member with required custom field
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "string", "value" => "test"}
}
]
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(%{
@ -387,13 +448,16 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
custom_field_values: custom_field_values
})
# Try to update with empty value
updated_custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "string", "value" => ""}
}
]
# 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, %{
@ -404,16 +468,12 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
assert error_message(errors, :custom_field_values) =~ field.name
end
test "succeeds when updating required custom field to valid value", %{
required_string_field: field
} do
# Create member with required custom field
custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "string", "value" => "old value"}
}
]
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(%{
@ -423,13 +483,31 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do
custom_field_values: custom_field_values
})
# Update with new valid value
updated_custom_field_values = [
%{
"custom_field_id" => field.id,
"value" => %{"type" => "string", "value" => "new value"}
}
]
# 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, %{