From 0c7d195e523e347cca0c92f12259391baef67880 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 6 Mar 2026 07:57:19 -0600 Subject: [PATCH] chore: remove backup files from sed operations --- test/towerops/alerts_test.exs.bak | 629 -------- test/towerops/api_tokens_test.exs.bak | 641 -------- .../equipment/event_logger_test.exs.bak | 154 -- .../snmp/deferred_discovery_test.exs.bak | 300 ---- test/towerops/snmp/poller_test.exs.bak | 169 -- .../workers/device_poller_worker_test.exs.bak | 660 -------- .../channels/agent_channel_test.exs.bak | 1378 ----------------- .../api/account_data_controller_test.exs.bak | 289 ---- .../live/device_live_nested/show_test.exs.bak | 181 --- .../update_session_activity_test.exs.bak | 111 -- 10 files changed, 4512 deletions(-) delete mode 100644 test/towerops/alerts_test.exs.bak delete mode 100644 test/towerops/api_tokens_test.exs.bak delete mode 100644 test/towerops/equipment/event_logger_test.exs.bak delete mode 100644 test/towerops/snmp/deferred_discovery_test.exs.bak delete mode 100644 test/towerops/snmp/poller_test.exs.bak delete mode 100644 test/towerops/workers/device_poller_worker_test.exs.bak delete mode 100644 test/towerops_web/channels/agent_channel_test.exs.bak delete mode 100644 test/towerops_web/controllers/api/account_data_controller_test.exs.bak delete mode 100644 test/towerops_web/live/device_live_nested/show_test.exs.bak delete mode 100644 test/towerops_web/plugs/update_session_activity_test.exs.bak diff --git a/test/towerops/alerts_test.exs.bak b/test/towerops/alerts_test.exs.bak deleted file mode 100644 index 716fc50a..00000000 --- a/test/towerops/alerts_test.exs.bak +++ /dev/null @@ -1,629 +0,0 @@ -defmodule Towerops.AlertsTest do - use Towerops.DataCase - - alias Towerops.Alerts - - describe "alerts" do - import Towerops.AccountsFixtures - - alias Towerops.Alerts.Alert - - setup do - user = user_fixture() - {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id) - - {:ok, site} = - Towerops.Sites.create_site(%{ - name: "Test Site", - organization_id: organization.id - }) - - {:ok, device} = - Towerops.Devices.create_device(%{ - name: "Router 1", - ip_address: "192.168.1.1", - site_id: site.id, - organization_id: organization.id - }) - - %{device: device, organization: organization, user: user} - end - - @valid_attrs %{ - alert_type: "device_down", - triggered_at: ~U[2025-12-21 12:00:00Z], - message: "Equipment is not responding" - } - - test "create_alert/1 with valid data creates an alert", %{device: device} do - attrs = Map.put(@valid_attrs, :device_id, device.id) - assert {:ok, %Alert{} = alert} = Alerts.create_alert(attrs) - assert alert.alert_type == "device_down" - assert alert.message == "Equipment is not responding" - end - - test "create_alert/1 with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = Alerts.create_alert(%{}) - end - - test "list_devices_alerts/2 returns alerts for device", %{device: device} do - attrs = Map.put(@valid_attrs, :device_id, device.id) - {:ok, alert} = Alerts.create_alert(attrs) - [result] = Alerts.list_devices_alerts(device.id) - assert result.id == alert.id - end - - test "list_organization_active_alerts/1 returns unresolved alerts", %{ - device: device, - organization: organization - } do - attrs = Map.put(@valid_attrs, :device_id, device.id) - {:ok, alert} = Alerts.create_alert(attrs) - - # Create resolved alert - {:ok, resolved_alert} = - Alerts.create_alert(Map.put(attrs, :resolved_at, ~U[2025-12-21 13:00:00Z])) - - active = Alerts.list_organization_active_alerts(organization.id) - assert length(active) == 1 - assert hd(active).id == alert.id - refute Enum.any?(active, fn a -> a.id == resolved_alert.id end) - end - - test "list_organization_alerts/2 returns all alerts", %{ - device: device, - organization: organization - } do - attrs = Map.put(@valid_attrs, :device_id, device.id) - {:ok, _alert1} = Alerts.create_alert(attrs) - {:ok, _alert2} = Alerts.create_alert(Map.put(attrs, :alert_type, :device_up)) - - alerts = Alerts.list_organization_alerts(organization.id, 100) - assert length(alerts) == 2 - end - - test "get_alert!/1 returns the alert with given id", %{device: device} do - attrs = Map.put(@valid_attrs, :device_id, device.id) - {:ok, alert} = Alerts.create_alert(attrs) - assert Alerts.get_alert!(alert.id).id == alert.id - end - - test "acknowledge_alert/2 marks alert as acknowledged", %{device: device, user: user} do - attrs = Map.put(@valid_attrs, :device_id, device.id) - {:ok, alert} = Alerts.create_alert(attrs) - - assert {:ok, acknowledged} = Alerts.acknowledge_alert(alert, user.id) - assert acknowledged.acknowledged_at - assert acknowledged.acknowledged_by_id == user.id - end - - test "resolve_alert/1 marks alert as resolved", %{device: device} do - attrs = Map.put(@valid_attrs, :device_id, device.id) - {:ok, alert} = Alerts.create_alert(attrs) - - assert {:ok, resolved} = Alerts.resolve_alert(alert) - assert resolved.resolved_at - end - - test "has_active_alert?/2 returns true when active alert exists", %{device: device} do - attrs = Map.put(@valid_attrs, :device_id, device.id) - {:ok, _alert} = Alerts.create_alert(attrs) - - assert Alerts.has_active_alert?(device.id, :device_down) == true - assert Alerts.has_active_alert?(device.id, :device_up) == false - end - - test "has_active_alert?/2 returns false for resolved alerts", %{device: device} do - attrs = Map.put(@valid_attrs, :device_id, device.id) - {:ok, alert} = Alerts.create_alert(attrs) - {:ok, _resolved} = Alerts.resolve_alert(alert) - - assert Alerts.has_active_alert?(device.id, :device_down) == false - end - - test "get_active_alert/2 returns active alert of specific type", %{device: device} do - attrs = Map.put(@valid_attrs, :device_id, device.id) - {:ok, alert} = Alerts.create_alert(attrs) - - found = Alerts.get_active_alert(device.id, :device_down) - assert found.id == alert.id - end - - test "get_active_alert/2 returns nil when no active alert exists", %{device: device} do - assert Alerts.get_active_alert(device.id, :device_down) == nil - end - - test "get_active_alert/2 returns nil for resolved alerts", %{device: device} do - attrs = Map.put(@valid_attrs, :device_id, device.id) - {:ok, alert} = Alerts.create_alert(attrs) - {:ok, _resolved} = Alerts.resolve_alert(alert) - - assert Alerts.get_active_alert(device.id, :device_down) == nil - end - - test "count_active_alerts/1 returns count of unresolved alerts", %{ - device: device, - organization: organization - } do - attrs = Map.put(@valid_attrs, :device_id, device.id) - {:ok, _alert1} = Alerts.create_alert(attrs) - {:ok, _alert2} = Alerts.create_alert(attrs) - - # Create resolved alert - {:ok, resolved} = Alerts.create_alert(attrs) - {:ok, _} = Alerts.resolve_alert(resolved) - - assert Alerts.count_active_alerts(organization.id) == 2 - end - - test "count_active_alerts/1 returns 0 when no active alerts", %{organization: organization} do - assert Alerts.count_active_alerts(organization.id) == 0 - end - - test "list_devices_alerts/2 respects limit parameter", %{device: device} do - attrs = Map.put(@valid_attrs, :device_id, device.id) - - # Create 5 alerts - for _i <- 1..5 do - {:ok, _} = Alerts.create_alert(attrs) - end - - # Request only 3 - alerts = Alerts.list_devices_alerts(device.id, 3) - assert length(alerts) == 3 - end - - test "list_devices_alerts/2 orders by triggered_at descending", %{device: device} do - attrs = Map.put(@valid_attrs, :device_id, device.id) - - {:ok, old_alert} = - Alerts.create_alert(Map.put(attrs, :triggered_at, ~U[2025-12-21 10:00:00Z])) - - {:ok, new_alert} = - Alerts.create_alert(Map.put(attrs, :triggered_at, ~U[2025-12-21 12:00:00Z])) - - [first, second] = Alerts.list_devices_alerts(device.id) - assert first.id == new_alert.id - assert second.id == old_alert.id - end - - test "list_organization_alerts/2 with status filter active", %{ - device: device, - organization: organization, - user: user - } do - attrs = Map.put(@valid_attrs, :device_id, device.id) - - # Create unacknowledged alert - {:ok, active_alert} = Alerts.create_alert(attrs) - - # Create acknowledged alert - {:ok, acked_alert} = Alerts.create_alert(attrs) - {:ok, _} = Alerts.acknowledge_alert(acked_alert, user.id) - - # Create resolved alert - {:ok, resolved_alert} = Alerts.create_alert(attrs) - {:ok, _} = Alerts.resolve_alert(resolved_alert) - - active = Alerts.list_organization_alerts(organization.id, %{"status" => "active"}) - assert length(active) == 1 - assert hd(active).id == active_alert.id - end - - test "list_organization_alerts/2 with status filter acknowledged", %{ - device: device, - organization: organization, - user: user - } do - attrs = Map.put(@valid_attrs, :device_id, device.id) - - # Create acknowledged alert - {:ok, acked_alert} = Alerts.create_alert(attrs) - {:ok, _} = Alerts.acknowledge_alert(acked_alert, user.id) - - # Create unacknowledged alert - {:ok, _active} = Alerts.create_alert(attrs) - - acknowledged = - Alerts.list_organization_alerts(organization.id, %{"status" => "acknowledged"}) - - assert length(acknowledged) == 1 - assert hd(acknowledged).id == acked_alert.id - end - - test "list_organization_alerts/2 with status filter resolved", %{ - device: device, - organization: organization - } do - attrs = Map.put(@valid_attrs, :device_id, device.id) - - # Create resolved alert - {:ok, resolved_alert} = Alerts.create_alert(attrs) - {:ok, _} = Alerts.resolve_alert(resolved_alert) - - # Create active alert - {:ok, _active} = Alerts.create_alert(attrs) - - resolved = Alerts.list_organization_alerts(organization.id, %{"status" => "resolved"}) - assert length(resolved) == 1 - assert hd(resolved).id == resolved_alert.id - end - - test "list_organization_alerts/2 with limit in filters map", %{ - device: device, - organization: organization - } do - attrs = Map.put(@valid_attrs, :device_id, device.id) - - for _i <- 1..5 do - {:ok, _} = Alerts.create_alert(attrs) - end - - alerts = Alerts.list_organization_alerts(organization.id, %{"limit" => 2}) - assert length(alerts) == 2 - end - - test "list_organization_alerts/2 defaults to limit 100", %{ - device: device, - organization: organization - } do - attrs = Map.put(@valid_attrs, :device_id, device.id) - - # Just verify it doesn't error with no limit specified - {:ok, _} = Alerts.create_alert(attrs) - - alerts = Alerts.list_organization_alerts(organization.id, %{}) - assert length(alerts) == 1 - end - - test "list_organization_active_alerts/1 only returns device_down alerts", %{ - device: device, - organization: organization - } do - # Create device_down alert - {:ok, down_alert} = - Alerts.create_alert(%{ - device_id: device.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now() - }) - - # Create device_up alert - {:ok, _up_alert} = - Alerts.create_alert(%{ - device_id: device.id, - alert_type: "device_up", - triggered_at: DateTime.utc_now() - }) - - active = Alerts.list_organization_active_alerts(organization.id) - assert length(active) == 1 - assert hd(active).id == down_alert.id - assert hd(active).alert_type == "device_down" - end - - test "list_organization_active_alerts/1 preloads associations", %{ - device: device, - organization: organization - } do - attrs = Map.put(@valid_attrs, :device_id, device.id) - {:ok, _alert} = Alerts.create_alert(attrs) - - [alert] = Alerts.list_organization_active_alerts(organization.id) - assert Ecto.assoc_loaded?(alert.device) - assert Ecto.assoc_loaded?(alert.device.site) - end - - test "get_alert!/1 preloads associations", %{device: device} do - attrs = Map.put(@valid_attrs, :device_id, device.id) - {:ok, alert} = Alerts.create_alert(attrs) - - fetched = Alerts.get_alert!(alert.id) - assert Ecto.assoc_loaded?(fetched.device) - assert Ecto.assoc_loaded?(fetched.acknowledged_by) - end - - test "get_alert!/1 raises when alert doesn't exist" do - assert_raise Ecto.NoResultsError, fn -> - Alerts.get_alert!(Ecto.UUID.generate()) - end - end - - test "get_active_alert/2 returns most recent when multiple active alerts", %{ - device: device - } do - attrs = Map.put(@valid_attrs, :device_id, device.id) - - {:ok, old_alert} = - Alerts.create_alert(Map.put(attrs, :triggered_at, ~U[2025-12-21 10:00:00Z])) - - {:ok, new_alert} = - Alerts.create_alert(Map.put(attrs, :triggered_at, ~U[2025-12-21 12:00:00Z])) - - found = Alerts.get_active_alert(device.id, :device_down) - assert found.id == new_alert.id - refute found.id == old_alert.id - end - - test "count_active_alerts/1 only counts device_down alerts", %{ - device: device, - organization: organization - } do - # Create device_down alert - {:ok, _} = - Alerts.create_alert(%{ - device_id: device.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now() - }) - - # Create device_up alert (should not be counted) - {:ok, _} = - Alerts.create_alert(%{ - device_id: device.id, - alert_type: "device_up", - triggered_at: DateTime.utc_now() - }) - - assert Alerts.count_active_alerts(organization.id) == 1 - end - end - - describe "GDPR data access - list_alerts_for_organizations/2" do - import Towerops.AccountsFixtures - - setup do - user = user_fixture() - {:ok, org1} = Towerops.Organizations.create_organization(%{name: "Org 1"}, user.id) - - # Create org2 with a different owner to avoid subscription limits - other_user = user_fixture() - {:ok, org2} = Towerops.Organizations.create_organization(%{name: "Org 2"}, other_user.id) - - {:ok, site1} = - Towerops.Sites.create_site(%{ - name: "Org1 Site", - organization_id: org1.id - }) - - {:ok, site2} = - Towerops.Sites.create_site(%{ - name: "Org2 Site", - organization_id: org2.id - }) - - {:ok, device1} = - Towerops.Devices.create_device(%{ - name: "Device 1", - ip_address: "192.168.1.1", - site_id: site1.id, - organization_id: org1.id - }) - - {:ok, device2} = - Towerops.Devices.create_device(%{ - name: "Device 2", - ip_address: "192.168.1.2", - site_id: site2.id, - organization_id: org2.id - }) - - %{org1: org1, org2: org2, device1: device1, device2: device2} - end - - test "returns all alerts for given organizations", %{ - org1: org1, - org2: org2, - device1: device1, - device2: device2 - } do - {:ok, alert1} = - Alerts.create_alert(%{ - device_id: device1.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now(), - message: "Device 1 down" - }) - - {:ok, alert2} = - Alerts.create_alert(%{ - device_id: device2.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now(), - message: "Device 2 down" - }) - - # Default limit is 90 days - result = Alerts.list_alerts_for_organizations([org1.id, org2.id]) - assert length(result) == 2 - - alert_ids = Enum.map(result, & &1.id) - assert alert1.id in alert_ids - assert alert2.id in alert_ids - end - - test "returns only alerts from specified organizations", %{ - org1: org1, - device1: device1, - device2: device2 - } do - {:ok, alert1} = - Alerts.create_alert(%{ - device_id: device1.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now() - }) - - {:ok, _alert2} = - Alerts.create_alert(%{ - device_id: device2.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now() - }) - - # Only request alerts from org1 - result = Alerts.list_alerts_for_organizations([org1.id]) - assert length(result) == 1 - assert hd(result).id == alert1.id - end - - test "respects since parameter to filter old alerts", %{org1: org1, device1: device1} do - # Create recent alert (within 30 days) - {:ok, recent_alert} = - Alerts.create_alert(%{ - device_id: device1.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now() - }) - - # Sleep briefly to ensure different inserted_at times - Process.sleep(10) - - # Create old alert (older than 30 days based on inserted_at) - {:ok, old_alert} = - Alerts.create_alert(%{ - device_id: device1.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now() - }) - - # Manually set old alert's inserted_at to 60 days ago - sixty_days_ago = DateTime.utc_now() |> DateTime.add(-60, :day) |> DateTime.truncate(:second) - - old_alert - |> Ecto.Changeset.change(inserted_at: sixty_days_ago) - |> Towerops.Repo.update!() - - # Request only alerts from last 30 days - thirty_days_ago = DateTime.add(DateTime.utc_now(), -30, :day) - result = Alerts.list_alerts_for_organizations([org1.id], since: thirty_days_ago) - assert length(result) == 1 - assert hd(result).id == recent_alert.id - end - - test "returns all alerts when no since parameter provided", %{org1: org1, device1: device1} do - # Create two alerts with different ages - {:ok, recent_alert} = - Alerts.create_alert(%{ - device_id: device1.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now() - }) - - {:ok, old_alert} = - Alerts.create_alert(%{ - device_id: device1.id, - alert_type: "device_down", - triggered_at: DateTime.add(DateTime.utc_now(), -120, :day) - }) - - # Without since parameter, should return all alerts - result = Alerts.list_alerts_for_organizations([org1.id]) - assert length(result) == 2 - - alert_ids = Enum.map(result, & &1.id) - assert recent_alert.id in alert_ids - assert old_alert.id in alert_ids - end - - test "returns empty list when organizations have no alerts", %{org1: org1} do - result = Alerts.list_alerts_for_organizations([org1.id]) - assert result == [] - end - - test "returns empty list for empty organization list" do - result = Alerts.list_alerts_for_organizations([]) - assert result == [] - end - - test "preloads device and site associations", %{org1: org1, device1: device1} do - {:ok, _alert} = - Alerts.create_alert(%{ - device_id: device1.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now() - }) - - result = Alerts.list_alerts_for_organizations([org1.id]) - assert length(result) == 1 - loaded_alert = hd(result) - assert Ecto.assoc_loaded?(loaded_alert.device) - assert Ecto.assoc_loaded?(loaded_alert.device.site) - end - - test "orders alerts by triggered_at descending", %{org1: org1, device1: device1} do - {:ok, old_alert} = - Alerts.create_alert(%{ - device_id: device1.id, - alert_type: "device_down", - triggered_at: DateTime.add(DateTime.utc_now(), -10, :day) - }) - - {:ok, new_alert} = - Alerts.create_alert(%{ - device_id: device1.id, - alert_type: "device_down", - triggered_at: DateTime.add(DateTime.utc_now(), -5, :day) - }) - - result = Alerts.list_alerts_for_organizations([org1.id]) - assert length(result) == 2 - - # Most recent should be first - [first, second] = result - assert first.id == new_alert.id - assert second.id == old_alert.id - end - - test "includes both active and resolved alerts", %{org1: org1, device1: device1} do - # Create active alert - {:ok, active_alert} = - Alerts.create_alert(%{ - device_id: device1.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now() - }) - - # Create resolved alert - {:ok, resolved_alert} = - Alerts.create_alert(%{ - device_id: device1.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now() - }) - - {:ok, _} = Alerts.resolve_alert(resolved_alert) - - # Should include both - result = Alerts.list_alerts_for_organizations([org1.id]) - assert length(result) == 2 - - alert_ids = Enum.map(result, & &1.id) - assert active_alert.id in alert_ids - assert resolved_alert.id in alert_ids - end - - test "includes all alert types", %{org1: org1, device1: device1} do - {:ok, _down_alert} = - Alerts.create_alert(%{ - device_id: device1.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now() - }) - - {:ok, _up_alert} = - Alerts.create_alert(%{ - device_id: device1.id, - alert_type: "device_up", - triggered_at: DateTime.utc_now() - }) - - result = Alerts.list_alerts_for_organizations([org1.id]) - assert length(result) == 2 - - alert_types = Enum.map(result, & &1.alert_type) - assert "device_down" in alert_types - assert "device_up" in alert_types - end - end -end diff --git a/test/towerops/api_tokens_test.exs.bak b/test/towerops/api_tokens_test.exs.bak deleted file mode 100644 index 4fe62476..00000000 --- a/test/towerops/api_tokens_test.exs.bak +++ /dev/null @@ -1,641 +0,0 @@ -defmodule Towerops.ApiTokensTest do - use Towerops.DataCase - use ExUnitProperties - - import Towerops.AccountsFixtures - import Towerops.OrganizationsFixtures - - alias Towerops.ApiTokens - alias Towerops.ApiTokens.ApiToken - - describe "create_api_token/1" do - setup do - user = user_fixture() - organization = organization_fixture(user.id) - %{user: user, organization: organization} - end - - test "creates an API token with valid attributes", %{ - user: user, - organization: organization - } do - attrs = %{ - organization_id: organization.id, - user_id: user.id, - name: "Test Token" - } - - assert {:ok, {api_token, raw_token}} = ApiTokens.create_api_token(attrs) - assert api_token.name == "Test Token" - assert api_token.organization_id == organization.id - assert api_token.user_id == user.id - assert is_binary(api_token.token_hash) - assert String.starts_with?(raw_token, "towerops_") - assert is_nil(api_token.last_used_at) - assert is_nil(api_token.expires_at) - end - - test "creates an API token without user_id", %{organization: organization} do - attrs = %{ - organization_id: organization.id, - name: "Test Token" - } - - assert {:ok, {api_token, raw_token}} = ApiTokens.create_api_token(attrs) - assert api_token.name == "Test Token" - assert api_token.organization_id == organization.id - assert is_nil(api_token.user_id) - assert is_binary(api_token.token_hash) - assert String.starts_with?(raw_token, "towerops_") - end - - test "creates an API token with expiration", %{ - user: user, - organization: organization - } do - expires_at = DateTime.add(DateTime.utc_now(), 30, :day) - - attrs = %{ - organization_id: organization.id, - user_id: user.id, - name: "Expiring Token", - expires_at: expires_at - } - - assert {:ok, {api_token, _raw_token}} = ApiTokens.create_api_token(attrs) - assert api_token.expires_at == DateTime.truncate(expires_at, :second) - end - - test "returns error with invalid attributes" do - assert {:error, %Ecto.Changeset{}} = ApiTokens.create_api_token(%{}) - end - - test "returns error with blank name", %{organization: organization} do - attrs = %{ - organization_id: organization.id, - name: "" - } - - assert {:error, changeset} = ApiTokens.create_api_token(attrs) - assert "can't be blank" in errors_on(changeset).name - end - - test "returns error without organization_id" do - attrs = %{ - name: "Test Token" - } - - assert {:error, changeset} = ApiTokens.create_api_token(attrs) - assert "can't be blank" in errors_on(changeset).organization_id - end - - test "generates unique tokens", %{user: user, organization: organization} do - attrs = %{ - organization_id: organization.id, - user_id: user.id, - name: "Test Token" - } - - {:ok, {_token1, raw_token1}} = ApiTokens.create_api_token(attrs) - {:ok, {_token2, raw_token2}} = ApiTokens.create_api_token(attrs) - - assert raw_token1 != raw_token2 - end - end - - describe "list_organization_api_tokens/1" do - setup do - user = user_fixture() - organization = organization_fixture(user.id) - %{user: user, organization: organization} - end - - test "returns all tokens for an organization", %{ - user: user, - organization: organization - } do - {:ok, {token1, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Token 1" - }) - - {:ok, {token2, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Token 2" - }) - - tokens = ApiTokens.list_organization_api_tokens(organization.id) - - assert length(tokens) == 2 - assert Enum.any?(tokens, fn t -> t.id == token1.id end) - assert Enum.any?(tokens, fn t -> t.id == token2.id end) - end - - test "returns tokens ordered by creation date", %{ - user: user, - organization: organization - } do - {:ok, {token1, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Token 1" - }) - - {:ok, {token2, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Token 2" - }) - - tokens = ApiTokens.list_organization_api_tokens(organization.id) - - assert length(tokens) == 2 - # Verify tokens are ordered by inserted_at (descending) - [first, second] = tokens - - assert DateTime.compare(first.inserted_at, second.inserted_at) in [:gt, :eq] - # Verify both tokens are present - token_ids = Enum.map(tokens, & &1.id) - assert token1.id in token_ids - assert token2.id in token_ids - end - - test "returns empty list for organization with no tokens", %{organization: organization} do - assert [] = ApiTokens.list_organization_api_tokens(organization.id) - end - - test "does not return tokens from other organizations", %{user: user, organization: org1} do - org2 = organization_fixture(user.id) - - {:ok, {_token1, _}} = - ApiTokens.create_api_token(%{ - organization_id: org1.id, - user_id: user.id, - name: "Org 1 Token" - }) - - {:ok, {_token2, _}} = - ApiTokens.create_api_token(%{ - organization_id: org2.id, - user_id: user.id, - name: "Org 2 Token" - }) - - tokens = ApiTokens.list_organization_api_tokens(org1.id) - - assert length(tokens) == 1 - assert hd(tokens).name == "Org 1 Token" - end - end - - describe "list_user_api_tokens/1" do - setup do - user = user_fixture() - organization = organization_fixture(user.id) - %{user: user, organization: organization} - end - - test "returns all tokens created by a user", %{user: user, organization: organization} do - {:ok, {token1, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Token 1" - }) - - {:ok, {token2, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Token 2" - }) - - tokens = ApiTokens.list_user_api_tokens(user.id) - - assert length(tokens) == 2 - assert Enum.any?(tokens, fn t -> t.id == token1.id end) - assert Enum.any?(tokens, fn t -> t.id == token2.id end) - end - - test "preloads organization", %{user: user, organization: organization} do - {:ok, {_token, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Token 1" - }) - - tokens = ApiTokens.list_user_api_tokens(user.id) - - assert [token] = tokens - assert %Ecto.Association.NotLoaded{} != token.organization - assert token.organization.id == organization.id - end - - test "returns tokens ordered by creation date", %{ - user: user, - organization: organization - } do - {:ok, {token1, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Token 1" - }) - - {:ok, {token2, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Token 2" - }) - - tokens = ApiTokens.list_user_api_tokens(user.id) - - assert length(tokens) == 2 - # Verify tokens are ordered by inserted_at (descending) - [first, second] = tokens - - assert DateTime.compare(first.inserted_at, second.inserted_at) in [:gt, :eq] - # Verify both tokens are present - token_ids = Enum.map(tokens, & &1.id) - assert token1.id in token_ids - assert token2.id in token_ids - end - - test "returns empty list for user with no tokens", %{user: user} do - assert [] = ApiTokens.list_user_api_tokens(user.id) - end - - test "does not return tokens created by other users", %{organization: organization} do - user1 = user_fixture() - user2 = user_fixture() - - {:ok, {_token1, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user1.id, - name: "User 1 Token" - }) - - {:ok, {_token2, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user2.id, - name: "User 2 Token" - }) - - tokens = ApiTokens.list_user_api_tokens(user1.id) - - assert length(tokens) == 1 - assert hd(tokens).name == "User 1 Token" - end - end - - describe "get_api_token!/1" do - setup do - user = user_fixture() - organization = organization_fixture(user.id) - - {:ok, {token, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Test Token" - }) - - %{token: token} - end - - test "returns the token with the given id", %{token: token} do - fetched_token = ApiTokens.get_api_token!(token.id) - assert fetched_token.id == token.id - assert fetched_token.name == token.name - end - - test "raises if the token does not exist" do - assert_raise Ecto.NoResultsError, fn -> - ApiTokens.get_api_token!(Ecto.UUID.generate()) - end - end - end - - describe "verify_token/1" do - setup do - user = user_fixture() - organization = organization_fixture(user.id) - - {:ok, {token, raw_token}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Test Token" - }) - - %{token: token, raw_token: raw_token, organization: organization, user: user} - end - - test "returns organization_id and user for valid token", %{ - raw_token: raw_token, - organization: organization, - user: user - } do - assert {:ok, org_id, returned_user} = ApiTokens.verify_token(raw_token) - assert org_id == organization.id - assert returned_user.id == user.id - end - - test "updates last_used_at timestamp", %{token: token, raw_token: raw_token} do - assert is_nil(token.last_used_at) - - {:ok, _org_id, _user} = ApiTokens.verify_token(raw_token) - - # Give the async Task time to complete - Process.sleep(50) - - updated_token = ApiTokens.get_api_token!(token.id) - assert updated_token.last_used_at - assert DateTime.diff(updated_token.last_used_at, DateTime.utc_now(), :second) <= 1 - end - - test "returns error for invalid token" do - assert {:error, :invalid_token} = ApiTokens.verify_token("invalid_token") - end - - test "returns error for expired token", %{ - user: user, - organization: organization - } do - expires_at = DateTime.add(DateTime.utc_now(), -1, :day) - - {:ok, {_token, raw_token}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Expired Token", - expires_at: expires_at - }) - - assert {:error, :invalid_token} = ApiTokens.verify_token(raw_token) - end - - test "works for token without expiration", %{ - raw_token: raw_token, - organization: organization - } do - assert {:ok, org_id, _user} = ApiTokens.verify_token(raw_token) - assert org_id == organization.id - end - - test "works for token without user_id", %{organization: organization} do - {:ok, {_token, raw_token}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - name: "No User Token" - }) - - assert {:ok, org_id, nil} = ApiTokens.verify_token(raw_token) - assert org_id == organization.id - end - end - - describe "delete_api_token/1" do - setup do - user = user_fixture() - organization = organization_fixture(user.id) - - {:ok, {token, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Test Token" - }) - - %{token: token} - end - - test "deletes the token", %{token: token} do - assert {:ok, %ApiToken{}} = ApiTokens.delete_api_token(token) - - assert_raise Ecto.NoResultsError, fn -> - ApiTokens.get_api_token!(token.id) - end - end - end - - describe "update_api_token/2" do - setup do - user = user_fixture() - organization = organization_fixture(user.id) - - {:ok, {token, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Test Token" - }) - - %{token: token} - end - - test "updates the token name", %{token: token} do - assert {:ok, updated_token} = ApiTokens.update_api_token(token, %{name: "New Name"}) - assert updated_token.name == "New Name" - end - - test "updates the expiration", %{token: token} do - expires_at = DateTime.add(DateTime.utc_now(), 30, :day) - - assert {:ok, updated_token} = - ApiTokens.update_api_token(token, %{expires_at: expires_at}) - - assert updated_token.expires_at == DateTime.truncate(expires_at, :second) - end - - test "returns error with invalid name", %{token: token} do - assert {:error, changeset} = ApiTokens.update_api_token(token, %{name: ""}) - assert "can't be blank" in errors_on(changeset).name - end - - test "returns error with too long name", %{token: token} do - long_name = String.duplicate("a", 256) - assert {:error, changeset} = ApiTokens.update_api_token(token, %{name: long_name}) - assert "should be at most 255 character(s)" in errors_on(changeset).name - end - end - - describe "property-based tests" do - property "generated tokens always start with towerops_" do - check all(_ <- constant(nil), max_runs: 50) do - user = user_fixture() - organization = organization_fixture(user.id) - - {:ok, {_token, raw_token}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Test Token" - }) - - assert String.starts_with?(raw_token, "towerops_") - # Token should be sufficiently long (prefix + base64 encoded 32 bytes) - assert String.length(raw_token) > 40 - end - end - - property "tokens with valid names can always be created" do - check all(name <- string(:alphanumeric, min_length: 1, max_length: 255), max_runs: 10) do - user = user_fixture() - organization = organization_fixture(user.id) - - result = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: name - }) - - case result do - {:ok, {token, raw_token}} -> - assert token.name == name - assert String.starts_with?(raw_token, "towerops_") - - {:error, _changeset} -> - flunk("Valid name '#{name}' was rejected") - end - end - end - - property "verify_token roundtrip always works for non-expired tokens" do - check all(_ <- constant(nil), max_runs: 20) do - user = user_fixture() - organization = organization_fixture(user.id) - - {:ok, {_token, raw_token}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Test Token" - }) - - # Verify the token we just created - assert {:ok, org_id, returned_user} = ApiTokens.verify_token(raw_token) - assert org_id == organization.id - assert returned_user.id == user.id - end - end - - property "expired tokens always return error" do - # Create fixtures once outside the property check - user = user_fixture() - organization = organization_fixture(user.id) - - check all(days_ago <- integer(1..365), max_runs: 20) do - expires_at = DateTime.add(DateTime.utc_now(), -days_ago, :day) - - {:ok, {_token, raw_token}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Expired Token", - expires_at: expires_at - }) - - assert {:error, :invalid_token} = ApiTokens.verify_token(raw_token) - end - end - - property "list operations return consistent counts" do - check all(token_count <- integer(0..5), max_runs: 10) do - user = user_fixture() - organization = organization_fixture(user.id) - - # Create specified number of tokens - if token_count > 0 do - for i <- 1..token_count do - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Token #{i}" - }) - end - end - - org_tokens = ApiTokens.list_organization_api_tokens(organization.id) - user_tokens = ApiTokens.list_user_api_tokens(user.id) - - assert length(org_tokens) == token_count - assert length(user_tokens) == token_count - assert length(org_tokens) == length(user_tokens) - end - end - - property "deleting tokens reduces count by one" do - check all(_ <- constant(nil), max_runs: 20) do - user = user_fixture() - organization = organization_fixture(user.id) - - # Create two tokens - {:ok, {token1, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Token 1" - }) - - {:ok, {_token2, _}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: "Token 2" - }) - - initial_count = length(ApiTokens.list_organization_api_tokens(organization.id)) - assert initial_count == 2 - - # Delete one token - {:ok, _} = ApiTokens.delete_api_token(token1) - - final_count = length(ApiTokens.list_organization_api_tokens(organization.id)) - assert final_count == initial_count - 1 - end - end - - property "updating token name preserves token hash" do - check all( - original_name <- string(:alphanumeric, min_length: 1, max_length: 50), - new_name <- string(:alphanumeric, min_length: 1, max_length: 50), - max_runs: 10 - ) do - user = user_fixture() - organization = organization_fixture(user.id) - - {:ok, {token, raw_token}} = - ApiTokens.create_api_token(%{ - organization_id: organization.id, - user_id: user.id, - name: original_name - }) - - original_hash = token.token_hash - - # Update the name - {:ok, updated_token} = ApiTokens.update_api_token(token, %{name: new_name}) - - # Token hash should remain the same (name changes don't affect token) - assert updated_token.token_hash == original_hash - assert updated_token.name == new_name - - # Original raw token should still work - assert {:ok, org_id, _user} = ApiTokens.verify_token(raw_token) - assert org_id == organization.id - end - end - end -end diff --git a/test/towerops/equipment/event_logger_test.exs.bak b/test/towerops/equipment/event_logger_test.exs.bak deleted file mode 100644 index d8ac6221..00000000 --- a/test/towerops/equipment/event_logger_test.exs.bak +++ /dev/null @@ -1,154 +0,0 @@ -defmodule Towerops.Devices.EventLoggerTest do - use Towerops.DataCase, async: false - - import Towerops.AccountsFixtures - - alias Towerops.Devices.EventLogger - - describe "event logging via PubSub" do - setup do - # Start EventLogger for these tests - start_supervised!(EventLogger) - - user = user_fixture() - - {:ok, organization} = - Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id) - - {:ok, site} = - Towerops.Sites.create_site(%{ - name: "Test Site", - organization_id: organization.id - }) - - {:ok, device} = - Towerops.Devices.create_device(%{ - name: "Test Equipment", - site_id: site.id, - organization_id: site.organization_id, - ip_address: "192.168.1.1" - }) - - %{user: user, organization: organization, site: site, device: device} - end - - test "EventLogger logs events broadcast via PubSub", %{device: device} do - event_attrs = %{ - device_id: device.id, - event_type: "interface_speed_change", - severity: "info", - message: "Interface eth0 speed detected: 1.0 Gbps", - metadata: %{ - interface_id: "test-interface-id", - interface_name: "eth0", - old_speed: nil, - new_speed: 1_000_000_000 - }, - occurred_at: DateTime.truncate(DateTime.utc_now(), :second) - } - - # Broadcast event via PubSub - Phoenix.PubSub.broadcast( - Towerops.PubSub, - "device:events", - {:device_event, event_attrs} - ) - - # Poll for event with early exit (max 100ms) - events = - Enum.reduce_while(1..10, [], fn _, _acc -> - events = Towerops.Devices.list_devices_events(device.id, 10) - - if events == [] do - Process.sleep(10) - {:cont, []} - else - {:halt, events} - end - end) - - assert [_event] = events - - event = hd(events) - assert event.device_id == device.id - assert event.event_type == "interface_speed_change" - assert event.severity == "info" - assert event.message == "Interface eth0 speed detected: 1.0 Gbps" - end - - test "EventLogger handles multiple events in sequence", %{device: device} do - now = DateTime.truncate(DateTime.utc_now(), :second) - - # Broadcast multiple events - for i <- 1..3 do - Phoenix.PubSub.broadcast( - Towerops.PubSub, - "device:events", - {:device_event, - %{ - device_id: device.id, - event_type: "interface_speed_change", - severity: "info", - message: "Event #{i}", - metadata: %{}, - occurred_at: DateTime.add(now, i, :second) - }} - ) - end - - # Poll for events with early exit (max 100ms) - events = - Enum.reduce_while(1..10, [], fn _, _acc -> - events = Towerops.Devices.list_devices_events(device.id, 10) - - if length(events) >= 3 do - {:halt, events} - else - Process.sleep(10) - {:cont, []} - end - end) - - # Verify all events were created - assert length(events) == 3 - end - - test "EventLogger logs errors for invalid events", %{device: device} do - # Broadcast an invalid event (missing required fields) - invalid_event = %{ - device_id: device.id - # Missing required fields - } - - # Capture log output - log = - ExUnit.CaptureLog.capture_log(fn -> - Phoenix.PubSub.broadcast( - Towerops.PubSub, - "device:events", - {:device_event, invalid_event} - ) - - Process.sleep(50) - end) - - # Verify error was logged - assert log =~ "Failed to log event" - end - - test "EventLogger ignores non-event messages" do - # Send a message that's not an event - Phoenix.PubSub.broadcast( - Towerops.PubSub, - "device:events", - {:some_other_message, "data"} - ) - - # Should not crash - just log and move on - Process.sleep(50) - - # EventLogger should still be running - assert Process.whereis(EventLogger) - end - end -end diff --git a/test/towerops/snmp/deferred_discovery_test.exs.bak b/test/towerops/snmp/deferred_discovery_test.exs.bak deleted file mode 100644 index f32d0318..00000000 --- a/test/towerops/snmp/deferred_discovery_test.exs.bak +++ /dev/null @@ -1,300 +0,0 @@ -defmodule Towerops.Snmp.DeferredDiscoveryTest do - use ExUnit.Case, async: true - - import Mox - - alias Towerops.Snmp.DeferredDiscovery - alias Towerops.Snmp.SnmpMock - - setup :verify_on_exit! - - describe "fast_check/3" do - test "returns result when check completes within timeout" do - client_opts = %{host: "192.168.1.1"} - - result = - DeferredDiscovery.fast_check(client_opts, fn -> - {:ok, "test result"} - end) - - assert result == {:ok, "test result"} - end - - test "returns error when check times out" do - client_opts = %{host: "192.168.1.1"} - - result = - DeferredDiscovery.fast_check( - client_opts, - fn -> - Process.sleep(100) - {:ok, "too slow"} - end, - timeout: 50 - ) - - assert result == {:error, :timeout} - end - - test "returns error result from check function" do - client_opts = %{host: "192.168.1.1"} - - result = - DeferredDiscovery.fast_check(client_opts, fn -> - {:error, :connection_failed} - end) - - assert result == {:error, :connection_failed} - end - end - - describe "slow_check/3" do - test "returns result when check completes" do - client_opts = %{host: "192.168.1.1"} - - result = - DeferredDiscovery.slow_check(client_opts, fn -> - {:ok, ["sensor1", "sensor2"]} - end) - - assert result == {:ok, ["sensor1", "sensor2"]} - end - - test "returns default value when check times out" do - client_opts = %{host: "192.168.1.1"} - - result = - DeferredDiscovery.slow_check( - client_opts, - fn -> - Process.sleep(100) - {:ok, "too slow"} - end, - timeout: 50, - default: [] - ) - - assert result == {:ok, []} - end - - test "uses custom default value on timeout" do - client_opts = %{host: "192.168.1.1"} - - result = - DeferredDiscovery.slow_check( - client_opts, - fn -> - Process.sleep(100) - {:ok, "too slow"} - end, - timeout: 50, - default: %{status: :timeout} - ) - - assert result == {:ok, %{status: :timeout}} - end - end - - describe "deferred_check/3" do - test "returns ok result when check succeeds" do - client_opts = %{host: "192.168.1.1"} - - result = - DeferredDiscovery.deferred_check(client_opts, fn -> - {:ok, ["neighbor1", "neighbor2"]} - end) - - assert result == {:ok, ["neighbor1", "neighbor2"]} - end - - test "returns deferred status when check fails" do - client_opts = %{host: "192.168.1.1"} - - result = - DeferredDiscovery.deferred_check( - client_opts, - fn -> - {:error, :not_supported} - end, - name: "neighbors" - ) - - assert {:deferred, [], :not_supported} = result - end - - test "returns timeout status when check times out" do - client_opts = %{host: "192.168.1.1"} - - result = - DeferredDiscovery.deferred_check( - client_opts, - fn -> - Process.sleep(100) - {:ok, "too slow"} - end, - timeout: 50, - name: "neighbors" - ) - - assert {:timeout, []} = result - end - end - - describe "parallel_checks/1" do - test "runs multiple checks in parallel" do - checks = [ - {:sensors, fn -> {:ok, ["temp1", "temp2"]} end, []}, - {:interfaces, fn -> {:ok, ["eth0", "eth1"]} end, []}, - {:neighbors, fn -> {:ok, ["switch1"]} end, []} - ] - - results = DeferredDiscovery.parallel_checks(checks) - - assert map_size(results) == 3 - assert {:ok, ["temp1", "temp2"]} = results[:sensors] - assert {:ok, ["eth0", "eth1"]} = results[:interfaces] - assert {:ok, ["switch1"]} = results[:neighbors] - end - - test "handles mixed success and failure results" do - checks = [ - {:sensors, fn -> {:ok, ["temp1"]} end, []}, - {:interfaces, fn -> {:error, :walk_failed} end, []}, - {:neighbors, fn -> {:ok, []} end, []} - ] - - results = DeferredDiscovery.parallel_checks(checks) - - assert {:ok, ["temp1"]} = results[:sensors] - assert {:error, :walk_failed} = results[:interfaces] - assert {:ok, []} = results[:neighbors] - end - - test "handles timeout with default value" do - checks = [ - {:fast_check, fn -> {:ok, "fast"} end, [timeout: 1000]}, - {:slow_check, - fn -> - Process.sleep(100) - {:ok, "slow"} - end, [timeout: 50, default: []]} - ] - - results = DeferredDiscovery.parallel_checks(checks) - - assert {:ok, "fast"} = results[:fast_check] - assert {:timeout, []} = results[:slow_check] - end - - test "handles exceptions gracefully" do - checks = [ - {:good_check, fn -> {:ok, "good"} end, []}, - {:bad_check, fn -> raise "something went wrong" end, []} - ] - - results = DeferredDiscovery.parallel_checks(checks) - - assert {:ok, "good"} = results[:good_check] - assert {:error, "something went wrong"} = results[:bad_check] - end - - test "handles empty list" do - results = DeferredDiscovery.parallel_checks([]) - assert results == %{} - end - end - - describe "device_responsive?/2" do - test "returns true when device responds" do - expect(SnmpMock, :get, fn _target, _oid, _opts -> - {:ok, [1, 3, 6, 1, 4, 1, 9]} - end) - - client_opts = [ip: "192.168.1.1", community: "public", version: "2c"] - - assert DeferredDiscovery.device_responsive?(client_opts) == true - end - - test "returns false when device does not respond" do - expect(SnmpMock, :get, fn _target, _oid, _opts -> - {:error, :timeout} - end) - - client_opts = [ip: "192.168.1.1", community: "public", version: "2c"] - - assert DeferredDiscovery.device_responsive?(client_opts) == false - end - - test "returns false when check times out" do - expect(SnmpMock, :get, fn _target, _oid, _opts -> - Process.sleep(100) - {:ok, [1, 3, 6, 1, 4, 1, 9]} - end) - - client_opts = [ip: "192.168.1.1", community: "public", version: "2c"] - - assert DeferredDiscovery.device_responsive?(client_opts, timeout: 50) == false - end - end - - describe "categorize_device_speed/2" do - test "returns :fast for quick responses" do - expect(SnmpMock, :get, fn _target, _oid, _opts -> - {:ok, "Fast Device"} - end) - - client_opts = [ip: "192.168.1.1", community: "public", version: "2c"] - - assert DeferredDiscovery.categorize_device_speed(client_opts) == :fast - end - - test "returns :slow for delayed responses" do - expect(SnmpMock, :get, fn _target, _oid, _opts -> - # Sleep just past the threshold to be classified as slow - Process.sleep(60) - {:ok, "Slow Device"} - end) - - client_opts = [ip: "192.168.1.1", community: "public", version: "2c"] - - # Use a shorter threshold (50ms) so 60ms delay = slow - assert DeferredDiscovery.categorize_device_speed(client_opts, fast_threshold: 50) == :slow - end - - test "returns :unresponsive when device errors" do - expect(SnmpMock, :get, fn _target, _oid, _opts -> - {:error, :timeout} - end) - - client_opts = [ip: "192.168.1.1", community: "public", version: "2c"] - - assert DeferredDiscovery.categorize_device_speed(client_opts) == :unresponsive - end - end - - describe "timeouts_for_speed/1" do - test "returns short timeouts for fast devices" do - timeouts = DeferredDiscovery.timeouts_for_speed(:fast) - - assert timeouts[:fast] == 5_000 - assert timeouts[:slow] == 15_000 - assert timeouts[:deferred] == 30_000 - end - - test "returns medium timeouts for slow devices" do - timeouts = DeferredDiscovery.timeouts_for_speed(:slow) - - assert timeouts[:fast] == 15_000 - assert timeouts[:slow] == 45_000 - assert timeouts[:deferred] == 90_000 - end - - test "returns long timeouts for unresponsive devices" do - timeouts = DeferredDiscovery.timeouts_for_speed(:unresponsive) - - assert timeouts[:fast] == 30_000 - assert timeouts[:slow] == 60_000 - assert timeouts[:deferred] == 120_000 - end - end -end diff --git a/test/towerops/snmp/poller_test.exs.bak b/test/towerops/snmp/poller_test.exs.bak deleted file mode 100644 index fffa4797..00000000 --- a/test/towerops/snmp/poller_test.exs.bak +++ /dev/null @@ -1,169 +0,0 @@ -defmodule Towerops.Snmp.PollerTest do - use Towerops.DataCase, async: true - - import Mox - - alias Towerops.Snmp.Poller - alias Towerops.Snmp.SnmpMock - - setup :verify_on_exit! - - describe "check_device/1" do - test "returns response time on successful check" do - client_opts = [ - ip: "192.168.1.1", - community: "public", - version: "2c", - port: 161, - timeout: 5000 - ] - - expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ -> - {:ok, {:timeticks, 12_345_678}} - end) - - assert {:ok, response_time} = Poller.check_device(client_opts) - assert is_integer(response_time) - assert response_time >= 0 - end - - test "returns error on timeout" do - client_opts = [ - ip: "192.168.1.99", - community: "public", - version: "2c", - port: 161, - timeout: 1000 - ] - - expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ -> - {:error, :timeout} - end) - - assert {:error, :timeout} = Poller.check_device(client_opts) - end - - test "returns error on authentication failure" do - client_opts = [ - ip: "192.168.1.1", - community: "wrong_community", - version: "2c", - port: 161, - timeout: 5000 - ] - - expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ -> - {:error, :auth_failure} - end) - - assert {:error, :auth_failure} = Poller.check_device(client_opts) - end - - test "returns error on network unreachable" do - client_opts = [ - ip: "10.255.255.1", - community: "public", - version: "2c", - port: 161, - timeout: 5000 - ] - - expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ -> - {:error, :network_unreachable} - end) - - assert {:error, :network_unreachable} = Poller.check_device(client_opts) - end - - test "measures response time accurately" do - client_opts = [ - ip: "192.168.1.1", - community: "public", - version: "2c", - port: 161, - timeout: 5000 - ] - - # Simulate a delayed response - expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ -> - Process.sleep(10) - {:ok, {:timeticks, 999}} - end) - - assert {:ok, response_time} = Poller.check_device(client_opts) - # Response time should be at least 10ms due to the sleep - assert response_time >= 10 - end - end - - describe "build_client_opts/1" do - test "builds client options from device map" do - device = %{ - ip_address: "192.168.1.100", - snmp_community: "private", - snmp_version: "2c", - snmp_port: 161 - } - - client_opts = Poller.build_client_opts(device) - - assert client_opts[:ip] == "192.168.1.100" - assert client_opts[:community] == "private" - assert client_opts[:version] == "2c" - assert client_opts[:port] == 161 - assert client_opts[:timeout] == 5000 - end - - test "uses default port 161 when snmp_port is nil" do - device = %{ - ip_address: "192.168.1.100", - snmp_community: "public", - snmp_version: "2c", - snmp_port: nil - } - - client_opts = Poller.build_client_opts(device) - - assert client_opts[:port] == 161 - end - - test "handles custom SNMP port" do - device = %{ - ip_address: "192.168.1.100", - snmp_community: "public", - snmp_version: "2c", - snmp_port: 1161 - } - - client_opts = Poller.build_client_opts(device) - - assert client_opts[:port] == 1161 - end - - test "builds options for SNMPv1" do - device = %{ - ip_address: "10.0.0.1", - snmp_community: "public", - snmp_version: "1", - snmp_port: 161 - } - - client_opts = Poller.build_client_opts(device) - - assert client_opts[:version] == "1" - end - - test "includes standard timeout of 5 seconds" do - device = %{ - ip_address: "192.168.1.1", - snmp_community: "public", - snmp_version: "2c", - snmp_port: 161 - } - - client_opts = Poller.build_client_opts(device) - - assert client_opts[:timeout] == 5000 - end - end -end diff --git a/test/towerops/workers/device_poller_worker_test.exs.bak b/test/towerops/workers/device_poller_worker_test.exs.bak deleted file mode 100644 index 25d86ef2..00000000 --- a/test/towerops/workers/device_poller_worker_test.exs.bak +++ /dev/null @@ -1,660 +0,0 @@ -defmodule Towerops.Workers.DevicePollerWorkerTest do - use Towerops.DataCase, async: false - - # DevicePollerWorker is deprecated in favor of CheckExecutorWorker - # These tests remain for documentation but are skipped - import Mox - import Towerops.AccountsFixtures - - alias Towerops.Devices - alias Towerops.Organizations - alias Towerops.Repo - alias Towerops.Sites - alias Towerops.Snmp.Device - alias Towerops.Snmp.Interface - alias Towerops.Snmp.Sensor - alias Towerops.Snmp.SnmpMock - alias Towerops.Snmp.StateSensor - alias Towerops.Workers.DevicePollerWorker - alias Towerops.Workers.PollingOffset - - setup :verify_on_exit! - - setup do - # Configure Mock SNMP Adapter - old_adapter = Application.get_env(:towerops, :snmp_adapter) - Application.put_env(:towerops, :snmp_adapter, SnmpMock) - - # Disable Phoenix SNMP check for tests - old_disable = Application.get_env(:towerops, :disable_phoenix_snmp) - Application.put_env(:towerops, :disable_phoenix_snmp, false) - - on_exit(fn -> - if old_adapter do - Application.put_env(:towerops, :snmp_adapter, old_adapter) - else - Application.delete_env(:towerops, :snmp_adapter) - end - - if old_disable == nil do - Application.delete_env(:towerops, :disable_phoenix_snmp) - else - Application.put_env(:towerops, :disable_phoenix_snmp, old_disable) - end - end) - - user = user_fixture() - {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id) - - {:ok, site} = - Sites.create_site(%{ - name: "Test Site", - organization_id: organization.id - }) - - %{organization: organization, site: site, user: user} - end - - describe "perform/1" do - @describetag :skip - - test "returns :ok when device does not exist" do - assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => Ecto.UUID.generate()}}) - end - - test "skips polling when snmp_enabled is false", %{site: site} do - {:ok, device} = - Devices.create_device(%{ - name: "Router No SNMP", - ip_address: "192.168.1.10", - site_id: site.id, - organization_id: site.organization_id, - snmp_enabled: false - }) - - # Ensure no jobs exist initially (created by create_device) - Repo.delete_all(Oban.Job) - - assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) - - # FIXED: Should NOT schedule next poll when SNMP is disabled - # This prevents zombie jobs from continuing to poll disabled devices - # Previously, it would reschedule even when disabled, causing unnecessary work - assert Repo.aggregate(Oban.Job, :count) == 0 - end - - test "polls device and schedules next run", %{site: site} do - {:ok, device} = - Devices.create_device(%{ - name: "Router Poll", - ip_address: "192.168.1.11", - site_id: site.id, - organization_id: site.organization_id, - snmp_enabled: true, - check_interval_seconds: 60 - }) - - # Clear existing jobs - Repo.delete_all(Oban.Job) - - assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) - - # Verify next job scheduled - assert Repo.aggregate(Oban.Job, :count) == 1 - end - end - - describe "polling logic" do - @describetag :skip - - test "polls sensors and updates values", %{site: site} do - # 1. Create Device - {:ok, device} = - Devices.create_device(%{ - name: "Sensor Device", - ip_address: "192.168.1.50", - site_id: site.id, - organization_id: site.organization_id, - snmp_enabled: true - }) - - # 2. Create SnmpDevice association - {:ok, snmp_device} = - Repo.insert(%Device{ - device_id: device.id, - sys_object_id: "1.3.6.1.4.1.9.1", - sys_descr: "Cisco Router", - sys_name: "router01" - }) - - # 3. Create Sensor - {:ok, sensor} = - Repo.insert(%Sensor{ - snmp_device_id: snmp_device.id, - sensor_type: "temperature", - sensor_index: "1", - sensor_oid: "1.3.6.1.2.1.99.1.1.1.4.1", - sensor_descr: "Chassis Temp", - sensor_unit: "C", - sensor_divisor: 1, - monitored: true - }) - - # 4. Create Interface - {:ok, interface} = - Repo.insert(%Interface{ - snmp_device_id: snmp_device.id, - if_index: 2, - if_name: "eth0", - if_descr: "Ethernet 0", - if_type: 6, - if_speed: 100_000_000, - if_phys_address: "00:11:22:33:44:55", - if_admin_status: "up", - if_oper_status: "up", - monitored: true - }) - - # 4b. Create State Sensor - {:ok, state_sensor} = - Repo.insert(%StateSensor{ - snmp_device_id: snmp_device.id, - sensor_descr: "Power Supply 1", - sensor_oid: "1.3.6.1.4.1.9.9.13.1.5.1.3.1", - sensor_index: "1", - metadata: %{"states" => %{"1" => "unknown", "2" => "enabled", "3" => "disabled"}} - # status defaults to unknown - }) - - # 4c. Create Processor - {:ok, processor} = - Repo.insert(%Towerops.Snmp.Processor{ - snmp_device_id: snmp_device.id, - processor_index: "1", - processor_type: "hr_processor", - description: "CPU 1" - }) - - # 5. Mock SNMP response - # The worker runs tasks in parallel, so we need to be careful with expectations. - # It calls poll_device_sensors, poll_device_state_sensors, etc. - # We only have one sensor, so poll_device_sensors will try to fetch it. - - SnmpMock - |> stub(:get, fn _target, oid, _opts -> - case oid do - # Sensor - "1.3.6.1.2.1.99.1.1.1.4.1" -> {:ok, 45} - # State Sensor - # enabled/ok - "1.3.6.1.4.1.9.9.13.1.5.1.3.1" -> {:ok, 2} - # Processor Load - # 25% load - "1.3.6.1.2.1.25.3.3.1.2.1" -> {:ok, 25} - # Interface HC In Octets - "1.3.6.1.2.1.31.1.1.1.6.2" -> {:ok, 1000} - # Interface HC Out Octets - "1.3.6.1.2.1.31.1.1.1.10.2" -> {:ok, 2000} - # Interface Errors - "1.3.6.1.2.1.2.2.1.14.2" -> {:ok, 0} - "1.3.6.1.2.1.2.2.1.20.2" -> {:ok, 0} - "1.3.6.1.2.1.2.2.1.13.2" -> {:ok, 0} - "1.3.6.1.2.1.2.2.1.19.2" -> {:ok, 0} - # Interface change detection OIDs (for interface 2) - # Speed - "1.3.6.1.2.1.2.2.1.5.2" -> {:ok, 100_000_000} - # Physical address (MAC) - "1.3.6.1.2.1.2.2.1.6.2" -> {:ok, <<0, 17, 34, 51, 68, 85>>} - # Admin status (down=2) - "1.3.6.1.2.1.2.2.1.7.2" -> {:ok, 2} - # Oper status (up=1) - "1.3.6.1.2.1.2.2.1.8.2" -> {:ok, 1} - _ -> {:error, :no_such_object} - end - end) - # Allow other calls (like interfaces, neighbors etc) to return empty/ok - |> stub(:walk, fn _target, _oid, _opts -> {:ok, []} end) - - # 6. Run Perform - Repo.delete_all(Oban.Job) - assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) - - # 7. Verify Sensor Updated - updated_sensor = Repo.get(Sensor, sensor.id) - assert updated_sensor.last_value == 45.0 - assert updated_sensor.last_checked_at - - # 8. Verify Interface Stats - stats = Repo.all(Towerops.Snmp.InterfaceStat) - refute Enum.empty?(stats) - stat = List.last(stats) - assert stat.interface_id == interface.id - assert stat.if_in_octets == 1000 - assert stat.if_out_octets == 2000 - - # 9. Verify Interface Changed - updated_interface = Repo.get(Interface, interface.id) - assert updated_interface.if_admin_status == "down" - - # 10. Verify State Sensor - updated_state = Repo.get(StateSensor, state_sensor.id) - assert updated_state.state_value == 2 - assert updated_state.status == "ok" - assert updated_state.state_descr == "enabled" - - # 11. Verify Processor - readings = Repo.all(Towerops.Snmp.ProcessorReading) - refute Enum.empty?(readings) - reading = List.last(readings) - assert reading.processor_id == processor.id - assert reading.load_percent == 25.0 - end - - test "handles SNMP errors gracefully", %{site: site} do - {:ok, device} = - Devices.create_device(%{ - name: "Error Device", - ip_address: "192.168.1.51", - site_id: site.id, - organization_id: site.organization_id, - snmp_enabled: true - }) - - {:ok, snmp_device} = Repo.insert(%Device{device_id: device.id}) - - {:ok, _sensor} = - Repo.insert(%Sensor{ - snmp_device_id: snmp_device.id, - sensor_type: "temperature", - sensor_index: "1", - sensor_oid: "1.3.6.1.2.1.99.1.1.1.4.1", - sensor_descr: "Chassis Temp" - }) - - # Mock timeout - SnmpMock - |> expect(:get, fn _target, "1.3.6.1.2.1.99.1.1.1.4.1", _opts -> - {:error, :timeout} - end) - |> stub(:walk, fn _, _, _ -> {:ok, []} end) - |> stub(:get, fn _, _, _ -> {:error, :no_such_object} end) - - assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) - end - end - - describe "start_polling/1" do - @describetag :skip - - test "schedules initial job with offset", %{site: site} do - {:ok, device} = - Devices.create_device(%{ - name: "Router 1", - ip_address: "192.168.1.1", - site_id: site.id, - organization_id: site.organization_id, - check_interval_seconds: 300 - }) - - # Get the expected offset - expected_offset = PollingOffset.calculate_offset(device.id, 300) - - # Start polling - assert {:ok, job} = DevicePollerWorker.start_polling(device.id) - - # Verify job is scheduled with offset - assert job.args["device_id"] == device.id - assert job.scheduled_at - - # Calculate the delay (scheduled_at - inserted_at) - delay_seconds = DateTime.diff(job.scheduled_at, job.inserted_at, :second) - assert delay_seconds == expected_offset - end - - test "schedules initial job with offset for different interval", %{site: site} do - {:ok, device} = - Devices.create_device(%{ - name: "Router 2", - ip_address: "192.168.1.2", - site_id: site.id, - organization_id: site.organization_id, - check_interval_seconds: 600 - }) - - # Get the expected offset - expected_offset = PollingOffset.calculate_offset(device.id, 600) - - # Start polling - assert {:ok, job} = DevicePollerWorker.start_polling(device.id) - - # Verify job is scheduled with offset - delay_seconds = DateTime.diff(job.scheduled_at, job.inserted_at, :second) - assert delay_seconds == expected_offset - end - end - - describe "stop_polling/1" do - @describetag :skip - - test "cancels all jobs for device", %{site: site} do - {:ok, device} = - Devices.create_device(%{ - name: "Router 3", - ip_address: "192.168.1.3", - site_id: site.id, - organization_id: site.organization_id - }) - - # Start polling - assert {:ok, _job} = DevicePollerWorker.start_polling(device.id) - - # Stop polling - assert {:ok, cancelled_jobs} = DevicePollerWorker.stop_polling(device.id) - refute cancelled_jobs == [] - end - end - - describe "race condition handling" do - @describetag :skip - - test "handles device deletion during poll gracefully", %{organization: org, site: site} do - {:ok, device} = - Devices.create_device(%{ - name: "Test Device", - ip_address: "192.168.1.100", - snmp_enabled: true, - snmp_version: "2c", - snmp_community: "public", - site_id: site.id, - organization_id: org.id - }) - - # Perform polling on device without SNMP setup (no sensors/interfaces) - # Should complete without crashing - assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) - - # Delete device - Devices.delete_device(device) - - # Verify polling with non-existent device doesn't crash - assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) - end - - test "discards poll results when device is reassigned to agent", %{ - organization: org, - site: site - } do - # Create a local agent token (is_cloud_poller defaults to false) - {:ok, agent_token, _token} = Towerops.Agents.create_agent_token(org.id, "Local Agent") - - {:ok, device} = - Devices.create_device(%{ - name: "Test Device", - ip_address: "192.168.1.100", - snmp_enabled: true, - snmp_version: "2c", - snmp_community: "public", - site_id: site.id, - organization_id: org.id - }) - - # Create SNMP device - snmp_device = - %Device{} - |> Device.changeset(%{ - device_id: device.id, - sys_name: "test-device", - sys_descr: "Test Device" - }) - |> Repo.insert!() - - # Create a sensor - %Sensor{} - |> Sensor.changeset(%{ - snmp_device_id: snmp_device.id, - sensor_type: "temperature", - sensor_index: "1", - sensor_oid: "1.3.6.1.2.1.99.1.1.1.4.1", - sensor_descr: "Test Sensor", - sensor_unit: "C", - sensor_divisor: 1, - current_reading: 25.0 - }) - |> Repo.insert!() - - # Initial state: Phoenix should poll (no agent assigned) - assert Towerops.Agents.should_phoenix_poll_device?(device) - - # Mock SNMP to simulate slow polling - poll_started = :erlang.monotonic_time(:millisecond) - - expect(SnmpMock, :get, fn _, _, _ -> - # Simulate device reassignment during poll - if :erlang.monotonic_time(:millisecond) - poll_started < 100 do - # First call - assign to agent - {:ok, _} = Towerops.Agents.assign_device_to_agent(agent_token.id, device.id) - Process.sleep(50) - end - - {:ok, 30} - end) - - expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end) - - # Perform polling - should detect reassignment and discard results - assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) - - # Verify device is now assigned to local agent - device = Repo.reload!(device) - refute Towerops.Agents.should_phoenix_poll_device?(device) - end - - test "uses fresh poll interval when rescheduling after config change", %{ - organization: org, - site: site - } do - {:ok, device} = - Devices.create_device(%{ - name: "Test Device", - ip_address: "192.168.1.100", - snmp_enabled: true, - check_interval_seconds: 60, - site_id: site.id, - organization_id: org.id - }) - - # Change interval during "polling" (before scheduling next poll) - {:ok, _device} = Devices.update_device(device, %{check_interval_seconds: 300}) - - # Perform polling - should use NEW interval (300s) when scheduling next poll - # Device has no SNMP setup, so it won't actually poll but will reschedule - assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) - - # The code path re-fetches the device for fresh config before rescheduling - end - - test "stops rescheduling when device is deleted", %{organization: org, site: site} do - {:ok, device} = - Devices.create_device(%{ - name: "Test Device", - ip_address: "192.168.1.100", - snmp_enabled: true, - site_id: site.id, - organization_id: org.id - }) - - # Perform polling - assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) - - # Delete device - Devices.delete_device(device) - - # Perform polling again - should not crash and not reschedule - assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) - end - end - - describe "job uniqueness" do - test "only one job per device exists at a time", %{site: site} do - {:ok, device} = - Devices.create_device(%{ - name: "Unique Test", - ip_address: "192.168.1.200", - site_id: site.id, - organization_id: site.organization_id, - snmp_enabled: true, - check_interval_seconds: 300 - }) - - # Clear jobs created by create_device - Repo.delete_all(Oban.Job) - - # Insert two jobs for the same device - {:ok, _job1} = DevicePollerWorker.start_polling(device.id) - {:ok, _job2} = DevicePollerWorker.start_polling(device.id) - - # Should only have one job in the database - job_count = - Oban.Job - |> Ecto.Query.where(worker: "Towerops.Workers.DevicePollerWorker") - |> Repo.aggregate(:count) - - assert job_count == 1 - end - - test "new job replaces scheduled_at of existing job", %{site: site} do - {:ok, device} = - Devices.create_device(%{ - name: "Replace Test", - ip_address: "192.168.1.201", - site_id: site.id, - organization_id: site.organization_id, - snmp_enabled: true, - check_interval_seconds: 300 - }) - - # Clear jobs created by create_device - Repo.delete_all(Oban.Job) - - # Insert first job scheduled far in the future - {:ok, job1} = - %{device_id: device.id} - |> DevicePollerWorker.new(schedule_in: 3600) - |> Oban.insert() - - original_scheduled_at = job1.scheduled_at - - # Insert second job scheduled sooner - {:ok, _job2} = - %{device_id: device.id} - |> DevicePollerWorker.new(schedule_in: 60) - |> Oban.insert() - - # Reload the job from DB - should have updated scheduled_at - updated_job = Repo.get!(Oban.Job, job1.id) - assert DateTime.before?(updated_job.scheduled_at, original_scheduled_at) - end - - test "self-scheduling works while job is executing", %{site: site} do - {:ok, device} = - Devices.create_device(%{ - name: "Executing Test", - ip_address: "192.168.1.202", - site_id: site.id, - organization_id: site.organization_id, - snmp_enabled: true, - check_interval_seconds: 300 - }) - - # Clear jobs created by create_device - Repo.delete_all(Oban.Job) - - # Insert a job and manually set it to executing state - {:ok, job1} = - %{device_id: device.id} - |> DevicePollerWorker.new() - |> Oban.insert() - - # Transition the job to executing state - Repo.update_all( - Ecto.Query.where(Oban.Job, id: ^job1.id), - set: [state: "executing"] - ) - - # Insert another job for the same device (simulating self-scheduling) - {:ok, job2} = - %{device_id: device.id} - |> DevicePollerWorker.new(schedule_in: 300) - |> Oban.insert() - - # Should succeed - the new job should be a different job - assert job2.id != job1.id - - # Should have 2 jobs: one executing, one scheduled - job_count = - Oban.Job - |> Ecto.Query.where(worker: "Towerops.Workers.DevicePollerWorker") - |> Repo.aggregate(:count) - - assert job_count == 2 - end - - test "max_attempts is 1", %{site: site} do - {:ok, device} = - Devices.create_device(%{ - name: "Max Attempts Test", - ip_address: "192.168.1.203", - site_id: site.id, - organization_id: site.organization_id, - snmp_enabled: true - }) - - # Clear jobs created by create_device - Repo.delete_all(Oban.Job) - - {:ok, job} = DevicePollerWorker.start_polling(device.id) - assert job.max_attempts == 1 - end - end - - describe "reliability fixes - Task.yield_many race condition" do - @tag :skip - test "handles Task.yield_many result count mismatch gracefully", %{site: site} do - # SKIP: Oban.Testing API changed - perform_job now expects job struct - # The underlying fix is implemented and working in production code - # This test verifies the fix for the race condition where Task.yield_many - # can return fewer results than tasks if any crash or timeout. - # The fix adds length validation and error logging. - - {:ok, device} = - Devices.create_device(%{ - name: "Race Condition Test", - ip_address: "192.168.1.250", - site_id: site.id, - organization_id: site.organization_id, - snmp_enabled: true - }) - - # Create SNMP device - {:ok, snmp_device} = - Repo.insert(%Device{ - device_id: device.id, - sys_descr: "Test Device", - manufacturer: "Test", - model: "Test Model" - }) - - # Add sensor - Repo.insert!(%Sensor{ - snmp_device_id: snmp_device.id, - sensor_index: "1", - sensor_descr: "Test Sensor", - sensor_type: "temperature", - sensor_oid: ".1.3.6.1.4.1.9.9.13.1.3.1.3.1" - }) - - # Verify worker runs without crashing (fix prevents crashes from mismatched results) - assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) - end - end -end diff --git a/test/towerops_web/channels/agent_channel_test.exs.bak b/test/towerops_web/channels/agent_channel_test.exs.bak deleted file mode 100644 index 29ce914c..00000000 --- a/test/towerops_web/channels/agent_channel_test.exs.bak +++ /dev/null @@ -1,1378 +0,0 @@ -defmodule ToweropsWeb.AgentChannelTest do - use Towerops.DataCase, async: false - - import Phoenix.ChannelTest - - alias Towerops.AccountsFixtures - alias Towerops.Agent.AgentError - alias Towerops.Agent.AgentHeartbeat - alias Towerops.Agent.AgentJob - alias Towerops.Agent.AgentJobList - alias Towerops.Agent.CredentialTestResult - alias Towerops.Agent.MikrotikResult - alias Towerops.Agent.MikrotikSentence - alias Towerops.Agent.MonitoringCheck - alias Towerops.Agent.SnmpResult - alias Towerops.Agents - alias Towerops.AgentsFixtures - alias Towerops.DevicesFixtures - alias Towerops.OrganizationsFixtures - alias Towerops.Snmp.AgentDiscovery - alias Towerops.Snmp.Sensor - alias ToweropsWeb.AgentSocket - - @endpoint ToweropsWeb.Endpoint - - setup do - # Create organization, site, device, and agent token - user = AccountsFixtures.user_fixture() - organization = OrganizationsFixtures.organization_fixture(user.id) - - device = - DevicesFixtures.device_fixture(%{ - organization_id: organization.id, - snmp_version: "2c", - snmp_community: "public" - }) - - {:ok, agent_token, token_string} = - AgentsFixtures.agent_token_fixture(organization.id) - - # Assign device to agent - {:ok, _assignment} = - AgentsFixtures.agent_assignment_fixture(agent_token.id, device.id) - - # Connect socket and join channel - {:ok, socket} = connect(AgentSocket, %{}) - - {:ok, _, socket} = - subscribe_and_join(socket, "agent:#{agent_token.id}", %{"token" => token_string}) - - # Drain the initial :send_jobs message pushed on join - assert_push "jobs", _initial_jobs - - %{ - socket: socket, - device: device, - agent_token: agent_token, - token_string: token_string, - organization: organization - } - end - - # Helper to encode a protobuf struct to base64 payload map - defp encode_payload(protobuf_struct) do - binary = protobuf_struct.__struct__.encode(protobuf_struct) - %{"binary" => Base.encode64(binary)} - end - - # Poll for a condition to become true, exiting early on success - defp poll_until(fun, opts \\ []) do - max_attempts = Keyword.get(opts, :max_attempts, 20) - delay_ms = Keyword.get(opts, :delay_ms, 5) - - Enum.reduce_while(1..max_attempts, nil, fn _, _ -> - case fun.() do - nil -> - Process.sleep(delay_ms) - {:cont, nil} - - false -> - Process.sleep(delay_ms) - {:cont, nil} - - result -> - {:halt, result} - end - end) || fun.() - end - - defp build_heartbeat(attrs \\ %{}) do - Map.merge( - %AgentHeartbeat{ - version: "1.0.0", - hostname: "test-agent", - uptime_seconds: 3600, - ip_address: "", - arch: "x86_64" - }, - attrs - ) - end - - defp build_agent_error(device_id) do - %AgentError{ - device_id: device_id, - job_id: "poll:#{device_id}", - message: "SNMP timeout", - timestamp: DateTime.to_unix(DateTime.utc_now()) - } - end - - defp build_monitoring_check(device_id, status) do - %MonitoringCheck{ - device_id: device_id, - status: status, - response_time_ms: 10.0, - timestamp: DateTime.to_unix(DateTime.utc_now()) - } - end - - defp build_credential_test_result(test_id, success) do - %CredentialTestResult{ - test_id: test_id, - success: success, - error_message: if(success, do: "", else: "Connection refused"), - system_description: if(success, do: "Test Device v1.0", else: ""), - timestamp: DateTime.to_unix(DateTime.utc_now()) - } - end - - defp build_mikrotik_result(device_id, opts \\ []) do - %MikrotikResult{ - device_id: device_id, - job_id: Keyword.get(opts, :job_id, "mikrotik:#{device_id}"), - sentences: Keyword.get(opts, :sentences, []), - error: Keyword.get(opts, :error, ""), - timestamp: DateTime.to_unix(DateTime.utc_now()) - } - end - - # ── Stage 1: join/3 and terminate/2 ────────────────────────────────── - - describe "join/3" do - test "join with invalid token returns error" do - {:ok, socket} = connect(AgentSocket, %{}) - - assert {:error, %{reason: "unauthorized"}} = - subscribe_and_join(socket, "agent:some-id", %{"token" => "invalid-token"}) - end - - test "join with missing token returns error" do - {:ok, socket} = connect(AgentSocket, %{}) - - assert {:error, %{reason: "missing token"}} = - subscribe_and_join(socket, "agent:some-id", %{}) - end - - test "join sets socket assigns", %{socket: socket, agent_token: agent_token, organization: organization} do - assert socket.assigns.agent_token_id == agent_token.id - assert socket.assigns.organization_id == organization.id - assert Map.has_key?(socket.assigns, :debug_enabled) - assert Map.has_key?(socket.assigns, :last_heartbeat_at) - end - - test "join broadcasts agent_connected", %{organization: organization} do - # Subscribe to health topic before joining - Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health") - - {:ok, new_agent_token, new_token_string} = - AgentsFixtures.agent_token_fixture(organization.id) - - new_agent_token_id = new_agent_token.id - - {:ok, new_socket} = connect(AgentSocket, %{}) - - {:ok, _, _socket} = - subscribe_and_join(new_socket, "agent:#{new_agent_token.id}", %{"token" => new_token_string}) - - assert_receive {:agent_connected, ^new_agent_token_id, _org_id} - end - - test "terminate broadcasts agent_disconnected", %{socket: socket, agent_token: agent_token} do - Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health") - - agent_token_id = agent_token.id - - Process.flag(:trap_exit, true) - leave(socket) - - assert_receive {:agent_disconnected, ^agent_token_id, _org_id} - end - end - - # ── Stage 2: handle_in "heartbeat" ────────────────────────────────── - - describe "handle_in heartbeat" do - test "valid heartbeat updates agent token in DB", %{socket: socket, agent_token: agent_token} do - heartbeat = build_heartbeat(%{version: "2.0.0"}) - payload = encode_payload(heartbeat) - - push(socket, "heartbeat", payload) - - # Poll until DB update completes - updated_token = - poll_until(fn -> - token = Agents.get_agent_token!(agent_token.id) - if token.last_seen_at && token.metadata["version"] == "2.0.0", do: token - end) - - assert updated_token.last_seen_at - assert updated_token.metadata["version"] == "2.0.0" - end - - test "valid heartbeat broadcasts to agents:health", %{socket: socket, agent_token: agent_token} do - Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health") - - agent_token_id = agent_token.id - heartbeat = build_heartbeat() - payload = encode_payload(heartbeat) - - push(socket, "heartbeat", payload) - - assert_receive {:agent_heartbeat, ^agent_token_id, _org_id} - end - - test "invalid base64 heartbeat is handled gracefully", %{socket: socket} do - push(socket, "heartbeat", %{"binary" => "not-valid-base64!!!"}) - - # Channel should not crash - verify by sending another message - ref = push(socket, "heartbeat", encode_payload(build_heartbeat())) - refute_reply ref, :error - end - - test "invalid protobuf heartbeat is handled gracefully", %{socket: socket} do - push(socket, "heartbeat", %{"binary" => Base.encode64("not-a-protobuf")}) - - # Channel should not crash - ref = push(socket, "heartbeat", encode_payload(build_heartbeat())) - refute_reply ref, :error - end - end - - # ── Stage 3: handle_in "error" and "credential_test_result" ───────── - - describe "handle_in error" do - test "valid error message is handled without crash", %{socket: socket, device: device} do - error = build_agent_error(device.id) - payload = encode_payload(error) - - push(socket, "error", payload) - - # Channel should not crash - assert Process.alive?(socket.channel_pid) - end - - test "invalid base64 error payload is handled gracefully", %{socket: socket} do - push(socket, "error", %{"binary" => "not-valid-base64!!!"}) - - ref = push(socket, "heartbeat", encode_payload(build_heartbeat())) - refute_reply ref, :error - end - end - - describe "handle_in credential_test_result" do - test "valid credential test result is broadcast", %{socket: socket} do - test_id = Ecto.UUID.generate() - - # Subscribe to the credential test topic - Phoenix.PubSub.subscribe(Towerops.PubSub, "credential_test:#{test_id}") - - result = build_credential_test_result(test_id, true) - payload = encode_payload(result) - - push(socket, "credential_test_result", payload) - - assert_receive {:credential_test_result, received_result} - assert received_result.test_id == test_id - assert received_result.success == true - end - - test "failed credential test result is broadcast with error", %{socket: socket} do - test_id = Ecto.UUID.generate() - - Phoenix.PubSub.subscribe(Towerops.PubSub, "credential_test:#{test_id}") - - result = build_credential_test_result(test_id, false) - payload = encode_payload(result) - - push(socket, "credential_test_result", payload) - - assert_receive {:credential_test_result, received_result} - assert received_result.success == false - assert received_result.error_message == "Connection refused" - end - - test "invalid base64 credential test result is handled gracefully", %{socket: socket} do - push(socket, "credential_test_result", %{"binary" => "not-valid-base64!!!"}) - - ref = push(socket, "heartbeat", encode_payload(build_heartbeat())) - refute_reply ref, :error - end - end - - # ── Stage 4: handle_in "monitoring_check" ─────────────────────────── - - describe "handle_in monitoring_check" do - test "valid monitoring check creates check record", %{socket: socket, device: device} do - import Ecto.Query - - check = build_monitoring_check(device.id, "success") - payload = encode_payload(check) - - push(socket, "monitoring_check", payload) - - Process.sleep(25) - - # Verify check was stored - checks = Towerops.Repo.all(from(m in Towerops.Monitoring.MonitoringCheck, where: m.device_id == ^device.id)) - assert checks != [] - assert hd(checks).status == "success" - end - - test "monitoring check updates device status to up on success", %{socket: socket, device: device} do - check = build_monitoring_check(device.id, "success") - payload = encode_payload(check) - - push(socket, "monitoring_check", payload) - - Process.sleep(25) - - updated_device = Towerops.Devices.get_device!(device.id) - assert updated_device.status == :up - end - - test "monitoring check creates alert on status change to down", %{socket: socket, device: device} do - # First set device as up - Towerops.Devices.update_device_status(device, :up) - - check = build_monitoring_check(device.id, "failure") - payload = encode_payload(check) - - push(socket, "monitoring_check", payload) - - Process.sleep(25) - - updated_device = Towerops.Devices.get_device!(device.id) - assert updated_device.status == :down - - # Verify alert was created - assert Towerops.Alerts.has_active_alert?(device.id, :device_down) - end - - test "monitoring check creates device_up alert on recovery", %{socket: socket, device: device} do - # Set device as down first - Towerops.Devices.update_device_status(device, :down) - - check = build_monitoring_check(device.id, "success") - payload = encode_payload(check) - - push(socket, "monitoring_check", payload) - - Process.sleep(25) - - updated_device = Towerops.Devices.get_device!(device.id) - assert updated_device.status == :up - end - - test "invalid base64 monitoring check is handled gracefully", %{socket: socket} do - push(socket, "monitoring_check", %{"binary" => "not-valid-base64!!!"}) - - ref = push(socket, "heartbeat", encode_payload(build_heartbeat())) - refute_reply ref, :error - end - - test "invalid protobuf monitoring check is handled gracefully", %{socket: socket} do - push(socket, "monitoring_check", %{"binary" => Base.encode64("not-a-protobuf")}) - - ref = push(socket, "heartbeat", encode_payload(build_heartbeat())) - refute_reply ref, :error - end - end - - # ── Stage 5: handle_in "mikrotik_result" and catch-all ───────────── - - describe "handle_in mikrotik_result" do - test "valid mikrotik result is processed without crash", %{socket: socket, device: device} do - result = build_mikrotik_result(device.id) - payload = encode_payload(result) - - push(socket, "mikrotik_result", payload) - - # Channel should not crash - ref = push(socket, "heartbeat", encode_payload(build_heartbeat())) - refute_reply ref, :error - end - - test "mikrotik result with sentences is processed", %{socket: socket, device: device} do - sentence = %MikrotikSentence{ - attributes: %{"name" => "router1"} - } - - result = build_mikrotik_result(device.id, sentences: [sentence]) - payload = encode_payload(result) - - push(socket, "mikrotik_result", payload) - - ref = push(socket, "heartbeat", encode_payload(build_heartbeat())) - refute_reply ref, :error - end - - test "invalid base64 mikrotik result is handled gracefully", %{socket: socket} do - push(socket, "mikrotik_result", %{"binary" => "not-valid-base64!!!"}) - - ref = push(socket, "heartbeat", encode_payload(build_heartbeat())) - refute_reply ref, :error - end - - test "invalid protobuf mikrotik result is handled gracefully", %{socket: socket} do - push(socket, "mikrotik_result", %{"binary" => Base.encode64("not-a-protobuf")}) - - ref = push(socket, "heartbeat", encode_payload(build_heartbeat())) - refute_reply ref, :error - end - end - - describe "handle_in catch-all" do - test "unknown event is handled gracefully", %{socket: socket} do - push(socket, "unknown_event", %{"data" => "test"}) - - # Channel should not crash - ref = push(socket, "heartbeat", encode_payload(build_heartbeat())) - refute_reply ref, :error - end - end - - # ── Stage 6: handle_info lifecycle messages ───────────────────────── - - describe "handle_info :send_jobs" do - test "pushes jobs to channel", %{socket: socket} do - send(socket.channel_pid, :send_jobs) - - assert_push "jobs", %{binary: jobs_binary} - - {:ok, decoded_binary} = Base.decode64(jobs_binary) - job_list = AgentJobList.decode(decoded_binary) - assert is_list(job_list.jobs) - end - end - - describe "handle_info :check_heartbeat" do - test "reschedules when heartbeat is recent", %{socket: socket} do - # last_heartbeat_at was set during join (recent), so check should pass - send(socket.channel_pid, :check_heartbeat) - - # Channel should still be alive - Process.sleep(50) - assert Process.alive?(socket.channel_pid) - end - - test "stops channel when heartbeat is stale", %{socket: socket} do - Process.flag(:trap_exit, true) - - # Monitor the channel process before it stops - ref = Process.monitor(socket.channel_pid) - - # Set last_heartbeat_at to >300 seconds ago to trigger timeout - stale_time = DateTime.add(DateTime.utc_now(), -400, :second) - - # Phoenix Channel.Server GenServer state is the socket struct directly - :sys.replace_state(socket.channel_pid, fn socket_state -> - Phoenix.Socket.assign(socket_state, :last_heartbeat_at, stale_time) - end) - - send(socket.channel_pid, :check_heartbeat) - - assert_receive {:DOWN, ^ref, :process, _, _}, 5_000 - end - end - - describe "handle_info :token_disabled" do - test "stops channel", %{socket: socket} do - Process.flag(:trap_exit, true) - ref = Process.monitor(socket.channel_pid) - - send(socket.channel_pid, :token_disabled) - - assert_receive {:DOWN, ^ref, :process, _, _}, 5_000 - end - end - - describe "handle_info :restart_requested" do - test "pushes restart and stops channel", %{socket: socket} do - Process.flag(:trap_exit, true) - ref = Process.monitor(socket.channel_pid) - - send(socket.channel_pid, :restart_requested) - - assert_push "restart", %{} - assert_receive {:DOWN, ^ref, :process, _, _}, 5_000 - end - end - - describe "handle_info {:update_requested, url, checksum}" do - test "pushes update with url and checksum", %{socket: socket} do - send(socket.channel_pid, {:update_requested, "https://example.com/agent.tar.gz", "sha256:abc123"}) - - assert_push "update", %{url: "https://example.com/agent.tar.gz", checksum: "sha256:abc123"} - end - end - - # ── Stage 7: handle_info PubSub messages ──────────────────────────── - - describe "handle_info {:assignments_changed, _}" do - test "triggers debounced job refresh", %{socket: socket} do - send(socket.channel_pid, {:assignments_changed, :assigned}) - - # Jobs push should come after debounce delay (50ms in test) - assert_push "jobs", %{binary: _jobs_binary}, 150 - end - - test "debounces rapid assignment changes", %{socket: socket} do - # Send multiple rapid changes - send(socket.channel_pid, {:assignments_changed, :assigned}) - send(socket.channel_pid, {:assignments_changed, :unassigned}) - send(socket.channel_pid, {:assignments_changed, :assigned}) - - # Should only get one push (debounced) - 50ms debounce in test - assert_push "jobs", %{binary: _}, 150 - refute_push "jobs", %{}, 50 - end - end - - describe "handle_info {:discovery_requested, device_id}" do - test "pushes discovery job for valid device", %{socket: socket, device: device} do - send(socket.channel_pid, {:discovery_requested, device.id}) - - assert_push "discovery_job", %{binary: jobs_binary} - - {:ok, decoded_binary} = Base.decode64(jobs_binary) - job_list = AgentJobList.decode(decoded_binary) - - discover_job = Enum.find(job_list.jobs, &(&1.job_type == :DISCOVER)) - assert discover_job - assert discover_job.device_id == device.id - end - - test "handles nonexistent device without crash", %{socket: socket} do - send(socket.channel_pid, {:discovery_requested, Ecto.UUID.generate()}) - - # Channel should not crash - Process.sleep(50) - assert Process.alive?(socket.channel_pid) - end - end - - describe "handle_info {:credential_test_requested, test_id, config}" do - test "pushes credential test job", %{socket: socket} do - test_id = Ecto.UUID.generate() - - snmp_config = [ - ip: "192.168.1.100", - port: 161, - version: "2c", - community: "public", - v3_security_level: "", - v3_username: "", - v3_auth_protocol: "", - v3_auth_password: "", - v3_priv_protocol: "", - v3_priv_password: "" - ] - - send(socket.channel_pid, {:credential_test_requested, test_id, snmp_config}) - - assert_push "jobs", %{binary: jobs_binary} - - {:ok, decoded_binary} = Base.decode64(jobs_binary) - job_list = AgentJobList.decode(decoded_binary) - - test_job = Enum.find(job_list.jobs, &(&1.job_type == :TEST_CREDENTIALS)) - assert test_job - assert test_job.job_id == test_id - assert test_job.snmp_device.ip == "192.168.1.100" - assert test_job.snmp_device.community == "public" - end - end - - describe "handle_info {:live_poll_requested, ...}" do - test "pushes live poll job for valid device", %{socket: socket, device: device} do - sensor_oids = ["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.3.0"] - reply_topic = "live_poll:#{device.id}:sensor_data" - - send(socket.channel_pid, {:live_poll_requested, device.id, sensor_oids, reply_topic}) - - assert_push "jobs", %{binary: jobs_binary} - - {:ok, decoded_binary} = Base.decode64(jobs_binary) - job_list = AgentJobList.decode(decoded_binary) - - poll_job = Enum.find(job_list.jobs, &(&1.job_type == :POLL)) - assert poll_job - assert poll_job.device_id == device.id - assert poll_job.job_id == "live_poll:#{device.id}:#{reply_topic}" - - # Verify requested OIDs are in the query - query_oids = Enum.flat_map(poll_job.queries, & &1.oids) - assert "1.3.6.1.2.1.1.1.0" in query_oids - assert "1.3.6.1.2.1.1.3.0" in query_oids - end - - test "handles nonexistent device without crash", %{socket: socket} do - send( - socket.channel_pid, - {:live_poll_requested, Ecto.UUID.generate(), ["1.3.6.1.2.1.1.1.0"], "reply:topic"} - ) - - Process.sleep(50) - assert Process.alive?(socket.channel_pid) - end - end - - describe "handle_info {:live_poll_timeout, reply_topic}" do - test "broadcasts error via channel process", %{socket: socket} do - reply_topic = "live_poll:test:timeout_test" - Phoenix.PubSub.subscribe(Towerops.PubSub, reply_topic) - - send(socket.channel_pid, {:live_poll_timeout, reply_topic}) - - assert_receive {:live_poll_error, :timeout}, 1_000 - end - end - - describe "handle_info {:backup_requested, job}" do - test "pushes backup job to agent", %{socket: socket, device: device} do - job = %AgentJob{ - job_id: "backup:#{device.id}", - job_type: :MIKROTIK, - device_id: device.id, - mikrotik_device: nil, - mikrotik_commands: [] - } - - send(socket.channel_pid, {:backup_requested, job}) - - assert_push "backup_job", %{binary: jobs_binary} - - {:ok, decoded_binary} = Base.decode64(jobs_binary) - job_list = AgentJobList.decode(decoded_binary) - - backup_job = hd(job_list.jobs) - assert backup_job.job_id == "backup:#{device.id}" - assert backup_job.device_id == device.id - end - end - - describe "handle_info {:poll_after_discovery, device_id}" do - test "pushes poll jobs for discovered device", %{socket: socket, device: device} do - # First run discovery so device has an snmp_device record - # (build_polling_queries requires device.snmp_device.sensors and .interfaces) - discovery_result = %SnmpResult{ - device_id: device.id, - job_type: :DISCOVER, - job_id: "discover:#{device.id}", - timestamp: DateTime.to_unix(DateTime.utc_now()), - oid_values: %{ - "1.3.6.1.2.1.1.1.0" => "Test Device", - "1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.9.1.1", - "1.3.6.1.2.1.1.3.0" => "123456", - "1.3.6.1.2.1.1.4.0" => "admin@test.com", - "1.3.6.1.2.1.1.5.0" => "test-device", - "1.3.6.1.2.1.1.6.0" => "Test Location" - } - } - - push(socket, "result", encode_payload(discovery_result)) - - # Wait for discovery to complete and the automatic poll_after_discovery - assert_push "jobs", %{binary: jobs_binary}, 5_000 - - {:ok, decoded_binary} = Base.decode64(jobs_binary) - job_list = AgentJobList.decode(decoded_binary) - - # Should have at least a poll job - assert job_list.jobs != [] - end - - test "handles nonexistent device without crash", %{socket: socket} do - Process.flag(:trap_exit, true) - send(socket.channel_pid, {:poll_after_discovery, Ecto.UUID.generate()}) - - Process.sleep(50) - assert Process.alive?(socket.channel_pid) - end - end - - # ── Stage 8: SNMP result edge cases ───────────────────────────────── - - describe "handle_in result edge cases" do - test "invalid base64 result is handled gracefully", %{socket: socket} do - push(socket, "result", %{"binary" => "not-valid-base64!!!"}) - - ref = push(socket, "heartbeat", encode_payload(build_heartbeat())) - refute_reply ref, :error - end - - test "invalid protobuf result is handled gracefully", %{socket: socket} do - push(socket, "result", %{"binary" => Base.encode64("not-a-protobuf")}) - - ref = push(socket, "heartbeat", encode_payload(build_heartbeat())) - refute_reply ref, :error - end - - test "result for nonexistent device is handled", %{socket: socket} do - result = %SnmpResult{ - device_id: Ecto.UUID.generate(), - job_type: :POLL, - job_id: "poll:#{Ecto.UUID.generate()}", - timestamp: DateTime.to_unix(DateTime.utc_now()), - oid_values: %{"1.3.6.1.2.1.1.1.0" => "Test"} - } - - payload = encode_payload(result) - push(socket, "result", payload) - - # Channel should not crash - Process.sleep(50) - assert Process.alive?(socket.channel_pid) - end - - test "result for device in wrong organization is rejected", %{socket: socket} do - # Create a device in a different organization - other_user = AccountsFixtures.user_fixture() - other_org = OrganizationsFixtures.organization_fixture(other_user.id) - - other_device = - DevicesFixtures.device_fixture(%{ - organization_id: other_org.id, - snmp_version: "2c", - snmp_community: "public" - }) - - result = %SnmpResult{ - device_id: other_device.id, - job_type: :POLL, - job_id: "poll:#{other_device.id}", - timestamp: DateTime.to_unix(DateTime.utc_now()), - oid_values: %{"1.3.6.1.2.1.1.1.0" => "Test"} - } - - payload = encode_payload(result) - push(socket, "result", payload) - - # Channel should not crash - Process.sleep(50) - assert Process.alive?(socket.channel_pid) - end - - test "live poll result broadcasts to reply topic", %{socket: socket, device: device} do - reply_topic = "live_poll:#{device.id}:sensor_data" - Phoenix.PubSub.subscribe(Towerops.PubSub, reply_topic) - - result = %SnmpResult{ - device_id: device.id, - job_type: :POLL, - job_id: "live_poll:#{device.id}:#{reply_topic}", - timestamp: DateTime.to_unix(DateTime.utc_now()), - oid_values: %{ - "1.3.6.1.2.1.1.1.0" => "Test Device", - "1.3.6.1.2.1.1.3.0" => "123456" - } - } - - payload = encode_payload(result) - push(socket, "result", payload) - - assert_receive {:live_poll_result, oid_values}, 2_000 - assert Map.has_key?(oid_values, "1.3.6.1.2.1.1.1.0") - end - end - - # ── Stage 9: Polling result processing with sensors/interfaces ───── - - describe "poll result with sensor and interface data" do - setup %{device: device} do - # First run discovery to create snmp_device record - oid_values = %{ - "1.3.6.1.2.1.1.1.0" => "Test Device", - "1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.9.1.1", - "1.3.6.1.2.1.1.3.0" => "123456", - "1.3.6.1.2.1.1.4.0" => "admin@test.com", - "1.3.6.1.2.1.1.5.0" => "test-device", - "1.3.6.1.2.1.1.6.0" => "Test Location", - # Interface data - "1.3.6.1.2.1.2.2.1.1.1" => "1", - "1.3.6.1.2.1.2.2.1.2.1" => "GigabitEthernet0/1", - "1.3.6.1.2.1.2.2.1.3.1" => "6", - "1.3.6.1.2.1.2.2.1.5.1" => "1000000000", - "1.3.6.1.2.1.2.2.1.6.1" => "aa:bb:cc:dd:ee:ff", - "1.3.6.1.2.1.2.2.1.7.1" => "1", - "1.3.6.1.2.1.2.2.1.8.1" => "1" - } - - {:ok, _discovered} = AgentDiscovery.process_agent_discovery(device, oid_values) - - # Create a sensor on the snmp_device - snmp_device = Towerops.Snmp.get_device_with_associations(device.id) - - {:ok, sensor} = - Towerops.Repo.insert(%Sensor{ - snmp_device_id: snmp_device.id, - sensor_type: "temperature", - sensor_index: "1001", - sensor_oid: "1.3.6.1.4.1.9.9.13.1.3.1.3.1001", - sensor_descr: "CPU Temperature", - sensor_unit: "celsius", - sensor_divisor: 1 - }) - - # Reload device with all associations - device_with_details = Towerops.Devices.get_device_with_details(device.id) - - %{ - discovered_device: device_with_details, - snmp_device: snmp_device, - sensor: sensor, - interface: hd(snmp_device.interfaces) - } - end - - test "poll result stores sensor readings", %{ - socket: socket, - device: device, - sensor: sensor - } do - result = %SnmpResult{ - device_id: device.id, - job_type: :POLL, - job_id: "poll:#{device.id}", - timestamp: DateTime.to_unix(DateTime.utc_now()), - oid_values: %{ - sensor.sensor_oid => "45" - } - } - - push(socket, "result", encode_payload(result)) - Process.sleep(50) - - # Verify sensor reading was stored - updated_sensor = Towerops.Repo.get!(Sensor, sensor.id) - assert updated_sensor.last_value == 45.0 - end - - test "poll result stores interface stats", %{ - socket: socket, - device: device, - interface: interface - } do - idx = interface.if_index - - result = %SnmpResult{ - device_id: device.id, - job_type: :POLL, - job_id: "poll:#{device.id}", - timestamp: DateTime.to_unix(DateTime.utc_now()), - oid_values: %{ - "1.3.6.1.2.1.31.1.1.1.6.#{idx}" => "1000000", - "1.3.6.1.2.1.31.1.1.1.10.#{idx}" => "2000000", - "1.3.6.1.2.1.2.2.1.14.#{idx}" => "5", - "1.3.6.1.2.1.2.2.1.20.#{idx}" => "3", - "1.3.6.1.2.1.2.2.1.13.#{idx}" => "1", - "1.3.6.1.2.1.2.2.1.19.#{idx}" => "0" - } - } - - push(socket, "result", encode_payload(result)) - Process.sleep(50) - - # Verify interface stat was stored - stats = Towerops.Snmp.get_interface_stats(interface.id) - assert stats != [] - stat = hd(stats) - assert stat.if_in_octets == 1_000_000 - assert stat.if_out_octets == 2_000_000 - end - - test "poll result with leading dot OIDs is normalized", %{ - socket: socket, - device: device, - sensor: sensor - } do - # Agent sends OIDs with leading dots, channel should normalize them - result = %SnmpResult{ - device_id: device.id, - job_type: :POLL, - job_id: "poll:#{device.id}", - timestamp: DateTime.to_unix(DateTime.utc_now()), - oid_values: %{ - ".#{sensor.sensor_oid}" => "72" - } - } - - push(socket, "result", encode_payload(result)) - Process.sleep(50) - - updated_sensor = Towerops.Repo.get!(Sensor, sensor.id) - assert updated_sensor.last_value == 72.0 - end - - test "poll result with sensor divisor scales value", %{ - socket: socket, - device: device, - snmp_device: snmp_device - } do - # Create a sensor with divisor of 10 - {:ok, sensor_with_divisor} = - Towerops.Repo.insert(%Sensor{ - snmp_device_id: snmp_device.id, - sensor_type: "voltage", - sensor_index: "2001", - sensor_oid: "1.3.6.1.4.1.9.9.13.1.2.1.3.2001", - sensor_descr: "Voltage Rail", - sensor_unit: "volts", - sensor_divisor: 10 - }) - - result = %SnmpResult{ - device_id: device.id, - job_type: :POLL, - job_id: "poll:#{device.id}", - timestamp: DateTime.to_unix(DateTime.utc_now()), - oid_values: %{ - sensor_with_divisor.sensor_oid => "120" - } - } - - push(socket, "result", encode_payload(result)) - Process.sleep(50) - - updated_sensor = Towerops.Repo.get!(Sensor, sensor_with_divisor.id) - assert updated_sensor.last_value == 12.0 - end - - test "poll result auto-resolves device_down alert when device comes back up", %{ - socket: socket, - discovered_device: device, - sensor: sensor - } do - # Set device status to down - {:ok, device} = Towerops.Devices.update_device_status(device, :down) - - # Create a device_down alert - {:ok, alert} = - Towerops.Alerts.create_alert(%{ - device_id: device.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now(), - message: "Device is not responding to SNMP" - }) - - assert is_nil(alert.resolved_at) - assert device.status == :down - - # Send successful SNMP polling result - result = %SnmpResult{ - device_id: device.id, - job_type: :POLL, - job_id: "poll:#{device.id}", - timestamp: DateTime.to_unix(DateTime.utc_now()), - oid_values: %{ - sensor.sensor_oid => "45" - } - } - - push(socket, "result", encode_payload(result)) - Process.sleep(50) - - # Verify device status updated to up - updated_device = Towerops.Devices.get_device(device.id) - assert updated_device.status == :up - - # Verify device_down alert was auto-resolved - updated_alert = Towerops.Alerts.get_alert(alert.id) - assert updated_alert.resolved_at - - # Verify device_up alert was created - up_alerts = - device.id - |> Towerops.Alerts.list_devices_alerts() - |> Enum.filter(&(&1.alert_type == "device_up")) - - assert length(up_alerts) == 1 - up_alert = hd(up_alerts) - assert up_alert.message == "Device is now responding to SNMP" - assert up_alert.resolved_at - end - end - - # ── Stage 10: SNMPv3 credential path ──────────────────────────────── - - describe "SNMPv3 device job building" do - test "sends discovery job with v3 credentials", %{organization: organization} do - # Create a v3 device with required SNMPv3 credentials - v3_device = - DevicesFixtures.device_fixture(%{ - organization_id: organization.id, - snmp_version: "3", - snmp_community: "", - snmpv3_username: "testuser", - snmpv3_auth_protocol: "SHA-256", - snmpv3_auth_password: "authpass123", - snmpv3_priv_protocol: "AES", - snmpv3_priv_password: "privpass123", - snmpv3_security_level: "authPriv", - snmpv3_credential_source: "device" - }) - - {:ok, agent_token2, token_string2} = - AgentsFixtures.agent_token_fixture(organization.id) - - {:ok, _} = AgentsFixtures.agent_assignment_fixture(agent_token2.id, v3_device.id) - - {:ok, socket2} = connect(AgentSocket, %{}) - - {:ok, _, socket2} = - subscribe_and_join(socket2, "agent:#{agent_token2.id}", %{"token" => token_string2}) - - # Drain initial jobs - assert_push "jobs", %{binary: jobs_binary} - - {:ok, decoded_binary} = Base.decode64(jobs_binary) - job_list = AgentJobList.decode(decoded_binary) - - discover_job = Enum.find(job_list.jobs, &(&1.job_type == :DISCOVER)) - assert discover_job - assert discover_job.snmp_device.version == "3" - - Process.flag(:trap_exit, true) - leave(socket2) - end - end - - # ── Stage 11: Poll after discovery with monitoring enabled ────────── - - describe "poll after discovery with monitoring" do - test "includes ping job when monitoring is enabled", %{organization: organization} do - # Create device with monitoring enabled - monitored_device = - DevicesFixtures.device_fixture(%{ - organization_id: organization.id, - snmp_version: "2c", - snmp_community: "public", - monitoring_enabled: true - }) - - {:ok, agent_token3, token_string3} = - AgentsFixtures.agent_token_fixture(organization.id) - - {:ok, _} = AgentsFixtures.agent_assignment_fixture(agent_token3.id, monitored_device.id) - - {:ok, socket3} = connect(AgentSocket, %{}) - - {:ok, _, socket3} = - subscribe_and_join(socket3, "agent:#{agent_token3.id}", %{"token" => token_string3}) - - # Drain initial jobs push - assert_push "jobs", %{binary: jobs_binary} - - {:ok, decoded_binary} = Base.decode64(jobs_binary) - job_list = AgentJobList.decode(decoded_binary) - - # Should have both a discover/poll job and a ping job - ping_job = Enum.find(job_list.jobs, &(&1.job_type == :PING)) - assert ping_job - assert ping_job.device_id == monitored_device.id - - Process.flag(:trap_exit, true) - leave(socket3) - end - end - - # ── Stage 12: Device reassignment rejection ───────────────────────── - - describe "device reassignment" do - test "rejects result for device assigned to a different agent", %{ - socket: socket, - organization: organization - } do - # Create a device assigned to a different agent - other_device = - DevicesFixtures.device_fixture(%{ - organization_id: organization.id, - snmp_version: "2c", - snmp_community: "public" - }) - - {:ok, other_agent, _} = AgentsFixtures.agent_token_fixture(organization.id) - {:ok, _} = AgentsFixtures.agent_assignment_fixture(other_agent.id, other_device.id) - - result = %SnmpResult{ - device_id: other_device.id, - job_type: :POLL, - job_id: "poll:#{other_device.id}", - timestamp: DateTime.to_unix(DateTime.utc_now()), - oid_values: %{"1.3.6.1.2.1.1.1.0" => "Test"} - } - - push(socket, "result", encode_payload(result)) - - # Channel should handle gracefully without crash - Process.sleep(50) - assert Process.alive?(socket.channel_pid) - end - end - - # ── Existing tests (preserved) ────────────────────────────────────── - - describe "poll after discovery" do - test "pushes poll jobs after successful discovery result", %{ - socket: socket, - device: device - } do - # Build a minimal discovery result with system info OIDs - # that AgentDiscovery.process_agent_discovery can handle - discovery_result = %SnmpResult{ - device_id: device.id, - job_type: :DISCOVER, - job_id: "discover:#{device.id}", - timestamp: DateTime.to_unix(DateTime.utc_now()), - oid_values: %{ - # sysDescr - "1.3.6.1.2.1.1.1.0" => "Test Device Description", - # sysObjectID - "1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.9.1.1", - # sysUpTime - "1.3.6.1.2.1.1.3.0" => "123456", - # sysContact - "1.3.6.1.2.1.1.4.0" => "admin@test.com", - # sysName - "1.3.6.1.2.1.1.5.0" => "test-device", - # sysLocation - "1.3.6.1.2.1.1.6.0" => "Test Location" - } - } - - binary = SnmpResult.encode(discovery_result) - payload = %{"binary" => Base.encode64(binary)} - - # Send the discovery result to the channel - push(socket, "result", payload) - - # Should receive a "jobs" push with poll job(s) for the discovered device - assert_push "jobs", %{binary: jobs_binary}, 5_000 - - # Decode the job list and verify it contains a POLL job for our device - {:ok, decoded_binary} = Base.decode64(jobs_binary) - job_list = AgentJobList.decode(decoded_binary) - - poll_job = - Enum.find(job_list.jobs, fn job -> - job.job_type == :POLL and job.device_id == device.id - end) - - assert poll_job != nil, "Expected a POLL job for device #{device.id}" - assert poll_job.job_id == "poll:#{device.id}" - end - - test "poll job uses 64-bit HC counters for interface octets", %{ - socket: socket, - device: device - } do - # Build discovery result with interface data so the poll job - # will include interface OIDs - discovery_result = %SnmpResult{ - device_id: device.id, - job_type: :DISCOVER, - job_id: "discover:#{device.id}", - timestamp: DateTime.to_unix(DateTime.utc_now()), - oid_values: %{ - # System info - "1.3.6.1.2.1.1.1.0" => "Test Device Description", - "1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.9.1.1", - "1.3.6.1.2.1.1.3.0" => "123456", - "1.3.6.1.2.1.1.4.0" => "admin@test.com", - "1.3.6.1.2.1.1.5.0" => "test-device", - "1.3.6.1.2.1.1.6.0" => "Test Location", - # ifTable - interface index - "1.3.6.1.2.1.2.2.1.1.1" => "1", - # ifDescr - "1.3.6.1.2.1.2.2.1.2.1" => "GigabitEthernet0/1", - # ifType (ethernetCsmacd = 6) - "1.3.6.1.2.1.2.2.1.3.1" => "6", - # ifSpeed - "1.3.6.1.2.1.2.2.1.5.1" => "1000000000", - # ifPhysAddress - "1.3.6.1.2.1.2.2.1.6.1" => "aa:bb:cc:dd:ee:ff", - # ifAdminStatus (up = 1) - "1.3.6.1.2.1.2.2.1.7.1" => "1", - # ifOperStatus (up = 1) - "1.3.6.1.2.1.2.2.1.8.1" => "1" - } - } - - binary = SnmpResult.encode(discovery_result) - payload = %{"binary" => Base.encode64(binary)} - - push(socket, "result", payload) - - # Receive the poll job pushed after discovery - assert_push "jobs", %{binary: jobs_binary}, 5_000 - - {:ok, decoded_binary} = Base.decode64(jobs_binary) - job_list = AgentJobList.decode(decoded_binary) - - poll_job = - Enum.find(job_list.jobs, fn job -> - job.job_type == :POLL and job.device_id == device.id - end) - - assert poll_job != nil, "Expected a POLL job for device #{device.id}" - - # Collect all OIDs from all queries in the poll job - all_oids = Enum.flat_map(poll_job.queries, & &1.oids) - - # Must use 64-bit HC counters (ifHCInOctets / ifHCOutOctets) - assert Enum.any?(all_oids, &String.starts_with?(&1, "1.3.6.1.2.1.31.1.1.1.6.")), - "Expected ifHCInOctets OIDs (1.3.6.1.2.1.31.1.1.1.6.x) but found: #{inspect(all_oids)}" - - assert Enum.any?(all_oids, &String.starts_with?(&1, "1.3.6.1.2.1.31.1.1.1.10.")), - "Expected ifHCOutOctets OIDs (1.3.6.1.2.1.31.1.1.1.10.x) but found: #{inspect(all_oids)}" - - # Must NOT use old 32-bit counters (ifInOctets / ifOutOctets) - refute Enum.any?(all_oids, &String.starts_with?(&1, "1.3.6.1.2.1.2.2.1.10.")), - "Found old 32-bit ifInOctets OIDs (1.3.6.1.2.1.2.2.1.10.x) - should use HC counters" - - refute Enum.any?(all_oids, &String.starts_with?(&1, "1.3.6.1.2.1.2.2.1.16.")), - "Found old 32-bit ifOutOctets OIDs (1.3.6.1.2.1.2.2.1.16.x) - should use HC counters" - end - end - - describe "discovery job OIDs" do - test "initial discovery job contains expected OIDs", %{socket: socket} do - # Trigger a fresh job push by sending :send_jobs - send(socket.channel_pid, :send_jobs) - - assert_push "jobs", %{binary: jobs_binary}, 5_000 - - {:ok, decoded_binary} = Base.decode64(jobs_binary) - job_list = AgentJobList.decode(decoded_binary) - - discover_job = - Enum.find(job_list.jobs, fn job -> - job.job_type == :DISCOVER - end) - - assert discover_job != nil, "Expected a DISCOVER job in initial jobs" - - # Collect all OIDs from all queries in the discovery job - all_oids = Enum.flat_map(discover_job.queries, & &1.oids) - - # ENTITY-STATE-MIB - assert "1.3.6.1.2.1.131.1.1.1.1" in all_oids, - "Missing ENTITY-STATE-MIB OID (1.3.6.1.2.1.131.1.1.1.1)" - - # HOST-RESOURCES-MIB: hrProcessorTable - assert "1.3.6.1.2.1.25.3.3" in all_oids, - "Missing hrProcessorTable OID (1.3.6.1.2.1.25.3.3)" - - # HOST-RESOURCES-MIB: hrStorageTable - assert "1.3.6.1.2.1.25.2.3" in all_oids, - "Missing hrStorageTable OID (1.3.6.1.2.1.25.2.3)" - - # HOST-RESOURCES-MIB: hrDeviceTable - assert "1.3.6.1.2.1.25.3.2" in all_oids, - "Missing hrDeviceTable OID (1.3.6.1.2.1.25.3.2)" - - # UCD-SNMP-MIB: ssCpuUser - assert "1.3.6.1.4.1.2021.11.9.0" in all_oids, - "Missing ssCpuUser OID (1.3.6.1.4.1.2021.11.9.0)" - end - end - - # ── Stage: Device deletion and job refresh ───────────────────────────────── - - describe "device deletion and job refresh" do - test "agent receives updated job list after device deletion", %{ - device: device, - agent_token: agent_token - } do - # Create a second device to verify the list shrinks correctly - device2 = - DevicesFixtures.device_fixture(%{ - organization_id: device.organization_id, - snmp_version: "2c", - snmp_community: "public", - ip_address: "192.168.1.2" - }) - - {:ok, _assignment} = - AgentsFixtures.agent_assignment_fixture(agent_token.id, device2.id) - - # Trigger initial job refresh to get baseline - Phoenix.PubSub.broadcast( - Towerops.PubSub, - "agent:#{agent_token.id}:assignments", - {:assignments_changed, :test_setup} - ) - - # Should receive jobs message with 2 devices - assert_push "jobs", %{binary: initial_jobs_binary}, 150 - {:ok, decoded_initial} = Base.decode64(initial_jobs_binary) - initial_job_list = AgentJobList.decode(decoded_initial) - initial_device_ids = initial_job_list.jobs |> Enum.map(& &1.device_id) |> Enum.uniq() - assert length(initial_device_ids) == 2 - assert device.id in initial_device_ids - assert device2.id in initial_device_ids - - # Delete the first device - {:ok, _deleted} = Towerops.Devices.delete_device(device) - - # Should receive updated jobs message with only 1 device - assert_push "jobs", %{binary: updated_jobs_binary}, 150 - {:ok, decoded_updated} = Base.decode64(updated_jobs_binary) - updated_job_list = AgentJobList.decode(decoded_updated) - updated_device_ids = updated_job_list.jobs |> Enum.map(& &1.device_id) |> Enum.uniq() - - # Verify the deleted device is NOT in the new job list - assert length(updated_device_ids) == 1 - refute device.id in updated_device_ids - assert device2.id in updated_device_ids - end - - test "agent result for deleted device is rejected", %{socket: socket, device: device} do - # Delete the device - {:ok, _deleted} = Towerops.Devices.delete_device(device) - - # Wait for jobs refresh - assert_push "jobs", _, 150 - - # Try to send a result for the deleted device - result = %SnmpResult{ - device_id: device.id, - job_type: :POLL, - job_id: "poll:#{device.id}", - timestamp: DateTime.to_unix(DateTime.utc_now()), - oid_values: %{"1.3.6.1.2.1.1.1.0" => "Test"} - } - - payload = encode_payload(result) - push(socket, "result", payload) - - # Channel should remain alive (graceful error handling) - Process.sleep(25) - assert Process.alive?(socket.channel_pid) - - # The result should be logged as "Device not found" but not crash - # (This is verified by the channel staying alive) - end - - test "multiple rapid assignment changes are debounced", %{agent_token: agent_token} do - # Trigger multiple rapid assignment changes - for i <- 1..5 do - Phoenix.PubSub.broadcast( - Towerops.PubSub, - "agent:#{agent_token.id}:assignments", - {:assignments_changed, "rapid_change_#{i}"} - ) - - Process.sleep(5) - end - - # Should only receive ONE jobs message (debounced) - 50ms debounce in test - assert_push "jobs", %{binary: _jobs_binary}, 150 - - # Should NOT receive additional jobs messages - refute_push "jobs", _, 50 - end - end -end diff --git a/test/towerops_web/controllers/api/account_data_controller_test.exs.bak b/test/towerops_web/controllers/api/account_data_controller_test.exs.bak deleted file mode 100644 index 99b9ba27..00000000 --- a/test/towerops_web/controllers/api/account_data_controller_test.exs.bak +++ /dev/null @@ -1,289 +0,0 @@ -defmodule ToweropsWeb.Api.AccountDataControllerTest do - use ToweropsWeb.ConnCase, async: true - - import Towerops.AccountsFixtures - - alias Towerops.Admin - alias Towerops.Alerts - alias Towerops.Devices - alias Towerops.Organizations - - setup :register_and_log_in_user - - describe "GET /api/v1/account/data" do - test "requires authentication", %{conn: _conn} do - # Build a new conn without authentication - conn = build_conn() - conn = get(conn, ~p"/api/v1/account/data") - assert response(conn, 302) - assert redirected_to(conn) == ~p"/users/log-in" - end - - test "returns JSON data with correct content type", %{conn: conn} do - conn = get(conn, ~p"/api/v1/account/data") - assert response(conn, 200) - assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"] - end - - test "sets content-disposition header for download", %{conn: conn, user: user} do - conn = get(conn, ~p"/api/v1/account/data") - [disposition] = get_resp_header(conn, "content-disposition") - assert disposition =~ "attachment" - assert disposition =~ "towerops-data-#{user.id}" - assert disposition =~ ".json" - end - - test "includes profile data in response", %{conn: conn, user: user} do - conn = get(conn, ~p"/api/v1/account/data") - data = json_response(conn, 200) - - assert data["profile"]["id"] == user.id - assert data["profile"]["email"] == user.email - assert data["profile"]["first_name"] == user.first_name - assert data["profile"]["last_name"] == user.last_name - # Default timezone is "UTC" even if user record has nil - assert data["profile"]["timezone"] in ["UTC", user.timezone] - assert data["profile"]["is_superuser"] == user.is_superuser - end - - test "includes organizations data", %{conn: conn, user: user} do - {:ok, org} = Organizations.create_organization(%{name: "Test Org"}, user.id) - - conn = get(conn, ~p"/api/v1/account/data") - data = json_response(conn, 200) - - assert length(data["organizations"]) == 1 - [org_data] = data["organizations"] - assert org_data["id"] == org.id - assert org_data["name"] == "Test Org" - assert org_data["role"] == "owner" - end - - test "includes devices data with redacted SNMP community", %{conn: conn, user: user} do - {:ok, org} = Organizations.create_organization(%{name: "Test Org"}, user.id) - - {:ok, site} = - Towerops.Sites.create_site(%{ - name: "Test Site", - organization_id: org.id - }) - - {:ok, device} = - Devices.create_device(%{ - name: "Test Router", - ip_address: "192.168.1.1", - site_id: site.id, - organization_id: org.id, - snmp_enabled: true, - snmp_community: "secret-community-string" - }) - - conn = get(conn, ~p"/api/v1/account/data") - data = json_response(conn, 200) - - assert length(data["devices"]) == 1 - [device_data] = data["devices"] - assert device_data["id"] == device.id - assert device_data["name"] == "Test Router" - assert device_data["ip_address"] == "192.168.1.1" - # SNMP community should be redacted for security - assert device_data["snmp_community"] == "[REDACTED]" - assert device_data["organization"]["name"] == "Test Org" - assert device_data["site"]["name"] == "Test Site" - end - - test "returns empty devices when user has no organizations", %{conn: conn} do - conn = get(conn, ~p"/api/v1/account/data") - data = json_response(conn, 200) - - assert data["devices"] == [] - end - - test "includes alerts data from last 90 days", %{conn: conn, user: user} do - {:ok, org} = Organizations.create_organization(%{name: "Test Org"}, user.id) - - {:ok, site} = - Towerops.Sites.create_site(%{ - name: "Test Site", - organization_id: org.id - }) - - {:ok, device} = - Devices.create_device(%{ - name: "Test Router", - ip_address: "192.168.1.1", - site_id: site.id, - organization_id: org.id - }) - - # Create a recent alert - {:ok, alert} = - Alerts.create_alert(%{ - device_id: device.id, - alert_type: "device_down", - triggered_at: DateTime.utc_now(), - message: "Device down" - }) - - conn = get(conn, ~p"/api/v1/account/data") - data = json_response(conn, 200) - - assert length(data["alerts"]) == 1 - [alert_data] = data["alerts"] - assert alert_data["id"] == alert.id - assert alert_data["alert_type"] == "device_down" - assert alert_data["message"] == "Device down" - assert alert_data["device"]["name"] == "Test Router" - end - - test "includes all alerts regardless of age (filtered by inserted_at, not triggered_at)", %{ - conn: conn, - user: user - } do - {:ok, org} = Organizations.create_organization(%{name: "Test Org"}, user.id) - - {:ok, site} = - Towerops.Sites.create_site(%{ - name: "Test Site", - organization_id: org.id - }) - - {:ok, device} = - Devices.create_device(%{ - name: "Test Router", - ip_address: "192.168.1.1", - site_id: site.id, - organization_id: org.id - }) - - # Create an alert with an old triggered_at (120 days ago) - # Note: The API filters by inserted_at (when record was created), not triggered_at - {:ok, alert} = - Alerts.create_alert(%{ - device_id: device.id, - alert_type: "device_down", - triggered_at: DateTime.add(DateTime.utc_now(), -120, :day), - message: "Old triggered time but recent record" - }) - - conn = get(conn, ~p"/api/v1/account/data") - data = json_response(conn, 200) - - # Alert should be included because it was recently inserted (even though triggered long ago) - assert length(data["alerts"]) == 1 - assert hd(data["alerts"])["id"] == alert.id - end - - test "returns empty alerts when user has no organizations", %{conn: conn} do - conn = get(conn, ~p"/api/v1/account/data") - data = json_response(conn, 200) - - assert data["alerts"] == [] - end - - test "includes audit logs data", %{conn: conn, user: user} do - # Create superuser for audit log - superuser = user_fixture(%{superuser: true}) - - {:ok, _log} = - Admin.create_audit_log(%{ - action: "impersonate_start", - superuser_id: superuser.id, - target_user_id: user.id, - ip_address: "127.0.0.1" - }) - - # Sleep briefly to ensure different timestamps - Process.sleep(50) - - conn = get(conn, ~p"/api/v1/account/data") - data = json_response(conn, 200) - - assert length(data["audit_logs"]) == 2 - - # Find the logs by action (ordering might vary slightly) - export_log = Enum.find(data["audit_logs"], &(&1["action"] == "user_data_exported")) - impersonate_log = Enum.find(data["audit_logs"], &(&1["action"] == "impersonate_start")) - - # Verify export log - assert export_log - assert export_log["actor_email"] == user.email - - # Verify impersonation log - assert impersonate_log - assert impersonate_log["target_user_id"] == user.id - assert impersonate_log["actor_email"] == superuser.email - end - - test "includes export metadata", %{conn: conn} do - conn = get(conn, ~p"/api/v1/account/data") - data = json_response(conn, 200) - - assert data["export_info"]["format"] == "JSON" - assert data["export_info"]["gdpr_article"] == "Article 15 - Right to Access" - assert is_binary(data["export_info"]["exported_at"]) - end - - test "creates audit log entry for data export", %{conn: conn, user: user} do - # Count existing audit logs - initial_count = user.id |> Admin.list_audit_logs_for_user() |> length() - - conn = get(conn, ~p"/api/v1/account/data") - assert response(conn, 200) - - # Should have one more audit log - logs = Admin.list_audit_logs_for_user(user.id) - assert length(logs) == initial_count + 1 - - # Most recent log should be the data export - [latest_log | _] = logs - assert latest_log.action == "user_data_exported" - assert latest_log.superuser_id == user.id - end - - test "handles user with multiple organizations", %{conn: conn, user: user} do - {:ok, _org1} = Organizations.create_organization(%{name: "Org 1"}, user.id) - - # Create second org by adding membership instead of creating new one - # (to avoid subscription limit issues in tests) - other_user = user_fixture() - {:ok, org2} = Organizations.create_organization(%{name: "Org 2"}, other_user.id) - - {:ok, _membership} = - Organizations.create_membership(%{ - organization_id: org2.id, - user_id: user.id, - role: :member - }) - - conn = get(conn, ~p"/api/v1/account/data") - data = json_response(conn, 200) - - assert length(data["organizations"]) == 2 - org_names = data["organizations"] |> Enum.map(& &1["name"]) |> Enum.sort() - assert org_names == ["Org 1", "Org 2"] - end - - test "handles user as member (not owner) of organization", %{conn: conn, user: user} do - # Create another user who owns the org - owner = user_fixture() - {:ok, org} = Organizations.create_organization(%{name: "Owner's Org"}, owner.id) - - # Add current user as member - {:ok, _membership} = - Organizations.create_membership(%{ - organization_id: org.id, - user_id: user.id, - role: :member - }) - - conn = get(conn, ~p"/api/v1/account/data") - data = json_response(conn, 200) - - assert length(data["organizations"]) == 1 - [org_data] = data["organizations"] - assert org_data["name"] == "Owner's Org" - assert org_data["role"] == "member" - end - end -end diff --git a/test/towerops_web/live/device_live_nested/show_test.exs.bak b/test/towerops_web/live/device_live_nested/show_test.exs.bak deleted file mode 100644 index 93d62112..00000000 --- a/test/towerops_web/live/device_live_nested/show_test.exs.bak +++ /dev/null @@ -1,181 +0,0 @@ -defmodule ToweropsWeb.DeviceLive.NestedShowTest do - use ToweropsWeb.ConnCase - - import Phoenix.LiveViewTest - - alias Towerops.Monitoring - alias Towerops.Organizations - alias Towerops.Sites - - setup :register_and_log_in_user - - setup %{user: user} do - {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id) - - {:ok, site} = - Sites.create_site(%{ - name: "Test Site", - organization_id: organization.id - }) - - {:ok, device} = - Towerops.Devices.create_device(%{ - name: "Test Router", - ip_address: "192.168.1.1", - site_id: site.id, - organization_id: organization.id - }) - - %{organization: organization, site: site, device: device} - end - - describe "Show" do - test "displays device information", %{conn: conn, device: device, organization: _org} do - {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=overview") - - assert html =~ device.name - assert html =~ device.ip_address - end - - test "displays overview tab by default", %{conn: conn, device: device, organization: _org} do - {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=overview") - - assert html =~ "Overview" - end - - test "displays metrics when device has checks", %{ - conn: conn, - device: device, - organization: _org - } do - # Create some checks - Monitoring.create_check(%{ - device_id: device.id, - status: :success, - response_time_ms: 10, - checked_at: DateTime.utc_now() - }) - - Monitoring.create_check(%{ - device_id: device.id, - status: :success, - response_time_ms: 15, - checked_at: DateTime.utc_now() - }) - - Monitoring.create_check(%{ - device_id: device.id, - status: :failure, - response_time_ms: nil, - checked_at: DateTime.utc_now() - }) - - {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=overview") - - assert html =~ "Device Information" - end - - test "displays empty state when no checks exist", %{ - conn: conn, - device: device, - organization: _org - } do - {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=overview") - - assert html =~ device.name - end - - test "refreshes data periodically", %{conn: conn, device: device, organization: _org} do - {:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview") - - # Trigger a status change - Monitoring.create_check(%{ - device_id: device.id, - status: :failure, - response_time_ms: nil, - checked_at: DateTime.utc_now() - }) - - # Send refresh message - send(view.pid, :refresh_data) - - # Poll for processing with early exit (max 50ms) - Enum.reduce_while(1..5, nil, fn _, _ -> - try do - {:halt, render(view)} - rescue - _ -> - Process.sleep(10) - {:cont, nil} - end - end) - - # View should still be alive - assert render(view) - end - - test "requires authentication", %{device: device, organization: _org} do - conn = build_conn() - {:error, redirect} = live(conn, ~p"/devices/#{device.id}") - - assert {:redirect, %{to: path}} = redirect - assert path == ~p"/users/log-in" - end - - test "handles equipment_status_changed event", %{ - conn: conn, - device: device, - organization: _org - } do - {:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview") - - # Simulate status change event - send(view.pid, {:device_status_changed, device.id, :up, 25}) - - # Poll for processing with early exit (max 50ms) - Enum.reduce_while(1..5, nil, fn _, _ -> - try do - {:halt, render(view)} - rescue - _ -> - Process.sleep(10) - {:cont, nil} - end - end) - - # View should still be alive and render - assert render(view) - end - - test "handles discovery_completed event", %{ - conn: conn, - device: device, - organization: _org - } do - {:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview") - - # Simulate discovery completed event - send(view.pid, {:discovery_completed, device.id}) - - # Poll for processing with early exit (max 50ms) - html = - Enum.reduce_while(1..5, nil, fn _, _ -> - try do - {:halt, render(view)} - rescue - _ -> - Process.sleep(10) - {:cont, nil} - end - end) || render(view) - - assert html =~ "Discovery completed" - end - - test "switches to different tabs", %{conn: conn, device: device, organization: _org} do - {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=events") - - assert html =~ device.name - end - end -end diff --git a/test/towerops_web/plugs/update_session_activity_test.exs.bak b/test/towerops_web/plugs/update_session_activity_test.exs.bak deleted file mode 100644 index 81edbbab..00000000 --- a/test/towerops_web/plugs/update_session_activity_test.exs.bak +++ /dev/null @@ -1,111 +0,0 @@ -defmodule ToweropsWeb.Plugs.UpdateSessionActivityTest do - use ToweropsWeb.ConnCase, async: true - - import Towerops.AccountsFixtures - - alias Towerops.Accounts - alias ToweropsWeb.Plugs.UpdateSessionActivity - - describe "init/1" do - test "returns options unchanged" do - assert UpdateSessionActivity.init([]) == [] - assert UpdateSessionActivity.init(some: :option) == [some: :option] - end - end - - describe "call/2" do - test "returns conn unchanged" do - conn = Plug.Test.init_test_session(build_conn(), %{}) - - result = UpdateSessionActivity.call(conn, []) - - assert result == conn - end - - test "spawns background task to update session activity" do - user = user_fixture() - {token, user_token} = Accounts.generate_user_session_token_with_record(user) - - now = DateTime.utc_now() - - # Create browser session - {:ok, _session} = - Accounts.create_browser_session(%{ - user_id: user.id, - user_token_id: user_token.id, - device_name: "Test Browser", - ip_address: "127.0.0.1", - user_agent: "Mozilla/5.0 Test", - last_activity_at: now, - expires_at: DateTime.add(now, 30, :day) - }) - - # Call plug with session token - should spawn background task without blocking - result = - build_conn() - |> Plug.Test.init_test_session(%{user_token: token}) - |> UpdateSessionActivity.call([]) - - # Should return immediately (not block on background task) - assert result - end - - test "handles missing session token gracefully" do - build_conn() - |> Plug.Test.init_test_session(%{}) - |> UpdateSessionActivity.call([]) - - # Should not raise an error - test passes if no exception - end - - test "handles invalid session token gracefully" do - build_conn() - |> Plug.Test.init_test_session(%{user_token: "invalid-token-does-not-exist"}) - |> UpdateSessionActivity.call([]) - - # Wait for background task to complete - Process.sleep(100) - - # Should not raise an error - test passes if no exception - end - - test "does not block the request while updating" do - user = user_fixture() - {token, user_token} = Accounts.generate_user_session_token_with_record(user) - - now = DateTime.utc_now() - - {:ok, _session} = - Accounts.create_browser_session(%{ - user_id: user.id, - user_token_id: user_token.id, - device_name: "Test Browser", - ip_address: "127.0.0.1", - user_agent: "Mozilla/5.0 Test", - last_activity_at: now, - expires_at: DateTime.add(now, 30, :day) - }) - - # Time the request - should be fast even though update happens in background - start_time = System.monotonic_time(:millisecond) - - build_conn() - |> Plug.Test.init_test_session(%{user_token: token}) - |> UpdateSessionActivity.call([]) - - end_time = System.monotonic_time(:millisecond) - duration = end_time - start_time - - # Should complete very quickly since update is async - assert duration < 200 - end - - test "handles nil token gracefully" do - build_conn() - |> Plug.Test.init_test_session(%{user_token: nil}) - |> UpdateSessionActivity.call([]) - - # Should not raise an error - test passes if no exception - end - end -end