diff --git a/test/towerops/devices_test.exs b/test/towerops/devices_test.exs index ecd7f379..878fd698 100644 --- a/test/towerops/devices_test.exs +++ b/test/towerops/devices_test.exs @@ -1752,4 +1752,604 @@ defmodule Towerops.EquipmentTest do refute_receive {:device_status_changed, _} end end + + describe "get_device_by_ip/3" do + import Towerops.AccountsFixtures + + 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 + }) + + %{organization: organization, site: site} + end + + test "returns device matching IP and site", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id + }) + + found = Devices.get_device_by_ip("192.168.1.1", site.id) + assert found.id == device.id + end + + test "returns nil when IP not found in site", %{site: site} do + assert Devices.get_device_by_ip("10.0.0.1", site.id) == nil + end + + test "returns nil when site_id is nil" do + assert Devices.get_device_by_ip("192.168.1.1", nil) == nil + end + + test "excludes device with given exclude_id", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id + }) + + assert Devices.get_device_by_ip("192.168.1.1", site.id, device.id) == nil + end + end + + describe "get_mikrotik_config/1" do + import Towerops.AccountsFixtures + + 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 + }) + + %{organization: organization, site: site} + end + + test "returns mikrotik config from device", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "MikroTik Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + mikrotik_enabled: true, + mikrotik_username: "admin", + mikrotik_port: 8729, + mikrotik_use_ssl: true + }) + + config = Devices.get_mikrotik_config(device) + assert config.username == "admin" + assert config.port == 8729 + assert config.use_ssl == true + assert config.enabled == true + end + + test "returns defaults when fields are nil", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id + }) + + config = Devices.get_mikrotik_config(device) + assert config.port == 8729 + assert config.ssh_port == 22 + assert config.use_ssl == true + assert config.enabled == false + end + + test "accepts device_id string", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + mikrotik_enabled: true, + mikrotik_username: "admin" + }) + + config = Devices.get_mikrotik_config(device.id) + assert config.username == "admin" + end + end + + describe "get_snmpv3_config/1" do + import Towerops.AccountsFixtures + + 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 + }) + + %{organization: organization, site: site} + end + + test "returns nil for non-v3 device", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + snmp_version: "2c" + }) + + assert Devices.get_snmpv3_config(device) == nil + end + + test "returns v3 config for v3 device", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + snmp_version: "3", + snmpv3_security_level: "authPriv", + snmpv3_username: "snmpuser", + snmpv3_auth_protocol: "SHA-256", + snmpv3_auth_password: "authpass123", + snmpv3_priv_protocol: "AES", + snmpv3_priv_password: "privpass123" + }) + + config = Devices.get_snmpv3_config(device) + assert config.security_level == "authPriv" + assert config.username == "snmpuser" + assert config.auth_protocol == "SHA-256" + assert config.priv_protocol == "AES" + end + + test "accepts device_id string", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + snmp_version: "3", + snmpv3_username: "snmpuser", + snmpv3_auth_password: "authpass123", + snmpv3_priv_password: "privpass123" + }) + + config = Devices.get_snmpv3_config(device.id) + assert config.username == "snmpuser" + end + end + + describe "propagate_site_community_change/2" do + import Towerops.AccountsFixtures + + setup do + user = user_fixture() + {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id) + + # Set community on site so devices will inherit from it + {:ok, site} = + Towerops.Sites.create_site(%{ + name: "Test Site", + organization_id: organization.id, + snmp_community: "site-community" + }) + + %{organization: organization, site: site} + end + + test "updates devices inheriting from site", %{organization: organization, site: site} do + # Device created without community → inherits from site, source = "site" + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + snmp_community: nil + }) + + assert device.snmp_community_source == "site" + + assert :ok = Devices.propagate_site_community_change(site.id, "new-community") + + updated = Devices.get_device!(device.id) + assert updated.snmp_community == "new-community" + end + + test "does not update devices with device-level community", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + snmp_community: "device-community" + }) + + assert device.snmp_community_source == "device" + + assert :ok = Devices.propagate_site_community_change(site.id, "new-community") + + updated = Devices.get_device!(device.id) + assert updated.snmp_community == "device-community" + end + end + + describe "propagate_organization_community_change/2" do + import Towerops.AccountsFixtures + + setup do + user = user_fixture() + + {:ok, organization} = + Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id) + + # Update org with community so devices can inherit from it + {:ok, organization} = + Towerops.Organizations.update_organization(organization, %{snmp_community: "org-community"}) + + # Site without community so devices inherit from org + {:ok, site} = + Towerops.Sites.create_site(%{ + name: "Test Site", + organization_id: organization.id + }) + + %{organization: organization, site: site} + end + + test "updates devices inheriting from organization", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + snmp_community: nil + }) + + assert device.snmp_community_source == "organization" + + assert :ok = Devices.propagate_organization_community_change(organization.id, "new-community") + + updated = Devices.get_device!(device.id) + assert updated.snmp_community == "new-community" + end + + test "does not update devices with device-level source", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + snmp_community: "device-community" + }) + + assert device.snmp_community_source == "device" + + assert :ok = Devices.propagate_organization_community_change(organization.id, "new-community") + + updated = Devices.get_device!(device.id) + assert updated.snmp_community == "device-community" + end + end + + describe "propagate_site_mikrotik_change/2" do + import Towerops.AccountsFixtures + + setup do + user = user_fixture() + {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id) + + # Set mikrotik creds on site so devices inherit + {:ok, site} = + Towerops.Sites.create_site(%{ + name: "Test Site", + organization_id: organization.id, + mikrotik_username: "site-user" + }) + + %{organization: organization, site: site} + end + + test "updates devices inheriting mikrotik from site", %{organization: organization, site: site} do + # Device without mikrotik_username → inherits from site + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id + }) + + assert device.mikrotik_credential_source == "site" + + assert :ok = + Devices.propagate_site_mikrotik_change(site.id, %{ + mikrotik_username: "new-user" + }) + + updated = Devices.get_device!(device.id) + assert updated.mikrotik_username == "new-user" + end + end + + describe "propagate_organization_mikrotik_change/2" do + import Towerops.AccountsFixtures + + setup do + user = user_fixture() + {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id) + + # Set mikrotik creds on org so devices inherit + {:ok, organization} = + Towerops.Organizations.update_organization(organization, %{mikrotik_username: "org-user"}) + + # Site without mikrotik creds so devices inherit from org + {:ok, site} = + Towerops.Sites.create_site(%{ + name: "Test Site", + organization_id: organization.id + }) + + %{organization: organization, site: site} + end + + test "updates devices inheriting mikrotik from organization", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id + }) + + assert device.mikrotik_credential_source == "organization" + + assert :ok = + Devices.propagate_organization_mikrotik_change(organization.id, %{ + mikrotik_username: "new-user" + }) + + updated = Devices.get_device!(device.id) + assert updated.mikrotik_username == "new-user" + end + end + + describe "propagate_site_snmpv3_change/2" do + import Towerops.AccountsFixtures + + setup do + user = user_fixture() + {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id) + + # Set snmpv3 creds on site + {:ok, site} = + Towerops.Sites.create_site(%{ + name: "Test Site", + organization_id: organization.id, + snmpv3_username: "site-v3-user", + snmpv3_auth_password: "authpass123", + snmpv3_priv_password: "privpass123" + }) + + %{organization: organization, site: site} + end + + test "updates devices inheriting snmpv3 from site", %{organization: organization, site: site} do + # Create device and manually set snmpv3_credential_source to "site" + # (can't use create_device with v3 + no username due to changeset validation order) + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + snmpv3_username: "site-v3-user", + snmpv3_auth_password: "authpass123", + snmpv3_priv_password: "privpass123", + snmp_version: "3" + }) + + # Force source to "site" for propagation test + device + |> Ecto.Changeset.change(%{snmpv3_credential_source: "site"}) + |> Towerops.Repo.update!() + + assert :ok = + Devices.propagate_site_snmpv3_change(site.id, %{ + snmpv3_username: "new-v3-user" + }) + + updated = Devices.get_device!(device.id) + assert updated.snmpv3_username == "new-v3-user" + end + end + + describe "propagate_organization_snmpv3_change/2" do + import Towerops.AccountsFixtures + + setup do + user = user_fixture() + {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id) + + # Set snmpv3 creds on org (with passwords required for v3 validation) + {:ok, organization} = + Towerops.Organizations.update_organization(organization, %{ + snmpv3_username: "org-v3-user", + snmpv3_auth_password: "authpass123", + snmpv3_priv_password: "privpass123" + }) + + # Site without snmpv3 creds + {:ok, site} = + Towerops.Sites.create_site(%{ + name: "Test Site", + organization_id: organization.id + }) + + %{organization: organization, site: site} + end + + test "updates devices inheriting snmpv3 from organization", %{organization: organization, site: site} do + # Create device with v3 credentials and force source to "organization" + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + snmpv3_username: "org-v3-user", + snmpv3_auth_password: "authpass123", + snmpv3_priv_password: "privpass123", + snmp_version: "3" + }) + + # Force source to "organization" for propagation test + device + |> Ecto.Changeset.change(%{snmpv3_credential_source: "organization"}) + |> Towerops.Repo.update!() + + assert :ok = + Devices.propagate_organization_snmpv3_change(organization.id, %{ + snmpv3_username: "new-v3-user" + }) + + updated = Devices.get_device!(device.id) + assert updated.snmpv3_username == "new-v3-user" + end + end + + describe "resolve_snmp_community/1" do + import Towerops.AccountsFixtures + + 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 + }) + + %{organization: organization, site: site} + end + + test "returns device community when source is device", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + snmp_community: "my-community" + }) + + assert Devices.resolve_snmp_community(device) == "my-community" + end + + test "returns site community when device has none", %{organization: organization, site: site} do + {:ok, site} = Towerops.Sites.update_site(site, %{snmp_community: "site-community"}) + + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + snmp_community: nil, + snmp_community_source: "site" + }) + + assert Devices.resolve_snmp_community(device) == "site-community" + end + + test "returns org community when device and site have none", %{organization: organization, site: site} do + {:ok, _org} = + Towerops.Organizations.update_organization(organization, %{snmp_community: "org-community"}) + + {:ok, device} = + Devices.create_device(%{ + name: "Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + snmp_community: nil, + snmp_community_source: "organization" + }) + + assert Devices.resolve_snmp_community(device) == "org-community" + end + end + + describe "list_mikrotik_devices_with_api/0" do + import Towerops.AccountsFixtures + + 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 + }) + + %{organization: organization, site: site} + end + + test "returns devices with mikrotik enabled and username", %{organization: organization, site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "MikroTik Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + mikrotik_enabled: true, + mikrotik_username: "admin" + }) + + result = Devices.list_mikrotik_devices_with_api() + device_ids = Enum.map(result, & &1.id) + assert device.id in device_ids + end + + test "excludes devices without mikrotik enabled", %{organization: organization, site: site} do + {:ok, _device} = + Devices.create_device(%{ + name: "Regular Router", + ip_address: "192.168.1.1", + site_id: site.id, + organization_id: organization.id, + mikrotik_enabled: false + }) + + result = Devices.list_mikrotik_devices_with_api() + assert result == [] + end + end end diff --git a/test/towerops/workers/device_monitor_worker_test.exs b/test/towerops/workers/device_monitor_worker_test.exs index 49761393..05c21e62 100644 --- a/test/towerops/workers/device_monitor_worker_test.exs +++ b/test/towerops/workers/device_monitor_worker_test.exs @@ -4,6 +4,7 @@ defmodule Towerops.Workers.DeviceMonitorWorkerTest do import Towerops.AccountsFixtures alias Towerops.Devices + alias Towerops.Monitoring.PingMock alias Towerops.Organizations alias Towerops.Sites alias Towerops.Workers.DeviceMonitorWorker @@ -87,4 +88,78 @@ defmodule Towerops.Workers.DeviceMonitorWorkerTest do refute cancelled_jobs == [] end end + + describe "perform/1" do + test "skips check for deleted device" do + fake_id = Ecto.UUID.generate() + assert :ok == DeviceMonitorWorker.perform(%Oban.Job{args: %{"device_id" => fake_id}}) + end + + test "performs check and reschedules for monitoring-enabled device", %{site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Monitor Me", + ip_address: "192.168.1.10", + site_id: site.id, + organization_id: site.organization_id, + monitoring_enabled: true + }) + + Mox.expect(PingMock, :ping, fn _ip -> {:ok, 5.0} end) + + assert :ok == DeviceMonitorWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) + + # Device should be marked up + updated = Devices.get_device!(device.id) + assert updated.status == :up + end + + test "handles ping failure and marks device down", %{site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Failing Device", + ip_address: "192.168.1.11", + site_id: site.id, + organization_id: site.organization_id, + monitoring_enabled: true + }) + + Mox.expect(PingMock, :ping, fn _ip -> {:error, :timeout} end) + + assert :ok == DeviceMonitorWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) + + updated = Devices.get_device!(device.id) + assert updated.status == :down + end + + test "does not check when monitoring is disabled", %{site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "No Monitor", + ip_address: "192.168.1.12", + site_id: site.id, + organization_id: site.organization_id, + monitoring_enabled: false + }) + + # No mock expectation - ping should not be called + assert :ok == DeviceMonitorWorker.perform(%Oban.Job{args: %{"device_id" => device.id}}) + end + end + + describe "trigger_check/1" do + test "inserts an immediate job", %{site: site} do + {:ok, device} = + Devices.create_device(%{ + name: "Trigger Me", + ip_address: "192.168.1.13", + site_id: site.id, + organization_id: site.organization_id, + monitoring_enabled: true + }) + + assert {:ok, job} = DeviceMonitorWorker.trigger_check(device.id) + assert job.args["device_id"] == device.id + end + end end diff --git a/test/towerops/workers/discovery_worker_test.exs b/test/towerops/workers/discovery_worker_test.exs index fb477916..5a212ff0 100644 --- a/test/towerops/workers/discovery_worker_test.exs +++ b/test/towerops/workers/discovery_worker_test.exs @@ -132,6 +132,39 @@ defmodule Towerops.Workers.DiscoveryWorkerTest do end end + describe "enqueue/1" do + setup do + user = user_fixture() + {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Enqueue Org"}, user.id) + + {:ok, site} = + Towerops.Sites.create_site(%{ + name: "Enqueue Site", + organization_id: organization.id + }) + + {:ok, device} = + Towerops.Devices.create_device(%{ + name: "Enqueue Device", + ip_address: "192.168.1.50", + snmp_enabled: true, + site_id: site.id, + organization_id: organization.id + }) + + {:ok, device: device} + end + + test "enqueues a discovery job", %{device: device} do + assert :ok = DiscoveryWorker.enqueue(device.id) + end + + test "deduplicates jobs within 60 seconds", %{device: device} do + assert :ok = DiscoveryWorker.enqueue(device.id) + assert :ok = DiscoveryWorker.enqueue(device.id) + end + end + describe "retryable_error?/1" do test "timeout is retryable" do assert DiscoveryWorker.retryable_error?(:timeout) @@ -157,6 +190,14 @@ defmodule Towerops.Workers.DiscoveryWorkerTest do refute DiscoveryWorker.retryable_error?(:invalid_config) end + test "no_snmp_credentials is not retryable" do + refute DiscoveryWorker.retryable_error?(:no_snmp_credentials) + end + + test "device_deleted is not retryable" do + refute DiscoveryWorker.retryable_error?(:device_deleted) + end + test "unknown errors are retryable by default" do assert DiscoveryWorker.retryable_error?(:some_unknown_error) end diff --git a/test/towerops/workers/firmware_version_fetcher_worker_test.exs b/test/towerops/workers/firmware_version_fetcher_worker_test.exs index 6cccb307..6acc4eff 100644 --- a/test/towerops/workers/firmware_version_fetcher_worker_test.exs +++ b/test/towerops/workers/firmware_version_fetcher_worker_test.exs @@ -128,6 +128,67 @@ defmodule Towerops.Workers.FirmwareVersionFetcherWorkerTest do assert is_nil(data.release_date) end + test "extracts version number from 'RouterOS X.Y.Z [stable]' format" do + rss_with_full_version = """ + + + + RouterOS latest stable version + + RouterOS 7.21.2 [stable] + https://mikrotik.com/download + Wed, 15 Jan 2025 12:00:00 +0000 + RouterOS 7.21.2 stable release + + + + """ + + assert {:ok, data} = FirmwareVersionFetcherWorker.parse_rss_feed(rss_with_full_version) + assert data.version == "7.21.2" + assert data.metadata["raw_version"] == "RouterOS 7.21.2 [stable]" + end + + test "extracts two-part version number" do + rss_with_two_part = """ + + + + RouterOS latest stable version + + RouterOS 7.21 [stable] + https://mikrotik.com/download + Wed, 15 Jan 2025 12:00:00 +0000 + Test + + + + """ + + assert {:ok, data} = FirmwareVersionFetcherWorker.parse_rss_feed(rss_with_two_part) + assert data.version == "7.21" + end + + test "handles missing pubDate" do + rss_no_date = """ + + + + RouterOS latest stable version + + 7.14.1 + https://mikrotik.com/download + Test + + + + """ + + assert {:ok, data} = FirmwareVersionFetcherWorker.parse_rss_feed(rss_no_date) + assert data.version == "7.14.1" + assert is_nil(data.release_date) + end + test "stores metadata correctly" do rss_with_metadata = """ diff --git a/test/towerops_web/controllers/user_reset_password_controller_test.exs b/test/towerops_web/controllers/user_reset_password_controller_test.exs index f7c48323..fb2a4ed1 100644 --- a/test/towerops_web/controllers/user_reset_password_controller_test.exs +++ b/test/towerops_web/controllers/user_reset_password_controller_test.exs @@ -3,6 +3,8 @@ defmodule ToweropsWeb.UserResetPasswordControllerTest do import Towerops.AccountsFixtures + # Note: edit/update routes are handled by UserResetPasswordLive, not this controller + setup do %{user: user_fixture(enable_totp: false)} end diff --git a/test/towerops_web/live/account_live/activity_test.exs b/test/towerops_web/live/account_live/activity_test.exs index 6d889aee..b8789667 100644 --- a/test/towerops_web/live/account_live/activity_test.exs +++ b/test/towerops_web/live/account_live/activity_test.exs @@ -26,5 +26,152 @@ defmodule ToweropsWeb.AccountLive.ActivityTest do assert html =~ "User data viewed" end + + test "shows sudo_mode_granted from setup", %{conn: conn} do + {:ok, _view, html} = live(conn, ~p"/account/activity") + + assert html =~ "Sudo mode granted" + end + end + + describe "action descriptions" do + test "displays user_data_exported action", %{conn: conn, user: user} do + {:ok, _} = + Admin.create_audit_log(%{ + action: "user_data_exported", + superuser_id: user.id, + target_user_id: user.id + }) + + {:ok, _view, html} = live(conn, ~p"/account/activity") + assert html =~ "exported your account data" + end + + test "displays user_profile_updated action", %{conn: conn, user: user} do + {:ok, _} = + Admin.create_audit_log(%{ + action: "user_profile_updated", + superuser_id: user.id, + target_user_id: user.id + }) + + {:ok, _view, html} = live(conn, ~p"/account/activity") + assert html =~ "updated your profile" + end + + test "displays device_created action with metadata", %{conn: conn, user: user} do + {:ok, _} = + Admin.create_audit_log(%{ + action: "device_created", + superuser_id: user.id, + target_user_id: user.id, + metadata: %{"device_name" => "Test Router"} + }) + + {:ok, _view, html} = live(conn, ~p"/account/activity") + assert html =~ "Test Router" + end + + test "displays device_deleted action", %{conn: conn, user: user} do + {:ok, _} = + Admin.create_audit_log(%{ + action: "device_deleted", + superuser_id: user.id, + target_user_id: user.id, + metadata: %{"device_name" => "Old Router"} + }) + + {:ok, _view, html} = live(conn, ~p"/account/activity") + assert html =~ "Old Router" + end + + test "displays device_updated action", %{conn: conn, user: user} do + {:ok, _} = + Admin.create_audit_log(%{ + action: "device_updated", + superuser_id: user.id, + target_user_id: user.id, + metadata: %{"device_id" => "abc-123"} + }) + + {:ok, _view, html} = live(conn, ~p"/account/activity") + assert html =~ "abc-123" + end + + test "displays failed_access_attempt action", %{conn: conn, user: user} do + {:ok, _} = + Admin.create_audit_log(%{ + action: "failed_access_attempt", + superuser_id: user.id, + target_user_id: user.id, + metadata: %{"resource" => "/admin/secret"} + }) + + {:ok, _view, html} = live(conn, ~p"/account/activity") + assert html =~ "/admin/secret" + end + + test "displays privilege_escalation action", %{conn: conn, user: user} do + {:ok, _} = + Admin.create_audit_log(%{ + action: "privilege_escalation", + superuser_id: user.id, + target_user_id: user.id, + metadata: %{"from_role" => "member", "to_role" => "admin"} + }) + + {:ok, _view, html} = live(conn, ~p"/account/activity") + assert html =~ "member" + assert html =~ "admin" + end + + test "displays impersonate_start action", %{conn: conn, user: user} do + {:ok, _} = + Admin.create_audit_log(%{ + action: "impersonate_start", + superuser_id: user.id, + target_user_id: user.id + }) + + {:ok, _view, html} = live(conn, ~p"/account/activity") + assert html =~ "impersonating" + end + + test "displays impersonate_end action", %{conn: conn, user: user} do + {:ok, _} = + Admin.create_audit_log(%{ + action: "impersonate_end", + superuser_id: user.id, + target_user_id: user.id + }) + + {:ok, _view, html} = live(conn, ~p"/account/activity") + assert html =~ "stopped impersonating" + end + + test "displays org_data_accessed action", %{conn: conn, user: user} do + {:ok, _} = + Admin.create_audit_log(%{ + action: "org_data_accessed", + superuser_id: user.id, + target_user_id: user.id, + metadata: %{"organization_id" => "org-123", "action" => "exported"} + }) + + {:ok, _view, html} = live(conn, ~p"/account/activity") + assert html =~ "org-123" + end + + test "displays fallback for action without handler", %{conn: conn, user: user} do + {:ok, _} = + Admin.create_audit_log(%{ + action: "monitoring_data_queried", + superuser_id: user.id, + target_user_id: user.id + }) + + {:ok, _view, html} = live(conn, ~p"/account/activity") + assert html =~ "Monitoring data queried" + end end end