feat: Add validation constraints and tests for CustomField and CustomFieldValue
This commit is contained in:
parent
8400e727a7
commit
e9290b7156
4 changed files with 523 additions and 9 deletions
|
|
@ -15,17 +15,18 @@ defmodule Mv.Membership.CustomField do
|
||||||
- `required` - If true, all members must have this custom field (future feature)
|
- `required` - If true, all members must have this custom field (future feature)
|
||||||
|
|
||||||
## Supported Value Types
|
## Supported Value Types
|
||||||
- `:string` - Text data (unlimited length)
|
- `:string` - Text data (max 10,000 characters)
|
||||||
- `:integer` - Numeric data (64-bit integers)
|
- `:integer` - Numeric data (64-bit integers)
|
||||||
- `:boolean` - True/false flags
|
- `:boolean` - True/false flags
|
||||||
- `:date` - Date values (no time component)
|
- `:date` - Date values (no time component)
|
||||||
- `:email` - Validated email addresses
|
- `:email` - Validated email addresses (max 254 characters)
|
||||||
|
|
||||||
## Relationships
|
## Relationships
|
||||||
- `has_many :custom_field_values` - All custom field values of this type
|
- `has_many :custom_field_values` - All custom field values of this type
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
- Name must be unique across all custom fields
|
- Name must be unique across all custom fields
|
||||||
|
- Name maximum length: 100 characters
|
||||||
- Cannot delete a custom field that has existing custom field values (RESTRICT)
|
- Cannot delete a custom field that has existing custom field values (RESTRICT)
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
@ -60,14 +61,26 @@ defmodule Mv.Membership.CustomField do
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
|
||||||
attribute :name, :string, allow_nil?: false, public?: true
|
attribute :name, :string,
|
||||||
|
allow_nil?: false,
|
||||||
|
public?: true,
|
||||||
|
constraints: [
|
||||||
|
max_length: 100,
|
||||||
|
trim?: true
|
||||||
|
]
|
||||||
|
|
||||||
attribute :value_type, :atom,
|
attribute :value_type, :atom,
|
||||||
constraints: [one_of: [:string, :integer, :boolean, :date, :email]],
|
constraints: [one_of: [:string, :integer, :boolean, :date, :email]],
|
||||||
allow_nil?: false,
|
allow_nil?: false,
|
||||||
description: "Defines the datatype `CustomFieldValue.value` is interpreted as"
|
description: "Defines the datatype `CustomFieldValue.value` is interpreted as"
|
||||||
|
|
||||||
attribute :description, :string, allow_nil?: true, public?: true
|
attribute :description, :string,
|
||||||
|
allow_nil?: true,
|
||||||
|
public?: true,
|
||||||
|
constraints: [
|
||||||
|
max_length: 500,
|
||||||
|
trim?: true
|
||||||
|
]
|
||||||
|
|
||||||
attribute :immutable, :boolean,
|
attribute :immutable, :boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ defmodule Mv.Membership.CustomFieldValue do
|
||||||
## Constraints
|
## Constraints
|
||||||
- Each member can have only one custom field value per custom field (unique composite index)
|
- Each member can have only one custom field value per custom field (unique composite index)
|
||||||
- Custom field values are deleted when the associated member is deleted (CASCADE)
|
- Custom field values are deleted when the associated member is deleted (CASCADE)
|
||||||
|
- String values maximum length: 10,000 characters
|
||||||
|
- Email values maximum length: 254 characters (RFC 5321)
|
||||||
|
|
||||||
|
## Future Features
|
||||||
|
- Type-matching validation (value type must match custom field's value_type) - to be implemented
|
||||||
"""
|
"""
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
domain: Mv.Membership,
|
domain: Mv.Membership,
|
||||||
|
|
@ -56,11 +61,25 @@ defmodule Mv.Membership.CustomFieldValue do
|
||||||
constraints: [
|
constraints: [
|
||||||
storage: :type_and_value,
|
storage: :type_and_value,
|
||||||
types: [
|
types: [
|
||||||
boolean: [type: :boolean],
|
boolean: [
|
||||||
date: [type: :date],
|
type: :boolean
|
||||||
integer: [type: :integer],
|
],
|
||||||
string: [type: :string],
|
date: [
|
||||||
email: [type: Mv.Membership.Email]
|
type: :date
|
||||||
|
],
|
||||||
|
integer: [
|
||||||
|
type: :integer
|
||||||
|
],
|
||||||
|
string: [
|
||||||
|
type: :string,
|
||||||
|
constraints: [
|
||||||
|
max_length: 10_000,
|
||||||
|
trim?: true
|
||||||
|
]
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
type: Mv.Membership.Email
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
||||||
206
test/membership/custom_field_validation_test.exs
Normal file
206
test/membership/custom_field_validation_test.exs
Normal 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
|
||||||
|
|
||||||
276
test/membership/custom_field_value_validation_test.exs
Normal file
276
test/membership/custom_field_value_validation_test.exs
Normal 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
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue