- CacheTest: fix sweep timing race by using negative TTL (-1) instead of positive TTL (1) for already-expired entries - ScoreCache: replace ETS match-spec DateTime comparisons with :ets.foldl + DateTime.compare — DateTime structs are maps in Elixir >= 1.15 and ETS can't compare maps with :< / :> guards - Accounts: drop unsupported returning: true on delete_all, return [] for expired tokens list - Backtest: catch ArgumentError from String.to_existing_atom for unknown feature names, preserving the helpful Mix.Error - ContactLive IndexTest: invalidate monthly_bars cache before assertion so test data is visible - RoverLocationsLive MapTest: invalidate cached points before assertion - StatusLiveTest: add DB cleanup in setup to reduce test interference; parameterize NARR candidate coordinates
242 lines
7.9 KiB
Elixir
242 lines
7.9 KiB
Elixir
defmodule MicrowavepropWeb.ContactLive.IndexTest do
|
|
use MicrowavepropWeb.ConnCase, async: true
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Microwaveprop.Radio.Contact
|
|
alias Microwaveprop.Repo
|
|
alias Microwaveprop.Terrain.ElevationClient
|
|
alias Microwaveprop.Weather.HrrrNativeProfile
|
|
alias Microwaveprop.Weather.NarrProfile
|
|
|
|
setup do
|
|
Req.Test.stub(ElevationClient, fn conn ->
|
|
Req.Test.json(conn, [])
|
|
end)
|
|
|
|
Req.Test.stub(Microwaveprop.Weather.HrrrClient, fn conn ->
|
|
Plug.Conn.send_resp(conn, 404, "not found")
|
|
end)
|
|
|
|
Req.Test.stub(Microwaveprop.Weather.SolarClient, fn conn ->
|
|
Req.Test.text(conn, "")
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
defp create_contact(attrs \\ %{}) do
|
|
default = %{
|
|
station1: "W5XD",
|
|
station2: "K5TR",
|
|
qso_timestamp: ~U[2026-03-28 18:00:00Z],
|
|
mode: "CW",
|
|
band: Decimal.new("1296"),
|
|
grid1: "EM12",
|
|
grid2: "EM00",
|
|
pos1: %{"lat" => 32.9, "lon" => -97.0},
|
|
pos2: %{"lat" => 30.3, "lon" => -97.7},
|
|
distance_km: Decimal.new("295")
|
|
}
|
|
|
|
merged = Map.merge(default, attrs)
|
|
{user_id, changeset_attrs} = Map.pop(merged, :user_id)
|
|
|
|
{:ok, contact} =
|
|
%Contact{user_id: user_id}
|
|
|> Contact.changeset(changeset_attrs)
|
|
|> Repo.insert()
|
|
|
|
contact
|
|
end
|
|
|
|
describe "Index" do
|
|
test "renders page with Contacts heading", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
assert html =~ "Contacts"
|
|
end
|
|
|
|
test "shows total contact count in subtitle", %{conn: conn} do
|
|
create_contact()
|
|
create_contact(%{station1: "N5AC"})
|
|
create_contact(%{station1: "K5TR"})
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
assert html =~ "3 total"
|
|
end
|
|
|
|
test "table shows contact data", %{conn: conn} do
|
|
create_contact()
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
assert html =~ "W5XD"
|
|
assert html =~ "K5TR"
|
|
assert html =~ "CW"
|
|
assert html =~ "1296"
|
|
end
|
|
|
|
test "row links to detail page", %{conn: conn} do
|
|
contact = create_contact()
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
assert html =~ ~p"/contacts/#{contact.id}"
|
|
end
|
|
|
|
test "sort params order the table", %{conn: conn} do
|
|
create_contact(%{station1: "ZZ9ZZ"})
|
|
create_contact(%{station1: "AA1AA"})
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts?sort_params[station1]=asc")
|
|
|
|
aa_pos = html |> :binary.match("AA1AA") |> elem(0)
|
|
zz_pos = html |> :binary.match("ZZ9ZZ") |> elem(0)
|
|
assert aa_pos < zz_pos
|
|
end
|
|
|
|
test "search by single callsign matches either station", %{conn: conn} do
|
|
create_contact(%{station1: "W5LUA", station2: "W5HN"})
|
|
create_contact(%{station1: "K5TR", station2: "N5AC"})
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts?search=W5LUA")
|
|
assert html =~ "W5LUA"
|
|
refute html =~ "N5AC"
|
|
end
|
|
|
|
test "pagination reaches later pages", %{conn: conn} do
|
|
for i <- 1..25 do
|
|
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
|
|
create_contact(%{qso_timestamp: ts, station1: "W#{i}TEST"})
|
|
end
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
assert html =~ "Page"
|
|
|
|
{:ok, _lv, html2} = live(conn, ~p"/contacts?page=2")
|
|
assert html2 =~ "Page"
|
|
end
|
|
|
|
test "renders a per-month bar chart that sums across years", %{conn: conn} do
|
|
create_contact(%{qso_timestamp: ~U[2025-03-15 12:00:00Z], station1: "W5MAR"})
|
|
create_contact(%{qso_timestamp: ~U[2026-03-22 12:00:00Z], station1: "K5MAR"})
|
|
create_contact(%{qso_timestamp: ~U[2026-07-10 12:00:00Z], station1: "W5JUL"})
|
|
|
|
Microwaveprop.Cache.invalidate({MicrowavepropWeb.ContactLive.Index, :monthly_bars})
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
|
|
assert html =~ "Contacts by month"
|
|
assert html =~ ~s|data-month="3"|
|
|
assert html =~ ~s|data-month-count="2"|
|
|
assert html =~ ~s|data-month="7"|
|
|
assert html =~ ~s|data-month-count="1"|
|
|
assert html =~ ~s|data-month="1"|
|
|
assert html =~ ~s|data-month-count="0"|
|
|
end
|
|
end
|
|
|
|
describe "Index private visibility" do
|
|
import Microwaveprop.AccountsFixtures
|
|
|
|
test "anonymous viewer does not see private contacts in the table", %{conn: conn} do
|
|
_private = create_contact(%{station1: "W5PRV", private: true})
|
|
_public = create_contact(%{station1: "W5PUB", qso_timestamp: ~U[2026-03-28 19:00:00Z]})
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
|
|
assert html =~ "W5PUB"
|
|
refute html =~ "W5PRV"
|
|
end
|
|
|
|
test "owner sees their own private inline with Yes badge", %{conn: conn} do
|
|
owner = user_fixture()
|
|
_private = create_contact(%{station1: "W5PRV", private: true, user_id: owner.id})
|
|
_public = create_contact(%{station1: "W5PUB", qso_timestamp: ~U[2026-03-28 19:00:00Z]})
|
|
conn = log_in_user(conn, owner)
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
|
|
assert html =~ "W5PRV"
|
|
assert html =~ "W5PUB"
|
|
assert html =~ "badge-warning"
|
|
end
|
|
|
|
test "admin sees all private contacts", %{conn: conn} do
|
|
admin = user_fixture() |> Ecto.Changeset.change(is_admin: true) |> Repo.update!()
|
|
_private = create_contact(%{station1: "W5PRV", private: true})
|
|
_public = create_contact(%{station1: "W5PUB", qso_timestamp: ~U[2026-03-28 19:00:00Z]})
|
|
conn = log_in_user(conn, admin)
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
assert html =~ "W5PRV"
|
|
assert html =~ "W5PUB"
|
|
end
|
|
|
|
test "non-owning user does not see others' private", %{conn: conn} do
|
|
owner = user_fixture()
|
|
viewer = user_fixture()
|
|
_private = create_contact(%{station1: "W5PRV", private: true, user_id: owner.id})
|
|
_public = create_contact(%{station1: "W5PUB", qso_timestamp: ~U[2026-03-28 19:00:00Z]})
|
|
conn = log_in_user(conn, viewer)
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
assert html =~ "W5PUB"
|
|
refute html =~ "W5PRV"
|
|
end
|
|
end
|
|
|
|
describe "Index private column visibility" do
|
|
import Microwaveprop.AccountsFixtures
|
|
|
|
test "column hidden for anonymous viewer when no private contacts exist", %{conn: conn} do
|
|
_public = create_contact(%{station1: "W5PUB"})
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
refute html =~ ">Private<"
|
|
end
|
|
|
|
test "column hidden for anonymous viewer even when others have private contacts", %{conn: conn} do
|
|
_private = create_contact(%{station1: "W5PRV", private: true})
|
|
_public = create_contact(%{station1: "W5PUB"})
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
refute html =~ ">Private<"
|
|
end
|
|
|
|
test "column hidden for a user who has no private contacts of their own", %{conn: conn} do
|
|
owner = user_fixture()
|
|
viewer = user_fixture()
|
|
_others_private = create_contact(%{station1: "W5PRV", private: true, user_id: owner.id})
|
|
conn = log_in_user(conn, viewer)
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
refute html =~ ">Private<"
|
|
end
|
|
|
|
test "column visible for a user with at least one of their own private contacts", %{conn: conn} do
|
|
owner = user_fixture()
|
|
_private = create_contact(%{station1: "W5PRV", private: true, user_id: owner.id})
|
|
conn = log_in_user(conn, owner)
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
assert html =~ ">Private<"
|
|
end
|
|
|
|
test "column hidden for admin when there are no private contacts anywhere", %{conn: conn} do
|
|
admin = user_fixture() |> Ecto.Changeset.change(is_admin: true) |> Repo.update!()
|
|
_public = create_contact(%{station1: "W5PUB"})
|
|
conn = log_in_user(conn, admin)
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
refute html =~ ">Private<"
|
|
end
|
|
|
|
test "column visible for admin as soon as any private contact exists", %{conn: conn} do
|
|
admin = user_fixture() |> Ecto.Changeset.change(is_admin: true) |> Repo.update!()
|
|
_private = create_contact(%{station1: "W5PRV", private: true})
|
|
conn = log_in_user(conn, admin)
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
assert html =~ ">Private<"
|
|
end
|
|
end
|
|
end
|