defmodule ToweropsWeb.GraphQL.Resolvers.AuthenticatedTest do @moduledoc """ Direct invocation tests for happy and not-found paths of GraphQL resolvers. Skips the Absinthe pipeline so we can test resolver logic in isolation. """ use Towerops.DataCase, async: true import Towerops.AccountsFixtures import Towerops.AgentsFixtures import Towerops.DevicesFixtures import Towerops.OrganizationsFixtures alias Towerops.Repo alias ToweropsWeb.GraphQL.Resolvers setup do user = user_fixture() org = organization_fixture(user.id) %{user: user, organization: org, ctx: %{context: %{organization_id: org.id, user: user}}} end describe "Activity.list/3" do test "returns ok-tuple for authenticated context", %{ctx: ctx} do assert {:ok, items} = Resolvers.Activity.list(nil, %{}, ctx) assert is_list(items) end test "ignores invalid type strings", %{ctx: ctx} do assert {:ok, _items} = Resolvers.Activity.list(nil, %{type: "definitely_not_an_atom_xyz"}, ctx) end test "accepts known-existing atom type", %{ctx: ctx} do _ = :alert_created assert {:ok, _items} = Resolvers.Activity.list(nil, %{type: "alert_created"}, ctx) end end describe "Agent.list/3 and create/3 / delete/3" do test "lists agent tokens for the org", %{organization: org, ctx: ctx} do _token = agent_token_fixture(org.id, "Foo") assert {:ok, agents} = Resolvers.Agent.list(nil, %{}, ctx) assert Enum.any?(agents, &(&1.name == "Foo")) end test "creates a new agent token and returns the raw token", %{ctx: ctx} do assert {:ok, %{token: raw, name: name}} = Resolvers.Agent.create(nil, %{name: "Bar"}, ctx) assert is_binary(raw) and raw != "" assert name == "Bar" end test "returns 'Agent not found' for unknown id", %{ctx: ctx} do assert {:error, "Agent not found"} = Resolvers.Agent.get(nil, %{id: Ecto.UUID.generate()}, ctx) end test "delete on unknown id returns not-found error", %{ctx: ctx} do assert {:error, "Agent not found"} = Resolvers.Agent.delete(nil, %{id: Ecto.UUID.generate()}, ctx) end end describe "Organization.list_mine/3 and get/3 and update/3" do test "list_mine includes the user's org", %{user: user, organization: org} do ctx = %{context: %{user: user}} assert {:ok, orgs} = Resolvers.Organization.list_mine(nil, %{}, ctx) assert Enum.any?(orgs, &(&1.id == org.id)) end test "get/3 returns the org from context", %{ctx: ctx, organization: org} do assert {:ok, fetched} = Resolvers.Organization.get(nil, %{}, ctx) assert fetched.id == org.id end test "update/3 with valid input changes the name", %{ctx: ctx} do assert {:ok, updated} = Resolvers.Organization.update(nil, %{input: %{name: "Renamed Co"}}, ctx) assert updated.name == "Renamed Co" end test "update/3 with invalid input returns an error string", %{ctx: ctx} do assert {:error, msg} = Resolvers.Organization.update(nil, %{input: %{name: "x"}}, ctx) assert is_binary(msg) and byte_size(msg) > 0 end end describe "Site resolvers" do test "list/3 returns sites for org, get/3 returns one, create/3 succeeds, update/3 + delete/3 work", %{ organization: org, ctx: ctx } do device = device_fixture(%{organization_id: org.id}) site_id = device.site_id assert {:ok, sites} = Resolvers.Site.list(nil, %{}, ctx) assert Enum.any?(sites, &(&1.id == site_id)) assert {:ok, fetched} = Resolvers.Site.get(nil, %{id: site_id}, ctx) assert fetched.id == site_id assert {:ok, created} = Resolvers.Site.create(nil, %{input: %{name: "New Site"}}, ctx) assert created.name == "New Site" assert {:ok, updated} = Resolvers.Site.update(nil, %{id: created.id, input: %{name: "Renamed"}}, ctx) assert updated.name == "Renamed" assert {:ok, %{success: true}} = Resolvers.Site.delete(nil, %{id: created.id}, ctx) refute Repo.get(Towerops.Sites.Site, created.id) end test "get/3 with unknown id returns not-found", %{ctx: ctx} do assert {:error, "Site not found"} = Resolvers.Site.get(nil, %{id: Ecto.UUID.generate()}, ctx) end test "update/3 with unknown id returns not-found", %{ctx: ctx} do assert {:error, "Site not found"} = Resolvers.Site.update( nil, %{id: Ecto.UUID.generate(), input: %{name: "x"}}, ctx ) end test "delete/3 with unknown id returns not-found", %{ctx: ctx} do assert {:error, "Site not found"} = Resolvers.Site.delete(nil, %{id: Ecto.UUID.generate()}, ctx) end end describe "Integration resolvers" do test "list/3 returns []", %{ctx: ctx} do assert {:ok, []} = Resolvers.Integration.list(nil, %{}, ctx) end test "update on unknown id returns 'Integration not found'", %{ctx: ctx} do assert {:error, "Integration not found"} = Resolvers.Integration.update( nil, %{id: Ecto.UUID.generate(), input: %{name: "x"}}, ctx ) end test "delete on unknown id returns 'Integration not found'", %{ctx: ctx} do assert {:error, "Integration not found"} = Resolvers.Integration.delete(nil, %{id: Ecto.UUID.generate()}, ctx) end test "test_connection on unknown id returns 'Integration not found'", %{ctx: ctx} do assert {:error, "Integration not found"} = Resolvers.Integration.test_connection(nil, %{id: Ecto.UUID.generate()}, ctx) end end describe "MaintenanceWindow resolvers" do test "list/3 returns []", %{ctx: ctx} do assert {:ok, []} = Resolvers.MaintenanceWindow.list(nil, %{}, ctx) end test "list/3 honors filter args", %{ctx: ctx} do for filter <- ["active", "upcoming", "past", "everything"] do assert {:ok, _} = Resolvers.MaintenanceWindow.list(nil, %{filter: filter}, ctx) end end test "get/3 unknown id returns not-found", %{ctx: ctx} do assert {:error, "Maintenance window not found"} = Resolvers.MaintenanceWindow.get(nil, %{id: Ecto.UUID.generate()}, ctx) end test "update on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Maintenance window not found"} = Resolvers.MaintenanceWindow.update( nil, %{id: Ecto.UUID.generate(), input: %{}}, ctx ) end test "delete on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Maintenance window not found"} = Resolvers.MaintenanceWindow.delete(nil, %{id: Ecto.UUID.generate()}, ctx) end test "create + get + update + delete happy path", %{ctx: ctx} do starts = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.truncate(:second) ends = DateTime.add(starts, 7200, :second) assert {:ok, window} = Resolvers.MaintenanceWindow.create( nil, %{ input: %{ name: "Smoke Window", starts_at: starts, ends_at: ends, suppress_alerts: true } }, ctx ) assert {:ok, fetched} = Resolvers.MaintenanceWindow.get(nil, %{id: window.id}, ctx) assert fetched.id == window.id assert {:ok, updated} = Resolvers.MaintenanceWindow.update( nil, %{id: window.id, input: %{name: "Renamed"}}, ctx ) assert updated.name == "Renamed" assert {:ok, %{success: true}} = Resolvers.MaintenanceWindow.delete(nil, %{id: window.id}, ctx) end test "create with invalid input returns formatted error", %{ctx: ctx} do assert {:error, msg} = Resolvers.MaintenanceWindow.create(nil, %{input: %{}}, ctx) assert is_binary(msg) and byte_size(msg) > 0 end end describe "Alert resolvers — happy paths" do test "list returns inserted alerts; get/3 fetches by id", %{organization: org, ctx: ctx} do device = device_fixture(%{organization_id: org.id}) {:ok, alert} = Towerops.Alerts.create_alert(%{ device_id: device.id, alert_type: "device_down", triggered_at: DateTime.truncate(DateTime.utc_now(), :second), message: "down" }) assert {:ok, alerts} = Resolvers.Alert.list(nil, %{}, ctx) assert Enum.any?(alerts, &(&1.id == alert.id)) assert {:ok, fetched} = Resolvers.Alert.get(nil, %{id: alert.id}, ctx) assert fetched.id == alert.id end test "acknowledge transitions an alert and returns it", %{organization: org, ctx: ctx} do device = device_fixture(%{organization_id: org.id}) {:ok, alert} = Towerops.Alerts.create_alert(%{ device_id: device.id, alert_type: "device_down", triggered_at: DateTime.truncate(DateTime.utc_now(), :second) }) assert {:ok, acked} = Resolvers.Alert.acknowledge(nil, %{id: alert.id}, ctx) assert acked.id == alert.id assert acked.acknowledged_at end test "resolve_alert transitions and returns it", %{organization: org, ctx: ctx} do device = device_fixture(%{organization_id: org.id}) {:ok, alert} = Towerops.Alerts.create_alert(%{ device_id: device.id, alert_type: "device_down", triggered_at: DateTime.truncate(DateTime.utc_now(), :second) }) assert {:ok, resolved} = Resolvers.Alert.resolve_alert(nil, %{id: alert.id}, ctx) assert resolved.resolved_at end end describe "EscalationPolicy resolvers (not-found paths)" do test "get on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Escalation policy not found"} = Resolvers.EscalationPolicy.get(nil, %{id: Ecto.UUID.generate()}, ctx) end test "update on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Escalation policy not found"} = Resolvers.EscalationPolicy.update( nil, %{id: Ecto.UUID.generate(), input: %{}}, ctx ) end test "delete on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Escalation policy not found"} = Resolvers.EscalationPolicy.delete(nil, %{id: Ecto.UUID.generate()}, ctx) end test "create_rule with unknown policy returns not-found", %{ctx: ctx} do assert {:error, "Escalation policy not found"} = Resolvers.EscalationPolicy.create_rule( nil, %{escalation_policy_id: Ecto.UUID.generate(), input: %{}}, ctx ) end test "update_rule on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Rule not found"} = Resolvers.EscalationPolicy.update_rule( nil, %{id: Ecto.UUID.generate(), input: %{}}, ctx ) end test "delete_rule on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Rule not found"} = Resolvers.EscalationPolicy.delete_rule(nil, %{id: Ecto.UUID.generate()}, ctx) end test "delete_target on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Target not found"} = Resolvers.EscalationPolicy.delete_target(nil, %{id: Ecto.UUID.generate()}, ctx) end end describe "Schedule resolvers (not-found paths)" do test "get on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Schedule not found"} = Resolvers.Schedule.get(nil, %{id: Ecto.UUID.generate()}, ctx) end test "on_call with unknown id returns not-found", %{ctx: ctx} do assert {:error, "Schedule not found"} = Resolvers.Schedule.on_call(nil, %{schedule_id: Ecto.UUID.generate()}, ctx) end test "update on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Schedule not found"} = Resolvers.Schedule.update( nil, %{id: Ecto.UUID.generate(), input: %{}}, ctx ) end test "delete on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Schedule not found"} = Resolvers.Schedule.delete(nil, %{id: Ecto.UUID.generate()}, ctx) end test "create_layer with unknown schedule returns not-found", %{ctx: ctx} do assert {:error, "Schedule not found"} = Resolvers.Schedule.create_layer( nil, %{schedule_id: Ecto.UUID.generate(), input: %{}}, ctx ) end test "delete_layer on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Layer not found"} = Resolvers.Schedule.delete_layer(nil, %{id: Ecto.UUID.generate()}, ctx) end test "remove_member on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Member not found"} = Resolvers.Schedule.remove_member(nil, %{id: Ecto.UUID.generate()}, ctx) end test "create_override with unknown schedule returns not-found", %{ctx: ctx} do assert {:error, "Schedule not found"} = Resolvers.Schedule.create_override( nil, %{schedule_id: Ecto.UUID.generate(), input: %{}}, ctx ) end test "delete_override on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Override not found"} = Resolvers.Schedule.delete_override(nil, %{id: Ecto.UUID.generate()}, ctx) end end describe "Member resolvers (error paths)" do test "remove on unknown user returns 'Member not found'", %{ctx: ctx} do assert {:error, "Member not found"} = Resolvers.Member.remove(nil, %{id: Ecto.UUID.generate()}, ctx) end test "update_role on unknown user returns 'Member not found'", %{ctx: ctx} do assert {:error, "Member not found"} = Resolvers.Member.update_role( nil, %{id: Ecto.UUID.generate(), role: "technician"}, ctx ) end test "cancel_invitation on unknown id returns 'Invitation not found'", %{ctx: ctx} do assert {:error, "Invitation not found"} = Resolvers.Member.cancel_invitation(nil, %{id: Ecto.UUID.generate()}, ctx) end test "list/3 returns owner as a member", %{ctx: ctx, user: user} do assert {:ok, members} = Resolvers.Member.list(nil, %{}, ctx) assert Enum.any?(members, &(&1.user_id == user.id)) end end describe "Device resolvers" do test "list/3 with no devices returns []", %{ctx: ctx} do assert {:ok, []} = Resolvers.Device.list(nil, %{}, ctx) end test "get/3 returns the device", %{organization: org, ctx: ctx} do d = device_fixture(%{organization_id: org.id}) assert {:ok, found} = Resolvers.Device.get(nil, %{id: d.id}, ctx) assert found.id == d.id end test "get on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Device not found"} = Resolvers.Device.get(nil, %{id: Ecto.UUID.generate()}, ctx) end test "delete on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Device not found"} = Resolvers.Device.delete(nil, %{id: Ecto.UUID.generate()}, ctx) end test "update on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Device not found"} = Resolvers.Device.update( nil, %{id: Ecto.UUID.generate(), input: %{}}, ctx ) end test "metrics on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Device not found"} = Resolvers.Device.metrics(nil, %{device_id: Ecto.UUID.generate()}, ctx) end test "interfaces on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Device not found"} = Resolvers.Device.interfaces(nil, %{device_id: Ecto.UUID.generate()}, ctx) end test "interfaces returns [] when device has no snmp_device", %{organization: org, ctx: ctx} do d = device_fixture(%{organization_id: org.id}) assert {:ok, []} = Resolvers.Device.interfaces(nil, %{device_id: d.id}, ctx) end test "metrics returns [] when device has no checks", %{organization: org, ctx: ctx} do d = device_fixture(%{organization_id: org.id}) assert {:ok, []} = Resolvers.Device.metrics( nil, %{device_id: d.id, time_range: "12h", sensor_type: nil}, ctx ) end test "metrics with malformed time_range falls back to 24h", %{organization: org, ctx: ctx} do d = device_fixture(%{organization_id: org.id}) assert {:ok, []} = Resolvers.Device.metrics( nil, %{device_id: d.id, time_range: "garbage"}, ctx ) end end describe "Alert resolvers" do test "list/3 returns []", %{ctx: ctx} do assert {:ok, []} = Resolvers.Alert.list(nil, %{}, ctx) end test "get on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Alert not found"} = Resolvers.Alert.get(nil, %{id: Ecto.UUID.generate()}, ctx) end test "acknowledge on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Alert not found"} = Resolvers.Alert.acknowledge(nil, %{id: Ecto.UUID.generate()}, ctx) end test "resolve_alert on unknown id returns not-found", %{ctx: ctx} do assert {:error, "Alert not found"} = Resolvers.Alert.resolve_alert(nil, %{id: Ecto.UUID.generate()}, ctx) end end end