187 lines
5.5 KiB
Elixir
187 lines
5.5 KiB
Elixir
defmodule Towerops.Preseem.FleetIntelligenceTest do
|
|
use Towerops.DataCase, async: true
|
|
|
|
import Towerops.AccountsFixtures
|
|
import Towerops.DevicesFixtures
|
|
import Towerops.OrganizationsFixtures
|
|
|
|
alias Towerops.Preseem.AccessPoint
|
|
alias Towerops.Preseem.FleetIntelligence
|
|
alias Towerops.Preseem.FleetProfile
|
|
|
|
setup do
|
|
user = user_fixture()
|
|
org = organization_fixture(user.id)
|
|
%{org: org}
|
|
end
|
|
|
|
describe "compute_fleet_profiles/1" do
|
|
test "computes profiles grouped by model", %{org: org} do
|
|
for i <- 1..3 do
|
|
insert_matched_ap_with_model(org, "Ubiquiti AF5XHD", %{
|
|
subscriber_count: 30 + i * 5,
|
|
qoe_score: 80.0 + i,
|
|
capacity_score: 70.0 + i
|
|
})
|
|
end
|
|
|
|
assert {:ok, 1} = FleetIntelligence.compute_fleet_profiles(org.id)
|
|
|
|
profiles = Repo.all(FleetProfile)
|
|
assert length(profiles) == 1
|
|
profile = hd(profiles)
|
|
assert profile.model == "Ubiquiti AF5XHD"
|
|
assert profile.manufacturer == "Ubiquiti"
|
|
assert profile.device_count == 3
|
|
assert profile.avg_subscribers
|
|
assert profile.avg_qoe_score
|
|
assert profile.avg_capacity_score
|
|
end
|
|
|
|
test "creates separate profiles for different models", %{org: org} do
|
|
for _i <- 1..2 do
|
|
insert_matched_ap_with_model(org, "Ubiquiti AF5XHD", %{
|
|
subscriber_count: 30,
|
|
qoe_score: 80.0
|
|
})
|
|
end
|
|
|
|
for _i <- 1..2 do
|
|
insert_matched_ap_with_model(org, "MikroTik RB4011", %{
|
|
subscriber_count: 20,
|
|
qoe_score: 75.0
|
|
})
|
|
end
|
|
|
|
assert {:ok, 2} = FleetIntelligence.compute_fleet_profiles(org.id)
|
|
|
|
profiles = Repo.all(FleetProfile)
|
|
assert length(profiles) == 2
|
|
|
|
manufacturers = profiles |> Enum.map(& &1.manufacturer) |> Enum.sort()
|
|
assert manufacturers == ["MikroTik", "Ubiquiti"]
|
|
end
|
|
|
|
test "handles empty organization", %{org: org} do
|
|
assert {:ok, 0} = FleetIntelligence.compute_fleet_profiles(org.id)
|
|
end
|
|
|
|
test "skips APs without model", %{org: org} do
|
|
device = device_fixture(%{organization_id: org.id})
|
|
|
|
%AccessPoint{}
|
|
|> AccessPoint.changeset(%{
|
|
organization_id: org.id,
|
|
preseem_id: "ap-no-model-#{System.unique_integer([:positive])}",
|
|
name: "No Model",
|
|
device_id: device.id,
|
|
match_confidence: "auto_ip"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
assert {:ok, 0} = FleetIntelligence.compute_fleet_profiles(org.id)
|
|
end
|
|
|
|
test "skips unmatched APs", %{org: org} do
|
|
%AccessPoint{}
|
|
|> AccessPoint.changeset(%{
|
|
organization_id: org.id,
|
|
preseem_id: "ap-unmatched-#{System.unique_integer([:positive])}",
|
|
name: "Unmatched",
|
|
model: "Ubiquiti AF5XHD"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
assert {:ok, 0} = FleetIntelligence.compute_fleet_profiles(org.id)
|
|
end
|
|
|
|
test "computes capacity ceiling from APs with good QoE", %{org: org} do
|
|
# AP with high subscribers and good QoE - should define ceiling
|
|
insert_matched_ap_with_model(org, "Ubiquiti AF5XHD", %{
|
|
subscriber_count: 50,
|
|
qoe_score: 85.0
|
|
})
|
|
|
|
# AP with even higher subscribers but poor QoE - excluded from ceiling
|
|
insert_matched_ap_with_model(org, "Ubiquiti AF5XHD", %{
|
|
subscriber_count: 80,
|
|
qoe_score: 55.0
|
|
})
|
|
|
|
# AP with low subscribers and good QoE
|
|
insert_matched_ap_with_model(org, "Ubiquiti AF5XHD", %{
|
|
subscriber_count: 20,
|
|
qoe_score: 95.0
|
|
})
|
|
|
|
assert {:ok, 1} = FleetIntelligence.compute_fleet_profiles(org.id)
|
|
|
|
profile = Repo.one!(FleetProfile)
|
|
# Capacity ceiling should be 50 (highest subscriber count with QoE >= 70)
|
|
assert profile.capacity_ceiling == 50
|
|
end
|
|
|
|
test "upserts profiles on recomputation", %{org: org} do
|
|
for _i <- 1..3 do
|
|
insert_matched_ap_with_model(org, "Ubiquiti AF5XHD", %{
|
|
subscriber_count: 30,
|
|
qoe_score: 80.0
|
|
})
|
|
end
|
|
|
|
assert {:ok, 1} = FleetIntelligence.compute_fleet_profiles(org.id)
|
|
assert length(Repo.all(FleetProfile)) == 1
|
|
|
|
# Recompute - should update, not duplicate
|
|
assert {:ok, 1} = FleetIntelligence.compute_fleet_profiles(org.id)
|
|
assert length(Repo.all(FleetProfile)) == 1
|
|
end
|
|
|
|
test "includes performance_data with subscriber range and firmware distribution", %{org: org} do
|
|
insert_matched_ap_with_model(org, "Ubiquiti AF5XHD", %{
|
|
subscriber_count: 20,
|
|
firmware: "v4.3.1"
|
|
})
|
|
|
|
insert_matched_ap_with_model(org, "Ubiquiti AF5XHD", %{
|
|
subscriber_count: 50,
|
|
firmware: "v4.3.1"
|
|
})
|
|
|
|
insert_matched_ap_with_model(org, "Ubiquiti AF5XHD", %{
|
|
subscriber_count: 35,
|
|
firmware: "v4.3.2"
|
|
})
|
|
|
|
assert {:ok, 1} = FleetIntelligence.compute_fleet_profiles(org.id)
|
|
|
|
profile = Repo.one!(FleetProfile)
|
|
assert profile.performance_data["subscriber_range"]["min"] == 20
|
|
assert profile.performance_data["subscriber_range"]["max"] == 50
|
|
assert profile.performance_data["firmware_distribution"] == %{"v4.3.1" => 2, "v4.3.2" => 1}
|
|
end
|
|
end
|
|
|
|
# Helpers
|
|
|
|
defp insert_matched_ap_with_model(org, model, extra_attrs) do
|
|
device = device_fixture(%{organization_id: org.id})
|
|
|
|
attrs =
|
|
Map.merge(
|
|
%{
|
|
organization_id: org.id,
|
|
preseem_id: "ap-#{System.unique_integer([:positive])}",
|
|
name: "AP",
|
|
model: model,
|
|
device_id: device.id,
|
|
match_confidence: "auto_ip"
|
|
},
|
|
extra_attrs
|
|
)
|
|
|
|
%AccessPoint{}
|
|
|> AccessPoint.changeset(attrs)
|
|
|> Repo.insert!()
|
|
end
|
|
end
|