mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-05-01 15:00:31 +00:00
279 lines
8.4 KiB
Elixir
279 lines
8.4 KiB
Elixir
defmodule WandererApp.TestHelpers do
|
|
@moduledoc """
|
|
Common test utilities and helpers for the test suite.
|
|
"""
|
|
|
|
import ExUnit.Assertions
|
|
|
|
@doc """
|
|
Converts string keys to atom keys in a map, recursively.
|
|
Useful for comparing API responses with expected data.
|
|
"""
|
|
def atomize_keys(map) when is_map(map) do
|
|
Map.new(map, fn {k, v} -> {atomize_key(k), atomize_keys(v)} end)
|
|
end
|
|
|
|
def atomize_keys(list) when is_list(list) do
|
|
Enum.map(list, &atomize_keys/1)
|
|
end
|
|
|
|
def atomize_keys(value), do: value
|
|
|
|
defp atomize_key(key) when is_binary(key), do: String.to_atom(key)
|
|
defp atomize_key(key) when is_atom(key), do: key
|
|
|
|
@doc """
|
|
Asserts that a map contains all expected key-value pairs.
|
|
Useful for partial matching of API responses.
|
|
"""
|
|
def assert_maps_equal(actual, expected, message \\ nil) do
|
|
missing_keys = Map.keys(expected) -- Map.keys(actual)
|
|
|
|
if missing_keys != [] do
|
|
flunk(
|
|
message ||
|
|
"Expected map to contain keys #{inspect(missing_keys)}, but they were missing. Actual: #{inspect(actual)}"
|
|
)
|
|
end
|
|
|
|
Enum.each(expected, fn {key, expected_value} ->
|
|
actual_value = Map.get(actual, key)
|
|
|
|
assert actual_value == expected_value,
|
|
message ||
|
|
"Expected #{inspect(key)} to be #{inspect(expected_value)}, got #{inspect(actual_value)}"
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Asserts that a list contains items that match the given criteria.
|
|
"""
|
|
def assert_list_contains(list, matcher) when is_function(matcher) do
|
|
found = Enum.any?(list, matcher)
|
|
|
|
assert found,
|
|
"Expected list to contain an item matching the criteria, but none found. List: #{inspect(list)}"
|
|
end
|
|
|
|
def assert_list_contains(list, expected_item) do
|
|
assert expected_item in list,
|
|
"Expected list to contain #{inspect(expected_item)}, but it was not found. List: #{inspect(list)}"
|
|
end
|
|
|
|
@doc """
|
|
Asserts that a value is within a tolerance of an expected value.
|
|
Useful for testing timestamps or floating point values.
|
|
"""
|
|
def assert_within_tolerance(actual, expected, tolerance)
|
|
when is_number(actual) and is_number(expected) do
|
|
diff = abs(actual - expected)
|
|
|
|
assert diff <= tolerance,
|
|
"Expected #{actual} to be within #{tolerance} of #{expected}, but difference was #{diff}"
|
|
end
|
|
|
|
@doc """
|
|
Asserts that a DateTime is recent (within the last few seconds).
|
|
"""
|
|
def assert_recent_datetime(datetime, seconds_ago \\ 10)
|
|
|
|
def assert_recent_datetime(%DateTime{} = datetime, seconds_ago) do
|
|
now = DateTime.utc_now()
|
|
min_time = DateTime.add(now, -seconds_ago, :second)
|
|
|
|
assert DateTime.compare(datetime, min_time) != :lt,
|
|
"Expected #{datetime} to be within the last #{seconds_ago} seconds, but it was too old"
|
|
end
|
|
|
|
def assert_recent_datetime(%NaiveDateTime{} = naive_datetime, seconds_ago) do
|
|
datetime = DateTime.from_naive!(naive_datetime, "Etc/UTC")
|
|
assert_recent_datetime(datetime, seconds_ago)
|
|
end
|
|
|
|
@doc """
|
|
Retries a function until it succeeds or times out.
|
|
Useful for testing eventual consistency or async operations.
|
|
"""
|
|
def eventually(fun, opts \\ []) do
|
|
timeout = Keyword.get(opts, :timeout, 5000)
|
|
interval = Keyword.get(opts, :interval, 100)
|
|
end_time = System.monotonic_time(:millisecond) + timeout
|
|
|
|
do_eventually(fun, end_time, interval)
|
|
end
|
|
|
|
defp do_eventually(fun, end_time, interval) do
|
|
try do
|
|
fun.()
|
|
rescue
|
|
_ ->
|
|
if System.monotonic_time(:millisecond) < end_time do
|
|
:timer.sleep(interval)
|
|
do_eventually(fun, end_time, interval)
|
|
else
|
|
# Let it fail with the actual error
|
|
fun.()
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Creates a unique test identifier using the current test name and a counter.
|
|
"""
|
|
def unique_test_id do
|
|
counter = System.unique_integer([:positive])
|
|
"test_#{counter}"
|
|
end
|
|
|
|
@doc """
|
|
Generates a random string of the specified length.
|
|
"""
|
|
def random_string(length \\ 10) do
|
|
length
|
|
|> :crypto.strong_rand_bytes()
|
|
|> Base.url_encode64()
|
|
|> binary_part(0, length)
|
|
end
|
|
|
|
@doc """
|
|
Waits for a GenServer to be available and ready.
|
|
"""
|
|
def wait_for_genserver(name, timeout \\ 5000) do
|
|
end_time = System.monotonic_time(:millisecond) + timeout
|
|
|
|
do_wait_for_genserver(name, end_time)
|
|
end
|
|
|
|
defp do_wait_for_genserver(name, end_time) do
|
|
case GenServer.whereis(name) do
|
|
nil ->
|
|
if System.monotonic_time(:millisecond) < end_time do
|
|
:timer.sleep(100)
|
|
do_wait_for_genserver(name, end_time)
|
|
else
|
|
flunk("GenServer #{name} did not start within timeout")
|
|
end
|
|
|
|
pid ->
|
|
pid
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Captures and formats Phoenix logs for test assertions.
|
|
"""
|
|
def capture_log(fun) do
|
|
ExUnit.CaptureLog.capture_log(fun)
|
|
end
|
|
|
|
@doc """
|
|
Asserts that a log message was captured.
|
|
"""
|
|
def assert_logged(log_output, expected_message) do
|
|
assert log_output =~ expected_message,
|
|
"Expected log to contain '#{expected_message}', but got: #{log_output}"
|
|
end
|
|
|
|
@doc """
|
|
Ensures a map server is started for testing.
|
|
"""
|
|
def ensure_map_server_started(map_id) do
|
|
case WandererApp.Map.Server.map_pid(map_id) do
|
|
pid when is_pid(pid) ->
|
|
# Make sure existing server has database access
|
|
WandererApp.DataCase.allow_database_access(pid)
|
|
# Also allow database access for any spawned processes
|
|
allow_map_server_children_database_access(pid)
|
|
# Ensure global Mox mode is maintained
|
|
if Code.ensure_loaded?(Mox), do: Mox.set_mox_global()
|
|
:ok
|
|
|
|
nil ->
|
|
# Ensure global Mox mode before starting map server
|
|
if Code.ensure_loaded?(Mox), do: Mox.set_mox_global()
|
|
# Start the map server directly for tests
|
|
{:ok, pid} = start_map_server_directly(map_id)
|
|
# Grant database access to the new map server process
|
|
WandererApp.DataCase.allow_database_access(pid)
|
|
# Allow database access for any spawned processes
|
|
allow_map_server_children_database_access(pid)
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp start_map_server_directly(map_id) do
|
|
# Use the same approach as MapManager.start_map_server/1
|
|
case DynamicSupervisor.start_child(
|
|
{:via, PartitionSupervisor, {WandererApp.Map.DynamicSupervisors, self()}},
|
|
{WandererApp.Map.ServerSupervisor, map_id: map_id}
|
|
) do
|
|
{:ok, pid} ->
|
|
# Allow database access for the supervisor and its children
|
|
WandererApp.DataCase.allow_genserver_database_access(pid)
|
|
|
|
# Allow Mox access for the supervisor process if in test mode
|
|
WandererApp.Test.MockAllowance.setup_genserver_mocks(pid)
|
|
|
|
# Also get the actual map server pid and allow access
|
|
case WandererApp.Map.Server.map_pid(map_id) do
|
|
server_pid when is_pid(server_pid) ->
|
|
WandererApp.DataCase.allow_genserver_database_access(server_pid)
|
|
|
|
# Allow Mox access for the map server process if in test mode
|
|
WandererApp.Test.MockAllowance.setup_genserver_mocks(server_pid)
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
|
|
{:ok, pid}
|
|
|
|
{:error, {:already_started, pid}} ->
|
|
WandererApp.DataCase.allow_database_access(pid)
|
|
{:ok, pid}
|
|
|
|
{:error, :max_children} ->
|
|
# If we hit max children, wait a bit and retry
|
|
:timer.sleep(100)
|
|
start_map_server_directly(map_id)
|
|
|
|
error ->
|
|
error
|
|
end
|
|
end
|
|
|
|
defp allow_map_server_children_database_access(map_server_pid) do
|
|
# Allow database access for all children processes
|
|
# This is important for MapEventRelay and other spawned processes
|
|
|
|
# Wait a bit for children to spawn
|
|
:timer.sleep(100)
|
|
|
|
# Get all linked processes
|
|
case Process.info(map_server_pid, :links) do
|
|
{:links, linked_pids} ->
|
|
Enum.each(linked_pids, fn linked_pid ->
|
|
if is_pid(linked_pid) and Process.alive?(linked_pid) do
|
|
WandererApp.DataCase.allow_database_access(linked_pid)
|
|
|
|
# Also check for their children
|
|
case Process.info(linked_pid, :links) do
|
|
{:links, sub_links} ->
|
|
Enum.each(sub_links, fn sub_pid ->
|
|
if is_pid(sub_pid) and Process.alive?(sub_pid) and sub_pid != map_server_pid do
|
|
WandererApp.DataCase.allow_database_access(sub_pid)
|
|
end
|
|
end)
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
end
|
|
end)
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
end
|
|
end
|