towerops/test/towerops_web/graphql/resolvers/time_series_test.exs

615 lines
16 KiB
Elixir

defmodule ToweropsWeb.GraphQL.Resolvers.TimeSeriesTest do
use ToweropsWeb.ConnCase, async: false
import Towerops.AccountsFixtures
import Towerops.DevicesFixtures
import Towerops.OrganizationsFixtures
import Towerops.SnmpFixtures
alias Towerops.ApiTokens
alias Towerops.Monitoring
alias Towerops.Repo
alias Towerops.Snmp
alias Towerops.Snmp.Interface
alias Towerops.Snmp.Sensor
setup do
user = user_fixture()
organization = organization_fixture(user.id)
{:ok, site} = Towerops.Sites.create_site(%{name: "Test Site", organization_id: organization.id})
device = device_fixture(organization.id, site.id, %{name: "Test Router"})
snmp_device = snmp_device_fixture(%{device: device})
{:ok, {_token, raw_token}} =
ApiTokens.create_api_token(%{
organization_id: organization.id,
user_id: user.id,
name: "GraphQL Test Token"
})
%{
user: user,
organization: organization,
device: device,
snmp_device: snmp_device,
raw_token: raw_token
}
end
defp graphql_query(conn, query, variables) do
conn
|> put_req_header("content-type", "application/json")
|> post("/api/graphql", Jason.encode!(%{query: query, variables: variables}))
|> json_response(200)
end
defp auth_conn(conn, raw_token) do
put_req_header(conn, "authorization", "Bearer #{raw_token}")
end
describe "device_sensors query" do
test "returns sensors for a device", %{
conn: conn,
device: device,
snmp_device: snmp_device,
raw_token: raw_token
} do
_sensor =
%Sensor{}
|> Sensor.changeset(%{
snmp_device_id: snmp_device.id,
sensor_type: "temperature",
sensor_index: "1",
sensor_oid: "1.3.6.1.4.1.9.1.1",
sensor_descr: "CPU Temp",
sensor_unit: "°C",
monitored: true
})
|> Repo.insert!()
query = """
query($deviceId: ID!) {
deviceSensors(deviceId: $deviceId) {
id
sensorType
sensorUnit
sensorDescr
monitored
}
}
"""
result =
conn
|> auth_conn(raw_token)
|> graphql_query(query, %{"deviceId" => device.id})
assert %{"data" => %{"deviceSensors" => sensors}} = result
assert length(sensors) == 1
[sensor] = sensors
assert sensor["sensorType"] == "temperature"
assert sensor["sensorUnit"] == "°C"
assert sensor["sensorDescr"] == "CPU Temp"
assert sensor["monitored"] == true
end
test "requires authentication", %{conn: conn, device: device} do
query = """
query($deviceId: ID!) {
deviceSensors(deviceId: $deviceId) { id }
}
"""
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/graphql", Jason.encode!(%{query: query, variables: %{"deviceId" => device.id}}))
assert json_response(conn, 401)["error"] =~ "Authorization"
end
test "returns error for device in different org", %{
conn: conn,
raw_token: raw_token
} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
{:ok, other_site} = Towerops.Sites.create_site(%{name: "Other Site", organization_id: other_org.id})
other_device = device_fixture(other_org.id, other_site.id)
query = """
query($deviceId: ID!) {
deviceSensors(deviceId: $deviceId) { id }
}
"""
result =
conn
|> auth_conn(raw_token)
|> graphql_query(query, %{"deviceId" => other_device.id})
assert %{"errors" => [%{"message" => "Device not found"}]} = result
end
end
describe "sensor_readings query" do
test "returns sensor readings", %{
conn: conn,
snmp_device: snmp_device,
raw_token: raw_token
} do
sensor =
%Sensor{}
|> Sensor.changeset(%{
snmp_device_id: snmp_device.id,
sensor_type: "temperature",
sensor_index: "1",
sensor_oid: "1.3.6.1.4.1.9.1.2",
sensor_descr: "CPU Temp",
sensor_unit: "°C"
})
|> Repo.insert!()
now = Towerops.Time.now()
{:ok, _} =
Snmp.create_sensor_reading(%{
sensor_id: sensor.id,
value: 42.5,
status: "ok",
checked_at: now
})
{:ok, _} =
Snmp.create_sensor_reading(%{
sensor_id: sensor.id,
value: 43.0,
status: "ok",
checked_at: DateTime.add(now, -60, :second)
})
query = """
query($sensorId: ID!, $timeRange: String) {
sensorReadings(sensorId: $sensorId, timeRange: $timeRange) {
sensorId
value
status
checkedAt
}
}
"""
result =
conn
|> auth_conn(raw_token)
|> graphql_query(query, %{"sensorId" => sensor.id, "timeRange" => "24h"})
assert %{"data" => %{"sensorReadings" => readings}} = result
assert length(readings) == 2
[first | _] = readings
assert first["value"] == 42.5
assert first["status"] == "ok"
end
test "returns empty list when no readings exist", %{
conn: conn,
snmp_device: snmp_device,
raw_token: raw_token
} do
sensor =
%Sensor{}
|> Sensor.changeset(%{
snmp_device_id: snmp_device.id,
sensor_type: "temperature",
sensor_index: "1",
sensor_oid: "1.3.6.1.4.1.9.1.3",
sensor_descr: "CPU Temp",
sensor_unit: "°C"
})
|> Repo.insert!()
query = """
query($sensorId: ID!, $timeRange: String) {
sensorReadings(sensorId: $sensorId, timeRange: $timeRange) {
value
}
}
"""
result =
conn
|> auth_conn(raw_token)
|> graphql_query(query, %{"sensorId" => sensor.id, "timeRange" => "24h"})
assert %{"data" => %{"sensorReadings" => []}} = result
end
test "returns error for sensor in different org", %{
conn: conn,
raw_token: raw_token
} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
{:ok, other_site} = Towerops.Sites.create_site(%{name: "Other Site", organization_id: other_org.id})
other_device = device_fixture(other_org.id, other_site.id)
other_snmp_device = snmp_device_fixture(%{device: other_device})
other_sensor =
%Sensor{}
|> Sensor.changeset(%{
snmp_device_id: other_snmp_device.id,
sensor_type: "temperature",
sensor_index: "1",
sensor_oid: "1.3.6.1.4.1.9.1.4",
sensor_descr: "Other Sensor",
sensor_unit: "°C"
})
|> Repo.insert!()
query = """
query($sensorId: ID!) {
sensorReadings(sensorId: $sensorId) { value }
}
"""
result =
conn
|> auth_conn(raw_token)
|> graphql_query(query, %{"sensorId" => other_sensor.id})
assert %{"errors" => [%{"message" => "Sensor not found"}]} = result
end
test "requires authentication", %{conn: conn} do
query = """
query($sensorId: ID!) {
sensorReadings(sensorId: $sensorId) { value }
}
"""
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/graphql", Jason.encode!(%{query: query, variables: %{"sensorId" => Ecto.UUID.generate()}}))
assert json_response(conn, 401)["error"] =~ "Authorization"
end
end
describe "interface_traffic query" do
test "returns traffic data computed from counter deltas", %{
conn: conn,
snmp_device: snmp_device,
raw_token: raw_token
} do
interface =
%Interface{}
|> Interface.changeset(%{
snmp_device_id: snmp_device.id,
if_index: 1,
if_name: "eth0",
if_descr: "Ethernet 0"
})
|> Repo.insert!()
now = Towerops.Time.now()
# Two data points 60 seconds apart:
# Stat 1: 1000 bytes in, 2000 bytes out
# Stat 2: 2000 bytes in, 4000 bytes out
# => in_bps = (1000 * 8) / 60 = 133.33, out_bps = (2000 * 8) / 60 = 266.67
{:ok, _} =
Snmp.create_interface_stat(%{
interface_id: interface.id,
if_in_octets: 1000,
if_out_octets: 2000,
if_in_errors: 0,
if_out_errors: 0,
checked_at: DateTime.add(now, -60, :second)
})
{:ok, _} =
Snmp.create_interface_stat(%{
interface_id: interface.id,
if_in_octets: 2000,
if_out_octets: 4000,
if_in_errors: 1,
if_out_errors: 0,
checked_at: now
})
query = """
query($interfaceId: ID!, $timeRange: String) {
interfaceTraffic(interfaceId: $interfaceId, timeRange: $timeRange) {
timestamp
inBps
outBps
inErrors
outErrors
}
}
"""
result =
conn
|> auth_conn(raw_token)
|> graphql_query(query, %{"interfaceId" => interface.id, "timeRange" => "24h"})
assert %{"data" => %{"interfaceTraffic" => points}} = result
assert length(points) == 1
[point] = points
assert_in_delta point["inBps"], 133.33, 0.1
assert_in_delta point["outBps"], 266.67, 0.1
assert point["inErrors"] == 1
assert point["outErrors"] == 0
end
test "returns empty list with single data point", %{
conn: conn,
snmp_device: snmp_device,
raw_token: raw_token
} do
interface =
%Interface{}
|> Interface.changeset(%{
snmp_device_id: snmp_device.id,
if_index: 2,
if_name: "eth1",
if_descr: "Ethernet 1"
})
|> Repo.insert!()
now = Towerops.Time.now()
{:ok, _} =
Snmp.create_interface_stat(%{
interface_id: interface.id,
if_in_octets: 1000,
if_out_octets: 2000,
if_in_errors: 0,
if_out_errors: 0,
checked_at: now
})
query = """
query($interfaceId: ID!, $timeRange: String) {
interfaceTraffic(interfaceId: $interfaceId, timeRange: $timeRange) {
inBps
}
}
"""
result =
conn
|> auth_conn(raw_token)
|> graphql_query(query, %{"interfaceId" => interface.id, "timeRange" => "24h"})
assert %{"data" => %{"interfaceTraffic" => []}} = result
end
test "returns error for interface in different org", %{
conn: conn,
raw_token: raw_token
} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
{:ok, other_site} = Towerops.Sites.create_site(%{name: "Other Site", organization_id: other_org.id})
other_device = device_fixture(other_org.id, other_site.id)
other_snmp_device = snmp_device_fixture(%{device: other_device})
other_interface =
%Interface{}
|> Interface.changeset(%{
snmp_device_id: other_snmp_device.id,
if_index: 1,
if_name: "eth0",
if_descr: "Ethernet 0"
})
|> Repo.insert!()
query = """
query($interfaceId: ID!) {
interfaceTraffic(interfaceId: $interfaceId) { inBps }
}
"""
result =
conn
|> auth_conn(raw_token)
|> graphql_query(query, %{"interfaceId" => other_interface.id})
assert %{"errors" => [%{"message" => "Interface not found"}]} = result
end
test "requires authentication", %{conn: conn} do
query = """
query($interfaceId: ID!) {
interfaceTraffic(interfaceId: $interfaceId) { inBps }
}
"""
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/graphql", Jason.encode!(%{query: query, variables: %{"interfaceId" => Ecto.UUID.generate()}}))
assert json_response(conn, 401)["error"] =~ "Authorization"
end
end
describe "check_results query" do
test "returns check results", %{
conn: conn,
device: device,
organization: organization,
raw_token: raw_token
} do
{:ok, check} =
Monitoring.create_check(%{
name: "Ping Check",
check_type: "ping",
organization_id: organization.id,
device_id: device.id,
config: %{"host" => "10.0.0.1"}
})
now = Towerops.Time.now()
{:ok, _} =
Monitoring.create_check_result(%{
check_id: check.id,
organization_id: organization.id,
checked_at: now,
status: 0,
value: 10.5,
response_time_ms: 10.5
})
query = """
query($checkId: ID!, $timeRange: String) {
checkResults(checkId: $checkId, timeRange: $timeRange) {
checkId
value
status
checkedAt
responseTimeMs
}
}
"""
result =
conn
|> auth_conn(raw_token)
|> graphql_query(query, %{"checkId" => check.id, "timeRange" => "1h"})
assert %{"data" => %{"checkResults" => results}} = result
assert length(results) == 1
[res] = results
assert res["checkId"] == check.id
assert res["value"] == 10.5
assert res["status"] == 0
assert res["responseTimeMs"] == 10.5
end
test "returns empty list when no results exist", %{
conn: conn,
device: device,
organization: organization,
raw_token: raw_token
} do
{:ok, check} =
Monitoring.create_check(%{
name: "Empty Check",
check_type: "ping",
organization_id: organization.id,
device_id: device.id,
config: %{"host" => "10.0.0.2"}
})
query = """
query($checkId: ID!, $timeRange: String) {
checkResults(checkId: $checkId, timeRange: $timeRange) {
value
}
}
"""
result =
conn
|> auth_conn(raw_token)
|> graphql_query(query, %{"checkId" => check.id, "timeRange" => "1h"})
assert %{"data" => %{"checkResults" => []}} = result
end
test "returns error for check in different org", %{
conn: conn,
raw_token: raw_token
} do
other_user = user_fixture()
other_org = organization_fixture(other_user.id)
{:ok, other_site} = Towerops.Sites.create_site(%{name: "Other Site", organization_id: other_org.id})
other_device = device_fixture(other_org.id, other_site.id)
{:ok, other_check} =
Monitoring.create_check(%{
name: "Other Check",
check_type: "ping",
organization_id: other_org.id,
device_id: other_device.id,
config: %{"host" => "10.0.0.99"}
})
query = """
query($checkId: ID!) {
checkResults(checkId: $checkId) { value }
}
"""
result =
conn
|> auth_conn(raw_token)
|> graphql_query(query, %{"checkId" => other_check.id})
assert %{"errors" => [%{"message" => "Check not found"}]} = result
end
test "requires authentication", %{conn: conn} do
query = """
query($checkId: ID!) {
checkResults(checkId: $checkId) { value }
}
"""
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/graphql", Jason.encode!(%{query: query, variables: %{"checkId" => Ecto.UUID.generate()}}))
assert json_response(conn, 401)["error"] =~ "Authorization"
end
end
describe "device_sensors edge cases" do
test "returns empty list for device without SNMP device", %{
conn: conn,
organization: organization,
raw_token: raw_token
} do
{:ok, site} = Towerops.Sites.create_site(%{name: "No SNMP Site", organization_id: organization.id})
bare_device = device_fixture(organization.id, site.id, %{name: "No SNMP Router"})
query = """
query($deviceId: ID!) {
deviceSensors(deviceId: $deviceId) { id }
}
"""
result =
conn
|> auth_conn(raw_token)
|> graphql_query(query, %{"deviceId" => bare_device.id})
assert %{"data" => %{"deviceSensors" => []}} = result
end
test "returns empty list for device with SNMP device but no sensors", %{
conn: conn,
device: device,
raw_token: raw_token
} do
query = """
query($deviceId: ID!) {
deviceSensors(deviceId: $deviceId) { id }
}
"""
result =
conn
|> auth_conn(raw_token)
|> graphql_query(query, %{"deviceId" => device.id})
assert %{"data" => %{"deviceSensors" => []}} = result
end
end
end