towerops/test/towerops/preseem_test.exs
Graham McIntire 8d00a04187
feat: add FluxCD image automation, fix Preseem duplicate AP crash
Switch deployment pipeline from GitLab Agent kubectl to FluxCD image
automation. CI now only builds and pushes tagged images to Forgejo
registry; FluxCD detects new tags and updates the deployment manifest.

Fix Ecto.MultipleResultsError on /dashboard when a device has multiple
Preseem access point records by using limit(1) instead of Repo.get_by.
2026-02-14 10:30:12 -06:00

193 lines
5.9 KiB
Elixir

defmodule Towerops.PreseemTest do
use Towerops.DataCase, async: true
import Towerops.AccountsFixtures
import Towerops.DevicesFixtures
import Towerops.OrganizationsFixtures
alias Towerops.Preseem
alias Towerops.Preseem.AccessPoint
alias Towerops.Preseem.SubscriberMetric
setup do
user = user_fixture()
org = organization_fixture(user.id)
%{organization: org}
end
defp insert_access_point!(org, attrs \\ %{}) do
default = %{
organization_id: org.id,
preseem_id: "ap-#{System.unique_integer([:positive])}",
name: "Test AP"
}
{:ok, ap} =
%AccessPoint{}
|> AccessPoint.changeset(Map.merge(default, attrs))
|> Repo.insert()
ap
end
defp insert_metric!(ap, attrs) do
default = %{
preseem_access_point_id: ap.id,
recorded_at: DateTime.truncate(DateTime.utc_now(), :second)
}
{:ok, metric} =
%SubscriberMetric{}
|> SubscriberMetric.changeset(Map.merge(default, attrs))
|> Repo.insert()
metric
end
describe "get_access_point_for_device/1" do
test "returns nil when no AP linked to device" do
assert is_nil(Preseem.get_access_point_for_device(Ecto.UUID.generate()))
end
test "returns AP when linked to device", %{organization: org} do
device = device_fixture(%{organization_id: org.id})
ap =
insert_access_point!(org, %{
device_id: device.id,
match_confidence: "auto_ip"
})
result = Preseem.get_access_point_for_device(device.id)
assert result.id == ap.id
end
test "returns one AP when multiple are linked to same device", %{organization: org} do
device = device_fixture(%{organization_id: org.id})
_ap1 =
insert_access_point!(org, %{
device_id: device.id,
match_confidence: "auto_ip"
})
_ap2 =
insert_access_point!(org, %{
device_id: device.id,
match_confidence: "auto_mac"
})
result = Preseem.get_access_point_for_device(device.id)
assert %AccessPoint{} = result
end
end
describe "list_access_points/1" do
test "returns all APs for an organization ordered by name", %{organization: org} do
insert_access_point!(org, %{name: "Bravo AP"})
insert_access_point!(org, %{name: "Alpha AP"})
aps = Preseem.list_access_points(org.id)
assert length(aps) == 2
assert Enum.map(aps, & &1.name) == ["Alpha AP", "Bravo AP"]
end
test "does not return APs from other organizations", %{organization: org} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
insert_access_point!(org, %{name: "Our AP"})
insert_access_point!(other_org, %{name: "Their AP"})
aps = Preseem.list_access_points(org.id)
assert length(aps) == 1
assert hd(aps).name == "Our AP"
end
end
describe "list_unmatched_access_points/1" do
test "returns only unmatched and ambiguous APs", %{organization: org} do
insert_access_point!(org, %{name: "Matched", match_confidence: "auto_mac"})
insert_access_point!(org, %{name: "Unmatched", match_confidence: "unmatched"})
insert_access_point!(org, %{name: "Ambiguous", match_confidence: "ambiguous"})
insert_access_point!(org, %{name: "Manual", match_confidence: "manual"})
unmatched = Preseem.list_unmatched_access_points(org.id)
assert length(unmatched) == 2
names = unmatched |> Enum.map(& &1.name) |> Enum.sort()
assert names == ["Ambiguous", "Unmatched"]
end
end
describe "list_subscriber_metrics/2" do
test "returns metrics ordered by recorded_at desc", %{organization: org} do
ap = insert_access_point!(org)
now = DateTime.truncate(DateTime.utc_now(), :second)
old = DateTime.add(now, -3600, :second)
insert_metric!(ap, %{recorded_at: old, avg_latency: 10.0})
insert_metric!(ap, %{recorded_at: now, avg_latency: 20.0})
metrics = Preseem.list_subscriber_metrics(ap.id)
assert length(metrics) == 2
assert hd(metrics).avg_latency == 20.0
end
test "respects limit option", %{organization: org} do
ap = insert_access_point!(org)
now = DateTime.truncate(DateTime.utc_now(), :second)
for i <- 1..5 do
insert_metric!(ap, %{
recorded_at: DateTime.add(now, -i, :second),
avg_latency: i * 1.0
})
end
metrics = Preseem.list_subscriber_metrics(ap.id, limit: 2)
assert length(metrics) == 2
end
test "defaults to 100 limit", %{organization: org} do
ap = insert_access_point!(org)
# Just verify the function works with default
metrics = Preseem.list_subscriber_metrics(ap.id)
assert is_list(metrics)
end
end
describe "link_access_point/2" do
test "links AP to device with manual confidence", %{organization: org} do
ap = insert_access_point!(org, %{match_confidence: "unmatched"})
device = device_fixture(%{organization_id: org.id})
assert {:ok, linked} = Preseem.link_access_point(ap.id, device.id)
assert linked.device_id == device.id
assert linked.match_confidence == "manual"
end
test "returns error for non-existent AP" do
assert {:error, :not_found} = Preseem.link_access_point(Ecto.UUID.generate(), Ecto.UUID.generate())
end
end
describe "unlink_access_point/1" do
test "unlinks AP and resets to unmatched", %{organization: org} do
device = device_fixture(%{organization_id: org.id})
ap =
insert_access_point!(org, %{
device_id: device.id,
match_confidence: "manual"
})
assert {:ok, unlinked} = Preseem.unlink_access_point(ap.id)
assert is_nil(unlinked.device_id)
assert unlinked.match_confidence == "unmatched"
end
test "returns error for non-existent AP" do
assert {:error, :not_found} = Preseem.unlink_access_point(Ecto.UUID.generate())
end
end
end