547 lines
16 KiB
Elixir
547 lines
16 KiB
Elixir
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
|