towerops/test/towerops_web/controllers/api/v1/coverages_controller_test.exs
2026-06-16 09:56:51 -05:00

212 lines
6.9 KiB
Elixir

defmodule ToweropsWeb.Api.V1.CoveragesControllerTest do
use ToweropsWeb.ConnCase, async: false
import Ecto.Query
import Towerops.AccountsFixtures
import Towerops.CoveragesFixtures
import Towerops.OrganizationsFixtures
alias Towerops.ApiTokens
alias Towerops.Coverages.Coverage
setup %{conn: conn} do
user = user_fixture()
org = organization_fixture(user.id)
site = site_fixture(org.id, %{latitude: 30.27, longitude: -97.74})
{:ok, {_token, raw}} =
ApiTokens.create_api_token(%{
organization_id: org.id,
user_id: user.id,
name: "test"
})
conn = put_req_header(conn, "authorization", "Bearer #{raw}")
%{conn: conn, org: org, user: user, site: site}
end
describe "GET /api/v1/coverages" do
test "returns coverages for the authenticated organization", %{conn: conn, org: org, site: site} do
cov = coverage_fixture(org.id, site.id, %{name: "Sector A"})
resp = conn |> get(~p"/api/v1/coverages") |> json_response(200)
assert [%{"id" => id, "name" => "Sector A"}] = resp["coverages"]
assert id == cov.id
end
test "returns empty list when org has no coverages", %{conn: conn} do
assert %{"coverages" => []} = conn |> get(~p"/api/v1/coverages") |> json_response(200)
end
end
describe "POST /api/v1/coverages" do
test "creates and queues a coverage", %{conn: conn, site: site} do
attrs = %{
"name" => "API-created",
"site_id" => site.id,
"antenna_slug" => "simulate-isotropic-omni-0",
"frequency_ghz" => 5.8,
"tx_power_dbm" => 20.0,
"height_agl_ft" => 80,
"azimuth_deg" => 0,
"downtilt_deg" => 0,
"radius_mi" => 3
}
resp = conn |> post(~p"/api/v1/coverages", coverage: attrs) |> json_response(201)
assert resp["name"] == "API-created"
assert resp["status"] == "queued"
assert resp["site_id"] == site.id
assert is_binary(resp["id"]) and byte_size(resp["id"]) > 0
end
test "returns 422 with field errors on bad input", %{conn: conn} do
resp =
conn
|> post(~p"/api/v1/coverages", coverage: %{"name" => ""})
|> json_response(422)
assert resp["errors"]
end
test "returns 400 when 'coverage' wrapper is missing", %{conn: conn} do
resp = conn |> post(~p"/api/v1/coverages", %{}) |> json_response(400)
assert resp["error"] =~ "Missing"
end
end
describe "GET /api/v1/coverages/:id" do
test "returns a single coverage", %{conn: conn, org: org, site: site} do
cov = coverage_fixture(org.id, site.id)
resp = conn |> get(~p"/api/v1/coverages/#{cov.id}") |> json_response(200)
assert resp["id"] == cov.id
end
test "returns 404 for cross-org coverage", %{conn: conn} do
assert conn
|> get(~p"/api/v1/coverages/#{Ecto.UUID.generate()}")
|> json_response(404)
end
end
describe "PATCH /api/v1/coverages/:id" do
test "updates editable fields", %{conn: conn, org: org, site: site} do
cov = coverage_fixture(org.id, site.id, %{name: "before"})
resp =
conn
|> patch(~p"/api/v1/coverages/#{cov.id}", coverage: %{"name" => "after"})
|> json_response(200)
assert resp["name"] == "after"
end
end
describe "DELETE /api/v1/coverages/:id" do
test "deletes the coverage", %{conn: conn, org: org, site: site} do
cov = coverage_fixture(org.id, site.id)
conn = delete(conn, ~p"/api/v1/coverages/#{cov.id}")
assert response(conn, 204)
assert nil == Towerops.Coverages.get_coverage(org.id, cov.id)
end
end
describe "POST /api/v1/coverages/:id/recompute" do
test "queues a recompute and returns 202", %{conn: conn, org: org, site: site} do
cov = coverage_fixture(org.id, site.id, %{status: "ready"})
resp = conn |> post(~p"/api/v1/coverages/#{cov.id}/recompute") |> json_response(202)
assert resp["status"] == "queued"
end
end
describe "GET /api/v1/coverages/:id/kmz" do
defp set_coverage_state(cov, attrs) do
Towerops.Repo.update_all(
from(c in Coverage, where: c.id == ^cov.id),
set: Map.to_list(attrs)
)
Towerops.Repo.get!(Coverage, cov.id)
end
test "returns 409 when coverage is not ready", %{conn: conn, org: org, site: site} do
cov = org.id |> coverage_fixture(site.id) |> set_coverage_state(%{status: "queued"})
resp = conn |> get(~p"/api/v1/coverages/#{cov.id}/kmz") |> json_response(409)
assert resp["error"] =~ "not ready"
end
test "returns 409 when coverage is ready but png_path is nil",
%{conn: conn, org: org, site: site} do
cov =
org.id
|> coverage_fixture(site.id)
|> set_coverage_state(%{status: "ready", png_path: nil})
resp = conn |> get(~p"/api/v1/coverages/#{cov.id}/kmz") |> json_response(409)
assert resp["error"] =~ "not ready"
end
test "returns 500 when png file is missing on disk",
%{conn: conn, org: org, site: site} do
cov =
org.id
|> coverage_fixture(site.id)
|> set_coverage_state(%{
status: "ready",
png_path: "/coverage/missing/#{System.unique_integer([:positive])}.png",
bbox_min_lat: 30.0,
bbox_max_lat: 30.5,
bbox_min_lon: -97.5,
bbox_max_lon: -97.0
})
resp = conn |> get(~p"/api/v1/coverages/#{cov.id}/kmz") |> json_response(500)
assert resp["error"] =~ "Failed to build KMZ"
end
test "returns 404 for an unknown coverage id", %{conn: conn} do
conn = get(conn, ~p"/api/v1/coverages/#{Ecto.UUID.generate()}/kmz")
assert response(conn, 404)
end
test "returns 200 + KMZ bytes when png exists on disk",
%{conn: conn, org: org, site: site} do
png_dir = Path.join([Application.app_dir(:towerops, "priv/static"), "coverage", "kmztest"])
File.mkdir_p!(png_dir)
png_path_rel = "/coverage/kmztest/cov-#{System.unique_integer([:positive])}.png"
png_abs = Path.join(Application.app_dir(:towerops, "priv/static"), Path.relative_to(png_path_rel, "/"))
File.write!(png_abs, <<137, 80, 78, 71, 13, 10, 26, 10>>)
on_exit(fn -> File.rm(png_abs) end)
cov =
org.id
|> coverage_fixture(site.id)
|> set_coverage_state(%{
status: "ready",
png_path: png_path_rel,
name: "T<&> coverage",
bbox_min_lat: 30.1,
bbox_max_lat: 30.5,
bbox_min_lon: -97.5,
bbox_max_lon: -97.0
})
conn = get(conn, ~p"/api/v1/coverages/#{cov.id}/kmz")
assert response(conn, 200)
assert [disposition] = get_resp_header(conn, "content-disposition")
assert disposition =~ "coverage-#{cov.id}.kmz"
end
end
describe "auth" do
test "rejects requests without a Bearer token" do
conn = get(build_conn(), ~p"/api/v1/coverages")
assert response(conn, 401)
end
end
end