towerops/test/towerops_web/graphql/resolvers/check_test.exs
Graham McIntire 28e97ff5f0 add service checks REST API, GraphQL API, and ping executor (#52)
- REST CRUD at /api/v1/checks for HTTP, TCP, DNS, ping checks
- GraphQL queries (checks, check) and mutations (create/update/delete)
- ping executor using system ping with macOS/Linux parsing
- check config validation for ping type (requires host)
- API documentation updated with checks resource section

Reviewed-on: graham/towerops-web#52
2026-03-16 18:40:18 -05:00

275 lines
6.9 KiB
Elixir

defmodule ToweropsWeb.GraphQL.Resolvers.CheckTest do
use ToweropsWeb.ConnCase, async: true
import Towerops.AccountsFixtures
import Towerops.OrganizationsFixtures
alias Towerops.ApiTokens
alias Towerops.Monitoring
setup do
user = user_fixture()
organization = organization_fixture(user.id)
{:ok, {_token, raw_token}} =
ApiTokens.create_api_token(%{
organization_id: organization.id,
user_id: user.id,
name: "Test Token"
})
conn =
build_conn()
|> put_req_header("authorization", "Bearer #{raw_token}")
|> put_req_header("content-type", "application/json")
%{conn: conn, organization: organization}
end
defp graphql_query(conn, query, variables \\ %{}) do
conn
|> post("/api/graphql", Jason.encode!(%{query: query, variables: variables}))
|> json_response(200)
end
defp create_check(organization_id, attrs) do
defaults = %{
name: "Test Check",
check_type: "http",
organization_id: organization_id,
config: %{"url" => "https://example.com"}
}
{:ok, check} = Monitoring.create_check(Map.merge(defaults, attrs))
check
end
describe "checks query" do
test "lists service checks", %{conn: conn, organization: org} do
_http = create_check(org.id, %{name: "HTTP Check"})
_tcp = create_check(org.id, %{name: "TCP Check", check_type: "tcp", config: %{"host" => "10.0.0.1", "port" => 80}})
query = """
{
checks {
id
name
checkType
enabled
}
}
"""
result = graphql_query(conn, query)
assert %{"data" => %{"checks" => checks}} = result
assert length(checks) == 2
names = Enum.map(checks, & &1["name"])
assert "HTTP Check" in names
assert "TCP Check" in names
end
test "filters by check_type", %{conn: conn, organization: org} do
_http = create_check(org.id, %{name: "HTTP Check"})
_tcp = create_check(org.id, %{name: "TCP Check", check_type: "tcp", config: %{"host" => "10.0.0.1", "port" => 80}})
query = """
query($type: String) {
checks(checkType: $type) {
name
checkType
}
}
"""
result = graphql_query(conn, query, %{"type" => "tcp"})
assert %{"data" => %{"checks" => [check]}} = result
assert check["name"] == "TCP Check"
end
test "excludes SNMP checks", %{conn: conn, organization: org} do
_http = create_check(org.id, %{name: "HTTP Check"})
{:ok, _snmp} =
Monitoring.create_check(%{
name: "SNMP Sensor",
check_type: "snmp_sensor",
organization_id: org.id,
config: %{"oid" => "1.3.6.1.2.1.1.3.0"}
})
query = """
{
checks {
name
checkType
}
}
"""
result = graphql_query(conn, query)
assert %{"data" => %{"checks" => checks}} = result
assert length(checks) == 1
assert List.first(checks)["checkType"] == "http"
end
end
describe "check query" do
test "returns a single check", %{conn: conn, organization: org} do
check = create_check(org.id, %{name: "Single Check"})
query = """
query($id: ID!) {
check(id: $id) {
id
name
checkType
config
enabled
intervalSeconds
}
}
"""
result = graphql_query(conn, query, %{"id" => check.id})
assert %{"data" => %{"check" => data}} = result
assert data["id"] == check.id
assert data["name"] == "Single Check"
assert data["checkType"] == "http"
assert data["enabled"] == true
end
test "returns error for non-existent check", %{conn: conn} do
query = """
query($id: ID!) {
check(id: $id) {
id
name
}
}
"""
result = graphql_query(conn, query, %{"id" => Ecto.UUID.generate()})
assert %{"errors" => [%{"message" => "Check not found"}]} = result
end
end
describe "createCheck mutation" do
test "creates an HTTP check", %{conn: conn, organization: org} do
mutation = """
mutation($input: CheckInput!) {
createCheck(input: $input) {
id
name
checkType
enabled
}
}
"""
input = %{
"name" => "New HTTP Check",
"check_type" => "http",
"config" => Jason.encode!(%{"url" => "https://example.com/health"})
}
result = graphql_query(conn, mutation, %{"input" => input})
assert %{"data" => %{"createCheck" => check}} = result
assert check["name"] == "New HTTP Check"
assert check["checkType"] == "http"
assert check["enabled"] == true
# Verify it exists in DB
assert Monitoring.get_check(check["id"]).organization_id == org.id
end
test "returns error for invalid check type", %{conn: conn} do
mutation = """
mutation($input: CheckInput!) {
createCheck(input: $input) {
id
}
}
"""
input = %{
"name" => "Bad Check",
"check_type" => "snmp_sensor",
"config" => Jason.encode!(%{"oid" => "1.3.6.1.2.1.1.3.0"})
}
result = graphql_query(conn, mutation, %{"input" => input})
assert %{"errors" => [%{"message" => message}]} = result
assert message =~ "Invalid check type"
end
end
describe "updateCheck mutation" do
test "updates an existing check", %{conn: conn, organization: org} do
check = create_check(org.id, %{name: "Original"})
mutation = """
mutation($id: ID!, $input: CheckInput!) {
updateCheck(id: $id, input: $input) {
id
name
intervalSeconds
}
}
"""
input = %{"name" => "Updated", "interval_seconds" => 120}
result = graphql_query(conn, mutation, %{"id" => check.id, "input" => input})
assert %{"data" => %{"updateCheck" => updated}} = result
assert updated["name"] == "Updated"
assert updated["intervalSeconds"] == 120
end
end
describe "deleteCheck mutation" do
test "deletes a check", %{conn: conn, organization: org} do
check = create_check(org.id, %{name: "Doomed"})
mutation = """
mutation($id: ID!) {
deleteCheck(id: $id) {
success
message
}
}
"""
result = graphql_query(conn, mutation, %{"id" => check.id})
assert %{"data" => %{"deleteCheck" => %{"success" => true}}} = result
assert Monitoring.get_check(check.id) == nil
end
end
describe "authentication" do
test "returns 401 without auth" do
conn = put_req_header(build_conn(), "content-type", "application/json")
query = """
{
checks {
id
}
}
"""
resp = post(conn, "/api/graphql", Jason.encode!(%{query: query}))
assert resp.status == 401
end
end
end