test cleanup
This commit is contained in:
parent
fe7a44e5e2
commit
77d4b25da7
23 changed files with 236 additions and 2228 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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} =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(%{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue