defmodule ToweropsWeb.GraphQL.Resolvers.DeviceTest do use Towerops.DataCase, async: true import Towerops.AccountsFixtures import Towerops.DevicesFixtures import Towerops.OrganizationsFixtures alias ToweropsWeb.GraphQL.Resolvers.Device, as: Resolver setup do user = user_fixture() org = organization_fixture(user.id) other_user = user_fixture() other_org = organization_fixture(other_user.id) {:ok, site} = Towerops.Sites.create_site(%{name: "S1", organization_id: org.id}) %{ user: user, org: org, other_org: other_org, site: site, ctx: %{organization_id: org.id} } end describe "list/3" do test "returns devices for the org", %{org: org, site: site, ctx: ctx} do _ = device_fixture(%{ name: "Listed", organization_id: org.id, site_id: site.id }) assert {:ok, devices} = Resolver.list(nil, %{}, %{context: ctx}) assert Enum.any?(devices, &(&1.name == "Listed")) end test "without context returns auth error" do assert {:error, _} = Resolver.list(nil, %{}, %{}) end end describe "get/3" do test "returns device with site preloaded", %{org: org, site: site, ctx: ctx} do device = device_fixture(%{name: "Found", organization_id: org.id, site_id: site.id}) assert {:ok, found} = Resolver.get(nil, %{id: device.id}, %{context: ctx}) assert found.id == device.id end test "returns 'not found' for cross-org id", %{other_org: other_org, ctx: ctx} do foreign = device_fixture(%{organization_id: other_org.id}) assert {:error, "Device not found"} = Resolver.get(nil, %{id: foreign.id}, %{context: ctx}) end test "without context returns auth error" do assert {:error, _} = Resolver.get(nil, %{id: Ecto.UUID.generate()}, %{}) end end describe "create/3, update/3, delete/3" do test "creates a device under the org", %{site: site, ctx: ctx} do assert {:ok, device} = Resolver.create( nil, %{ input: %{ name: "Created", site_id: site.id, ip_address: "10.10.10.10" } }, %{context: ctx} ) assert device.name == "Created" assert device.organization_id == ctx.organization_id end test "create returns formatted errors on invalid input", %{ctx: ctx} do assert {:error, errors} = Resolver.create(nil, %{input: %{name: ""}}, %{context: ctx}) assert Enum.any?([is_list(errors), is_binary(errors), is_map(errors)]) end test "create without context returns auth error" do assert {:error, _} = Resolver.create(nil, %{input: %{name: "x"}}, %{}) end test "update + delete round-trip", %{org: org, site: site, ctx: ctx} do device = device_fixture(%{name: "Old", organization_id: org.id, site_id: site.id}) assert {:ok, updated} = Resolver.update(nil, %{id: device.id, input: %{name: "New"}}, %{ context: ctx }) assert updated.name == "New" assert {:ok, %{success: true}} = Resolver.delete(nil, %{id: device.id}, %{context: ctx}) end test "update on cross-org id returns 'not found'", %{other_org: other_org, ctx: ctx} do foreign = device_fixture(%{organization_id: other_org.id}) assert {:error, "Device not found"} = Resolver.update(nil, %{id: foreign.id, input: %{name: "x"}}, %{ context: ctx }) end test "update / delete without context returns auth errors" do assert {:error, _} = Resolver.update(nil, %{id: Ecto.UUID.generate(), input: %{}}, %{}) assert {:error, _} = Resolver.delete(nil, %{id: Ecto.UUID.generate()}, %{}) end end describe "metrics/3" do test "returns [] for a device with no checks", %{org: org, site: site, ctx: ctx} do device = device_fixture(%{name: "NoChecks", organization_id: org.id, site_id: site.id}) assert {:ok, []} = Resolver.metrics( nil, %{device_id: device.id, time_range: "24h"}, %{context: ctx} ) end test "returns [] when device has a check but no results yet", %{org: org, site: site, ctx: ctx} do device = device_fixture(%{name: "PendingMetric", organization_id: org.id, site_id: site.id}) {:ok, _check} = Towerops.Monitoring.create_check(%{ name: "Inline Ping", check_type: "ping", device_id: device.id, organization_id: org.id, enabled: true, interval_seconds: 60, timeout_ms: 1000, config: %{"host" => "127.0.0.1"} }) assert {:ok, []} = Resolver.metrics( nil, %{device_id: device.id, time_range: "24h"}, %{context: ctx} ) end test "metrics without context returns auth error" do assert {:error, _} = Resolver.metrics(nil, %{device_id: Ecto.UUID.generate()}, %{}) end test "metrics on cross-org id returns 'not found'", %{other_org: other_org, ctx: ctx} do foreign = device_fixture(%{organization_id: other_org.id}) assert {:error, "Device not found"} = Resolver.metrics( nil, %{device_id: foreign.id, time_range: "1h"}, %{context: ctx} ) end test "metrics falls back to 24h when time_range is unparseable", %{org: org, site: site, ctx: ctx} do device = device_fixture(%{name: "Fallback", organization_id: org.id, site_id: site.id}) assert {:ok, _} = Resolver.metrics( nil, %{device_id: device.id, time_range: "garbage"}, %{context: ctx} ) end end describe "interfaces/3" do test "returns [] when device has no snmp_device", %{org: org, site: site, ctx: ctx} do device = device_fixture(%{name: "NoSnmp", organization_id: org.id, site_id: site.id}) assert {:ok, []} = Resolver.interfaces(nil, %{device_id: device.id}, %{context: ctx}) end test "interfaces returns 'not found' for cross-org id", %{other_org: other_org, ctx: ctx} do foreign = device_fixture(%{organization_id: other_org.id}) assert {:error, "Device not found"} = Resolver.interfaces(nil, %{device_id: foreign.id}, %{context: ctx}) end test "interfaces without context returns auth error" do assert {:error, _} = Resolver.interfaces(nil, %{device_id: Ecto.UUID.generate()}, %{}) end end end