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"]) 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