the version regex only allowed [a-zA-Z0-9.] in semver suffixes, which rejected hyphens from git-describe output (e.g. 0.1.0-5-gabcdef0). this caused every heartbeat to fail validation, preventing last_seen_at from updating, which led to agent status showing "warning" and eventual channel disconnect after 360s. also accept bare git SHAs and short identifiers like "dev".
1519 lines
44 KiB
Elixir
1519 lines
44 KiB
Elixir
defmodule Towerops.Agent.ValidatorTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Towerops.Agent.AgentError
|
|
alias Towerops.Agent.AgentHeartbeat
|
|
alias Towerops.Agent.CredentialTestResult
|
|
alias Towerops.Agent.InterfaceStat
|
|
alias Towerops.Agent.Metric
|
|
alias Towerops.Agent.MetricBatch
|
|
alias Towerops.Agent.MikrotikResult
|
|
alias Towerops.Agent.MikrotikSentence
|
|
alias Towerops.Agent.MonitoringCheck
|
|
alias Towerops.Agent.NeighborDiscovery
|
|
alias Towerops.Agent.SensorReading
|
|
alias Towerops.Agent.SnmpResult
|
|
alias Towerops.Agent.Validator
|
|
|
|
describe "validate_heartbeat/1" do
|
|
test "validates valid heartbeat" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.2.3",
|
|
hostname: "agent01.example.com",
|
|
uptime_seconds: 12_345,
|
|
ip_address: "192.168.1.100"
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, validated} = Validator.validate_heartbeat(binary)
|
|
assert validated.version == "1.2.3"
|
|
assert validated.hostname == "agent01.example.com"
|
|
end
|
|
|
|
test "validates heartbeat with IPv6 address" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "2.0.0",
|
|
hostname: "agent02",
|
|
uptime_seconds: 100,
|
|
ip_address: "2001:db8::1"
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, _} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "rejects invalid version format" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "not-semver",
|
|
hostname: "agent03",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:error, {:invalid_version, _}} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "rejects invalid hostname" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: "invalid..hostname",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:error, {:invalid_hostname, _}} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "rejects excessive uptime" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: "agent04",
|
|
uptime_seconds: 5_000_000_000,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:error, {:invalid_uptime, _}} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "rejects invalid IP address" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: "agent05",
|
|
uptime_seconds: 100,
|
|
ip_address: "999.999.999.999"
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:error, {:invalid_ip, _}} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "accepts empty IP address" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: "agent06",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, _} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "rejects malformed protobuf" do
|
|
invalid_binary = <<0, 1, 2, 3, 4, 5>>
|
|
assert {:error, {:decode_error, _}} = Validator.validate_heartbeat(invalid_binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_metric_batch/1" do
|
|
test "validates batch with sensor readings" do
|
|
reading = %SensorReading{
|
|
sensor_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
value: 23.5,
|
|
status: "ok",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:sensor_reading, reading}}
|
|
]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, validated} = Validator.validate_metric_batch(binary)
|
|
assert length(validated.metrics) == 1
|
|
end
|
|
|
|
test "validates batch with interface stats" do
|
|
stat = %InterfaceStat{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440001",
|
|
if_in_octets: 1000,
|
|
if_out_octets: 2000,
|
|
if_in_errors: 0,
|
|
if_out_errors: 0,
|
|
if_in_discards: 0,
|
|
if_out_discards: 0,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:interface_stat, stat}}
|
|
]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, _} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "validates batch with neighbor discovery" do
|
|
discovery = %NeighborDiscovery{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440002",
|
|
protocol: "lldp",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
remote_system_name: "switch01",
|
|
remote_system_description: "Cisco IOS",
|
|
remote_platform: "C9300",
|
|
remote_port_id: "Gi1/0/1",
|
|
remote_port_description: "uplink",
|
|
remote_address: "192.168.1.1",
|
|
remote_capabilities: ["bridge", "router"],
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:neighbor_discovery, discovery}}
|
|
]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, _} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "validates batch with monitoring check" do
|
|
check = %MonitoringCheck{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440003",
|
|
status: "success",
|
|
response_time_ms: 12.5,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:monitoring_check, check}}
|
|
]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, _} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects invalid sensor UUID" do
|
|
reading = %SensorReading{
|
|
sensor_id: "not-a-uuid",
|
|
value: 23.5,
|
|
status: "ok",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:sensor_reading, reading}}
|
|
]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_sensor_id, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects invalid status" do
|
|
reading = %SensorReading{
|
|
sensor_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
value: 23.5,
|
|
status: "invalid_status",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:sensor_reading, reading}}
|
|
]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_status, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects negative counters" do
|
|
stat = %InterfaceStat{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440001",
|
|
if_in_octets: -1000,
|
|
if_out_octets: 2000,
|
|
if_in_errors: 0,
|
|
if_out_errors: 0,
|
|
if_in_discards: 0,
|
|
if_out_discards: 0,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:interface_stat, stat}}
|
|
]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_counter, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects invalid protocol" do
|
|
discovery = %NeighborDiscovery{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440002",
|
|
protocol: "invalid",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
remote_system_name: "switch01",
|
|
remote_system_description: "",
|
|
remote_platform: "",
|
|
remote_port_id: "",
|
|
remote_port_description: "",
|
|
remote_address: "",
|
|
remote_capabilities: [],
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:neighbor_discovery, discovery}}
|
|
]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_protocol, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects excessive response time" do
|
|
check = %MonitoringCheck{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440003",
|
|
status: "success",
|
|
response_time_ms: 99_999.0,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:monitoring_check, check}}
|
|
]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_response_time, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects future timestamp" do
|
|
reading = %SensorReading{
|
|
sensor_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
value: 23.5,
|
|
status: "ok",
|
|
timestamp: 9_999_999_999
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:sensor_reading, reading}}
|
|
]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_timestamp, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects too many capabilities" do
|
|
discovery = %NeighborDiscovery{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440002",
|
|
protocol: "lldp",
|
|
remote_chassis_id: "",
|
|
remote_system_name: "",
|
|
remote_system_description: "",
|
|
remote_platform: "",
|
|
remote_port_id: "",
|
|
remote_port_description: "",
|
|
remote_address: "",
|
|
remote_capabilities: Enum.map(1..25, &"cap#{&1}"),
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:neighbor_discovery, discovery}}
|
|
]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:too_many_capabilities, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_snmp_result/1" do
|
|
test "validates valid SNMP result" do
|
|
result = %SnmpResult{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_type: 1,
|
|
oid_values: %{"1.3.6.1.2.1.1.1.0" => "Test Device"},
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = SnmpResult.encode(result)
|
|
assert {:ok, validated} = Validator.validate_snmp_result(binary)
|
|
assert validated.device_id == result.device_id
|
|
end
|
|
|
|
test "rejects invalid device UUID" do
|
|
result = %SnmpResult{
|
|
device_id: "not-a-uuid",
|
|
job_type: 1,
|
|
oid_values: %{},
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = SnmpResult.encode(result)
|
|
assert {:error, {:invalid_device_id, _}} = Validator.validate_snmp_result(binary)
|
|
end
|
|
|
|
test "rejects too many OID values" do
|
|
# Create map with more than max allowed OIDs (max is 50_000)
|
|
oid_values = Map.new(1..50_100, fn i -> {"1.3.6.1.#{i}", "value"} end)
|
|
|
|
result = %SnmpResult{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_type: 1,
|
|
oid_values: oid_values,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = SnmpResult.encode(result)
|
|
assert {:error, {:too_many_oids, _}} = Validator.validate_snmp_result(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_agent_error/1" do
|
|
test "validates valid agent error" do
|
|
error = %AgentError{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: "job123",
|
|
message: "Connection timeout"
|
|
}
|
|
|
|
binary = AgentError.encode(error)
|
|
assert {:ok, validated} = Validator.validate_agent_error(binary)
|
|
assert validated.message == "Connection timeout"
|
|
end
|
|
|
|
test "rejects excessively long error message" do
|
|
long_message = String.duplicate("error ", 250)
|
|
|
|
error = %AgentError{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: "job123",
|
|
message: long_message
|
|
}
|
|
|
|
binary = AgentError.encode(error)
|
|
assert {:error, {:string_too_long, _}} = Validator.validate_agent_error(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_credential_test_result/1" do
|
|
test "validates successful credential test" do
|
|
result = %CredentialTestResult{
|
|
test_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
success: true,
|
|
error_message: "",
|
|
system_description: "Cisco IOS Software",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = CredentialTestResult.encode(result)
|
|
assert {:ok, validated} = Validator.validate_credential_test_result(binary)
|
|
assert validated.success == true
|
|
end
|
|
|
|
test "validates failed credential test" do
|
|
result = %CredentialTestResult{
|
|
test_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
success: false,
|
|
error_message: "Authentication failed",
|
|
system_description: "",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = CredentialTestResult.encode(result)
|
|
assert {:ok, validated} = Validator.validate_credential_test_result(binary)
|
|
assert validated.success == false
|
|
end
|
|
|
|
test "rejects invalid test ID" do
|
|
result = %CredentialTestResult{
|
|
test_id: "invalid-id",
|
|
success: true,
|
|
error_message: "",
|
|
system_description: "",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = CredentialTestResult.encode(result)
|
|
assert {:error, {:invalid_test_id, _}} = Validator.validate_credential_test_result(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_mikrotik_result/1" do
|
|
test "validates valid MikroTik result" do
|
|
sentence = %MikrotikSentence{
|
|
attributes: %{"name" => "ether1", "running" => "true"}
|
|
}
|
|
|
|
result = %MikrotikResult{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: "job123",
|
|
sentences: [sentence],
|
|
error: "",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = MikrotikResult.encode(result)
|
|
assert {:ok, validated} = Validator.validate_mikrotik_result(binary)
|
|
assert length(validated.sentences) == 1
|
|
end
|
|
|
|
test "rejects too many sentences" do
|
|
sentences =
|
|
Enum.map(1..1100, fn _ ->
|
|
%MikrotikSentence{attributes: %{}}
|
|
end)
|
|
|
|
result = %MikrotikResult{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: "job123",
|
|
sentences: sentences,
|
|
error: "",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = MikrotikResult.encode(result)
|
|
assert {:error, {:too_many_sentences, _}} = Validator.validate_mikrotik_result(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_heartbeat/1 - hostname length" do
|
|
test "rejects hostname exceeding 255 characters" do
|
|
long_hostname = String.duplicate("a", 256)
|
|
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: long_hostname,
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:error, {:invalid_hostname, "Hostname too long"}} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "accepts hostname at exactly 255 characters" do
|
|
# Build a valid RFC 1123 hostname at max length: segments like "a" joined by dots
|
|
# Each segment must start/end with alnum, max 63 chars
|
|
segment = String.duplicate("a", 63)
|
|
# 63 * 4 + 3 dots = 255
|
|
hostname = Enum.join([segment, segment, segment, segment], ".")
|
|
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: hostname,
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, _} = Validator.validate_heartbeat(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_heartbeat/1 - version edge cases" do
|
|
test "accepts version with pre-release suffix" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.2.3-beta.1",
|
|
hostname: "agent01",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, _} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "accepts git-describe version with commit count and SHA" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "0.1.0-5-gabcdef0",
|
|
hostname: "agent01",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, validated} = Validator.validate_heartbeat(binary)
|
|
assert validated.version == "0.1.0-5-gabcdef0"
|
|
end
|
|
|
|
test "accepts bare git short SHA as version" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "abcdef0",
|
|
hostname: "agent01",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, _} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "accepts 'dev' as version" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "dev",
|
|
hostname: "agent01",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, _} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "accepts RFC 3339 timestamp version format" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "2025-02-09T15:30:45Z",
|
|
hostname: "agent01",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, validated} = Validator.validate_heartbeat(binary)
|
|
assert validated.version == "2025-02-09T15:30:45Z"
|
|
end
|
|
|
|
test "accepts RFC 3339 timestamp with different date/time" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "2026-12-31T23:59:59Z",
|
|
hostname: "agent02",
|
|
uptime_seconds: 200,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, _} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "rejects RFC 3339 timestamp without Z suffix" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "2025-02-09T15:30:45",
|
|
hostname: "agent01",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:error, {:invalid_version, _}} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "rejects RFC 3339 timestamp with offset instead of Z" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "2025-02-09T15:30:45+00:00",
|
|
hostname: "agent01",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:error, {:invalid_version, _}} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "rejects version with spaces" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.2 .3",
|
|
hostname: "agent01",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:error, {:invalid_version, _}} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "rejects version exceeding 255 characters" do
|
|
long_version = "1.2.3-" <> String.duplicate("a", 250)
|
|
|
|
heartbeat = %AgentHeartbeat{
|
|
version: long_version,
|
|
hostname: "agent01",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:error, {:invalid_version, _}} = Validator.validate_heartbeat(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_heartbeat/1 - uptime boundary" do
|
|
test "accepts uptime at max value (4_294_967_295)" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: "agent01",
|
|
uptime_seconds: 4_294_967_295,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, _} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "rejects uptime at max value + 1" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: "agent01",
|
|
uptime_seconds: 4_294_967_296,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:error, {:invalid_uptime, _}} = Validator.validate_heartbeat(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_metric_batch/1 - empty status" do
|
|
test "accepts empty status on sensor reading (backward compat)" do
|
|
reading = %SensorReading{
|
|
sensor_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
value: 23.5,
|
|
status: "",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:sensor_reading, reading}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, _} = Validator.validate_metric_batch(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_metric_batch/1 - all valid statuses" do
|
|
for status <- ["success", "failure", "up", "down", "ok", "error", "warning"] do
|
|
test "accepts status '#{status}'" do
|
|
reading = %SensorReading{
|
|
sensor_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
value: 23.5,
|
|
status: unquote(status),
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:sensor_reading, reading}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, _} = Validator.validate_metric_batch(binary)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "validate_metric_batch/1 - neighbor discovery string lengths" do
|
|
test "rejects remote_chassis_id exceeding 255 characters" do
|
|
discovery = %NeighborDiscovery{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440002",
|
|
protocol: "lldp",
|
|
remote_chassis_id: String.duplicate("x", 256),
|
|
remote_system_name: "",
|
|
remote_system_description: "",
|
|
remote_platform: "",
|
|
remote_port_id: "",
|
|
remote_port_description: "",
|
|
remote_address: "",
|
|
remote_capabilities: [],
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:neighbor_discovery, discovery}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:string_too_long, msg}} = Validator.validate_metric_batch(binary)
|
|
assert msg =~ "remote_chassis_id"
|
|
end
|
|
|
|
test "rejects remote_system_description exceeding 10_000 characters" do
|
|
discovery = %NeighborDiscovery{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440002",
|
|
protocol: "lldp",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
remote_system_name: "switch01",
|
|
remote_system_description: String.duplicate("d", 10_001),
|
|
remote_platform: "",
|
|
remote_port_id: "",
|
|
remote_port_description: "",
|
|
remote_address: "",
|
|
remote_capabilities: [],
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:neighbor_discovery, discovery}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:string_too_long, msg}} = Validator.validate_metric_batch(binary)
|
|
assert msg =~ "remote_system_description"
|
|
end
|
|
|
|
test "accepts cdp protocol" do
|
|
discovery = %NeighborDiscovery{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440002",
|
|
protocol: "cdp",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
remote_system_name: "switch01",
|
|
remote_system_description: "",
|
|
remote_platform: "",
|
|
remote_port_id: "",
|
|
remote_port_description: "",
|
|
remote_address: "",
|
|
remote_capabilities: [],
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:neighbor_discovery, discovery}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, _} = Validator.validate_metric_batch(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_metric_batch/1 - monitoring check edge cases" do
|
|
test "rejects negative response time" do
|
|
check = %MonitoringCheck{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440003",
|
|
status: "success",
|
|
response_time_ms: -1.0,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:monitoring_check, check}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_response_time, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "accepts response time at max boundary (60_000ms)" do
|
|
check = %MonitoringCheck{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440003",
|
|
status: "success",
|
|
response_time_ms: 60_000.0,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:monitoring_check, check}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, _} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects response time just over max boundary" do
|
|
check = %MonitoringCheck{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440003",
|
|
status: "success",
|
|
response_time_ms: 60_001.0,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:monitoring_check, check}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_response_time, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "accepts zero response time" do
|
|
check = %MonitoringCheck{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440003",
|
|
status: "up",
|
|
response_time_ms: 0.0,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:monitoring_check, check}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, _} = Validator.validate_metric_batch(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_metric_batch/1 - timestamp boundaries" do
|
|
test "accepts timestamp at zero" do
|
|
reading = %SensorReading{
|
|
sensor_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
value: 1.0,
|
|
status: "ok",
|
|
timestamp: 0
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:sensor_reading, reading}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, _} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "accepts timestamp at max value (2_147_483_647)" do
|
|
reading = %SensorReading{
|
|
sensor_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
value: 1.0,
|
|
status: "ok",
|
|
timestamp: 2_147_483_647
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:sensor_reading, reading}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, _} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects timestamp at max value + 1" do
|
|
reading = %SensorReading{
|
|
sensor_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
value: 1.0,
|
|
status: "ok",
|
|
timestamp: 2_147_483_648
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:sensor_reading, reading}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_timestamp, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects negative timestamp" do
|
|
reading = %SensorReading{
|
|
sensor_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
value: 1.0,
|
|
status: "ok",
|
|
timestamp: -1
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:sensor_reading, reading}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_timestamp, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_metric_batch/1 - empty batch" do
|
|
test "accepts empty metrics list" do
|
|
batch = %MetricBatch{metrics: []}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, validated} = Validator.validate_metric_batch(binary)
|
|
assert validated.metrics == []
|
|
end
|
|
end
|
|
|
|
describe "validate_metric_batch/1 - multiple metrics" do
|
|
test "validates batch with multiple valid metrics of different types" do
|
|
reading = %SensorReading{
|
|
sensor_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
value: 23.5,
|
|
status: "ok",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
stat = %InterfaceStat{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440001",
|
|
if_in_octets: 1000,
|
|
if_out_octets: 2000,
|
|
if_in_errors: 0,
|
|
if_out_errors: 0,
|
|
if_in_discards: 0,
|
|
if_out_discards: 0,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:sensor_reading, reading}},
|
|
%Metric{metric_type: {:interface_stat, stat}}
|
|
]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, validated} = Validator.validate_metric_batch(binary)
|
|
assert length(validated.metrics) == 2
|
|
end
|
|
|
|
test "rejects batch when second metric is invalid" do
|
|
valid_reading = %SensorReading{
|
|
sensor_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
value: 23.5,
|
|
status: "ok",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
invalid_reading = %SensorReading{
|
|
sensor_id: "not-a-uuid",
|
|
value: 1.0,
|
|
status: "ok",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [
|
|
%Metric{metric_type: {:sensor_reading, valid_reading}},
|
|
%Metric{metric_type: {:sensor_reading, invalid_reading}}
|
|
]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_sensor_id, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_snmp_result/1 - timestamp" do
|
|
test "rejects SNMP result with negative timestamp" do
|
|
result = %SnmpResult{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_type: 1,
|
|
oid_values: %{},
|
|
timestamp: -1
|
|
}
|
|
|
|
binary = SnmpResult.encode(result)
|
|
assert {:error, {:invalid_timestamp, _}} = Validator.validate_snmp_result(binary)
|
|
end
|
|
|
|
test "rejects SNMP result with timestamp exceeding max" do
|
|
result = %SnmpResult{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_type: 1,
|
|
oid_values: %{},
|
|
timestamp: 2_147_483_648
|
|
}
|
|
|
|
binary = SnmpResult.encode(result)
|
|
assert {:error, {:invalid_timestamp, _}} = Validator.validate_snmp_result(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_agent_error/1 - edge cases" do
|
|
test "rejects empty job_id" do
|
|
error = %AgentError{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: "",
|
|
message: "Some error"
|
|
}
|
|
|
|
binary = AgentError.encode(error)
|
|
assert {:error, {:invalid_job_id, _}} = Validator.validate_agent_error(binary)
|
|
end
|
|
|
|
test "accepts job_id at 255 characters" do
|
|
error = %AgentError{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: String.duplicate("j", 255),
|
|
message: "Some error"
|
|
}
|
|
|
|
binary = AgentError.encode(error)
|
|
assert {:ok, _} = Validator.validate_agent_error(binary)
|
|
end
|
|
|
|
test "rejects job_id exceeding 255 characters" do
|
|
error = %AgentError{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: String.duplicate("j", 256),
|
|
message: "Some error"
|
|
}
|
|
|
|
binary = AgentError.encode(error)
|
|
assert {:error, {:invalid_job_id, _}} = Validator.validate_agent_error(binary)
|
|
end
|
|
|
|
test "accepts empty error message" do
|
|
error = %AgentError{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: "job123",
|
|
message: ""
|
|
}
|
|
|
|
binary = AgentError.encode(error)
|
|
assert {:ok, _} = Validator.validate_agent_error(binary)
|
|
end
|
|
|
|
test "accepts error message at exactly 1000 characters" do
|
|
error = %AgentError{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: "job123",
|
|
message: String.duplicate("e", 1000)
|
|
}
|
|
|
|
binary = AgentError.encode(error)
|
|
assert {:ok, _} = Validator.validate_agent_error(binary)
|
|
end
|
|
|
|
test "rejects error message at 1001 characters" do
|
|
error = %AgentError{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: "job123",
|
|
message: String.duplicate("e", 1001)
|
|
}
|
|
|
|
binary = AgentError.encode(error)
|
|
assert {:error, {:string_too_long, _}} = Validator.validate_agent_error(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_credential_test_result/1 - system description length" do
|
|
test "rejects system description exceeding 10_000 characters" do
|
|
result = %CredentialTestResult{
|
|
test_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
success: true,
|
|
error_message: "",
|
|
system_description: String.duplicate("d", 10_001),
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = CredentialTestResult.encode(result)
|
|
assert {:error, {:string_too_long, msg}} = Validator.validate_credential_test_result(binary)
|
|
assert msg =~ "System description"
|
|
end
|
|
|
|
test "accepts system description at exactly 10_000 characters" do
|
|
result = %CredentialTestResult{
|
|
test_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
success: true,
|
|
error_message: "",
|
|
system_description: String.duplicate("d", 10_000),
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = CredentialTestResult.encode(result)
|
|
assert {:ok, _} = Validator.validate_credential_test_result(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_credential_test_result/1 - error message length" do
|
|
test "rejects error message exceeding 1000 characters" do
|
|
result = %CredentialTestResult{
|
|
test_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
success: false,
|
|
error_message: String.duplicate("e", 1001),
|
|
system_description: "",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = CredentialTestResult.encode(result)
|
|
assert {:error, {:string_too_long, _}} = Validator.validate_credential_test_result(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_credential_test_result/1 - timestamp" do
|
|
test "rejects invalid timestamp" do
|
|
result = %CredentialTestResult{
|
|
test_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
success: true,
|
|
error_message: "",
|
|
system_description: "",
|
|
timestamp: 9_999_999_999
|
|
}
|
|
|
|
binary = CredentialTestResult.encode(result)
|
|
assert {:error, {:invalid_timestamp, _}} = Validator.validate_credential_test_result(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_mikrotik_result/1 - edge cases" do
|
|
test "rejects invalid device_id" do
|
|
result = %MikrotikResult{
|
|
device_id: "not-valid",
|
|
job_id: "job123",
|
|
sentences: [],
|
|
error: "",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = MikrotikResult.encode(result)
|
|
assert {:error, {:invalid_device_id, _}} = Validator.validate_mikrotik_result(binary)
|
|
end
|
|
|
|
test "rejects empty job_id" do
|
|
result = %MikrotikResult{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: "",
|
|
sentences: [],
|
|
error: "",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = MikrotikResult.encode(result)
|
|
assert {:error, {:invalid_job_id, _}} = Validator.validate_mikrotik_result(binary)
|
|
end
|
|
|
|
test "rejects error message exceeding 1000 characters" do
|
|
result = %MikrotikResult{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: "job123",
|
|
sentences: [],
|
|
error: String.duplicate("e", 1001),
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = MikrotikResult.encode(result)
|
|
assert {:error, {:string_too_long, _}} = Validator.validate_mikrotik_result(binary)
|
|
end
|
|
|
|
test "rejects invalid timestamp" do
|
|
result = %MikrotikResult{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: "job123",
|
|
sentences: [],
|
|
error: "",
|
|
timestamp: 9_999_999_999
|
|
}
|
|
|
|
binary = MikrotikResult.encode(result)
|
|
assert {:error, {:invalid_timestamp, _}} = Validator.validate_mikrotik_result(binary)
|
|
end
|
|
|
|
test "accepts empty sentences list" do
|
|
result = %MikrotikResult{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: "job123",
|
|
sentences: [],
|
|
error: "",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = MikrotikResult.encode(result)
|
|
assert {:ok, _} = Validator.validate_mikrotik_result(binary)
|
|
end
|
|
|
|
test "accepts sentences at exactly 1000" do
|
|
sentences = Enum.map(1..1000, fn _ -> %MikrotikSentence{attributes: %{}} end)
|
|
|
|
result = %MikrotikResult{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_id: "job123",
|
|
sentences: sentences,
|
|
error: "",
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = MikrotikResult.encode(result)
|
|
assert {:ok, _} = Validator.validate_mikrotik_result(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_metric_batch/1 - interface stat counter boundaries" do
|
|
test "rejects negative if_out_errors" do
|
|
stat = %InterfaceStat{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440001",
|
|
if_in_octets: 0,
|
|
if_out_octets: 0,
|
|
if_in_errors: 0,
|
|
if_out_errors: -1,
|
|
if_in_discards: 0,
|
|
if_out_discards: 0,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:interface_stat, stat}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_counter, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects negative if_in_discards" do
|
|
stat = %InterfaceStat{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440001",
|
|
if_in_octets: 0,
|
|
if_out_octets: 0,
|
|
if_in_errors: 0,
|
|
if_out_errors: 0,
|
|
if_in_discards: -5,
|
|
if_out_discards: 0,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:interface_stat, stat}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_counter, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects negative if_out_discards" do
|
|
stat = %InterfaceStat{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440001",
|
|
if_in_octets: 0,
|
|
if_out_octets: 0,
|
|
if_in_errors: 0,
|
|
if_out_errors: 0,
|
|
if_in_discards: 0,
|
|
if_out_discards: -1,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:interface_stat, stat}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_counter, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects invalid interface UUID" do
|
|
stat = %InterfaceStat{
|
|
interface_id: "bad-uuid",
|
|
if_in_octets: 0,
|
|
if_out_octets: 0,
|
|
if_in_errors: 0,
|
|
if_out_errors: 0,
|
|
if_in_discards: 0,
|
|
if_out_discards: 0,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:interface_stat, stat}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_interface_id, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_metric_batch/1 - neighbor discovery with remote IPv6" do
|
|
test "accepts remote_address as IPv6" do
|
|
discovery = %NeighborDiscovery{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440002",
|
|
protocol: "lldp",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
remote_system_name: "switch01",
|
|
remote_system_description: "",
|
|
remote_platform: "",
|
|
remote_port_id: "",
|
|
remote_port_description: "",
|
|
remote_address: "fe80::1",
|
|
remote_capabilities: [],
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:neighbor_discovery, discovery}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, _} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "rejects invalid remote_address IP" do
|
|
discovery = %NeighborDiscovery{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440002",
|
|
protocol: "lldp",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
remote_system_name: "switch01",
|
|
remote_system_description: "",
|
|
remote_platform: "",
|
|
remote_port_id: "",
|
|
remote_port_description: "",
|
|
remote_address: "not-an-ip",
|
|
remote_capabilities: [],
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:neighbor_discovery, discovery}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_ip, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_metric_batch/1 - monitoring check with invalid device_id" do
|
|
test "rejects monitoring check with non-UUID device_id" do
|
|
check = %MonitoringCheck{
|
|
device_id: "not-valid",
|
|
status: "success",
|
|
response_time_ms: 1.0,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:monitoring_check, check}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:error, {:invalid_device_id, _}} = Validator.validate_metric_batch(binary)
|
|
end
|
|
end
|
|
|
|
describe "validate_snmp_result/1 - malformed protobuf" do
|
|
test "rejects malformed protobuf binary" do
|
|
assert {:error, {:decode_error, _}} = Validator.validate_snmp_result(<<255, 255, 255>>)
|
|
end
|
|
end
|
|
|
|
describe "validate_agent_error/1 - malformed protobuf" do
|
|
test "rejects malformed protobuf binary" do
|
|
assert {:error, {:decode_error, _}} = Validator.validate_agent_error(<<255, 255, 255>>)
|
|
end
|
|
end
|
|
|
|
describe "validate_credential_test_result/1 - malformed protobuf" do
|
|
test "rejects malformed protobuf binary" do
|
|
assert {:error, {:decode_error, _}} = Validator.validate_credential_test_result(<<255, 255, 255>>)
|
|
end
|
|
end
|
|
|
|
describe "validate_mikrotik_result/1 - malformed protobuf" do
|
|
test "rejects malformed protobuf binary" do
|
|
assert {:error, {:decode_error, _}} = Validator.validate_mikrotik_result(<<255, 255, 255>>)
|
|
end
|
|
end
|
|
|
|
describe "validate_metric_batch/1 - malformed protobuf" do
|
|
test "rejects malformed protobuf binary" do
|
|
assert {:error, {:decode_error, _}} = Validator.validate_metric_batch(<<255, 255, 255>>)
|
|
end
|
|
end
|
|
|
|
describe "edge cases" do
|
|
test "handles empty strings gracefully" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "",
|
|
hostname: "",
|
|
uptime_seconds: 0,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
# Empty version should be accepted (backward compat)
|
|
assert {:ok, _} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "handles zero values" do
|
|
stat = %InterfaceStat{
|
|
interface_id: "550e8400-e29b-41d4-a716-446655440001",
|
|
if_in_octets: 0,
|
|
if_out_octets: 0,
|
|
if_in_errors: 0,
|
|
if_out_errors: 0,
|
|
if_in_discards: 0,
|
|
if_out_discards: 0,
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
batch = %MetricBatch{
|
|
metrics: [%Metric{metric_type: {:interface_stat, stat}}]
|
|
}
|
|
|
|
binary = MetricBatch.encode(batch)
|
|
assert {:ok, _} = Validator.validate_metric_batch(binary)
|
|
end
|
|
|
|
test "handles empty collections" do
|
|
result = %SnmpResult{
|
|
device_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
job_type: 1,
|
|
oid_values: %{},
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = SnmpResult.encode(result)
|
|
assert {:ok, _} = Validator.validate_snmp_result(binary)
|
|
end
|
|
|
|
test "accepts UUID with uppercase hex characters" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: "agent01",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, _} = Validator.validate_heartbeat(binary)
|
|
|
|
# Test with uppercase UUID in device_id
|
|
result = %SnmpResult{
|
|
device_id: "550E8400-E29B-41D4-A716-446655440000",
|
|
job_type: 1,
|
|
oid_values: %{},
|
|
timestamp: 1_700_000_000
|
|
}
|
|
|
|
binary = SnmpResult.encode(result)
|
|
assert {:ok, _} = Validator.validate_snmp_result(binary)
|
|
end
|
|
|
|
test "handles hostname with hyphens" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: "my-agent-01",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:ok, _} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "rejects hostname starting with hyphen" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: "-invalid",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:error, {:invalid_hostname, _}} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "rejects hostname ending with hyphen" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: "invalid-",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:error, {:invalid_hostname, _}} = Validator.validate_heartbeat(binary)
|
|
end
|
|
|
|
test "rejects hostname with special characters" do
|
|
heartbeat = %AgentHeartbeat{
|
|
version: "1.0.0",
|
|
hostname: "host_name@bad",
|
|
uptime_seconds: 100,
|
|
ip_address: ""
|
|
}
|
|
|
|
binary = AgentHeartbeat.encode(heartbeat)
|
|
assert {:error, {:invalid_hostname, _}} = Validator.validate_heartbeat(binary)
|
|
end
|
|
end
|
|
end
|