End-to-end CRUD scaffold for per-site, per-antenna RF coverage prediction. Schema, context, antenna registry, Oban worker stub, and three LiveViews are wired up; the actual ITM + LIDAR + buildings compute pipeline lands in a follow-up. - Migration + Coverage schema with full validation (RF params, pixel- budget cap, antenna_slug existence). organization_id excluded from cast (programmatic-only). - Coverages context: org-scoped CRUD, validate_site_in_organization rejects cross-tenant site_id refs at insert/update, queue_compute with PubSub broadcast on per-coverage and per-org topics. - MSI Planet .ant parser + persistent_term registry loaded at boot from priv/antennas/. Two synthetic .ant fixtures (omni + 90deg sector) bundled. - CoverageWorker on new :coverage Oban queue (concurrency 2). Job args carry organization_id; worker refuses to run on org mismatch. Stub flips status to "failed" with "compute not yet implemented" so the UI flow is exercisable. - LiveViews: index (org-wide, live status updates), form (antenna picker grouped by manufacturer), show (status banners, params panel, map placeholder). Sidebar nav link added. - Routes /coverage, /coverage/new, /coverage/:id, /coverage/:id/edit under :require_authenticated_user_with_default_org. - 41 new tests covering schema validation, .ant parsing, context CRUD with cross-org guards, Oban job enqueue.
200 lines
7.2 KiB
Elixir
200 lines
7.2 KiB
Elixir
defmodule Towerops.CoveragesTest do
|
|
use Towerops.DataCase, async: true
|
|
use Oban.Testing, repo: Towerops.Repo
|
|
|
|
import Towerops.AccountsFixtures
|
|
import Towerops.CoveragesFixtures
|
|
import Towerops.OrganizationsFixtures
|
|
|
|
alias Towerops.Coverages
|
|
alias Towerops.Coverages.Coverage
|
|
|
|
setup do
|
|
user = user_fixture()
|
|
org = organization_fixture(user.id)
|
|
site = site_fixture(org.id)
|
|
%{user: user, organization: org, site: site}
|
|
end
|
|
|
|
describe "create_coverage/2" do
|
|
test "inserts with valid attrs and starts in draft", %{organization: org, site: site} do
|
|
attrs = Map.put(valid_coverage_attrs(), :site_id, site.id)
|
|
|
|
assert {:ok, %Coverage{} = coverage} = Coverages.create_coverage(org.id, attrs)
|
|
assert coverage.status == "draft"
|
|
assert coverage.progress_pct == 0
|
|
assert coverage.organization_id == org.id
|
|
assert coverage.site_id == site.id
|
|
end
|
|
|
|
test "ignores any organization_id supplied in attrs (set from arg)",
|
|
%{organization: org, site: site} do
|
|
other_org_id = Ecto.UUID.generate()
|
|
|
|
attrs =
|
|
valid_coverage_attrs()
|
|
|> Map.put(:site_id, site.id)
|
|
|> Map.put(:organization_id, other_org_id)
|
|
|
|
assert {:ok, coverage} = Coverages.create_coverage(org.id, attrs)
|
|
assert coverage.organization_id == org.id
|
|
end
|
|
|
|
test "rejects a site that belongs to another organization",
|
|
%{organization: org} do
|
|
other_user = user_fixture()
|
|
other_org = organization_fixture(other_user.id)
|
|
foreign_site = site_fixture(other_org.id)
|
|
|
|
attrs = Map.put(valid_coverage_attrs(), :site_id, foreign_site.id)
|
|
|
|
assert {:error, changeset} = Coverages.create_coverage(org.id, attrs)
|
|
assert "does not belong to your organization" in errors_on(changeset).site_id
|
|
end
|
|
|
|
test "rejects duplicate name within the same site", %{organization: org, site: site} do
|
|
attrs =
|
|
%{name: "duplicate"}
|
|
|> valid_coverage_attrs()
|
|
|> Map.put(:site_id, site.id)
|
|
|
|
assert {:ok, _} = Coverages.create_coverage(org.id, attrs)
|
|
assert {:error, changeset} = Coverages.create_coverage(org.id, attrs)
|
|
assert "has already been taken for this site" in errors_on(changeset).name
|
|
end
|
|
|
|
test "allows the same name on different sites", %{organization: org, site: site} do
|
|
site2 = site_fixture(org.id)
|
|
|
|
attrs1 = %{name: "same"} |> valid_coverage_attrs() |> Map.put(:site_id, site.id)
|
|
attrs2 = Map.put(attrs1, :site_id, site2.id)
|
|
|
|
assert {:ok, _} = Coverages.create_coverage(org.id, attrs1)
|
|
assert {:ok, _} = Coverages.create_coverage(org.id, attrs2)
|
|
end
|
|
|
|
test "returns error changeset on invalid attrs", %{organization: org, site: site} do
|
|
attrs = %{name: ""} |> valid_coverage_attrs() |> Map.put(:site_id, site.id)
|
|
|
|
assert {:error, %Ecto.Changeset{}} = Coverages.create_coverage(org.id, attrs)
|
|
end
|
|
end
|
|
|
|
describe "list_for_site/2 and list_for_organization/1" do
|
|
test "scopes results by org and site", %{user: _user, organization: org, site: site} do
|
|
_ = coverage_fixture(org.id, site.id, %{name: "alpha"})
|
|
_ = coverage_fixture(org.id, site.id, %{name: "beta"})
|
|
|
|
site2 = site_fixture(org.id)
|
|
_ = coverage_fixture(org.id, site2.id, %{name: "gamma"})
|
|
|
|
other_user = user_fixture()
|
|
other_org = organization_fixture(other_user.id)
|
|
other_site = site_fixture(other_org.id)
|
|
_ = coverage_fixture(other_org.id, other_site.id, %{name: "delta"})
|
|
|
|
assert org.id |> Coverages.list_for_site(site.id) |> Enum.map(& &1.name) |> Enum.sort() ==
|
|
~w(alpha beta)
|
|
|
|
org_names = org.id |> Coverages.list_for_organization() |> Enum.map(& &1.name) |> Enum.sort()
|
|
assert org_names == ~w(alpha beta gamma)
|
|
end
|
|
|
|
test "list_for_site does not leak across organizations", %{organization: org, site: site} do
|
|
coverage = coverage_fixture(org.id, site.id, %{name: "mine"})
|
|
|
|
other_user = user_fixture()
|
|
other_org = organization_fixture(other_user.id)
|
|
assert Coverages.list_for_site(other_org.id, site.id) == []
|
|
assert [%{id: id}] = Coverages.list_for_site(org.id, site.id)
|
|
assert id == coverage.id
|
|
end
|
|
end
|
|
|
|
describe "get_coverage!/2" do
|
|
test "returns the coverage when scoped to the right org", %{organization: org, site: site} do
|
|
coverage = coverage_fixture(org.id, site.id)
|
|
assert %Coverage{id: id} = Coverages.get_coverage!(org.id, coverage.id)
|
|
assert id == coverage.id
|
|
end
|
|
|
|
test "raises when scoped to a different org", %{organization: org, site: site} do
|
|
coverage = coverage_fixture(org.id, site.id)
|
|
other_user = user_fixture()
|
|
other_org = organization_fixture(other_user.id)
|
|
|
|
assert_raise Ecto.NoResultsError, fn ->
|
|
Coverages.get_coverage!(other_org.id, coverage.id)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "update_coverage/2" do
|
|
test "updates valid fields", %{organization: org, site: site} do
|
|
coverage = coverage_fixture(org.id, site.id)
|
|
assert {:ok, updated} = Coverages.update_coverage(coverage, %{name: "renamed"})
|
|
assert updated.name == "renamed"
|
|
end
|
|
|
|
test "rejects switching to a site in another organization",
|
|
%{organization: org, site: site} do
|
|
coverage = coverage_fixture(org.id, site.id)
|
|
|
|
other_user = user_fixture()
|
|
other_org = organization_fixture(other_user.id)
|
|
foreign_site = site_fixture(other_org.id)
|
|
|
|
assert {:error, changeset} =
|
|
Coverages.update_coverage(coverage, %{site_id: foreign_site.id})
|
|
|
|
assert "does not belong to your organization" in errors_on(changeset).site_id
|
|
end
|
|
end
|
|
|
|
describe "delete_coverage/1" do
|
|
test "removes the record", %{organization: org, site: site} do
|
|
coverage = coverage_fixture(org.id, site.id)
|
|
assert {:ok, _} = Coverages.delete_coverage(coverage)
|
|
assert_raise Ecto.NoResultsError, fn -> Coverages.get_coverage!(org.id, coverage.id) end
|
|
end
|
|
end
|
|
|
|
describe "mark_status/3" do
|
|
test "updates status, progress, error, computed_at without re-validating form fields",
|
|
%{organization: org, site: site} do
|
|
coverage = coverage_fixture(org.id, site.id)
|
|
|
|
assert {:ok, updated} =
|
|
Coverages.mark_status(coverage, "computing", %{progress_pct: 25})
|
|
|
|
assert updated.status == "computing"
|
|
assert updated.progress_pct == 25
|
|
|
|
assert {:ok, failed} =
|
|
Coverages.mark_status(coverage, "failed", %{error_message: "ITM crashed"})
|
|
|
|
assert failed.status == "failed"
|
|
assert failed.error_message == "ITM crashed"
|
|
end
|
|
|
|
test "rejects unknown status", %{organization: org, site: site} do
|
|
coverage = coverage_fixture(org.id, site.id)
|
|
assert {:error, %Ecto.Changeset{}} = Coverages.mark_status(coverage, "bogus", %{})
|
|
end
|
|
end
|
|
|
|
describe "queue_compute/1" do
|
|
test "transitions draft → queued and schedules an Oban job",
|
|
%{organization: org, site: site} do
|
|
coverage = coverage_fixture(org.id, site.id)
|
|
|
|
assert {:ok, queued} = Coverages.queue_compute(coverage)
|
|
assert queued.status == "queued"
|
|
|
|
assert_enqueued(
|
|
worker: Towerops.Workers.CoverageWorker,
|
|
args: %{"coverage_id" => coverage.id, "organization_id" => coverage.organization_id}
|
|
)
|
|
end
|
|
end
|
|
end
|