towerops/test/towerops/capacity_test.exs

263 lines
7.8 KiB
Elixir

defmodule Towerops.CapacityTest do
use Towerops.DataCase
import Towerops.AccountsFixtures
alias Towerops.Capacity
alias Towerops.Snmp.Device, as: SnmpDevice
alias Towerops.Snmp.Interface
alias Towerops.Snmp.InterfaceStat
setup do
user = user_fixture()
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test ISP"}, user.id)
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Tower Alpha",
organization_id: organization.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "PTP Radio",
ip_address: "10.0.0.1",
snmp_enabled: true,
snmp_version: "2c",
snmp_community: "public",
site_id: site.id,
organization_id: organization.id
})
snmp_device =
%SnmpDevice{}
|> SnmpDevice.changeset(%{device_id: device.id, sys_name: "radio", sys_descr: "Cambium PTP"})
|> Repo.insert!()
interface =
%Interface{}
|> Interface.changeset(%{
snmp_device_id: snmp_device.id,
if_index: 1,
if_name: "eth0",
if_speed: 1_000_000_000,
if_type: 6,
configured_capacity_bps: 300_000_000,
capacity_source: "manual"
})
|> Repo.insert!()
%{
organization: organization,
site: site,
device: device,
snmp_device: snmp_device,
interface: interface
}
end
describe "calculate_throughput/3" do
test "calculates throughput from interface stats", %{interface: interface} do
now = DateTime.utc_now()
t1 = DateTime.add(now, -120, :second)
t2 = DateTime.add(now, -60, :second)
insert_interface_stat(interface.id, %{
if_in_octets: 1_000_000,
if_out_octets: 2_000_000,
checked_at: t1
})
insert_interface_stat(interface.id, %{
if_in_octets: 1_500_000,
if_out_octets: 3_000_000,
checked_at: t2
})
result = Capacity.calculate_throughput(interface.id, DateTime.add(now, -180, :second))
# (500_000 bytes * 8) / 60 seconds = 66_666.67 bps
assert_in_delta result.in_bps, 66_666.67, 1.0
# (1_000_000 bytes * 8) / 60 seconds = 133_333.33 bps
assert_in_delta result.out_bps, 133_333.33, 1.0
assert_in_delta result.max_bps, 133_333.33, 1.0
end
test "clamps negative deltas to zero (counter wrap)", %{interface: interface} do
now = DateTime.utc_now()
t1 = DateTime.add(now, -120, :second)
t2 = DateTime.add(now, -60, :second)
insert_interface_stat(interface.id, %{
if_in_octets: 5_000_000,
if_out_octets: 5_000_000,
checked_at: t1
})
# Counter wrapped - lower value than previous
insert_interface_stat(interface.id, %{
if_in_octets: 1_000_000,
if_out_octets: 6_000_000,
checked_at: t2
})
result = Capacity.calculate_throughput(interface.id, DateTime.add(now, -180, :second))
# Negative delta clamped to 0
assert result.in_bps == 0.0
assert_in_delta result.out_bps, 133_333.33, 1.0
end
test "returns zero throughput when no stats exist", %{interface: interface} do
result = Capacity.calculate_throughput(interface.id, DateTime.add(DateTime.utc_now(), -3600, :second))
assert result.in_bps == 0.0
assert result.out_bps == 0.0
assert result.max_bps == 0.0
end
test "returns zero throughput with only one stat point", %{interface: interface} do
insert_interface_stat(interface.id, %{
if_in_octets: 1_000_000,
if_out_octets: 2_000_000,
checked_at: DateTime.utc_now()
})
result = Capacity.calculate_throughput(interface.id, DateTime.add(DateTime.utc_now(), -3600, :second))
assert result.in_bps == 0.0
assert result.out_bps == 0.0
end
end
describe "get_utilization/1" do
test "calculates utilization percentage", %{interface: interface} do
now = DateTime.utc_now()
insert_interface_stat(interface.id, %{
if_in_octets: 0,
if_out_octets: 0,
checked_at: DateTime.add(now, -120, :second)
})
# 300 Mbps capacity, sending at ~150 Mbps = 50% utilization
# 150 Mbps = 18_750_000 bytes/sec * 60 sec = 1_125_000_000 bytes
insert_interface_stat(interface.id, %{
if_in_octets: 0,
if_out_octets: 1_125_000_000,
checked_at: DateTime.add(now, -60, :second)
})
result = Capacity.get_utilization(interface)
assert result
assert_in_delta result.utilization_pct, 50.0, 1.0
end
test "returns nil when interface has no configured capacity", %{snmp_device: snmp_device} do
interface =
%Interface{}
|> Interface.changeset(%{
snmp_device_id: snmp_device.id,
if_index: 99,
if_name: "lo0"
})
|> Repo.insert!()
assert nil == Capacity.get_utilization(interface)
end
end
describe "get_site_capacity_summary/1" do
test "aggregates capacity across interfaces at a site", %{
site: site,
snmp_device: _snmp_device,
interface: interface
} do
now = DateTime.utc_now()
# Add stats for existing interface (300 Mbps capacity)
insert_interface_stat(interface.id, %{
if_in_octets: 0,
if_out_octets: 0,
checked_at: DateTime.add(now, -120, :second)
})
insert_interface_stat(interface.id, %{
if_in_octets: 0,
if_out_octets: 1_125_000_000,
checked_at: DateTime.add(now, -60, :second)
})
# Add a second device with capacity at the same site
{:ok, device2} =
Towerops.Devices.create_device(%{
name: "Second Radio",
ip_address: "10.0.0.2",
snmp_enabled: true,
snmp_version: "2c",
snmp_community: "public",
site_id: site.id,
organization_id: site.organization_id
})
snmp_device2 =
%SnmpDevice{}
|> SnmpDevice.changeset(%{device_id: device2.id, sys_name: "radio2", sys_descr: "Test"})
|> Repo.insert!()
interface2 =
%Interface{}
|> Interface.changeset(%{
snmp_device_id: snmp_device2.id,
if_index: 1,
if_name: "eth0",
configured_capacity_bps: 500_000_000,
capacity_source: "sensor"
})
|> Repo.insert!()
insert_interface_stat(interface2.id, %{
if_in_octets: 0,
if_out_octets: 0,
checked_at: DateTime.add(now, -120, :second)
})
insert_interface_stat(interface2.id, %{
if_in_octets: 0,
if_out_octets: 0,
checked_at: DateTime.add(now, -60, :second)
})
summary = Capacity.get_site_capacity_summary(site.id)
# 300M + 500M = 800M total capacity
assert summary.total_capacity_bps == 800_000_000
assert length(summary.interfaces) == 2
assert summary.utilization_pct >= 0
end
test "returns empty summary when no interfaces have capacity", %{organization: organization} do
{:ok, empty_site} =
Towerops.Sites.create_site(%{name: "Empty Site", organization_id: organization.id})
summary = Capacity.get_site_capacity_summary(empty_site.id)
assert summary.total_capacity_bps == 0
assert summary.interfaces == []
end
end
describe "get_organization_capacity_summary/1" do
test "returns per-site capacity summaries", %{organization: organization, site: site} do
summaries = Capacity.get_organization_capacity_summary(organization.id)
assert summaries != []
site_summary = Enum.find(summaries, &(&1.site_id == site.id))
assert site_summary
assert site_summary.total_capacity_bps == 300_000_000
end
end
defp insert_interface_stat(interface_id, attrs) do
%InterfaceStat{}
|> InterfaceStat.changeset(Map.put(attrs, :interface_id, interface_id))
|> Repo.insert!()
end
end