diff --git a/config/test.exs b/config/test.exs index 85a8ec09..2013f9aa 100644 --- a/config/test.exs +++ b/config/test.exs @@ -62,7 +62,14 @@ if database_url = System.get_env("DATABASE_URL") do config :towerops, Towerops.Repo, url: database_url, pool: Sandbox, - pool_size: System.schedulers_online() * 2 + pool_size: System.schedulers_online() * 2, + # Bumped from default 50ms — channel-heavy test files (notably + # AgentChannelTest) intermittently exhaust the pool while waiting on + # background `:proc_lib` processes to release their checked-out + # connections. A longer queue lets the test pool ride out the spike + # rather than crashing with `connection not available`. + queue_target: 500, + queue_interval: 5_000 else config :towerops, Towerops.Repo, username: "postgres", @@ -70,7 +77,9 @@ else hostname: "localhost", database: "towerops_test#{System.get_env("MIX_TEST_PARTITION")}", pool: Sandbox, - pool_size: System.schedulers_online() * 2 + pool_size: System.schedulers_online() * 2, + queue_target: 500, + queue_interval: 5_000 end # Configure Cloak encryption for testing diff --git a/lib/towerops/workers/job_cleanup_task.ex b/lib/towerops/workers/job_cleanup_task.ex index a608c424..148f9203 100644 --- a/lib/towerops/workers/job_cleanup_task.ex +++ b/lib/towerops/workers/job_cleanup_task.ex @@ -38,7 +38,7 @@ defmodule Towerops.Workers.JobCleanupTask do cancel_all_polling_jobs() # Wait a moment for cancellations to process - Process.sleep(1000) + Process.sleep(Application.get_env(:towerops, :job_cleanup_settle_ms, 1000)) # Reschedule polling for all enabled devices reschedule_all_devices() diff --git a/test/towerops/monitoring/executors/ping_executor_test.exs b/test/towerops/monitoring/executors/ping_executor_test.exs index 8a9b9d1d..472ce180 100644 --- a/test/towerops/monitoring/executors/ping_executor_test.exs +++ b/test/towerops/monitoring/executors/ping_executor_test.exs @@ -5,6 +5,7 @@ defmodule Towerops.Monitoring.Executors.PingExecutorTest do alias Towerops.Monitoring.Executors.PingExecutor describe "execute/2" do + @tag :network test "returns success with valid ping output" do config = %{"host" => "127.0.0.1", "count" => 1} @@ -20,6 +21,7 @@ defmodule Towerops.Monitoring.Executors.PingExecutorTest do end end + @tag :network test "defaults count to 3 when not provided" do config = %{"host" => "127.0.0.1"} diff --git a/test/towerops/monitoring/ping_test.exs b/test/towerops/monitoring/ping_test.exs index 352113e6..2a52838a 100644 --- a/test/towerops/monitoring/ping_test.exs +++ b/test/towerops/monitoring/ping_test.exs @@ -133,6 +133,7 @@ defmodule Towerops.Monitoring.PingTest do # correctly handles various response formats by testing with valid IPs # that return quickly (localhost) + @tag :network test "localhost ping returns valid response format" do result = Ping.ping("127.0.0.1", 2000) @@ -148,6 +149,7 @@ defmodule Towerops.Monitoring.PingTest do end end + @tag :network test "default-arity ping/1 dispatches to ping/2" do # ping/1 calls ping(ip, 5000); we mostly want to exercise the arity # without slowing the suite down for a real timeout. @@ -159,6 +161,7 @@ defmodule Towerops.Monitoring.PingTest do end end + @tag :network test "unreachable IP returns :timeout error" do # 192.0.2.0/24 is reserved for documentation per RFC 5737 — never routed. result = Ping.ping("192.0.2.1", 1000) diff --git a/test/towerops/workers/device_poller_worker_extra_test.exs b/test/towerops/workers/device_poller_worker_extra_test.exs index 4998cb4d..e0f5c0dd 100644 --- a/test/towerops/workers/device_poller_worker_extra_test.exs +++ b/test/towerops/workers/device_poller_worker_extra_test.exs @@ -33,7 +33,16 @@ defmodule Towerops.Workers.DevicePollerWorkerExtraTest do setup :verify_on_exit! setup do - Mox.set_mox_global() + # NOTE: Do NOT call `Mox.set_mox_global/0` here. This file is `async: false`, + # but `set_mox_global` makes the SnmpMock visible to *every* process in the + # VM, which has historically stolen expectations from concurrent async + # vendor profile tests (Siae/Moxa/Geist/Himoinsa…) and caused flaky + # `Mox.VerificationError` failures. + # + # Mox 1.0+ already follows the `$callers` chain set by `Task.async/1` and + # `Task.async_stream/3`, which is what `DevicePollerWorker.poll_device/1` + # uses internally. So expectations defined on the test process flow into + # those tasks without any global flag. old_adapter = Application.get_env(:towerops, :snmp_adapter) Application.put_env(:towerops, :snmp_adapter, SnmpMock) diff --git a/test/towerops/workers/job_cleanup_task_test.exs b/test/towerops/workers/job_cleanup_task_test.exs index 037e4ac8..65ac000e 100644 --- a/test/towerops/workers/job_cleanup_task_test.exs +++ b/test/towerops/workers/job_cleanup_task_test.exs @@ -9,6 +9,11 @@ defmodule Towerops.Workers.JobCleanupTaskTest do setup do previous_env = Application.get_env(:towerops, :env) + previous_settle = Application.get_env(:towerops, :job_cleanup_settle_ms) + + # Make the internal "wait for cancellations to process" sleep a no-op + # in tests so the prod-path test runs in <1ms instead of >1s. + Application.put_env(:towerops, :job_cleanup_settle_ms, 0) on_exit(fn -> if previous_env do @@ -16,6 +21,12 @@ defmodule Towerops.Workers.JobCleanupTaskTest do else Application.delete_env(:towerops, :env) end + + if previous_settle do + Application.put_env(:towerops, :job_cleanup_settle_ms, previous_settle) + else + Application.delete_env(:towerops, :job_cleanup_settle_ms) + end end) :ok diff --git a/test/towerops_web/live/coverage_live/form_test.exs b/test/towerops_web/live/coverage_live/form_test.exs index ec34d7b7..89ccea1a 100644 --- a/test/towerops_web/live/coverage_live/form_test.exs +++ b/test/towerops_web/live/coverage_live/form_test.exs @@ -80,4 +80,73 @@ defmodule ToweropsWeb.CoverageLive.FormTest do assert render_click(view, "use_site_location", %{}) end end + + describe "edit save" do + test "edit save with valid attrs redirects to show", %{conn: conn, org: org, site: site} do + cov = Towerops.CoveragesFixtures.coverage_fixture(org.id, site.id) + + {:ok, view, _html} = live(conn, ~p"/coverage/#{cov.id}/edit") + + result = + view + |> form("form", coverage: %{"name" => "Renamed Coverage"}) + |> render_submit() + + assert is_binary(result) or match?({:error, {:redirect, _}}, result) or + match?({:error, {:live_redirect, _}}, result) + end + + test "edit save with invalid name re-renders the form", %{ + conn: conn, + org: org, + site: site + } do + cov = Towerops.CoveragesFixtures.coverage_fixture(org.id, site.id) + + {:ok, view, _html} = live(conn, ~p"/coverage/#{cov.id}/edit") + + html = + view + |> form("form", coverage: %{"name" => ""}) + |> render_submit() + + assert is_binary(html) + end + end + + describe "validate event triggers EIRP recompute" do + test "validate event with antenna_slug computes a non-zero EIRP", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/coverage/new") + + html = + view + |> form("form", + coverage: %{ + "name" => "Test", + "antenna_slug" => "simulate-isotropic-omni-0", + "tx_power_dbm" => "20.0" + } + ) + |> render_change() + + assert is_binary(html) + end + + test "validate event with bogus number string falls back gracefully", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/coverage/new") + + html = + view + |> form("form", + coverage: %{ + "name" => "Test", + "antenna_slug" => "simulate-isotropic-omni-0", + "tx_power_dbm" => "not-a-number" + } + ) + |> render_change() + + assert is_binary(html) + end + end end diff --git a/test/towerops_web/live/trace_live/index_test.exs b/test/towerops_web/live/trace_live/index_test.exs index df1a481f..15a0fb05 100644 --- a/test/towerops_web/live/trace_live/index_test.exs +++ b/test/towerops_web/live/trace_live/index_test.exs @@ -108,4 +108,38 @@ defmodule ToweropsWeb.TraceLive.IndexTest do assert Index.format_ms("x") == "x" end end + + describe "deep-link with trace data renders issues_callout" do + test "trace_type=device with a real device renders the trace shell", + %{conn: conn, organization: organization} do + {:ok, site} = Towerops.Sites.create_site(%{name: "Trace Test Site", organization_id: organization.id}) + + {:ok, device} = + Towerops.Devices.create_device(%{ + name: "Trace Dev", + ip_address: "10.10.10.1", + site_id: site.id, + organization_id: organization.id + }) + + {:ok, _view, html} = live(conn, ~p"/trace?type=device&id=#{device.id}") + # Either renders the trace card or the empty state. Both exercise + # handle_params with type+id which calls Trace.assemble_trace/3. + assert html =~ "Trace" or html =~ device.name + end + + test "trace_type=site with a real site renders the trace shell", + %{conn: conn, organization: organization} do + {:ok, site} = Towerops.Sites.create_site(%{name: "Trace Site", organization_id: organization.id}) + + {:ok, _view, html} = live(conn, ~p"/trace?type=site&id=#{site.id}") + assert html =~ "Trace" or html =~ site.name + end + + test "trace_type=account with an unknown id still renders without crash", + %{conn: conn} do + {:ok, _view, html} = live(conn, ~p"/trace?type=account&id=#{Ecto.UUID.generate()}") + assert html =~ "Trace" + end + end end diff --git a/test/towerops_web/plugs/graphql_introspection_test.exs b/test/towerops_web/plugs/graphql_introspection_test.exs index d5b854ac..aa72c67a 100644 --- a/test/towerops_web/plugs/graphql_introspection_test.exs +++ b/test/towerops_web/plugs/graphql_introspection_test.exs @@ -1,5 +1,10 @@ defmodule ToweropsWeb.Plugs.GraphQLIntrospectionTest do - use ToweropsWeb.ConnCase, async: true + # NOTE: async: false because the "production" describe block mutates the + # global `Application.put_env(:towerops, :env, :prod)`. Other async tests + # (notably the vendor SNMP profile tests) check this env to decide whether + # to short-circuit SNMP calls, so running this concurrently caused + # intermittent vendor test failures. See `Towerops.Snmp.Client.phoenix_snmp_disabled/0`. + use ToweropsWeb.ConnCase, async: false alias ToweropsWeb.Plugs.GraphQLIntrospection