test: CloudflareClient unblock error + transport-failure branches
This commit is contained in:
parent
29c4bf5582
commit
e5c6b32f32
2 changed files with 445 additions and 0 deletions
|
|
@ -99,5 +99,60 @@ defmodule Towerops.Security.CloudflareClientTest do
|
|||
|
||||
assert {:error, :not_configured} = CloudflareClient.unblock_ip("1.2.3.4")
|
||||
end
|
||||
|
||||
test "unblock returns API error string when delete fails with non-2xx" do
|
||||
Req.Test.stub(CloudflareClient, fn conn ->
|
||||
case {conn.method, conn.request_path} do
|
||||
{"GET", "/client/v4/zones/" <> _} ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.send_resp(
|
||||
200,
|
||||
Jason.encode!(%{
|
||||
"result" => [%{"id" => "rule_xyz", "configuration" => %{"value" => "5.6.7.8"}}]
|
||||
})
|
||||
)
|
||||
|
||||
{"DELETE", "/client/v4/zones/" <> _} ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.send_resp(500, Jason.encode!(%{"error" => "internal"}))
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:error, "API returned status 500"} = CloudflareClient.unblock_ip("5.6.7.8")
|
||||
end
|
||||
|
||||
test "unblock returns transport error when delete fails to connect" do
|
||||
Req.Test.stub(CloudflareClient, fn conn ->
|
||||
case {conn.method, conn.request_path} do
|
||||
{"GET", "/client/v4/zones/" <> _} ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.send_resp(
|
||||
200,
|
||||
Jason.encode!(%{
|
||||
"result" => [%{"id" => "rule_xyz", "configuration" => %{"value" => "5.6.7.8"}}]
|
||||
})
|
||||
)
|
||||
|
||||
{"DELETE", _} ->
|
||||
Req.Test.transport_error(conn, :econnrefused)
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:error, _reason} = CloudflareClient.unblock_ip("5.6.7.8")
|
||||
end
|
||||
|
||||
test "unblock returns :not_found when find_rule_by_ip surfaces a non-200 status" do
|
||||
Req.Test.stub(CloudflareClient, fn conn ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.send_resp(403, Jason.encode!(%{"error" => "forbidden"}))
|
||||
end)
|
||||
|
||||
# The error from find_rule_by_ip is propagated as-is by the catch-all
|
||||
assert {:error, _} = CloudflareClient.unblock_ip("5.6.7.8")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
390
test/towerops/workers/device_poller_worker_airfiber_test.exs
Normal file
390
test/towerops/workers/device_poller_worker_airfiber_test.exs
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
defmodule Towerops.Workers.DevicePollerWorkerAirfiberTest do
|
||||
@moduledoc """
|
||||
Coverage for `DevicePollerWorker` AirFiber proprietary-counter detection
|
||||
branches. Covers:
|
||||
* `detect_airfiber_counter_overrides/2` returning `:airfiber`
|
||||
* `detect_airfiber_counter_overrides/2` returning `:airfiber_ltu`
|
||||
* `detect_airfiber_counter_overrides/2` returning `nil` for non-UBNT
|
||||
* `get_airfiber_stats/3` skip-loopback branch (`if_descr in ["lo", "sit0"]`)
|
||||
* `fetch_proprietary_stats/3` happy path
|
||||
|
||||
AirFiber traffic counters live on `1.3.6.1.4.1.41112.*` so we drive the
|
||||
detection by setting the SNMP device's `sys_object_id` accordingly.
|
||||
"""
|
||||
use Towerops.DataCase, async: false
|
||||
|
||||
import Mox
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Organizations
|
||||
alias Towerops.Repo
|
||||
alias Towerops.Sites
|
||||
alias Towerops.Snmp.Device, as: SnmpDevice
|
||||
alias Towerops.Snmp.Interface
|
||||
alias Towerops.Snmp.InterfaceStat
|
||||
alias Towerops.Snmp.SnmpMock
|
||||
alias Towerops.Workers.DevicePollerWorker
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
setup do
|
||||
old_adapter = Application.get_env(:towerops, :snmp_adapter)
|
||||
Application.put_env(:towerops, :snmp_adapter, SnmpMock)
|
||||
|
||||
on_exit(fn ->
|
||||
if old_adapter do
|
||||
Application.put_env(:towerops, :snmp_adapter, old_adapter)
|
||||
else
|
||||
Application.delete_env(:towerops, :snmp_adapter)
|
||||
end
|
||||
end)
|
||||
|
||||
user = user_fixture()
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "AF Org"}, user.id)
|
||||
{:ok, site} = Sites.create_site(%{name: "AF Site", organization_id: organization.id})
|
||||
|
||||
%{organization: organization, site: site}
|
||||
end
|
||||
|
||||
defp create_polled_device(site) do
|
||||
{:ok, device} =
|
||||
Devices.create_device(%{
|
||||
name: "AF Dev #{System.unique_integer([:positive])}",
|
||||
ip_address: "10.5.5.#{Enum.random(2..250)}",
|
||||
site_id: site.id,
|
||||
organization_id: site.organization_id,
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "public",
|
||||
check_interval_seconds: 600
|
||||
})
|
||||
|
||||
Repo.delete_all(Oban.Job)
|
||||
device
|
||||
end
|
||||
|
||||
describe "AirFiber detection - regular AirFiber" do
|
||||
test "uses proprietary counters when LTU probe fails and AirFiber probe succeeds",
|
||||
%{site: site} do
|
||||
device = create_polled_device(site)
|
||||
|
||||
snmp =
|
||||
%SnmpDevice{}
|
||||
|> SnmpDevice.changeset(%{
|
||||
device_id: device.id,
|
||||
sys_name: "af11",
|
||||
sys_descr: "Ubiquiti AirFiber AF11",
|
||||
# UBNT enterprise OID - drives is_ubnt = true
|
||||
sys_object_id: "1.3.6.1.4.1.41112.1.3"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: snmp.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0",
|
||||
if_descr: "ethernet",
|
||||
if_type: 6,
|
||||
if_speed: 1_000_000_000,
|
||||
if_admin_status: "up",
|
||||
if_oper_status: "up",
|
||||
monitored: true
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||||
case oid do
|
||||
# LTU probe fails → falls into AirFiber detection
|
||||
"1.3.6.1.4.1.41112.1.10.1.2.2.0" -> {:error, :no_such_object}
|
||||
# AirFiber probe succeeds (val != nil)
|
||||
"1.3.6.1.4.1.41112.1.3.3.1.1.1" -> {:ok, 1}
|
||||
# AirFiber proprietary counters
|
||||
"1.3.6.1.4.1.41112.1.3.3.1.7.1" -> {:ok, 12_345}
|
||||
"1.3.6.1.4.1.41112.1.3.3.1.6.1" -> {:ok, 54_321}
|
||||
"1.3.6.1.4.1.41112.1.3.3.1.10.1" -> {:ok, 1}
|
||||
"1.3.6.1.4.1.41112.1.3.3.1.11.1" -> {:ok, 2}
|
||||
# Generic interface change-detection OIDs
|
||||
"1.3.6.1.2.1.2.2.1.5.1" -> {:ok, 1_000_000_000}
|
||||
"1.3.6.1.2.1.2.2.1.6.1" -> {:ok, <<0, 17, 34, 51, 68, 85>>}
|
||||
"1.3.6.1.2.1.2.2.1.7.1" -> {:ok, 1}
|
||||
"1.3.6.1.2.1.2.2.1.8.1" -> {:ok, 1}
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
stub(SnmpMock, :get_multiple, fn _, _oids, _ ->
|
||||
{:ok,
|
||||
%{
|
||||
"1.3.6.1.2.1.2.2.1.5.1" => {:integer, 1_000_000_000},
|
||||
"1.3.6.1.2.1.2.2.1.6.1" => {:"OCTET STRING", <<0, 17, 34, 51, 68, 85>>},
|
||||
"1.3.6.1.2.1.2.2.1.7.1" => {:integer, 1},
|
||||
"1.3.6.1.2.1.2.2.1.8.1" => {:integer, 1}
|
||||
}}
|
||||
end)
|
||||
|
||||
stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})
|
||||
|
||||
stat = Repo.one(Ecto.Query.where(InterfaceStat, interface_id: ^interface.id))
|
||||
assert stat
|
||||
# AirFiber counters: rxOctetsOK -> if_in_octets
|
||||
assert stat.if_in_octets == 12_345
|
||||
assert stat.if_out_octets == 54_321
|
||||
end
|
||||
end
|
||||
|
||||
describe "AirFiber detection - LTU variant" do
|
||||
test "uses LTU counters when LTU probe succeeds", %{site: site} do
|
||||
device = create_polled_device(site)
|
||||
|
||||
snmp =
|
||||
%SnmpDevice{}
|
||||
|> SnmpDevice.changeset(%{
|
||||
device_id: device.id,
|
||||
sys_name: "afltu",
|
||||
sys_descr: "Ubiquiti AirFiber LTU",
|
||||
sys_object_id: "1.3.6.1.4.1.41112.1.10"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: snmp.id,
|
||||
if_index: 1,
|
||||
if_name: "eth0",
|
||||
if_descr: "ethernet",
|
||||
if_type: 6,
|
||||
if_speed: 1_000_000_000,
|
||||
if_admin_status: "up",
|
||||
if_oper_status: "up",
|
||||
monitored: true
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||||
case oid do
|
||||
# LTU probe succeeds → :airfiber_ltu
|
||||
"1.3.6.1.4.1.41112.1.10.1.2.2.0" -> {:ok, 100}
|
||||
# LTU traffic counters
|
||||
"1.3.6.1.4.1.41112.1.10.1.6.1.6.0" -> {:ok, 9_876}
|
||||
"1.3.6.1.4.1.41112.1.10.1.6.1.4.0" -> {:ok, 6_789}
|
||||
"1.3.6.1.2.1.2.2.1.5.1" -> {:ok, 1_000_000_000}
|
||||
"1.3.6.1.2.1.2.2.1.6.1" -> {:ok, <<0, 17, 34, 51, 68, 85>>}
|
||||
"1.3.6.1.2.1.2.2.1.7.1" -> {:ok, 1}
|
||||
"1.3.6.1.2.1.2.2.1.8.1" -> {:ok, 1}
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
stub(SnmpMock, :get_multiple, fn _, _, _ ->
|
||||
{:ok,
|
||||
%{
|
||||
"1.3.6.1.2.1.2.2.1.5.1" => {:integer, 1_000_000_000},
|
||||
"1.3.6.1.2.1.2.2.1.6.1" => {:"OCTET STRING", <<0, 17, 34, 51, 68, 85>>},
|
||||
"1.3.6.1.2.1.2.2.1.7.1" => {:integer, 1},
|
||||
"1.3.6.1.2.1.2.2.1.8.1" => {:integer, 1}
|
||||
}}
|
||||
end)
|
||||
|
||||
stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})
|
||||
|
||||
stat = Repo.one(Ecto.Query.where(InterfaceStat, interface_id: ^interface.id))
|
||||
assert stat
|
||||
assert stat.if_in_octets == 9_876
|
||||
assert stat.if_out_octets == 6_789
|
||||
end
|
||||
end
|
||||
|
||||
describe "AirFiber detection - non-UBNT path" do
|
||||
test "skips AirFiber counters and uses standard IF-MIB for non-UBNT device",
|
||||
%{site: site} do
|
||||
device = create_polled_device(site)
|
||||
|
||||
snmp =
|
||||
%SnmpDevice{}
|
||||
|> SnmpDevice.changeset(%{
|
||||
device_id: device.id,
|
||||
sys_name: "cisco01",
|
||||
sys_descr: "Cisco IOS",
|
||||
# Cisco enterprise OID, NOT UBNT
|
||||
sys_object_id: "1.3.6.1.4.1.9.1.1"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: snmp.id,
|
||||
if_index: 1,
|
||||
if_name: "Gi0/0",
|
||||
if_descr: "ethernet",
|
||||
if_type: 6,
|
||||
if_speed: 1_000_000_000,
|
||||
if_admin_status: "up",
|
||||
if_oper_status: "up",
|
||||
monitored: true
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.31.1.1.1.6.1" -> {:ok, 100}
|
||||
"1.3.6.1.2.1.31.1.1.1.10.1" -> {:ok, 200}
|
||||
"1.3.6.1.2.1.2.2.1.14.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.20.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.13.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.19.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.5.1" -> {:ok, 1_000_000_000}
|
||||
"1.3.6.1.2.1.2.2.1.6.1" -> {:ok, <<0, 17, 34, 51, 68, 85>>}
|
||||
"1.3.6.1.2.1.2.2.1.7.1" -> {:ok, 1}
|
||||
"1.3.6.1.2.1.2.2.1.8.1" -> {:ok, 1}
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
stub(SnmpMock, :get_multiple, fn _, _, _ -> {:error, :timeout} end)
|
||||
stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})
|
||||
|
||||
stat = Repo.one(Ecto.Query.where(InterfaceStat, interface_id: ^interface.id))
|
||||
assert stat
|
||||
assert stat.if_in_octets == 100
|
||||
assert stat.if_out_octets == 200
|
||||
end
|
||||
end
|
||||
|
||||
describe "AirFiber detection - LTU probe error then AirFiber probe nil" do
|
||||
test "falls back to standard counters when both LTU and AirFiber probes fail",
|
||||
%{site: site} do
|
||||
device = create_polled_device(site)
|
||||
|
||||
snmp =
|
||||
%SnmpDevice{}
|
||||
|> SnmpDevice.changeset(%{
|
||||
device_id: device.id,
|
||||
sys_name: "ubnt-other",
|
||||
sys_descr: "Ubiquiti something",
|
||||
# Old UBNT enterprise OID
|
||||
sys_object_id: "1.3.6.1.4.1.10002.1"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
interface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: snmp.id,
|
||||
if_index: 1,
|
||||
if_name: "ath0",
|
||||
if_descr: "wireless",
|
||||
if_type: 6,
|
||||
if_speed: 100_000_000,
|
||||
if_admin_status: "up",
|
||||
if_oper_status: "up",
|
||||
monitored: true
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||||
case oid do
|
||||
# LTU probe error
|
||||
"1.3.6.1.4.1.41112.1.10.1.2.2.0" -> {:error, :no_such_object}
|
||||
# AirFiber probe error
|
||||
"1.3.6.1.4.1.41112.1.3.3.1.1.1" -> {:error, :no_such_object}
|
||||
# Standard counters
|
||||
"1.3.6.1.2.1.31.1.1.1.6.1" -> {:ok, 11}
|
||||
"1.3.6.1.2.1.31.1.1.1.10.1" -> {:ok, 22}
|
||||
"1.3.6.1.2.1.2.2.1.14.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.20.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.13.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.19.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.5.1" -> {:ok, 100_000_000}
|
||||
"1.3.6.1.2.1.2.2.1.6.1" -> {:ok, <<0, 17, 34, 51, 68, 85>>}
|
||||
"1.3.6.1.2.1.2.2.1.7.1" -> {:ok, 1}
|
||||
"1.3.6.1.2.1.2.2.1.8.1" -> {:ok, 1}
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
stub(SnmpMock, :get_multiple, fn _, _, _ -> {:error, :timeout} end)
|
||||
stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})
|
||||
|
||||
stat = Repo.one(Ecto.Query.where(InterfaceStat, interface_id: ^interface.id))
|
||||
assert stat
|
||||
assert stat.if_in_octets == 11
|
||||
assert stat.if_out_octets == 22
|
||||
end
|
||||
end
|
||||
|
||||
describe "AirFiber + loopback skip" do
|
||||
test "loopback interface uses standard counters even on AirFiber device",
|
||||
%{site: site} do
|
||||
device = create_polled_device(site)
|
||||
|
||||
snmp =
|
||||
%SnmpDevice{}
|
||||
|> SnmpDevice.changeset(%{
|
||||
device_id: device.id,
|
||||
sys_name: "af-with-lo",
|
||||
sys_descr: "Ubiquiti AirFiber",
|
||||
sys_object_id: "1.3.6.1.4.1.41112.1.3"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
# Two interfaces: lo (skip airfiber) and air0 (use airfiber)
|
||||
lo =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: snmp.id,
|
||||
if_index: 1,
|
||||
if_name: "lo",
|
||||
if_descr: "lo",
|
||||
if_type: 24,
|
||||
if_speed: 0,
|
||||
if_admin_status: "up",
|
||||
if_oper_status: "up",
|
||||
monitored: true
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||||
case oid do
|
||||
# AirFiber detection: LTU probe error, AirFiber probe ok
|
||||
"1.3.6.1.4.1.41112.1.10.1.2.2.0" -> {:error, :no_such_object}
|
||||
"1.3.6.1.4.1.41112.1.3.3.1.1.1" -> {:ok, 1}
|
||||
# Loopback fallback: standard counters
|
||||
"1.3.6.1.2.1.31.1.1.1.6.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.31.1.1.1.10.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.14.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.20.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.13.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.19.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.5.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.6.1" -> {:ok, ""}
|
||||
"1.3.6.1.2.1.2.2.1.7.1" -> {:ok, 1}
|
||||
"1.3.6.1.2.1.2.2.1.8.1" -> {:ok, 1}
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
stub(SnmpMock, :get_multiple, fn _, _, _ -> {:error, :timeout} end)
|
||||
stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
|
||||
assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})
|
||||
|
||||
# The loopback interface should have a stat (from standard counters,
|
||||
# since AirFiber proprietary counters are skipped for "lo").
|
||||
stat = Repo.one(Ecto.Query.where(InterfaceStat, interface_id: ^lo.id))
|
||||
assert stat
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue