634 lines
17 KiB
Elixir
634 lines
17 KiB
Elixir
defmodule ToweropsWeb.GraphLive.ShowTest do
|
|
use ToweropsWeb.ConnCase
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
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
|
|
|
|
setup %{user: user} do
|
|
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
|
|
|
{:ok, site} =
|
|
Sites.create_site(%{
|
|
name: "Test Site",
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, device} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Test Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id,
|
|
snmp_enabled: true
|
|
})
|
|
|
|
# Create SNMP device for sensor tests
|
|
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 content", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org
|
|
} do
|
|
# Create monitoring checks
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 10.5,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
|
|
|
|
# Verify page renders with correct content
|
|
assert html =~ "Ping Latency"
|
|
assert html =~ device.name
|
|
assert html =~ "24 Hours"
|
|
end
|
|
|
|
test "uses current_organization in back link", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org
|
|
} do
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
|
|
|
|
# Verify back link uses organization slug (this was the bug)
|
|
assert html =~ ~s[/devices/#{device.id}]
|
|
end
|
|
|
|
test "displays empty state when no latency data", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org
|
|
} do
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
|
|
|
|
assert html =~ "No sensor data available"
|
|
end
|
|
end
|
|
|
|
describe "Processor graph" do
|
|
test "renders processor graph page", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org,
|
|
snmp_device: snmp_device
|
|
} do
|
|
# Create a CPU sensor
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "cpu_load",
|
|
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(%{
|
|
sensor_id: sensor.id,
|
|
value: 45.2,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/processors")
|
|
|
|
assert html =~ "Processor Usage"
|
|
assert html =~ device.name
|
|
end
|
|
end
|
|
|
|
describe "Memory graph" do
|
|
test "renders memory graph page", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org,
|
|
snmp_device: snmp_device
|
|
} do
|
|
# Create a memory sensor
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "memory_usage",
|
|
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(%{
|
|
sensor_id: sensor.id,
|
|
value: 75.5,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/memory")
|
|
|
|
assert html =~ "Memory Usage"
|
|
assert html =~ device.name
|
|
end
|
|
end
|
|
|
|
describe "Traffic graph" do
|
|
test "renders overall traffic graph page", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org,
|
|
snmp_device: snmp_device
|
|
} do
|
|
# Create an interface
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_descr: "eth0",
|
|
if_name: "eth0",
|
|
if_oper_status: "up",
|
|
if_speed: 1_000_000_000
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create interface stats
|
|
Snmp.create_interface_stat(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 1000,
|
|
if_out_octets: 2000,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/traffic")
|
|
|
|
assert html =~ "Overall Traffic"
|
|
assert html =~ device.name
|
|
end
|
|
|
|
test "renders specific interface traffic graph", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org,
|
|
snmp_device: snmp_device
|
|
} do
|
|
# Create an interface
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_descr: "eth0",
|
|
if_name: "eth0",
|
|
if_oper_status: "up",
|
|
if_speed: 1_000_000_000
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create interface stats
|
|
Snmp.create_interface_stat(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 1000,
|
|
if_out_octets: 2000,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|
|
{:ok, _view, html} =
|
|
live(
|
|
conn,
|
|
~p"/devices/#{device.id}/graph/traffic?interface_id=#{interface.id}"
|
|
)
|
|
|
|
assert html =~ "Overall Traffic"
|
|
assert html =~ device.name
|
|
end
|
|
|
|
test "handles nil if_in_octets gracefully", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org,
|
|
snmp_device: snmp_device
|
|
} do
|
|
# Create an interface
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_descr: "eth0",
|
|
if_name: "eth0",
|
|
if_oper_status: "up",
|
|
if_speed: 1_000_000_000
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create interface stats with nil if_in_octets
|
|
base_time = DateTime.utc_now()
|
|
|
|
Snmp.create_interface_stat(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: nil,
|
|
if_out_octets: 1000,
|
|
checked_at: DateTime.add(base_time, -60, :second)
|
|
})
|
|
|
|
Snmp.create_interface_stat(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: nil,
|
|
if_out_octets: 2000,
|
|
checked_at: base_time
|
|
})
|
|
|
|
# Should not crash
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/traffic")
|
|
|
|
assert html =~ "Overall Traffic"
|
|
assert html =~ device.name
|
|
end
|
|
|
|
test "handles nil if_out_octets gracefully", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org,
|
|
snmp_device: snmp_device
|
|
} do
|
|
# Create an interface
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_descr: "eth0",
|
|
if_name: "eth0",
|
|
if_oper_status: "up",
|
|
if_speed: 1_000_000_000
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create interface stats with nil if_out_octets
|
|
base_time = DateTime.utc_now()
|
|
|
|
Snmp.create_interface_stat(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 1000,
|
|
if_out_octets: nil,
|
|
checked_at: DateTime.add(base_time, -60, :second)
|
|
})
|
|
|
|
Snmp.create_interface_stat(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 2000,
|
|
if_out_octets: nil,
|
|
checked_at: base_time
|
|
})
|
|
|
|
# Should not crash
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/traffic")
|
|
|
|
assert html =~ "Overall Traffic"
|
|
assert html =~ device.name
|
|
end
|
|
|
|
test "handles mixed nil and valid octets values", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org,
|
|
snmp_device: snmp_device
|
|
} do
|
|
# Create an interface
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_descr: "eth0",
|
|
if_name: "eth0",
|
|
if_oper_status: "up",
|
|
if_speed: 1_000_000_000
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create interface stats with mixed nil and valid values
|
|
base_time = DateTime.utc_now()
|
|
|
|
# First stat: nil values
|
|
Snmp.create_interface_stat(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: nil,
|
|
if_out_octets: nil,
|
|
checked_at: DateTime.add(base_time, -120, :second)
|
|
})
|
|
|
|
# Second stat: valid values
|
|
Snmp.create_interface_stat(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 1000,
|
|
if_out_octets: 2000,
|
|
checked_at: DateTime.add(base_time, -60, :second)
|
|
})
|
|
|
|
# Third stat: valid values
|
|
Snmp.create_interface_stat(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 2000,
|
|
if_out_octets: 4000,
|
|
checked_at: base_time
|
|
})
|
|
|
|
# Should not crash and should render valid data points
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/traffic")
|
|
|
|
assert html =~ "Overall Traffic"
|
|
assert html =~ device.name
|
|
end
|
|
|
|
test "renders backhaul traffic and capacity with consistent polarity", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org,
|
|
snmp_device: snmp_device
|
|
} do
|
|
device =
|
|
device
|
|
|> Ecto.Changeset.change(device_role: "backhaul")
|
|
|> Repo.update!()
|
|
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_descr: "eth0",
|
|
if_name: "eth0",
|
|
if_oper_status: "up",
|
|
if_speed: 1_000_000_000,
|
|
configured_capacity_bps: 500_000_000,
|
|
capacity_source: "manual"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
base_time = DateTime.utc_now()
|
|
|
|
Snmp.create_interface_stat(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 1_000,
|
|
if_out_octets: 2_000,
|
|
checked_at: DateTime.add(base_time, -60, :second)
|
|
})
|
|
|
|
Snmp.create_interface_stat(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 10_000,
|
|
if_out_octets: 14_000,
|
|
checked_at: base_time
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/traffic")
|
|
|
|
datasets = chart_datasets_from_html(html, "detail-chart")
|
|
|
|
assert dataset_y_values(datasets, "eth0 Out") == [-1600.0]
|
|
assert dataset_y_values(datasets, "eth0 In") == [1200.0]
|
|
assert dataset_y_values(datasets, "Capacity (In)") == [500_000_000.0]
|
|
assert dataset_y_values(datasets, "Capacity (Out)") == [-500_000_000.0]
|
|
assert dataset_fill(datasets, "Capacity (In)") == false
|
|
assert dataset_fill(datasets, "Capacity (Out)") == false
|
|
end
|
|
end
|
|
|
|
defp chart_datasets_from_html(html, chart_id) do
|
|
pattern = ~r/id="#{chart_id}"[^>]*data-chart="([^"]+)"/
|
|
[_, encoded_json] = Regex.run(pattern, html)
|
|
|
|
encoded_json
|
|
|> String.replace(""", "\"")
|
|
|> Jason.decode!()
|
|
|> Map.fetch!("datasets")
|
|
end
|
|
|
|
defp dataset_y_values(datasets, label) do
|
|
datasets
|
|
|> Enum.find(&(Map.get(&1, "label") == label))
|
|
|> Map.fetch!("data")
|
|
|> Enum.map(&Map.fetch!(&1, "y"))
|
|
end
|
|
|
|
defp dataset_fill(datasets, label) do
|
|
datasets
|
|
|> Enum.find(&(Map.get(&1, "label") == label))
|
|
|> Map.get("fill")
|
|
end
|
|
|
|
describe "Time range selection" do
|
|
test "defaults to 24h range", %{conn: conn, device: device, organization: _org} do
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
|
|
|
|
assert html =~ "24 Hours"
|
|
end
|
|
|
|
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 html =~ "1 Hour"
|
|
end
|
|
|
|
test "changes range when user clicks range button", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org
|
|
} do
|
|
{:ok, view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
|
|
|
|
# Initially 24h
|
|
assert html =~ "24 Hours"
|
|
|
|
# Change to 1h
|
|
html =
|
|
view
|
|
|> element("button", "1 Hour")
|
|
|> render_click()
|
|
|
|
assert html =~ "1 Hour"
|
|
end
|
|
|
|
test "preserves interface_id when changing range", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org,
|
|
snmp_device: snmp_device
|
|
} do
|
|
# Create an interface
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_descr: "eth0",
|
|
if_name: "eth0",
|
|
if_oper_status: "up",
|
|
if_speed: 1_000_000_000
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
{:ok, view, _html} =
|
|
live(
|
|
conn,
|
|
~p"/devices/#{device.id}/graph/traffic?interface_id=#{interface.id}"
|
|
)
|
|
|
|
# Change range and verify page still works
|
|
html =
|
|
view
|
|
|> element("button", "1 Hour")
|
|
|> render_click()
|
|
|
|
assert html =~ "1 Hour"
|
|
assert html =~ "Overall Traffic"
|
|
end
|
|
end
|
|
|
|
describe "Temperature graph" do
|
|
test "renders temperature graph page", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org,
|
|
snmp_device: snmp_device
|
|
} do
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.3.6.1.4.1.9.9.13.1.3.1.3.1",
|
|
sensor_descr: "Chassis Temp",
|
|
sensor_unit: "C"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
Snmp.create_sensor_reading(%{
|
|
sensor_id: sensor.id,
|
|
value: 42.0,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/temperature")
|
|
|
|
assert html =~ "Temperature"
|
|
assert html =~ device.name
|
|
end
|
|
end
|
|
|
|
describe "Storage graph" do
|
|
test "renders storage graph page", %{conn: conn, device: device, organization: _org} do
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/storage")
|
|
|
|
assert html =~ "Storage Usage"
|
|
assert html =~ device.name
|
|
end
|
|
end
|
|
|
|
describe "Sensor-specific graph" do
|
|
test "renders graph for specific sensor by ID", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org,
|
|
snmp_device: snmp_device
|
|
} do
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "voltage",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.3.6.1.4.1.9.9.13.1.2.1.3.1",
|
|
sensor_descr: "PSU Voltage",
|
|
sensor_unit: "V"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
Snmp.create_sensor_reading(%{
|
|
sensor_id: sensor.id,
|
|
value: 12.1,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|
|
{:ok, _view, html} =
|
|
live(conn, ~p"/devices/#{device.id}/graph/voltage?sensor_id=#{sensor.id}")
|
|
|
|
assert html =~ "Voltage"
|
|
assert html =~ "PSU Voltage"
|
|
end
|
|
end
|
|
|
|
describe "handle_info events" do
|
|
test "handles device_status_changed", %{conn: conn, device: device, organization: _org} do
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
|
|
|
|
send(view.pid, {:device_status_changed, device.id, :up, 10})
|
|
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles state_sensors_updated", %{conn: conn, device: device, organization: _org} do
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
|
|
|
|
send(view.pid, {:state_sensors_updated, device.id})
|
|
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles neighbors_updated", %{conn: conn, device: device, organization: _org} do
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
|
|
|
|
send(view.pid, {:neighbors_updated, device.id})
|
|
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles live_poll when not in live mode", %{
|
|
conn: conn,
|
|
device: device,
|
|
organization: _org
|
|
} do
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/graph/latency")
|
|
|
|
send(view.pid, :live_poll)
|
|
|
|
assert render(view)
|
|
end
|
|
end
|
|
|
|
describe "access control" do
|
|
test "redirects when device not found", %{conn: conn} do
|
|
fake_id = Ecto.UUID.generate()
|
|
assert {:error, {:live_redirect, %{to: "/devices"}}} = live(conn, ~p"/devices/#{fake_id}/graph/latency")
|
|
end
|
|
end
|
|
end
|