defmodule ToweropsWeb.BeaconMonitorLiveTest do use ToweropsWeb.ConnCase import Phoenix.LiveViewTest import Towerops.BeaconsFixtures alias Towerops.Beacons setup :register_and_log_in_user setup %{user: user} do {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id) %{organization: organization} end describe "Index" do test "lists user's beacon monitors", %{conn: conn, organization: org, user: user} do _my_beacon = beacon_monitor_fixture(org.id, user.id, %{name: "My HTTP Monitor"}) {:ok, _view, html} = live(conn, ~p"/beacons") assert html =~ "My HTTP Monitor" end test "shows empty state when no beacons", %{conn: conn} do {:ok, _view, html} = live(conn, ~p"/beacons") assert html =~ "No beacon monitors" end test "does not show other user's beacons", %{conn: conn, organization: org} do other_user = Towerops.AccountsFixtures.user_fixture(enable_totp: true) _theirs = beacon_monitor_fixture(org.id, other_user.id, %{name: "Not Mine"}) {:ok, _view, html} = live(conn, ~p"/beacons") refute html =~ "Not Mine" end test "requires authentication" do conn = build_conn() {:error, redirect} = live(conn, ~p"/beacons") assert {:redirect, %{to: path}} = redirect assert path == ~p"/users/log-in" end end describe "Create beacon" do test "creates a new beacon monitor via modal", %{conn: conn, organization: org, user: user} do {:ok, view, _html} = live(conn, ~p"/beacons/new") view |> form("#beacon-form-new", beacon_monitor: %{ name: "New HTTP Check", check_type: "http", target_url: "https://example.com", interval_seconds: "30", timeout_ms: "3000" } ) |> render_submit() assert_redirect(view, ~p"/beacons") # Verify the beacon was created in the database beacons = Beacons.list_user_beacons(org.id, user.id) assert Enum.any?(beacons, &(&1.name == "New HTTP Check" and &1.target_url == "https://example.com")) end test "validates required fields on submit", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/beacons/new") html = view |> form("#beacon-form-new", beacon_monitor: %{ name: "", check_type: "http", target_url: "", interval_seconds: "", timeout_ms: "" } ) |> render_submit() assert html =~ "can't be blank" end test "open_edit navigates to edit route", %{conn: conn, organization: org, user: user} do beacon = beacon_monitor_fixture(org.id, user.id, %{name: "Edit Me"}) {:ok, view, _html} = live(conn, ~p"/beacons") view |> element("#edit-beacon-#{beacon.id}") |> render_click() assert_redirect(view, ~p"/beacons/#{beacon.id}/edit") end end describe "Edit beacon" do test "loads edit modal with existing beacon data", %{conn: conn, organization: org, user: user} do beacon = beacon_monitor_fixture(org.id, user.id, %{name: "Original Name"}) {:ok, _view, html} = live(conn, ~p"/beacons/#{beacon.id}/edit") assert html =~ "Edit Beacon Monitor" assert html =~ "Original Name" end test "updates beacon name and redirects", %{conn: conn, organization: org, user: user} do beacon = beacon_monitor_fixture(org.id, user.id, %{name: "Old Name"}) {:ok, view, _html} = live(conn, ~p"/beacons/#{beacon.id}/edit") view |> form("#beacon-form-edit", beacon_monitor: %{ name: "Updated Name", check_type: "http", target_url: beacon.target_url, interval_seconds: to_string(beacon.interval_seconds), timeout_ms: to_string(beacon.timeout_ms) } ) |> render_submit() assert_redirect(view, ~p"/beacons") updated = Beacons.get_beacon(beacon.id) assert updated.name == "Updated Name" end end describe "Toggle beacon" do test "disables an enabled beacon", %{conn: conn, organization: org, user: user} do beacon = beacon_monitor_fixture(org.id, user.id, %{enabled: true}) {:ok, view, _html} = live(conn, ~p"/beacons") view |> element("#toggle-beacon-#{beacon.id}") |> render_click() refute Beacons.get_beacon(beacon.id).enabled end test "enables a disabled beacon", %{conn: conn, organization: org, user: user} do beacon = beacon_monitor_fixture(org.id, user.id, %{enabled: false}) {:ok, view, _html} = live(conn, ~p"/beacons") view |> element("#toggle-beacon-#{beacon.id}") |> render_click() assert Beacons.get_beacon(beacon.id).enabled end end describe "Delete beacon" do test "deletes a beacon", %{conn: conn, organization: org, user: user} do beacon = beacon_monitor_fixture(org.id, user.id, %{name: "To Delete"}) {:ok, view, _html} = live(conn, ~p"/beacons") view |> element("#delete-beacon-#{beacon.id}") |> render_click() html = render(view) assert html =~ "Beacon deleted successfully" refute Beacons.get_beacon(beacon.id) end end describe "Timer cleanup" do test "cleans up timer on LiveView terminate", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/beacons") assert render(view) =~ "Beacon Monitors" stop_live(view) end test "handles nil timer ref in terminate gracefully", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/beacons") stop_live(view) end defp stop_live(view) do Process.exit(view.pid, :kill) ref = Process.monitor(view.pid) assert_receive {:DOWN, ^ref, :process, _, _}, 1000 end end describe "Admin index" do setup do user = Towerops.AccountsFixtures.user_fixture(enable_totp: true) user = user |> Ecto.Changeset.change(%{is_superuser: true}) |> Towerops.Repo.update!() {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id) token = Towerops.Accounts.generate_user_session_token(user) conn = build_conn() |> Phoenix.ConnTest.init_test_session(%{}) |> Plug.Conn.put_session(:user_token, token) %{conn: conn, user: user, organization: organization} end test "lists all beacon monitors across orgs", %{conn: conn, user: user, organization: org} do other_user = Towerops.AccountsFixtures.user_fixture(enable_totp: false) {:ok, org2} = Towerops.Organizations.create_organization(%{name: "Org B"}, other_user.id, bypass_limits: true) _b1 = beacon_monitor_fixture(org.id, user.id, %{name: "Beacon A"}) _b2 = beacon_monitor_fixture(org2.id, other_user.id, %{name: "Beacon B"}) {:ok, _view, html} = live(conn, ~p"/admin/beacons") assert html =~ "Beacon A" assert html =~ "Beacon B" end test "redirects non-superuser", %{user: user} do regular_user = user |> Ecto.Changeset.change(%{is_superuser: false}) |> Towerops.Repo.update!() token = Towerops.Accounts.generate_user_session_token(regular_user) conn = build_conn() |> Phoenix.ConnTest.init_test_session(%{}) |> Plug.Conn.put_session(:user_token, token) assert {:error, {:redirect, %{to: "/orgs"}}} = live(conn, ~p"/admin/beacons") end test "requires authentication for admin" do conn = build_conn() assert {:error, {:redirect, %{to: "/users/log-in"}}} = live(conn, ~p"/admin/beacons") end test "admin can toggle beacon", %{conn: conn, user: user, organization: org} do beacon = beacon_monitor_fixture(org.id, user.id, %{enabled: true, name: "Admin Toggle"}) {:ok, view, _html} = live(conn, ~p"/admin/beacons") view |> element("#admin-toggle-beacon-#{beacon.id}") |> render_click() refute Beacons.get_beacon(beacon.id).enabled end test "admin can delete beacon", %{conn: conn, user: user, organization: org} do beacon = beacon_monitor_fixture(org.id, user.id, %{name: "Admin Delete"}) {:ok, view, _html} = live(conn, ~p"/admin/beacons") view |> element("#admin-delete-beacon-#{beacon.id}") |> render_click() refute Beacons.get_beacon(beacon.id) end test "shows empty state in admin when no beacons", %{conn: conn} do {:ok, _view, html} = live(conn, ~p"/admin/beacons") assert html =~ "No beacon monitors" end end end