towerops/test/towerops/sonar/client_test.exs
Graham McIntire 3ca0834ef0 tests: raise coverage to 70% via helper promotion + new unit/property tests
Promoted pure presentation and utility helpers from `defp` to `def @doc false`
across ~20 LiveViews, Oban workers, and sync modules so they're reachable from
unit tests. Refactored several `cond` blocks into idiomatic function heads with
guards. Added ~250 new test cases in new files under test/towerops and
test/towerops_web, including DB-backed tests for CnMaestro.Sync and
AlertNotificationWorker, and removed dead LiveView tab components and
CapacityLive (no callers anywhere in lib/test).

Configured mix.exs test_coverage.ignore_modules to exclude vendored third-party
code (SnmpKit, protobuf-generated Towerops.Agent.*, Absinthe GraphQL types,
Phoenix HTML modules, Inspect protocol impls) from coverage calculations —
these are not our project code.

Coverage: 66.93% → 70.09%. Full suite: 10,127 tests, 0 failures.
2026-04-24 09:49:06 -05:00

186 lines
5.8 KiB
Elixir

defmodule Towerops.Sonar.ClientTest do
use ExUnit.Case, async: true
alias Towerops.Sonar.Client
@url "https://sonar.example.com"
@token "test-token"
describe "query/4" do
test "returns {:ok, data} on 200 success" do
Req.Test.stub(Client, fn conn ->
assert conn.request_path == "/api/graphql"
assert Plug.Conn.get_req_header(conn, "authorization") == ["Bearer #{@token}"]
{:ok, body, conn} = Plug.Conn.read_body(conn)
assert Jason.decode!(body)["query"] =~ "accounts"
Req.Test.json(conn, %{"data" => %{"accounts" => %{"entities" => []}}})
end)
assert {:ok, data} = Client.query(@url, @token, "{ accounts { entities { id } } }")
assert data == %{"accounts" => %{"entities" => []}}
end
test "returns :unauthorized on 401" do
Req.Test.stub(Client, fn conn ->
conn |> Plug.Conn.put_status(401) |> Req.Test.json(%{})
end)
assert {:error, :unauthorized} = Client.query(@url, @token, "{ q }")
end
test "returns :forbidden on 403" do
Req.Test.stub(Client, fn conn ->
conn |> Plug.Conn.put_status(403) |> Req.Test.json(%{})
end)
assert {:error, :forbidden} = Client.query(@url, @token, "{ q }")
end
test "returns {:rate_limited, seconds} on 429 with retry-after" do
Req.Test.stub(Client, fn conn ->
conn
|> Plug.Conn.put_resp_header("retry-after", "120")
|> Plug.Conn.put_status(429)
|> Req.Test.json(%{})
end)
assert {:error, {:rate_limited, 120}} = Client.query(@url, @token, "{ q }")
end
test "returns {:rate_limited, 60} on 429 with no retry-after" do
Req.Test.stub(Client, fn conn ->
conn |> Plug.Conn.put_status(429) |> Req.Test.json(%{})
end)
assert {:error, {:rate_limited, 60}} = Client.query(@url, @token, "{ q }")
end
test "returns {:rate_limited, 60} on 429 with non-integer retry-after" do
Req.Test.stub(Client, fn conn ->
conn
|> Plug.Conn.put_resp_header("retry-after", "invalid")
|> Plug.Conn.put_status(429)
|> Req.Test.json(%{})
end)
assert {:error, {:rate_limited, 60}} = Client.query(@url, @token, "{ q }")
end
test "returns graphql_errors on 200 with errors field" do
Req.Test.stub(Client, fn conn ->
Req.Test.json(conn, %{"errors" => [%{"message" => "schema error"}]})
end)
assert {:error, {:graphql_errors, [%{"message" => "schema error"}]}} =
Client.query(@url, @token, "{ bad }")
end
test "returns graphql_errors on 400 with errors field" do
Req.Test.stub(Client, fn conn ->
conn
|> Plug.Conn.put_status(400)
|> Req.Test.json(%{"errors" => [%{"message" => "bad query"}]})
end)
assert {:error, {:graphql_errors, _}} = Client.query(@url, @token, "{ bad }")
end
test "returns {:unexpected_status, status} on other non-2xx" do
Req.Test.stub(Client, fn conn ->
conn |> Plug.Conn.put_status(502) |> Req.Test.json(%{})
end)
assert {:error, {:unexpected_status, 502}} = Client.query(@url, @token, "{ q }")
end
test "trims trailing slash from instance url" do
Req.Test.stub(Client, fn conn ->
assert conn.request_path == "/api/graphql"
Req.Test.json(conn, %{"data" => %{}})
end)
assert {:ok, _} = Client.query("#{@url}/", @token, "{ q }")
end
test "sends variables in request body" do
Req.Test.stub(Client, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
payload = Jason.decode!(body)
assert payload["variables"] == %{"page" => 2, "limit" => 50}
Req.Test.json(conn, %{"data" => %{}})
end)
assert {:ok, _} = Client.query(@url, @token, "{ q }", %{"page" => 2, "limit" => 50})
end
end
describe "test_connection/2" do
test "returns ok when query succeeds" do
Req.Test.stub(Client, fn conn ->
Req.Test.json(conn, %{"data" => %{"accounts" => %{"entities" => []}}})
end)
assert {:ok, %{}} = Client.test_connection(@url, @token)
end
test "returns error when query fails" do
Req.Test.stub(Client, fn conn ->
conn |> Plug.Conn.put_status(401) |> Req.Test.json(%{})
end)
assert {:error, :unauthorized} = Client.test_connection(@url, @token)
end
end
describe "list_accounts/2 with pagination" do
test "aggregates entities across pages" do
{:ok, agent} = Agent.start_link(fn -> 1 end)
Req.Test.stub(Client, fn conn ->
page = Agent.get_and_update(agent, fn p -> {p, p + 1} end)
{:ok, body, conn} = Plug.Conn.read_body(conn)
payload = Jason.decode!(body)
assert payload["variables"]["page"] == page
Req.Test.json(conn, %{
"data" => %{
"accounts" => %{
"entities" => [%{"id" => "acct-#{page}"}],
"page_info" => %{"page" => page, "total_pages" => 2}
}
}
})
end)
assert {:ok, entities} = Client.list_accounts(@url, @token)
assert length(entities) == 2
assert Enum.map(entities, & &1["id"]) == ["acct-1", "acct-2"]
end
test "stops at single page when total_pages is 1" do
Req.Test.stub(Client, fn conn ->
Req.Test.json(conn, %{
"data" => %{
"accounts" => %{
"entities" => [%{"id" => "a"}, %{"id" => "b"}],
"page_info" => %{"page" => 1, "total_pages" => 1}
}
}
})
end)
assert {:ok, [%{"id" => "a"}, %{"id" => "b"}]} = Client.list_accounts(@url, @token)
end
test "propagates errors" do
Req.Test.stub(Client, fn conn ->
conn |> Plug.Conn.put_status(401) |> Req.Test.json(%{})
end)
assert {:error, :unauthorized} = Client.list_accounts(@url, @token)
end
end
end