defmodule ToweropsWeb.Api.AgentControllerTest do use ToweropsWeb.ConnCase import Towerops.AccountsFixtures 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 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