1707 lines
50 KiB
Elixir
1707 lines
50 KiB
Elixir
defmodule Towerops.Agent.ProtoTest do
|
|
@moduledoc """
|
|
Tests for Protocol Buffer generated modules.
|
|
|
|
These tests verify that encoding and decoding works correctly for all
|
|
protobuf message types used in agent communication. The
|
|
implementation uses {:ok, struct} tuples for decode where available,
|
|
and some modules are encode-only.
|
|
"""
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Towerops.Agent.AgentError
|
|
alias Towerops.Agent.AgentHeartbeat
|
|
alias Towerops.Agent.AgentJob
|
|
alias Towerops.Agent.AgentJobList
|
|
alias Towerops.Agent.CheckResult
|
|
alias Towerops.Agent.CredentialTestResult
|
|
alias Towerops.Agent.HeartbeatMetadata
|
|
alias Towerops.Agent.HeartbeatResponse
|
|
alias Towerops.Agent.LldpTopologyResult
|
|
alias Towerops.Agent.MikrotikCommand
|
|
alias Towerops.Agent.MikrotikDevice
|
|
alias Towerops.Agent.MikrotikResult
|
|
alias Towerops.Agent.MikrotikSentence
|
|
alias Towerops.Agent.MonitoringCheck
|
|
alias Towerops.Agent.NeighborDiscovery
|
|
alias Towerops.Agent.Sensor
|
|
alias Towerops.Agent.SnmpDevice
|
|
alias Towerops.Agent.SnmpQuery
|
|
alias Towerops.Agent.SnmpResult
|
|
alias Towerops.Proto.Decode
|
|
alias Towerops.Proto.Encode
|
|
alias Towerops.Proto.Types.AgentConfig
|
|
alias Towerops.Proto.Types.Check
|
|
alias Towerops.Proto.Types.CheckList
|
|
alias Towerops.Proto.Types.Device
|
|
alias Towerops.Proto.Types.HttpCheckConfig
|
|
alias Towerops.Proto.Types.LldpNeighbor
|
|
|
|
@device_uuid "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
@job_uuid "b2c3d4e5-f6a7-8901-bcde-f12345678901"
|
|
|
|
describe "AgentError" do
|
|
test "encodes and decodes correctly" do
|
|
error = %AgentError{
|
|
device_id: @device_uuid,
|
|
job_id: @job_uuid,
|
|
message: "SNMP timeout",
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = AgentError.encode(error)
|
|
assert {:ok, decoded} = AgentError.decode(encoded)
|
|
|
|
assert decoded.device_id == error.device_id
|
|
assert decoded.job_id == error.job_id
|
|
assert decoded.message == error.message
|
|
assert decoded.timestamp == error.timestamp
|
|
end
|
|
|
|
test "decodes empty device_id as error" do
|
|
error = %AgentError{}
|
|
encoded = AgentError.encode(error)
|
|
assert {:error, _reason} = AgentError.decode(encoded)
|
|
end
|
|
end
|
|
|
|
describe "AgentHeartbeat" do
|
|
test "encodes and decodes correctly" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: "agent-host",
|
|
uptime_seconds: 3600,
|
|
ip_address: "192.168.1.100"
|
|
}
|
|
|
|
encoded = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, decoded} = AgentHeartbeat.decode(encoded)
|
|
|
|
assert decoded.version == heartbeat.version
|
|
assert decoded.hostname == heartbeat.hostname
|
|
assert decoded.uptime_seconds == heartbeat.uptime_seconds
|
|
assert decoded.ip_address == heartbeat.ip_address
|
|
end
|
|
|
|
test "encodes empty message" do
|
|
heartbeat = %AgentHeartbeat{}
|
|
encoded = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, decoded} = AgentHeartbeat.decode(encoded)
|
|
|
|
assert decoded.version == ""
|
|
assert decoded.hostname == ""
|
|
assert decoded.uptime_seconds == 0
|
|
assert decoded.ip_address == ""
|
|
end
|
|
end
|
|
|
|
describe "JobType enum" do
|
|
test "encodes DISCOVER constant" do
|
|
job = %AgentJob{job_id: "test", job_type: :DISCOVER, device_id: "dev-1"}
|
|
encoded = AgentJob.encode(job)
|
|
assert {:ok, decoded} = AgentJob.decode(encoded)
|
|
assert decoded.job_type == :DISCOVER
|
|
end
|
|
|
|
test "encodes POLL constant" do
|
|
job = %AgentJob{job_id: "test", job_type: :POLL, device_id: "dev-1"}
|
|
encoded = AgentJob.encode(job)
|
|
assert {:ok, decoded} = AgentJob.decode(encoded)
|
|
assert decoded.job_type == :POLL
|
|
end
|
|
end
|
|
|
|
describe "QueryType enum" do
|
|
test "encodes GET constant via AgentJob round-trip" do
|
|
query = %SnmpQuery{query_type: :GET, oids: ["1.3.6.1.2.1.1.1.0"]}
|
|
|
|
job = %AgentJob{
|
|
job_id: "test-get",
|
|
job_type: :POLL,
|
|
device_id: "dev-1",
|
|
snmp_device: %SnmpDevice{ip: "192.168.1.1"},
|
|
queries: [query]
|
|
}
|
|
|
|
encoded = AgentJob.encode(job)
|
|
assert {:ok, decoded} = AgentJob.decode(encoded)
|
|
assert hd(decoded.queries).query_type == :GET
|
|
end
|
|
|
|
test "encodes WALK constant via AgentJob round-trip" do
|
|
query = %SnmpQuery{query_type: :WALK, oids: ["1.3.6.1.2.1.2.2.1"]}
|
|
|
|
job = %AgentJob{
|
|
job_id: "test-walk",
|
|
job_type: :POLL,
|
|
device_id: "dev-1",
|
|
snmp_device: %SnmpDevice{ip: "192.168.1.1"},
|
|
queries: [query]
|
|
}
|
|
|
|
encoded = AgentJob.encode(job)
|
|
assert {:ok, decoded} = AgentJob.decode(encoded)
|
|
assert hd(decoded.queries).query_type == :WALK
|
|
end
|
|
end
|
|
|
|
describe "SnmpDevice" do
|
|
test "encodes correctly" do
|
|
device = %SnmpDevice{
|
|
ip: "192.168.1.1",
|
|
community: "public",
|
|
version: "2c",
|
|
port: 161
|
|
}
|
|
|
|
encoded = SnmpDevice.encode(device)
|
|
assert is_binary(encoded)
|
|
end
|
|
|
|
test "encodes empty message" do
|
|
device = %SnmpDevice{}
|
|
encoded = SnmpDevice.encode(device)
|
|
assert is_binary(encoded)
|
|
end
|
|
|
|
test "round-trips through AgentJob" do
|
|
device = %SnmpDevice{
|
|
ip: "192.168.1.1",
|
|
community: "public",
|
|
version: "2c",
|
|
port: 161
|
|
}
|
|
|
|
job = %AgentJob{
|
|
job_id: "test-device",
|
|
job_type: :POLL,
|
|
device_id: "dev-1",
|
|
snmp_device: device,
|
|
queries: []
|
|
}
|
|
|
|
encoded = AgentJob.encode(job)
|
|
assert {:ok, decoded} = AgentJob.decode(encoded)
|
|
|
|
assert decoded.snmp_device.ip == device.ip
|
|
assert decoded.snmp_device.community == device.community
|
|
assert decoded.snmp_device.version == device.version
|
|
assert decoded.snmp_device.port == device.port
|
|
end
|
|
end
|
|
|
|
describe "SnmpQuery" do
|
|
test "encodes GET query" do
|
|
query = %SnmpQuery{
|
|
query_type: :GET,
|
|
oids: ["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.2.0"]
|
|
}
|
|
|
|
encoded = SnmpQuery.encode(query)
|
|
assert is_binary(encoded)
|
|
end
|
|
|
|
test "encodes WALK query" do
|
|
query = %SnmpQuery{
|
|
query_type: :WALK,
|
|
oids: ["1.3.6.1.2.1.2.2.1"]
|
|
}
|
|
|
|
encoded = SnmpQuery.encode(query)
|
|
assert is_binary(encoded)
|
|
end
|
|
|
|
test "encodes empty message" do
|
|
query = %SnmpQuery{}
|
|
encoded = SnmpQuery.encode(query)
|
|
assert is_binary(encoded)
|
|
end
|
|
|
|
test "round-trips through AgentJob" do
|
|
queries = [
|
|
%SnmpQuery{query_type: :GET, oids: ["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.2.0"]},
|
|
%SnmpQuery{query_type: :WALK, oids: ["1.3.6.1.2.1.2.2.1"]}
|
|
]
|
|
|
|
job = %AgentJob{
|
|
job_id: "test-queries",
|
|
job_type: :POLL,
|
|
device_id: "dev-1",
|
|
snmp_device: %SnmpDevice{ip: "192.168.1.1"},
|
|
queries: queries
|
|
}
|
|
|
|
encoded = AgentJob.encode(job)
|
|
assert {:ok, decoded} = AgentJob.decode(encoded)
|
|
|
|
assert length(decoded.queries) == 2
|
|
assert Enum.at(decoded.queries, 0).query_type == :GET
|
|
assert Enum.at(decoded.queries, 0).oids == ["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.2.0"]
|
|
assert Enum.at(decoded.queries, 1).query_type == :WALK
|
|
assert Enum.at(decoded.queries, 1).oids == ["1.3.6.1.2.1.2.2.1"]
|
|
end
|
|
end
|
|
|
|
describe "AgentJob" do
|
|
test "encodes and decodes DISCOVER job" do
|
|
job = %AgentJob{
|
|
job_id: "discover:device-123",
|
|
job_type: :DISCOVER,
|
|
device_id: "device-123",
|
|
snmp_device: %SnmpDevice{
|
|
ip: "192.168.1.1",
|
|
community: "public",
|
|
version: "2c",
|
|
port: 161
|
|
},
|
|
queries: [
|
|
%SnmpQuery{
|
|
query_type: :GET,
|
|
oids: ["1.3.6.1.2.1.1.1.0"]
|
|
}
|
|
]
|
|
}
|
|
|
|
encoded = AgentJob.encode(job)
|
|
assert {:ok, decoded} = AgentJob.decode(encoded)
|
|
|
|
assert decoded.job_id == job.job_id
|
|
assert decoded.job_type == :DISCOVER
|
|
assert decoded.device_id == job.device_id
|
|
assert decoded.snmp_device.ip == job.snmp_device.ip
|
|
assert decoded.snmp_device.community == job.snmp_device.community
|
|
assert decoded.snmp_device.version == job.snmp_device.version
|
|
assert decoded.snmp_device.port == job.snmp_device.port
|
|
|
|
assert length(decoded.queries) == 1
|
|
assert hd(decoded.queries).query_type == :GET
|
|
assert hd(decoded.queries).oids == ["1.3.6.1.2.1.1.1.0"]
|
|
end
|
|
|
|
test "encodes and decodes POLL job" do
|
|
job = %AgentJob{
|
|
job_id: "poll:device-123",
|
|
job_type: :POLL,
|
|
device_id: "device-123",
|
|
snmp_device: %SnmpDevice{
|
|
ip: "192.168.1.1",
|
|
community: "public",
|
|
version: "2c",
|
|
port: 161
|
|
},
|
|
queries: [
|
|
%SnmpQuery{
|
|
query_type: :GET,
|
|
oids: ["1.3.6.1.4.1.14988.1.1.3.8.0"]
|
|
}
|
|
]
|
|
}
|
|
|
|
encoded = AgentJob.encode(job)
|
|
assert {:ok, decoded} = AgentJob.decode(encoded)
|
|
|
|
assert decoded.job_id == job.job_id
|
|
assert decoded.job_type == :POLL
|
|
assert decoded.device_id == job.device_id
|
|
assert decoded.snmp_device.ip == job.snmp_device.ip
|
|
assert length(decoded.queries) == 1
|
|
end
|
|
|
|
test "encodes empty message" do
|
|
job = %AgentJob{}
|
|
encoded = AgentJob.encode(job)
|
|
assert {:ok, decoded} = AgentJob.decode(encoded)
|
|
|
|
assert decoded.job_id == ""
|
|
assert decoded.job_type == :DISCOVER
|
|
assert decoded.device_id == ""
|
|
assert decoded.snmp_device == nil
|
|
assert decoded.queries == []
|
|
end
|
|
end
|
|
|
|
describe "AgentJobList" do
|
|
test "encodes list of jobs" do
|
|
job_list = %AgentJobList{
|
|
jobs: [
|
|
%AgentJob{
|
|
job_id: "poll:device-1",
|
|
job_type: :POLL,
|
|
device_id: "device-1",
|
|
snmp_device: %SnmpDevice{ip: "192.168.1.1"},
|
|
queries: []
|
|
},
|
|
%AgentJob{
|
|
job_id: "discover:device-2",
|
|
job_type: :DISCOVER,
|
|
device_id: "device-2",
|
|
snmp_device: %SnmpDevice{ip: "192.168.1.2"},
|
|
queries: []
|
|
}
|
|
]
|
|
}
|
|
|
|
encoded = AgentJobList.encode(job_list)
|
|
assert is_binary(encoded)
|
|
end
|
|
|
|
test "encodes empty job list" do
|
|
job_list = %AgentJobList{jobs: []}
|
|
encoded = AgentJobList.encode(job_list)
|
|
assert is_binary(encoded)
|
|
end
|
|
end
|
|
|
|
describe "SnmpResult" do
|
|
test "encodes and decodes with oid_values map" do
|
|
result = %SnmpResult{
|
|
device_id: @device_uuid,
|
|
job_type: :POLL,
|
|
oid_values: %{
|
|
"1.3.6.1.2.1.1.1.0" => "Cisco IOS",
|
|
"1.3.6.1.2.1.1.3.0" => "12345"
|
|
},
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = SnmpResult.encode(result)
|
|
assert {:ok, decoded} = SnmpResult.decode(encoded)
|
|
|
|
assert decoded.device_id == result.device_id
|
|
assert decoded.job_type == :POLL
|
|
assert decoded.oid_values == result.oid_values
|
|
assert decoded.timestamp == result.timestamp
|
|
end
|
|
|
|
test "encodes and decodes DISCOVER result" do
|
|
result = %SnmpResult{
|
|
device_id: @device_uuid,
|
|
job_type: :DISCOVER,
|
|
oid_values: %{},
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = SnmpResult.encode(result)
|
|
assert {:ok, decoded} = SnmpResult.decode(encoded)
|
|
|
|
assert decoded.device_id == result.device_id
|
|
assert decoded.job_type == :DISCOVER
|
|
assert decoded.timestamp == result.timestamp
|
|
end
|
|
|
|
test "decodes empty device_id as error" do
|
|
result = %SnmpResult{}
|
|
encoded = SnmpResult.encode(result)
|
|
assert {:error, _reason} = SnmpResult.decode(encoded)
|
|
end
|
|
end
|
|
|
|
describe "MonitoringCheck" do
|
|
test "encodes and decodes success check" do
|
|
check = %MonitoringCheck{
|
|
device_id: @device_uuid,
|
|
status: "success",
|
|
response_time_ms: 45.2,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = MonitoringCheck.encode(check)
|
|
assert {:ok, decoded} = MonitoringCheck.decode(encoded)
|
|
|
|
assert decoded.device_id == check.device_id
|
|
assert decoded.status == check.status
|
|
assert decoded.response_time_ms == check.response_time_ms
|
|
assert decoded.timestamp == check.timestamp
|
|
end
|
|
|
|
test "encodes and decodes failure check" do
|
|
check = %MonitoringCheck{
|
|
device_id: @device_uuid,
|
|
status: "failure",
|
|
response_time_ms: 0.0,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = MonitoringCheck.encode(check)
|
|
assert {:ok, decoded} = MonitoringCheck.decode(encoded)
|
|
|
|
assert decoded.device_id == check.device_id
|
|
assert decoded.status == check.status
|
|
assert decoded.response_time_ms == check.response_time_ms
|
|
assert decoded.timestamp == check.timestamp
|
|
end
|
|
|
|
test "decodes empty device_id as error" do
|
|
check = %MonitoringCheck{}
|
|
encoded = MonitoringCheck.encode(check)
|
|
assert {:error, _reason} = MonitoringCheck.decode(encoded)
|
|
end
|
|
end
|
|
|
|
describe "NeighborDiscovery" do
|
|
test "encodes LLDP neighbor" do
|
|
neighbor = %NeighborDiscovery{
|
|
interface_id: "interface-123",
|
|
protocol: "LLDP",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
remote_system_name: "neighbor-switch",
|
|
remote_system_description: "Cisco IOS",
|
|
remote_platform: "cisco",
|
|
remote_port_id: "GigabitEthernet1/0/1",
|
|
remote_port_description: "Uplink to Core",
|
|
remote_address: "192.168.1.2",
|
|
remote_capabilities: ["bridge", "router"],
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = NeighborDiscovery.encode(neighbor)
|
|
assert is_binary(encoded)
|
|
end
|
|
|
|
test "encodes CDP neighbor" do
|
|
neighbor = %NeighborDiscovery{
|
|
interface_id: "interface-456",
|
|
protocol: "CDP",
|
|
remote_chassis_id: "Router-2",
|
|
remote_system_name: "core-router",
|
|
remote_platform: "Cisco 2960",
|
|
remote_port_id: "FastEthernet0/1",
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = NeighborDiscovery.encode(neighbor)
|
|
assert is_binary(encoded)
|
|
end
|
|
|
|
test "encodes empty message" do
|
|
neighbor = %NeighborDiscovery{}
|
|
encoded = NeighborDiscovery.encode(neighbor)
|
|
assert is_binary(encoded)
|
|
end
|
|
end
|
|
|
|
describe "Sensor.MetadataEntry" do
|
|
test "creates struct with key and value" do
|
|
entry = %Sensor.MetadataEntry{
|
|
key: "location",
|
|
value: "CPU Core 1"
|
|
}
|
|
|
|
assert entry.key == "location"
|
|
assert entry.value == "CPU Core 1"
|
|
end
|
|
|
|
test "creates empty entry" do
|
|
entry = %Sensor.MetadataEntry{}
|
|
|
|
assert entry.key == ""
|
|
assert entry.value == ""
|
|
end
|
|
end
|
|
|
|
describe "Sensor (protobuf)" do
|
|
test "encodes and decodes sensor with metadata" do
|
|
sensor = %Sensor{
|
|
id: "sensor-123",
|
|
type: "temperature",
|
|
oid: "1.3.6.1.4.1.14988.1.1.3.10.0",
|
|
divisor: 10.0,
|
|
unit: "celsius",
|
|
metadata: %{
|
|
"location" => "CPU Core 1",
|
|
"threshold" => "80"
|
|
}
|
|
}
|
|
|
|
encoded = Sensor.encode(sensor)
|
|
decoded = Sensor.decode(encoded)
|
|
|
|
assert decoded.id == sensor.id
|
|
assert decoded.type == sensor.type
|
|
assert decoded.oid == sensor.oid
|
|
assert decoded.divisor == sensor.divisor
|
|
assert decoded.unit == sensor.unit
|
|
assert decoded.metadata == sensor.metadata
|
|
end
|
|
|
|
test "encodes and decodes sensor without metadata" do
|
|
sensor = %Sensor{
|
|
id: "sensor-456",
|
|
type: "voltage",
|
|
oid: "1.3.6.1.4.1.14988.1.1.3.8.0",
|
|
divisor: 10.0,
|
|
unit: "volts"
|
|
}
|
|
|
|
encoded = Sensor.encode(sensor)
|
|
decoded = Sensor.decode(encoded)
|
|
|
|
assert decoded.id == sensor.id
|
|
assert decoded.type == sensor.type
|
|
assert decoded.oid == sensor.oid
|
|
assert decoded.divisor == sensor.divisor
|
|
assert decoded.unit == sensor.unit
|
|
end
|
|
|
|
test "encodes empty message" do
|
|
sensor = %Sensor{}
|
|
encoded = Sensor.encode(sensor)
|
|
decoded = Sensor.decode(encoded)
|
|
|
|
assert decoded.id == ""
|
|
assert decoded.type == ""
|
|
assert decoded.oid == ""
|
|
assert decoded.divisor == 0.0
|
|
assert decoded.unit == ""
|
|
assert decoded.metadata == %{}
|
|
end
|
|
end
|
|
|
|
describe "CheckResult" do
|
|
test "encodes and decodes success check result" do
|
|
result = %CheckResult{
|
|
check_id: @device_uuid,
|
|
status: 0,
|
|
output: "OK",
|
|
response_time_ms: 12.3,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = CheckResult.encode(result)
|
|
assert {:ok, decoded} = CheckResult.decode(encoded)
|
|
|
|
assert decoded.check_id == result.check_id
|
|
assert decoded.status == result.status
|
|
assert decoded.output == result.output
|
|
assert decoded.response_time_ms == result.response_time_ms
|
|
assert decoded.timestamp == result.timestamp
|
|
end
|
|
|
|
test "decodes empty check_id as error" do
|
|
result = %CheckResult{timestamp: 1_234_567_890}
|
|
encoded = CheckResult.encode(result)
|
|
assert {:error, _reason} = CheckResult.decode(encoded)
|
|
end
|
|
end
|
|
|
|
describe "CredentialTestResult" do
|
|
test "encodes and decodes success result" do
|
|
result = %CredentialTestResult{
|
|
test_id: @device_uuid,
|
|
success: true,
|
|
system_description: "Router OS 7.1",
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = CredentialTestResult.encode(result)
|
|
assert {:ok, decoded} = CredentialTestResult.decode(encoded)
|
|
|
|
assert decoded.test_id == result.test_id
|
|
assert decoded.success == result.success
|
|
assert decoded.system_description == result.system_description
|
|
assert decoded.timestamp == result.timestamp
|
|
end
|
|
|
|
test "encodes and decodes failure result" do
|
|
result = %CredentialTestResult{
|
|
test_id: @device_uuid,
|
|
success: false,
|
|
error_message: "Authentication failed",
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = CredentialTestResult.encode(result)
|
|
assert {:ok, decoded} = CredentialTestResult.decode(encoded)
|
|
|
|
assert decoded.test_id == result.test_id
|
|
assert decoded.success == false
|
|
assert decoded.error_message == result.error_message
|
|
end
|
|
|
|
test "decodes empty test_id as error" do
|
|
result = %CredentialTestResult{timestamp: 1_234_567_890}
|
|
encoded = CredentialTestResult.encode(result)
|
|
assert {:error, _reason} = CredentialTestResult.decode(encoded)
|
|
end
|
|
end
|
|
|
|
describe "MikrotikResult" do
|
|
test "encodes and decodes with sentences" do
|
|
result = %MikrotikResult{
|
|
device_id: @device_uuid,
|
|
job_id: @job_uuid,
|
|
sentences: [
|
|
%MikrotikSentence{attributes: %{"name" => "ether1", "mac-address" => "00:11:22:33:44:55"}},
|
|
%MikrotikSentence{attributes: %{"name" => "ether2", "mac-address" => "66:77:88:99:AA:BB"}}
|
|
],
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = MikrotikResult.encode(result)
|
|
assert {:ok, decoded} = MikrotikResult.decode(encoded)
|
|
|
|
assert decoded.device_id == result.device_id
|
|
assert decoded.job_id == result.job_id
|
|
assert length(decoded.sentences) == 2
|
|
assert hd(decoded.sentences).attributes["name"] == "ether1"
|
|
end
|
|
|
|
test "encodes and decodes with error" do
|
|
result = %MikrotikResult{
|
|
device_id: @device_uuid,
|
|
job_id: @job_uuid,
|
|
sentences: [],
|
|
error: "Connection refused",
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = MikrotikResult.encode(result)
|
|
assert {:ok, decoded} = MikrotikResult.decode(encoded)
|
|
|
|
assert decoded.error == result.error
|
|
end
|
|
|
|
test "decodes empty device_id as error" do
|
|
result = %MikrotikResult{job_id: @job_uuid, timestamp: 1_234_567_890}
|
|
encoded = MikrotikResult.encode(result)
|
|
assert {:error, _reason} = MikrotikResult.decode(encoded)
|
|
end
|
|
end
|
|
|
|
describe "LldpTopologyResult" do
|
|
test "decodes correctly with neighbors" do
|
|
result = %Towerops.Proto.Types.LldpTopologyResult{
|
|
device_id: @device_uuid,
|
|
job_id: @job_uuid,
|
|
local_system_name: "switch01",
|
|
neighbors: [
|
|
%LldpNeighbor{
|
|
neighbor_name: "router01",
|
|
local_port: "Eth1/0/1",
|
|
remote_port: "Gi0/1",
|
|
remote_port_id: "Gi0/1",
|
|
management_addresses: ["192.168.1.1"]
|
|
}
|
|
],
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = Encode.encode_lldp_topology_result(result)
|
|
decoded = LldpTopologyResult.decode(encoded)
|
|
|
|
assert {:ok, decoded_result} = decoded
|
|
assert decoded_result.device_id == result.device_id
|
|
assert decoded_result.job_id == result.job_id
|
|
assert decoded_result.local_system_name == result.local_system_name
|
|
assert length(decoded_result.neighbors) == 1
|
|
end
|
|
|
|
test "decodes empty device_id as error" do
|
|
result = %Towerops.Proto.Types.LldpTopologyResult{
|
|
device_id: "",
|
|
job_id: @job_uuid,
|
|
local_system_name: "",
|
|
neighbors: [],
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = Encode.encode_lldp_topology_result(result)
|
|
assert {:error, _reason} = LldpTopologyResult.decode(encoded)
|
|
end
|
|
end
|
|
|
|
describe "AgentJob with mikrotik" do
|
|
test "encodes job with mikrotik_device" do
|
|
job = %AgentJob{
|
|
job_id: "mikrotik:device-123",
|
|
job_type: :POLL,
|
|
device_id: "device-123",
|
|
mikrotik_device: %MikrotikDevice{
|
|
ip: "192.168.88.1",
|
|
port: 8728,
|
|
username: "admin",
|
|
password: "password",
|
|
use_ssl: false,
|
|
ssh_port: 22
|
|
},
|
|
mikrotik_commands: [
|
|
%MikrotikCommand{command: "/interface/print", args: %{}}
|
|
]
|
|
}
|
|
|
|
encoded = AgentJob.encode(job)
|
|
assert {:ok, decoded} = AgentJob.decode(encoded)
|
|
|
|
assert decoded.job_id == job.job_id
|
|
assert decoded.mikrotik_device.ip == "192.168.88.1"
|
|
assert decoded.mikrotik_device.port == 8728
|
|
assert hd(decoded.mikrotik_commands).command == "/interface/print"
|
|
end
|
|
end
|
|
|
|
describe "Validation error paths" do
|
|
test "heartbeat rejects version string that is too long" do
|
|
hb = %AgentHeartbeat{
|
|
version: String.duplicate("a", 256),
|
|
hostname: "test",
|
|
uptime_seconds: 60,
|
|
ip_address: "127.0.0.1"
|
|
}
|
|
|
|
encoded = AgentHeartbeat.encode(hb)
|
|
assert {:error, _reason} = AgentHeartbeat.decode(encoded)
|
|
end
|
|
|
|
test "heartbeat rejects uptime that is too large" do
|
|
hb = %AgentHeartbeat{
|
|
version: "1.0",
|
|
hostname: "test",
|
|
uptime_seconds: 5_000_000_000,
|
|
ip_address: "127.0.0.1"
|
|
}
|
|
|
|
encoded = AgentHeartbeat.encode(hb)
|
|
assert {:error, _reason} = AgentHeartbeat.decode(encoded)
|
|
end
|
|
|
|
test "agent error rejects message that is too long" do
|
|
error = %AgentError{
|
|
device_id: @device_uuid,
|
|
job_id: @job_uuid,
|
|
message: String.duplicate("a", 1001),
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = AgentError.encode(error)
|
|
assert {:error, _reason} = AgentError.decode(encoded)
|
|
end
|
|
|
|
test "monitoring check rejects response time that is negative" do
|
|
check = %MonitoringCheck{
|
|
device_id: @device_uuid,
|
|
status: "success",
|
|
response_time_ms: -1.0,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = MonitoringCheck.encode(check)
|
|
assert {:error, _reason} = MonitoringCheck.decode(encoded)
|
|
end
|
|
|
|
test "monitoring check rejects negative timestamp" do
|
|
check = %MonitoringCheck{
|
|
device_id: @device_uuid,
|
|
status: "success",
|
|
response_time_ms: 10.0,
|
|
timestamp: -1
|
|
}
|
|
|
|
encoded = MonitoringCheck.encode(check)
|
|
assert {:error, _reason} = MonitoringCheck.decode(encoded)
|
|
end
|
|
|
|
test "snmp result rejects device_id with invalid UUID format" do
|
|
result = %SnmpResult{
|
|
device_id: "not-a-uuid",
|
|
job_type: :POLL,
|
|
oid_values: %{},
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = SnmpResult.encode(result)
|
|
assert {:error, _reason} = SnmpResult.decode(encoded)
|
|
end
|
|
|
|
test "monitoring check rejects status string that is too long" do
|
|
check = %MonitoringCheck{
|
|
device_id: @device_uuid,
|
|
status: String.duplicate("a", 256),
|
|
response_time_ms: 10.0,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = MonitoringCheck.encode(check)
|
|
assert {:error, _reason} = MonitoringCheck.decode(encoded)
|
|
end
|
|
|
|
test "monitoring check rejects response time that is too large" do
|
|
check = %MonitoringCheck{
|
|
device_id: @device_uuid,
|
|
status: "success",
|
|
response_time_ms: 1_000_000,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = MonitoringCheck.encode(check)
|
|
assert {:error, _reason} = MonitoringCheck.decode(encoded)
|
|
end
|
|
|
|
test "credential test result rejects negative timestamp" do
|
|
result = %CredentialTestResult{
|
|
test_id: @device_uuid,
|
|
success: true,
|
|
timestamp: -1
|
|
}
|
|
|
|
encoded = CredentialTestResult.encode(result)
|
|
assert {:error, _reason} = CredentialTestResult.decode(encoded)
|
|
end
|
|
|
|
test "credential test result rejects too long system_description" do
|
|
result = %CredentialTestResult{
|
|
test_id: @device_uuid,
|
|
success: true,
|
|
system_description: String.duplicate("a", 10_001),
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = CredentialTestResult.encode(result)
|
|
assert {:error, _reason} = CredentialTestResult.decode(encoded)
|
|
end
|
|
|
|
test "check result rejects response time that is too large" do
|
|
result = %CheckResult{
|
|
check_id: @device_uuid,
|
|
status: 0,
|
|
output: "OK",
|
|
response_time_ms: 1_000_000,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = CheckResult.encode(result)
|
|
assert {:error, _reason} = CheckResult.decode(encoded)
|
|
end
|
|
|
|
test "check result rejects too long output" do
|
|
result = %CheckResult{
|
|
check_id: @device_uuid,
|
|
status: 0,
|
|
output: String.duplicate("a", 10_001),
|
|
response_time_ms: 10.0,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = CheckResult.encode(result)
|
|
assert {:error, _reason} = CheckResult.decode(encoded)
|
|
end
|
|
|
|
test "lldp topology result rejects too long local_system_name" do
|
|
result = %Towerops.Proto.Types.LldpTopologyResult{
|
|
device_id: @device_uuid,
|
|
job_id: @job_uuid,
|
|
local_system_name: String.duplicate("a", 10_001),
|
|
neighbors: [],
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = Encode.encode_lldp_topology_result(result)
|
|
assert {:error, _reason} = LldpTopologyResult.decode(encoded)
|
|
end
|
|
|
|
test "check result rejects negative timestamp" do
|
|
result = %CheckResult{
|
|
check_id: @device_uuid,
|
|
status: 0,
|
|
output: "OK",
|
|
response_time_ms: 10.0,
|
|
timestamp: -1
|
|
}
|
|
|
|
encoded = CheckResult.encode(result)
|
|
assert {:error, _reason} = CheckResult.decode(encoded)
|
|
end
|
|
|
|
test "mikrotik result rejects too many sentences" do
|
|
sentences =
|
|
Enum.map(1..1001, fn i ->
|
|
%MikrotikSentence{attributes: %{"name" => "item#{i}"}}
|
|
end)
|
|
|
|
result = %MikrotikResult{
|
|
device_id: @device_uuid,
|
|
job_id: @job_uuid,
|
|
sentences: sentences,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = MikrotikResult.encode(result)
|
|
assert {:error, _reason} = MikrotikResult.decode(encoded)
|
|
end
|
|
|
|
test "mikrotik result rejects too long error message" do
|
|
result = %MikrotikResult{
|
|
device_id: @device_uuid,
|
|
job_id: @job_uuid,
|
|
sentences: [],
|
|
error: String.duplicate("a", 1001),
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = MikrotikResult.encode(result)
|
|
assert {:error, _reason} = MikrotikResult.decode(encoded)
|
|
end
|
|
|
|
test "lldp topology result rejects too many neighbors" do
|
|
neighbors =
|
|
Enum.map(1..1001, fn i ->
|
|
%LldpNeighbor{
|
|
neighbor_name: "neighbor-#{i}",
|
|
local_port: "Eth1/0/1",
|
|
remote_port: "Gi0/1",
|
|
remote_port_id: "Gi0/1",
|
|
management_addresses: []
|
|
}
|
|
end)
|
|
|
|
result = %Towerops.Proto.Types.LldpTopologyResult{
|
|
device_id: @device_uuid,
|
|
job_id: @job_uuid,
|
|
local_system_name: "switch01",
|
|
neighbors: neighbors,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = Encode.encode_lldp_topology_result(result)
|
|
assert {:error, _reason} = LldpTopologyResult.decode(encoded)
|
|
end
|
|
|
|
test "agent error rejects too long timestamp" do
|
|
error = %AgentError{
|
|
device_id: @device_uuid,
|
|
job_id: @job_uuid,
|
|
message: "Error message",
|
|
timestamp: 5_000_000_000
|
|
}
|
|
|
|
encoded = AgentError.encode(error)
|
|
assert {:error, _reason} = AgentError.decode(encoded)
|
|
end
|
|
|
|
test "decodes string field with wrong wire type" do
|
|
bin = <<0x08, 0x01>>
|
|
assert {:error, _reason} = AgentHeartbeat.decode(bin)
|
|
end
|
|
|
|
test "decodes uint field with wrong wire type" do
|
|
bin = <<0x1A, 0x01, 0x00>>
|
|
assert {:error, _reason} = AgentHeartbeat.decode(bin)
|
|
end
|
|
|
|
test "decodes int64 field with wrong wire type" do
|
|
bin = <<0x22, 0x01, 0x00>>
|
|
assert {:error, _reason} = AgentError.decode(bin)
|
|
end
|
|
|
|
test "decodes double field with wrong wire type" do
|
|
bin = <<0x18, 0x00>>
|
|
assert {:error, _reason} = MonitoringCheck.decode(bin)
|
|
end
|
|
|
|
test "decodes bool field with wrong wire type" do
|
|
bin = <<0x12, 0x01, 0x00>>
|
|
assert {:error, _reason} = CredentialTestResult.decode(bin)
|
|
end
|
|
|
|
test "decodes enum field with wrong wire type" do
|
|
bin = <<0x12, 0x01, 0x00>>
|
|
assert {:error, _reason} = AgentJob.decode(bin)
|
|
end
|
|
end
|
|
|
|
describe "HeartbeatResponse" do
|
|
test "encodes and decodes status" do
|
|
response = %HeartbeatResponse{status: "ok"}
|
|
encoded = HeartbeatResponse.encode(response)
|
|
decoded = HeartbeatResponse.decode(encoded)
|
|
|
|
assert decoded.status == response.status
|
|
end
|
|
|
|
test "encodes empty message" do
|
|
response = %HeartbeatResponse{}
|
|
encoded = HeartbeatResponse.encode(response)
|
|
decoded = HeartbeatResponse.decode(encoded)
|
|
|
|
assert decoded.status == ""
|
|
end
|
|
end
|
|
|
|
describe "HeartbeatMetadata" do
|
|
test "encodes and decodes" do
|
|
hb = %HeartbeatMetadata{
|
|
version: "1.0.0",
|
|
hostname: "agent-host",
|
|
uptime_seconds: 3600
|
|
}
|
|
|
|
encoded = HeartbeatMetadata.encode(hb)
|
|
decoded = HeartbeatMetadata.decode(encoded)
|
|
|
|
assert decoded.version == hb.version
|
|
assert decoded.hostname == hb.hostname
|
|
assert decoded.uptime_seconds == hb.uptime_seconds
|
|
end
|
|
|
|
test "encodes empty message" do
|
|
encoded = HeartbeatMetadata.encode(%HeartbeatMetadata{})
|
|
decoded = HeartbeatMetadata.decode(encoded)
|
|
|
|
assert decoded.version == ""
|
|
assert decoded.hostname == ""
|
|
assert decoded.uptime_seconds == 0
|
|
end
|
|
end
|
|
|
|
describe "Decode.decode_agent_config" do
|
|
defp full_agent_config do
|
|
%AgentConfig{
|
|
version: "1.0.0",
|
|
poll_interval_seconds: 300,
|
|
devices: [
|
|
%Device{
|
|
id: "device-1",
|
|
name: "core-switch",
|
|
ip_address: "192.168.1.1",
|
|
snmp: %Towerops.Proto.Types.SnmpConfig{
|
|
enabled: true,
|
|
version: "2c",
|
|
community: "public",
|
|
port: 161,
|
|
transport: "udp"
|
|
},
|
|
poll_interval_seconds: 300,
|
|
sensors: [
|
|
%Towerops.Proto.Types.Sensor{
|
|
id: "sensor-1",
|
|
sensor_type: "temperature",
|
|
oid: "1.3.6.1.4.1.14988.1.1.3.10.0",
|
|
divisor: 10.0,
|
|
unit: "celsius",
|
|
metadata: %{"location" => "CPU Core 1"}
|
|
}
|
|
],
|
|
interfaces: [
|
|
%Towerops.Proto.Types.Interface{
|
|
id: "if-1",
|
|
if_index: 1,
|
|
if_name: "GigabitEthernet0/1"
|
|
}
|
|
],
|
|
monitoring_enabled: true,
|
|
check_interval_seconds: 60
|
|
}
|
|
],
|
|
checks: [
|
|
%Check{
|
|
id: "check-1",
|
|
check_type: "http",
|
|
interval_seconds: 60,
|
|
timeout_ms: 5000,
|
|
config:
|
|
{:http,
|
|
%HttpCheckConfig{
|
|
url: "https://example.com/health",
|
|
method: "GET",
|
|
expected_status: 200,
|
|
verify_ssl: true,
|
|
headers: %{},
|
|
body: "",
|
|
regex: "",
|
|
follow_redirects: false
|
|
}}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
test "encodes and decodes the config version and top-level structure" do
|
|
config = full_agent_config()
|
|
encoded = Encode.encode_agent_config(config)
|
|
assert {:ok, decoded} = Decode.decode_agent_config(encoded)
|
|
|
|
assert decoded.version == config.version
|
|
assert decoded.poll_interval_seconds == config.poll_interval_seconds
|
|
assert length(decoded.devices) == 1
|
|
assert length(decoded.checks) == 1
|
|
end
|
|
|
|
test "encodes and decodes device id, name, ip and snmp config" do
|
|
config = full_agent_config()
|
|
encoded = Encode.encode_agent_config(config)
|
|
assert {:ok, decoded} = Decode.decode_agent_config(encoded)
|
|
|
|
dev = hd(decoded.devices)
|
|
assert dev.id == "device-1"
|
|
assert dev.name == "core-switch"
|
|
assert dev.ip_address == "192.168.1.1"
|
|
assert dev.snmp.enabled == true
|
|
assert dev.snmp.version == "2c"
|
|
assert dev.snmp.community == "public"
|
|
assert dev.snmp.port == 161
|
|
end
|
|
|
|
test "encodes and decodes device sensors and interfaces" do
|
|
config = full_agent_config()
|
|
encoded = Encode.encode_agent_config(config)
|
|
assert {:ok, decoded} = Decode.decode_agent_config(encoded)
|
|
|
|
dev = hd(decoded.devices)
|
|
assert dev.poll_interval_seconds == 300
|
|
assert length(dev.sensors) == 1
|
|
assert hd(dev.sensors).id == "sensor-1"
|
|
assert hd(dev.sensors).sensor_type == "temperature"
|
|
assert hd(dev.sensors).divisor == 10.0
|
|
assert length(dev.interfaces) == 1
|
|
assert hd(dev.interfaces).if_index == 1
|
|
assert hd(dev.interfaces).if_name == "GigabitEthernet0/1"
|
|
assert dev.monitoring_enabled == true
|
|
assert dev.check_interval_seconds == 60
|
|
end
|
|
|
|
test "encodes and decodes the http check" do
|
|
config = full_agent_config()
|
|
encoded = Encode.encode_agent_config(config)
|
|
assert {:ok, decoded} = Decode.decode_agent_config(encoded)
|
|
|
|
chk = hd(decoded.checks)
|
|
assert chk.id == "check-1"
|
|
assert chk.check_type == "http"
|
|
assert chk.interval_seconds == 60
|
|
assert chk.timeout_ms == 5000
|
|
end
|
|
|
|
test "encodes and decodes config with empty devices and checks" do
|
|
config = %AgentConfig{
|
|
version: "2.0.0",
|
|
poll_interval_seconds: 120,
|
|
devices: [],
|
|
checks: []
|
|
}
|
|
|
|
encoded = Encode.encode_agent_config(config)
|
|
assert {:ok, decoded} = Decode.decode_agent_config(encoded)
|
|
|
|
assert decoded.version == "2.0.0"
|
|
assert decoded.poll_interval_seconds == 120
|
|
assert decoded.devices == []
|
|
assert decoded.checks == []
|
|
end
|
|
|
|
test "encodes and decodes config with device without snmp" do
|
|
config = %AgentConfig{
|
|
version: "1.0.0",
|
|
poll_interval_seconds: 300,
|
|
devices: [
|
|
%Device{
|
|
id: "device-nosnmp",
|
|
name: "non-snmp-device",
|
|
ip_address: "10.0.0.1",
|
|
snmp: nil,
|
|
poll_interval_seconds: 300,
|
|
sensors: [],
|
|
interfaces: [],
|
|
monitoring_enabled: false,
|
|
check_interval_seconds: 0
|
|
}
|
|
],
|
|
checks: []
|
|
}
|
|
|
|
encoded = Encode.encode_agent_config(config)
|
|
assert {:ok, decoded} = Decode.decode_agent_config(encoded)
|
|
|
|
assert length(decoded.devices) == 1
|
|
assert hd(decoded.devices).snmp == nil
|
|
assert hd(decoded.devices).monitoring_enabled == false
|
|
end
|
|
|
|
test "rejects wrong wire type for string field" do
|
|
# field 1 (version) expects wire type 2 (LEN), send wire type 0 (VARINT)
|
|
bin = <<0x08, 0x01>>
|
|
assert {:error, _reason} = Decode.decode_agent_config(bin)
|
|
end
|
|
end
|
|
|
|
describe "Decode.decode_agent_job_list" do
|
|
test "encodes and decodes a list of jobs" do
|
|
job_list = %Towerops.Proto.Types.AgentJobList{
|
|
jobs: [
|
|
%Towerops.Proto.Types.AgentJob{
|
|
job_id: "poll:device-1",
|
|
job_type: :poll,
|
|
device_id: "device-1",
|
|
snmp_device: %Towerops.Proto.Types.SnmpDevice{
|
|
ip: "192.168.1.1",
|
|
community: "public",
|
|
version: "2c",
|
|
port: 161,
|
|
v3_security_level: "",
|
|
v3_username: "",
|
|
v3_auth_protocol: "",
|
|
v3_auth_password: "",
|
|
v3_priv_protocol: "",
|
|
v3_priv_password: "",
|
|
transport: "udp"
|
|
},
|
|
queries: [
|
|
%Towerops.Proto.Types.SnmpQuery{
|
|
query_type: :get,
|
|
oids: ["1.3.6.1.2.1.1.1.0"]
|
|
}
|
|
],
|
|
mikrotik_device: nil,
|
|
mikrotik_commands: []
|
|
},
|
|
%Towerops.Proto.Types.AgentJob{
|
|
job_id: "discover:device-2",
|
|
job_type: :discover,
|
|
device_id: "device-2",
|
|
snmp_device: %Towerops.Proto.Types.SnmpDevice{
|
|
ip: "10.0.0.2",
|
|
community: "private",
|
|
version: "2c",
|
|
port: 161,
|
|
v3_security_level: "",
|
|
v3_username: "",
|
|
v3_auth_protocol: "",
|
|
v3_auth_password: "",
|
|
v3_priv_protocol: "",
|
|
v3_priv_password: "",
|
|
transport: ""
|
|
},
|
|
queries: [
|
|
%Towerops.Proto.Types.SnmpQuery{
|
|
query_type: :walk,
|
|
oids: ["1.3.6.1.2.1.2.2.1"]
|
|
}
|
|
],
|
|
mikrotik_device: nil,
|
|
mikrotik_commands: []
|
|
}
|
|
]
|
|
}
|
|
|
|
encoded = Encode.encode_agent_job_list(job_list)
|
|
assert {:ok, decoded} = Decode.decode_agent_job_list(encoded)
|
|
|
|
assert length(decoded.jobs) == 2
|
|
|
|
first = Enum.at(decoded.jobs, 0)
|
|
assert first.job_id == "poll:device-1"
|
|
assert first.job_type == :poll
|
|
assert first.device_id == "device-1"
|
|
assert first.snmp_device.ip == "192.168.1.1"
|
|
assert hd(first.queries).query_type == :get
|
|
|
|
second = Enum.at(decoded.jobs, 1)
|
|
assert second.job_id == "discover:device-2"
|
|
assert second.job_type == :discover
|
|
assert second.device_id == "device-2"
|
|
assert hd(second.queries).query_type == :walk
|
|
end
|
|
|
|
test "encodes and decodes empty job list" do
|
|
job_list = %Towerops.Proto.Types.AgentJobList{jobs: []}
|
|
encoded = Encode.encode_agent_job_list(job_list)
|
|
assert {:ok, decoded} = Decode.decode_agent_job_list(encoded)
|
|
|
|
assert decoded.jobs == []
|
|
end
|
|
|
|
test "encodes and decodes single job" do
|
|
job_list = %Towerops.Proto.Types.AgentJobList{
|
|
jobs: [
|
|
%Towerops.Proto.Types.AgentJob{
|
|
job_id: "single:device-1",
|
|
job_type: :poll,
|
|
device_id: "device-1",
|
|
snmp_device: nil,
|
|
queries: [],
|
|
mikrotik_device: nil,
|
|
mikrotik_commands: []
|
|
}
|
|
]
|
|
}
|
|
|
|
encoded = Encode.encode_agent_job_list(job_list)
|
|
assert {:ok, decoded} = Decode.decode_agent_job_list(encoded)
|
|
|
|
assert length(decoded.jobs) == 1
|
|
assert hd(decoded.jobs).job_id == "single:device-1"
|
|
end
|
|
|
|
test "rejects wrong wire type" do
|
|
bin = <<0x08, 0x01>>
|
|
assert {:error, _reason} = Decode.decode_agent_job_list(bin)
|
|
end
|
|
end
|
|
|
|
describe "Decode.decode_check_list" do
|
|
test "encodes and decodes a list of checks with http config" do
|
|
check_list = %CheckList{
|
|
checks: [
|
|
%Check{
|
|
id: "http-check-1",
|
|
check_type: "http",
|
|
interval_seconds: 60,
|
|
timeout_ms: 5000,
|
|
config:
|
|
{:http,
|
|
%HttpCheckConfig{
|
|
url: "https://example.com/ping",
|
|
method: "GET",
|
|
expected_status: 200,
|
|
verify_ssl: true,
|
|
headers: %{},
|
|
body: "",
|
|
regex: "OK",
|
|
follow_redirects: false
|
|
}}
|
|
}
|
|
]
|
|
}
|
|
|
|
encoded = Encode.encode_check_list(check_list)
|
|
assert {:ok, decoded} = Decode.decode_check_list(encoded)
|
|
|
|
assert length(decoded.checks) == 1
|
|
check = hd(decoded.checks)
|
|
assert check.id == "http-check-1"
|
|
assert check.check_type == "http"
|
|
assert check.interval_seconds == 60
|
|
assert check.timeout_ms == 5000
|
|
|
|
assert {:http, http_config} = check.config
|
|
assert http_config.url == "https://example.com/ping"
|
|
assert http_config.method == "GET"
|
|
assert http_config.expected_status == 200
|
|
assert http_config.verify_ssl == true
|
|
assert http_config.regex == "OK"
|
|
end
|
|
|
|
test "encodes and decodes checks with different config types" do
|
|
check_list = %CheckList{
|
|
checks: [
|
|
%Check{
|
|
id: "tcp-check-1",
|
|
check_type: "tcp",
|
|
interval_seconds: 120,
|
|
timeout_ms: 3000,
|
|
config:
|
|
{:tcp,
|
|
%Towerops.Proto.Types.TcpCheckConfig{
|
|
host: "smtp.example.com",
|
|
port: 25,
|
|
send: "",
|
|
expect: "220"
|
|
}}
|
|
},
|
|
%Check{
|
|
id: "dns-check-1",
|
|
check_type: "dns",
|
|
interval_seconds: 300,
|
|
timeout_ms: 5000,
|
|
config:
|
|
{:dns,
|
|
%Towerops.Proto.Types.DnsCheckConfig{
|
|
hostname: "example.com",
|
|
server: "8.8.8.8",
|
|
record_type: "A",
|
|
expected: "93.184.216.34"
|
|
}}
|
|
},
|
|
%Check{
|
|
id: "ssl-check-1",
|
|
check_type: "ssl",
|
|
interval_seconds: 86_400,
|
|
timeout_ms: 10_000,
|
|
config:
|
|
{:ssl,
|
|
%Towerops.Proto.Types.SslCheckConfig{
|
|
host: "example.com",
|
|
port: 443,
|
|
warning_days: 30
|
|
}}
|
|
}
|
|
]
|
|
}
|
|
|
|
encoded = Encode.encode_check_list(check_list)
|
|
assert {:ok, decoded} = Decode.decode_check_list(encoded)
|
|
|
|
assert length(decoded.checks) == 3
|
|
|
|
tcp = Enum.at(decoded.checks, 0)
|
|
assert tcp.id == "tcp-check-1"
|
|
assert {:tcp, tcp_conf} = tcp.config
|
|
assert tcp_conf.host == "smtp.example.com"
|
|
assert tcp_conf.port == 25
|
|
|
|
dns = Enum.at(decoded.checks, 1)
|
|
assert dns.id == "dns-check-1"
|
|
assert {:dns, dns_conf} = dns.config
|
|
assert dns_conf.hostname == "example.com"
|
|
assert dns_conf.server == "8.8.8.8"
|
|
|
|
ssl = Enum.at(decoded.checks, 2)
|
|
assert ssl.id == "ssl-check-1"
|
|
assert {:ssl, ssl_conf} = ssl.config
|
|
assert ssl_conf.host == "example.com"
|
|
assert ssl_conf.port == 443
|
|
assert ssl_conf.warning_days == 30
|
|
end
|
|
|
|
test "encodes and decodes empty check list" do
|
|
check_list = %CheckList{checks: []}
|
|
encoded = Encode.encode_check_list(check_list)
|
|
assert {:ok, decoded} = Decode.decode_check_list(encoded)
|
|
|
|
assert decoded.checks == []
|
|
end
|
|
|
|
test "encodes and decodes check with no_config" do
|
|
check_list = %CheckList{
|
|
checks: [
|
|
%Check{
|
|
id: "no-config-check",
|
|
check_type: "custom",
|
|
interval_seconds: 60,
|
|
timeout_ms: 1000,
|
|
config: :no_config
|
|
}
|
|
]
|
|
}
|
|
|
|
encoded = Encode.encode_check_list(check_list)
|
|
assert {:ok, decoded} = Decode.decode_check_list(encoded)
|
|
|
|
assert length(decoded.checks) == 1
|
|
assert hd(decoded.checks).config == :no_config
|
|
end
|
|
|
|
test "rejects wrong wire type" do
|
|
bin = <<0x08, 0x01>>
|
|
assert {:error, _reason} = Decode.decode_check_list(bin)
|
|
end
|
|
end
|
|
|
|
describe "Decode.decode_sensor" do
|
|
test "encodes and decodes sensor with metadata" do
|
|
sensor = %Towerops.Proto.Types.Sensor{
|
|
id: "sensor-cpu-temp",
|
|
sensor_type: "temperature",
|
|
oid: "1.3.6.1.4.1.14988.1.1.3.10.0",
|
|
divisor: 10.0,
|
|
unit: "celsius",
|
|
metadata: %{"location" => "CPU Core 1", "threshold" => "80"}
|
|
}
|
|
|
|
encoded = Encode.encode_sensor(sensor)
|
|
assert {:ok, decoded} = Decode.decode_sensor(encoded)
|
|
|
|
assert decoded.id == sensor.id
|
|
assert decoded.sensor_type == sensor.sensor_type
|
|
assert decoded.oid == sensor.oid
|
|
assert decoded.divisor == sensor.divisor
|
|
assert decoded.unit == sensor.unit
|
|
assert decoded.metadata == sensor.metadata
|
|
assert decoded.metadata["location"] == "CPU Core 1"
|
|
end
|
|
|
|
test "encodes and decodes sensor without metadata" do
|
|
sensor = %Towerops.Proto.Types.Sensor{
|
|
id: "sensor-voltage",
|
|
sensor_type: "voltage",
|
|
oid: "1.3.6.1.4.1.14988.1.1.3.8.0",
|
|
divisor: 1.0,
|
|
unit: "volts",
|
|
metadata: %{}
|
|
}
|
|
|
|
encoded = Encode.encode_sensor(sensor)
|
|
assert {:ok, decoded} = Decode.decode_sensor(encoded)
|
|
|
|
assert decoded.id == sensor.id
|
|
assert decoded.sensor_type == sensor.sensor_type
|
|
assert decoded.oid == sensor.oid
|
|
assert decoded.divisor == sensor.divisor
|
|
assert decoded.unit == sensor.unit
|
|
assert decoded.metadata == %{}
|
|
end
|
|
|
|
test "encodes and decodes empty sensor" do
|
|
sensor = %Towerops.Proto.Types.Sensor{
|
|
id: "",
|
|
sensor_type: "",
|
|
oid: "",
|
|
divisor: 0.0,
|
|
unit: "",
|
|
metadata: %{}
|
|
}
|
|
|
|
encoded = Encode.encode_sensor(sensor)
|
|
assert {:ok, decoded} = Decode.decode_sensor(encoded)
|
|
|
|
assert decoded.id == ""
|
|
assert decoded.sensor_type == ""
|
|
assert decoded.oid == ""
|
|
assert decoded.divisor == 0.0
|
|
assert decoded.unit == ""
|
|
assert decoded.metadata == %{}
|
|
end
|
|
|
|
test "encodes and decodes sensor with zero divisor" do
|
|
sensor = %Towerops.Proto.Types.Sensor{
|
|
id: "sensor-unscaled",
|
|
sensor_type: "counter",
|
|
oid: "1.3.6.1.2.1.2.2.1.10",
|
|
divisor: 0.0,
|
|
unit: "bytes",
|
|
metadata: %{}
|
|
}
|
|
|
|
encoded = Encode.encode_sensor(sensor)
|
|
assert {:ok, decoded} = Decode.decode_sensor(encoded)
|
|
|
|
assert decoded.divisor == 0.0
|
|
end
|
|
|
|
test "rejects wrong wire type for string field" do
|
|
# field 1 (id) expects wire type 2 (LEN), send wire type 0 (VARINT)
|
|
bin = <<0x08, 0x01>>
|
|
assert {:error, _reason} = Decode.decode_sensor(bin)
|
|
end
|
|
end
|
|
|
|
describe "Decode.decode_monitoring_check" do
|
|
test "encodes and decodes success check" do
|
|
check = %Towerops.Proto.Types.MonitoringCheck{
|
|
device_id: @device_uuid,
|
|
status: "success",
|
|
response_time_ms: 45.2,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = Encode.encode_monitoring_check(check)
|
|
assert {:ok, decoded} = Decode.decode_monitoring_check(encoded)
|
|
|
|
assert decoded.device_id == check.device_id
|
|
assert decoded.status == check.status
|
|
assert decoded.response_time_ms == check.response_time_ms
|
|
assert decoded.timestamp == check.timestamp
|
|
end
|
|
|
|
test "encodes and decodes failure check" do
|
|
check = %Towerops.Proto.Types.MonitoringCheck{
|
|
device_id: @device_uuid,
|
|
status: "failure",
|
|
response_time_ms: 0.0,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = Encode.encode_monitoring_check(check)
|
|
assert {:ok, decoded} = Decode.decode_monitoring_check(encoded)
|
|
|
|
assert decoded.device_id == check.device_id
|
|
assert decoded.status == check.status
|
|
assert decoded.response_time_ms == check.response_time_ms
|
|
assert decoded.timestamp == check.timestamp
|
|
end
|
|
|
|
test "decodes empty device_id as error" do
|
|
check = %Towerops.Proto.Types.MonitoringCheck{
|
|
device_id: "",
|
|
status: "success",
|
|
response_time_ms: 10.0,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = Encode.encode_monitoring_check(check)
|
|
assert {:error, _reason} = Decode.decode_monitoring_check(encoded)
|
|
end
|
|
|
|
test "decodes invalid device_id format as error" do
|
|
check = %Towerops.Proto.Types.MonitoringCheck{
|
|
device_id: "not-a-uuid",
|
|
status: "success",
|
|
response_time_ms: 10.0,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = Encode.encode_monitoring_check(check)
|
|
assert {:error, _reason} = Decode.decode_monitoring_check(encoded)
|
|
end
|
|
|
|
test "rejects status string that is too long" do
|
|
check = %Towerops.Proto.Types.MonitoringCheck{
|
|
device_id: @device_uuid,
|
|
status: String.duplicate("a", 256),
|
|
response_time_ms: 10.0,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = Encode.encode_monitoring_check(check)
|
|
assert {:error, _reason} = Decode.decode_monitoring_check(encoded)
|
|
end
|
|
|
|
test "rejects negative response time" do
|
|
check = %Towerops.Proto.Types.MonitoringCheck{
|
|
device_id: @device_uuid,
|
|
status: "success",
|
|
response_time_ms: -1.0,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = Encode.encode_monitoring_check(check)
|
|
assert {:error, _reason} = Decode.decode_monitoring_check(encoded)
|
|
end
|
|
|
|
test "rejects response time that is too large" do
|
|
check = %Towerops.Proto.Types.MonitoringCheck{
|
|
device_id: @device_uuid,
|
|
status: "success",
|
|
response_time_ms: 60_001,
|
|
timestamp: 1_234_567_890
|
|
}
|
|
|
|
encoded = Encode.encode_monitoring_check(check)
|
|
assert {:error, _reason} = Decode.decode_monitoring_check(encoded)
|
|
end
|
|
|
|
test "rejects negative timestamp" do
|
|
check = %Towerops.Proto.Types.MonitoringCheck{
|
|
device_id: @device_uuid,
|
|
status: "success",
|
|
response_time_ms: 10.0,
|
|
timestamp: -1
|
|
}
|
|
|
|
encoded = Encode.encode_monitoring_check(check)
|
|
assert {:error, _reason} = Decode.decode_monitoring_check(encoded)
|
|
end
|
|
|
|
test "rejects timestamp that is too large" do
|
|
check = %Towerops.Proto.Types.MonitoringCheck{
|
|
device_id: @device_uuid,
|
|
status: "success",
|
|
response_time_ms: 10.0,
|
|
timestamp: 2_147_483_648
|
|
}
|
|
|
|
encoded = Encode.encode_monitoring_check(check)
|
|
assert {:error, _reason} = Decode.decode_monitoring_check(encoded)
|
|
end
|
|
|
|
test "rejects wrong wire type for string field" do
|
|
# field 1 (device_id) expects wire type 2 (LEN), send wire type 0 (VARINT)
|
|
bin = <<0x08, 0x01>>
|
|
assert {:error, _reason} = Decode.decode_monitoring_check(bin)
|
|
end
|
|
|
|
test "rejects wrong wire type for double field" do
|
|
# field 3 (response_time_ms) expects wire type 1 (64-bit), send wire type 0 (VARINT)
|
|
bin = <<0x18, 0x00>>
|
|
assert {:error, _reason} = Decode.decode_monitoring_check(bin)
|
|
end
|
|
end
|
|
end
|