mitgliederverwaltung/test/membership/member_search_with_custom_fields_test.exs

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