From b51759dfdfeb0453047de8313a2c9cb1f00c7bc4 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 9 May 2026 09:20:27 -0500 Subject: [PATCH] test: cloud poller AgentChannel + SNMP sensor v3/no-snmp + firmware month parser --- .../executors/snmp_sensor_executor_test.exs | 35 ++++++++ .../firmware_version_fetcher_worker_test.exs | 58 +++++++++++++ .../agent_channel_cloud_poller_test.exs | 82 +++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 test/towerops_web/channels/agent_channel_cloud_poller_test.exs diff --git a/test/towerops/monitoring/executors/snmp_sensor_executor_test.exs b/test/towerops/monitoring/executors/snmp_sensor_executor_test.exs index 851b91fe..4f8888da 100644 --- a/test/towerops/monitoring/executors/snmp_sensor_executor_test.exs +++ b/test/towerops/monitoring/executors/snmp_sensor_executor_test.exs @@ -286,5 +286,40 @@ defmodule Towerops.Monitoring.Executors.SnmpSensorExecutorTest do assert {:error, :timeout} = SnmpSensorExecutor.execute(check) end + + test "returns error when device has no SNMP device record", %{organization: organization} do + # Create a device with snmp_enabled but no Snmp.Device association inserted. + {:ok, site} = Towerops.Sites.create_site(%{name: "No-SNMP Site", organization_id: organization.id}) + + {:ok, device} = + Towerops.Devices.create_device(%{ + name: "No-SNMP Device", + ip_address: "10.99.99.99", + snmp_enabled: true, + snmp_version: "2c", + snmp_community: "public", + snmp_port: 161, + site_id: site.id, + organization_id: organization.id + }) + + {:ok, check} = + Monitoring.create_check(%{ + organization_id: organization.id, + device_id: device.id, + name: "Bad device check", + check_type: "snmp_sensor", + source_type: "manual", + interval_seconds: 60, + enabled: true, + config: %{ + "sensor_id" => Ecto.UUID.generate(), + "sensor_oid" => "1.2.3.4", + "sensor_type" => "temperature" + } + }) + + assert {:error, _reason} = SnmpSensorExecutor.execute(check) + end end end diff --git a/test/towerops/workers/firmware_version_fetcher_worker_test.exs b/test/towerops/workers/firmware_version_fetcher_worker_test.exs index 40e4da56..e4073736 100644 --- a/test/towerops/workers/firmware_version_fetcher_worker_test.exs +++ b/test/towerops/workers/firmware_version_fetcher_worker_test.exs @@ -175,6 +175,64 @@ defmodule Towerops.Workers.FirmwareVersionFetcherWorkerTest do assert data.release_date == ~D[2025-02-01] end + test "parses every month abbreviation" do + months = [ + {"Jan", 1}, + {"Feb", 2}, + {"Mar", 3}, + {"Apr", 4}, + {"May", 5}, + {"Jun", 6}, + {"Jul", 7}, + {"Aug", 8}, + {"Sep", 9}, + {"Oct", 10}, + {"Nov", 11}, + {"Dec", 12} + ] + + for {abbrev, month_num} <- months do + rss = """ + + + + RouterOS latest stable version + + 7.14.1 + https://mikrotik.com/download + Mon, 15 #{abbrev} 2025 12:00:00 +0000 + Test + + + + """ + + assert {:ok, data} = FirmwareVersionFetcherWorker.parse_rss_feed(rss) + assert data.release_date.month == month_num + assert data.release_date.year == 2025 + end + end + + test "returns nil release_date for unrecognized month" do + rss = """ + + + + RouterOS latest stable version + + 7.14.1 + https://mikrotik.com/download + Mon, 15 Foo 2025 12:00:00 +0000 + Test + + + + """ + + assert {:ok, data} = FirmwareVersionFetcherWorker.parse_rss_feed(rss) + assert is_nil(data.release_date) + end + test "handles invalid RFC822 date format gracefully" do invalid_date = """ diff --git a/test/towerops_web/channels/agent_channel_cloud_poller_test.exs b/test/towerops_web/channels/agent_channel_cloud_poller_test.exs new file mode 100644 index 00000000..b2a46a4f --- /dev/null +++ b/test/towerops_web/channels/agent_channel_cloud_poller_test.exs @@ -0,0 +1,82 @@ +defmodule ToweropsWeb.AgentChannelCloudPollerTest do + @moduledoc """ + Drives the `is_cloud_poller` join branch + latency_probe_jobs subscribe + side-effect. The default AgentChannel test connects with a regular + (non-cloud) agent token; this file specifically tests the cloud-poller + path so the `Phoenix.PubSub.subscribe(... :latency_probe)` line + downstream + handler executes with cloud_poller=true. + """ + use Towerops.DataCase, async: false + + import Phoenix.ChannelTest + + alias Towerops.AccountsFixtures + alias Towerops.Agent.AgentJobList + alias Towerops.Agents + alias Towerops.OrganizationsFixtures + alias ToweropsWeb.AgentSocket + + @endpoint ToweropsWeb.Endpoint + + setup do + user = AccountsFixtures.user_fixture() + organization = OrganizationsFixtures.organization_fixture(user.id) + + {:ok, agent_token, token_string} = Agents.create_cloud_poller("Test Cloud Poller") + + {:ok, socket} = connect(AgentSocket, %{}) + + {:ok, _, socket} = + subscribe_and_join(socket, "agent:#{agent_token.id}", %{"token" => token_string}) + + assert_push("jobs", _initial_jobs, 1_000) + + %{ + socket: socket, + agent_token: agent_token, + token_string: token_string, + organization: organization, + user: user + } + end + + describe "cloud poller join + latency_probe subscription" do + test "join with a cloud-poller token sets the cloud poller branch", %{ + agent_token: agent_token, + organization: organization + } do + # Cloud-poller tokens are global (no organization_id), so we use the + # test's user-owned organization for the test device. The setup itself + # proves the join path subscribed to the latency_probe PubSub topic; + # we confirm by broadcasting on it and watching for a "jobs" push. + assert agent_token.is_cloud_poller + + {:ok, site} = + Towerops.Sites.create_site(%{ + name: "Cloud Site", + organization_id: organization.id + }) + + {:ok, device} = + Towerops.Devices.create_device(%{ + name: "Probe Device", + ip_address: "10.66.0.1", + site_id: site.id, + organization_id: organization.id + }) + + Phoenix.PubSub.broadcast( + Towerops.PubSub, + "agent:#{agent_token.id}:latency_probe", + {:latency_probe_jobs, [device]} + ) + + assert_push("jobs", %{binary: jobs_binary}, 2_000) + {:ok, decoded} = Base.decode64(jobs_binary) + {:ok, job_list} = AgentJobList.decode(decoded) + + ping_jobs = Enum.filter(job_list.jobs, &(&1.job_type == :PING)) + assert length(ping_jobs) == 5 + end + end +end