defmodule ToweropsWeb.ConfigTimelineLiveTest do use ToweropsWeb.ConnCase, async: true import Phoenix.LiveViewTest import Towerops.DevicesFixtures alias Towerops.ConfigChanges.ConfigChangeEvent alias Towerops.Devices.MikrotikBackups setup :register_and_log_in_user setup %{user: user} do {:ok, organization} = Towerops.Organizations.create_organization(%{name: "CT Org"}, user.id) %{organization: organization} end describe "mount + handle_params" do test "redirects to /devices when device is not in user's org", %{conn: conn, organization: org} do conn = put_session(conn, :current_organization_id, org.id) assert {:error, {:redirect, %{to: to}}} = live(conn, ~p"/devices/#{Ecto.UUID.generate()}/config-timeline") assert to == ~p"/devices" end test "renders the timeline for an accessible device", %{conn: conn, organization: org} do conn = put_session(conn, :current_organization_id, org.id) device = device_fixture(%{organization_id: org.id}) assert {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/config-timeline") assert html =~ "Config Timeline" assert html =~ device.name end end describe "events" do setup %{conn: conn, organization: org} do conn = put_session(conn, :current_organization_id, org.id) device = device_fixture(%{organization_id: org.id}) %{conn: conn, device: device} end test "select_range patches the URL with new range", %{conn: conn, device: device} do {:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/config-timeline") _ = render_hook(view, "select_range", %{"range" => "24h"}) assert Process.alive?(view.pid) end test "close_event clears the selected event", %{conn: conn, device: device} do {:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/config-timeline") _ = render_hook(view, "close_event", %{}) assert Process.alive?(view.pid) end test "renders timeline rows when device has change events", %{ conn: conn, device: device, organization: org } do # Insert two change events in the device's recent history. # Need real backup ids since the template builds /backups/compare URLs. {:ok, b1} = MikrotikBackups.create_backup(device.id, "/ip dns set\n") {:ok, b2} = MikrotikBackups.create_backup(device.id, "/ip firewall add\n") now = DateTime.truncate(DateTime.utc_now(), :second) earlier = DateTime.add(now, -3 * 3600, :second) e1 = insert_event!(device.id, org.id, earlier, ["firewall"], 12, b1.id, b2.id) _e2 = insert_event!(device.id, org.id, now, ["dns", "firewall"], 4, b1.id, b2.id) {:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/config-timeline") html = render(view) # Section badges from the template's :for over event.sections_changed assert html =~ "firewall" assert html =~ "dns" # change_size badge text assert html =~ "12 lines" assert html =~ "4 lines" # select_event renders the modal panel (covers selected_event branches) _ = render_hook(view, "select_event", %{"id" => e1.id}) html = render(view) assert html =~ "Change Details" assert html =~ "Sections Changed" assert html =~ "Lines Changed" end test "selected event without diff_summary still renders the modal", %{ conn: conn, device: device, organization: org } do {:ok, b1} = MikrotikBackups.create_backup(device.id, "/ip dns set 1\n") {:ok, b2} = MikrotikBackups.create_backup(device.id, "/ip dns set 2\n") e = %ConfigChangeEvent{} |> ConfigChangeEvent.changeset(%{ device_id: device.id, organization_id: org.id, changed_at: DateTime.truncate(DateTime.utc_now(), :second), # Force empty sections to exercise the "Unknown" branch sections_changed: [], change_size: 0, diff_summary: nil, backup_before_id: b1.id, backup_after_id: b2.id }) |> Towerops.Repo.insert!() {:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/config-timeline") _ = render_hook(view, "select_event", %{"id" => e.id}) html = render(view) assert html =~ "Change Details" # Empty sections branch renders "Unknown" assert html =~ "Unknown" end end defp insert_event!(device_id, org_id, changed_at, sections, change_size, before_id, after_id) do %ConfigChangeEvent{} |> ConfigChangeEvent.changeset(%{ device_id: device_id, organization_id: org_id, changed_at: changed_at, diff_summary: "@@ -1,3 +1,4 @@\n+/ip firewall added\n", sections_changed: sections, change_size: change_size, backup_before_id: before_id, backup_after_id: after_id }) |> Towerops.Repo.insert!() end end