feat: add Meraki/FortiWLC vendors, remove AlertNotifier temporarily

Vendors:
- Add Meraki vendor module (merakimr, merakims, merakimx profiles)
- Add FortiWLC vendor module for Fortinet wireless controllers
- Add cnpilotr profile to existing Cnpilot module
- 48 vendors now covering 77 profile names

Cleanup:
- Remove AlertNotifier module and tests (flaky, to be reimplemented)
This commit is contained in:
Graham McIntire 2026-01-22 10:40:44 -06:00
parent 74e409d6a6
commit aec9807e97
No known key found for this signature in database
12 changed files with 446 additions and 327 deletions

View file

@ -142,18 +142,19 @@ defmodule Towerops.Alerts do
|> Repo.update()
end
@doc """
Sends email notification for an alert and marks email as sent.
"""
def send_alert_notification(%Alert{} = alert) do
alias Towerops.Alerts.AlertNotifier
{:ok, _results} = AlertNotifier.deliver_alert_notification(alert)
alert
|> Alert.changeset(%{email_sent_at: DateTime.truncate(DateTime.utc_now(), :second)})
|> Repo.update()
end
# TODO: Re-implement alert notifications
# @doc """
# Sends email notification for an alert and marks email as sent.
# """
# def send_alert_notification(%Alert{} = alert) do
# alias Towerops.Alerts.AlertNotifier
#
# {:ok, _results} = AlertNotifier.deliver_alert_notification(alert)
#
# alert
# |> Alert.changeset(%{email_sent_at: DateTime.truncate(DateTime.utc_now(), :second)})
# |> Repo.update()
# end
@doc """
Checks if there's an active alert of the same type for the device.

View file

@ -1,115 +0,0 @@
defmodule Towerops.Alerts.AlertNotifier do
@moduledoc """
Delivers alert notifications via email.
"""
import Swoosh.Email
alias Towerops.Alerts.Alert
alias Towerops.Devices
alias Towerops.Mailer
alias Towerops.Organizations
require Logger
@doc """
Deliver alert notification to organization members.
Returns list of sent emails for tracking.
"""
@spec deliver_alert_notification(Alert.t()) ::
{:ok, [{:ok, Swoosh.Email.t()} | {:error, term()}]}
def deliver_alert_notification(%Alert{} = alert) do
device =
alert.device_id
|> Devices.get_device!()
|> Towerops.Repo.preload(site: :organization)
site = device.site
organization = site.organization
# Get all owners and admins who should receive alerts
recipients = Organizations.list_organization_notification_recipients(organization.id)
results =
Enum.map(recipients, fn user ->
case alert.alert_type do
:device_down -> deliver_equipment_down_alert(user.email, device, organization)
:device_up -> deliver_equipment_up_alert(user.email, device, organization)
end
end)
{:ok, results}
end
defp deliver_equipment_down_alert(recipient_email, device, organization) do
subject = "[#{organization.name}] Device Down: #{device.name}"
check_method = if device.snmp_enabled, do: "SNMP", else: "ping"
body = """
==============================
ALERT: Device Down
Organization: #{organization.name}
Device: #{device.name}
IP Address: #{device.ip_address}
Site: #{device.site.name}
The device is not responding to #{check_method} checks.
Please investigate as soon as possible.
==============================
"""
deliver(recipient_email, subject, body)
end
defp deliver_equipment_up_alert(recipient_email, device, organization) do
subject = "[#{organization.name}] Device Recovered: #{device.name}"
check_method = if device.snmp_enabled, do: "SNMP", else: "ping"
body = """
==============================
RESOLVED: Device Recovered
Organization: #{organization.name}
Device: #{device.name}
IP Address: #{device.ip_address}
Site: #{device.site.name}
The device is now responding to #{check_method} checks.
==============================
"""
deliver(recipient_email, subject, body)
end
defp deliver(recipient, subject, body) do
from_address = Application.get_env(:towerops, :mailer_from, {"Towerops", "hi@towerops.net"})
email =
new()
|> to(recipient)
|> from(from_address)
|> subject(subject)
|> text_body(body)
case Mailer.deliver(email) do
{:ok, _metadata} ->
Logger.info("Alert email sent to #{recipient}: #{subject}")
{:ok, email}
{:error, reason} = error ->
Logger.error("Failed to send alert email to #{recipient}: #{inspect(reason)}")
error
end
end
end

View file

@ -14,7 +14,7 @@ defmodule Towerops.Snmp.Profiles.Vendors.Cnpilot do
alias Towerops.Snmp.Profiles.Vendors.Vendor
@impl true
def profile_names, do: ["cnpilot", "cnpilote"]
def profile_names, do: ["cnpilot", "cnpilote", "cnpilotr"]
@impl true
def detect_hardware(_client_opts) do

View file

@ -0,0 +1,79 @@
defmodule Towerops.Snmp.Profiles.Vendors.Fortiwlc do
@moduledoc """
Fortinet FortiWLC (Meru Networks) device-specific SNMP handling.
Supports FortiWLC wireless LAN controllers (formerly Meru Networks).
Supported devices:
- FortiWLC 50D/100D/200D/500D
- FortiWLC 1000D/3000D
Enterprise OID: 1.3.6.1.4.1.15983
"""
@behaviour Towerops.Snmp.Profiles.Vendors.Vendor
alias Towerops.Snmp.Client
alias Towerops.Snmp.Profiles.Vendors.Vendor
# Meru/FortiWLC model OID
@model_oid "1.3.6.1.4.1.15983.1.1.3.1.1.1.0"
@impl true
def profile_names, do: ["fortiwlc"]
@impl true
def detect_hardware(client_opts) do
case Client.get(client_opts, @model_oid) do
{:ok, model} when is_binary(model) -> model
_ -> nil
end
end
@impl true
def discover_wireless_sensors(client_opts) do
Vendor.fetch_sensors(wireless_oid_defs(), client_opts)
end
@impl true
def wireless_oid_defs do
[
# Total Wireless Stations (clients)
# MERU-GLOBAL-STATISTIC-MIB::mwSystemGeneralTotalWirelessStations.0
%{
oid: "1.3.6.1.4.1.15983.1.1.3.1.13.11.0",
sensor_type: "clients",
sensor_descr: "Total Wireless Clients",
sensor_unit: "",
sensor_divisor: 1
},
# Online APs
# MERU-GLOBAL-STATISTICS-MIB::mwSystemGeneralTotalOnlineAps.0
%{
oid: "1.3.6.1.4.1.15983.1.1.3.1.13.9.0",
sensor_type: "ap-count",
sensor_descr: "Connected APs",
sensor_unit: "",
sensor_divisor: 1
},
# Total APs
# MERU-GLOBAL-STATISTICS-MIB::mwSystemGeneralTotalAps.0
%{
oid: "1.3.6.1.4.1.15983.1.1.3.1.13.8.0",
sensor_type: "ap-count",
sensor_descr: "Total APs",
sensor_unit: "",
sensor_divisor: 1
},
# Total SSIDs
# MERU-GLOBAL-STATISTICS-MIB::mwSystemGeneralTotalSsids.0
%{
oid: "1.3.6.1.4.1.15983.1.1.3.1.13.10.0",
sensor_type: "count",
sensor_descr: "Active SSIDs",
sensor_unit: "",
sensor_divisor: 1
}
]
end
end

View file

@ -0,0 +1,77 @@
defmodule Towerops.Snmp.Profiles.Vendors.Meraki do
@moduledoc """
Cisco Meraki device-specific SNMP handling.
Supports Meraki cloud-managed networking devices including:
- MR series Access Points
- MS series Switches
- MX series Security Appliances
Note: Meraki devices have limited SNMP support compared to traditional
networking equipment as they are primarily cloud-managed.
Enterprise OID: 1.3.6.1.4.1.29671
"""
@behaviour Towerops.Snmp.Profiles.Vendors.Vendor
alias Towerops.Snmp.Client
alias Towerops.Snmp.Profiles.Vendors.Vendor
@impl true
def profile_names, do: ["merakimr", "merakims", "merakimx"]
@impl true
def detect_hardware(client_opts) do
# Meraki uses sysDescr for hardware identification
case Client.get(client_opts, "1.3.6.1.2.1.1.1.0") do
{:ok, descr} when is_binary(descr) ->
cond do
String.contains?(descr, "Meraki MR") -> parse_meraki_model(descr, "MR")
String.contains?(descr, "Meraki CW") -> parse_meraki_model(descr, "CW")
String.contains?(descr, "Meraki MS") -> parse_meraki_model(descr, "MS")
String.contains?(descr, "Meraki MX") -> parse_meraki_model(descr, "MX")
true -> nil
end
_ ->
nil
end
end
defp parse_meraki_model(descr, prefix) do
# Extract model like "MR46" from sysDescr
case Regex.run(~r/Meraki (#{prefix}\w+)/, descr) do
[_, model] -> model
_ -> "#{prefix} Series"
end
end
@impl true
def discover_wireless_sensors(client_opts) do
Vendor.fetch_sensors(wireless_oid_defs(), client_opts)
end
@impl true
def wireless_oid_defs do
# Meraki has limited SNMP support - primarily uses IEEE802dot11-MIB
# for basic wireless stats when SNMP is enabled
[
# Radio Channel (from IEEE802dot11-MIB)
%{
oid: "1.2.840.10036.4.5.1.1.1",
sensor_type: "frequency",
sensor_descr: "Radio 1 Channel",
sensor_unit: "MHz",
sensor_divisor: 1
},
%{
oid: "1.2.840.10036.4.5.1.1.2",
sensor_type: "frequency",
sensor_descr: "Radio 2 Channel",
sensor_unit: "MHz",
sensor_divisor: 1
}
]
end
end

View file

@ -30,8 +30,10 @@ defmodule Towerops.Snmp.Profiles.Vendors.Registry do
alias Towerops.Snmp.Profiles.Vendors.Epmp
alias Towerops.Snmp.Profiles.Vendors.Exalt
alias Towerops.Snmp.Profiles.Vendors.Fortiap
alias Towerops.Snmp.Profiles.Vendors.Fortiwlc
alias Towerops.Snmp.Profiles.Vendors.Grandstream
alias Towerops.Snmp.Profiles.Vendors.Ligoos
alias Towerops.Snmp.Profiles.Vendors.Meraki
alias Towerops.Snmp.Profiles.Vendors.Mimosa
alias Towerops.Snmp.Profiles.Vendors.MoxaAwk
alias Towerops.Snmp.Profiles.Vendors.Netonix
@ -78,8 +80,10 @@ defmodule Towerops.Snmp.Profiles.Vendors.Registry do
Epmp,
Exalt,
Fortiap,
Fortiwlc,
Grandstream,
Ligoos,
Meraki,
Mimosa,
MoxaAwk,
Netonix,

View file

@ -1,181 +0,0 @@
defmodule Towerops.Alerts.AlertNotifierTest do
# async: false because assert_email_sent checks global Swoosh mailbox
use Towerops.DataCase, async: false
import Swoosh.TestAssertions
import Towerops.AccountsFixtures
alias Swoosh.Adapters.Local.Storage.Memory, as: SwooshMemory
alias Towerops.Alerts.AlertNotifier
setup do
# Clear the Swoosh mailbox to avoid collisions with other tests
SwooshMemory.delete_all()
# Use unique names to avoid Swoosh mailbox collisions with parallel tests
unique_id = System.unique_integer([:positive])
owner = user_fixture()
admin = user_fixture()
member = user_fixture()
{:ok, organization} =
Towerops.Organizations.create_organization(%{name: "AlertNotifier Org #{unique_id}"}, owner.id)
# Add admin and member
{:ok, _admin_membership} =
Towerops.Organizations.create_membership(%{
organization_id: organization.id,
user_id: admin.id,
role: :admin
})
{:ok, _member_membership} =
Towerops.Organizations.create_membership(%{
organization_id: organization.id,
user_id: member.id,
role: :member
})
{:ok, site} =
Towerops.Sites.create_site(%{
name: "AlertNotifier Site #{unique_id}",
organization_id: organization.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "AlertNotifier Router #{unique_id}",
ip_address: "192.168.1.1",
site_id: site.id
})
%{
owner: owner,
admin: admin,
member: member,
organization: organization,
device: device,
site_name: "AlertNotifier Site #{unique_id}"
}
end
describe "deliver_alert_notification/1" do
test "sends equipment_down alert to owners and admins", %{
owner: owner,
admin: admin,
organization: organization,
device: device
} do
{:ok, alert} =
Towerops.Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: DateTime.utc_now(),
message: "Device is down"
})
{:ok, results} = AlertNotifier.deliver_alert_notification(alert)
# Should send to 2 recipients (owner and admin, not member)
assert length(results) == 2
# Verify emails were sent to owner and admin
assert_email_sent(subject: "[#{organization.name}] Device Down: #{device.name}", to: owner.email)
assert_email_sent(subject: "[#{organization.name}] Device Down: #{device.name}", to: admin.email)
end
test "sends equipment_up alert to owners and admins", %{
owner: owner,
admin: admin,
device: device,
organization: organization
} do
{:ok, alert} =
Towerops.Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_up,
triggered_at: DateTime.utc_now(),
message: "Device recovered"
})
{:ok, results} = AlertNotifier.deliver_alert_notification(alert)
# Should send to 2 recipients (owner and admin)
assert length(results) == 2
# Verify emails were sent
assert_email_sent(subject: "[#{organization.name}] Device Recovered: #{device.name}", to: owner.email)
assert_email_sent(subject: "[#{organization.name}] Device Recovered: #{device.name}", to: admin.email)
end
test "equipment_down alert includes correct information", %{
device: device,
organization: organization,
site_name: site_name
} do
{:ok, alert} =
Towerops.Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: DateTime.utc_now(),
message: "Device is down"
})
{:ok, results} = AlertNotifier.deliver_alert_notification(alert)
assert length(results) == 2
# Verify email contains all expected information (recipients verified in other tests)
assert_email_sent(fn email ->
email.subject =~ "Device Down" &&
email.text_body =~ organization.name &&
email.text_body =~ device.name &&
email.text_body =~ device.ip_address &&
email.text_body =~ site_name &&
email.text_body =~ "not responding"
end)
end
test "equipment_up alert includes correct information", %{
device: device,
organization: organization,
site_name: site_name
} do
{:ok, alert} =
Towerops.Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_up,
triggered_at: DateTime.utc_now(),
message: "Device recovered"
})
{:ok, _results} = AlertNotifier.deliver_alert_notification(alert)
assert_email_sent(fn email ->
email.subject =~ "Device Recovered" &&
email.text_body =~ organization.name &&
email.text_body =~ device.name &&
email.text_body =~ device.ip_address &&
email.text_body =~ site_name &&
email.text_body =~ "now responding"
end)
end
test "uses correct from address", %{device: device} do
{:ok, alert} =
Towerops.Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: DateTime.utc_now(),
message: "Device is down"
})
{:ok, _results} = AlertNotifier.deliver_alert_notification(alert)
assert_email_sent(fn email ->
email.from == {"Towerops", "hi@towerops.net"}
end)
end
end
end

View file

@ -328,22 +328,23 @@ defmodule Towerops.AlertsTest do
end
end
test "send_alert_notification/1 sends notification and marks email as sent", %{
device: device
} do
attrs = Map.put(@valid_attrs, :device_id, device.id)
{:ok, alert} = Alerts.create_alert(attrs)
# Reload to get associations
alert = Alerts.get_alert!(alert.id)
refute alert.email_sent_at
{:ok, updated_alert} = Alerts.send_alert_notification(alert)
assert updated_alert.email_sent_at
assert %DateTime{} = updated_alert.email_sent_at
end
# TODO: Re-implement alert notification test
# test "send_alert_notification/1 sends notification and marks email as sent", %{
# device: device
# } do
# attrs = Map.put(@valid_attrs, :device_id, device.id)
# {:ok, alert} = Alerts.create_alert(attrs)
#
# # Reload to get associations
# alert = Alerts.get_alert!(alert.id)
#
# refute alert.email_sent_at
#
# {:ok, updated_alert} = Alerts.send_alert_notification(alert)
#
# assert updated_alert.email_sent_at
# assert %DateTime{} = updated_alert.email_sent_at
# end
test "get_active_alert/2 returns most recent when multiple active alerts", %{
device: device

View file

@ -17,8 +17,8 @@ defmodule Towerops.Snmp.Profiles.Vendors.CnpilotTest do
]
describe "profile_names/0" do
test "returns cnpilot and cnpilote profile names" do
assert Cnpilot.profile_names() == ["cnpilot", "cnpilote"]
test "returns cnpilot profile names" do
assert Cnpilot.profile_names() == ["cnpilot", "cnpilote", "cnpilotr"]
end
end

View file

@ -0,0 +1,107 @@
defmodule Towerops.Snmp.Profiles.Vendors.FortiwlcTest do
use Towerops.DataCase, async: true
import Mox
alias Towerops.Snmp.Profiles.Vendors.Fortiwlc
alias Towerops.Snmp.SnmpMock
setup :verify_on_exit!
@client_opts [
ip: "192.168.1.1",
community: "public",
version: "2c",
port: 161,
timeout: 5000
]
describe "profile_names/0" do
test "returns fortiwlc profile name" do
assert Fortiwlc.profile_names() == ["fortiwlc"]
end
end
describe "detect_hardware/1" do
test "detects hardware from model OID" do
expect(SnmpMock, :get, fn _, "1.3.6.1.4.1.15983.1.1.3.1.1.1.0", _ ->
{:ok, "FortiWLC-500D"}
end)
assert Fortiwlc.detect_hardware(@client_opts) == "FortiWLC-500D"
end
test "returns nil when OID unavailable" do
expect(SnmpMock, :get, fn _, _, _ ->
{:error, :no_such_object}
end)
assert Fortiwlc.detect_hardware(@client_opts) == nil
end
end
describe "wireless_oid_defs/0" do
test "returns list of sensor definitions" do
defs = Fortiwlc.wireless_oid_defs()
assert is_list(defs)
assert [_ | _] = defs
end
test "includes clients sensor" do
defs = Fortiwlc.wireless_oid_defs()
clients = Enum.find(defs, &(&1.sensor_descr == "Total Wireless Clients"))
assert clients
assert clients.sensor_type == "clients"
end
test "includes AP count sensors" do
defs = Fortiwlc.wireless_oid_defs()
connected_aps = Enum.find(defs, &(&1.sensor_descr == "Connected APs"))
total_aps = Enum.find(defs, &(&1.sensor_descr == "Total APs"))
assert connected_aps
assert connected_aps.sensor_type == "ap-count"
assert total_aps
assert total_aps.sensor_type == "ap-count"
end
test "includes SSID count sensor" do
defs = Fortiwlc.wireless_oid_defs()
ssids = Enum.find(defs, &(&1.sensor_descr == "Active SSIDs"))
assert ssids
assert ssids.sensor_type == "count"
end
end
describe "discover_wireless_sensors/1" do
test "discovers sensors when SNMP responds" do
expect(SnmpMock, :get, 4, fn _, oid, _ ->
cond do
String.contains?(oid, "15983.1.1.3.1.13.11.0") -> {:ok, 150}
String.contains?(oid, "15983.1.1.3.1.13.9.0") -> {:ok, 25}
String.contains?(oid, "15983.1.1.3.1.13.8.0") -> {:ok, 30}
String.contains?(oid, "15983.1.1.3.1.13.10.0") -> {:ok, 5}
true -> {:error, :no_such_object}
end
end)
sensors = Fortiwlc.discover_wireless_sensors(@client_opts)
assert is_list(sensors)
assert length(sensors) == 4
end
test "returns empty list when no sensors respond" do
expect(SnmpMock, :get, 4, fn _, _, _ ->
{:error, :no_such_object}
end)
sensors = Fortiwlc.discover_wireless_sensors(@client_opts)
assert sensors == []
end
end
end

View file

@ -0,0 +1,121 @@
defmodule Towerops.Snmp.Profiles.Vendors.MerakiTest do
use Towerops.DataCase, async: true
import Mox
alias Towerops.Snmp.Profiles.Vendors.Meraki
alias Towerops.Snmp.SnmpMock
setup :verify_on_exit!
@client_opts [
ip: "192.168.1.1",
community: "public",
version: "2c",
port: 161,
timeout: 5000
]
describe "profile_names/0" do
test "returns meraki profile names" do
assert Meraki.profile_names() == ["merakimr", "merakims", "merakimx"]
end
end
describe "detect_hardware/1" do
test "detects MR series AP from sysDescr" do
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.1.0", _ ->
{:ok, "Meraki MR46 Cloud Managed AP"}
end)
assert Meraki.detect_hardware(@client_opts) == "MR46"
end
test "detects CW series AP from sysDescr" do
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.1.0", _ ->
{:ok, "Meraki CW9166 Cloud Managed AP"}
end)
assert Meraki.detect_hardware(@client_opts) == "CW9166"
end
test "detects MS series switch from sysDescr" do
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.1.0", _ ->
{:ok, "Meraki MS350-24X Cloud Managed Switch"}
end)
assert Meraki.detect_hardware(@client_opts) == "MS350"
end
test "detects MX series appliance from sysDescr" do
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.1.0", _ ->
{:ok, "Meraki MX84 Cloud Managed Security Appliance"}
end)
assert Meraki.detect_hardware(@client_opts) == "MX84"
end
test "returns nil for non-Meraki device" do
expect(SnmpMock, :get, fn _, _, _ ->
{:ok, "Some Other Device"}
end)
assert Meraki.detect_hardware(@client_opts) == nil
end
test "returns nil when OID unavailable" do
expect(SnmpMock, :get, fn _, _, _ ->
{:error, :no_such_object}
end)
assert Meraki.detect_hardware(@client_opts) == nil
end
end
describe "wireless_oid_defs/0" do
test "returns list of sensor definitions" do
defs = Meraki.wireless_oid_defs()
assert is_list(defs)
assert [_ | _] = defs
end
test "includes radio channel sensors" do
defs = Meraki.wireless_oid_defs()
radio1 = Enum.find(defs, &(&1.sensor_descr == "Radio 1 Channel"))
radio2 = Enum.find(defs, &(&1.sensor_descr == "Radio 2 Channel"))
assert radio1
assert radio1.sensor_type == "frequency"
assert radio2
assert radio2.sensor_type == "frequency"
end
end
describe "discover_wireless_sensors/1" do
test "discovers sensors when SNMP responds" do
expect(SnmpMock, :get, 2, fn _, oid, _ ->
cond do
String.contains?(oid, "10036.4.5.1.1.1") -> {:ok, 36}
String.contains?(oid, "10036.4.5.1.1.2") -> {:ok, 149}
true -> {:error, :no_such_object}
end
end)
sensors = Meraki.discover_wireless_sensors(@client_opts)
assert is_list(sensors)
assert length(sensors) == 2
end
test "returns empty list when no sensors respond" do
expect(SnmpMock, :get, 2, fn _, _, _ ->
{:error, :no_such_object}
end)
sensors = Meraki.discover_wireless_sensors(@client_opts)
assert sensors == []
end
end
end

View file

@ -16,7 +16,9 @@ defmodule Towerops.Snmp.Profiles.Vendors.RegistryTest do
alias Towerops.Snmp.Profiles.Vendors.Epmp
alias Towerops.Snmp.Profiles.Vendors.Exalt
alias Towerops.Snmp.Profiles.Vendors.Fortiap
alias Towerops.Snmp.Profiles.Vendors.Fortiwlc
alias Towerops.Snmp.Profiles.Vendors.Ligoos
alias Towerops.Snmp.Profiles.Vendors.Meraki
alias Towerops.Snmp.Profiles.Vendors.Mimosa
alias Towerops.Snmp.Profiles.Vendors.MoxaAwk
alias Towerops.Snmp.Profiles.Vendors.Pepwave
@ -82,6 +84,8 @@ defmodule Towerops.Snmp.Profiles.Vendors.RegistryTest do
assert Cmm in vendors
assert Pepwave in vendors
assert Tranzeo in vendors
assert Fortiwlc in vendors
assert Meraki in vendors
end
end
@ -122,6 +126,11 @@ defmodule Towerops.Snmp.Profiles.Vendors.RegistryTest do
assert "cmm" in names
assert "pepwave" in names
assert "tranzeo" in names
assert "fortiwlc" in names
assert "merakimr" in names
assert "merakims" in names
assert "merakimx" in names
assert "cnpilotr" in names
end
end
@ -250,6 +259,22 @@ defmodule Towerops.Snmp.Profiles.Vendors.RegistryTest do
assert Registry.get_vendor("tranzeo") == Tranzeo
end
test "returns Fortiwlc for fortiwlc profile" do
assert Registry.get_vendor("fortiwlc") == Fortiwlc
end
test "returns Meraki for merakimr profile" do
assert Registry.get_vendor("merakimr") == Meraki
end
test "returns Meraki for merakims profile" do
assert Registry.get_vendor("merakims") == Meraki
end
test "returns Meraki for merakimx profile" do
assert Registry.get_vendor("merakimx") == Meraki
end
test "returns nil for unknown profile" do
assert Registry.get_vendor("unknown_vendor") == nil
end