test: adds tests for custom field search
This commit is contained in:
parent
9cda832b82
commit
276b4a2120
1 changed files with 547 additions and 0 deletions
547
test/membership/member_search_with_custom_fields_test.exs
Normal file
547
test/membership/member_search_with_custom_fields_test.exs
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
|
||||
@moduledoc """
|
||||
Tests for full-text search including custom_field_values.
|
||||
|
||||
Tests verify that custom field values are included in the search_vector
|
||||
and can be found through the fuzzy_search functionality.
|
||||
"""
|
||||
use Mv.DataCase, async: false
|
||||
|
||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
# Create test members
|
||||
{:ok, member1} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Alice",
|
||||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, member2} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Bob",
|
||||
last_name: "Brown",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, member3} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Charlie",
|
||||
last_name: "Clark",
|
||||
email: "charlie@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom fields for different types
|
||||
{:ok, string_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "membership_number",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, integer_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "member_id_number",
|
||||
value_type: :integer
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, email_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "secondary_email",
|
||||
value_type: :email
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, date_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "birthday",
|
||||
value_type: :date
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, boolean_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "newsletter",
|
||||
value_type: :boolean
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
%{
|
||||
member1: member1,
|
||||
member2: member2,
|
||||
member3: member3,
|
||||
string_field: string_field,
|
||||
integer_field: integer_field,
|
||||
email_field: email_field,
|
||||
date_field: date_field,
|
||||
boolean_field: boolean_field
|
||||
}
|
||||
end
|
||||
|
||||
describe "search with custom field values" do
|
||||
test "finds member by string custom field value", %{
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
# Create custom field value
|
||||
{:ok, _cfv} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "MEMBER12345"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Force search_vector update by reloading member
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|
||||
# Search for the custom field value
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "MEMBER12345"})
|
||||
|> Ash.read!()
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "finds member by integer custom field value", %{
|
||||
member1: member1,
|
||||
integer_field: integer_field
|
||||
} do
|
||||
# Create custom field value
|
||||
{:ok, _cfv} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 42_424}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|
||||
# Search for the custom field value
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "42424"})
|
||||
|> Ash.read!()
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "finds member by email custom field value", %{
|
||||
member1: member1,
|
||||
email_field: email_field
|
||||
} do
|
||||
# Create custom field value
|
||||
{:ok, _cfv} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "alice.secondary@example.com"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|
||||
# Search for partial custom field value (should work via FTS or custom field filter)
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "alice.secondary"})
|
||||
|> Ash.read!()
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
|
||||
# Search for full email address (should work via custom field filter LIKE)
|
||||
results_full =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "alice.secondary@example.com"})
|
||||
|> Ash.read!()
|
||||
|
||||
assert length(results_full) == 1
|
||||
assert List.first(results_full).id == member1.id
|
||||
|
||||
# Search for domain part (should work via FTS or custom field filter)
|
||||
# Note: May return multiple results if other members have same domain
|
||||
results_domain =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "example.com"})
|
||||
|> Ash.read!()
|
||||
|
||||
# Verify that member1 is in the results (may have other members too)
|
||||
ids = Enum.map(results_domain, & &1.id)
|
||||
assert member1.id in ids
|
||||
end
|
||||
|
||||
test "finds member by date custom field value", %{
|
||||
member1: member1,
|
||||
date_field: date_field
|
||||
} do
|
||||
# Create custom field value
|
||||
{:ok, _cfv} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: date_field.id,
|
||||
value: %{"_union_type" => "date", "_union_value" => ~D[1990-05-15]}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|
||||
# Search for the custom field value (date is stored as text in search_vector)
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "1990-05-15"})
|
||||
|> Ash.read!()
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "finds member by boolean custom field value", %{
|
||||
member1: member1,
|
||||
boolean_field: boolean_field
|
||||
} do
|
||||
# Create custom field value
|
||||
{:ok, _cfv} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: boolean_field.id,
|
||||
value: %{"_union_type" => "boolean", "_union_value" => true}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|
||||
# Search for the custom field value (boolean is stored as "true" or "false" text)
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "true"})
|
||||
|> Ash.read!()
|
||||
|
||||
# Note: "true" might match other things, so we check that member1 is in results
|
||||
assert Enum.any?(results, fn m -> m.id == member1.id end)
|
||||
end
|
||||
|
||||
test "custom field value update triggers search_vector update", %{
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
# Create initial custom field value
|
||||
{:ok, cfv} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "OLDVALUE"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|
||||
# Update custom field value
|
||||
{:ok, _updated_cfv} =
|
||||
cfv
|
||||
|> Ash.Changeset.for_update(:update, %{
|
||||
value: %{"_union_type" => "string", "_union_value" => "NEWVALUE123"}
|
||||
})
|
||||
|> Ash.update()
|
||||
|
||||
# Search for the new value
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "NEWVALUE123"})
|
||||
|> Ash.read!()
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
|
||||
# Old value should not be found
|
||||
old_results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "OLDVALUE"})
|
||||
|> Ash.read!()
|
||||
|
||||
refute Enum.any?(old_results, fn m -> m.id == member1.id end)
|
||||
end
|
||||
|
||||
test "custom field value delete triggers search_vector update", %{
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
# Create custom field value
|
||||
{:ok, cfv} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "TOBEDELETED"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|
||||
# Verify it's searchable
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "TOBEDELETED"})
|
||||
|> Ash.read!()
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
|
||||
# Delete custom field value
|
||||
assert :ok = Ash.destroy(cfv)
|
||||
|
||||
# Value should no longer be found
|
||||
deleted_results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "TOBEDELETED"})
|
||||
|> Ash.read!()
|
||||
|
||||
refute Enum.any?(deleted_results, fn m -> m.id == member1.id end)
|
||||
end
|
||||
|
||||
test "custom field value create triggers search_vector update", %{
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
# Create custom field value (trigger should update search_vector automatically)
|
||||
{:ok, _cfv} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "AUTOUPDATE"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Search should find it immediately (trigger should have updated search_vector)
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "AUTOUPDATE"})
|
||||
|> Ash.read!()
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "member update includes custom field values in search_vector", %{
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
# Create custom field value
|
||||
{:ok, _cfv} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "MEMBERUPDATE"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Update member (should trigger search_vector update including custom fields)
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{notes: "Updated notes"})
|
||||
|> Ash.update()
|
||||
|
||||
# Search should find the custom field value
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "MEMBERUPDATE"})
|
||||
|> Ash.read!()
|
||||
|
||||
assert length(results) == 1
|
||||
assert List.first(results).id == member1.id
|
||||
end
|
||||
|
||||
test "multiple custom field values are all searchable", %{
|
||||
member1: member1,
|
||||
string_field: string_field,
|
||||
integer_field: integer_field,
|
||||
email_field: email_field
|
||||
} do
|
||||
# Create multiple custom field values
|
||||
{:ok, _cfv1} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "MULTI1"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 99_999}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv3} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "multi@test.com"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|
||||
# All values should be searchable
|
||||
results1 =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "MULTI1"})
|
||||
|> Ash.read!()
|
||||
|
||||
assert Enum.any?(results1, fn m -> m.id == member1.id end)
|
||||
|
||||
results2 =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "99999"})
|
||||
|> Ash.read!()
|
||||
|
||||
assert Enum.any?(results2, fn m -> m.id == member1.id end)
|
||||
|
||||
results3 =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "multi@test.com"})
|
||||
|> Ash.read!()
|
||||
|
||||
assert Enum.any?(results3, fn m -> m.id == member1.id end)
|
||||
end
|
||||
|
||||
test "finds member by custom field value with numbers in text field (e.g. phone number)", %{
|
||||
member1: member1,
|
||||
string_field: string_field
|
||||
} do
|
||||
# Create custom field value with numbers and text (like phone number or ID)
|
||||
{:ok, _cfv} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "M-123-456"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|
||||
# Search for full value (should work via search_vector)
|
||||
results_full =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "M-123-456"})
|
||||
|> Ash.read!()
|
||||
|
||||
assert Enum.any?(results_full, fn m -> m.id == member1.id end),
|
||||
"Full value search should find member via search_vector"
|
||||
|
||||
# Note: Partial substring search may require additional implementation
|
||||
# For now, we test that the full value is searchable, which is the primary use case
|
||||
# Substring matching for custom fields may need to be implemented separately
|
||||
end
|
||||
|
||||
test "finds member by phone number in Emergency Contact custom field", %{
|
||||
member1: member1
|
||||
} do
|
||||
# Create Emergency Contact custom field
|
||||
{:ok, emergency_contact_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Emergency Contact",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom field value with phone number
|
||||
phone_number = "+49 123 456789"
|
||||
|
||||
{:ok, _cfv} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: emergency_contact_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => phone_number}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Force search_vector update
|
||||
{:ok, _updated_member} =
|
||||
member1
|
||||
|> Ash.Changeset.for_update(:update_member, %{})
|
||||
|> Ash.update()
|
||||
|
||||
# Search for full phone number (should work via search_vector)
|
||||
results_full =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: phone_number})
|
||||
|> Ash.read!()
|
||||
|
||||
assert Enum.any?(results_full, fn m -> m.id == member1.id end),
|
||||
"Full phone number search should find member via search_vector"
|
||||
|
||||
# Note: Partial substring search may require additional implementation
|
||||
# For now, we test that the full phone number is searchable, which is the primary use case
|
||||
# Substring matching for custom fields may need to be implemented separately
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue