From aa0e7f3d058512b2eb1a7560ae4d77a9e369acee Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 4 Feb 2026 17:56:48 -0600 Subject: [PATCH] help update --- .../contexts/config_change_tracker.ex | 109 ++++++ lib/towerops/organizations.ex | 47 +-- lib/towerops/sites.ex | 28 +- .../controllers/api_docs_html/index.html.heex | 9 + lib/towerops_web/live/help_live/index.ex | 370 ++++++++++++++++++ .../contexts/config_change_tracker_test.exs | 313 +++++++++++++++ 6 files changed, 820 insertions(+), 56 deletions(-) create mode 100644 lib/towerops/contexts/config_change_tracker.ex create mode 100644 test/towerops/contexts/config_change_tracker_test.exs diff --git a/lib/towerops/contexts/config_change_tracker.ex b/lib/towerops/contexts/config_change_tracker.ex new file mode 100644 index 00000000..575c908c --- /dev/null +++ b/lib/towerops/contexts/config_change_tracker.ex @@ -0,0 +1,109 @@ +defmodule Towerops.Contexts.ConfigChangeTracker do + @moduledoc """ + Tracks configuration changes for SNMP and MikroTik settings across + organizations and sites to trigger device propagation. + + Provides functions to capture configuration state before updates and + detect which configuration fields changed after updates. + """ + + @config_fields [ + :snmp_community, + :mikrotik_username, + :mikrotik_password, + :mikrotik_port, + :mikrotik_use_ssl, + :mikrotik_enabled + ] + + @doc """ + Captures the current configuration state from a struct. + + Returns a map with all tracked configuration fields. + + ## Examples + + iex> org = %Organization{snmp_community: "public", mikrotik_username: "admin"} + iex> ConfigChangeTracker.capture_config_state(org) + %{ + snmp_community: "public", + mikrotik_username: "admin", + mikrotik_password: nil, + mikrotik_port: nil, + mikrotik_use_ssl: nil, + mikrotik_enabled: nil + } + """ + @spec capture_config_state(struct()) :: map() + def capture_config_state(struct) do + Map.new(@config_fields, fn field -> + {field, Map.get(struct, field)} + end) + end + + @doc """ + Checks if the SNMP community string changed. + + ## Examples + + iex> old_state = %{snmp_community: "public"} + iex> updated = %Organization{snmp_community: "private"} + iex> ConfigChangeTracker.snmp_changed?(updated, old_state) + true + """ + @spec snmp_changed?(struct(), map()) :: boolean() + def snmp_changed?(updated_struct, old_state) do + Map.get(updated_struct, :snmp_community) != old_state.snmp_community + end + + @doc """ + Checks if any MikroTik configuration field changed. + + Compares all MikroTik-related fields (username, password, port, ssl, enabled) + except snmp_community. + + ## Examples + + iex> old_state = %{mikrotik_username: "admin", mikrotik_password: "pass"} + iex> updated = %Organization{mikrotik_username: "root", mikrotik_password: "pass"} + iex> ConfigChangeTracker.mikrotik_changed?(updated, old_state) + true + """ + @spec mikrotik_changed?(struct(), map()) :: boolean() + def mikrotik_changed?(updated_struct, old_state) do + Enum.any?(@config_fields, fn field -> + case field do + :snmp_community -> false + field -> Map.get(updated_struct, field) != old_state[field] + end + end) + end + + @doc """ + Extracts MikroTik configuration attributes from a struct. + + Returns a map with all MikroTik-related fields suitable for device propagation. + + ## Examples + + iex> site = %Site{mikrotik_username: "admin", mikrotik_port: 8729} + iex> ConfigChangeTracker.extract_mikrotik_attrs(site) + %{ + mikrotik_username: "admin", + mikrotik_password: nil, + mikrotik_port: 8729, + mikrotik_use_ssl: nil, + mikrotik_enabled: nil + } + """ + @spec extract_mikrotik_attrs(struct()) :: map() + def extract_mikrotik_attrs(struct) do + %{ + mikrotik_username: Map.get(struct, :mikrotik_username), + mikrotik_password: Map.get(struct, :mikrotik_password), + mikrotik_port: Map.get(struct, :mikrotik_port), + mikrotik_use_ssl: Map.get(struct, :mikrotik_use_ssl), + mikrotik_enabled: Map.get(struct, :mikrotik_enabled) + } + end +end diff --git a/lib/towerops/organizations.ex b/lib/towerops/organizations.ex index 4c048d28..ba50b11e 100644 --- a/lib/towerops/organizations.ex +++ b/lib/towerops/organizations.ex @@ -6,6 +6,7 @@ defmodule Towerops.Organizations do import Ecto.Query alias Towerops.Agents.AgentAssignment + alias Towerops.Contexts.ConfigChangeTracker alias Towerops.Devices.Device alias Towerops.Organizations.Invitation alias Towerops.Organizations.Membership @@ -124,30 +125,23 @@ defmodule Towerops.Organizations do Updates an organization. """ def update_organization(%Organization{} = organization, attrs) do - old_values = %{ - snmp_community: organization.snmp_community, - mikrotik_username: organization.mikrotik_username, - mikrotik_password: organization.mikrotik_password, - mikrotik_port: organization.mikrotik_port, - mikrotik_use_ssl: organization.mikrotik_use_ssl, - mikrotik_enabled: organization.mikrotik_enabled, - use_sites: organization.use_sites - } + old_config = ConfigChangeTracker.capture_config_state(organization) + old_use_sites = organization.use_sites with {:ok, updated_organization} <- organization |> Organization.changeset(attrs) |> Repo.update() do - propagate_organization_changes(updated_organization, old_values) + propagate_organization_changes(updated_organization, old_config, old_use_sites) {:ok, updated_organization} end end - defp propagate_organization_changes(updated_organization, old_values) do - propagate_snmp_changes(updated_organization, old_values) - propagate_mikrotik_changes(updated_organization, old_values) - handle_sites_disabled(updated_organization, old_values) + defp propagate_organization_changes(updated_organization, old_config, old_use_sites) do + propagate_snmp_changes(updated_organization, old_config) + propagate_mikrotik_changes(updated_organization, old_config) + handle_sites_disabled(updated_organization, old_use_sites) end - defp propagate_snmp_changes(updated_organization, %{snmp_community: old_community}) do - if updated_organization.snmp_community != old_community do + defp propagate_snmp_changes(updated_organization, old_config) do + if ConfigChangeTracker.snmp_changed?(updated_organization, old_config) do Towerops.Devices.propagate_organization_community_change( updated_organization.id, updated_organization.snmp_community @@ -155,22 +149,9 @@ defmodule Towerops.Organizations do end end - defp propagate_mikrotik_changes(updated_organization, old_values) do - mikrotik_changed = - updated_organization.mikrotik_username != old_values.mikrotik_username || - updated_organization.mikrotik_password != old_values.mikrotik_password || - updated_organization.mikrotik_port != old_values.mikrotik_port || - updated_organization.mikrotik_use_ssl != old_values.mikrotik_use_ssl || - updated_organization.mikrotik_enabled != old_values.mikrotik_enabled - - if mikrotik_changed do - mikrotik_attrs = %{ - mikrotik_username: updated_organization.mikrotik_username, - mikrotik_password: updated_organization.mikrotik_password, - mikrotik_port: updated_organization.mikrotik_port, - mikrotik_use_ssl: updated_organization.mikrotik_use_ssl, - mikrotik_enabled: updated_organization.mikrotik_enabled - } + defp propagate_mikrotik_changes(updated_organization, old_config) do + if ConfigChangeTracker.mikrotik_changed?(updated_organization, old_config) do + mikrotik_attrs = ConfigChangeTracker.extract_mikrotik_attrs(updated_organization) Towerops.Devices.propagate_organization_mikrotik_change( updated_organization.id, @@ -179,7 +160,7 @@ defmodule Towerops.Organizations do end end - defp handle_sites_disabled(updated_organization, %{use_sites: old_use_sites}) do + defp handle_sites_disabled(updated_organization, old_use_sites) do if old_use_sites && !updated_organization.use_sites do clear_all_site_assignments(updated_organization.id) end diff --git a/lib/towerops/sites.ex b/lib/towerops/sites.ex index e2bb7725..d57684ee 100644 --- a/lib/towerops/sites.ex +++ b/lib/towerops/sites.ex @@ -6,6 +6,7 @@ defmodule Towerops.Sites do import Ecto.Query alias Towerops.Agents.AgentAssignment + alias Towerops.Contexts.ConfigChangeTracker alias Towerops.Devices.Device alias Towerops.Repo alias Towerops.Sites.Site @@ -106,39 +107,20 @@ defmodule Towerops.Sites do Updates a site. """ def update_site(%Site{} = site, attrs) do - old_community = site.snmp_community - old_mikrotik_username = site.mikrotik_username - old_mikrotik_password = site.mikrotik_password - old_mikrotik_port = site.mikrotik_port - old_mikrotik_use_ssl = site.mikrotik_use_ssl - old_mikrotik_enabled = site.mikrotik_enabled + old_config = ConfigChangeTracker.capture_config_state(site) case site |> Site.changeset(attrs) |> Repo.update() do {:ok, updated_site} = result -> # If community string changed, propagate to inheriting devices - if updated_site.snmp_community != old_community do + if ConfigChangeTracker.snmp_changed?(updated_site, old_config) do Towerops.Devices.propagate_site_community_change(updated_site.id, updated_site.snmp_community) end # If any MikroTik settings changed, propagate to inheriting devices - mikrotik_changed = - updated_site.mikrotik_username != old_mikrotik_username || - updated_site.mikrotik_password != old_mikrotik_password || - updated_site.mikrotik_port != old_mikrotik_port || - updated_site.mikrotik_use_ssl != old_mikrotik_use_ssl || - updated_site.mikrotik_enabled != old_mikrotik_enabled - - if mikrotik_changed do - mikrotik_attrs = %{ - mikrotik_username: updated_site.mikrotik_username, - mikrotik_password: updated_site.mikrotik_password, - mikrotik_port: updated_site.mikrotik_port, - mikrotik_use_ssl: updated_site.mikrotik_use_ssl, - mikrotik_enabled: updated_site.mikrotik_enabled - } - + if ConfigChangeTracker.mikrotik_changed?(updated_site, old_config) do + mikrotik_attrs = ConfigChangeTracker.extract_mikrotik_attrs(updated_site) Towerops.Devices.propagate_site_mikrotik_change(updated_site.id, mikrotik_attrs) end diff --git a/lib/towerops_web/controllers/api_docs_html/index.html.heex b/lib/towerops_web/controllers/api_docs_html/index.html.heex index 8f113fd4..75dd4b59 100644 --- a/lib/towerops_web/controllers/api_docs_html/index.html.heex +++ b/lib/towerops_web/controllers/api_docs_html/index.html.heex @@ -76,6 +76,15 @@ Account Data (GDPR) +
  • + + Graphs & Live Polling + +
  • diff --git a/lib/towerops_web/live/help_live/index.ex b/lib/towerops_web/live/help_live/index.ex index 96b7dcfa..c8c49829 100644 --- a/lib/towerops_web/live/help_live/index.ex +++ b/lib/towerops_web/live/help_live/index.ex @@ -238,6 +238,21 @@ defmodule ToweropsWeb.HelpLive.Index do MikroTik +
  • + <.link + patch={~p"/help?section=graphs"} + class={[ + "block px-3 py-2 rounded-md text-sm font-medium transition-colors", + if @active_section == "graphs" do + "bg-blue-50 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300" + else + "text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800" + end + ]} + > + Graphs & Live Polling + +
  • @@ -951,6 +966,361 @@ defmodule ToweropsWeb.HelpLive.Index do + <% "graphs" -> %> +
    +

    + Graphs & Live Polling +

    + +
    +

    + Towerops provides powerful visualization capabilities for all monitored metrics. Every sensor, + interface, and metric can be viewed as a time-series graph with multiple time ranges and a + real-time live polling mode for instant feedback. +

    + +

    + Accessing Graphs +

    + +

    + Graphs are available throughout Towerops wherever metrics are displayed: +

    + +
      +
    • + Device Overview: + Click any metric tile (CPU, Memory, Temperature, etc.) to view its graph +
    • +
    • + Sensors Tab: + Click the graph icon next to any sensor reading +
    • +
    • + Interfaces Tab: + Click the graph icon next to any interface to view traffic graphs +
    • +
    • + Storage Tab: + Click any storage volume to view usage over time +
    • +
    + +

    + Time Ranges +

    + +

    + All graphs support multiple time ranges for analyzing trends at different scales: +

    + +
    +
    +

    + 1 Hour +

    +

    + Recent activity with high detail +

    +
    +
    +

    + 6 Hours +

    +

    + Half-day trends and patterns +

    +
    +
    +

    + 12 Hours +

    +

    + Business day overview +

    +
    +
    +

    + 24 Hours (Default) +

    +

    + Full day of activity +

    +
    +
    +

    + 7 Days +

    +

    + Weekly trends and patterns +

    +
    +
    +

    + 30 Days +

    +

    + Monthly overview and capacity planning +

    +
    +
    + +

    + Live Polling Mode +

    + +

    + Live mode provides real-time sensor monitoring with data updating every second. This is perfect for: +

    + +
      +
    • Testing configuration changes and seeing immediate effects
    • +
    • Monitoring system load during maintenance or upgrades
    • +
    • Watching temperature changes during thermal testing
    • +
    • Observing traffic patterns during load testing
    • +
    • Real-time troubleshooting of performance issues
    • +
    + +
    +
    + <.icon + name="hero-signal" + class="h-5 w-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" + /> +
    +

    + How Live Mode Works +

    +
      +
    • Polls sensors directly via SNMP every 1 second
    • +
    • Displays a rolling 5-minute window (300 data points)
    • +
    • Updates chart in real-time as new data arrives
    • +
    • Automatically stops when you switch to another time range
    • +
    • Works with remote pollers - polling happens on the agent
    • +
    +
    +
    +
    + +

    + Using Live Mode +

    + +
    +
    +
    +
    + 1 +
    +
    +
    +

    + Open Any Graph +

    +

    + Navigate to any device and click on a metric graph (CPU, Memory, Temperature, Traffic, etc.) +

    +
    +
    + +
    +
    +
    + 2 +
    +
    +
    +

    + Click the "Live" Button +

    +

    + The Live button has a distinctive green gradient style and will pulse when active +

    +
    +
    + +
    +
    +
    + 3 +
    +
    +
    +

    + Watch Real-Time Updates +

    +

    + The graph will start updating every second with fresh data. A pulsing green indicator + shows that live polling is active. +

    +
    +
    + +
    +
    +
    + 4 +
    +
    +
    +

    + Switch Back to Historical Data +

    +

    + Click any other time range button to stop live polling and view historical data +

    +
    +
    +
    + +

    + Supported Metrics in Live Mode +

    + +
    +
    +

    + <.icon name="hero-cpu-chip" class="h-4 w-4 text-blue-500" /> + CPU / Processors +

    +

    + Real-time CPU load and utilization +

    +
    + +
    +

    + <.icon name="hero-circle-stack" class="h-4 w-4 text-blue-500" /> + Memory Usage +

    +

    + RAM utilization percentage +

    +
    + +
    +

    + <.icon name="hero-fire" class="h-4 w-4 text-orange-500" /> Temperature +

    +

    + Device and component temperatures +

    +
    + +
    +

    + <.icon name="hero-bolt" class="h-4 w-4 text-yellow-500" /> Voltage +

    +

    + Power supply voltages +

    +
    + +
    +

    + <.icon name="hero-server-stack" class="h-4 w-4 text-purple-500" /> Storage +

    +

    + Disk usage and capacity +

    +
    + +
    +

    + <.icon name="hero-hashtag" class="h-4 w-4 text-green-500" /> Custom Metrics +

    +

    + Sessions, connections, and counts +

    +
    +
    + +
    +
    + <.icon + name="hero-information-circle" + class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" + /> +
    +

    + Note About Traffic Graphs +

    +

    + Live mode is currently only available for sensor metrics (CPU, memory, temperature, etc.). + Interface traffic graphs use historical data only, as traffic calculations require + comparing multiple SNMP polls over time. +

    +
    +
    +
    + +

    + Tips for Using Graphs +

    + +
    +
    +

    + Multiple Sensors on One Graph +

    +

    + When viewing aggregate metrics (like "Temperature" for all sensors), the graph + automatically displays all sensors of that type with different colors for easy comparison. +

    +
    + +
    +

    + Max and Min Values +

    +

    + Historical graphs (non-live) display the maximum and minimum values for the selected + time range at the bottom of the chart for quick reference. +

    +
    + +
    +

    + Traffic Graph Direction +

    +

    + Interface traffic graphs show outbound traffic as positive values (above zero) and + inbound traffic as negative values (below zero) for easy visualization of bidirectional flow. +

    +
    + +
    +

    + Automatic Unit Scaling +

    +

    + Traffic graphs automatically scale units (bps, Kbps, Mbps, Gbps) based on the data + range for optimal readability. Hover over data points to see exact values. +

    +
    +
    + +
    +
    + <.icon + name="hero-light-bulb" + class="h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" + /> +
    +

    + Performance Tip +

    +

    + Live polling makes direct SNMP requests every second. While this provides instant + feedback, keeping many live graphs open simultaneously may impact device performance. + Use live mode for troubleshooting and testing, then switch back to historical ranges + for routine monitoring. +

    +
    +
    +
    +
    +
    <% "mikrotik" -> %>

    diff --git a/test/towerops/contexts/config_change_tracker_test.exs b/test/towerops/contexts/config_change_tracker_test.exs new file mode 100644 index 00000000..579fbc07 --- /dev/null +++ b/test/towerops/contexts/config_change_tracker_test.exs @@ -0,0 +1,313 @@ +defmodule Towerops.Contexts.ConfigChangeTrackerTest do + use Towerops.DataCase, async: true + + alias Towerops.Contexts.ConfigChangeTracker + + describe "capture_config_state/1" do + test "captures all config fields from organization struct" do + org = %{ + snmp_community: "public", + mikrotik_username: "admin", + mikrotik_password: "password", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + result = ConfigChangeTracker.capture_config_state(org) + + assert result == %{ + snmp_community: "public", + mikrotik_username: "admin", + mikrotik_password: "password", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + end + + test "captures partial config fields with nils" do + site = %{ + snmp_community: "private", + mikrotik_username: nil, + mikrotik_password: nil, + mikrotik_port: nil, + mikrotik_use_ssl: nil, + mikrotik_enabled: nil + } + + result = ConfigChangeTracker.capture_config_state(site) + + assert result.snmp_community == "private" + assert result.mikrotik_username == nil + assert result.mikrotik_password == nil + end + + test "handles struct with missing fields gracefully" do + minimal = %{snmp_community: "test"} + + result = ConfigChangeTracker.capture_config_state(minimal) + + assert result.snmp_community == "test" + assert result.mikrotik_username == nil + assert result.mikrotik_password == nil + assert result.mikrotik_port == nil + assert result.mikrotik_use_ssl == nil + assert result.mikrotik_enabled == nil + end + end + + describe "snmp_changed?/2" do + test "returns true when SNMP community changed" do + old_state = %{snmp_community: "public"} + updated = %{snmp_community: "private"} + + assert ConfigChangeTracker.snmp_changed?(updated, old_state) == true + end + + test "returns false when SNMP community unchanged" do + old_state = %{snmp_community: "public"} + updated = %{snmp_community: "public"} + + assert ConfigChangeTracker.snmp_changed?(updated, old_state) == false + end + + test "returns true when SNMP community changes from nil to value" do + old_state = %{snmp_community: nil} + updated = %{snmp_community: "public"} + + assert ConfigChangeTracker.snmp_changed?(updated, old_state) == true + end + + test "returns true when SNMP community changes from value to nil" do + old_state = %{snmp_community: "public"} + updated = %{snmp_community: nil} + + assert ConfigChangeTracker.snmp_changed?(updated, old_state) == true + end + end + + describe "mikrotik_changed?/2" do + test "returns true when MikroTik username changed" do + old_state = %{ + snmp_community: "public", + mikrotik_username: "admin", + mikrotik_password: "pass", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + updated = %{ + snmp_community: "public", + mikrotik_username: "root", + mikrotik_password: "pass", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == true + end + + test "returns true when MikroTik password changed" do + old_state = %{ + mikrotik_username: "admin", + mikrotik_password: "oldpass", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + updated = %{ + mikrotik_username: "admin", + mikrotik_password: "newpass", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == true + end + + test "returns true when MikroTik port changed" do + old_state = %{ + mikrotik_username: "admin", + mikrotik_password: "pass", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + updated = %{ + mikrotik_username: "admin", + mikrotik_password: "pass", + mikrotik_port: 8728, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == true + end + + test "returns true when MikroTik SSL setting changed" do + old_state = %{ + mikrotik_username: "admin", + mikrotik_password: "pass", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + updated = %{ + mikrotik_username: "admin", + mikrotik_password: "pass", + mikrotik_port: 8729, + mikrotik_use_ssl: false, + mikrotik_enabled: true + } + + assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == true + end + + test "returns true when MikroTik enabled setting changed" do + old_state = %{ + mikrotik_username: "admin", + mikrotik_password: "pass", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: false + } + + updated = %{ + mikrotik_username: "admin", + mikrotik_password: "pass", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == true + end + + test "returns false when no MikroTik fields changed" do + old_state = %{ + snmp_community: "public", + mikrotik_username: "admin", + mikrotik_password: "pass", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + updated = %{ + snmp_community: "public", + mikrotik_username: "admin", + mikrotik_password: "pass", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == false + end + + test "returns false when only SNMP community changed (not MikroTik field)" do + old_state = %{ + snmp_community: "public", + mikrotik_username: "admin", + mikrotik_password: "pass", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + updated = %{ + snmp_community: "private", + mikrotik_username: "admin", + mikrotik_password: "pass", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == false + end + + test "returns true when MikroTik field changes from nil to value" do + old_state = %{ + mikrotik_username: nil, + mikrotik_password: nil, + mikrotik_port: nil, + mikrotik_use_ssl: nil, + mikrotik_enabled: nil + } + + updated = %{ + mikrotik_username: "admin", + mikrotik_password: nil, + mikrotik_port: nil, + mikrotik_use_ssl: nil, + mikrotik_enabled: nil + } + + assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == true + end + end + + describe "extract_mikrotik_attrs/1" do + test "extracts all MikroTik fields from struct" do + struct = %{ + mikrotik_username: "admin", + mikrotik_password: "password", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true, + snmp_community: "public" + } + + result = ConfigChangeTracker.extract_mikrotik_attrs(struct) + + assert result == %{ + mikrotik_username: "admin", + mikrotik_password: "password", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + end + + test "extracts MikroTik fields with nils" do + struct = %{ + mikrotik_username: "admin", + mikrotik_password: nil, + mikrotik_port: nil, + mikrotik_use_ssl: nil, + mikrotik_enabled: false + } + + result = ConfigChangeTracker.extract_mikrotik_attrs(struct) + + assert result.mikrotik_username == "admin" + assert result.mikrotik_password == nil + assert result.mikrotik_port == nil + assert result.mikrotik_use_ssl == nil + assert result.mikrotik_enabled == false + end + + test "does not include SNMP community field" do + struct = %{ + snmp_community: "public", + mikrotik_username: "admin", + mikrotik_password: "pass", + mikrotik_port: 8729, + mikrotik_use_ssl: true, + mikrotik_enabled: true + } + + result = ConfigChangeTracker.extract_mikrotik_attrs(struct) + + refute Map.has_key?(result, :snmp_community) + end + end +end