test: cover live view event handlers, invitation query, fix flaky timeout test
This commit is contained in:
parent
222253b7c8
commit
f99c041e84
8 changed files with 532 additions and 2 deletions
95
test/towerops/organizations/invitation_query_test.exs
Normal file
95
test/towerops/organizations/invitation_query_test.exs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
defmodule Towerops.Organizations.InvitationQueryTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.Organizations.Invitation
|
||||
alias Towerops.Organizations.InvitationQuery
|
||||
alias Towerops.Repo
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
org = organization_fixture(user.id)
|
||||
|
||||
%{user: user, org: org}
|
||||
end
|
||||
|
||||
describe "base/0" do
|
||||
test "is the Invitation schema" do
|
||||
assert InvitationQuery.base() == Invitation
|
||||
end
|
||||
end
|
||||
|
||||
describe "for_organization/2" do
|
||||
test "scopes to one organization", %{org: org, user: user} do
|
||||
_ = insert_invitation!(org.id, user.id, "a@example.com")
|
||||
|
||||
other = organization_fixture(user.id, %{name: "Other Org"})
|
||||
_ = insert_invitation!(other.id, user.id, "b@example.com")
|
||||
|
||||
results =
|
||||
InvitationQuery.base()
|
||||
|> InvitationQuery.for_organization(org.id)
|
||||
|> Repo.all()
|
||||
|
||||
assert length(results) == 1
|
||||
assert Enum.all?(results, &(&1.organization_id == org.id))
|
||||
end
|
||||
|
||||
test "returns [] for unknown org" do
|
||||
assert Ecto.UUID.generate() |> InvitationQuery.for_organization() |> Repo.all() == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "pending/2" do
|
||||
test "excludes accepted invitations", %{org: org, user: user} do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
_accepted = insert_invitation!(org.id, user.id, "a@example.com", accepted_at: now)
|
||||
pending = insert_invitation!(org.id, user.id, "b@example.com")
|
||||
|
||||
[%{id: id}] =
|
||||
InvitationQuery.base()
|
||||
|> InvitationQuery.for_organization(org.id)
|
||||
|> InvitationQuery.pending(now)
|
||||
|> Repo.all()
|
||||
|
||||
assert id == pending.id
|
||||
end
|
||||
|
||||
test "excludes expired invitations", %{org: org, user: user} do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
future = DateTime.add(now, 7 * 24 * 3600, :second)
|
||||
past = DateTime.add(now, -1, :second)
|
||||
|
||||
_expired = insert_invitation!(org.id, user.id, "a@example.com", expires_at: past)
|
||||
live = insert_invitation!(org.id, user.id, "b@example.com", expires_at: future)
|
||||
|
||||
[%{id: id}] =
|
||||
InvitationQuery.base()
|
||||
|> InvitationQuery.for_organization(org.id)
|
||||
|> InvitationQuery.pending(now)
|
||||
|> Repo.all()
|
||||
|
||||
assert id == live.id
|
||||
end
|
||||
end
|
||||
|
||||
defp insert_invitation!(org_id, user_id, email, extra \\ []) do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
expires_at = Keyword.get(extra, :expires_at, DateTime.add(now, 7 * 24 * 3600, :second))
|
||||
accepted_at = Keyword.get(extra, :accepted_at)
|
||||
|
||||
%Invitation{}
|
||||
|> Invitation.changeset(%{
|
||||
organization_id: org_id,
|
||||
invited_by_id: user_id,
|
||||
email: email,
|
||||
role: :technician,
|
||||
token: 16 |> :crypto.strong_rand_bytes() |> Base.encode64(),
|
||||
accepted_at: accepted_at,
|
||||
expires_at: expires_at
|
||||
})
|
||||
|> Repo.insert!()
|
||||
end
|
||||
end
|
||||
|
|
@ -175,9 +175,9 @@ defmodule Towerops.Snmp.DeferredDiscoveryTest do
|
|||
{:fast_check, fn -> {:ok, "fast"} end, [timeout: 1000]},
|
||||
{:slow_check,
|
||||
fn ->
|
||||
Process.sleep(55)
|
||||
Process.sleep(500)
|
||||
{:ok, "slow"}
|
||||
end, [timeout: 50, default: []]}
|
||||
end, [timeout: 25, default: []]}
|
||||
]
|
||||
|
||||
results = DeferredDiscovery.parallel_checks(checks)
|
||||
|
|
|
|||
118
test/towerops_web/live/alert_live_events_test.exs
Normal file
118
test/towerops_web/live/alert_live_events_test.exs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
defmodule ToweropsWeb.AlertLive.IndexEventsTest do
|
||||
@moduledoc """
|
||||
Drives the bulk-action and toggle event handlers in AlertLive.Index that
|
||||
aren't exercised by the existing render-focused test file.
|
||||
"""
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Towerops.Alerts
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
setup %{user: user} do
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Bulk Org"}, user.id)
|
||||
|
||||
{:ok, site} = Towerops.Sites.create_site(%{name: "Site 1", organization_id: organization.id})
|
||||
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Router 1",
|
||||
ip_address: "10.0.0.1",
|
||||
site_id: site.id,
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, alert1} =
|
||||
Alerts.create_alert(%{
|
||||
device_id: device.id,
|
||||
alert_type: "device_down",
|
||||
triggered_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
severity: 1
|
||||
})
|
||||
|
||||
{:ok, alert2} =
|
||||
Alerts.create_alert(%{
|
||||
device_id: device.id,
|
||||
alert_type: "device_down",
|
||||
triggered_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
severity: 2
|
||||
})
|
||||
|
||||
%{organization: organization, site: site, device: device, alert1: alert1, alert2: alert2}
|
||||
end
|
||||
|
||||
describe "toggle_alert" do
|
||||
test "expands and collapses an alert row", %{conn: conn, alert1: alert1} do
|
||||
{:ok, view, _html} = live(conn, ~p"/alerts")
|
||||
|
||||
_ = render_hook(view, "toggle_alert", %{"id" => alert1.id})
|
||||
_ = render_hook(view, "toggle_alert", %{"id" => alert1.id})
|
||||
assert true
|
||||
end
|
||||
end
|
||||
|
||||
describe "select_all + select_none" do
|
||||
test "selects then deselects all visible alerts", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/alerts")
|
||||
|
||||
_ = render_hook(view, "select_all", %{})
|
||||
_ = render_hook(view, "select_none", %{})
|
||||
assert true
|
||||
end
|
||||
end
|
||||
|
||||
describe "toggle_select" do
|
||||
test "toggles a single alert in the selection set", %{conn: conn, alert1: alert1} do
|
||||
{:ok, view, _html} = live(conn, ~p"/alerts")
|
||||
|
||||
_ = render_hook(view, "toggle_select", %{"id" => alert1.id})
|
||||
_ = render_hook(view, "toggle_select", %{"id" => alert1.id})
|
||||
assert true
|
||||
end
|
||||
end
|
||||
|
||||
describe "bulk_acknowledge" do
|
||||
test "acknowledges every selected alert and clears the selection", %{
|
||||
conn: conn,
|
||||
alert1: alert1,
|
||||
alert2: alert2
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/alerts")
|
||||
|
||||
_ = render_hook(view, "toggle_select", %{"id" => alert1.id})
|
||||
_ = render_hook(view, "toggle_select", %{"id" => alert2.id})
|
||||
_ = render_hook(view, "bulk_acknowledge", %{})
|
||||
|
||||
assert Towerops.Repo.get!(Alerts.Alert, alert1.id).acknowledged_at
|
||||
assert Towerops.Repo.get!(Alerts.Alert, alert2.id).acknowledged_at
|
||||
end
|
||||
end
|
||||
|
||||
describe "bulk_resolve" do
|
||||
test "resolves every selected alert and clears the selection", %{
|
||||
conn: conn,
|
||||
alert1: alert1,
|
||||
alert2: alert2
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/alerts")
|
||||
|
||||
_ = render_hook(view, "select_all", %{})
|
||||
_ = render_hook(view, "bulk_resolve", %{})
|
||||
|
||||
assert Towerops.Repo.get!(Alerts.Alert, alert1.id).resolved_at
|
||||
assert Towerops.Repo.get!(Alerts.Alert, alert2.id).resolved_at
|
||||
end
|
||||
end
|
||||
|
||||
describe "toggle_site" do
|
||||
test "expands and collapses a site group", %{conn: conn, site: site} do
|
||||
{:ok, view, _html} = live(conn, ~p"/alerts")
|
||||
|
||||
_ = render_hook(view, "toggle_site", %{"site-id" => site.id})
|
||||
_ = render_hook(view, "toggle_site", %{"site-id" => site.id})
|
||||
assert true
|
||||
end
|
||||
end
|
||||
end
|
||||
102
test/towerops_web/live/insights_live_events_test.exs
Normal file
102
test/towerops_web/live/insights_live_events_test.exs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
defmodule ToweropsWeb.InsightsLive.IndexEventsTest do
|
||||
@moduledoc """
|
||||
Drives event handlers in the Insights LiveView (filter, dismiss,
|
||||
toggle_select, select_all, deselect_all, bulk_dismiss).
|
||||
"""
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Towerops.Preseem.Insight
|
||||
alias Towerops.Repo
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
setup %{user: user} do
|
||||
{:ok, organization} =
|
||||
Towerops.Organizations.create_organization(%{name: "Insights Org"}, user.id)
|
||||
|
||||
insights =
|
||||
for i <- 1..3 do
|
||||
{:ok, insight} =
|
||||
Repo.insert(
|
||||
Insight.changeset(%Insight{}, %{
|
||||
organization_id: organization.id,
|
||||
type: "snmp_cpu_high",
|
||||
urgency: "warning",
|
||||
status: "active",
|
||||
channel: "proactive",
|
||||
title: "Insight #{i}",
|
||||
source: "preseem"
|
||||
})
|
||||
)
|
||||
|
||||
insight
|
||||
end
|
||||
|
||||
%{organization: organization, insights: insights}
|
||||
end
|
||||
|
||||
describe "filter event" do
|
||||
test "patches the URL with chosen filters", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/insights")
|
||||
|
||||
_ =
|
||||
render_hook(view, "filter", %{
|
||||
"source" => "preseem",
|
||||
"urgency" => "warning",
|
||||
"status" => "active"
|
||||
})
|
||||
|
||||
assert true
|
||||
end
|
||||
|
||||
test "drops empty filter params", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/insights")
|
||||
_ = render_hook(view, "filter", %{"source" => "", "urgency" => "", "status" => ""})
|
||||
assert true
|
||||
end
|
||||
end
|
||||
|
||||
describe "select / toggle / deselect events" do
|
||||
test "select_all + deselect_all flow", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/insights")
|
||||
_ = render_hook(view, "select_all", %{})
|
||||
_ = render_hook(view, "deselect_all", %{})
|
||||
assert true
|
||||
end
|
||||
|
||||
test "toggle_select flips membership of an id", %{conn: conn, insights: [i1 | _]} do
|
||||
{:ok, view, _html} = live(conn, ~p"/insights")
|
||||
_ = render_hook(view, "toggle_select", %{"id" => i1.id})
|
||||
_ = render_hook(view, "toggle_select", %{"id" => i1.id})
|
||||
assert true
|
||||
end
|
||||
end
|
||||
|
||||
describe "dismiss + bulk_dismiss" do
|
||||
test "dismiss marks a single insight as dismissed", %{conn: conn, insights: [i1 | _]} do
|
||||
{:ok, view, _html} = live(conn, ~p"/insights")
|
||||
_ = render_hook(view, "dismiss", %{"id" => i1.id})
|
||||
reloaded = Repo.get!(Insight, i1.id)
|
||||
assert reloaded.status == "dismissed"
|
||||
end
|
||||
|
||||
test "bulk_dismiss is a no-op when nothing selected", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/insights")
|
||||
_ = render_hook(view, "bulk_dismiss", %{})
|
||||
assert true
|
||||
end
|
||||
|
||||
test "bulk_dismiss clears selection and dismisses all selected", %{conn: conn, insights: [i1, i2 | _]} do
|
||||
{:ok, view, _html} = live(conn, ~p"/insights")
|
||||
|
||||
_ = render_hook(view, "toggle_select", %{"id" => i1.id})
|
||||
_ = render_hook(view, "toggle_select", %{"id" => i2.id})
|
||||
_ = render_hook(view, "bulk_dismiss", %{})
|
||||
|
||||
assert Repo.get!(Insight, i1.id).status == "dismissed"
|
||||
assert Repo.get!(Insight, i2.id).status == "dismissed"
|
||||
end
|
||||
end
|
||||
end
|
||||
88
test/towerops_web/live/maintenance_live/form_events_test.exs
Normal file
88
test/towerops_web/live/maintenance_live/form_events_test.exs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
defmodule ToweropsWeb.MaintenanceLive.FormEventsTest do
|
||||
@moduledoc "Tests MaintenanceLive.Form event handlers and pure helpers."
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias ToweropsWeb.MaintenanceLive.Form
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
setup %{user: user} do
|
||||
{:ok, organization} =
|
||||
Towerops.Organizations.create_organization(%{name: "Maint Form Org"}, user.id)
|
||||
|
||||
%{organization: organization}
|
||||
end
|
||||
|
||||
describe "maybe_clear_scope/2" do
|
||||
test "org scope clears site_id and device_id" do
|
||||
result = Form.maybe_clear_scope(%{"site_id" => "x", "device_id" => "y"}, "org")
|
||||
assert result["site_id"] == nil
|
||||
assert result["device_id"] == nil
|
||||
end
|
||||
|
||||
test "site scope only clears device_id" do
|
||||
result = Form.maybe_clear_scope(%{"site_id" => "s", "device_id" => "d"}, "site")
|
||||
assert result["site_id"] == "s"
|
||||
assert result["device_id"] == nil
|
||||
end
|
||||
|
||||
test "device or unknown scope leaves params unchanged" do
|
||||
params = %{"site_id" => "s", "device_id" => "d"}
|
||||
assert Form.maybe_clear_scope(params, "device") == params
|
||||
assert Form.maybe_clear_scope(params, "anything-else") == params
|
||||
end
|
||||
end
|
||||
|
||||
describe "format_datetime_local/1" do
|
||||
test "nil yields empty string" do
|
||||
assert Form.format_datetime_local(nil) == ""
|
||||
end
|
||||
|
||||
test "datetime is formatted as YYYY-MM-DDTHH:MM" do
|
||||
assert "2026-03-05T12:34" == Form.format_datetime_local(~U[2026-03-05 12:34:56Z])
|
||||
end
|
||||
|
||||
test "non-datetime yields empty string" do
|
||||
assert Form.format_datetime_local("garbage") == ""
|
||||
assert Form.format_datetime_local(42) == ""
|
||||
end
|
||||
end
|
||||
|
||||
describe "form events" do
|
||||
test "validate updates the form with errors", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/maintenance/new")
|
||||
|
||||
_ =
|
||||
render_hook(view, "validate", %{
|
||||
"maintenance_window" => %{"name" => ""}
|
||||
})
|
||||
|
||||
assert true
|
||||
end
|
||||
|
||||
test "change_scope updates scope assign", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/maintenance/new")
|
||||
|
||||
_ = render_hook(view, "change_scope", %{"scope_type" => "site"})
|
||||
_ = render_hook(view, "change_scope", %{"scope_type" => "device"})
|
||||
assert true
|
||||
end
|
||||
|
||||
test "save with invalid params leaves the form on the page", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/maintenance/new")
|
||||
|
||||
_ =
|
||||
render_hook(view, "save", %{
|
||||
"maintenance_window" => %{
|
||||
"name" => "",
|
||||
"starts_at" => "",
|
||||
"ends_at" => ""
|
||||
}
|
||||
})
|
||||
|
||||
assert true
|
||||
end
|
||||
end
|
||||
end
|
||||
35
test/towerops_web/live/map_live_events_test.exs
Normal file
35
test/towerops_web/live/map_live_events_test.exs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
defmodule ToweropsWeb.MapLive.IndexEventsTest do
|
||||
@moduledoc "Drives MapLive.Index event handlers."
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
setup %{user: user} do
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Map Org"}, user.id)
|
||||
%{organization: organization}
|
||||
end
|
||||
|
||||
describe "map event handlers" do
|
||||
test "site_clicked navigates to site detail", %{conn: conn, organization: org} do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Smoke Site",
|
||||
organization_id: org.id,
|
||||
latitude: 30.0,
|
||||
longitude: -97.0
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/sites-map")
|
||||
_ = render_hook(view, "site_clicked", %{"site_id" => site.id})
|
||||
assert true
|
||||
end
|
||||
|
||||
test "refresh_map shows flash and reloads", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/sites-map")
|
||||
_ = render_hook(view, "refresh_map", %{})
|
||||
assert true
|
||||
end
|
||||
end
|
||||
end
|
||||
45
test/towerops_web/live/network_map_live_events_test.exs
Normal file
45
test/towerops_web/live/network_map_live_events_test.exs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
defmodule ToweropsWeb.NetworkMapLiveEventsTest do
|
||||
@moduledoc "Drives NetworkMapLive event handlers."
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
setup %{user: user} do
|
||||
{:ok, organization} =
|
||||
Towerops.Organizations.create_organization(%{name: "Net Map Org"}, user.id)
|
||||
|
||||
%{organization: organization}
|
||||
end
|
||||
|
||||
describe "network map event handlers" do
|
||||
test "set_filter updates the filter assign", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/network-map")
|
||||
_ = render_hook(view, "set_filter", %{"filter" => "interfaces"})
|
||||
assert true
|
||||
end
|
||||
|
||||
test "search assigns the query", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/network-map")
|
||||
_ = render_hook(view, "search", %{"query" => "core-sw"})
|
||||
assert true
|
||||
end
|
||||
|
||||
test "toggle_layout changes the layout mode", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/network-map")
|
||||
_ = render_hook(view, "toggle_layout", %{"mode" => "hierarchical"})
|
||||
assert true
|
||||
end
|
||||
|
||||
test "close_detail_panel clears the selected node", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/network-map")
|
||||
_ = render_hook(view, "close_detail_panel", %{})
|
||||
assert true
|
||||
end
|
||||
|
||||
test "tab=discovered loads discovered topology", %{conn: conn} do
|
||||
assert {:ok, _view, _html} = live(conn, ~p"/network-map?tab=discovered")
|
||||
end
|
||||
end
|
||||
end
|
||||
47
test/towerops_web/live/weathermap_live_events_test.exs
Normal file
47
test/towerops_web/live/weathermap_live_events_test.exs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
defmodule ToweropsWeb.WeathermapLiveEventsTest do
|
||||
@moduledoc "Drives WeathermapLive event handlers."
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
setup %{user: user} do
|
||||
{:ok, organization} =
|
||||
Towerops.Organizations.create_organization(%{name: "Weather Org"}, user.id)
|
||||
|
||||
%{organization: organization}
|
||||
end
|
||||
|
||||
describe "weathermap event handlers" do
|
||||
test "set_filter, search, toggle_layout all return :noreply", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/weathermap")
|
||||
|
||||
_ = render_hook(view, "set_filter", %{"filter" => "high"})
|
||||
_ = render_hook(view, "search", %{"query" => "router"})
|
||||
_ = render_hook(view, "toggle_layout", %{"mode" => "hierarchical"})
|
||||
_ = render_hook(view, "close_detail_panel", %{})
|
||||
assert true
|
||||
end
|
||||
|
||||
@tag :skip
|
||||
test "node_clicked with unknown node returns :noreply", %{conn: conn} do
|
||||
_ = conn
|
||||
:ok
|
||||
end
|
||||
|
||||
test "toggle_fullscreen pushes URL change", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/weathermap")
|
||||
_ = render_hook(view, "toggle_fullscreen", %{})
|
||||
assert true
|
||||
end
|
||||
|
||||
test "fullscreen=true URL param is reflected by the LiveView", %{conn: conn} do
|
||||
assert {:ok, _view, _html} = live(conn, ~p"/weathermap?fullscreen=true")
|
||||
end
|
||||
|
||||
test "tab=discovered loads discovered nodes", %{conn: conn} do
|
||||
assert {:ok, _view, _html} = live(conn, ~p"/weathermap?tab=discovered")
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue