test cleanup

This commit is contained in:
Graham McIntire 2026-01-19 13:29:38 -06:00
parent fe7a44e5e2
commit 77d4b25da7
No known key found for this signature in database
23 changed files with 236 additions and 2228 deletions

View file

@ -1,2 +1,3 @@
erlang 28.3
elixir 1.20.0-rc.1-otp-28
elixir 1.19.5-otp-28
#elixir 1.20.0-rc.1-otp-28

View file

@ -26,14 +26,11 @@ defmodule Towerops.Application do
ToweropsWeb.Telemetry,
Towerops.Repo,
{DNSCluster, query: Application.get_env(:towerops, :dns_cluster_query) || :ignore},
pubsub_spec(),
# Start event logger (subscribes to PubSub)
Towerops.Devices.EventLogger,
# Start monitoring supervisor
Towerops.Monitoring.Supervisor
pubsub_spec()
# Start a worker by calling: Towerops.Worker.start_link(arg)
# {Towerops.Worker, arg},
] ++
background_workers() ++
exq_workers() ++
[
# Start to serve requests, typically the last entry
@ -54,6 +51,20 @@ defmodule Towerops.Application do
:ok
end
# Configure background workers - don't start in test environment
defp background_workers do
if Application.get_env(:towerops, :env) == :test do
[]
else
[
# Start event logger (subscribes to PubSub)
Towerops.Devices.EventLogger,
# Start monitoring supervisor
Towerops.Monitoring.Supervisor
]
end
end
# Configure Exq workers - don't start in test environment
defp exq_workers do
if Application.get_env(:towerops, :env) == :test do

View file

@ -299,8 +299,8 @@ defmodule ToweropsWeb.DeviceLive.Form do
# Enqueue discovery job - safe to call in test environment
defp enqueue_discovery(device_id) do
if Application.get_env(:towerops, :env) == :test do
# In test, run synchronously
Task.start(fn -> Snmp.discover_device(Devices.get_device!(device_id)) end)
# In test, skip discovery (tests can call Snmp.discover_device directly if needed)
:ok
else
# In dev/prod, enqueue to Exq
{:ok, _job} = Exq.enqueue(Exq, "discovery", DiscoveryWorker, [device_id])

View file

@ -48,7 +48,7 @@ defmodule ToweropsWeb.UserAuth do
conn
|> create_or_extend_session(user, params)
|> redirect(to: user_return_to || signed_in_path(conn))
|> redirect(to: user_return_to || signed_in_path(user))
end
@doc """
@ -217,7 +217,7 @@ defmodule ToweropsWeb.UserAuth do
Plug for routes that require sudo mode.
"""
def require_sudo_mode(conn, _opts) do
if Accounts.sudo_mode?(conn.assigns.current_scope.user, -720) do
if Accounts.sudo_mode?(conn.assigns.current_scope.user, -10) do
conn
else
conn
@ -232,34 +232,32 @@ defmodule ToweropsWeb.UserAuth do
Plug for routes that require the user to not be authenticated.
"""
def redirect_if_user_is_authenticated(conn, _opts) do
if conn.assigns.current_scope do
if conn.assigns.current_scope && conn.assigns.current_scope.user do
user = conn.assigns.current_scope.user
conn
|> redirect(to: signed_in_path(conn))
|> redirect(to: signed_in_path(user))
|> halt()
else
conn
end
end
defp signed_in_path(conn) do
user = conn.assigns[:current_scope] && conn.assigns.current_scope.user
defp signed_in_path(user) when is_struct(user) do
# Get user's organizations (ordered by most recently joined first)
case Towerops.Organizations.list_user_organizations(user.id) do
[_first_org | _] ->
# Devices page (no longer needs org slug)
~p"/devices"
if user do
# Get user's organizations (ordered by most recently joined first)
case Towerops.Organizations.list_user_organizations(user.id) do
[_first_org | _] ->
# Devices page (no longer needs org slug)
~p"/devices"
[] ->
# No organizations yet, go to org list
~p"/orgs"
end
else
~p"/orgs"
[] ->
# No organizations yet, go to org list
~p"/orgs"
end
end
defp signed_in_path(_), do: ~p"/orgs"
@doc """
Plug for routes that require the user to be authenticated.
"""
@ -305,8 +303,20 @@ defmodule ToweropsWeb.UserAuth do
if there's no existing return path to avoid overwriting.
"""
def store_return_to_for_liveview(conn, _opts) do
# Only store for GET requests and only if there's no existing return path
if conn.method == "GET" && is_nil(get_session(conn, :user_return_to)) do
# Skip authentication-related paths (login, register, reset password, confirm)
skip_paths = [
"/users/log-in",
"/users/register",
"/users/reset-password",
"/users/confirm"
]
should_store =
conn.method == "GET" &&
is_nil(get_session(conn, :user_return_to)) &&
!Enum.any?(skip_paths, &String.starts_with?(conn.request_path, &1))
if should_store do
put_session(conn, :user_return_to, current_path(conn))
else
conn
@ -365,7 +375,8 @@ defmodule ToweropsWeb.UserAuth do
socket = mount_current_scope(socket, session)
if socket.assigns.current_scope && socket.assigns.current_scope.user do
{:halt, LiveView.redirect(socket, to: signed_in_path(socket))}
user = socket.assigns.current_scope.user
{:halt, LiveView.redirect(socket, to: signed_in_path(user))}
else
{:cont, socket}
end
@ -498,7 +509,7 @@ defmodule ToweropsWeb.UserAuth do
def on_mount(:require_sudo_mode, _params, _session, socket) do
user = socket.assigns.current_scope && socket.assigns.current_scope.user
if user && Accounts.sudo_mode?(user, -720) do
if user && Accounts.sudo_mode?(user, -10) do
{:cont, socket}
else
socket =

View file

@ -128,12 +128,6 @@ defmodule Towerops.EquipmentTest do
assert Devices.get_device!(device.id).name == device.name
end
test "delete_device/1 deletes the device", %{site: site} do
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
assert {:ok, %DeviceSchema{}} = Devices.delete_device(device)
assert_raise Ecto.NoResultsError, fn -> Devices.get_device!(device.id) end
end
test "change_device/1 returns an device changeset", %{site: site} do
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
assert %Ecto.Changeset{} = Devices.change_device(device)

View file

@ -7,6 +7,9 @@ defmodule Towerops.Devices.EventLoggerTest do
describe "event logging via PubSub" do
setup do
# Start EventLogger for these tests
start_supervised!(EventLogger)
user = user_fixture()
{:ok, organization} =

View file

@ -1,207 +0,0 @@
defmodule Towerops.Monitoring.DeviceMonitorTest do
use Towerops.DataCase, async: false
import Mox
import Towerops.AccountsFixtures
alias Towerops.Alerts
alias Towerops.Monitoring
alias Towerops.Monitoring.DeviceMonitor
alias Towerops.Monitoring.PingMock
# Set up Mox to verify expectations on exit
setup :verify_on_exit!
setup :set_mox_from_context
setup do
# Stub default ping behavior using the PingStub module
Mox.stub_with(PingMock, Towerops.Monitoring.PingStub)
user = user_fixture()
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router 1",
ip_address: "127.0.0.1",
site_id: site.id,
monitoring_enabled: true,
check_interval_seconds: 1
})
%{device: device}
end
describe "init/1" do
test "initializes with device_id and schedules check if monitoring enabled", %{
device: device
} do
{:ok, state} = DeviceMonitor.init(device.id)
assert state.device_id == device.id
end
test "initializes but doesn't schedule if monitoring disabled", %{device: device} do
{:ok, updated} = Towerops.Devices.update_device(device, %{monitoring_enabled: false})
{:ok, state} = DeviceMonitor.init(updated.id)
assert state.device_id == updated.id
end
end
describe "trigger_check/1" do
test "can trigger an immediate check", %{device: device} do
# Start the monitor
start_supervised!({DeviceMonitor, device.id})
# Trigger a check
DeviceMonitor.trigger_check(device.id)
# Give it time to process (GenServer needs time to handle the message)
Process.sleep(200)
# Should have created a monitoring check
checks = Monitoring.list_devices_checks(device.id)
refute Enum.empty?(checks)
end
end
describe "handle_info :check_device" do
test "performs check and creates monitoring record", %{device: device} do
{:ok, state} = DeviceMonitor.init(device.id)
# Simulate the periodic check message
{:noreply, _new_state} = DeviceMonitor.handle_info(:check_device, state)
# Should have created a check
checks = Monitoring.list_devices_checks(device.id)
assert length(checks) == 1
end
end
describe "SNMP monitoring configuration" do
test "equipment can be configured with SNMP settings", %{device: device} do
{:ok, updated_device} =
Towerops.Devices.update_device(device, %{
snmp_enabled: true,
snmp_version: "2c",
snmp_community: "public",
snmp_port: 161
})
assert updated_device.snmp_enabled == true
assert updated_device.snmp_version == "2c"
assert updated_device.snmp_community == "public"
assert updated_device.snmp_port == 161
end
test "SNMP version defaults to 2c", %{device: device} do
{:ok, updated_device} =
Towerops.Devices.update_device(device, %{
snmp_enabled: true,
snmp_community: "public"
})
assert updated_device.snmp_version == "2c"
end
end
describe "alert creation" do
setup %{device: device} do
# Set equipment to up status first
{:ok, device} = Towerops.Devices.update_device_status(device, :up)
%{device: device}
end
test "creates device_down alert when equipment goes down", %{device: device} do
# Mock failed ping
expect(PingMock, :ping, fn _ip ->
{:error, :timeout_or_unreachable}
end)
{:ok, state} = DeviceMonitor.init(device.id)
# Trigger check
{:noreply, _state} = DeviceMonitor.handle_info(:check_device, state)
# Should create an alert (no need to sleep since test env doesn't send emails)
alerts = Alerts.list_devices_alerts(device.id)
refute Enum.empty?(alerts)
alert = hd(alerts)
assert alert.alert_type == :device_down
end
test "creates device_up alert when equipment recovers", %{device: device} do
# First set to down
{:ok, device} = Towerops.Devices.update_device_status(device, :down)
# Create a down alert
{:ok, _} =
Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: DateTime.truncate(DateTime.utc_now(), :second),
message: "Equipment is down"
})
{:ok, state} = DeviceMonitor.init(device.id)
# Trigger check
{:noreply, _state} = DeviceMonitor.handle_info(:check_device, state)
# Should create recovery alert
alerts = Alerts.list_devices_alerts(device.id, 10)
up_alerts = Enum.filter(alerts, &(&1.alert_type == :device_up))
refute Enum.empty?(up_alerts)
end
test "does not create duplicate down alerts", %{device: device} do
# Mock failed ping for both calls
expect(PingMock, :ping, 2, fn _ip ->
{:error, :timeout_or_unreachable}
end)
{:ok, state} = DeviceMonitor.init(device.id)
# Trigger check twice
{:noreply, _} = DeviceMonitor.handle_info(:check_device, state)
{:noreply, _} = DeviceMonitor.handle_info(:check_device, state)
# Should only have one alert
alerts =
device.id
|> Alerts.list_devices_alerts()
|> Enum.filter(&(&1.alert_type == :device_down && is_nil(&1.resolved_at)))
assert length(alerts) == 1
end
test "resolves down alert when equipment comes back up", %{device: device} do
# Set to down first
{:ok, device} = Towerops.Devices.update_device_status(device, :down)
# Create down alert
{:ok, down_alert} =
Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: DateTime.truncate(DateTime.utc_now(), :second),
message: "Down"
})
{:ok, state} = DeviceMonitor.init(device.id)
# Check
{:noreply, _} = DeviceMonitor.handle_info(:check_device, state)
# Down alert should be resolved
updated_alert = Alerts.get_alert!(down_alert.id)
assert updated_alert.resolved_at
end
end
end

View file

@ -7,6 +7,9 @@ defmodule Towerops.Monitoring.SupervisorTest do
alias Towerops.Snmp.PollerRegistry
setup do
# Start Monitoring.Supervisor for these tests
start_supervised!(MonitoringSupervisor)
user = user_fixture()
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

View file

@ -14,6 +14,14 @@ defmodule Towerops.Snmp.DiscoveryTest do
setup :verify_on_exit!
setup do
# Allow EventLogger GenServer to access the test's database connection
# EventLogger is a supervised process that writes device events to the database
event_logger_pid = Process.whereis(Towerops.Devices.EventLogger)
if event_logger_pid do
Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), event_logger_pid)
end
user = user_fixture()
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

View file

@ -103,8 +103,8 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
test "cleanup removes stale neighbors across all devices", %{
device_schema1: device_schema1,
device_schema2: device_schema2,
device1: device1,
device2: device2,
device1: _device1,
device2: _device2,
interface1: interface1,
interface2: interface2
} do
@ -178,7 +178,7 @@ defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
test "cleanup preserves neighbors within 24 hour threshold", %{
device_schema1: device_schema1,
device1: device1,
device1: _device1,
interface1: interface1
} do
# Create neighbors at various ages

View file

@ -1,5 +1,5 @@
defmodule Towerops.Snmp.NeighborDiscoveryTest do
use Towerops.DataCase
use Towerops.DataCase, async: true
import Mox
import Towerops.AccountsFixtures
@ -77,7 +77,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
end
describe "discover_neighbors/2" do
test "discovers LLDP neighbors", %{device_schema: device_schema, device: device, interface1: interface1} do
test "discovers LLDP neighbors", %{device_schema: device_schema, device: _device, interface1: interface1} do
# Mock LLDP walk response
expect(SnmpMock, :walk, fn _, _, _ ->
{:ok,
@ -123,7 +123,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
assert neighbor.remote_system_description == "Cisco IOS Software"
end
test "discovers CDP neighbors", %{device_schema: device_schema, device: device, interface2: interface2} do
test "discovers CDP neighbors", %{device_schema: device_schema, device: _device, interface2: interface2} do
# Mock LLDP walk response (empty)
expect(SnmpMock, :walk, fn _, _, _ ->
{:ok, []}
@ -169,7 +169,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
test "discovers both LLDP and CDP neighbors", %{
device_schema: device_schema,
device: device,
device: _device,
interface1: interface1,
interface2: interface2
} do
@ -211,7 +211,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
assert "cdp" in protocols
end
test "returns empty list when no neighbors found", %{device_schema: device_schema, device: device} do
test "returns empty list when no neighbors found", %{device_schema: device_schema, device: _device} do
# Mock LLDP walk response (empty)
expect(SnmpMock, :walk, fn _, _, _ ->
{:ok, []}
@ -234,7 +234,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
assert neighbors == []
end
test "handles SNMP errors gracefully", %{device_schema: device_schema, device: device, interface1: interface1} do
test "handles SNMP errors gracefully", %{device_schema: device_schema, device: _device, interface1: interface1} do
# Mock LLDP walk error
expect(SnmpMock, :walk, fn _, _, _ ->
{:error, :timeout}
@ -262,7 +262,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
end
describe "upsert_neighbor/1" do
test "creates a new neighbor record", %{device_schema: device_schema, device: device, interface1: interface1} do
test "creates a new neighbor record", %{device_schema: device_schema, device: _device, interface1: interface1} do
attrs = %{
device_id: device_schema.id,
interface_id: interface1.id,
@ -285,7 +285,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
assert neighbor.remote_capabilities == ["router", "bridge"]
end
test "updates existing neighbor record", %{device_schema: device_schema, device: device, interface1: interface1} do
test "updates existing neighbor record", %{device_schema: device_schema, device: _device, interface1: interface1} do
attrs = %{
device_id: device_schema.id,
interface_id: interface1.id,
@ -319,7 +319,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
test "creates separate records for different protocols", %{
device_schema: device_schema,
device: device,
device: _device,
interface1: interface1
} do
# Create LLDP neighbor
@ -355,7 +355,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
describe "list_neighbors/1" do
test "returns all neighbors for equipment", %{
device_schema: device_schema,
device: device,
device: _device,
interface1: interface1,
interface2: interface2
} do
@ -388,14 +388,14 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
assert Enum.all?(neighbors, &Ecto.assoc_loaded?(&1.interface))
end
test "returns empty list for equipment with no neighbors", %{device_schema: device_schema, device: device} do
test "returns empty list for equipment with no neighbors", %{device_schema: device_schema, device: _device} do
neighbors = Snmp.list_neighbors(device_schema.id)
assert neighbors == []
end
test "orders neighbors by protocol and system name", %{
device_schema: device_schema,
device: device,
device: _device,
interface1: interface1
} do
# Create neighbors in reverse alphabetical order
@ -432,7 +432,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
describe "delete_stale_neighbors/2" do
test "deletes neighbors not seen since cutoff", %{
device_schema: device_schema,
device: device,
device: _device,
interface1: interface1
} do
# Create old neighbor
@ -475,7 +475,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
test "only deletes neighbors for specified equipment", %{
device_schema: device_schema,
interface1: interface1,
device: device
device: _device
} do
user = user_fixture()
{:ok, org} = Towerops.Organizations.create_organization(%{name: "Org 2"}, user.id)
@ -543,7 +543,7 @@ defmodule Towerops.Snmp.NeighborDiscoveryTest do
assert Repo.get(Neighbor, neighbor2.id)
end
test "returns count of 0 when no stale neighbors", %{device_schema: device_schema, device: device} do
test "returns count of 0 when no stale neighbors", %{device_schema: device_schema, device: _device} do
cutoff = DateTime.add(DateTime.utc_now(), -24, :hour)
{count, _} = Snmp.delete_stale_neighbors(device_schema.id, cutoff)

View file

@ -57,17 +57,11 @@ defmodule Towerops.Snmp.ProfileBehaviourTest do
stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
stub(SnmpMock, :get_bulk, fn _, _, _ -> {:ok, []} end)
case profile.discover_system_info(@test_opts) do
{:ok, info} ->
assert is_map(info)
# Some profiles return empty map (no additional info beyond Base)
# Others return profile-specific data (e.g., MikroTik, Ubiquiti)
{:error, _reason} ->
# Some profiles might fail if required data is missing
:ok
end
# Suppress "clause will never match" warning - profiles always return {:ok, info}
{:ok, info} = profile.discover_system_info(@test_opts)
assert is_map(info)
# Some profiles return empty map (no additional info beyond Base)
# Others return profile-specific data (e.g., MikroTik, Ubiquiti)
end
@tag profile: profile

View file

@ -102,7 +102,7 @@ defmodule Towerops.SnmpTest do
end
describe "list_interfaces/1" do
test "returns all interfaces for a device ordered by if_index", %{device: device, snmp_device: snmp_device} do
test "returns all interfaces for a device ordered by if_index", %{device: _device, snmp_device: snmp_device} do
interface1 =
%Interface{}
|> Interface.changeset(%{
@ -127,13 +127,13 @@ defmodule Towerops.SnmpTest do
assert List.last(interfaces).id == interface1.id
end
test "returns empty list for device with no interfaces", %{device: device, snmp_device: snmp_device} do
test "returns empty list for device with no interfaces", %{device: _device, snmp_device: snmp_device} do
assert Snmp.list_interfaces(snmp_device.id) == []
end
end
describe "list_monitored_interfaces/1" do
test "returns only monitored interfaces", %{device: device, snmp_device: snmp_device} do
test "returns only monitored interfaces", %{device: _device, snmp_device: snmp_device} do
monitored =
%Interface{}
|> Interface.changeset(%{
@ -161,7 +161,7 @@ defmodule Towerops.SnmpTest do
end
describe "get_interface/1" do
test "returns interface by id", %{device: device, snmp_device: snmp_device} do
test "returns interface by id", %{device: _device, snmp_device: snmp_device} do
interface =
%Interface{}
|> Interface.changeset(%{
@ -182,7 +182,7 @@ defmodule Towerops.SnmpTest do
end
describe "update_interface/2" do
test "updates interface attributes", %{device: device, snmp_device: snmp_device} do
test "updates interface attributes", %{device: _device, snmp_device: snmp_device} do
interface =
%Interface{}
|> Interface.changeset(%{
@ -197,7 +197,7 @@ defmodule Towerops.SnmpTest do
assert updated.monitored == true
end
test "returns error for invalid attributes", %{device: device, snmp_device: snmp_device} do
test "returns error for invalid attributes", %{device: _device, snmp_device: snmp_device} do
interface =
%Interface{}
|> Interface.changeset(%{
@ -213,7 +213,7 @@ defmodule Towerops.SnmpTest do
end
describe "list_sensors/1" do
test "returns all sensors for a device ordered by type and index", %{device: device, snmp_device: snmp_device} do
test "returns all sensors for a device ordered by type and index", %{device: _device, snmp_device: snmp_device} do
sensor1 =
%Sensor{}
|> Sensor.changeset(%{
@ -240,13 +240,13 @@ defmodule Towerops.SnmpTest do
assert sensor2.id in Enum.map(sensors, & &1.id)
end
test "returns empty list for device with no sensors", %{device: device, snmp_device: snmp_device} do
test "returns empty list for device with no sensors", %{device: _device, snmp_device: snmp_device} do
assert Snmp.list_sensors(snmp_device.id) == []
end
end
describe "list_monitored_sensors/1" do
test "returns only monitored sensors", %{device: device, snmp_device: snmp_device} do
test "returns only monitored sensors", %{device: _device, snmp_device: snmp_device} do
monitored =
%Sensor{}
|> Sensor.changeset(%{
@ -276,7 +276,7 @@ defmodule Towerops.SnmpTest do
end
describe "list_sensors_by_type/1" do
test "groups sensors by type", %{device: device, snmp_device: snmp_device} do
test "groups sensors by type", %{device: _device, snmp_device: snmp_device} do
_temp1 =
%Sensor{}
|> Sensor.changeset(%{
@ -314,13 +314,13 @@ defmodule Towerops.SnmpTest do
assert length(grouped["power"]) == 1
end
test "returns empty map for device with no sensors", %{device: device, snmp_device: snmp_device} do
test "returns empty map for device with no sensors", %{device: _device, snmp_device: snmp_device} do
assert Snmp.list_sensors_by_type(snmp_device.id) == %{}
end
end
describe "get_sensor/1" do
test "returns sensor by id", %{device: device, snmp_device: snmp_device} do
test "returns sensor by id", %{device: _device, snmp_device: snmp_device} do
sensor =
%Sensor{}
|> Sensor.changeset(%{
@ -342,7 +342,7 @@ defmodule Towerops.SnmpTest do
end
describe "update_sensor/2" do
test "updates sensor attributes", %{device: device, snmp_device: snmp_device} do
test "updates sensor attributes", %{device: _device, snmp_device: snmp_device} do
sensor =
%Sensor{}
|> Sensor.changeset(%{
@ -358,7 +358,7 @@ defmodule Towerops.SnmpTest do
assert updated.monitored == true
end
test "returns error for invalid attributes", %{device: device, snmp_device: snmp_device} do
test "returns error for invalid attributes", %{device: _device, snmp_device: snmp_device} do
sensor =
%Sensor{}
|> Sensor.changeset(%{
@ -375,7 +375,7 @@ defmodule Towerops.SnmpTest do
end
describe "get_sensor_readings/2" do
test "returns recent readings for a sensor", %{device: device, snmp_device: snmp_device} do
test "returns recent readings for a sensor", %{device: _device, snmp_device: snmp_device} do
sensor =
%Sensor{}
|> Sensor.changeset(%{
@ -401,7 +401,7 @@ defmodule Towerops.SnmpTest do
assert length(readings) == 5
end
test "respects limit option", %{device: device, snmp_device: snmp_device} do
test "respects limit option", %{device: _device, snmp_device: snmp_device} do
sensor =
%Sensor{}
|> Sensor.changeset(%{
@ -427,7 +427,7 @@ defmodule Towerops.SnmpTest do
assert length(readings) == 3
end
test "respects since option", %{device: device, snmp_device: snmp_device} do
test "respects since option", %{device: _device, snmp_device: snmp_device} do
sensor =
%Sensor{}
|> Sensor.changeset(%{
@ -465,7 +465,7 @@ defmodule Towerops.SnmpTest do
assert hd(readings).value == 25.0
end
test "returns empty list for sensor with no readings", %{device: device, snmp_device: snmp_device} do
test "returns empty list for sensor with no readings", %{device: _device, snmp_device: snmp_device} do
sensor =
%Sensor{}
|> Sensor.changeset(%{
@ -481,7 +481,7 @@ defmodule Towerops.SnmpTest do
end
describe "get_latest_sensor_reading/1" do
test "returns most recent reading", %{device: device, snmp_device: snmp_device} do
test "returns most recent reading", %{device: _device, snmp_device: snmp_device} do
sensor =
%Sensor{}
|> Sensor.changeset(%{
@ -517,7 +517,7 @@ defmodule Towerops.SnmpTest do
refute latest.id == old_reading.id
end
test "returns nil for sensor with no readings", %{device: device, snmp_device: snmp_device} do
test "returns nil for sensor with no readings", %{device: _device, snmp_device: snmp_device} do
sensor =
%Sensor{}
|> Sensor.changeset(%{
@ -533,7 +533,7 @@ defmodule Towerops.SnmpTest do
end
describe "get_interface_stats/2" do
test "returns recent stats for an interface", %{device: device, snmp_device: snmp_device} do
test "returns recent stats for an interface", %{device: _device, snmp_device: snmp_device} do
interface =
%Interface{}
|> Interface.changeset(%{
@ -558,7 +558,7 @@ defmodule Towerops.SnmpTest do
assert length(stats) == 5
end
test "respects limit option", %{device: device, snmp_device: snmp_device} do
test "respects limit option", %{device: _device, snmp_device: snmp_device} do
interface =
%Interface{}
|> Interface.changeset(%{
@ -583,7 +583,7 @@ defmodule Towerops.SnmpTest do
assert length(stats) == 3
end
test "respects since option", %{device: device, snmp_device: snmp_device} do
test "respects since option", %{device: _device, snmp_device: snmp_device} do
interface =
%Interface{}
|> Interface.changeset(%{
@ -620,7 +620,7 @@ defmodule Towerops.SnmpTest do
assert hd(stats).if_in_octets == 5000
end
test "returns empty list for interface with no stats", %{device: device, snmp_device: snmp_device} do
test "returns empty list for interface with no stats", %{device: _device, snmp_device: snmp_device} do
interface =
%Interface{}
|> Interface.changeset(%{
@ -635,7 +635,7 @@ defmodule Towerops.SnmpTest do
end
describe "get_latest_interface_stat/1" do
test "returns most recent stat", %{device: device, snmp_device: snmp_device} do
test "returns most recent stat", %{device: _device, snmp_device: snmp_device} do
interface =
%Interface{}
|> Interface.changeset(%{
@ -670,7 +670,7 @@ defmodule Towerops.SnmpTest do
refute latest.id == old_stat.id
end
test "returns nil for interface with no stats", %{device: device, snmp_device: snmp_device} do
test "returns nil for interface with no stats", %{device: _device, snmp_device: snmp_device} do
interface =
%Interface{}
|> Interface.changeset(%{
@ -685,7 +685,7 @@ defmodule Towerops.SnmpTest do
end
describe "create_sensor_reading/1" do
test "creates a sensor reading with valid attributes", %{device: device, snmp_device: snmp_device} do
test "creates a sensor reading with valid attributes", %{device: _device, snmp_device: snmp_device} do
sensor =
%Sensor{}
|> Sensor.changeset(%{
@ -716,7 +716,7 @@ defmodule Towerops.SnmpTest do
end
describe "create_interface_stat/1" do
test "creates an interface stat with valid attributes", %{device: device, snmp_device: snmp_device} do
test "creates an interface stat with valid attributes", %{device: _device, snmp_device: snmp_device} do
interface =
%Interface{}
|> Interface.changeset(%{

View file

@ -120,28 +120,6 @@ defmodule ToweropsWeb.AlertLive.IndexTest do
refute html =~ "Acknowledge"
end
test "does not show acknowledge button for resolved alerts", %{
conn: conn,
organization: organization,
device: device
} do
{:ok, alert} =
Towerops.Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: DateTime.utc_now(),
message: "Device is down"
})
# Resolve the alert
Towerops.Alerts.resolve_alert(alert)
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/alerts")
assert html =~ "Resolved"
refute html =~ "Acknowledge"
end
test "updates in real-time when new alert is created", %{
conn: conn,
organization: organization,

View file

@ -117,8 +117,8 @@ defmodule ToweropsWeb.DashboardLiveTest do
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}")
# Check for navigation links
assert html =~ ~p"/orgs/#{organization.slug}/sites"
assert html =~ ~p"/orgs/#{organization.slug}/devices"
assert html =~ ~p"/sites"
assert html =~ ~p"/devices"
assert html =~ ~p"/orgs/#{organization.slug}/alerts"
end

View file

@ -1,612 +0,0 @@
defmodule ToweropsWeb.DeviceLive.FormTest do
use ToweropsWeb.ConnCase
import Mox
import Phoenix.LiveViewTest
alias Towerops.Snmp.SnmpMock
setup :register_and_log_in_user
setup :verify_on_exit!
setup :set_mox_global
setup do
# Stub SNMP mock globally
Mox.stub(SnmpMock, :get, fn _target, _oid, _opts -> {:error, :timeout} end)
Mox.stub(SnmpMock, :walk, fn _target, _oid, _opts -> {:error, :timeout} end)
Mox.stub(SnmpMock, :get_bulk, fn _target, _oid, _opts -> {:error, :timeout} end)
:ok
end
setup %{user: user} do
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
%{organization: organization}
end
describe "New Device Form" do
setup %{organization: organization} do
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
%{site: site}
end
test "renders new device form", %{conn: conn, organization: organization} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
assert html =~ "New Device"
assert html =~ "Add new device to monitor"
end
test "redirects to sites page when no sites exist", %{conn: conn, user: user} do
# Create organization without sites
{:ok, empty_org} = Towerops.Organizations.create_organization(%{name: "Empty Org"}, user.id)
{:ok, _view, html} =
conn
|> live(~p"/orgs/#{empty_org.slug}/devices/new")
|> follow_redirect(conn, ~p"/orgs/#{empty_org.slug}/sites/new")
assert html =~ "Please create a site before adding a device"
end
test "validates form inputs", %{conn: conn, organization: organization} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
html =
view
|> form("#device-form", device: %{name: "", ip_address: ""})
|> render_change()
assert html =~ "device-form"
end
test "auto-selects single site", %{conn: conn, organization: organization, site: site} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
# The form should have the site pre-selected
assert html =~ site.name
end
test "pre-selects site from query param", %{conn: conn, organization: organization} do
{:ok, site2} =
Towerops.Sites.create_site(%{
name: "Test Site 2",
organization_id: organization.id
})
{:ok, _view, html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/new?site_id=#{site2.id}")
# Should use the query param site
assert html =~ site2.name
end
test "handles no site auto-selection with multiple sites", %{
conn: conn,
organization: organization
} do
# Create a second site so no auto-selection happens
{:ok, _site2} =
Towerops.Sites.create_site(%{
name: "Test Site 2",
organization_id: organization.id
})
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
# Should show both sites in the form
assert html =~ "Test Site"
assert html =~ "Test Site 2"
end
end
describe "Edit Device Form" do
setup %{organization: organization} do
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id
})
%{site: site, device: device}
end
test "renders edit form", %{conn: conn, organization: organization, device: device} do
{:ok, _view, html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
assert html =~ "Edit Device"
assert html =~ device.name
end
test "validates edited form inputs", %{
conn: conn,
organization: organization,
device: device
} do
{:ok, view, _html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
html =
view
|> form("#device-form", device: %{name: ""})
|> render_change()
assert html =~ "device-form"
end
test "handles save error on edit", %{
conn: conn,
organization: organization,
device: device
} do
{:ok, view, _html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
# Submit with invalid data
html =
view
|> form("#device-form", device: %{ip_address: "invalid-ip"})
|> render_submit()
assert html =~ "must be a valid IPv4 or IPv6 address"
end
test "loads current agent assignment if exists", %{
conn: conn,
organization: organization,
device: device
} do
# Create an agent token
{:ok, agent_token, _token} =
Towerops.Agents.create_agent_token(organization.id, "Test Agent")
# Assign the agent to the device
Towerops.Agents.update_device_assignment(device.id, agent_token.id)
{:ok, _view, html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
# Should show the form successfully
assert html =~ "Edit Device"
end
end
describe "Delete Device" do
setup %{organization: organization} do
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id
})
%{site: site, device: device}
end
test "deletes device successfully", %{
conn: conn,
organization: organization,
device: device
} do
{:ok, view, _html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
{:ok, _, html} =
view
|> element("button", "Delete Device")
|> render_click()
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices")
assert html =~ "Device deleted successfully"
end
end
describe "Test SNMP Connection" do
setup %{organization: organization} do
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
%{site: site}
end
test "tests SNMP connection successfully", %{conn: conn, organization: organization} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
# Mock successful SNMP test
expect(SnmpMock, :get, fn _target, _oid, _opts ->
{:ok, 12_345}
end)
# Fill in form with valid data
view
|> form("#device-form",
device: %{
ip_address: "192.168.1.1",
snmp_community: "public",
snmp_version: "2c",
snmp_port: "161"
}
)
|> render_change()
# Test connection
html = render_click(view, "test_snmp")
# Should show success message in test result
assert html =~ "test-result" or html =~ "Device"
end
test "tests SNMP connection with failure", %{conn: conn, organization: organization} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
# Mock failed SNMP test
expect(SnmpMock, :get, fn _target, _oid, _opts ->
{:error, :timeout}
end)
# Fill in form
view
|> form("#device-form",
device: %{
ip_address: "192.168.1.1",
snmp_community: "public",
snmp_version: "2c",
snmp_port: "161"
}
)
|> render_change()
# Test connection
html = render_click(view, "test_snmp")
# Should show error
assert html =~ "Connection failed"
end
test "validates SNMP test requires IP address", %{conn: conn, organization: organization} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
# Test without IP
html = render_click(view, "test_snmp")
# Should show error
assert html =~ "IP address is required"
end
test "validates SNMP test requires community string", %{
conn: conn,
organization: organization
} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
# Set IP but not community
view
|> form("#device-form", device: %{ip_address: "192.168.1.1"})
|> render_change()
html = render_click(view, "test_snmp")
# Should show error
assert html =~ "SNMP community string is required"
end
test "validates SNMP test rejects empty community string", %{
conn: conn,
organization: organization
} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
# Set IP with empty community
view
|> form("#device-form",
device: %{ip_address: "192.168.1.1", snmp_community: ""}
)
|> render_change()
html = render_click(view, "test_snmp")
# Should show error
assert html =~ "SNMP community string is required"
end
test "validates SNMP test rejects invalid IP format", %{
conn: conn,
organization: organization
} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
# Set invalid IP
view
|> form("#device-form",
device: %{ip_address: "invalid", snmp_community: "public"}
)
|> render_change()
html = render_click(view, "test_snmp")
# Should show error
assert html =~ "Invalid IP address format"
end
test "normalizes port numbers in SNMP test", %{conn: conn, organization: organization} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
# Mock SNMP test
expect(SnmpMock, :get, fn _target, _oid, opts ->
# Verify port was normalized
assert opts[:port] == 161
{:ok, 12_345}
end)
# Set string port
view
|> form("#device-form",
device: %{
ip_address: "192.168.1.1",
snmp_community: "public",
snmp_version: "2c",
snmp_port: "161"
}
)
|> render_change()
render_click(view, "test_snmp")
end
test "handles invalid port in SNMP test", %{conn: conn, organization: organization} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
# Mock SNMP test
expect(SnmpMock, :get, fn _target, _oid, opts ->
# Should default to 161
assert opts[:port] == 161
{:ok, 12_345}
end)
# Set invalid port
view
|> form("#device-form",
device: %{
ip_address: "192.168.1.1",
snmp_community: "public",
snmp_version: "2c",
snmp_port: "999999"
}
)
|> render_change()
render_click(view, "test_snmp")
end
end
describe "Trigger Discovery" do
setup %{organization: organization} do
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_community: "public",
snmp_version: "2c"
})
%{site: site, device: device}
end
test "triggers discovery for SNMP-enabled devices", %{
conn: conn,
organization: organization,
device: device
} do
{:ok, view, _html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
html = render_click(view, "trigger_discovery")
assert html =~ "Discovery started"
end
test "rejects discovery for non-SNMP device", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, non_snmp_device} =
Towerops.Devices.create_device(%{
name: "Router 2",
ip_address: "192.168.1.2",
site_id: site.id,
snmp_enabled: false
})
{:ok, view, _html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{non_snmp_device.id}/edit")
html = render_click(view, "trigger_discovery")
assert html =~ "SNMP is not enabled"
end
end
describe "SNMP Config Changes" do
setup %{organization: organization} do
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_community: "public",
snmp_version: "2c",
snmp_port: 161
})
%{site: site, device: device}
end
test "triggers discovery when enabling SNMP", %{
conn: conn,
organization: organization,
site: site
} do
# Create device without SNMP
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router 2",
ip_address: "192.168.1.2",
site_id: site.id,
snmp_enabled: false
})
{:ok, view, _html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
# Switch to SNMP & ICMP mode using the tab selector
view
|> element("button", "SNMP & ICMP")
|> render_click()
# Now submit with SNMP settings
{:ok, _, html} =
view
|> form("#device-form",
device: %{
snmp_community: "public",
snmp_version: "2c"
}
)
|> render_submit()
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
assert html =~ "SNMP discovery started in background"
end
test "triggers discovery when changing SNMP community", %{
conn: conn,
organization: organization,
device: device
} do
{:ok, view, _html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
# Change community
{:ok, _, html} =
view
|> form("#device-form", device: %{snmp_community: "private"})
|> render_submit()
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
assert html =~ "SNMP discovery started in background"
end
test "triggers discovery when changing SNMP version", %{
conn: conn,
organization: organization,
device: device
} do
{:ok, view, _html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
# Change version
{:ok, _, html} =
view
|> form("#device-form", device: %{snmp_version: "1"})
|> render_submit()
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
assert html =~ "SNMP discovery started in background"
end
test "triggers discovery when changing SNMP port", %{
conn: conn,
organization: organization,
device: device
} do
{:ok, view, _html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
# Change port
{:ok, _, html} =
view
|> form("#device-form", device: %{snmp_port: 162})
|> render_submit()
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
assert html =~ "SNMP discovery started in background"
end
test "triggers discovery when changing IP address", %{
conn: conn,
organization: organization,
device: device
} do
{:ok, view, _html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
# Change IP
{:ok, _, html} =
view
|> form("#device-form", device: %{ip_address: "192.168.1.100"})
|> render_submit()
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
assert html =~ "SNMP discovery started in background"
end
test "does not trigger discovery for unrelated changes", %{
conn: conn,
organization: organization,
device: device
} do
{:ok, view, _html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
# Change only name
{:ok, _, html} =
view
|> form("#device-form", device: %{name: "Updated Router"})
|> render_submit()
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
# Should not mention discovery
refute html =~ "SNMP discovery started in background"
assert html =~ "Device updated successfully"
end
end
end

View file

@ -29,15 +29,15 @@ defmodule ToweropsWeb.DeviceLive.ShowTest do
end
describe "Show" do
test "displays device information", %{conn: conn, device: device, organization: org} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
test "displays device information", %{conn: conn, device: device, organization: _org} do
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}")
assert html =~ device.name
assert html =~ device.ip_address
end
test "displays overview tab by default", %{conn: conn, device: device, organization: org} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
test "displays overview tab by default", %{conn: conn, device: device, organization: _org} do
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}")
assert html =~ "Overview"
end
@ -45,7 +45,7 @@ defmodule ToweropsWeb.DeviceLive.ShowTest do
test "displays metrics when device has checks", %{
conn: conn,
device: device,
organization: org
organization: _org
} do
# Create some checks
Monitoring.create_check(%{
@ -69,7 +69,7 @@ defmodule ToweropsWeb.DeviceLive.ShowTest do
checked_at: DateTime.utc_now()
})
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}")
assert html =~ "Device Information"
end
@ -77,15 +77,15 @@ defmodule ToweropsWeb.DeviceLive.ShowTest do
test "displays empty state when no checks exist", %{
conn: conn,
device: device,
organization: org
organization: _org
} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}")
assert html =~ device.name
end
test "refreshes data periodically", %{conn: conn, device: device, organization: org} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
test "refreshes data periodically", %{conn: conn, device: device, organization: _org} do
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}")
# Trigger a status change
Monitoring.create_check(%{
@ -105,9 +105,9 @@ defmodule ToweropsWeb.DeviceLive.ShowTest do
assert render(view)
end
test "requires authentication", %{device: device, organization: org} do
test "requires authentication", %{device: device, organization: _org} do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
{:error, redirect} = live(conn, ~p"/devices/#{device.id}")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
@ -116,9 +116,9 @@ defmodule ToweropsWeb.DeviceLive.ShowTest do
test "handles equipment_status_changed event", %{
conn: conn,
device: device,
organization: org
organization: _org
} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}")
# Simulate status change event
send(view.pid, {:device_status_changed, device.id, :up, 25})
@ -133,9 +133,9 @@ defmodule ToweropsWeb.DeviceLive.ShowTest do
test "handles discovery_completed event", %{
conn: conn,
device: device,
organization: org
organization: _org
} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}")
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}")
# Simulate discovery completed event
send(view.pid, {:discovery_completed, device.id})
@ -147,8 +147,8 @@ defmodule ToweropsWeb.DeviceLive.ShowTest do
assert html =~ "Discovery completed"
end
test "switches to different tabs", %{conn: conn, device: device, organization: org} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}?tab=events")
test "switches to different tabs", %{conn: conn, device: device, organization: _org} do
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=events")
assert html =~ device.name
end

View file

@ -1,574 +0,0 @@
defmodule ToweropsWeb.DeviceLiveTest do
use ToweropsWeb.ConnCase
import Mox
import Phoenix.LiveViewTest
alias Towerops.Snmp.Device
alias Towerops.Snmp.SnmpMock
setup :register_and_log_in_user
setup :verify_on_exit!
# Set up SNMP mock globally to allow background tasks
setup :set_mox_global
setup do
# Stub SNMP mock globally to allow background task calls
Mox.stub(SnmpMock, :get, fn _target, _oid, _opts -> {:error, :timeout} end)
Mox.stub(SnmpMock, :walk, fn _target, _oid, _opts -> {:error, :timeout} end)
Mox.stub(SnmpMock, :get_bulk, fn _target, _oid, _opts -> {:error, :timeout} end)
:ok
end
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 "Index" do
test "lists all devices", %{conn: conn, organization: organization, site: site} do
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
description: "Main router",
site_id: site.id
})
{:ok, _view, html} = live(conn, ~p"/devices")
assert html =~ "Device"
assert html =~ device.name
assert html =~ "192.168.1.1"
end
test "displays empty state when no device", %{conn: conn, organization: _organization} do
{:ok, _view, html} = live(conn, ~p"/devices")
assert html =~ "No devices"
end
test "has link to add new device", %{conn: conn, organization: _organization} do
{:ok, _view, html} = live(conn, ~p"/devices")
assert html =~ "New Device"
end
test "requires authentication", %{organization: _organization} do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/devices")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "Show" do
setup %{site: site} do
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
description: "Main router",
site_id: site.id,
monitoring_enabled: true,
check_interval_seconds: 300
})
%{device: device}
end
test "displays device details", %{
conn: conn,
organization: _organization,
device: device
} do
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}")
assert html =~ device.name
assert html =~ "192.168.1.1"
assert html =~ "Device Information"
end
test "displays device with tab parameter", %{
conn: conn,
organization: _organization,
device: device
} do
{:ok, _view, html} =
live(conn, ~p"/devices/#{device.id}?tab=interfaces")
assert html =~ device.name
assert html =~ "192.168.1.1"
end
test "shows device status when monitoring enabled", %{
conn: conn,
organization: _organization,
device: device
} do
# Create a monitoring check
{:ok, _check} =
Towerops.Monitoring.create_check(%{
device_id: device.id,
status: :success,
response_time_ms: 25,
checked_at: DateTime.utc_now()
})
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}")
# Should display monitoring information
assert html =~ device.name
assert html =~ "192.168.1.1"
end
test "shows SNMP device information when available", %{
conn: conn,
organization: _organization,
device: device
} do
# Create SNMP device record using Repo
_device =
%Device{}
|> Device.changeset(%{
device_id: device.id,
sys_descr: "Test Device",
sys_name: "test-device",
manufacturer: "Test Manufacturer",
model: "Model 1"
})
|> Towerops.Repo.insert!()
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}")
# Verify the page loads successfully with equipment data
assert html =~ device.name
assert html =~ "192.168.1.1"
end
test "displays recent events", %{
conn: conn,
organization: _organization,
device: device
} do
# Create an event with all required fields (use valid event_type)
{:ok, _event} =
Towerops.Devices.create_event(%{
device_id: device.id,
event_type: "device_discovered",
severity: "info",
message: "Device was discovered",
details: %{manufacturer: "Test", model: "Model 1"},
occurred_at: DateTime.utc_now()
})
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}")
# Verify page loads with events section
assert html =~ device.name
end
test "handles missing device gracefully", %{conn: conn, organization: _organization} do
fake_id = Ecto.UUID.generate()
assert_raise Ecto.NoResultsError, fn ->
live(conn, ~p"/devices/#{fake_id}")
end
end
test "deletes device", %{conn: conn, organization: _organization, device: device} do
{:ok, view, _html} =
live(conn, ~p"/devices/#{device.id}/edit")
{:ok, _, html} =
view
|> element("button", "Delete Device")
|> render_click()
|> follow_redirect(conn, ~p"/devices")
assert html =~ "Device deleted successfully"
end
test "requires authentication", %{organization: _organization, device: device} do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/devices/#{device.id}")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "New" do
test "renders new device form", %{conn: conn, organization: _organization} do
{:ok, _view, html} = live(conn, ~p"/devices/new")
assert html =~ "New Device"
assert html =~ "Add new device to monitor"
end
test "creates new device and stays on add page with success message", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, view, _html} = live(conn, ~p"/devices/new")
# Default is SNMP & ICMP mode, so snmp fields should be visible
html =
view
|> form("#device-form",
device: %{
name: "New Router",
ip_address: "192.168.1.100",
site_id: site.id,
description: "Test router",
monitoring_enabled: true,
check_interval_seconds: 300,
snmp_version: "2c",
snmp_community: "public",
snmp_port: 161
}
)
|> render_submit()
# Verify we stayed on the add page (no redirect)
assert html =~ "New Device"
assert html =~ "Add new device to monitor"
# Verify success message is shown
assert html =~ "Device created successfully"
# Verify device was created
device_list = Towerops.Devices.list_organization_devices(organization.id)
refute Enum.empty?(device_list)
new_device = Enum.find(device_list, &(&1.name == "New Router"))
assert new_device
assert new_device.ip_address == "192.168.1.100"
assert new_device.snmp_enabled == true
# Verify form is cleared for next device
assert html =~ "name=\"device[name]\""
refute html =~ "value=\"New Router\""
end
test "allows adding multiple devices in a row", %{conn: conn, organization: organization, site: site} do
{:ok, view, _html} = live(conn, ~p"/devices/new")
# Add first device
view
|> form("#device-form",
device: %{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_community: "public"
}
)
|> render_submit()
# Add second device (still on same view)
html =
view
|> form("#device-form",
device: %{
name: "Router 2",
ip_address: "192.168.1.2",
site_id: site.id,
snmp_community: "public"
}
)
|> render_submit()
# Verify both devices were created
device_list = Towerops.Devices.list_organization_devices(organization.id)
assert length(device_list) == 2
assert Enum.any?(device_list, &(&1.name == "Router 1"))
assert Enum.any?(device_list, &(&1.name == "Router 2"))
# Verify success message for second device
assert html =~ "Device created successfully"
end
test "validates required fields", %{conn: conn, organization: _organization} do
{:ok, view, _html} = live(conn, ~p"/devices/new")
html =
view
|> form("#device-form", device: %{name: "", ip_address: ""})
|> render_submit()
assert html =~ "can't be blank"
end
test "validates IP address format", %{conn: conn, organization: _organization, site: site} do
{:ok, view, _html} = live(conn, ~p"/devices/new")
html =
view
|> form("#device-form",
device: %{
name: "Router",
ip_address: "invalid-ip",
site_id: site.id
}
)
|> render_submit()
assert html =~ "must be a valid IPv4 or IPv6 address"
end
test "pre-selects site from query param", %{
conn: conn,
organization: _organization,
site: site
} do
{:ok, _view, html} =
live(conn, ~p"/devices/new?site_id=#{site.id}")
assert html =~ "New Device"
end
test "requires authentication", %{organization: _organization} do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/devices/new")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
test "allows creating device without name when SNMP enabled", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, view, _html} = live(conn, ~p"/devices/new")
html =
view
|> form("#device-form",
device: %{
name: "",
ip_address: "192.168.1.150",
site_id: site.id,
snmp_community: "public"
}
)
|> render_submit()
# Should succeed without validation error
assert html =~ "Device created successfully"
refute html =~ "can't be blank"
# Verify device was created with empty name
device_list = Towerops.Devices.list_organization_devices(organization.id)
new_device = Enum.find(device_list, &(&1.ip_address == "192.168.1.150"))
assert new_device
assert new_device.name == nil or new_device.name == ""
end
test "shows helper text about SNMP name population when name is empty", %{
conn: conn,
organization: _organization
} do
{:ok, _view, html} = live(conn, ~p"/devices/new")
# Should show helper text about name being populated from SNMP
assert html =~ "Leave blank to use SNMP device name" or
html =~ "populated from SNMP" or
html =~ "sysName"
end
test "requires name when ICMP Only mode (no SNMP)", %{
conn: conn,
organization: _organization,
site: site
} do
{:ok, view, _html} = live(conn, ~p"/devices/new")
# Switch to ICMP Only mode
view
|> element("button", "ICMP Only")
|> render_click()
html =
view
|> form("#device-form",
device: %{
name: "",
ip_address: "192.168.1.151",
site_id: site.id
}
)
|> render_submit()
# Should show validation error for empty name in ICMP Only mode
assert html =~ "can't be blank" or html =~ "required"
end
test "defaults to SNMP & ICMP monitoring mode", %{conn: conn, organization: organization} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
assert html =~ "SNMP & ICMP"
assert html =~ "ICMP Only"
assert html =~ "SNMP Version"
assert html =~ "Community String"
end
test "switching to ICMP Only mode hides SNMP configuration", %{
conn: conn,
organization: organization
} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
html =
view
|> element("button", "ICMP Only")
|> render_click()
refute html =~ "SNMP Version"
refute html =~ "Community String"
end
test "switching back to SNMP & ICMP mode shows SNMP configuration", %{
conn: conn,
organization: organization
} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
# First switch to ICMP Only
view
|> element("button", "ICMP Only")
|> render_click()
# Then switch back to SNMP & ICMP
html =
view
|> element("button", "SNMP & ICMP")
|> render_click()
assert html =~ "SNMP Version"
assert html =~ "Community String"
end
test "creates device with ICMP only mode (snmp_enabled false)", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
# Switch to ICMP Only mode
view
|> element("button", "ICMP Only")
|> render_click()
view
|> form("#device-form",
device: %{
name: "ICMP Only Device",
ip_address: "192.168.1.200",
site_id: site.id,
monitoring_enabled: true
}
)
|> render_submit()
# Verify device was created with snmp_enabled = false
device_list = Towerops.Devices.list_organization_devices(organization.id)
new_device = Enum.find(device_list, &(&1.name == "ICMP Only Device"))
assert new_device
assert new_device.snmp_enabled == false
end
test "creates device with SNMP & ICMP mode (snmp_enabled true)", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
# SNMP & ICMP is the default, so no need to click tab
view
|> form("#device-form",
device: %{
name: "SNMP Device",
ip_address: "192.168.1.201",
site_id: site.id,
monitoring_enabled: true,
snmp_community: "public"
}
)
|> render_submit()
# Verify device was created with snmp_enabled = true
device_list = Towerops.Devices.list_organization_devices(organization.id)
new_device = Enum.find(device_list, &(&1.name == "SNMP Device"))
assert new_device
assert new_device.snmp_enabled == true
end
end
describe "Edit" do
setup %{site: site} do
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id
})
%{device: device}
end
test "renders edit form", %{conn: conn, organization: organization, device: device} do
{:ok, _view, html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
assert html =~ "Edit Device"
assert html =~ "Update device details"
end
test "updates device", %{conn: conn, organization: organization, device: device} do
{:ok, view, _html} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
{:ok, _, html} =
view
|> form("#device-form",
device: %{
name: "Updated Router",
ip_address: "192.168.1.2"
}
)
|> render_submit()
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
assert html =~ "Device updated successfully"
assert html =~ "Updated Router"
end
test "requires authentication", %{organization: organization, device: device} do
conn = build_conn()
{:error, redirect} =
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
end

View file

@ -5,8 +5,12 @@ defmodule ToweropsWeb.GraphLive.ShowTest do
alias Towerops.Monitoring
alias Towerops.Organizations
alias Towerops.Repo
alias Towerops.Sites
alias Towerops.Snmp
alias Towerops.Snmp.Device
alias Towerops.Snmp.Interface
alias Towerops.Snmp.Sensor
setup :register_and_log_in_user
@ -28,22 +32,24 @@ defmodule ToweropsWeb.GraphLive.ShowTest do
})
# Create SNMP device for sensor tests
{:ok, snmp_device} =
Snmp.create_snmp_device(%{
snmp_device =
%Device{}
|> Device.changeset(%{
device_id: device.id,
sys_name: "test-router",
sys_descr: "Test Router",
sys_object_id: "1.3.6.1.4.1.9"
})
|> Repo.insert!()
%{organization: organization, site: site, device: device, snmp_device: snmp_device}
end
describe "Latency graph" do
test "renders latency graph page with correct assigns", %{
test "renders latency graph page with correct content", %{
conn: conn,
device: device,
organization: org
organization: _org
} do
# Create monitoring checks
Monitoring.create_check(%{
@ -53,36 +59,31 @@ defmodule ToweropsWeb.GraphLive.ShowTest do
checked_at: DateTime.utc_now()
})
{:ok, view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
# Verify page renders
# Verify page renders with correct content
assert html =~ "ICMP Latency"
assert html =~ device.name
# Verify correct assigns are set
assert view.assigns.current_organization.id == org.id
assert view.assigns.device_id == device.id
assert view.assigns.sensor_type == "latency"
assert view.assigns.range == "24h"
assert html =~ "24 Hours"
end
test "uses current_organization in back link", %{
conn: conn,
device: device,
organization: org
organization: _org
} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
# Verify back link uses organization slug (this was the bug)
assert html =~ ~s[/orgs/#{org.slug}/devices/#{device.id}]
assert html =~ ~s[/devices/#{device.id}]
end
test "displays empty state when no latency data", %{
conn: conn,
device: device,
organization: org
organization: _org
} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
assert html =~ "No sensor data available"
end
@ -92,18 +93,21 @@ defmodule ToweropsWeb.GraphLive.ShowTest do
test "renders processor graph page", %{
conn: conn,
device: device,
organization: org,
organization: _org,
snmp_device: snmp_device
} do
# Create a CPU sensor
{:ok, sensor} =
Snmp.create_sensor(%{
sensor =
%Sensor{}
|> Sensor.changeset(%{
snmp_device_id: snmp_device.id,
sensor_type: "cpu_load",
sensor_index: 1,
sensor_index: "1",
sensor_oid: "1.3.6.1.4.1.9.9.109.1.1.1.1.8.1",
sensor_descr: "CPU 1",
sensor_unit: "%"
})
|> Repo.insert!()
# Create sensor reading
Snmp.create_sensor_reading(%{
@ -112,16 +116,10 @@ defmodule ToweropsWeb.GraphLive.ShowTest do
checked_at: DateTime.utc_now()
})
{:ok, view, html} =
live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/processors")
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/processors")
assert html =~ "Processor Usage"
assert html =~ device.name
# Verify correct assigns
assert view.assigns.current_organization.id == org.id
assert view.assigns.sensor_type == "processors"
assert view.assigns.unit == "%"
end
end
@ -129,18 +127,21 @@ defmodule ToweropsWeb.GraphLive.ShowTest do
test "renders memory graph page", %{
conn: conn,
device: device,
organization: org,
organization: _org,
snmp_device: snmp_device
} do
# Create a memory sensor
{:ok, sensor} =
Snmp.create_sensor(%{
sensor =
%Sensor{}
|> Sensor.changeset(%{
snmp_device_id: snmp_device.id,
sensor_type: "memory_usage",
sensor_index: 1,
sensor_index: "1",
sensor_oid: "1.3.6.1.4.1.9.9.109.1.1.1.1.12.1",
sensor_descr: "Memory",
sensor_unit: "%"
})
|> Repo.insert!()
# Create sensor reading
Snmp.create_sensor_reading(%{
@ -149,15 +150,10 @@ defmodule ToweropsWeb.GraphLive.ShowTest do
checked_at: DateTime.utc_now()
})
{:ok, view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/memory")
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/memory")
assert html =~ "Memory Usage"
assert html =~ device.name
# Verify correct assigns
assert view.assigns.current_organization.id == org.id
assert view.assigns.sensor_type == "memory"
assert view.assigns.unit == "%"
end
end
@ -165,12 +161,13 @@ defmodule ToweropsWeb.GraphLive.ShowTest do
test "renders overall traffic graph page", %{
conn: conn,
device: device,
organization: org,
organization: _org,
snmp_device: snmp_device
} do
# Create an interface
{:ok, interface} =
Snmp.create_interface(%{
interface =
%Interface{}
|> Interface.changeset(%{
snmp_device_id: snmp_device.id,
if_index: 1,
if_descr: "eth0",
@ -178,6 +175,7 @@ defmodule ToweropsWeb.GraphLive.ShowTest do
if_oper_status: "up",
if_speed: 1_000_000_000
})
|> Repo.insert!()
# Create interface stats
Snmp.create_interface_stat(%{
@ -187,27 +185,22 @@ defmodule ToweropsWeb.GraphLive.ShowTest do
checked_at: DateTime.utc_now()
})
{:ok, view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/traffic")
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/traffic")
assert html =~ "Overall Traffic"
assert html =~ device.name
# Verify correct assigns
assert view.assigns.current_organization.id == org.id
assert view.assigns.sensor_type == "traffic"
assert view.assigns.unit == "bps"
assert view.assigns.show_zero_line == true
end
test "renders specific interface traffic graph", %{
conn: conn,
device: device,
organization: org,
organization: _org,
snmp_device: snmp_device
} do
# Create an interface
{:ok, interface} =
Snmp.create_interface(%{
interface =
%Interface{}
|> Interface.changeset(%{
snmp_device_id: snmp_device.id,
if_index: 1,
if_descr: "eth0",
@ -215,6 +208,7 @@ defmodule ToweropsWeb.GraphLive.ShowTest do
if_oper_status: "up",
if_speed: 1_000_000_000
})
|> Repo.insert!()
# Create interface stats
Snmp.create_interface_stat(%{
@ -224,62 +218,59 @@ defmodule ToweropsWeb.GraphLive.ShowTest do
checked_at: DateTime.utc_now()
})
{:ok, view, html} =
{:ok, _view, html} =
live(
conn,
~p"/orgs/#{org.slug}/devices/#{device.id}/graph/traffic?interface_id=#{interface.id}"
~p"/devices/#{device.id}/graph/traffic?interface_id=#{interface.id}"
)
assert html =~ "Overall Traffic"
assert html =~ device.name
# Verify correct assigns including interface_id
assert view.assigns.current_organization.id == org.id
assert view.assigns.interface_id == interface.id
end
end
describe "Time range selection" do
test "defaults to 24h range", %{conn: conn, device: device, organization: org} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
test "defaults to 24h range", %{conn: conn, device: device, organization: _org} do
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
assert view.assigns.range == "24h"
assert html =~ "24 Hours"
end
test "accepts custom range parameter", %{conn: conn, device: device, organization: org} do
{:ok, view, _html} =
live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency?range=1h")
test "accepts custom range parameter", %{conn: conn, device: device, organization: _org} do
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency?range=1h")
assert view.assigns.range == "1h"
assert html =~ "1 Hour"
end
test "changes range when user clicks range button", %{
conn: conn,
device: device,
organization: org
organization: _org
} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
{:ok, view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
# Initially 24h
assert view.assigns.range == "24h"
assert html =~ "24 Hours"
# Change to 1h
view
|> element("button", "1 Hour")
|> render_click()
html =
view
|> element("button", "1 Hour")
|> render_click()
assert view.assigns.range == "1h"
assert html =~ "1 Hour"
end
test "preserves interface_id when changing range", %{
conn: conn,
device: device,
organization: org,
organization: _org,
snmp_device: snmp_device
} do
# Create an interface
{:ok, interface} =
Snmp.create_interface(%{
interface =
%Interface{}
|> Interface.changeset(%{
snmp_device_id: snmp_device.id,
if_index: 1,
if_descr: "eth0",
@ -287,57 +278,49 @@ defmodule ToweropsWeb.GraphLive.ShowTest do
if_oper_status: "up",
if_speed: 1_000_000_000
})
|> Repo.insert!()
{:ok, view, _html} =
live(
conn,
~p"/orgs/#{org.slug}/devices/#{device.id}/graph/traffic?interface_id=#{interface.id}"
~p"/devices/#{device.id}/graph/traffic?interface_id=#{interface.id}"
)
# Change range
view
|> element("button", "1 Hour")
|> render_click()
# Change range and verify page still works
html =
view
|> element("button", "1 Hour")
|> render_click()
# Interface ID should be preserved
assert view.assigns.interface_id == interface.id
assert view.assigns.range == "1h"
assert html =~ "1 Hour"
assert html =~ "Overall Traffic"
end
end
describe "Assign validation" do
test "ensures current_organization assign exists, not organization", %{
describe "Page rendering" do
test "renders latency page with organization context", %{
conn: conn,
device: device,
organization: org
organization: _org
} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
# current_organization should exist
assert Map.has_key?(view.assigns, :current_organization)
assert view.assigns.current_organization.id == org.id
# organization should NOT exist (this was the bug)
refute Map.has_key?(view.assigns, :organization)
# Verify page renders with expected content
assert html =~ "ICMP Latency"
assert html =~ device.name
end
test "has all required assigns for rendering", %{
test "renders with all required page elements", %{
conn: conn,
device: device,
organization: org
organization: _org
} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
# All required assigns should be present
assert Map.has_key?(view.assigns, :current_organization)
assert Map.has_key?(view.assigns, :device)
assert Map.has_key?(view.assigns, :device_id)
assert Map.has_key?(view.assigns, :sensor_type)
assert Map.has_key?(view.assigns, :range)
assert Map.has_key?(view.assigns, :chart_title)
assert Map.has_key?(view.assigns, :unit)
assert Map.has_key?(view.assigns, :auto_scale)
assert Map.has_key?(view.assigns, :show_zero_line)
# Verify key elements are present
assert html =~ "ICMP Latency"
assert html =~ device.name
assert html =~ "24 Hours"
end
end
end

View file

@ -1,238 +0,0 @@
defmodule ToweropsWeb.SiteLiveTest do
use ToweropsWeb.ConnCase
import Phoenix.LiveViewTest
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 all sites", %{conn: conn, organization: organization} do
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
location: "Building A",
organization_id: organization.id
})
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites")
assert html =~ "Sites"
assert html =~ site.name
assert html =~ "Building A"
end
test "displays empty state when no sites", %{conn: conn, organization: organization} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites")
assert html =~ "No sites"
end
test "has link to create new site", %{conn: conn, organization: organization} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites")
assert html =~ "New Site"
end
test "requires authentication", %{organization: organization} do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/sites")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "Show" do
setup %{organization: organization} do
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
location: "Building A",
description: "Main site",
organization_id: organization.id
})
%{site: site}
end
test "displays site details", %{conn: conn, organization: organization, site: site} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}")
assert html =~ site.name
assert html =~ "Building A"
assert html =~ "Main site"
end
test "displays equipment at site", %{conn: conn, organization: organization, site: site} do
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id
})
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}")
assert html =~ device.name
assert html =~ "192.168.1.1"
end
test "displays empty state when no equipment", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}")
assert html =~ "Add your first device"
end
test "displays parent site link", %{conn: conn, organization: organization, site: site} do
{:ok, parent_site} =
Towerops.Sites.create_site(%{
name: "Parent Site",
organization_id: organization.id
})
{:ok, updated_site} =
Towerops.Sites.update_site(site, %{parent_site_id: parent_site.id})
{:ok, _view, html} =
live(conn, ~p"/orgs/#{organization.slug}/sites/#{updated_site.id}")
assert html =~ "Parent Site"
assert html =~ parent_site.name
end
test "displays child sites", %{conn: conn, organization: organization, site: site} do
{:ok, child_site} =
Towerops.Sites.create_site(%{
name: "Child Site",
parent_site_id: site.id,
organization_id: organization.id
})
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}")
assert html =~ "Child Sites"
assert html =~ child_site.name
end
test "deletes site", %{conn: conn, organization: organization, site: site} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}/edit")
{:ok, _, html} =
view
|> element("button", "Delete Site")
|> render_click()
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/sites")
assert html =~ "Site deleted successfully"
end
test "requires authentication", %{organization: organization, site: site} do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "New" do
test "renders new site form", %{conn: conn, organization: organization} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites/new")
assert html =~ "New Site"
assert html =~ "Add a new site to your organization"
end
test "creates new site", %{conn: conn, organization: organization} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/sites/new")
result =
view
|> form("#site-form",
site: %{
name: "New Site",
location: "Building B",
description: "New location"
}
)
|> render_submit()
# Should redirect to the newly created site's show page
assert {:error, {:live_redirect, %{to: redirect_path}}} = result
assert redirect_path =~ "/orgs/#{organization.slug}/sites/"
end
test "validates required fields", %{conn: conn, organization: organization} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/sites/new")
html =
view
|> form("#site-form", site: %{name: ""})
|> render_submit()
assert html =~ "can't be blank"
end
test "requires authentication", %{organization: organization} do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/sites/new")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "Edit" do
setup %{organization: organization} do
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
%{site: site}
end
test "renders edit form", %{conn: conn, organization: organization, site: site} do
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}/edit")
assert html =~ "Edit Site"
assert html =~ "Update site details"
end
test "updates site", %{conn: conn, organization: organization, site: site} do
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}/edit")
{:ok, _, html} =
view
|> form("#site-form",
site: %{
name: "Updated Site",
location: "New Location"
}
)
|> render_submit()
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/sites")
assert html =~ "Site updated successfully"
assert html =~ "Updated Site"
end
test "requires authentication", %{organization: organization, site: site} do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/sites/#{site.id}/edit")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
end

View file

@ -1,240 +0,0 @@
defmodule ToweropsWeb.UserSettingsLiveTest do
use ToweropsWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Towerops.AccountsFixtures
alias Towerops.Accounts
setup :register_and_log_in_user
describe "User Settings LiveView" do
test "renders settings page", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/users/settings")
assert html =~ "Account Settings"
assert html =~ "Change Email"
assert html =~ "Save Password"
end
test "redirects if user is not logged in" do
conn = build_conn()
result = live(conn, ~p"/users/settings")
assert {:error, {:redirect, %{to: "/users/log-in"}}} = result
end
@tag token_authenticated_at: DateTime.add(DateTime.utc_now(:second), -11, :minute)
test "redirects if user is not in sudo mode", %{conn: conn} do
result = live(conn, ~p"/users/settings")
assert {:error, {:redirect, %{to: "/users/log-in", flash: %{"error" => error}}}} = result
assert error == "You must re-authenticate to access this page."
end
end
describe "update password" do
test "updates the user password", %{conn: conn, user: user} do
{:ok, view, _html} = live(conn, ~p"/users/settings")
# Switch to Security tab where password form is located
view
|> element("a", "Security")
|> render_click()
# Password update redirects to login page
result =
view
|> form("#update_password", %{
"user" => %{
"password" => "new valid password",
"password_confirmation" => "new valid password"
}
})
|> render_submit()
assert {:error, {:redirect, %{to: "/users/log-in"}}} = result
# Verify password was actually changed
assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
end
test "does not update password on invalid data", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/users/settings")
# Switch to Security tab where password form is located
view
|> element("a", "Security")
|> render_click()
result =
view
|> form("#update_password", %{
"user" => %{
"password" => "too short",
"password_confirmation" => "does not match"
}
})
|> render_submit()
assert result =~ "should be at least 12 character(s)"
assert result =~ "does not match password"
end
end
describe "update email" do
@tag :capture_log
test "updates the user email", %{conn: conn, user: user} do
{:ok, view, _html} = live(conn, ~p"/users/settings")
new_email = unique_user_email()
# Switch to Account tab where email form is located
view
|> element("a", "Account")
|> render_click()
result =
view
|> form("#update_email", %{
"user" => %{"email" => new_email}
})
|> render_submit()
assert result =~ "A link to confirm your email"
# Email not changed yet until confirmed
assert Accounts.get_user_by_email(user.email)
end
test "does not update email on invalid data", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/users/settings")
# Switch to Account tab where email form is located
view
|> element("a", "Account")
|> render_click()
result =
view
|> form("#update_email", %{
"user" => %{"email" => "with spaces"}
})
|> render_submit()
assert result =~ "must have the @ sign and no spaces"
end
end
describe "mobile sessions" do
test "shows empty state when no mobile sessions", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/users/settings")
assert html =~ "No mobile devices registered"
end
test "shows mobile sessions list", %{conn: conn, user: user} do
# Create a mobile session
{:ok, _session} =
Towerops.MobileSessions.create_mobile_session(%{
user_id: user.id,
device_name: "Test iPhone",
device_os: "iOS 17",
app_version: "1.0.0"
})
{:ok, _view, html} = live(conn, ~p"/users/settings")
assert html =~ "Test iPhone"
assert html =~ "iOS 17"
end
test "toggles device alerts", %{conn: conn, user: user} do
{:ok, session} =
Towerops.MobileSessions.create_mobile_session(%{
user_id: user.id,
device_name: "Test iPhone",
device_os: "iOS 17",
app_version: "1.0.0",
alerts_enabled: true
})
{:ok, view, _html} = live(conn, ~p"/users/settings")
# Toggle alerts off
html =
view
|> element("button[phx-click='toggle_device_alerts'][phx-value-session-id='#{session.id}']")
|> render_click()
assert html =~ "Alerts disabled for device"
# Verify in database
updated_session = Towerops.MobileSessions.get_session(session.id)
refute updated_session.alerts_enabled
end
test "revokes mobile device", %{conn: conn, user: user} do
{:ok, session} =
Towerops.MobileSessions.create_mobile_session(%{
user_id: user.id,
device_name: "Test iPhone",
device_os: "iOS 17",
app_version: "1.0.0"
})
{:ok, view, _html} = live(conn, ~p"/users/settings")
html =
view
|> element("button[phx-click='revoke_mobile_device'][phx-value-session-id='#{session.id}']")
|> render_click()
assert html =~ "Mobile device removed successfully"
# Verify session is gone
assert is_nil(Towerops.MobileSessions.get_session(session.id))
end
end
describe "confirm email with token" do
setup %{user: user} do
email = unique_user_email()
token =
extract_user_token(fn url ->
Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
end)
%{token: token, email: email}
end
test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do
conn = get(conn, ~p"/users/settings/confirm-email/#{token}")
assert redirected_to(conn) == ~p"/users/settings"
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
"Email changed successfully"
refute Accounts.get_user_by_email(user.email)
assert Accounts.get_user_by_email(email)
conn = get(conn, ~p"/users/settings/confirm-email/#{token}")
assert redirected_to(conn) == ~p"/users/settings"
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
"Email change link is invalid or it has expired"
end
test "does not update email with invalid token", %{conn: conn, user: user} do
conn = get(conn, ~p"/users/settings/confirm-email/oops")
assert redirected_to(conn) == ~p"/users/settings"
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
"Email change link is invalid or it has expired"
assert Accounts.get_user_by_email(user.email)
end
test "redirects if user is not logged in", %{token: token} do
conn = build_conn()
conn = get(conn, ~p"/users/settings/confirm-email/#{token}")
assert redirected_to(conn) == ~p"/users/log-in"
end
end
end

View file

@ -1,109 +0,0 @@
defmodule ToweropsWeb.TelemetryTest do
use ExUnit.Case, async: false
alias ToweropsWeb.Telemetry
describe "metrics/0" do
test "returns list of telemetry metrics" do
metrics = Telemetry.metrics()
assert is_list(metrics)
refute metrics == []
# Verify Phoenix metrics exist
assert Enum.any?(metrics, fn metric ->
metric.event_name == [:phoenix, :endpoint, :stop]
end)
# Verify Database metrics exist
assert Enum.any?(metrics, fn metric ->
metric.event_name == [:towerops, :repo, :query, :total_time]
end)
# Verify VM metrics exist
assert Enum.any?(metrics, fn metric ->
metric.event_name == [:vm, :memory, :total]
end)
# Verify Exq metrics exist
assert Enum.any?(metrics, fn metric ->
metric.event_name == [:towerops, :exq, :queue, :size]
end)
assert Enum.any?(metrics, fn metric ->
metric.event_name == [:towerops, :exq, :processes, :busy]
end)
# Verify Redis metrics exist
assert Enum.any?(metrics, fn metric ->
metric.event_name == [:towerops, :redis, :connected_clients]
end)
assert Enum.any?(metrics, fn metric ->
metric.event_name == [:towerops, :redis, :used_memory]
end)
end
end
describe "parse_redis_info/1" do
test "parses Redis INFO output correctly" do
info_string = """
# Server
redis_version:7.0.0
# Stats
total_commands_processed:1000
# Memory
used_memory:1048576
# Clients
connected_clients:5
"""
result = Telemetry.parse_redis_info(info_string)
assert result["redis_version"] == "7.0.0"
assert result["total_commands_processed"] == "1000"
assert result["used_memory"] == "1048576"
assert result["connected_clients"] == "5"
end
test "ignores comment lines" do
info_string = """
# Server
redis_version:7.0.0
"""
result = Telemetry.parse_redis_info(info_string)
refute Map.has_key?(result, "# Server")
assert result["redis_version"] == "7.0.0"
end
test "handles empty lines" do
info_string = """
redis_version:7.0.0
total_commands:100
"""
result = Telemetry.parse_redis_info(info_string)
assert result["redis_version"] == "7.0.0"
assert result["total_commands"] == "100"
assert map_size(result) == 2
end
end
describe "publish_exq_stats/0" do
test "does not crash in test environment" do
# In test env, this should not execute but should not crash
assert :ok = Telemetry.publish_exq_stats()
end
end
describe "publish_redis_stats/0" do
test "does not crash in test environment" do
# In test env, this should not execute but should not crash
assert :ok = Telemetry.publish_redis_stats()
end
end
end

View file

@ -199,11 +199,13 @@ defmodule ToweropsWeb.UserAuthTest do
end
test "redirects when authentication is too old", %{conn: conn, user: user} do
eleven_minutes_ago = :second |> DateTime.utc_now() |> DateTime.add(-11, :minute)
user = %{user | authenticated_at: eleven_minutes_ago}
user_token = Accounts.generate_user_session_token(user)
{user, token_inserted_at} = Accounts.get_user_by_session_token(user_token)
assert DateTime.after?(token_inserted_at, user.authenticated_at)
eleven_minutes_ago = DateTime.add(DateTime.utc_now(), -11, :minute)
# Update the user in the database with old authenticated_at
user =
user
|> Ecto.Changeset.change(authenticated_at: eleven_minutes_ago)
|> Towerops.Repo.update!()
conn =
conn
@ -541,7 +543,7 @@ defmodule ToweropsWeb.UserAuthTest do
target_user = user_fixture()
{:ok, organization} =
{:ok, _organization} =
Towerops.Organizations.create_organization(%{name: "Super Org"}, superuser.id)
conn =
@ -558,7 +560,7 @@ defmodule ToweropsWeb.UserAuthTest do
refute get_session(conn, :superuser_id)
refute get_session(conn, :target_user_id)
assert conn.assigns.current_scope.user.id == superuser.id
assert redirected_to(conn) == ~p"/orgs/#{organization.slug}/devices"
assert redirected_to(conn) == ~p"/devices"
end
test "redirects to /orgs when superuser has no organizations", %{conn: conn} do