defmodule ToweropsWeb.Admin.MonitoringLiveTest do use ToweropsWeb.ConnCase import Phoenix.LiveViewTest import Towerops.DevicesFixtures import Towerops.JobsFixtures describe "MonitoringLive" do setup [:register_and_log_in_superuser] test "renders monitoring dashboard", %{conn: conn} do {:ok, _view, html} = live(conn, ~p"/admin/monitoring") assert html =~ "Job Monitoring Dashboard" assert html =~ "Active Operations" assert html =~ "Problems" assert html =~ "Health Metrics" end test "subscribes to job lifecycle events", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/admin/monitoring") # Verify PubSub subscription by checking it's alive assert Process.alive?(view.pid) end test "displays active operations section", %{conn: conn} do {:ok, _view, html} = live(conn, ~p"/admin/monitoring") assert html =~ "Active Operations" assert html =~ "No jobs currently executing" end end describe "real-time updates" do setup [:register_and_log_in_superuser] test "displays executing jobs from database", %{conn: conn} do device = device_fixture() _job = oban_job_fixture(%{ worker: "Towerops.Workers.DevicePollerWorker", state: "executing", args: %{"device_id" => device.id}, attempted_at: DateTime.utc_now() }) {:ok, _view, html} = live(conn, ~p"/admin/monitoring") # Should show the device name in executing jobs assert html =~ device.name assert html =~ "Active Operations (1)" end end describe "metrics display" do setup [:register_and_log_in_superuser] test "shows current activity metrics", %{conn: conn} do device = device_fixture() oban_job_fixture(%{ worker: "Towerops.Workers.DevicePollerWorker", state: "executing", args: %{"device_id" => device.id} }) {:ok, _view, html} = live(conn, ~p"/admin/monitoring") assert html =~ "Executing" assert html =~ "1" end end describe "handle_info events" do setup [:register_and_log_in_superuser] test "handles job started event", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/admin/monitoring") send(view.pid, %{event: :started, job_id: 1, worker: "TestWorker"}) assert render(view) end test "handles job completed event", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/admin/monitoring") send(view.pid, %{event: :completed, job_id: 1, worker: "TestWorker"}) assert render(view) end test "handles job failed event", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/admin/monitoring") send(view.pid, %{event: :failed, job_id: 1, worker: "TestWorker"}) assert render(view) end test "handles unknown events gracefully", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/admin/monitoring") send(view.pid, :some_unknown_event) assert render(view) end end describe "renders helpers via template" do setup [:register_and_log_in_superuser] test "renders worker_name + duration_in_words for a stuck poller", %{conn: conn} do device = device_fixture() # Poller stuck > 2 minutes triggers the stuck-jobs render path five_minutes_ago = DateTime.add(DateTime.utc_now(), -5 * 60, :second) oban_job_fixture(%{ worker: "Towerops.Workers.DevicePollerWorker", state: "executing", attempted_at: five_minutes_ago, args: %{"device_id" => device.id} }) {:ok, _view, html} = live(conn, ~p"/admin/monitoring") assert html =~ "Stuck Jobs" # worker_name/1 maps the FQ worker to "Device Poll" assert html =~ "Device Poll" # duration_in_words/1 emits a "5m" or "Xm" string assert html =~ ~r/Running for \d+m/ end test "renders event_outcome / event_border_color for completed jobs in recent activity", %{ conn: conn } do device = device_fixture() oban_job_fixture(%{ worker: "Towerops.Workers.DevicePollerWorker", state: "completed", attempted_at: DateTime.add(DateTime.utc_now(), -120, :second), completed_at: DateTime.utc_now(), args: %{"device_id" => device.id} }) oban_job_fixture(%{ worker: "Towerops.Workers.DiscoveryWorker", state: "cancelled", attempted_at: DateTime.add(DateTime.utc_now(), -120, :second), completed_at: DateTime.utc_now(), args: %{"device_id" => device.id} }) oban_job_fixture(%{ worker: "Towerops.Workers.DevicePollerWorker", state: "discarded", attempted_at: DateTime.add(DateTime.utc_now(), -120, :second), completed_at: DateTime.utc_now(), args: %{"device_id" => device.id} }) {:ok, _view, html} = live(conn, ~p"/admin/monitoring") # event_outcome/1 strings assert html =~ "Completed" assert html =~ "Failed" assert html =~ "Discarded" # event_border_color/1 classes assert html =~ "border-green-500" assert html =~ "border-red-500" assert html =~ "border-gray-400" # SNMP Discovery from worker_name fallback assert html =~ "SNMP Discovery" end test "duration_in_words renders 'h m' for jobs stuck for hours", %{conn: conn} do device = device_fixture() two_hours_ago = DateTime.add(DateTime.utc_now(), -3 * 3600, :second) oban_job_fixture(%{ worker: "Towerops.Workers.DiscoveryWorker", state: "executing", attempted_at: two_hours_ago, args: %{"device_id" => device.id} }) {:ok, _view, html} = live(conn, ~p"/admin/monitoring") assert html =~ ~r/Running for \dh \d+m/ end end defp register_and_log_in_superuser(%{conn: conn}) do user = Towerops.AccountsFixtures.user_fixture(enable_totp: true) user = Towerops.Repo.update!(Ecto.Changeset.change(user, is_superuser: true)) %{conn: log_in_user(conn, user), user: user} end end