From 91b0cac1645022601d878ba76ac399a9311b92e2 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 8 May 2026 13:00:56 -0500 Subject: [PATCH] test: GraphQL.Resolvers.EscalationPolicy + MobileController list_alerts formatting --- .../api/mobile_controller_test.exs | 34 +++- .../resolvers/escalation_policy_test.exs | 182 ++++++++++++++++++ 2 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 test/towerops_web/graphql/resolvers/escalation_policy_test.exs diff --git a/test/towerops_web/controllers/api/mobile_controller_test.exs b/test/towerops_web/controllers/api/mobile_controller_test.exs index dfbbcb9d..7f91066f 100644 --- a/test/towerops_web/controllers/api/mobile_controller_test.exs +++ b/test/towerops_web/controllers/api/mobile_controller_test.exs @@ -5,6 +5,7 @@ defmodule ToweropsWeb.Api.MobileControllerTest do alias Towerops.MobileSessions alias Towerops.Organizations + alias Towerops.Snmp.Device setup do user = user_fixture() @@ -188,7 +189,7 @@ defmodule ToweropsWeb.Api.MobileControllerTest do }) {:ok, _snmp} = - Towerops.Repo.insert(%Towerops.Snmp.Device{ + Towerops.Repo.insert(%Device{ device_id: device.id, sys_uptime: (5 * 3600 + 30 * 60) * 100, sys_descr: "test" @@ -208,4 +209,35 @@ defmodule ToweropsWeb.Api.MobileControllerTest do assert body["uptime"] =~ "h" end end + + describe "list_alerts — formatting" do + test "returns formatted alert with status mapping", %{conn: conn, organization: org} do + {:ok, site} = Towerops.Sites.create_site(%{name: "Alert Site", organization_id: org.id}) + + {:ok, device} = + Towerops.Devices.create_device(%{ + name: "Alert-Dev", + ip_address: "10.20.30.40", + site_id: site.id, + organization_id: org.id + }) + + {:ok, _alert} = + Towerops.Alerts.create_alert(%{ + device_id: device.id, + alert_type: "device_down", + triggered_at: ~U[2026-01-01 00:00:00Z], + message: "down" + }) + + response = get(conn, ~p"/api/v1/mobile/organizations/#{org.id}/alerts") + %{"alerts" => alerts} = json_response(response, 200) + + [a] = alerts + assert a["status"] == "active" + assert a["message"] == "down" + assert a["equipment_name"] == "Alert-Dev" + assert a["device_id"] == device.id + end + end end diff --git a/test/towerops_web/graphql/resolvers/escalation_policy_test.exs b/test/towerops_web/graphql/resolvers/escalation_policy_test.exs new file mode 100644 index 00000000..262cf3a0 --- /dev/null +++ b/test/towerops_web/graphql/resolvers/escalation_policy_test.exs @@ -0,0 +1,182 @@ +defmodule ToweropsWeb.GraphQL.Resolvers.EscalationPolicyTest do + use Towerops.DataCase, async: true + + import Towerops.AccountsFixtures + import Towerops.OnCallFixtures + import Towerops.OrganizationsFixtures + + alias ToweropsWeb.GraphQL.Resolvers.EscalationPolicy, as: Resolver + + setup do + user = user_fixture() + org = organization_fixture(user.id) + other_user = user_fixture() + other_org = organization_fixture(other_user.id) + + %{user: user, org: org, other_org: other_org, ctx: %{organization_id: org.id}} + end + + describe "list/3" do + test "returns policies for the org", %{org: org, ctx: ctx} do + _p = escalation_policy_fixture(org.id, %{name: "P1"}) + assert {:ok, [policy]} = Resolver.list(nil, %{}, %{context: ctx}) + assert policy.name == "P1" + end + + test "without org context returns auth error" do + assert {:error, _} = Resolver.list(nil, %{}, %{}) + end + end + + describe "get/3" do + test "returns the policy preloaded with rules + targets", %{org: org, ctx: ctx} do + policy = escalation_policy_fixture(org.id) + assert {:ok, found} = Resolver.get(nil, %{id: policy.id}, %{context: ctx}) + assert found.id == policy.id + end + + test "returns error for cross-org id", %{other_org: other_org, ctx: ctx} do + foreign = escalation_policy_fixture(other_org.id) + + assert {:error, "Escalation policy not found"} = + Resolver.get(nil, %{id: foreign.id}, %{context: ctx}) + end + + test "without org context returns auth error" do + assert {:error, _} = Resolver.get(nil, %{id: Ecto.UUID.generate()}, %{}) + end + end + + describe "create/3" do + test "creates a policy under the resolved org", %{ctx: ctx} do + assert {:ok, policy} = + Resolver.create(nil, %{input: %{name: "Created"}}, %{context: ctx}) + + assert policy.name == "Created" + assert policy.organization_id == ctx.organization_id + end + + test "returns formatted errors on invalid input", %{ctx: ctx} do + assert {:error, errors} = Resolver.create(nil, %{input: %{name: ""}}, %{context: ctx}) + assert is_list(errors) or is_binary(errors) or is_map(errors) + end + + test "without org context returns auth error" do + assert {:error, _} = Resolver.create(nil, %{input: %{name: "x"}}, %{}) + end + end + + describe "update/3 and delete/3" do + test "update + delete a policy round-trip", %{org: org, ctx: ctx} do + policy = escalation_policy_fixture(org.id, %{name: "Original"}) + + assert {:ok, updated} = + Resolver.update(nil, %{id: policy.id, input: %{name: "Renamed"}}, %{context: ctx}) + + assert updated.name == "Renamed" + + assert {:ok, %{success: true}} = Resolver.delete(nil, %{id: policy.id}, %{context: ctx}) + end + + test "update without org context returns auth error" do + assert {:error, _} = + Resolver.update(nil, %{id: Ecto.UUID.generate(), input: %{name: "x"}}, %{}) + end + + test "delete without org context returns auth error" do + assert {:error, _} = Resolver.delete(nil, %{id: Ecto.UUID.generate()}, %{}) + end + + test "delete returns 'not found' for cross-org id", + %{other_org: other_org, ctx: ctx} do + foreign = escalation_policy_fixture(other_org.id) + assert {:error, _} = Resolver.delete(nil, %{id: foreign.id}, %{context: ctx}) + end + end + + describe "rule + target mutations" do + setup %{org: org} do + policy = escalation_policy_fixture(org.id) + rule = escalation_rule_fixture(policy.id, %{position: 0, timeout_minutes: 5}) + %{policy: policy, rule: rule} + end + + test "create_rule attaches a rule to the policy", + %{policy: policy, ctx: ctx} do + assert {:ok, _rule} = + Resolver.create_rule( + nil, + %{ + escalation_policy_id: policy.id, + input: %{position: 1, timeout_minutes: 5} + }, + %{context: ctx} + ) + end + + test "create_rule without context returns auth error" do + assert {:error, _} = + Resolver.create_rule( + nil, + %{escalation_policy_id: Ecto.UUID.generate(), input: %{}}, + %{} + ) + end + + test "update_rule and delete_rule round-trip", + %{rule: rule, ctx: ctx} do + assert {:ok, _} = + Resolver.update_rule(nil, %{id: rule.id, input: %{timeout_minutes: 10}}, %{ + context: ctx + }) + + assert {:ok, %{success: true}} = Resolver.delete_rule(nil, %{id: rule.id}, %{context: ctx}) + end + + test "update_rule + delete_rule auth errors without context" do + assert {:error, _} = + Resolver.update_rule(nil, %{id: Ecto.UUID.generate(), input: %{}}, %{}) + + assert {:error, _} = Resolver.delete_rule(nil, %{id: Ecto.UUID.generate()}, %{}) + end + + test "create_target attaches a target to a rule", + %{rule: rule, user: user, ctx: ctx} do + assert {:ok, _target} = + Resolver.create_target( + nil, + %{ + escalation_rule_id: rule.id, + input: %{target_type: "user", user_id: user.id} + }, + %{context: ctx} + ) + end + + test "create_target auth error without context" do + assert {:error, _} = + Resolver.create_target( + nil, + %{escalation_rule_id: Ecto.UUID.generate(), input: %{}}, + %{} + ) + end + + test "delete_target removes the target", + %{rule: rule, user: user, ctx: ctx} do + target = escalation_target_fixture(rule.id, %{target_type: "user", user_id: user.id}) + + assert {:ok, %{success: true}} = + Resolver.delete_target(nil, %{id: target.id}, %{context: ctx}) + end + + test "delete_target on missing id returns 'not found'", %{ctx: ctx} do + assert {:error, _} = + Resolver.delete_target(nil, %{id: Ecto.UUID.generate()}, %{context: ctx}) + end + + test "delete_target auth error without context" do + assert {:error, _} = Resolver.delete_target(nil, %{id: Ecto.UUID.generate()}, %{}) + end + end +end