defmodule ToweropsWeb.DashboardLiveTest do use ToweropsWeb.ConnCase import Phoenix.LiveViewTest alias Towerops.Preseem.Insights setup :register_and_log_in_user setup %{conn: conn, user: user} do {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org", use_sites: true}, user.id) {:ok, site} = Towerops.Sites.create_site(%{ name: "Test Site", organization_id: organization.id }) conn = Plug.Conn.put_session(conn, :current_organization_id, organization.id) %{conn: conn, organization: organization, site: site} end describe "empty state" do test "shows onboarding when no devices exist", %{conn: conn} do {:ok, _view, html} = live(conn, ~p"/dashboard") assert html =~ "Let's get started!" assert html =~ "Create Your First Site" end test "requires authentication" do conn = build_conn() {:error, redirect} = live(conn, ~p"/dashboard") assert {:redirect, %{to: path}} = redirect assert path == ~p"/users/log-in" end end describe "health overview" do test "displays health score", %{conn: conn, organization: organization, site: site} do {:ok, device} = create_device(organization, site) Towerops.Devices.update_device_status(device, :up) {:ok, _view, html} = live(conn, ~p"/dashboard") assert html =~ "Health" assert html =~ "100" end test "displays device counts with up/down/unknown", %{ conn: conn, organization: organization, site: site } do {:ok, d1} = create_device(organization, site, "Router 1", "192.168.1.1") {:ok, d2} = create_device(organization, site, "Router 2", "192.168.1.2") Towerops.Devices.update_device_status(d1, :up) Towerops.Devices.update_device_status(d2, :down) {:ok, _view, html} = live(conn, ~p"/dashboard") assert html =~ "Devices" assert html =~ "Up" assert html =~ "Down" end test "displays insight counts by urgency", %{ conn: conn, organization: organization, site: site } do {:ok, device} = create_device(organization, site) {:ok, _} = Insights.insert_insight_if_new(%{ organization_id: organization.id, device_id: device.id, type: "device_poll_gap", source: "snmp", urgency: "warning", channel: "proactive", title: "Device not polled" }) {:ok, _view, html} = live(conn, ~p"/dashboard") assert html =~ "Insights" end test "hides subscriber section when no Gaiia integration", %{ conn: conn, organization: organization, site: site } do {:ok, _device} = create_device(organization, site) {:ok, _view, html} = live(conn, ~p"/dashboard") refute html =~ "Subscribers" refute html =~ "MRR" end test "shows subscriber section when billing data exists", %{ conn: conn, organization: organization, site: site } do {:ok, _device} = create_device(organization, site) {:ok, integration} = Towerops.Integrations.create_integration(organization.id, %{ provider: "gaiia", enabled: true }) {:ok, _} = Towerops.Integrations.update_billing_totals(integration, 100, Decimal.new("5000.00")) {:ok, _view, html} = live(conn, ~p"/dashboard") assert html =~ "Subs" assert html =~ "100" assert html =~ "5,000" end end describe "alert feed" do test "shows alerts with device links", %{conn: conn, organization: organization, site: site} do {:ok, device} = create_device(organization, site, "Main Router", "10.0.0.1") Towerops.Devices.update_device_status(device, :down) {:ok, _alert} = Towerops.Alerts.create_alert(%{ device_id: device.id, alert_type: "device_down", triggered_at: DateTime.utc_now(), message: "Device is unreachable" }) {:ok, _view, html} = live(conn, ~p"/dashboard") assert html =~ "Main Router" assert html =~ "Device is unreachable" assert html =~ ~p"/devices/#{device.id}" end test "shows acknowledge button for unacknowledged alerts", %{ conn: conn, organization: organization, site: site } do {:ok, device} = create_device(organization, site) {:ok, _alert} = Towerops.Alerts.create_alert(%{ device_id: device.id, alert_type: "device_down", triggered_at: DateTime.utc_now(), message: "Device is down" }) {:ok, _view, html} = live(conn, ~p"/dashboard") assert html =~ "ACK" end end describe "insight feed" do test "shows insights from multiple sources", %{ conn: conn, organization: organization, site: site } do {:ok, device} = create_device(organization, site) {:ok, _} = Insights.insert_insight_if_new(%{ organization_id: organization.id, device_id: device.id, type: "device_poll_gap", source: "snmp", urgency: "warning", channel: "proactive", title: "Device not polled in 24h" }) {:ok, _view, html} = live(conn, ~p"/dashboard") assert html =~ "Device not polled in 24h" assert html =~ "snmp" end test "dismiss insight removes it from feed", %{ conn: conn, organization: organization, site: site } do {:ok, device} = create_device(organization, site) {:ok, insight} = Insights.insert_insight_if_new(%{ organization_id: organization.id, device_id: device.id, type: "device_poll_gap", source: "snmp", urgency: "warning", channel: "proactive", title: "Poll gap detected" }) {:ok, view, html} = live(conn, ~p"/dashboard") assert html =~ "Poll gap detected" view |> element("button[phx-value-id='#{insight.id}']") |> render_click() html = render(view) refute html =~ "Poll gap detected" end test "source filter filters insights via URL", %{ conn: conn, organization: organization, site: site } do {:ok, device} = create_device(organization, site) {:ok, _} = Insights.insert_insight_if_new(%{ organization_id: organization.id, device_id: device.id, type: "device_poll_gap", source: "snmp", urgency: "warning", channel: "proactive", title: "SNMP insight" }) {:ok, _} = Insights.insert_insight_if_new(%{ organization_id: organization.id, device_id: device.id, type: "reconciliation_finding", source: "gaiia", urgency: "info", channel: "proactive", title: "Gaiia insight", dedup_key: "test_dedup", metadata: %{"dedup_key" => "test_dedup"} }) {:ok, view, _html} = live(conn, ~p"/dashboard?insight_source=snmp") html = render(view) assert html =~ "SNMP insight" refute html =~ "Gaiia insight" end end describe "site health grid" do test "shows site cards with device counts", %{ conn: conn, organization: organization, site: site } do {:ok, device} = create_device(organization, site) Towerops.Devices.update_device_status(device, :up) {:ok, _view, html} = live(conn, ~p"/dashboard") assert html =~ "Test Site" assert html =~ ~p"/sites/#{site.id}" end test "shows subscriber data on site cards when Gaiia mapped", %{ conn: conn, organization: organization, site: site } do {:ok, device} = create_device(organization, site) Towerops.Devices.update_device_status(device, :up) {:ok, _} = Towerops.Gaiia.upsert_network_site(organization.id, %{ gaiia_id: "ns-site", name: "Tower", account_count: 42, total_mrr: Decimal.new("2100.00"), site_id: site.id }) {:ok, _view, html} = live(conn, ~p"/dashboard") assert html =~ "42" end end describe "real-time updates" do @tag :skip test "PubSub new alert event updates dashboard", %{ conn: conn, organization: organization, site: site } do {:ok, device} = create_device(organization, site) {:ok, view, _html} = live(conn, ~p"/dashboard") # Create alert and broadcast {:ok, _alert} = Towerops.Alerts.create_alert(%{ device_id: device.id, alert_type: "device_down", triggered_at: DateTime.utc_now(), message: "Device went down" }) Phoenix.PubSub.broadcast( Towerops.PubSub, "alerts:org:#{organization.id}:new", {:new_alert, device.id, :device_down} ) html = render(view) assert html =~ "Device went down" end @tag :skip test "PubSub alert resolved event updates dashboard", %{ conn: conn, organization: organization, site: site } do {:ok, device} = create_device(organization, site) {:ok, alert} = Towerops.Alerts.create_alert(%{ device_id: device.id, alert_type: "device_down", triggered_at: DateTime.utc_now(), message: "Device went down" }) {:ok, view, _html} = live(conn, ~p"/dashboard") # Resolve the alert Towerops.Alerts.resolve_alert(alert) Phoenix.PubSub.broadcast( Towerops.PubSub, "alerts:org:#{organization.id}:resolved", {:alert_resolved, device.id, :device_down} ) html = render(view) refute html =~ "Device went down" end test "handles alert_changed broadcasts without crashing", %{ conn: conn, organization: organization } do {:ok, view, _html} = live(conn, ~p"/dashboard") # Broadcast alert_changed message (sent by user_auth hooks) Phoenix.PubSub.broadcast( Towerops.PubSub, "organization:#{organization.id}:alerts", {:alert_changed, organization.id} ) # Should not crash and should remain connected # Verify we can still render the page html = render(view) assert html =~ organization.name end end defp create_device(organization, site, name \\ "Test Device", ip \\ "192.168.1.1") do Towerops.Devices.create_device(%{ name: name, ip_address: ip, site_id: site.id, organization_id: organization.id }) end end