test: GraphQL.Resolvers.Schedule and Device coverage
This commit is contained in:
parent
b71b1615eb
commit
ee76c568d1
2 changed files with 483 additions and 0 deletions
221
test/towerops_web/graphql/resolvers/device_test.exs
Normal file
221
test/towerops_web/graphql/resolvers/device_test.exs
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
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 is_list(errors) or is_binary(errors) or 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
|
||||
262
test/towerops_web/graphql/resolvers/schedule_test.exs
Normal file
262
test/towerops_web/graphql/resolvers/schedule_test.exs
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
defmodule ToweropsWeb.GraphQL.Resolvers.ScheduleTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.OnCallFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias ToweropsWeb.GraphQL.Resolvers.Schedule, 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 schedules for the org", %{org: org, ctx: ctx} do
|
||||
_ = schedule_fixture(org.id, %{name: "Primary Schedule"})
|
||||
|
||||
assert {:ok, schedules} = Resolver.list(nil, %{}, %{context: ctx})
|
||||
assert Enum.any?(schedules, &(&1.name == "Primary Schedule"))
|
||||
end
|
||||
|
||||
test "without org context returns auth error" do
|
||||
assert {:error, _} = Resolver.list(nil, %{}, %{})
|
||||
end
|
||||
end
|
||||
|
||||
describe "get/3" do
|
||||
test "returns the schedule preloaded with overrides + layers", %{org: org, ctx: ctx} do
|
||||
schedule = schedule_fixture(org.id)
|
||||
|
||||
assert {:ok, found} = Resolver.get(nil, %{id: schedule.id}, %{context: ctx})
|
||||
assert found.id == schedule.id
|
||||
end
|
||||
|
||||
test "returns 'not found' for cross-org id", %{other_org: other_org, ctx: ctx} do
|
||||
foreign = schedule_fixture(other_org.id)
|
||||
|
||||
assert {:error, "Schedule 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 "on_call/3" do
|
||||
test "returns nil when no one is on call (empty schedule)", %{org: org, ctx: ctx} do
|
||||
schedule = schedule_fixture(org.id)
|
||||
assert {:ok, nil} = Resolver.on_call(nil, %{schedule_id: schedule.id}, %{context: ctx})
|
||||
end
|
||||
|
||||
test "returns 'not found' for cross-org id", %{other_org: other_org, ctx: ctx} do
|
||||
foreign = schedule_fixture(other_org.id)
|
||||
|
||||
assert {:error, "Schedule not found"} =
|
||||
Resolver.on_call(nil, %{schedule_id: foreign.id}, %{context: ctx})
|
||||
end
|
||||
|
||||
test "without org context returns auth error" do
|
||||
assert {:error, _} =
|
||||
Resolver.on_call(nil, %{schedule_id: Ecto.UUID.generate()}, %{})
|
||||
end
|
||||
end
|
||||
|
||||
describe "create/3, update/3, delete/3" do
|
||||
test "creates a schedule scoped to the resolved org", %{ctx: ctx} do
|
||||
assert {:ok, schedule} =
|
||||
Resolver.create(nil, %{input: %{name: "Created", timezone: "UTC"}}, %{
|
||||
context: ctx
|
||||
})
|
||||
|
||||
assert schedule.name == "Created"
|
||||
assert schedule.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 is_list(errors) or is_binary(errors) or is_map(errors)
|
||||
end
|
||||
|
||||
test "create without org context returns auth error" do
|
||||
assert {:error, _} = Resolver.create(nil, %{input: %{name: "x"}}, %{})
|
||||
end
|
||||
|
||||
test "update + delete round-trip", %{org: org, ctx: ctx} do
|
||||
schedule = schedule_fixture(org.id, %{name: "Original"})
|
||||
|
||||
assert {:ok, updated} =
|
||||
Resolver.update(nil, %{id: schedule.id, input: %{name: "Renamed"}}, %{
|
||||
context: ctx
|
||||
})
|
||||
|
||||
assert updated.name == "Renamed"
|
||||
|
||||
assert {:ok, %{success: true}} =
|
||||
Resolver.delete(nil, %{id: schedule.id}, %{context: ctx})
|
||||
end
|
||||
|
||||
test "update on cross-org id returns 'not found'",
|
||||
%{other_org: other_org, ctx: ctx} do
|
||||
foreign = schedule_fixture(other_org.id)
|
||||
|
||||
assert {:error, "Schedule not found"} =
|
||||
Resolver.update(nil, %{id: foreign.id, input: %{name: "x"}}, %{context: ctx})
|
||||
end
|
||||
|
||||
test "update / delete / create 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 "layer mutations" do
|
||||
test "create_layer attaches a layer to a schedule", %{org: org, ctx: ctx} do
|
||||
schedule = schedule_fixture(org.id)
|
||||
|
||||
assert {:ok, layer} =
|
||||
Resolver.create_layer(
|
||||
nil,
|
||||
%{
|
||||
schedule_id: schedule.id,
|
||||
input: %{
|
||||
name: "Primary",
|
||||
position: 1,
|
||||
rotation_type: "weekly",
|
||||
rotation_interval: 1,
|
||||
handoff_time: ~T[09:00:00],
|
||||
handoff_day: 1,
|
||||
start_date: ~U[2026-01-01 09:00:00Z]
|
||||
}
|
||||
},
|
||||
%{context: ctx}
|
||||
)
|
||||
|
||||
assert layer.schedule_id == schedule.id
|
||||
end
|
||||
|
||||
test "create_layer auth error without context" do
|
||||
assert {:error, _} =
|
||||
Resolver.create_layer(
|
||||
nil,
|
||||
%{schedule_id: Ecto.UUID.generate(), input: %{}},
|
||||
%{}
|
||||
)
|
||||
end
|
||||
|
||||
test "update_layer + delete_layer round-trip", %{org: org, ctx: ctx} do
|
||||
schedule = schedule_fixture(org.id)
|
||||
layer = layer_fixture(schedule.id, %{name: "Old"})
|
||||
|
||||
assert {:ok, updated} =
|
||||
Resolver.update_layer(nil, %{id: layer.id, input: %{name: "New"}}, %{
|
||||
context: ctx
|
||||
})
|
||||
|
||||
assert updated.name == "New"
|
||||
|
||||
assert {:ok, %{success: true}} =
|
||||
Resolver.delete_layer(nil, %{id: layer.id}, %{context: ctx})
|
||||
end
|
||||
|
||||
test "update_layer on missing layer returns 'not found'", %{ctx: ctx} do
|
||||
assert {:error, "Layer not found"} =
|
||||
Resolver.update_layer(nil, %{id: Ecto.UUID.generate(), input: %{}}, %{
|
||||
context: ctx
|
||||
})
|
||||
end
|
||||
|
||||
test "update_layer auth error without context" do
|
||||
assert {:error, _} =
|
||||
Resolver.update_layer(nil, %{id: Ecto.UUID.generate(), input: %{}}, %{})
|
||||
end
|
||||
|
||||
test "delete_layer auth error without context" do
|
||||
assert {:error, _} =
|
||||
Resolver.delete_layer(nil, %{id: Ecto.UUID.generate()}, %{})
|
||||
end
|
||||
end
|
||||
|
||||
describe "member mutations" do
|
||||
test "add_member + remove_member round-trip", %{org: org, user: user, ctx: ctx} do
|
||||
schedule = schedule_fixture(org.id)
|
||||
layer = layer_fixture(schedule.id)
|
||||
|
||||
assert {:ok, member} =
|
||||
Resolver.add_member(
|
||||
nil,
|
||||
%{layer_id: layer.id, user_id: user.id, position: 0},
|
||||
%{context: ctx}
|
||||
)
|
||||
|
||||
assert {:ok, %{success: true}} =
|
||||
Resolver.remove_member(nil, %{id: member.id}, %{context: ctx})
|
||||
end
|
||||
|
||||
test "remove_member returns 'not found' for unknown id", %{ctx: ctx} do
|
||||
assert {:error, "Member not found"} =
|
||||
Resolver.remove_member(nil, %{id: Ecto.UUID.generate()}, %{context: ctx})
|
||||
end
|
||||
|
||||
test "add_member + remove_member auth errors without context" do
|
||||
assert {:error, _} =
|
||||
Resolver.add_member(
|
||||
nil,
|
||||
%{layer_id: Ecto.UUID.generate(), user_id: Ecto.UUID.generate(), position: 0},
|
||||
%{}
|
||||
)
|
||||
|
||||
assert {:error, _} = Resolver.remove_member(nil, %{id: Ecto.UUID.generate()}, %{})
|
||||
end
|
||||
end
|
||||
|
||||
describe "override mutations" do
|
||||
test "create_override + delete_override round-trip",
|
||||
%{org: org, user: user, ctx: ctx} do
|
||||
schedule = schedule_fixture(org.id)
|
||||
|
||||
assert {:ok, override} =
|
||||
Resolver.create_override(
|
||||
nil,
|
||||
%{
|
||||
schedule_id: schedule.id,
|
||||
input: %{
|
||||
user_id: user.id,
|
||||
start_time: ~U[2026-04-01 09:00:00Z],
|
||||
end_time: ~U[2026-04-02 09:00:00Z]
|
||||
}
|
||||
},
|
||||
%{context: ctx}
|
||||
)
|
||||
|
||||
assert {:ok, %{success: true}} =
|
||||
Resolver.delete_override(nil, %{id: override.id}, %{context: ctx})
|
||||
end
|
||||
|
||||
test "delete_override returns 'not found' for unknown id", %{ctx: ctx} do
|
||||
assert {:error, "Override not found"} =
|
||||
Resolver.delete_override(nil, %{id: Ecto.UUID.generate()}, %{context: ctx})
|
||||
end
|
||||
|
||||
test "create_override + delete_override auth errors without context" do
|
||||
assert {:error, _} =
|
||||
Resolver.create_override(
|
||||
nil,
|
||||
%{schedule_id: Ecto.UUID.generate(), input: %{}},
|
||||
%{}
|
||||
)
|
||||
|
||||
assert {:error, _} = Resolver.delete_override(nil, %{id: Ecto.UUID.generate()}, %{})
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue