diff --git a/lib/towerops_web/live/device_live/form.ex b/lib/towerops_web/live/device_live/form.ex index 96faf0c6..832767f4 100644 --- a/lib/towerops_web/live/device_live/form.ex +++ b/lib/towerops_web/live/device_live/form.ex @@ -180,7 +180,8 @@ defmodule ToweropsWeb.DeviceLive.Form do defp mikrotik_device?(device) do # Check if device is MikroTik based on SNMP discovery data - device.snmp_device && + # Must return boolean (not nil) to work with 'and' operator in templates + not is_nil(device.snmp_device) and (String.contains?(device.snmp_device.manufacturer || "", "MikroTik") || String.contains?(device.snmp_device.sys_descr || "", "RouterOS")) end diff --git a/test/towerops_web/live/device_live/form_test.exs b/test/towerops_web/live/device_live/form_test.exs index 485fac5f..1432ac73 100644 --- a/test/towerops_web/live/device_live/form_test.exs +++ b/test/towerops_web/live/device_live/form_test.exs @@ -1,6 +1,107 @@ defmodule ToweropsWeb.DeviceLive.FormTest do 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: "Test Org"}, user.id) + + {:ok, site} = + Towerops.Sites.create_site(%{ + name: "Test Site", + organization_id: organization.id + }) + + %{organization: organization, site: site} + end + + describe "edit page rendering" do + test "renders edit page when device has no SNMP discovery data (bug fix for nil boolean)", %{ + conn: conn, + site: site + } do + # Regression test for production bug where visiting edit page for a device + # with no SNMP discovery data would crash with "expected boolean, got nil" + # because mikrotik_device?/1 could return nil instead of false. + + # Create a device without any snmp_device association (before SNMP discovery) + {:ok, device} = + Towerops.Devices.create_device(%{ + name: "Test Device", + ip_address: "192.168.1.1", + site_id: site.id, + snmp_enabled: true, + snmp_version: "2c", + snmp_port: 161, + monitoring_enabled: true, + check_interval_seconds: 300 + }) + + # Ensure device has no snmp_device (this is the bug condition) + device_with_snmp = Towerops.Repo.preload(device, :snmp_device) + assert is_nil(device_with_snmp.snmp_device) + + # Visit edit page - should not crash with "expected boolean, got nil" error + assert {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit") + + # Page should render successfully + assert html =~ "Edit Device" + assert html =~ device.name + end + + test "renders MikroTik section for superuser when device is MikroTik", %{ + conn: conn, + site: site, + user: user + } do + alias Towerops.Snmp.Device, as: SnmpDevice + # Make user a superuser using Ecto.Changeset (standard test pattern) + _user = + user + |> Ecto.Changeset.change(%{is_superuser: true}) + |> Towerops.Repo.update!() + + # Create a device with MikroTik SNMP discovery data + {:ok, device} = + Towerops.Devices.create_device(%{ + name: "MikroTik Router", + ip_address: "192.168.1.254", + site_id: site.id, + snmp_enabled: true, + snmp_version: "2c", + snmp_port: 161, + monitoring_enabled: true, + check_interval_seconds: 300 + }) + + # Create SNMP device with MikroTik manufacturer (using Towerops.Snmp.Device schema) + _snmp_device = + %SnmpDevice{} + |> SnmpDevice.changeset(%{ + device_id: device.id, + manufacturer: "MikroTik", + sys_descr: "RouterOS 7.8", + sys_object_id: "1.3.6.1.4.1.14988.1", + sys_uptime: 0, + sys_name: "test-router" + }) + |> Towerops.Repo.insert!() + + # Visit edit page + {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit") + + # Page should render successfully + assert html =~ "Edit Device" + assert html =~ device.name + + # MikroTik section SHOULD be visible (superuser + MikroTik device) + assert html =~ "MikroTik API Configuration" + end + end + describe "non_routable_ip?/1" do # Access private function via Module.get_attribute or test via public interface # Since these are private functions, we test through the validation behavior