towerops/test/towerops/trace_test.exs
Graham McIntire 8fe1850d8a test: lift coverage 79.78% → 80.92%+ with broad new tests
Adds 100+ new tests across:

- Snmp.Discovery sync_*: mempools/transceivers/printer_supplies/
  entity_physical/save_neighbors/save_arp_entries
- Topology: compute_wireless_stats, compute_rf_link_stats,
  get_topology_for_weathermap, LLDP discovery/list/upsert,
  remove_stale + find_device_by_name fallthroughs
- Trace: account/inventory/access_point assemble_trace + trace_from_device
  inventory→account linking
- DiscoveryWorker: assigned-agent fallback, agent offline mid-discovery,
  device deleted during discovery, perform/1 rescue (34% → 64%)
- Snmp.Profiles.Base: discover_state_sensors/vlans/ipv6_addresses/
  memory_pools/processors + identify_device for many vendors
- ChartBuilders: load_*_chart_data nil/empty + DB-backed sensor charts
- OrgSettingsLive events: apply_snmp/apply_agent/toggle_default/
  toggle_enabled
- DeviceLive.Form events: edit access errors, validate branches,
  test_snmp w/o agent + with v2c/v3 credentials, query param prefill
- DeviceLive.Index: reorder, search, filter_status, force rediscover
- CheckLive.FormComponent: full mount/change_type/validate/save flow
  for http/tcp/dns/ssl + edit/close
2026-05-08 09:11:06 -05:00

450 lines
14 KiB
Elixir

defmodule Towerops.TraceTest do
use Towerops.DataCase, async: true
import Towerops.AccountsFixtures
alias Towerops.Gaiia.Account
alias Towerops.Gaiia.BillingSubscription
alias Towerops.Gaiia.InventoryItem
alias Towerops.Preseem.AccessPoint
alias Towerops.Repo
alias Towerops.Trace
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,
location: "Downtown"
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router-1",
ip_address: "10.0.0.1",
site_id: site.id,
organization_id: organization.id,
snmp_enabled: true
})
%{organization: organization, site: site, device: device}
end
describe "search/2" do
test "finds sites by name", %{organization: organization} do
results = Trace.search(organization.id, "Tower")
site_results = Enum.filter(results, &(&1.type == :site))
assert length(site_results) == 1
assert hd(site_results).label == "Tower Alpha"
end
test "finds sites by location", %{organization: organization} do
results = Trace.search(organization.id, "Downtown")
site_results = Enum.filter(results, &(&1.type == :site))
assert length(site_results) == 1
end
test "finds devices by name", %{organization: organization} do
results = Trace.search(organization.id, "Router")
device_results = Enum.filter(results, &(&1.type == :device))
assert length(device_results) == 1
assert hd(device_results).label == "Router-1"
end
test "returns empty list for short queries", %{organization: organization} do
assert Trace.search(organization.id, "a") == []
end
end
describe "assemble_trace/3" do
test "assembles trace from site with its devices", %{
organization: organization,
site: site,
device: device
} do
trace = Trace.assemble_trace(organization.id, :site, site.id)
assert trace
assert trace.site.id == site.id
assert trace.site.name == "Tower Alpha"
assert length(trace.devices) == 1
assert hd(trace.devices).id == device.id
end
test "returns nil for non-existent site", %{organization: organization} do
assert Trace.assemble_trace(organization.id, :site, Ecto.UUID.generate()) == nil
end
test "assembles trace from device", %{organization: organization, device: device} do
trace = Trace.assemble_trace(organization.id, :device, device.id)
assert trace
assert trace.device.id == device.id
# No account/sub
assert trace.subscriber == nil
end
test "returns nil for non-existent device", %{organization: organization} do
assert Trace.assemble_trace(organization.id, :device, Ecto.UUID.generate()) == nil
end
test "returns nil for non-existent account", %{organization: organization} do
assert Trace.assemble_trace(organization.id, :account, Ecto.UUID.generate()) == nil
end
test "returns nil for non-existent inventory item", %{organization: organization} do
assert Trace.assemble_trace(organization.id, :inventory_item, Ecto.UUID.generate()) == nil
end
test "returns nil for non-existent access_point", %{organization: organization} do
assert Trace.assemble_trace(organization.id, :access_point, Ecto.UUID.generate()) == nil
end
test "returns nil for unknown type", %{organization: organization} do
assert Trace.assemble_trace(organization.id, :unknown, Ecto.UUID.generate()) == nil
end
end
describe "format_address/1" do
test "returns nil for nil" do
assert Trace.format_address(nil) == nil
end
test "returns nil for empty map" do
assert Trace.format_address(%{}) == nil
end
test "returns nil for non-map" do
assert Trace.format_address("string") == nil
assert Trace.format_address(123) == nil
assert Trace.format_address(:atom) == nil
end
test "joins present components with commas" do
addr = %{"street1" => "123 Main", "city" => "Springfield", "state" => "IL", "zip" => "62701"}
assert "123 Main, Springfield, IL, 62701" == Trace.format_address(addr)
end
test "prefers street1 over line1 and state over province" do
addr = %{"street1" => "first", "line1" => "alt", "state" => "S", "province" => "P"}
result = Trace.format_address(addr)
assert String.contains?(result, "first")
refute String.contains?(result, "alt")
assert String.contains?(result, "S")
end
test "falls back to line1 when street1 is missing" do
addr = %{"line1" => "fallback", "city" => "City"}
assert "fallback, City" == Trace.format_address(addr)
end
test "falls back to province/postal_code when state/zip absent" do
addr = %{"city" => "Montreal", "province" => "QC", "postal_code" => "H3A"}
assert "Montreal, QC, H3A" == Trace.format_address(addr)
end
test "trims whitespace-only as blank" do
addr = %{"street1" => " ", "city" => "Real"}
assert "Real" == Trace.format_address(addr)
end
test "skips nil components" do
addr = %{"street1" => nil, "city" => "Only"}
assert "Only" == Trace.format_address(addr)
end
test "handles line2/street2 (included when set)" do
addr = %{"street1" => "a", "street2" => "b", "city" => "c"}
assert "a, b, c" == Trace.format_address(addr)
end
end
describe "assemble_trace/3 — account/inventory/access_point happy paths" do
setup %{organization: organization, device: device} do
gaiia_id = "gaiia_acc_#{System.unique_integer([:positive])}"
{:ok, account} =
%Account{}
|> Account.changeset(%{
organization_id: organization.id,
gaiia_id: gaiia_id,
readable_id: "ACC-1234",
name: "Alice Subscriber",
status: "active",
mrr: Decimal.new("89.99"),
address: %{"street1" => "1 Test St", "city" => "Townsville"}
})
|> Repo.insert()
{:ok, subscription} =
%BillingSubscription{}
|> BillingSubscription.changeset(%{
organization_id: organization.id,
gaiia_id: "sub_#{System.unique_integer([:positive])}",
account_gaiia_id: gaiia_id,
status: "active",
product_name: "Gigabit Plan",
mrr_amount: Decimal.new("89.99"),
speed_download: 1000,
speed_upload: 100
})
|> Repo.insert()
{:ok, item} =
%InventoryItem{}
|> InventoryItem.changeset(%{
organization_id: organization.id,
gaiia_id: "item_#{System.unique_integer([:positive])}",
name: "Customer CPE",
ip_address: "192.168.1.50",
serial_number: "SN-ABC123",
model_name: "Model-X",
assigned_account_gaiia_id: gaiia_id,
device_id: device.id
})
|> Repo.insert()
{:ok, access_point} =
%AccessPoint{}
|> AccessPoint.changeset(%{
organization_id: organization.id,
preseem_id: "ap_#{System.unique_integer([:positive])}",
name: "Sector-A",
ip_address: "10.0.1.1",
model: "PMP-450",
qoe_score: 4.5,
capacity_score: 3.9,
rf_score: 4.2,
subscriber_count: 12,
airtime_utilization: 0.5,
device_id: device.id
})
|> Repo.insert()
%{
account: account,
subscription: subscription,
inventory_item: item,
access_point: access_point
}
end
test "assembles trace from account with subscriptions and inventory", %{
organization: organization,
account: account,
device: device,
access_point: access_point
} do
trace = Trace.assemble_trace(organization.id, :account, account.id)
assert trace
assert trace.subscriber.name == "Alice Subscriber"
assert trace.subscriber.account_id == "ACC-1234"
assert trace.subscriber.plan == "Gigabit Plan"
assert trace.subscriber.subscription_count == 1
assert trace.subscriber.speed_download == 1000
assert trace.subscriber.address =~ "Townsville"
assert length(trace.inventory_items) == 1
assert trace.device.id == device.id
assert trace.access_point.id == access_point.id
assert trace.qoe_metrics.qoe_score == 4.5
assert trace.qoe_metrics.subscriber_count == 12
assert trace.peer_impact.subscriber_count == 12
assert trace.peer_impact.peer_ap_count == 0
assert trace.alerts == []
assert trace.config_changes == []
assert trace.insights == []
end
test "assembles trace from inventory_item with linked account", %{
organization: organization,
inventory_item: item,
account: account,
device: device
} do
trace = Trace.assemble_trace(organization.id, :inventory_item, item.id)
assert trace
assert trace.subscriber.name == account.name
assert trace.device.id == device.id
assert trace.access_point
refute trace.inventory_items == []
end
test "assembles trace from inventory_item with no linked account", %{
organization: organization,
device: device
} do
{:ok, orphan} =
%InventoryItem{}
|> InventoryItem.changeset(%{
organization_id: organization.id,
gaiia_id: "item_orphan_#{System.unique_integer([:positive])}",
name: "Orphan CPE",
device_id: device.id
})
|> Repo.insert()
trace = Trace.assemble_trace(organization.id, :inventory_item, orphan.id)
assert trace
assert trace.subscriber == nil
# falls back to wrapping the item itself in inventory_items
assert Enum.any?(trace.inventory_items, &(&1.id == orphan.id))
assert trace.device.id == device.id
end
test "assembles trace from access_point", %{
organization: organization,
access_point: ap,
device: device
} do
trace = Trace.assemble_trace(organization.id, :access_point, ap.id)
assert trace
assert trace.access_point.id == ap.id
assert trace.device.id == device.id
assert trace.subscriber == nil
assert trace.inventory_items == []
assert trace.qoe_metrics.qoe_score == 4.5
end
test "assembles trace from access_point with no device", %{organization: organization} do
{:ok, ap} =
%AccessPoint{}
|> AccessPoint.changeset(%{
organization_id: organization.id,
preseem_id: "ap_nodev_#{System.unique_integer([:positive])}",
name: "Detached AP"
})
|> Repo.insert()
trace = Trace.assemble_trace(organization.id, :access_point, ap.id)
assert trace
assert trace.access_point.id == ap.id
assert trace.device == nil
assert trace.peer_impact.peer_ap_count == 0
end
test "trace_from_device finds account through inventory", %{
organization: organization,
device: device,
account: account
} do
trace = Trace.assemble_trace(organization.id, :device, device.id)
assert trace
assert trace.subscriber
assert trace.subscriber.name == account.name
refute trace.inventory_items == []
end
test "peer_impact counts other APs on the same device", %{
organization: organization,
access_point: ap,
device: device
} do
{:ok, _peer} =
%AccessPoint{}
|> AccessPoint.changeset(%{
organization_id: organization.id,
preseem_id: "ap_peer_#{System.unique_integer([:positive])}",
name: "Sector-B",
device_id: device.id
})
|> Repo.insert()
trace = Trace.assemble_trace(organization.id, :access_point, ap.id)
assert trace.peer_impact.peer_ap_count == 1
assert length(trace.peer_impact.peer_aps) == 1
end
test "search finds accounts by name", %{organization: organization} do
results = Trace.search(organization.id, "Alice")
account_results = Enum.filter(results, &(&1.type == :account))
assert length(account_results) == 1
assert hd(account_results).label == "Alice Subscriber"
end
test "search finds inventory by serial", %{organization: organization} do
results = Trace.search(organization.id, "SN-ABC")
inventory_results = Enum.filter(results, &(&1.type == :inventory_item))
assert length(inventory_results) == 1
end
test "search finds access_points by name", %{organization: organization} do
results = Trace.search(organization.id, "Sector")
ap_results = Enum.filter(results, &(&1.type == :access_point))
refute ap_results == []
end
end
describe "trace_from_device with no inventory items" do
test "returns nil subscriber when device has no inventory link", %{
organization: organization,
device: device
} do
trace = Trace.assemble_trace(organization.id, :device, device.id)
assert trace
assert trace.subscriber == nil
assert trace.inventory_items == []
end
test "returns nil subscriber when inventory exists but has no assigned account", %{
organization: organization,
device: device
} do
{:ok, _orphan} =
%InventoryItem{}
|> InventoryItem.changeset(%{
organization_id: organization.id,
gaiia_id: "orphan_inv_#{System.unique_integer([:positive])}",
device_id: device.id,
name: "Unassigned"
})
|> Repo.insert()
trace = Trace.assemble_trace(organization.id, :device, device.id)
assert trace
assert trace.subscriber == nil
assert length(trace.inventory_items) == 1
end
end
describe "search/2 (edge cases)" do
test "returns empty list for whitespace query", %{organization: organization} do
assert Trace.search(organization.id, " ") == []
assert Trace.search(organization.id, "\t\n") == []
end
test "matches device by IP", %{organization: organization} do
results = Trace.search(organization.id, "10.0.0")
device_results = Enum.filter(results, &(&1.type == :device))
refute device_results == []
end
test "caps total results at 25", %{organization: organization, site: site} do
for i <- 1..30 do
Towerops.DevicesFixtures.device_fixture(%{
name: "Queryable-#{i}",
ip_address: "10.99.0.#{i}",
site_id: site.id,
organization_id: organization.id
})
end
results = Trace.search(organization.id, "Queryable")
assert Enum.count(results) <= 25
end
end
end