towerops/test/towerops/gaiia_test.exs

649 lines
22 KiB
Elixir

defmodule Towerops.GaiiaTest do
use Towerops.DataCase, async: true
import Towerops.AccountsFixtures
import Towerops.DevicesFixtures
import Towerops.OrganizationsFixtures
alias Towerops.Gaiia
alias Towerops.Snmp.Device
setup do
user = user_fixture()
org = organization_fixture(user.id)
%{org: org}
end
describe "accounts" do
test "upsert_account/2 creates a new account", %{org: org} do
attrs = %{gaiia_id: "acct-1", name: "Alice", status: "Active"}
assert {:ok, account} = Gaiia.upsert_account(org.id, attrs)
assert account.gaiia_id == "acct-1"
assert account.name == "Alice"
end
test "upsert_account/2 updates existing account", %{org: org} do
attrs = %{gaiia_id: "acct-1", name: "Alice"}
{:ok, _} = Gaiia.upsert_account(org.id, attrs)
{:ok, updated} = Gaiia.upsert_account(org.id, %{gaiia_id: "acct-1", name: "Alice Smith"})
assert updated.name == "Alice Smith"
assert length(Gaiia.list_accounts(org.id)) == 1
end
test "list_accounts/1 returns accounts for organization", %{org: org} do
{:ok, _} = Gaiia.upsert_account(org.id, %{gaiia_id: "acct-1", name: "Alice"})
{:ok, _} = Gaiia.upsert_account(org.id, %{gaiia_id: "acct-2", name: "Bob"})
assert length(Gaiia.list_accounts(org.id)) == 2
end
test "get_account/2 returns account by gaiia_id", %{org: org} do
{:ok, _} = Gaiia.upsert_account(org.id, %{gaiia_id: "acct-1", name: "Alice"})
assert %{name: "Alice"} = Gaiia.get_account(org.id, "acct-1")
assert is_nil(Gaiia.get_account(org.id, "nonexistent"))
end
end
describe "network_sites" do
test "upsert_network_site/2 creates a new site", %{org: org} do
attrs = %{gaiia_id: "site-1", name: "Tower 3", ip_blocks: ["10.0.0.0/24"]}
assert {:ok, site} = Gaiia.upsert_network_site(org.id, attrs)
assert site.gaiia_id == "site-1"
assert site.ip_blocks == ["10.0.0.0/24"]
end
test "upsert_network_site/2 preserves site_id mapping on update", %{org: org} do
{:ok, site} = Gaiia.upsert_network_site(org.id, %{gaiia_id: "site-1", name: "Tower 3"})
assert is_nil(site.site_id)
# Simulate user mapping
towerops_site = create_site(org.id)
Gaiia.update_network_site_mapping(site, %{site_id: towerops_site.id})
# Re-sync should preserve mapping
{:ok, updated} = Gaiia.upsert_network_site(org.id, %{gaiia_id: "site-1", name: "Tower 3 Updated"})
assert updated.name == "Tower 3 Updated"
assert updated.site_id == towerops_site.id
end
test "list_network_sites/1 returns sites for organization", %{org: org} do
{:ok, _} = Gaiia.upsert_network_site(org.id, %{gaiia_id: "site-1", name: "Tower 1"})
assert length(Gaiia.list_network_sites(org.id)) == 1
end
end
describe "inventory_items" do
test "upsert_inventory_item/2 creates a new item", %{org: org} do
attrs = %{gaiia_id: "item-1", name: "AF5XHD", serial_number: "SN123"}
assert {:ok, item} = Gaiia.upsert_inventory_item(org.id, attrs)
assert item.serial_number == "SN123"
end
test "upsert_inventory_item/2 preserves device_id mapping on update", %{org: org} do
{:ok, item} = Gaiia.upsert_inventory_item(org.id, %{gaiia_id: "item-1", name: "AF5XHD"})
assert is_nil(item.device_id)
device = device_fixture(%{organization_id: org.id})
Gaiia.update_inventory_item_mapping(item, %{device_id: device.id})
{:ok, updated} = Gaiia.upsert_inventory_item(org.id, %{gaiia_id: "item-1", name: "AF5XHD v2"})
assert updated.name == "AF5XHD v2"
assert updated.device_id == device.id
end
test "list_inventory_items/1 returns items for organization", %{org: org} do
{:ok, _} = Gaiia.upsert_inventory_item(org.id, %{gaiia_id: "item-1", name: "AF5XHD"})
assert length(Gaiia.list_inventory_items(org.id)) == 1
end
end
describe "billing_subscriptions" do
test "upsert_billing_subscription/2 creates a new subscription", %{org: org} do
attrs = %{gaiia_id: "sub-1", account_gaiia_id: "acct-1", product_name: "100Mbps"}
assert {:ok, sub} = Gaiia.upsert_billing_subscription(org.id, attrs)
assert sub.product_name == "100Mbps"
end
test "list_billing_subscriptions_for_account/2 filters by account", %{org: org} do
{:ok, _} = Gaiia.upsert_billing_subscription(org.id, %{gaiia_id: "sub-1", account_gaiia_id: "acct-1"})
{:ok, _} = Gaiia.upsert_billing_subscription(org.id, %{gaiia_id: "sub-2", account_gaiia_id: "acct-2"})
assert length(Gaiia.list_billing_subscriptions_for_account(org.id, "acct-1")) == 1
end
test "list_billing_subscriptions/1 returns all subscriptions", %{org: org} do
{:ok, _} = Gaiia.upsert_billing_subscription(org.id, %{gaiia_id: "sub-1"})
{:ok, _} = Gaiia.upsert_billing_subscription(org.id, %{gaiia_id: "sub-2"})
assert length(Gaiia.list_billing_subscriptions(org.id)) == 2
end
end
defp create_site(organization_id) do
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site #{System.unique_integer([:positive])}",
organization_id: organization_id
})
site
end
describe "get_org_subscriber_totals/1" do
test "returns zero totals when no network sites", %{org: org} do
assert %{total_subscribers: 0, total_mrr: total_mrr} = Gaiia.get_org_subscriber_totals(org.id)
assert Decimal.equal?(total_mrr, Decimal.new(0))
end
test "sums account_count and total_mrr from network sites", %{org: org} do
{:ok, _} =
Gaiia.upsert_network_site(org.id, %{
gaiia_id: "ns-1",
name: "Tower A",
account_count: 50,
total_mrr: Decimal.new("2500.00")
})
{:ok, _} =
Gaiia.upsert_network_site(org.id, %{
gaiia_id: "ns-2",
name: "Tower B",
account_count: 30,
total_mrr: Decimal.new("1500.00")
})
result = Gaiia.get_org_subscriber_totals(org.id)
assert result.total_subscribers == 80
assert Decimal.equal?(result.total_mrr, Decimal.new("4000.00"))
end
test "handles nil values gracefully", %{org: org} do
{:ok, _} =
Gaiia.upsert_network_site(org.id, %{
gaiia_id: "ns-nil",
name: "Tower Nil",
account_count: nil,
total_mrr: nil
})
result = Gaiia.get_org_subscriber_totals(org.id)
assert result.total_subscribers == 0
assert Decimal.equal?(result.total_mrr, Decimal.new(0))
end
end
describe "get_devices_impact/1" do
test "deduplicates accounts across devices, preferring higher-confidence matches", %{org: org} do
site = create_site(org.id)
# Create two devices: a router and an AP
router = device_fixture(%{organization_id: org.id, site: site, name: "Router"})
ap = device_fixture(%{organization_id: org.id, site: site, name: "AP-1"})
# Create an account with an active subscription
{:ok, account} =
Gaiia.upsert_account(org.id, %{
gaiia_id: "acct-1",
name: "Alice",
subscription_count: 1
})
{:ok, _} =
Gaiia.upsert_billing_subscription(org.id, %{
gaiia_id: "sub-1",
account_gaiia_id: "acct-1",
status: "ACTIVE",
mrr_amount: Decimal.new("50.00")
})
now = Towerops.Time.now()
# Same account linked to AP via wireless_mac (high confidence)
Repo.insert!(%Gaiia.DeviceSubscriberLink{
organization_id: org.id,
device_id: ap.id,
gaiia_account_id: account.id,
match_method: "wireless_mac",
confidence: "high",
subscriber_mac: "aa:bb:cc:dd:ee:ff",
inserted_at: now,
updated_at: now
})
# Same account linked to Router via arp_ip (medium confidence)
Repo.insert!(%Gaiia.DeviceSubscriberLink{
organization_id: org.id,
device_id: router.id,
gaiia_account_id: account.id,
match_method: "arp_ip",
confidence: "medium",
subscriber_ip: "10.0.0.5",
inserted_at: now,
updated_at: now
})
impact = Gaiia.get_devices_impact([router.id, ap.id])
# Account should be counted only on the AP (wireless_mac beats arp_ip)
assert impact[ap.id].subscriber_count == 1
assert Decimal.equal?(impact[ap.id].mrr, Decimal.new("50.00"))
# Router should have 0 subscribers (deduped to AP)
router_impact = Map.get(impact, router.id, %{subscriber_count: 0, mrr: Decimal.new(0)})
assert router_impact.subscriber_count == 0
end
test "keeps ARP match on router when no higher-confidence match exists elsewhere", %{org: org} do
site = create_site(org.id)
router = device_fixture(%{organization_id: org.id, site: site, name: "Router"})
ap = device_fixture(%{organization_id: org.id, site: site, name: "AP-1"})
{:ok, account} =
Gaiia.upsert_account(org.id, %{
gaiia_id: "acct-only-router",
name: "Bob",
subscription_count: 1
})
{:ok, _} =
Gaiia.upsert_billing_subscription(org.id, %{
gaiia_id: "sub-bob",
account_gaiia_id: "acct-only-router",
status: "ACTIVE",
mrr_amount: Decimal.new("75.00")
})
now = Towerops.Time.now()
# Account only linked to router via ARP (no AP match)
Repo.insert!(%Gaiia.DeviceSubscriberLink{
organization_id: org.id,
device_id: router.id,
gaiia_account_id: account.id,
match_method: "arp_ip",
confidence: "medium",
subscriber_ip: "10.0.0.10",
inserted_at: now,
updated_at: now
})
impact = Gaiia.get_devices_impact([router.id, ap.id])
# Router should keep the account since no better match exists
assert impact[router.id].subscriber_count == 1
assert Decimal.equal?(impact[router.id].mrr, Decimal.new("75.00"))
end
test "deduplicates across same-method matches, keeping first device", %{org: org} do
site = create_site(org.id)
router = device_fixture(%{organization_id: org.id, site: site, name: "Router"})
ap = device_fixture(%{organization_id: org.id, site: site, name: "AP-1"})
{:ok, account} =
Gaiia.upsert_account(org.id, %{
gaiia_id: "acct-both-arp",
name: "Charlie",
subscription_count: 1
})
{:ok, _} =
Gaiia.upsert_billing_subscription(org.id, %{
gaiia_id: "sub-charlie",
account_gaiia_id: "acct-both-arp",
status: "ACTIVE",
mrr_amount: Decimal.new("60.00")
})
now = Towerops.Time.now()
# Same account linked to both devices via arp_ip (same method)
Repo.insert!(%Gaiia.DeviceSubscriberLink{
organization_id: org.id,
device_id: ap.id,
gaiia_account_id: account.id,
match_method: "arp_ip",
confidence: "medium",
subscriber_ip: "10.0.0.20",
inserted_at: now,
updated_at: now
})
Repo.insert!(%Gaiia.DeviceSubscriberLink{
organization_id: org.id,
device_id: router.id,
gaiia_account_id: account.id,
match_method: "arp_ip",
confidence: "medium",
subscriber_ip: "100.64.0.20",
inserted_at: now,
updated_at: now
})
impact = Gaiia.get_devices_impact([router.id, ap.id])
# Account should be counted on exactly one device (not both)
router_count = get_in(impact, [router.id, :subscriber_count]) || 0
ap_count = get_in(impact, [ap.id, :subscriber_count]) || 0
assert router_count + ap_count == 1
end
test "excludes links matched through CGNAT IPs (100.64.0.0/10)", %{org: org} do
site = create_site(org.id)
router = device_fixture(%{organization_id: org.id, site: site, name: "Router"})
{:ok, account} =
Gaiia.upsert_account(org.id, %{
gaiia_id: "acct-cgnat",
name: "CGNAT Sub",
subscription_count: 1
})
{:ok, _} =
Gaiia.upsert_billing_subscription(org.id, %{
gaiia_id: "sub-cgnat",
account_gaiia_id: "acct-cgnat",
status: "ACTIVE",
mrr_amount: Decimal.new("50.00")
})
now = Towerops.Time.now()
# Link via CGNAT IP — should be excluded from per-device counts
Repo.insert!(%Gaiia.DeviceSubscriberLink{
organization_id: org.id,
device_id: router.id,
gaiia_account_id: account.id,
match_method: "arp_ip",
confidence: "medium",
subscriber_ip: "100.64.62.50",
inserted_at: now,
updated_at: now
})
impact = Gaiia.get_devices_impact([router.id])
router_impact = Map.get(impact, router.id, %{subscriber_count: 0, mrr: Decimal.new(0)})
assert router_impact.subscriber_count == 0
end
test "keeps links with non-CGNAT IPs", %{org: org} do
site = create_site(org.id)
ap = device_fixture(%{organization_id: org.id, site: site, name: "AP-1"})
{:ok, account} =
Gaiia.upsert_account(org.id, %{
gaiia_id: "acct-lan",
name: "LAN Sub",
subscription_count: 1
})
{:ok, _} =
Gaiia.upsert_billing_subscription(org.id, %{
gaiia_id: "sub-lan",
account_gaiia_id: "acct-lan",
status: "ACTIVE",
mrr_amount: Decimal.new("80.00")
})
now = Towerops.Time.now()
Repo.insert!(%Gaiia.DeviceSubscriberLink{
organization_id: org.id,
device_id: ap.id,
gaiia_account_id: account.id,
match_method: "arp_ip",
confidence: "medium",
subscriber_ip: "10.0.0.5",
inserted_at: now,
updated_at: now
})
impact = Gaiia.get_devices_impact([ap.id])
assert impact[ap.id].subscriber_count == 1
assert Decimal.equal?(impact[ap.id].mrr, Decimal.new("80.00"))
end
end
describe "get_site_impact/1" do
test "excludes router devices from per-device impact", %{org: org} do
site = create_site(org.id)
router = device_fixture(%{organization_id: org.id, site: site, name: "Router"})
ap = device_fixture(%{organization_id: org.id, site: site, name: "AP-1"})
# Mark router as a RouterOS device via SNMP
Repo.insert!(%Device{
device_id: router.id,
sys_descr: "RouterOS CCR1009-7G-1C-1S+",
sys_object_id: "1.3.6.1.4.1.14988.1",
sys_name: "router"
})
{:ok, account} =
Gaiia.upsert_account(org.id, %{
gaiia_id: "acct-r",
name: "Router Sub",
subscription_count: 1
})
{:ok, _} =
Gaiia.upsert_billing_subscription(org.id, %{
gaiia_id: "sub-r",
account_gaiia_id: "acct-r",
status: "ACTIVE",
mrr_amount: Decimal.new("50.00")
})
now = Towerops.Time.now()
# Link to router via LAN IP
Repo.insert!(%Gaiia.DeviceSubscriberLink{
organization_id: org.id,
device_id: router.id,
gaiia_account_id: account.id,
match_method: "arp_ip",
confidence: "medium",
subscriber_ip: "10.0.0.5",
inserted_at: now,
updated_at: now
})
result = Gaiia.get_site_impact(site.id)
# Site totals should exclude router-only subscribers
assert result.subscriber_count == 0
assert Decimal.equal?(result.mrr, Decimal.new(0))
# Router's per-device entry should show 0
router_device = Enum.find(result.devices, &(&1.device_id == router.id))
assert router_device.count == 0
# AP should show 0 (no links to it)
ap_device = Enum.find(result.devices, &(&1.device_id == ap.id))
assert ap_device.count == 0
end
test "site totals equal sum of non-router per-device impacts", %{org: org} do
site = create_site(org.id)
router = device_fixture(%{organization_id: org.id, site: site, name: "Router"})
ap1 = device_fixture(%{organization_id: org.id, site: site, name: "AP-1"})
ap2 = device_fixture(%{organization_id: org.id, site: site, name: "AP-2"})
Repo.insert!(%Device{
device_id: router.id,
sys_descr: "RouterOS CCR1009-7G-1C-1S+",
sys_object_id: "1.3.6.1.4.1.14988.1",
sys_name: "router"
})
{:ok, acct1} =
Gaiia.upsert_account(org.id, %{gaiia_id: "acct-a1", name: "Sub A", subscription_count: 1})
{:ok, acct2} =
Gaiia.upsert_account(org.id, %{gaiia_id: "acct-a2", name: "Sub B", subscription_count: 1})
{:ok, _} =
Gaiia.upsert_billing_subscription(org.id, %{
gaiia_id: "sub-a1",
account_gaiia_id: "acct-a1",
status: "ACTIVE",
mrr_amount: Decimal.new("40.00")
})
{:ok, _} =
Gaiia.upsert_billing_subscription(org.id, %{
gaiia_id: "sub-a2",
account_gaiia_id: "acct-a2",
status: "ACTIVE",
mrr_amount: Decimal.new("60.00")
})
now = Towerops.Time.now()
# acct1 on AP-1 and router (AP wins via wireless_mac)
Repo.insert!(%Gaiia.DeviceSubscriberLink{
organization_id: org.id,
device_id: ap1.id,
gaiia_account_id: acct1.id,
match_method: "wireless_mac",
confidence: "high",
subscriber_mac: "aa:bb:cc:dd:ee:01",
inserted_at: now,
updated_at: now
})
Repo.insert!(%Gaiia.DeviceSubscriberLink{
organization_id: org.id,
device_id: router.id,
gaiia_account_id: acct1.id,
match_method: "arp_ip",
confidence: "medium",
subscriber_ip: "10.0.0.1",
inserted_at: now,
updated_at: now
})
# acct2 on AP-2 only
Repo.insert!(%Gaiia.DeviceSubscriberLink{
organization_id: org.id,
device_id: ap2.id,
gaiia_account_id: acct2.id,
match_method: "arp_ip",
confidence: "medium",
subscriber_ip: "10.0.0.2",
inserted_at: now,
updated_at: now
})
result = Gaiia.get_site_impact(site.id)
# Site totals = AP-1 (1 sub, $40) + AP-2 (1 sub, $60) = 2 subs, $100
assert result.subscriber_count == 2
assert Decimal.equal?(result.mrr, Decimal.new("100.00"))
# Per-device
ap1_dev = Enum.find(result.devices, &(&1.device_id == ap1.id))
assert ap1_dev.count == 1
ap2_dev = Enum.find(result.devices, &(&1.device_id == ap2.id))
assert ap2_dev.count == 1
router_dev = Enum.find(result.devices, &(&1.device_id == router.id))
assert router_dev.count == 0
end
end
describe "get_site_subscriber_summary/1" do
test "returns subscriber data for a mapped site", %{org: org} do
site = create_site(org.id)
{:ok, _} =
Gaiia.upsert_network_site(org.id, %{
gaiia_id: "ns-mapped",
name: "Mapped Tower",
account_count: 42,
total_mrr: Decimal.new("3100.50"),
site_id: site.id
})
result = Gaiia.get_site_subscriber_summary(site.id)
assert result.account_count == 42
assert Decimal.equal?(result.total_mrr, Decimal.new("3100.50"))
end
test "returns nil for unmapped site", %{org: org} do
site = create_site(org.id)
assert is_nil(Gaiia.get_site_subscriber_summary(site.id))
end
end
describe "search_inventory_items/2" do
test "finds items by name (case insensitive)", %{org: org} do
{:ok, _} = Gaiia.upsert_inventory_item(org.id, %{gaiia_id: "item-1", name: "ePMP 3000", ip_address: "10.0.0.1"})
{:ok, _} = Gaiia.upsert_inventory_item(org.id, %{gaiia_id: "item-2", name: "LTU Rocket", ip_address: "10.0.0.2"})
results = Gaiia.search_inventory_items(org.id, "epmp")
assert length(results) == 1
assert hd(results).gaiia_id == "item-1"
end
test "finds items by IP address", %{org: org} do
{:ok, _} = Gaiia.upsert_inventory_item(org.id, %{gaiia_id: "item-1", name: "Device A", ip_address: "192.168.1.50"})
{:ok, _} = Gaiia.upsert_inventory_item(org.id, %{gaiia_id: "item-2", name: "Device B", ip_address: "10.0.0.1"})
results = Gaiia.search_inventory_items(org.id, "192.168.1")
assert length(results) == 1
assert hd(results).gaiia_id == "item-1"
end
test "finds items by model name", %{org: org} do
{:ok, _} = Gaiia.upsert_inventory_item(org.id, %{gaiia_id: "item-1", name: "Unit 1", model_name: "AC1000"})
{:ok, _} = Gaiia.upsert_inventory_item(org.id, %{gaiia_id: "item-2", name: "Unit 2", model_name: "AC2000"})
results = Gaiia.search_inventory_items(org.id, "AC1000")
assert length(results) == 1
assert hd(results).gaiia_id == "item-1"
end
test "returns empty list when no match", %{org: org} do
{:ok, _} = Gaiia.upsert_inventory_item(org.id, %{gaiia_id: "item-1", name: "Device A"})
results = Gaiia.search_inventory_items(org.id, "nonexistent")
assert results == []
end
test "does not return items from other orgs", %{org: org} do
other_user = Towerops.AccountsFixtures.user_fixture()
other_org = Towerops.OrganizationsFixtures.organization_fixture(other_user.id)
{:ok, _} = Gaiia.upsert_inventory_item(other_org.id, %{gaiia_id: "item-x", name: "ePMP 3000"})
results = Gaiia.search_inventory_items(org.id, "epmp")
assert results == []
end
end
describe "search_network_sites/2" do
test "finds network sites by name (case insensitive)", %{org: org} do
{:ok, _} = Gaiia.upsert_network_site(org.id, %{gaiia_id: "ns-1", name: "Tower North"})
{:ok, _} = Gaiia.upsert_network_site(org.id, %{gaiia_id: "ns-2", name: "Tower South"})
results = Gaiia.search_network_sites(org.id, "north")
assert length(results) == 1
assert hd(results).gaiia_id == "ns-1"
end
test "returns empty list when no match", %{org: org} do
{:ok, _} = Gaiia.upsert_network_site(org.id, %{gaiia_id: "ns-1", name: "Tower North"})
results = Gaiia.search_network_sites(org.id, "nothing")
assert results == []
end
test "does not return sites from other orgs", %{org: org} do
other_user = Towerops.AccountsFixtures.user_fixture()
other_org = Towerops.OrganizationsFixtures.organization_fixture(other_user.id)
{:ok, _} = Gaiia.upsert_network_site(other_org.id, %{gaiia_id: "ns-x", name: "Tower North"})
results = Gaiia.search_network_sites(org.id, "north")
assert results == []
end
end
end