towerops/test/towerops_web/controllers/api/agent_controller_test.exs
2026-01-09 17:33:01 -06:00

244 lines
7.3 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 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
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