615 lines
16 KiB
Elixir
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
|