defmodule ToweropsWeb.GraphQL.Resolvers.HappyPathTest do @moduledoc """ Authenticated happy-path coverage for the lower-coverage resolvers (Member, Integration, Schedule, EscalationPolicy). """ use Towerops.DataCase, async: false import Towerops.AccountsFixtures import Towerops.OrganizationsFixtures alias ToweropsWeb.GraphQL.Resolvers setup do user = user_fixture() org = organization_fixture(user.id) %{user: user, org: org, ctx: %{context: %{organization_id: org.id, user: user}}} end describe "Member" do test "invite/3 creates an invitation", %{ctx: ctx} do assert {:ok, invitation} = Resolvers.Member.invite(nil, %{email: "x@y.z", role: "technician"}, ctx) assert invitation.email == "x@y.z" end test "invite/3 with bad email returns formatted error", %{ctx: ctx} do assert {:error, msg} = Resolvers.Member.invite(nil, %{email: "not-an-email"}, ctx) assert is_binary(msg) end test "cancel_invitation/3 removes a pending invitation", %{ctx: ctx, org: org, user: user} do {:ok, invitation} = Towerops.Organizations.create_invitation(%{ email: "z@y.x", role: "technician", organization_id: org.id, invited_by_id: user.id }) assert {:ok, %{success: true}} = Resolvers.Member.cancel_invitation(nil, %{id: invitation.id}, ctx) end test "remove/3 returns 'Cannot remove owner' when removing the org owner", %{ ctx: ctx, user: user } do assert {:error, msg} = Resolvers.Member.remove(nil, %{id: user.id}, ctx) assert msg in ["Cannot remove owner", "Member not found"] end test "update_role/3 with unknown user returns not-found", %{ctx: ctx} do assert {:error, "Member not found"} = Resolvers.Member.update_role( nil, %{id: Ecto.UUID.generate(), role: "technician"}, ctx ) end test "remove/3 succeeds for a non-owner member", %{ctx: ctx, org: org} do member = user_fixture() {:ok, _membership} = Towerops.Organizations.create_membership(%{ organization_id: org.id, user_id: member.id, role: "technician" }) assert {:ok, %{success: true, message: "Member removed"}} = Resolvers.Member.remove(nil, %{id: member.id}, ctx) end test "update_role/3 succeeds for a non-owner member", %{ctx: ctx, org: org} do member = user_fixture() {:ok, _membership} = Towerops.Organizations.create_membership(%{ organization_id: org.id, user_id: member.id, role: "technician" }) assert {:ok, membership} = Resolvers.Member.update_role(nil, %{id: member.id, role: "admin"}, ctx) assert membership.user_id == member.id assert membership.role == :admin end test "update_role/3 with an invalid role returns formatted changeset error", %{ctx: ctx, org: org} do member = user_fixture() {:ok, _membership} = Towerops.Organizations.create_membership(%{ organization_id: org.id, user_id: member.id, role: "technician" }) assert {:error, msg} = Resolvers.Member.update_role(nil, %{id: member.id, role: "spaceship-captain"}, ctx) assert is_binary(msg) assert msg =~ "role" end test "update_role/3 cannot change owner role", %{ctx: ctx, user: user} do assert {:error, "Cannot change owner role"} = Resolvers.Member.update_role(nil, %{id: user.id, role: "technician"}, ctx) end end describe "Integration" do test "create/3 inserts the integration", %{ctx: ctx} do input = %{provider: "sonar", enabled: true, credentials: %{"api_key" => "k"}} assert {:ok, integration} = Resolvers.Integration.create(nil, %{input: input}, ctx) assert integration.provider == "sonar" end test "create/3 with invalid input returns a formatted error", %{ctx: ctx} do assert {:error, msg} = Resolvers.Integration.create(nil, %{input: %{}}, ctx) assert is_binary(msg) end test "test_connection/3 happy path returns success", %{ctx: ctx} do {:ok, integration} = Resolvers.Integration.create( nil, %{input: %{provider: "splynx", enabled: true, credentials: %{"api_key" => "k"}}}, ctx ) assert {:ok, %{success: true}} = Resolvers.Integration.test_connection(nil, %{id: integration.id}, ctx) end test "update/3 happy path", %{ctx: ctx} do {:ok, integration} = Resolvers.Integration.create( nil, %{input: %{provider: "splynx", enabled: true, credentials: %{"api_key" => "k"}}}, ctx ) assert {:ok, updated} = Resolvers.Integration.update( nil, %{id: integration.id, input: %{enabled: false}}, ctx ) assert updated.enabled == false end test "delete/3 happy path returns success", %{ctx: ctx} do {:ok, integration} = Resolvers.Integration.create( nil, %{input: %{provider: "uisp", enabled: true, credentials: %{"api_key" => "k"}}}, ctx ) assert {:ok, %{success: true}} = Resolvers.Integration.delete(nil, %{id: integration.id}, ctx) end end describe "Schedule (happy paths)" do test "create + list + get + update + delete schedule", %{ctx: ctx} do assert {:ok, schedule} = Resolvers.Schedule.create( nil, %{input: %{name: "On call A", timezone: "America/Chicago"}}, ctx ) assert {:ok, schedules} = Resolvers.Schedule.list(nil, %{}, ctx) assert Enum.any?(schedules, &(&1.id == schedule.id)) assert {:ok, fetched} = Resolvers.Schedule.get(nil, %{id: schedule.id}, ctx) assert fetched.id == schedule.id assert {:ok, updated} = Resolvers.Schedule.update( nil, %{id: schedule.id, input: %{name: "Renamed"}}, ctx ) assert updated.name == "Renamed" assert {:ok, %{success: true}} = Resolvers.Schedule.delete(nil, %{id: schedule.id}, ctx) end test "create_layer + add_member + delete_layer happy paths", %{ctx: ctx, user: user} do {:ok, schedule} = Resolvers.Schedule.create( nil, %{input: %{name: "Layered", timezone: "UTC"}}, ctx ) now = DateTime.truncate(DateTime.utc_now(), :second) assert {:ok, layer} = Resolvers.Schedule.create_layer( nil, %{ schedule_id: schedule.id, input: %{ name: "L1", position: 0, rotation_type: "weekly", rotation_interval: 1, handoff_time: ~T[09:00:00], start_date: now } }, ctx ) assert {:ok, member} = Resolvers.Schedule.add_member( nil, %{layer_id: layer.id, user_id: user.id, position: 0}, ctx ) assert {:ok, %{success: true}} = Resolvers.Schedule.remove_member(nil, %{id: member.id}, ctx) assert {:ok, %{success: true}} = Resolvers.Schedule.delete_layer(nil, %{id: layer.id}, ctx) end test "create_layer with unknown schedule errors", %{ctx: ctx} do assert {:error, "Schedule not found"} = Resolvers.Schedule.create_layer( nil, %{schedule_id: Ecto.UUID.generate(), input: %{name: "x"}}, ctx ) end test "update_layer/delete_layer with unknown id errors", %{ctx: ctx} do assert {:error, "Layer not found"} = Resolvers.Schedule.update_layer( nil, %{id: Ecto.UUID.generate(), input: %{name: "x"}}, ctx ) assert {:error, "Layer not found"} = Resolvers.Schedule.delete_layer(nil, %{id: Ecto.UUID.generate()}, ctx) end test "create_override happy + delete_override unknown errors", %{ctx: ctx, user: user} do {:ok, schedule} = Resolvers.Schedule.create( nil, %{input: %{name: "Override Me", timezone: "UTC"}}, ctx ) starts = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.truncate(:second) ends = DateTime.add(starts, 7200, :second) assert {:ok, override} = Resolvers.Schedule.create_override( nil, %{ schedule_id: schedule.id, input: %{user_id: user.id, start_time: starts, end_time: ends} }, ctx ) assert {:ok, %{success: true}} = Resolvers.Schedule.delete_override(nil, %{id: override.id}, ctx) assert {:error, "Override not found"} = Resolvers.Schedule.delete_override(nil, %{id: Ecto.UUID.generate()}, ctx) end test "on_call returns nil when no one is on call", %{ctx: ctx} do {:ok, schedule} = Resolvers.Schedule.create( nil, %{input: %{name: "Empty", timezone: "UTC"}}, ctx ) assert {:ok, _} = Resolvers.Schedule.on_call(nil, %{schedule_id: schedule.id}, ctx) end end describe "EscalationPolicy (happy paths)" do test "create + list + get + update + delete policy", %{ctx: ctx} do assert {:ok, policy} = Resolvers.EscalationPolicy.create( nil, %{input: %{name: "P0"}}, ctx ) assert {:ok, policies} = Resolvers.EscalationPolicy.list(nil, %{}, ctx) assert Enum.any?(policies, &(&1.id == policy.id)) assert {:ok, fetched} = Resolvers.EscalationPolicy.get(nil, %{id: policy.id}, ctx) assert fetched.id == policy.id assert {:ok, updated} = Resolvers.EscalationPolicy.update( nil, %{id: policy.id, input: %{name: "P0-renamed"}}, ctx ) assert updated.name == "P0-renamed" assert {:ok, %{success: true}} = Resolvers.EscalationPolicy.delete(nil, %{id: policy.id}, ctx) end test "create_rule + delete_rule happy paths", %{ctx: ctx} do {:ok, policy} = Resolvers.EscalationPolicy.create( nil, %{input: %{name: "P-Rules"}}, ctx ) assert {:ok, rule} = Resolvers.EscalationPolicy.create_rule( nil, %{escalation_policy_id: policy.id, input: %{position: 0, delay_minutes: 5}}, ctx ) assert {:ok, %{success: true}} = Resolvers.EscalationPolicy.delete_rule(nil, %{id: rule.id}, ctx) end test "update_rule on unknown id errors", %{ctx: ctx} do assert {:error, "Rule not found"} = Resolvers.EscalationPolicy.update_rule( nil, %{id: Ecto.UUID.generate(), input: %{}}, ctx ) end test "create_target on unknown rule errors", %{ctx: ctx} do assert {:error, "Rule not found"} = Resolvers.EscalationPolicy.create_target( nil, %{escalation_rule_id: Ecto.UUID.generate(), input: %{}}, ctx ) end test "delete_target on unknown id returns 'Target not found'", %{ctx: ctx} do assert {:error, "Target not found"} = Resolvers.EscalationPolicy.delete_target(nil, %{id: Ecto.UUID.generate()}, ctx) end end describe "Device (happy paths + lookups)" do test "create + update + delete + metrics + interfaces", %{ctx: ctx, org: org} do {:ok, site} = Towerops.Sites.create_site(%{name: "Dev Site", organization_id: org.id}) assert {:ok, device} = Resolvers.Device.create( nil, %{ input: %{ name: "Resolver Device", ip_address: "10.10.0.1", site_id: site.id } }, ctx ) assert device.name == "Resolver Device" assert {:ok, updated} = Resolvers.Device.update( nil, %{id: device.id, input: %{name: "Renamed"}}, ctx ) assert updated.name == "Renamed" assert {:ok, _metrics} = Resolvers.Device.metrics(nil, %{device_id: device.id}, ctx) assert {:ok, []} = Resolvers.Device.interfaces(nil, %{device_id: device.id}, ctx) assert {:ok, %{success: true}} = Resolvers.Device.delete(nil, %{id: device.id}, ctx) end test "create with bad input returns formatted error", %{ctx: ctx} do assert {:error, _msg} = Resolvers.Device.create(nil, %{input: %{name: ""}}, ctx) end test "update with unknown id errors", %{ctx: ctx} do assert {:error, "Device not found"} = Resolvers.Device.update( nil, %{id: Ecto.UUID.generate(), input: %{name: "x"}}, ctx ) end test "delete with unknown id errors", %{ctx: ctx} do assert {:error, "Device not found"} = Resolvers.Device.delete(nil, %{id: Ecto.UUID.generate()}, ctx) end test "metrics on unknown device errors", %{ctx: ctx} do assert {:error, "Device not found"} = Resolvers.Device.metrics(nil, %{device_id: Ecto.UUID.generate()}, ctx) end test "interfaces on unknown device errors", %{ctx: ctx} do assert {:error, "Device not found"} = Resolvers.Device.interfaces(nil, %{device_id: Ecto.UUID.generate()}, ctx) end test "list/3 returns devices for the org", %{ctx: ctx, org: org} do {:ok, site} = Towerops.Sites.create_site(%{name: "Listing", organization_id: org.id}) {:ok, _device} = Towerops.Devices.create_device(%{ name: "Listed", ip_address: "10.10.0.2", site_id: site.id, organization_id: org.id }) assert {:ok, devices} = Resolvers.Device.list(nil, %{}, ctx) assert Enum.any?(devices, &(&1.name == "Listed")) end test "list/3 without context returns auth error", %{} do assert {:error, _} = Resolvers.Device.list(nil, %{}, %{context: %{}}) end test "get/3 without context returns auth error", %{} do assert {:error, _} = Resolvers.Device.get(nil, %{id: Ecto.UUID.generate()}, %{context: %{}}) end test "create/3 without context returns auth error", %{} do assert {:error, _} = Resolvers.Device.create(nil, %{input: %{}}, %{context: %{}}) end end end