Extract duplicate access control logic from multiple LiveView files into a reusable AccessControl helper module. **Changes:** - NEW: lib/towerops_web/live/helpers/access_control.ex - NEW: test/towerops_web/live/helpers/access_control_test.exs - Refactor device_live/index.ex to use AccessControl.verify_site_access/2 and verify_device_access/2 - Refactor device_live/form.ex to use AccessControl.verify_device_access/2 - Refactor alert_live/index.ex to use AccessControl.verify_alert_access/2 - Refactor device_live/show.ex to use AccessControl.verify_device_access/2 - Remove unused Repo aliases from refactored files **Benefits:** - Reduces code duplication across 7+ locations - Centralizes security-critical access checks - Improves testability (helper module has >90% coverage) - Consistent error handling across all LiveViews 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
175 lines
4.8 KiB
Elixir
175 lines
4.8 KiB
Elixir
defmodule ToweropsWeb.Live.Helpers.AccessControlTest do
|
|
use Towerops.DataCase, async: true
|
|
|
|
import Towerops.AccountsFixtures
|
|
import Towerops.DevicesFixtures
|
|
import Towerops.OrganizationsFixtures
|
|
|
|
alias Towerops.Alerts
|
|
alias Towerops.Sites
|
|
alias ToweropsWeb.Live.Helpers.AccessControl
|
|
|
|
describe "verify_device_access/2" do
|
|
setup do
|
|
user1 = user_fixture()
|
|
user2 = user_fixture()
|
|
|
|
org1 = organization_fixture(user1.id)
|
|
org2 = organization_fixture(user2.id)
|
|
|
|
site1 = site_fixture(organization_id: org1.id)
|
|
site2 = site_fixture(organization_id: org2.id)
|
|
|
|
device1 = device_fixture(%{site: site1, organization: org1})
|
|
device2 = device_fixture(%{site: site2, organization: org2})
|
|
|
|
%{
|
|
org1: org1,
|
|
org2: org2,
|
|
site1: site1,
|
|
site2: site2,
|
|
device1: device1,
|
|
device2: device2
|
|
}
|
|
end
|
|
|
|
test "returns {:ok, device} when device belongs to organization", %{
|
|
device1: device,
|
|
org1: org
|
|
} do
|
|
assert {:ok, returned_device} = AccessControl.verify_device_access(device.id, org.id)
|
|
assert returned_device.id == device.id
|
|
end
|
|
|
|
test "returns {:error, :unauthorized} when device belongs to different organization", %{
|
|
device1: device,
|
|
org2: wrong_org
|
|
} do
|
|
assert {:error, :unauthorized} = AccessControl.verify_device_access(device.id, wrong_org.id)
|
|
end
|
|
|
|
test "returns {:error, :not_found} when device does not exist", %{org1: org} do
|
|
fake_id = Ecto.UUID.generate()
|
|
assert {:error, :not_found} = AccessControl.verify_device_access(fake_id, org.id)
|
|
end
|
|
end
|
|
|
|
describe "verify_site_access/2" do
|
|
setup do
|
|
user1 = user_fixture()
|
|
user2 = user_fixture()
|
|
|
|
org1 = organization_fixture(user1.id)
|
|
org2 = organization_fixture(user2.id)
|
|
|
|
site1 = site_fixture(organization_id: org1.id)
|
|
site2 = site_fixture(organization_id: org2.id)
|
|
|
|
%{
|
|
org1: org1,
|
|
org2: org2,
|
|
site1: site1,
|
|
site2: site2
|
|
}
|
|
end
|
|
|
|
test "returns {:ok, site} when site belongs to organization", %{
|
|
site1: site,
|
|
org1: org
|
|
} do
|
|
assert {:ok, returned_site} = AccessControl.verify_site_access(site.id, org.id)
|
|
assert returned_site.id == site.id
|
|
end
|
|
|
|
test "returns {:error, :unauthorized} when site belongs to different organization", %{
|
|
site1: site,
|
|
org2: wrong_org
|
|
} do
|
|
assert {:error, :unauthorized} = AccessControl.verify_site_access(site.id, wrong_org.id)
|
|
end
|
|
|
|
test "returns {:error, :not_found} when site does not exist", %{org1: org} do
|
|
fake_id = Ecto.UUID.generate()
|
|
assert {:error, :not_found} = AccessControl.verify_site_access(fake_id, org.id)
|
|
end
|
|
end
|
|
|
|
describe "verify_alert_access/2" do
|
|
setup do
|
|
user1 = user_fixture()
|
|
user2 = user_fixture()
|
|
|
|
org1 = organization_fixture(user1.id)
|
|
org2 = organization_fixture(user2.id)
|
|
|
|
site1 = site_fixture(organization_id: org1.id)
|
|
site2 = site_fixture(organization_id: org2.id)
|
|
|
|
device1 = device_fixture(%{site: site1, organization: org1})
|
|
device2 = device_fixture(%{site: site2, organization: org2})
|
|
|
|
alert1 = alert_fixture(device_id: device1.id)
|
|
alert2 = alert_fixture(device_id: device2.id)
|
|
|
|
%{
|
|
org1: org1,
|
|
org2: org2,
|
|
device1: device1,
|
|
device2: device2,
|
|
alert1: alert1,
|
|
alert2: alert2
|
|
}
|
|
end
|
|
|
|
test "returns {:ok, alert} when alert's device belongs to organization", %{
|
|
alert1: alert,
|
|
org1: org
|
|
} do
|
|
assert {:ok, returned_alert} = AccessControl.verify_alert_access(alert.id, org.id)
|
|
assert returned_alert.id == alert.id
|
|
|
|
# Verify preloading worked
|
|
assert returned_alert.device
|
|
assert returned_alert.device.site
|
|
assert returned_alert.device.site.organization
|
|
end
|
|
|
|
test "returns {:error, :unauthorized} when alert's device belongs to different organization",
|
|
%{
|
|
alert1: alert,
|
|
org2: wrong_org
|
|
} do
|
|
assert {:error, :unauthorized} = AccessControl.verify_alert_access(alert.id, wrong_org.id)
|
|
end
|
|
|
|
test "returns {:error, :not_found} when alert does not exist", %{org1: org} do
|
|
fake_id = Ecto.UUID.generate()
|
|
assert {:error, :not_found} = AccessControl.verify_alert_access(fake_id, org.id)
|
|
end
|
|
end
|
|
|
|
# Helper functions to create test data
|
|
defp site_fixture(attrs) do
|
|
{:ok, site} =
|
|
attrs
|
|
|> Enum.into(%{
|
|
name: "Test Site #{System.unique_integer([:positive])}"
|
|
})
|
|
|> Sites.create_site()
|
|
|
|
site
|
|
end
|
|
|
|
defp alert_fixture(attrs) do
|
|
{:ok, alert} =
|
|
attrs
|
|
|> Enum.into(%{
|
|
alert_type: :device_down,
|
|
message: "Test alert",
|
|
triggered_at: DateTime.utc_now()
|
|
})
|
|
|> Alerts.create_alert()
|
|
|
|
alert
|
|
end
|
|
end
|