Added 6 new tests covering previously untested scenarios: - Equipment without SNMP device (nil sensors/interfaces) - Protobuf metrics with unknown types - JSON metrics with unknown types - Metrics with nil timestamps - Metrics with invalid ISO8601 timestamp strings Coverage increased from 90.41% to near 100%. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
360 lines
11 KiB
Elixir
360 lines
11 KiB
Elixir
defmodule ToweropsWeb.Api.AgentControllerTest do
|
|
use ToweropsWeb.ConnCase
|
|
|
|
import Towerops.AccountsFixtures
|
|
|
|
alias Towerops.Agent.InterfaceStat
|
|
alias Towerops.Agent.Metric
|
|
alias Towerops.Agent.MetricBatch
|
|
alias Towerops.Agent.SensorReading
|
|
alias Towerops.Agents
|
|
alias Towerops.Repo
|
|
alias Towerops.Snmp.Device
|
|
alias Towerops.Snmp.Interface
|
|
alias Towerops.Snmp.Sensor
|
|
|
|
setup do
|
|
user = user_fixture()
|
|
|
|
{:ok, organization} =
|
|
Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
|
|
|
{:ok, agent_token, token} = Agents.create_agent_token(organization.id, "Test Agent")
|
|
|
|
%{organization: organization, agent_token: agent_token, token: token}
|
|
end
|
|
|
|
describe "GET /api/v1/agent/config" do
|
|
test "requires authentication", %{conn: conn} do
|
|
conn = get(conn, ~p"/api/v1/agent/config")
|
|
assert json_response(conn, 401)
|
|
end
|
|
|
|
test "returns config for authenticated agent with no equipment", %{conn: conn, token: token} do
|
|
conn =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer #{token}")
|
|
|> get(~p"/api/v1/agent/config")
|
|
|
|
assert %{
|
|
"version" => "1.0",
|
|
"poll_interval_seconds" => 60,
|
|
"equipment" => []
|
|
} = json_response(conn, 200)
|
|
end
|
|
|
|
test "returns config with equipment without SNMP device", %{
|
|
conn: conn,
|
|
token: token,
|
|
organization: org,
|
|
agent_token: agent_token
|
|
} do
|
|
{:ok, site} =
|
|
Towerops.Sites.create_site(%{
|
|
name: "Test Site",
|
|
organization_id: org.id
|
|
})
|
|
|
|
{:ok, equipment} =
|
|
Towerops.Equipment.create_equipment(%{
|
|
name: "Test Router",
|
|
ip_address: "192.168.1.1",
|
|
snmp_enabled: true,
|
|
snmp_version: "2c",
|
|
snmp_community: "public",
|
|
site_id: site.id
|
|
})
|
|
|
|
{:ok, _assignment} = Agents.assign_equipment_to_agent(agent_token.id, equipment.id)
|
|
|
|
conn =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer #{token}")
|
|
|> get(~p"/api/v1/agent/config")
|
|
|
|
response = json_response(conn, 200)
|
|
|
|
assert %{
|
|
"version" => "1.0",
|
|
"poll_interval_seconds" => 60,
|
|
"equipment" => [equipment_config]
|
|
} = response
|
|
|
|
assert equipment_config["id"] == equipment.id
|
|
assert equipment_config["sensors"] == []
|
|
assert equipment_config["interfaces"] == []
|
|
end
|
|
|
|
test "returns config with assigned equipment", %{
|
|
conn: conn,
|
|
token: token,
|
|
organization: org,
|
|
agent_token: agent_token
|
|
} do
|
|
{:ok, site} =
|
|
Towerops.Sites.create_site(%{
|
|
name: "Test Site",
|
|
organization_id: org.id
|
|
})
|
|
|
|
{:ok, equipment} =
|
|
Towerops.Equipment.create_equipment(%{
|
|
name: "Test Router",
|
|
ip_address: "192.168.1.1",
|
|
snmp_enabled: true,
|
|
snmp_version: "2c",
|
|
snmp_community: "public",
|
|
snmp_port: 161,
|
|
check_interval_seconds: 120,
|
|
site_id: site.id
|
|
})
|
|
|
|
device =
|
|
%Device{}
|
|
|> Device.changeset(%{
|
|
equipment_id: equipment.id,
|
|
sys_name: "Test Device",
|
|
sys_descr: "Test Description"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: device.id,
|
|
if_index: 1,
|
|
if_name: "GigabitEthernet0/1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
{:ok, _assignment} = Agents.assign_equipment_to_agent(agent_token.id, equipment.id)
|
|
|
|
conn =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer #{token}")
|
|
|> get(~p"/api/v1/agent/config")
|
|
|
|
response = json_response(conn, 200)
|
|
|
|
assert %{
|
|
"version" => "1.0",
|
|
"poll_interval_seconds" => 60,
|
|
"equipment" => [equipment_config]
|
|
} = response
|
|
|
|
assert equipment_config["id"] == equipment.id
|
|
assert equipment_config["name"] == "Test Router"
|
|
assert equipment_config["ip_address"] == "192.168.1.1"
|
|
assert equipment_config["snmp"]["enabled"] == true
|
|
assert equipment_config["snmp"]["version"] == "2c"
|
|
assert equipment_config["snmp"]["community"] == "public"
|
|
assert equipment_config["snmp"]["port"] == 161
|
|
assert equipment_config["poll_interval_seconds"] == 120
|
|
|
|
assert length(equipment_config["sensors"]) == 1
|
|
sensor_config = hd(equipment_config["sensors"])
|
|
assert sensor_config["id"] == sensor.id
|
|
assert sensor_config["type"] == "temperature"
|
|
assert sensor_config["oid"] == "1.2.3.4"
|
|
|
|
assert length(equipment_config["interfaces"]) == 1
|
|
interface_config = hd(equipment_config["interfaces"])
|
|
assert interface_config["id"] == interface.id
|
|
assert interface_config["if_index"] == 1
|
|
assert interface_config["if_name"] == "GigabitEthernet0/1"
|
|
end
|
|
end
|
|
|
|
describe "POST /api/v1/agent/metrics" do
|
|
test "requires authentication", %{conn: conn} do
|
|
conn = post(conn, ~p"/api/v1/agent/metrics", %{"metrics" => []})
|
|
assert json_response(conn, 401)
|
|
end
|
|
|
|
test "accepts valid metrics", %{conn: conn, token: token} do
|
|
metrics = [
|
|
%{
|
|
"type" => "sensor_reading",
|
|
"sensor_id" => Ecto.UUID.generate(),
|
|
"value" => 45.5,
|
|
"status" => "ok",
|
|
"timestamp" => DateTime.to_iso8601(DateTime.utc_now())
|
|
},
|
|
%{
|
|
"type" => "interface_stat",
|
|
"interface_id" => Ecto.UUID.generate(),
|
|
"if_in_octets" => 1_234_567,
|
|
"if_out_octets" => 9_876_543,
|
|
"if_in_errors" => 0,
|
|
"if_out_errors" => 0,
|
|
"if_in_discards" => 0,
|
|
"if_out_discards" => 0,
|
|
"timestamp" => DateTime.to_iso8601(DateTime.utc_now())
|
|
}
|
|
]
|
|
|
|
conn =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer #{token}")
|
|
|> post(~p"/api/v1/agent/metrics", %{"metrics" => metrics})
|
|
|
|
assert %{"status" => "accepted", "received" => 2} = json_response(conn, 200)
|
|
end
|
|
|
|
test "accepts protobuf metrics", %{conn: conn, token: token} do
|
|
# Create protobuf metrics
|
|
sensor_reading = %SensorReading{
|
|
sensor_id: Ecto.UUID.generate(),
|
|
value: 45.5,
|
|
status: "ok",
|
|
timestamp: DateTime.to_unix(DateTime.utc_now())
|
|
}
|
|
|
|
interface_stat = %InterfaceStat{
|
|
interface_id: Ecto.UUID.generate(),
|
|
if_in_octets: 1_234_567,
|
|
if_out_octets: 9_876_543,
|
|
if_in_errors: 0,
|
|
if_out_errors: 0,
|
|
if_in_discards: 0,
|
|
if_out_discards: 0,
|
|
timestamp: DateTime.to_unix(DateTime.utc_now())
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:sensor_reading, sensor_reading}},
|
|
%Metric{metric_type: {:interface_stat, interface_stat}}
|
|
]
|
|
}
|
|
|
|
# Encode to protobuf binary
|
|
encoded = MetricBatch.encode(batch)
|
|
|
|
conn =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer #{token}")
|
|
|> put_req_header("content-type", "application/x-protobuf")
|
|
|> post(~p"/api/v1/agent/metrics", encoded)
|
|
|
|
assert %{"status" => "accepted", "received" => 2} = json_response(conn, 200)
|
|
end
|
|
|
|
test "handles protobuf metrics with unknown types", %{conn: conn, token: token} do
|
|
# Create metric with unknown type (by using empty metric_type)
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: nil}
|
|
]
|
|
}
|
|
|
|
encoded = MetricBatch.encode(batch)
|
|
|
|
conn =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer #{token}")
|
|
|> put_req_header("content-type", "application/x-protobuf")
|
|
|> post(~p"/api/v1/agent/metrics", encoded)
|
|
|
|
# Unknown types are filtered out, so received count is 0
|
|
assert %{"status" => "accepted", "received" => 0} = json_response(conn, 200)
|
|
end
|
|
|
|
test "handles JSON metrics with unknown types", %{conn: conn, token: token} do
|
|
metrics = [
|
|
%{
|
|
"type" => "unknown_type",
|
|
"some_field" => "value"
|
|
}
|
|
]
|
|
|
|
conn =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer #{token}")
|
|
|> post(~p"/api/v1/agent/metrics", %{"metrics" => metrics})
|
|
|
|
assert %{"status" => "accepted", "received" => 1} = json_response(conn, 200)
|
|
end
|
|
|
|
test "handles metrics with nil timestamp", %{conn: conn, token: token} do
|
|
metrics = [
|
|
%{
|
|
"type" => "sensor_reading",
|
|
"sensor_id" => Ecto.UUID.generate(),
|
|
"value" => 45.5,
|
|
"status" => "ok",
|
|
"timestamp" => nil
|
|
}
|
|
]
|
|
|
|
conn =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer #{token}")
|
|
|> post(~p"/api/v1/agent/metrics", %{"metrics" => metrics})
|
|
|
|
assert %{"status" => "accepted", "received" => 1} = json_response(conn, 200)
|
|
end
|
|
|
|
test "handles metrics with invalid ISO8601 timestamp", %{conn: conn, token: token} do
|
|
metrics = [
|
|
%{
|
|
"type" => "sensor_reading",
|
|
"sensor_id" => Ecto.UUID.generate(),
|
|
"value" => 45.5,
|
|
"status" => "ok",
|
|
"timestamp" => "invalid-date-string"
|
|
}
|
|
]
|
|
|
|
conn =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer #{token}")
|
|
|> post(~p"/api/v1/agent/metrics", %{"metrics" => metrics})
|
|
|
|
assert %{"status" => "accepted", "received" => 1} = json_response(conn, 200)
|
|
end
|
|
end
|
|
|
|
describe "POST /api/v1/agent/heartbeat" do
|
|
test "requires authentication", %{conn: conn} do
|
|
conn = post(conn, ~p"/api/v1/agent/heartbeat", %{})
|
|
assert json_response(conn, 401)
|
|
end
|
|
|
|
test "updates heartbeat with metadata", %{
|
|
conn: conn,
|
|
token: token,
|
|
agent_token: agent_token
|
|
} do
|
|
conn =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer #{token}")
|
|
|> post(~p"/api/v1/agent/heartbeat", %{
|
|
"version" => "0.1.0",
|
|
"hostname" => "test-agent",
|
|
"uptime_seconds" => 3600
|
|
})
|
|
|
|
assert %{"status" => "ok"} = json_response(conn, 200)
|
|
|
|
# Give the async task time to complete
|
|
Process.sleep(100)
|
|
|
|
updated_token = Repo.get!(Agents.AgentToken, agent_token.id)
|
|
assert updated_token.last_seen_at
|
|
assert updated_token.metadata["version"] == "0.1.0"
|
|
assert updated_token.metadata["hostname"] == "test-agent"
|
|
assert updated_token.metadata["uptime_seconds"] == 3600
|
|
end
|
|
end
|
|
end
|