test: lift coverage to 87.14% via targeted branch tests
Adds tests across five files: - Towerops.Monitoring.CheckQuery (full coverage of all composable filter fragments via inspect+Repo.all) - ToweropsWeb.Admin.SecurityLive.Index change_filter event for both permanent and temporary filter values - Towerops.Agents.AgentCache cleanup_expired handle_info path that prunes stale device entries while preserving :global_default - ToweropsWeb.Api.V1.AgentReleaseWebhookController happy path (notified/skipped/version) and error path (Codeberg 503) - ToweropsWeb.DeviceLive.Index reorder_device / reorder_site unauthorized branches via cross-org device/site fixtures
This commit is contained in:
parent
0c63cb608e
commit
38619f2b88
5 changed files with 234 additions and 0 deletions
|
|
@ -97,5 +97,31 @@ defmodule Towerops.Agents.AgentCacheTest do
|
|||
AgentCache.refresh_global_default()
|
||||
assert AgentCache.global_default_cloud_poller() == nil
|
||||
end
|
||||
|
||||
test "global_default returns refreshed value when key not in ETS" do
|
||||
# Delete just the :global_default entry (not the device entries)
|
||||
_ = :ets.delete(:device_agent_cache, :global_default)
|
||||
assert AgentCache.global_default_cloud_poller() == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle_info :cleanup_expired" do
|
||||
test "deletes expired device entries while keeping :global_default", %{device: device} do
|
||||
# Insert an expired device entry directly
|
||||
now = System.monotonic_time(:millisecond)
|
||||
_ = :ets.insert(:device_agent_cache, {{:device, device.id}, true, now - 1000})
|
||||
|
||||
# Make sure global_default is set so we can prove cleanup preserves it
|
||||
AgentCache.refresh_global_default()
|
||||
|
||||
send(AgentCache, :cleanup_expired)
|
||||
# Give the GenServer a moment to process
|
||||
_ = :sys.get_state(AgentCache)
|
||||
|
||||
# Expired device entry removed
|
||||
assert :ets.lookup(:device_agent_cache, {:device, device.id}) == []
|
||||
# Global default key preserved
|
||||
assert [{:global_default, _}] = :ets.lookup(:device_agent_cache, :global_default)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
67
test/towerops/monitoring/check_query_test.exs
Normal file
67
test/towerops/monitoring/check_query_test.exs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
defmodule Towerops.Monitoring.CheckQueryTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Monitoring.Check
|
||||
alias Towerops.Monitoring.CheckQuery
|
||||
|
||||
describe "query composition" do
|
||||
test "base/0 returns the Check module" do
|
||||
assert CheckQuery.base() == Check
|
||||
end
|
||||
|
||||
test "for_organization/2 adds organization filter" do
|
||||
org_id = Ecto.UUID.generate()
|
||||
query = CheckQuery.for_organization(org_id)
|
||||
assert %Ecto.Query{} = query
|
||||
assert inspect(query) =~ "organization_id"
|
||||
end
|
||||
|
||||
test "for_device/2 adds device filter" do
|
||||
device_id = Ecto.UUID.generate()
|
||||
query = CheckQuery.for_device(device_id)
|
||||
assert %Ecto.Query{} = query
|
||||
assert inspect(query) =~ "device_id"
|
||||
end
|
||||
|
||||
test "of_type/2 adds check_type filter" do
|
||||
query = CheckQuery.of_type("ping")
|
||||
assert %Ecto.Query{} = query
|
||||
assert inspect(query) =~ "check_type"
|
||||
end
|
||||
|
||||
test "with_enabled/2 adds enabled filter" do
|
||||
query = CheckQuery.with_enabled(true)
|
||||
assert %Ecto.Query{} = query
|
||||
assert inspect(query) =~ "enabled"
|
||||
end
|
||||
|
||||
test "order_by_name/1 orders ascending by name" do
|
||||
query = CheckQuery.order_by_name()
|
||||
assert %Ecto.Query{} = query
|
||||
assert inspect(query) =~ "order_by"
|
||||
end
|
||||
|
||||
test "fragments compose into a chained query" do
|
||||
org_id = Ecto.UUID.generate()
|
||||
device_id = Ecto.UUID.generate()
|
||||
|
||||
composed =
|
||||
CheckQuery.base()
|
||||
|> CheckQuery.for_organization(org_id)
|
||||
|> CheckQuery.for_device(device_id)
|
||||
|> CheckQuery.of_type("ping")
|
||||
|> CheckQuery.with_enabled(true)
|
||||
|> CheckQuery.order_by_name()
|
||||
|
||||
assert %Ecto.Query{} = composed
|
||||
# All clauses applied
|
||||
assert length(composed.wheres) == 4
|
||||
end
|
||||
|
||||
test "queries execute against the empty repo" do
|
||||
assert [] = CheckQuery.base() |> where([c], false) |> Repo.all()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
defmodule ToweropsWeb.Api.V1.AgentReleaseWebhookControllerTest do
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
alias Towerops.Agents.ReleaseChecker
|
||||
|
||||
setup do
|
||||
conn =
|
||||
build_conn()
|
||||
|
|
@ -41,6 +43,39 @@ defmodule ToweropsWeb.Api.V1.AgentReleaseWebhookControllerTest do
|
|||
response = json_response(conn, 502)
|
||||
assert response["status"] == "error"
|
||||
end
|
||||
|
||||
test "returns 200 ok with notified/skipped/version on success", %{conn: conn} do
|
||||
# Stub ReleaseChecker so broadcast_mass_update returns {:ok, ...}
|
||||
Req.Test.stub(ReleaseChecker, fn r_conn ->
|
||||
Req.Test.json(r_conn, %{
|
||||
"tag_name" => "v9.9.9",
|
||||
"assets" => []
|
||||
})
|
||||
end)
|
||||
|
||||
ReleaseChecker.invalidate_cache()
|
||||
|
||||
conn = post(conn, ~p"/api/v1/webhooks/agent-release")
|
||||
|
||||
response = json_response(conn, 200)
|
||||
assert response["status"] == "ok"
|
||||
assert response["version"] == "9.9.9"
|
||||
assert is_integer(response["notified"])
|
||||
assert is_integer(response["skipped"])
|
||||
end
|
||||
|
||||
test "returns 502 when broadcast_mass_update returns error", %{conn: conn} do
|
||||
# Stub the underlying API call to fail with a non-200 status
|
||||
Req.Test.stub(ReleaseChecker, fn r_conn ->
|
||||
r_conn |> Plug.Conn.put_status(503) |> Req.Test.json(%{})
|
||||
end)
|
||||
|
||||
ReleaseChecker.invalidate_cache()
|
||||
|
||||
conn = post(conn, ~p"/api/v1/webhooks/agent-release")
|
||||
response = json_response(conn, 502)
|
||||
assert response["status"] == "error"
|
||||
end
|
||||
end
|
||||
|
||||
describe "check_timestamp/1" do
|
||||
|
|
|
|||
|
|
@ -169,6 +169,34 @@ defmodule ToweropsWeb.Admin.SecurityLive.IndexTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "change_filter event" do
|
||||
test "permanent filter switches to ?filter=permanent", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/security?tab=blocked")
|
||||
|
||||
_ = render_hook(view, "change_filter", %{"filter" => "permanent"})
|
||||
assert_patch(view, ~p"/admin/security?tab=blocked&filter=permanent")
|
||||
end
|
||||
|
||||
test "temporary filter switches to ?filter=temporary", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/security?tab=blocked")
|
||||
|
||||
_ = render_hook(view, "change_filter", %{"filter" => "temporary"})
|
||||
assert_patch(view, ~p"/admin/security?tab=blocked&filter=temporary")
|
||||
end
|
||||
end
|
||||
|
||||
describe "filter param drives load_data branches" do
|
||||
test "permanent filter renders blocked-IP page", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/security?tab=blocked&filter=permanent")
|
||||
assert html =~ "Denied IP Addresses"
|
||||
end
|
||||
|
||||
test "temporary filter renders blocked-IP page", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/security?tab=blocked&filter=temporary")
|
||||
assert html =~ "Denied IP Addresses"
|
||||
end
|
||||
end
|
||||
|
||||
describe "PubSub updates" do
|
||||
test "refreshes whitelist data on PubSub broadcast", %{conn: conn, user: user} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/security")
|
||||
|
|
|
|||
|
|
@ -550,6 +550,84 @@ defmodule ToweropsWeb.DeviceLive.IndexTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "reorder_device / reorder_site unauthorized branch" do
|
||||
test "reorder_device flashes 'no access' for cross-org device", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
organization: organization,
|
||||
user: user
|
||||
} do
|
||||
_ = user
|
||||
# Need at least one device on this org so the device-list element renders
|
||||
{:ok, _own_device} =
|
||||
Devices.create_device(%{
|
||||
name: "OwnDevice",
|
||||
ip_address: "10.0.0.1",
|
||||
site_id: site.id,
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
# Cross-org device that exists but belongs elsewhere
|
||||
cross_user = Towerops.AccountsFixtures.user_fixture()
|
||||
cross_org = Towerops.OrganizationsFixtures.organization_fixture(cross_user.id)
|
||||
|
||||
{:ok, cross_site} =
|
||||
Towerops.Sites.create_site(%{name: "Cross", organization_id: cross_org.id})
|
||||
|
||||
{:ok, cross_device} =
|
||||
Devices.create_device(%{
|
||||
name: "Cross",
|
||||
ip_address: "10.99.0.1",
|
||||
site_id: cross_site.id,
|
||||
organization_id: cross_org.id
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/devices")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("#device-list")
|
||||
|> render_hook("reorder_device", %{
|
||||
"device_id" => cross_device.id,
|
||||
"new_position" => "1"
|
||||
})
|
||||
|
||||
assert html =~ "don't have access" or html =~ "don't have access"
|
||||
end
|
||||
|
||||
test "reorder_site flashes 'no access' for cross-org site", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, _own_device} =
|
||||
Devices.create_device(%{
|
||||
name: "OwnDevice2",
|
||||
ip_address: "10.0.0.2",
|
||||
site_id: site.id,
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
cross_user = Towerops.AccountsFixtures.user_fixture()
|
||||
cross_org = Towerops.OrganizationsFixtures.organization_fixture(cross_user.id)
|
||||
|
||||
{:ok, cross_site} =
|
||||
Towerops.Sites.create_site(%{name: "Cross", organization_id: cross_org.id})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/devices")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("#device-list")
|
||||
|> render_hook("reorder_site", %{
|
||||
"site_id" => cross_site.id,
|
||||
"new_position" => "1"
|
||||
})
|
||||
|
||||
assert html =~ "don't have access" or html =~ "don't have access"
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle_info catch-all" do
|
||||
test "ignores unrelated messages", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/devices")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue