towerops/test/towerops/uisp/statistics_sync_integration_test.exs

227 lines
7.7 KiB
Elixir

defmodule Towerops.Uisp.StatisticsSyncIntegrationTest do
@moduledoc """
DB-backed tests for `Towerops.Uisp.StatisticsSync` covering
`recently_polled?/3` and the `sync_statistics/2` orchestration that
shells out through the UISP HTTP client (stubbed via `Req.Test.stub`).
Pure helper functions are exercised in `statistics_sync_test.exs`.
"""
use Towerops.DataCase, async: true
alias Towerops.AccountsFixtures
alias Towerops.DevicesFixtures
alias Towerops.Integrations
alias Towerops.OrganizationsFixtures
alias Towerops.Snmp.Interface
alias Towerops.Snmp.InterfaceStat
alias Towerops.SnmpFixtures
alias Towerops.Uisp.Client
alias Towerops.Uisp.StatisticsSync
defp setup_org_and_integration(extra_creds \\ %{}) do
user = AccountsFixtures.user_fixture()
org = OrganizationsFixtures.organization_fixture(user.id)
creds =
Map.merge(
%{"url" => "https://uisp.example.com/nms/api/v2.1", "api_key" => "test-key"},
extra_creds
)
{:ok, integration} =
Integrations.create_integration(org.id, %{
provider: "uisp",
enabled: true,
credentials: creds
})
{org, integration}
end
defp insert_interface(snmp_device_id, attrs \\ %{}) do
base = %{
snmp_device_id: snmp_device_id,
if_index: System.unique_integer([:positive])
}
%Interface{}
|> Interface.changeset(Map.merge(base, attrs))
|> Repo.insert!()
end
defp insert_interface_stat(interface_id, checked_at, attrs \\ %{}) do
base = %{
interface_id: interface_id,
if_in_octets: 0,
if_out_octets: 0,
if_in_errors: 0,
if_out_errors: 0,
if_in_discards: 0,
if_out_discards: 0,
checked_at: DateTime.truncate(checked_at, :second)
}
%InterfaceStat{}
|> InterfaceStat.changeset(Map.merge(base, attrs))
|> Repo.insert!()
end
describe "recently_polled?/3" do
test "returns false when no readings exist for the device" do
{_org, _integration} = setup_org_and_integration()
device = DevicesFixtures.device_fixture()
snmp = SnmpFixtures.snmp_device_fixture(%{device: device})
_iface = insert_interface(snmp.id)
now = DateTime.utc_now()
# NOTE: StatisticsSync queries Interfaces by `snmp_device_id == device_id`,
# so we mirror that call signature here.
refute StatisticsSync.recently_polled?(snmp.id, "any", now)
end
test "returns true when a reading exists within the dedup window" do
{_org, _integration} = setup_org_and_integration()
device = DevicesFixtures.device_fixture()
snmp = SnmpFixtures.snmp_device_fixture(%{device: device})
iface = insert_interface(snmp.id)
now = DateTime.utc_now()
recent = DateTime.add(now, -30, :second)
_ = insert_interface_stat(iface.id, recent)
assert StatisticsSync.recently_polled?(snmp.id, "any", now)
end
test "returns false when readings exist but only outside the dedup window" do
{_org, _integration} = setup_org_and_integration()
device = DevicesFixtures.device_fixture()
snmp = SnmpFixtures.snmp_device_fixture(%{device: device})
iface = insert_interface(snmp.id)
now = DateTime.utc_now()
old = DateTime.add(now, -10 * 60, :second)
_ = insert_interface_stat(iface.id, old)
refute StatisticsSync.recently_polled?(snmp.id, "any", now)
end
test "returns false for a different device id even if readings exist nearby" do
{_org, _integration} = setup_org_and_integration()
device = DevicesFixtures.device_fixture()
snmp = SnmpFixtures.snmp_device_fixture(%{device: device})
iface = insert_interface(snmp.id)
now = DateTime.utc_now()
_ = insert_interface_stat(iface.id, DateTime.add(now, -10, :second))
other_device = DevicesFixtures.device_fixture()
other_snmp = SnmpFixtures.snmp_device_fixture(%{device: other_device})
refute StatisticsSync.recently_polled?(other_snmp.id, "any", now)
end
end
describe "sync_statistics/2 — empty / error paths" do
test "empty device map returns zero counts without making HTTP calls" do
{_org, integration} = setup_org_and_integration()
assert {:ok, %{inserted: 0, skipped_dedup: 0}} =
StatisticsSync.sync_statistics(integration, %{})
end
test "HTTP errors are swallowed, leaving counts at zero" do
{_org, integration} = setup_org_and_integration()
device = DevicesFixtures.device_fixture()
Req.Test.stub(Client, fn conn ->
conn
|> Plug.Conn.put_status(404)
|> Req.Test.json(%{"error" => "not found"})
end)
assert {:ok, %{inserted: 0, skipped_dedup: 0}} =
StatisticsSync.sync_statistics(integration, %{"uisp-device-1" => device})
end
test "non-list response body is ignored without crashing" do
{_org, integration} = setup_org_and_integration()
device = DevicesFixtures.device_fixture()
Req.Test.stub(Client, fn conn ->
Req.Test.json(conn, %{"unexpected" => "shape"})
end)
assert {:ok, %{inserted: 0, skipped_dedup: 0}} =
StatisticsSync.sync_statistics(integration, %{"uisp-device-1" => device})
end
test "stats with missing/invalid timestamps are counted as skipped_dedup" do
{_org, integration} = setup_org_and_integration()
device = DevicesFixtures.device_fixture()
Req.Test.stub(Client, fn conn ->
Req.Test.json(conn, [
%{"interfaceName" => "eth0", "timestamp" => nil, "receive" => 100, "transmit" => 200},
%{"interfaceName" => "eth1", "timestamp" => "garbage", "receive" => 1, "transmit" => 2}
])
end)
assert {:ok, %{inserted: 0, skipped_dedup: 2}} =
StatisticsSync.sync_statistics(integration, %{"uisp-1" => device})
end
end
describe "sync_statistics/2 — valid stats" do
test "no primary interface (none for device.id) leaves counts at zero" do
# StatisticsSync queries Interfaces by `snmp_device_id == device.id`,
# so an Interface attached to the Device's Snmp.Device id is invisible
# to the lookup — the insert path returns {:error, :no_interface}.
{_org, integration} = setup_org_and_integration()
device = DevicesFixtures.device_fixture()
snmp = SnmpFixtures.snmp_device_fixture(%{device: device})
_iface = insert_interface(snmp.id, %{if_index: 1})
ts = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
Req.Test.stub(Client, fn conn ->
Req.Test.json(conn, [
%{
"interfaceName" => "eth0",
"timestamp" => ts,
"receive" => 1024,
"transmit" => 2048,
"errors" => 5
}
])
end)
assert {:ok, %{inserted: 0, skipped_dedup: 0}} =
StatisticsSync.sync_statistics(integration, %{"uisp-1" => device})
assert Repo.all(InterfaceStat) == []
end
test "non-list response from one device + valid (timestamped) stat from another" do
{_org, integration} = setup_org_and_integration()
d1 = DevicesFixtures.device_fixture()
d2 = DevicesFixtures.device_fixture()
Req.Test.stub(Client, fn conn ->
cond do
String.contains?(conn.request_path, "/devices/uisp-1/statistics") ->
Req.Test.json(conn, %{"unexpected" => "shape"})
String.contains?(conn.request_path, "/devices/uisp-2/statistics") ->
ts = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
Req.Test.json(conn, [%{"timestamp" => ts}])
end
end)
assert {:ok, %{inserted: 0, skipped_dedup: 0}} =
StatisticsSync.sync_statistics(integration, %{
"uisp-1" => d1,
"uisp-2" => d2
})
end
end
end