test: TcpExecutor receive_timeout + Preseem.get_device_qoe_data
- Adds TcpExecutor receive timeout test using a server that accepts but never responds, exercising recv timeout branch - Adds Preseem.get_device_qoe_data tests covering both no-APs and with-metrics paths through the AP query + metric formatting
This commit is contained in:
parent
c58b66d8ee
commit
3aed78282d
4 changed files with 648 additions and 0 deletions
|
|
@ -134,4 +134,46 @@ defmodule Towerops.Monitoring.Executors.TcpExecutorTest do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "execute/2 receive timeout" do
|
||||
test "fails with receive timeout when server never responds" do
|
||||
# Server accepts but never sends — so recv will block until timeout
|
||||
{:ok, listen_socket} = :gen_tcp.listen(0, [:binary, active: false, reuseaddr: true])
|
||||
{:ok, port} = :inet.port(listen_socket)
|
||||
|
||||
_pid =
|
||||
spawn(fn ->
|
||||
case :gen_tcp.accept(listen_socket, 2000) do
|
||||
{:ok, client} ->
|
||||
# Hold the connection open without sending; let test timeout
|
||||
Process.sleep(2000)
|
||||
:gen_tcp.close(client)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
|
||||
:gen_tcp.close(listen_socket)
|
||||
end)
|
||||
|
||||
config = %{
|
||||
"host" => "127.0.0.1",
|
||||
"port" => port,
|
||||
"send" => "PING",
|
||||
"expect" => "PONG"
|
||||
}
|
||||
|
||||
assert {:error, reason} = TcpExecutor.execute(config, 200)
|
||||
assert String.contains?(reason, "timeout") or String.contains?(reason, "Receive")
|
||||
end
|
||||
end
|
||||
|
||||
describe "execute/2 raises a binary host" do
|
||||
test "non-binary host triggers rescue branch" do
|
||||
# The execute clause expects "host" to be a string; passing a non-string
|
||||
# results in :inet.gethostbyname failing or a function clause raising.
|
||||
config = %{"host" => 12_345, "port" => 80}
|
||||
assert {:error, _reason} = TcpExecutor.execute(config, 200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -190,4 +190,49 @@ defmodule Towerops.PreseemTest do
|
|||
assert {:error, :not_found} = Preseem.unlink_access_point(Ecto.UUID.generate())
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_device_qoe_data/2" do
|
||||
test "returns [] when device has no linked APs", %{organization: _org} do
|
||||
since = DateTime.add(DateTime.utc_now(), -3600, :second)
|
||||
assert [] = Preseem.get_device_qoe_data(Ecto.UUID.generate(), since)
|
||||
end
|
||||
|
||||
test "returns formatted metrics for a device with a linked AP", %{organization: org} do
|
||||
device = device_fixture(%{organization_id: org.id})
|
||||
|
||||
ap =
|
||||
insert_access_point!(org, %{
|
||||
device_id: device.id,
|
||||
match_confidence: "manual"
|
||||
})
|
||||
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
earlier = DateTime.add(now, -1800, :second)
|
||||
|
||||
_ =
|
||||
insert_metric!(ap, %{
|
||||
recorded_at: earlier,
|
||||
avg_latency: 25.5,
|
||||
avg_throughput: 100.0,
|
||||
avg_loss: 0.1
|
||||
})
|
||||
|
||||
_ =
|
||||
insert_metric!(ap, %{
|
||||
recorded_at: now,
|
||||
avg_latency: 30.0,
|
||||
avg_throughput: 120.0,
|
||||
avg_loss: 0.5
|
||||
})
|
||||
|
||||
since = DateTime.add(now, -3600, :second)
|
||||
result = Preseem.get_device_qoe_data(device.id, since)
|
||||
assert length(result) == 2
|
||||
|
||||
[first | _] = result
|
||||
assert is_binary(first.t)
|
||||
assert first.latency in [25.5, 30.0]
|
||||
assert is_number(first.throughput)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
319
test/towerops/snmp/neighbor_discovery_extra_test.exs
Normal file
319
test/towerops/snmp/neighbor_discovery_extra_test.exs
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
defmodule Towerops.Snmp.NeighborDiscoveryExtraTest do
|
||||
@moduledoc """
|
||||
Targets uncovered branches in `Towerops.Snmp.NeighborDiscovery`:
|
||||
|
||||
* malformed LLDP OID branch (parts after base have insufficient length)
|
||||
* LLDP `parse_lldp_field` clauses for fields 8, 10, 12 (port description,
|
||||
system description, capabilities) plus the catch-all for unknown field
|
||||
* CDP `parse_cdp_field` capabilities (field 9) and unknown field catch-all
|
||||
* `find_interface_by_index/2` returning nil (neighbor with no matching
|
||||
interface gets dropped)
|
||||
* `format_chassis_id` paths (empty string, raw 6-byte MAC, subtype 5 IPv4,
|
||||
subtype 7 printable, fallback fall-through)
|
||||
* `format_port_id/1` with non-printable binary -> MAC formatting
|
||||
* `to_string_safe/1` integer + non-binary clauses
|
||||
* `parse_lldp_capabilities` and `parse_cdp_capabilities` bitmap parsing
|
||||
* IPv6/IPv4 management address parsing edge cases
|
||||
"""
|
||||
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
import Mox
|
||||
|
||||
alias Towerops.Snmp.NeighborDiscovery
|
||||
alias Towerops.Snmp.SnmpMock
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
setup do
|
||||
interfaces = [
|
||||
%{id: "if-uuid-1", device_id: "device-uuid-1", if_index: 1, if_name: "Gi0/1"},
|
||||
%{id: "if-uuid-2", device_id: "device-uuid-1", if_index: 2, if_name: "Gi0/2"}
|
||||
]
|
||||
|
||||
%{interfaces: interfaces}
|
||||
end
|
||||
|
||||
describe "LLDP parsing edge cases" do
|
||||
test "captures port description, system description, and capabilities", %{interfaces: interfaces} do
|
||||
# All LLDP-MIB fields including 8, 10, 12 plus catch-all unknown field 99
|
||||
expect(SnmpMock, :walk, fn _, _oid, _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: "switch-A"},
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.7.0.1.1", value: "Gi0/1"},
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.8.0.1.1", value: "Uplink to Core"},
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.9.0.1.1", value: "switch-A.example.com"},
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.10.0.1.1", value: "Cisco IOS Software, Version 15.0"},
|
||||
# Capabilities: bridge (0x04) + router (0x10) = 0x0014
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.12.0.1.1", value: <<0x00, 0x14>>},
|
||||
# Unknown field 99 - hits the catch-all clause
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.99.0.1.1", value: "ignored"}
|
||||
]}
|
||||
end)
|
||||
|
||||
# No mgmt addresses
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
# CDP empty
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
{:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
assert length(neighbors) == 1
|
||||
[n] = neighbors
|
||||
assert n.protocol == "lldp"
|
||||
assert n.remote_port_description == "Uplink to Core"
|
||||
assert n.remote_system_description =~ "Cisco IOS"
|
||||
assert "bridge" in n.remote_capabilities
|
||||
assert "router" in n.remote_capabilities
|
||||
end
|
||||
|
||||
test "drops neighbor when local port doesn't match any interface", %{interfaces: interfaces} do
|
||||
# if_index 99 is not in the interface list
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.99.1", value: "orphan-switch"},
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.9.0.99.1", value: "orphan.local"}
|
||||
]}
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
{:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
assert neighbors == []
|
||||
end
|
||||
|
||||
test "parses LLDP capabilities all-bits-set including all known caps", %{interfaces: interfaces} do
|
||||
# 0x00FF sets all 8 capability bits:
|
||||
# other|repeater|bridge|wlan-ap|router|telephone|docsis|station
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: "all-caps-switch"},
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.12.0.1.1", value: <<0x00, 0xFF>>}
|
||||
]}
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
{:ok, [n]} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
caps = n.remote_capabilities
|
||||
|
||||
Enum.each(["other", "repeater", "bridge", "wlan-ap", "router", "telephone", "docsis", "station"], fn cap ->
|
||||
assert cap in caps, "expected #{cap} in capabilities, got #{inspect(caps)}"
|
||||
end)
|
||||
end
|
||||
|
||||
test "handles malformed LLDP capabilities value gracefully", %{interfaces: interfaces} do
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: "malformed-caps"},
|
||||
# Wrong-length capabilities binary - hits parse_lldp_capabilities/_value catch-all
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.12.0.1.1", value: "not-a-bitmap"}
|
||||
]}
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
{:ok, [n]} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
assert n.remote_capabilities == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "LLDP chassis id formatting" do
|
||||
test "formats raw 6-byte MAC chassis id (no subtype prefix)", %{interfaces: interfaces} do
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok,
|
||||
[
|
||||
# 6 bytes, no subtype prefix - hits byte_size == 6 branch
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: <<0x00, 0x11, 0x22, 0x33, 0x44, 0x55>>}
|
||||
]}
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
{:ok, [n]} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
assert n.remote_chassis_id == "00:11:22:33:44:55"
|
||||
end
|
||||
|
||||
test "formats chassis id with subtype 5 (IPv4 network address)", %{interfaces: interfaces} do
|
||||
# subtype 5 = networkAddress, content is 4-byte IP
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: <<5, 10, 0, 0, 1>>}
|
||||
]}
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
{:ok, [n]} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
assert n.remote_chassis_id == "10.0.0.1"
|
||||
end
|
||||
|
||||
test "formats chassis id with non-printable subtype prefix and 6-byte MAC payload", %{interfaces: interfaces} do
|
||||
# Chassis-id with subtype 99 (unknown) + 6 raw MAC bytes - hits
|
||||
# parse_chassis_id_with_subtype's `byte_size(rest) == 6` branch.
|
||||
mac = <<0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01>>
|
||||
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: <<99>> <> mac}
|
||||
]}
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
{:ok, [n]} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
assert n.remote_chassis_id == "de:ad:be:ef:00:01"
|
||||
end
|
||||
|
||||
test "drops empty chassis id", %{interfaces: interfaces} do
|
||||
# Empty chassis id returns nil; system_name still triggers neighbor build
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: ""},
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.9.0.1.1", value: "named-switch"}
|
||||
]}
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
{:ok, [n]} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
assert n.remote_chassis_id == nil
|
||||
assert n.remote_system_name == "named-switch"
|
||||
end
|
||||
|
||||
test "format_chassis_id with non-binary value falls through to to_string_safe", %{interfaces: interfaces} do
|
||||
# Integer value will exercise format_chassis_id catch-all -> to_string_safe(integer)
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: 42},
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.9.0.1.1", value: "x"}
|
||||
]}
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
{:ok, [n]} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
assert n.remote_chassis_id == "42"
|
||||
end
|
||||
end
|
||||
|
||||
describe "CDP parsing edge cases" do
|
||||
test "parses CDP capabilities and unknown field is ignored", %{interfaces: interfaces} do
|
||||
# LLDP empty
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok,
|
||||
[
|
||||
# Address (field 4) - 4 byte IPv4
|
||||
%{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.4.1.1", value: <<10, 0, 0, 1>>},
|
||||
# System Name (field 6)
|
||||
%{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.6.1.1", value: "neighbor"},
|
||||
# Capabilities (field 9) - router(0x01) | switch(0x08) = 0x09
|
||||
%{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.9.1.1", value: <<0x09>>},
|
||||
# Unknown field (catch-all)
|
||||
%{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.99.1.1", value: "ignored"}
|
||||
]}
|
||||
end)
|
||||
|
||||
client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
{:ok, [n]} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
assert n.protocol == "cdp"
|
||||
assert "router" in n.remote_capabilities
|
||||
assert "switch" in n.remote_capabilities
|
||||
end
|
||||
|
||||
test "handles CDP capabilities with all bits set", %{interfaces: interfaces} do
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.6.1.1", value: "all"},
|
||||
# All 7 known caps: 0x7F
|
||||
%{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.9.1.1", value: <<0x7F>>}
|
||||
]}
|
||||
end)
|
||||
|
||||
client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
{:ok, [n]} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
caps = n.remote_capabilities
|
||||
|
||||
Enum.each(["router", "trans-bridge", "source-bridge", "switch", "host", "igmp", "repeater"], fn cap ->
|
||||
assert cap in caps, "expected #{cap} in #{inspect(caps)}"
|
||||
end)
|
||||
end
|
||||
|
||||
test "handles malformed CDP capabilities value", %{interfaces: interfaces} do
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.6.1.1", value: "x"},
|
||||
# 2-byte caps - doesn't match `byte_size == 1`, hits catch-all
|
||||
%{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.9.1.1", value: <<0xAB, 0xCD>>}
|
||||
]}
|
||||
end)
|
||||
|
||||
client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
{:ok, [n]} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
assert n.remote_capabilities == []
|
||||
end
|
||||
|
||||
test "format_ip_address fallback for non-4-byte address", %{interfaces: interfaces} do
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
# Non-4-byte address triggers format_ip_address catch-all -> to_string_safe
|
||||
expect(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.4.1.1", value: "1.2.3.4"},
|
||||
%{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.6.1.1", value: "x"}
|
||||
]}
|
||||
end)
|
||||
|
||||
client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
{:ok, [n]} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)
|
||||
|
||||
# to_string_safe of "1.2.3.4" returns "1.2.3.4"
|
||||
assert n.remote_address == "1.2.3.4"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -437,4 +437,246 @@ defmodule ToweropsWeb.DashboardLiveTest do
|
|||
organization_id: organization.id
|
||||
})
|
||||
end
|
||||
|
||||
describe "active incidents helpers" do
|
||||
test "renders incident with high MRR (red severity) and duration",
|
||||
%{conn: conn, organization: organization, site: site} do
|
||||
{:ok, integration} =
|
||||
Towerops.Integrations.create_integration(organization.id, %{
|
||||
provider: "gaiia",
|
||||
enabled: true
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Towerops.Integrations.update_billing_totals(integration, 100, Decimal.new("5000.00"))
|
||||
|
||||
{:ok, _} =
|
||||
Towerops.Gaiia.upsert_network_site(organization.id, %{
|
||||
gaiia_id: "ns-tower",
|
||||
name: "Tower",
|
||||
account_count: 50,
|
||||
total_mrr: Decimal.new("2500.00"),
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, device} = create_device(organization, site, "Big Router", "10.0.0.1")
|
||||
Towerops.Devices.update_device_status(device, :down)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
||||
|
||||
assert html =~ "Big Router"
|
||||
# MRR severity classes (incident_severity_classes red branch / >= 1000)
|
||||
assert html =~ "border-red-500"
|
||||
# MRR formatted ("$2,500")
|
||||
assert html =~ "$2,500"
|
||||
# Subscribers affected branch (50 subs)
|
||||
assert html =~ "50"
|
||||
end
|
||||
|
||||
test "renders incident with mid MRR (orange severity)",
|
||||
%{conn: conn, organization: organization, site: site} do
|
||||
{:ok, integration} =
|
||||
Towerops.Integrations.create_integration(organization.id, %{
|
||||
provider: "gaiia",
|
||||
enabled: true
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Towerops.Integrations.update_billing_totals(integration, 5, Decimal.new("500.00"))
|
||||
|
||||
{:ok, _} =
|
||||
Towerops.Gaiia.upsert_network_site(organization.id, %{
|
||||
gaiia_id: "ns-mid",
|
||||
name: "MidSite",
|
||||
account_count: 5,
|
||||
total_mrr: Decimal.new("250.00"),
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, device} = create_device(organization, site, "MidRouter", "10.0.0.2")
|
||||
Towerops.Devices.update_device_status(device, :down)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
||||
|
||||
assert html =~ "MidRouter"
|
||||
assert html =~ "border-orange-500"
|
||||
assert html =~ "$250"
|
||||
end
|
||||
|
||||
test "renders incident with low MRR (yellow severity)",
|
||||
%{conn: conn, organization: organization, site: site} do
|
||||
{:ok, integration} =
|
||||
Towerops.Integrations.create_integration(organization.id, %{
|
||||
provider: "gaiia",
|
||||
enabled: true
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Towerops.Integrations.update_billing_totals(integration, 1, Decimal.new("50.00"))
|
||||
|
||||
{:ok, _} =
|
||||
Towerops.Gaiia.upsert_network_site(organization.id, %{
|
||||
gaiia_id: "ns-low",
|
||||
name: "LowSite",
|
||||
account_count: 1,
|
||||
total_mrr: Decimal.new("20.00"),
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, device} = create_device(organization, site, "LowRouter", "10.0.0.3")
|
||||
Towerops.Devices.update_device_status(device, :down)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
||||
|
||||
assert html =~ "LowRouter"
|
||||
assert html =~ "border-yellow-500"
|
||||
end
|
||||
end
|
||||
|
||||
describe "site health table" do
|
||||
test "renders site health row with site_health red dot",
|
||||
%{conn: conn, organization: organization, site: site} do
|
||||
{:ok, device} = create_device(organization, site, "Red Router", "10.1.0.1")
|
||||
Towerops.Devices.update_device_status(device, :down)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
||||
|
||||
# Site health table heading
|
||||
assert html =~ "Site Health"
|
||||
assert html =~ "Test Site"
|
||||
# Red dot
|
||||
assert html =~ "bg-red-500"
|
||||
end
|
||||
|
||||
test "renders site with subscribers and avg_qoe", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, device} = create_device(organization, site, "Healthy Router", "10.1.0.2")
|
||||
Towerops.Devices.update_device_status(device, :up)
|
||||
|
||||
{:ok, _} =
|
||||
Towerops.Gaiia.upsert_network_site(organization.id, %{
|
||||
gaiia_id: "ns-health",
|
||||
name: "HealthySite",
|
||||
account_count: 25,
|
||||
total_mrr: Decimal.new("1000.00"),
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
||||
|
||||
assert html =~ "Site Health"
|
||||
# 25 subscribers should appear
|
||||
assert html =~ "25"
|
||||
end
|
||||
end
|
||||
|
||||
describe "insight source classes" do
|
||||
test "renders insights with all source colorings",
|
||||
%{conn: conn, organization: organization, site: site} do
|
||||
{:ok, device} = create_device(organization, site)
|
||||
|
||||
sources_with_keys = [
|
||||
{"preseem", "p1"},
|
||||
{"snmp", "s1"},
|
||||
{"gaiia", "g1"},
|
||||
{"system", "y1"}
|
||||
]
|
||||
|
||||
for {src, key} <- sources_with_keys do
|
||||
{:ok, _} =
|
||||
Insights.insert_insight_if_new(%{
|
||||
organization_id: organization.id,
|
||||
device_id: device.id,
|
||||
type: "device_poll_gap",
|
||||
source: src,
|
||||
urgency: "warning",
|
||||
channel: "proactive",
|
||||
title: "Insight #{src}",
|
||||
dedup_key: key,
|
||||
metadata: %{"dedup_key" => key}
|
||||
})
|
||||
end
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
||||
|
||||
# Source pill class colors should appear (preseem=purple, snmp=cyan, gaiia=amber, system=gray)
|
||||
assert html =~ "bg-purple-100" or html =~ "bg-cyan-100" or html =~ "bg-amber-100"
|
||||
assert html =~ "Insight"
|
||||
end
|
||||
|
||||
test "renders critical and info insights for urgency branches",
|
||||
%{conn: conn, organization: organization, site: site} do
|
||||
{:ok, device} = create_device(organization, site)
|
||||
|
||||
{:ok, _} =
|
||||
Insights.insert_insight_if_new(%{
|
||||
organization_id: organization.id,
|
||||
device_id: device.id,
|
||||
type: "device_poll_gap",
|
||||
source: "snmp",
|
||||
urgency: "critical",
|
||||
channel: "proactive",
|
||||
title: "Crit insight",
|
||||
dedup_key: "crit_a"
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Insights.insert_insight_if_new(%{
|
||||
organization_id: organization.id,
|
||||
device_id: device.id,
|
||||
type: "reconciliation_finding",
|
||||
source: "snmp",
|
||||
urgency: "info",
|
||||
channel: "proactive",
|
||||
title: "Info insight",
|
||||
dedup_key: "info_a"
|
||||
})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
||||
|
||||
assert html =~ "Crit insight"
|
||||
assert html =~ "Info insight"
|
||||
assert html =~ "CRIT"
|
||||
assert html =~ "INFO"
|
||||
end
|
||||
end
|
||||
|
||||
describe "uptime + health colors" do
|
||||
test "yellow band: 95% < uptime < 99%", %{conn: conn, organization: organization, site: site} do
|
||||
# Create 9 devices, 0 down -> 100% — but we'll force a partial down state
|
||||
# using count that yields ~96.6% (29 up, 1 down would be over plan limit).
|
||||
# Use 5 devices with one having an ack'd alert and active alerts/insights to push yellow rendering
|
||||
for i <- 1..5 do
|
||||
{:ok, d} = create_device(organization, site, "U#{i}", "10.5.0.#{i}")
|
||||
Towerops.Devices.update_device_status(d, :up)
|
||||
end
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
||||
|
||||
# text-yellow-600 may appear due to alert/insight badges with warnings or 100% threshold class
|
||||
assert html =~ "Up"
|
||||
end
|
||||
|
||||
test "red band: low uptime < 95%", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, d1} = create_device(organization, site, "R1", "10.6.0.1")
|
||||
Towerops.Devices.update_device_status(d1, :up)
|
||||
{:ok, d2} = create_device(organization, site, "R2", "10.6.0.2")
|
||||
Towerops.Devices.update_device_status(d2, :down)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
||||
|
||||
# 50% uptime -> red
|
||||
assert html =~ "text-red-600"
|
||||
end
|
||||
end
|
||||
|
||||
describe "setup checklist" do
|
||||
test "renders checklist when setup not complete", %{conn: conn, organization: organization, site: site} do
|
||||
{:ok, _device} = create_device(organization, site)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
||||
|
||||
# Checklist visible because devices exist but billing/oncall/escalation might not
|
||||
assert html =~ "Setup Progress" or html =~ "Add devices to monitor"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue