towerops/test/support/fixtures/snmp_fixtures.ex
Graham McIntire 6fd03ace16
feat: add comprehensive wireless client tracking and monitoring
Implements real-time wireless client monitoring with historical tracking,
LiveView UI, proactive alerting, and cross-browser e2e tests.

Phase 1: Historical Tracking
- Add TimescaleDB hypertable for wireless_client_readings
- Batch insert client metrics every 60 seconds from DevicePollerWorker
- 90-day retention with compression after 7 days
- Continuous aggregates for hourly (1 year) and daily (5 years) rollups

Phase 2: LiveView UI
- Add wireless tab to device detail page
- Real-time client list with PubSub updates
- Signal strength and SNR badges with 5-level thresholds
- Display MAC, IP, subscriber, TX/RX rates, distance, uptime
- Subscriber matching via device_subscriber_links
- Empty state handling

Phase 3: Proactive Alerting
- WirelessInsightWorker runs every 5 minutes via Oban cron
- 4 insight types with auto-resolution:
  * wireless_signal_weak: < -75 dBm (warning), < -85 dBm (critical)
  * wireless_snr_low: < 15 dB (warning), < 10 dB (critical)
  * wireless_ap_overloaded: > 50 clients (warning), > 75 clients (critical)
  * wireless_client_missing: expected subscribers not connecting
- Hysteresis thresholds prevent alert flapping
- Multi-organization isolation with proper deduplication

Code Quality:
- Refactored reload_current_tab_data to reduce cyclomatic complexity
- Combined double Enum.filter into single pass for efficiency
- Fixed length/1 comparison to use empty list check
- All Credo checks passing

Testing:
- 28 unit tests (ExUnit) - 100% passing
- 15 e2e tests (Playwright) - 100% passing across chromium/firefox/webkit
- Total: 73 tests, all passing

Files changed:
- lib/towerops/workers/wireless_insight_worker.ex (NEW)
- lib/towerops_web/live/device_live/show.ex (wireless tab + refactoring)
- lib/towerops_web/live/device_live/show.html.heex (wireless template)
- lib/towerops/snmp.ex (5 new query functions)
- lib/towerops/gaiia.ex (list_missing_subscribers)
- lib/towerops/preseem/insight.ex (5 new insight types)
- config/runtime.exs (Oban cron schedule)
- test/support/fixtures/snmp_fixtures.ex (NEW)
- test/towerops/workers/wireless_insight_worker_test.exs (NEW)
- test/towerops_web/live/device_live/show_test.exs (9 new tests)
- e2e/tests/wireless-clients.spec.ts (NEW - 15 cross-browser tests)
2026-03-10 09:57:12 -05:00

139 lines
4.2 KiB
Elixir

defmodule Towerops.SnmpFixtures do
@moduledoc """
This module defines test helpers for creating SNMP entities.
"""
alias Towerops.DevicesFixtures
alias Towerops.Organizations
alias Towerops.Snmp
@doc """
Generate an SNMP device with all required fields.
## Options
- `:device` - Existing device to attach SNMP device to
- `:device_id` - ID of device to attach SNMP device to
- All other SNMP Device fields (sys_name, sys_descr, etc.)
## Examples
iex> snmp_device_fixture()
%Snmp.Device{sys_name: "device", ...}
iex> snmp_device_fixture(%{sys_name: "router1"})
%Snmp.Device{sys_name: "router1", ...}
"""
def snmp_device_fixture(attrs \\ %{}) do
# Handle device creation or lookup
device =
cond do
attrs[:device] -> attrs[:device]
attrs[:device_id] -> Towerops.Devices.get_device!(attrs[:device_id])
true -> DevicesFixtures.device_fixture()
end
# Default SNMP device attributes
default_attrs = %{
device_id: device.id,
sys_name: "device-#{System.unique_integer([:positive])}",
sys_descr: "Test Device",
sys_object_id: "1.3.6.1.4.1.14988.1",
manufacturer: "Test Manufacturer",
model: "Test Model"
}
# Merge with provided attrs
merged_attrs =
Map.merge(
default_attrs,
attrs
|> Map.drop([:device, "device"])
|> Map.new(fn {k, v} -> {to_atom_key(k), v} end)
)
{:ok, snmp_device} =
%Snmp.Device{}
|> Snmp.Device.changeset(merged_attrs)
|> Towerops.Repo.insert()
snmp_device
end
@doc """
Generate a wireless client with all required fields.
## Options
- `:device` - Existing device to attach client to
- `:device_id` - ID of device to attach client to
- `:organization` - Organization for the client
- `:organization_id` - ID of organization for the client
- All other WirelessClient fields (mac_address, signal_strength, etc.)
## Examples
iex> wireless_client_fixture()
%WirelessClient{mac_address: "AA:BB:CC:DD:EE:FF", ...}
iex> wireless_client_fixture(%{signal_strength: -85})
%WirelessClient{signal_strength: -85, ...}
"""
def wireless_client_fixture(attrs \\ %{}) do
# Handle device creation or lookup
device = attrs[:device] || create_device_with_organization(attrs)
organization = device.organization || Organizations.get_organization!(device.organization_id)
# Generate unique MAC address
mac_suffix = 16_777_215 |> :rand.uniform() |> Integer.to_string(16) |> String.pad_leading(6, "0")
mac_parts = mac_suffix |> String.graphemes() |> Enum.chunk_every(2) |> Enum.map(&Enum.join/1)
default_mac = "AA:BB:CC:#{Enum.join(mac_parts, ":")}"
# Default wireless client attributes
default_attrs = %{
device_id: device.id,
organization_id: organization.id,
mac_address: default_mac,
ip_address: "10.0.#{:rand.uniform(254)}.#{:rand.uniform(254)}",
hostname: "client-#{System.unique_integer([:positive])}",
signal_strength: -65,
snr: 25,
distance: 1000,
tx_rate: 100_000,
rx_rate: 50_000,
uptime_seconds: 3600,
last_seen_at: DateTime.truncate(DateTime.utc_now(), :second),
metadata: %{}
}
# Merge with provided attrs, converting string keys if needed
merged_attrs =
Map.merge(
default_attrs,
attrs
|> Map.drop([:device, :organization, "device", "organization"])
|> Map.new(fn {k, v} -> {to_atom_key(k), v} end)
)
{:ok, wireless_client} =
%Snmp.WirelessClient{}
|> Snmp.WirelessClient.changeset(merged_attrs)
|> Towerops.Repo.insert()
wireless_client
end
defp to_atom_key(key) when is_atom(key), do: key
defp to_atom_key(key) when is_binary(key), do: String.to_existing_atom(key)
defp create_device_with_organization(attrs) do
case {attrs[:device_id], attrs[:organization_id]} do
{device_id, _} when not is_nil(device_id) ->
Towerops.Devices.get_device!(device_id)
{_, organization_id} when not is_nil(organization_id) ->
DevicesFixtures.device_fixture(%{organization_id: organization_id})
_ ->
DevicesFixtures.device_fixture()
end
end
end