diff --git a/lib/towerops/preseem/client.ex b/lib/towerops/preseem/client.ex index f8317d8b..d9bcce2f 100644 --- a/lib/towerops/preseem/client.ex +++ b/lib/towerops/preseem/client.ex @@ -90,12 +90,21 @@ defmodule Towerops.Preseem.Client do end defp request(method, url, api_key) do - Req.request( + opts = [ method: method, url: url, - headers: [{"authorization", "Bearer #{api_key}"}, {"accept", "application/json"}], - plug: {Req.Test, __MODULE__} - ) + headers: [{"authorization", "Bearer #{api_key}"}, {"accept", "application/json"}] + ] + + # Only use Req.Test plug in test environment + opts = + if Application.get_env(:towerops, :env) == :test do + Keyword.put(opts, :plug, {Req.Test, __MODULE__}) + else + opts + end + + Req.request(opts) rescue exception -> {:error, Exception.message(exception)} diff --git a/lib/towerops/preseem/fleet_intelligence.ex b/lib/towerops/preseem/fleet_intelligence.ex index 0d22dc7b..253e1ea0 100644 --- a/lib/towerops/preseem/fleet_intelligence.ex +++ b/lib/towerops/preseem/fleet_intelligence.ex @@ -74,8 +74,9 @@ defmodule Towerops.Preseem.FleetIntelligence do defp compute_capacity_ceiling(aps) do aps - |> Enum.filter(fn ap -> ap.qoe_score != nil and ap.subscriber_count != nil end) - |> Enum.filter(fn ap -> ap.qoe_score >= 70 end) + |> Enum.filter(fn ap -> + ap.qoe_score != nil and ap.subscriber_count != nil and ap.qoe_score >= 70 + end) |> Enum.map(& &1.subscriber_count) |> Enum.max(fn -> nil end) end diff --git a/lib/towerops/preseem/insights.ex b/lib/towerops/preseem/insights.ex index 76d991fe..bfe056d0 100644 --- a/lib/towerops/preseem/insights.ex +++ b/lib/towerops/preseem/insights.ex @@ -112,24 +112,28 @@ defmodule Towerops.Preseem.Insights do device_id: ap.device_id }) - existing = - Repo.get_by(Insight, - preseem_access_point_id: ap.id, - type: full_attrs.type, - status: "active" - ) - - if existing do - [] - else - case %Insight{} |> Insight.changeset(full_attrs) |> Repo.insert() do - {:ok, insight} -> [insight] - {:error, _} -> [] - end - end + insert_insight_if_new(ap.id, full_attrs) end) end + defp insert_insight_if_new(ap_id, full_attrs) do + existing = + Repo.get_by(Insight, + preseem_access_point_id: ap_id, + type: full_attrs.type, + status: "active" + ) + + if existing do + [] + else + case %Insight{} |> Insight.changeset(full_attrs) |> Repo.insert() do + {:ok, insight} -> [insight] + {:error, _} -> [] + end + end + end + defp check_qoe_degradation(ap, baselines) do qoe_baseline = Enum.find(baselines, &(&1.metric_name == "qoe_score" and &1.period == "all")) diff --git a/lib/towerops/security/brute_force.ex b/lib/towerops/security/brute_force.ex index 3a8b8290..a1cc8084 100644 --- a/lib/towerops/security/brute_force.ex +++ b/lib/towerops/security/brute_force.ex @@ -375,6 +375,10 @@ defmodule Towerops.Security.BruteForce do defp create_cache_table do :ets.new(@whitelist_cache_table, [:set, :public, :named_table, read_concurrency: true]) + rescue + ArgumentError -> + # Table already exists due to race condition in async tests - this is fine + :ets.whereis(@whitelist_cache_table) end defp load_whitelist do diff --git a/lib/towerops/workers/preseem_sync_worker.ex b/lib/towerops/workers/preseem_sync_worker.ex index 13944fb0..c3bf7e6d 100644 --- a/lib/towerops/workers/preseem_sync_worker.ex +++ b/lib/towerops/workers/preseem_sync_worker.ex @@ -17,24 +17,7 @@ defmodule Towerops.Workers.PreseemSyncWorker do def perform(%Oban.Job{}) do integrations = Integrations.list_enabled_integrations("preseem") - results = - Enum.map(integrations, fn integration -> - if should_sync?(integration) do - case Sync.sync_organization(integration) do - {:ok, result} -> - Logger.info("Preseem sync completed for org #{integration.organization_id}: #{inspect(result)}") - - {:ok, result} - - {:error, reason} -> - Logger.error("Preseem sync failed for org #{integration.organization_id}: #{inspect(reason)}") - - {:error, reason} - end - else - :skipped - end - end) + results = Enum.map(integrations, &sync_integration/1) synced = Enum.count(results, &match?({:ok, _}, &1)) failed = Enum.count(results, &match?({:error, _}, &1)) @@ -47,6 +30,22 @@ defmodule Towerops.Workers.PreseemSyncWorker do :ok end + defp sync_integration(integration) do + if should_sync?(integration) do + case Sync.sync_organization(integration) do + {:ok, result} -> + Logger.info("Preseem sync completed for org #{integration.organization_id}: #{inspect(result)}") + {:ok, result} + + {:error, reason} -> + Logger.error("Preseem sync failed for org #{integration.organization_id}: #{inspect(reason)}") + {:error, reason} + end + else + :skipped + end + end + defp should_sync?(integration) do case integration.last_synced_at do nil -> diff --git a/lib/towerops_web/live/org/integrations_live.ex b/lib/towerops_web/live/org/integrations_live.ex index 3a6346ad..1379db44 100644 --- a/lib/towerops_web/live/org/integrations_live.ex +++ b/lib/towerops_web/live/org/integrations_live.ex @@ -115,19 +115,8 @@ defmodule ToweropsWeb.Org.IntegrationsLive do if api_key == "" or is_nil(api_key) do {:noreply, assign(socket, :test_result, {:error, "Please enter an API key first"})} else - case PreseemClient.test_connection(api_key) do - {:ok, _body} -> - {:noreply, assign(socket, :test_result, {:ok, "Connection successful"})} - - {:error, :unauthorized} -> - {:noreply, assign(socket, :test_result, {:error, "Invalid API key"})} - - {:error, :forbidden} -> - {:noreply, assign(socket, :test_result, {:error, "Access forbidden"})} - - {:error, reason} -> - {:noreply, assign(socket, :test_result, {:error, "Connection failed: #{inspect(reason)}"})} - end + result = PreseemClient.test_connection(api_key) + {:noreply, assign(socket, :test_result, format_connection_result(result))} end end @@ -197,4 +186,23 @@ defmodule ToweropsWeb.Org.IntegrationsLive do _ -> nil end end + + defp format_connection_result({:ok, _body}), do: {:ok, "Connection successful"} + defp format_connection_result({:error, :unauthorized}), do: {:error, "Invalid API key"} + defp format_connection_result({:error, :forbidden}), do: {:error, "Access forbidden"} + + defp format_connection_result({:error, {:unexpected_status, status}}), + do: {:error, "API returned unexpected status: #{status}"} + + defp format_connection_result({:error, %{reason: :timeout}}), + do: {:error, "Connection timeout - unable to reach Preseem API"} + + defp format_connection_result({:error, %{reason: :econnrefused}}), + do: {:error, "Connection refused - check API endpoint"} + + defp format_connection_result({:error, %{reason: :nxdomain}}), do: {:error, "DNS lookup failed - check API endpoint"} + + # Never expose internal error details to users + defp format_connection_result({:error, _reason}), + do: {:error, "Connection failed - please check your API credentials and try again"} end diff --git a/test/towerops/preseem/baseline_test.exs b/test/towerops/preseem/baseline_test.exs index 512f9497..c69b585a 100644 --- a/test/towerops/preseem/baseline_test.exs +++ b/test/towerops/preseem/baseline_test.exs @@ -32,7 +32,7 @@ defmodule Towerops.Preseem.BaselineTest do assert {:ok, 1} = Baseline.compute_baselines(org.id) baselines = Repo.all(DeviceBaseline) - assert length(baselines) > 0 + refute baselines == [] latency_baseline = Enum.find(baselines, &(&1.metric_name == "avg_latency")) assert latency_baseline diff --git a/test/towerops/snmp/sensor_change_detector_test.exs b/test/towerops/snmp/sensor_change_detector_test.exs index aef9f3e6..895df234 100644 --- a/test/towerops/snmp/sensor_change_detector_test.exs +++ b/test/towerops/snmp/sensor_change_detector_test.exs @@ -40,7 +40,7 @@ defmodule Towerops.Snmp.SensorChangeDetectorTest do %{device: device, snmp_device: snmp_device} end - defp create_sensor(snmp_device, attrs \\ %{}) do + defp create_sensor(snmp_device, attrs) do defaults = %{ snmp_device_id: snmp_device.id, sensor_type: "frequency", diff --git a/test/towerops_native_test.exs b/test/towerops_native_test.exs index 105c0222..12e7f767 100644 --- a/test/towerops_native_test.exs +++ b/test/towerops_native_test.exs @@ -1,6 +1,9 @@ defmodule ToweropsNativeTest do use ExUnit.Case, async: false + # Suppress false positive dialyzer warnings for NIF return types + @dialyzer :no_match + # NIF not compiled in this worktree — skip all tests @moduletag :skip @@ -104,7 +107,7 @@ defmodule ToweropsNativeTest do oid1 = ToweropsNative.resolve_oid("SNMPv2-MIB::sysDescr") oid2 = ToweropsNative.resolve_oid("sysDescr") - # Both should resolve to the same OID + # Both should resolve to the same OID (dialyzer can't infer these always succeed) assert is_binary(oid1) assert is_binary(oid2) assert oid1 == oid2 diff --git a/test/towerops_web/channels/agent_channel_test.exs b/test/towerops_web/channels/agent_channel_test.exs index 84783978..6c8346e3 100644 --- a/test/towerops_web/channels/agent_channel_test.exs +++ b/test/towerops_web/channels/agent_channel_test.exs @@ -1206,7 +1206,6 @@ defmodule ToweropsWeb.AgentChannelTest do describe "device deletion and job refresh" do test "agent receives updated job list after device deletion", %{ - socket: socket, device: device, agent_token: agent_token } do @@ -1280,7 +1279,7 @@ defmodule ToweropsWeb.AgentChannelTest do # (This is verified by the channel staying alive) end - test "multiple rapid assignment changes are debounced", %{socket: socket, agent_token: agent_token} do + 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( diff --git a/test/towerops_web/live/site_live_test.exs b/test/towerops_web/live/site_live_test.exs index 7a4624c1..90633136 100644 --- a/test/towerops_web/live/site_live_test.exs +++ b/test/towerops_web/live/site_live_test.exs @@ -7,10 +7,13 @@ defmodule ToweropsWeb.SiteLiveTest do setup :register_and_log_in_user - setup %{user: user} do + setup %{conn: conn, user: user} do {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id) - %{organization: organization} + # Set the current organization in the session to ensure correct scope + conn = Plug.Conn.put_session(conn, :current_organization_id, organization.id) + + %{conn: conn, organization: organization} end describe "Index" do