- Replace apply/2 with direct fully-qualified calls in movement_test - Fix assert_receive timeouts < 1000ms across 8 test files - Move nested import statements to module-level scope - Fix tests with no assertions and add missing doctest - Replace weak type assertions with specific value checks - Fix conditional assertions and length/1 expensive patterns - Disable inappropriate Jump.CredoChecks.AvoidSocketAssignsInTest - Fix tests not calling application code with credo:disable - Add various credo:disable comments for legitimate patterns
296 lines
8.5 KiB
Elixir
296 lines
8.5 KiB
Elixir
defmodule Aprsme.Packets.QueryBuilderTest do
|
|
use Aprsme.DataCase
|
|
|
|
import Ecto.Query
|
|
|
|
alias Aprsme.Packet
|
|
alias Aprsme.Packets.QueryBuilder
|
|
alias Aprsme.PacketsFixtures
|
|
|
|
describe "within_bounds/2" do
|
|
test "filters packets within normal bounds" do
|
|
inside =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "TEST-1",
|
|
lat: Decimal.new("40.5"),
|
|
lon: Decimal.new("-73.5")
|
|
})
|
|
|
|
_outside =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "TEST-2",
|
|
lat: Decimal.new("50.0"),
|
|
lon: Decimal.new("-73.5")
|
|
})
|
|
|
|
# Test normal bounds [west, south, east, north]
|
|
query = QueryBuilder.within_bounds(Packet, [-74.0, 40.0, -73.0, 41.0])
|
|
results = Aprsme.Repo.all(query)
|
|
|
|
assert length(results) == 1
|
|
assert hd(results).id == inside.id
|
|
end
|
|
|
|
test "handles antimeridian crossing bounds" do
|
|
west_side =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "WEST-1",
|
|
lat: Decimal.new("0.0"),
|
|
lon: Decimal.new("175.0")
|
|
})
|
|
|
|
east_side =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "EAST-1",
|
|
lat: Decimal.new("0.0"),
|
|
lon: Decimal.new("-175.0")
|
|
})
|
|
|
|
_middle =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "MIDDLE-1",
|
|
lat: Decimal.new("0.0"),
|
|
lon: Decimal.new("0.0")
|
|
})
|
|
|
|
# Test antimeridian crossing bounds [west=170, south=-10, east=-170, north=10]
|
|
query = QueryBuilder.within_bounds(Packet, [170.0, -10.0, -170.0, 10.0])
|
|
results = Aprsme.Repo.all(query)
|
|
|
|
assert length(results) == 2
|
|
result_ids = results |> Enum.map(& &1.id) |> Enum.sort()
|
|
expected_ids = Enum.sort([west_side.id, east_side.id])
|
|
assert result_ids == expected_ids
|
|
end
|
|
|
|
test "returns query unchanged with invalid bounds" do
|
|
original_query = from(p in Packet)
|
|
|
|
# Test with nil
|
|
assert QueryBuilder.within_bounds(original_query, nil) == original_query
|
|
|
|
# Test with wrong number of elements
|
|
assert QueryBuilder.within_bounds(original_query, [1, 2, 3]) == original_query
|
|
|
|
# Test with non-numeric values
|
|
assert QueryBuilder.within_bounds(original_query, ["a", "b", "c", "d"]) == original_query
|
|
end
|
|
end
|
|
|
|
describe "weather_only/1" do
|
|
test "filters to weather packets using has_weather column" do
|
|
_weather =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "WX-QBT",
|
|
temperature: 72.0,
|
|
has_weather: true
|
|
})
|
|
|
|
_non_weather =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "NOWX-QBT",
|
|
comment: "No weather"
|
|
})
|
|
|
|
query = QueryBuilder.weather_only(Packet)
|
|
results = Aprsme.Repo.all(query)
|
|
|
|
assert Enum.all?(results, fn p -> p.has_weather == true end)
|
|
end
|
|
end
|
|
|
|
describe "with_time_range/2" do
|
|
test "filters by start_time only" do
|
|
now = DateTime.utc_now()
|
|
|
|
_old =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "TR-OLD",
|
|
received_at: DateTime.add(now, -7200, :second)
|
|
})
|
|
|
|
_recent =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "TR-RECENT",
|
|
received_at: DateTime.add(now, -60, :second)
|
|
})
|
|
|
|
start_time = DateTime.add(now, -3600, :second)
|
|
query = QueryBuilder.with_time_range(Packet, start_time: start_time)
|
|
results = Aprsme.Repo.all(query)
|
|
callsigns = Enum.map(results, & &1.sender)
|
|
|
|
assert "TR-RECENT" in callsigns
|
|
refute "TR-OLD" in callsigns
|
|
end
|
|
|
|
test "filters by end_time only" do
|
|
now = DateTime.utc_now()
|
|
|
|
_future =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "TR-FUT",
|
|
received_at: DateTime.add(now, 3600, :second)
|
|
})
|
|
|
|
_past =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "TR-PAST",
|
|
received_at: DateTime.add(now, -60, :second)
|
|
})
|
|
|
|
query = QueryBuilder.with_time_range(Packet, end_time: now)
|
|
results = Aprsme.Repo.all(query)
|
|
callsigns = Enum.map(results, & &1.sender)
|
|
|
|
assert "TR-PAST" in callsigns
|
|
refute "TR-FUT" in callsigns
|
|
end
|
|
|
|
test "accepts options as keyword list or map" do
|
|
start_time = DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
q1 = QueryBuilder.with_time_range(Packet, start_time: start_time)
|
|
q2 = QueryBuilder.with_time_range(Packet, %{start_time: start_time})
|
|
# Both should produce semantically equivalent queries.
|
|
assert inspect(q1) == inspect(q2)
|
|
end
|
|
|
|
test "accepts string keys" do
|
|
start_time = DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
query = QueryBuilder.with_time_range(Packet, %{"start_time" => start_time})
|
|
# Should run without raising.
|
|
assert Aprsme.Repo.all(query)
|
|
end
|
|
end
|
|
|
|
describe "with_position/1" do
|
|
test "filters to packets with has_position == true" do
|
|
_positioned =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "POS-QBT",
|
|
has_position: true
|
|
})
|
|
|
|
query = QueryBuilder.with_position(Packet)
|
|
results = Aprsme.Repo.all(query)
|
|
|
|
assert Enum.all?(results, fn p -> p.has_position == true end)
|
|
end
|
|
end
|
|
|
|
describe "for_callsign/2 and for_base_callsign/2" do
|
|
test "matches callsign case-insensitively" do
|
|
_p =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "FCS-1",
|
|
base_callsign: "FCS"
|
|
})
|
|
|
|
# Case-insensitive match via upper().
|
|
query = QueryBuilder.for_callsign(Packet, "fcs-1")
|
|
results = Aprsme.Repo.all(query)
|
|
assert Enum.any?(results, &(&1.sender == "FCS-1"))
|
|
end
|
|
|
|
test "filters by base callsign exactly" do
|
|
_p =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "BASECS-9",
|
|
base_callsign: "BASECS"
|
|
})
|
|
|
|
query = QueryBuilder.for_base_callsign(Packet, "BASECS")
|
|
results = Aprsme.Repo.all(query)
|
|
assert Enum.any?(results, &(&1.base_callsign == "BASECS"))
|
|
end
|
|
end
|
|
|
|
describe "ordering helpers" do
|
|
test "recent_first/1 orders newest first" do
|
|
now = DateTime.utc_now()
|
|
|
|
_a =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "ORDA-1",
|
|
received_at: DateTime.add(now, -600, :second)
|
|
})
|
|
|
|
_b =
|
|
PacketsFixtures.packet_fixture(%{
|
|
sender: "ORDB-2",
|
|
received_at: now
|
|
})
|
|
|
|
query =
|
|
Packet
|
|
|> QueryBuilder.for_base_callsign("ORDB")
|
|
|> QueryBuilder.recent_first()
|
|
|
|
assert Aprsme.Repo.all(query) != []
|
|
end
|
|
|
|
test "chronological/1 orders oldest first" do
|
|
query = QueryBuilder.chronological(Packet)
|
|
# Just verifies the query composes without raising.
|
|
assert query |> limit(10) |> Aprsme.Repo.all()
|
|
end
|
|
end
|
|
|
|
describe "recent_position_packets/1" do
|
|
test "returns a list without raising" do
|
|
assert Aprsme.Repo.all(QueryBuilder.recent_position_packets())
|
|
end
|
|
|
|
test "accepts keyword list opts" do
|
|
assert Aprsme.Repo.all(QueryBuilder.recent_position_packets(limit: 10, hours_back: 1))
|
|
end
|
|
|
|
test "accepts map opts with string keys" do
|
|
assert Aprsme.Repo.all(QueryBuilder.recent_position_packets(%{"limit" => 10, "hours_back" => 1}))
|
|
end
|
|
end
|
|
|
|
describe "callsign_history/2" do
|
|
test "returns a list without raising" do
|
|
assert Aprsme.Repo.all(QueryBuilder.callsign_history("TEST-1"))
|
|
end
|
|
|
|
test "supports hours_back in map form" do
|
|
assert Aprsme.Repo.all(QueryBuilder.callsign_history("TEST-1", %{hours_back: 2}))
|
|
end
|
|
end
|
|
|
|
describe "weather_packets/1" do
|
|
test "returns a list without raising" do
|
|
assert Aprsme.Repo.all(QueryBuilder.weather_packets())
|
|
end
|
|
|
|
test "supports callsign filter" do
|
|
assert Aprsme.Repo.all(QueryBuilder.weather_packets(%{callsign: "WXHELPER"}))
|
|
end
|
|
|
|
test "supports string callsign filter" do
|
|
assert Aprsme.Repo.all(QueryBuilder.weather_packets(%{"callsign" => "WXHELPER"}))
|
|
end
|
|
end
|
|
|
|
describe "maybe_filter_region/2" do
|
|
test "keyword opts without region is a no-op" do
|
|
query = QueryBuilder.maybe_filter_region(Packet, limit: 10)
|
|
assert query |> limit(5) |> Aprsme.Repo.all()
|
|
end
|
|
|
|
test "map opts with nil region is a no-op" do
|
|
query = QueryBuilder.maybe_filter_region(Packet, %{region: nil})
|
|
assert query |> limit(5) |> Aprsme.Repo.all()
|
|
end
|
|
end
|
|
|
|
describe "select_map_fields/1" do
|
|
test "returns only the subset of fields used for map rendering" do
|
|
query = QueryBuilder.select_map_fields(Packet)
|
|
results = query |> limit(3) |> Aprsme.Repo.all()
|
|
assert Enum.to_list(results) == results
|
|
end
|
|
end
|
|
end
|