more tests
This commit is contained in:
parent
6e57918386
commit
b8aecf0a55
8 changed files with 890 additions and 345 deletions
|
|
@ -39,7 +39,7 @@ services:
|
|||
image: gmcintire/towerops-agent:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TOWEROPS_API_URL=https://app.towerops.com
|
||||
- TOWEROPS_API_URL=https://towerops.net
|
||||
- TOWEROPS_AGENT_TOKEN=your-agent-token-here
|
||||
- LOG_LEVEL=info
|
||||
# Optional: Enable SNMP trap listener
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ services:
|
|||
|
||||
environment:
|
||||
# Required: Your Towerops API URL
|
||||
- TOWEROPS_API_URL=https://app.towerops.com
|
||||
- TOWEROPS_API_URL=https://towerops.net
|
||||
|
||||
# Required: Agent authentication token (from Towerops web UI)
|
||||
# Get this from: Organization > Agents > Create New Agent
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
mod storage;
|
||||
|
||||
pub use storage::{Storage, StorageError};
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
use crate::metrics::{Metric, Timestamp};
|
||||
use rusqlite::{params, Connection};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum StorageError {
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] rusqlite::Error),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, StorageError>;
|
||||
|
||||
/// SQLite storage for buffering metrics when API is unavailable
|
||||
#[derive(Clone)]
|
||||
pub struct Storage {
|
||||
conn: Arc<Mutex<Connection>>,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
/// Create a new storage instance
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let conn = Connection::open(path)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS metrics (
|
||||
id INTEGER PRIMARY KEY,
|
||||
metric_type TEXT NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
sent INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_metrics_sent ON metrics(sent, created_at)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS last_poll_times (
|
||||
equipment_id TEXT PRIMARY KEY,
|
||||
last_poll_time TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Store a metric in the buffer
|
||||
pub fn store_metric(&self, metric: &Metric) -> Result<()> {
|
||||
let data = serde_json::to_string(metric)?;
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO metrics (metric_type, data, timestamp) VALUES (?1, ?2, ?3)",
|
||||
params![metric.metric_type(), data, metric.timestamp().to_rfc3339()],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get pending metrics that haven't been sent yet
|
||||
pub fn get_pending_metrics(&self, limit: usize) -> Result<Vec<(i64, Metric)>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, data FROM metrics WHERE sent = 0 ORDER BY created_at LIMIT ?1")?;
|
||||
|
||||
let metrics = stmt
|
||||
.query_map([limit], |row| {
|
||||
let id: i64 = row.get(0)?;
|
||||
let data: String = row.get(1)?;
|
||||
Ok((id, data))
|
||||
})?
|
||||
.filter_map(|r| r.ok())
|
||||
.filter_map(|(id, data)| {
|
||||
serde_json::from_str::<Metric>(&data)
|
||||
.ok()
|
||||
.map(|metric| (id, metric))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(metrics)
|
||||
}
|
||||
|
||||
/// Mark metrics as sent
|
||||
pub fn mark_metrics_sent(&self, ids: &[i64]) -> Result<()> {
|
||||
if ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let placeholders = ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
|
||||
let query = format!("UPDATE metrics SET sent = 1 WHERE id IN ({})", placeholders);
|
||||
|
||||
let params: Vec<_> = ids.iter().map(|id| id as &dyn rusqlite::ToSql).collect();
|
||||
conn.execute(&query, params.as_slice())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clean up old metrics that have been sent
|
||||
pub fn cleanup_old_metrics(&self) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
conn.execute(
|
||||
"DELETE FROM metrics WHERE sent = 1 AND created_at < datetime('now', '-24 hours')",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the last poll time for equipment
|
||||
#[allow(dead_code)] // Used in full SNMP implementation
|
||||
pub fn get_last_poll_time(&self, equipment_id: &str) -> Result<Option<Timestamp>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let result: std::result::Result<String, _> = conn.query_row(
|
||||
"SELECT last_poll_time FROM last_poll_times WHERE equipment_id = ?1",
|
||||
params![equipment_id],
|
||||
|row| row.get(0),
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(_timestamp_str) => {
|
||||
// Return a Timestamp - parsing from string not implemented yet
|
||||
// For now just return None since this function isn't used yet
|
||||
Ok(None)
|
||||
}
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the last poll time for equipment
|
||||
pub fn update_last_poll_time(&self, equipment_id: &str) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let now = Timestamp::now().to_rfc3339();
|
||||
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO last_poll_times (equipment_id, last_poll_time) VALUES (?1, ?2)",
|
||||
params![equipment_id, now],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all last poll times
|
||||
pub fn get_all_last_poll_times(&self) -> Result<std::collections::HashMap<String, Timestamp>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let mut stmt = conn.prepare("SELECT equipment_id, last_poll_time FROM last_poll_times")?;
|
||||
|
||||
let times: std::collections::HashMap<String, Timestamp> = stmt
|
||||
.query_map([], |row| {
|
||||
let equipment_id: String = row.get(0)?;
|
||||
let _timestamp_str: String = row.get(1)?;
|
||||
Ok(equipment_id)
|
||||
})?
|
||||
.filter_map(|r| r.ok())
|
||||
.map(|id| (id, Timestamp::now())) // Simplified - not parsing timestamps yet
|
||||
.collect();
|
||||
|
||||
Ok(times)
|
||||
}
|
||||
}
|
||||
131
src/main.rs
131
src/main.rs
|
|
@ -172,7 +172,7 @@ fn convert_to_websocket_url(url: &str) -> String {
|
|||
#[command(name = "towerops-agent")]
|
||||
#[command(about = "Towerops remote SNMP polling agent", long_about = None)]
|
||||
struct Args {
|
||||
/// API URL (e.g., wss://app.towerops.com or https://app.towerops.com)
|
||||
/// API URL (e.g., wss://towerops.net or https://towerops.net)
|
||||
#[arg(long, env = "TOWEROPS_API_URL")]
|
||||
api_url: String,
|
||||
|
||||
|
|
@ -352,8 +352,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_convert_https_to_websocket() {
|
||||
assert_eq!(
|
||||
convert_to_websocket_url("https://app.towerops.com"),
|
||||
"wss://app.towerops.com"
|
||||
convert_to_websocket_url("https://towerops.net"),
|
||||
"wss://towerops.net"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -364,16 +364,16 @@ mod tests {
|
|||
"ws://localhost:4000"
|
||||
);
|
||||
assert_eq!(
|
||||
convert_to_websocket_url("wss://app.towerops.com"),
|
||||
"wss://app.towerops.com"
|
||||
convert_to_websocket_url("wss://towerops.net"),
|
||||
"wss://towerops.net"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bare_domain_gets_wss() {
|
||||
assert_eq!(
|
||||
convert_to_websocket_url("app.towerops.com"),
|
||||
"wss://app.towerops.com"
|
||||
convert_to_websocket_url("towerops.net"),
|
||||
"wss://towerops.net"
|
||||
);
|
||||
assert_eq!(
|
||||
convert_to_websocket_url("localhost:4000"),
|
||||
|
|
@ -420,7 +420,118 @@ mod tests {
|
|||
assert_eq!(days_to_month_day(364, false), (12, 31));
|
||||
}
|
||||
|
||||
// Note: main() function and init_logger() are not unit tested as they
|
||||
// involve global state and tokio runtime initialization.
|
||||
// They are tested via manual/integration testing.
|
||||
#[test]
|
||||
fn test_log_level_from_str_error() {
|
||||
assert_eq!(LogLevel::from_str("error"), LogLevel::Error);
|
||||
assert_eq!(LogLevel::from_str("ERROR"), LogLevel::Error);
|
||||
assert_eq!(LogLevel::from_str("Error"), LogLevel::Error);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_level_from_str_warn() {
|
||||
assert_eq!(LogLevel::from_str("warn"), LogLevel::Warn);
|
||||
assert_eq!(LogLevel::from_str("WARN"), LogLevel::Warn);
|
||||
assert_eq!(LogLevel::from_str("Warn"), LogLevel::Warn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_level_from_str_info() {
|
||||
assert_eq!(LogLevel::from_str("info"), LogLevel::Info);
|
||||
assert_eq!(LogLevel::from_str("INFO"), LogLevel::Info);
|
||||
assert_eq!(LogLevel::from_str("Info"), LogLevel::Info);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_level_from_str_debug() {
|
||||
assert_eq!(LogLevel::from_str("debug"), LogLevel::Debug);
|
||||
assert_eq!(LogLevel::from_str("DEBUG"), LogLevel::Debug);
|
||||
assert_eq!(LogLevel::from_str("Debug"), LogLevel::Debug);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_level_from_str_unknown() {
|
||||
// Unknown values default to Info
|
||||
assert_eq!(LogLevel::from_str("unknown"), LogLevel::Info);
|
||||
assert_eq!(LogLevel::from_str("trace"), LogLevel::Info);
|
||||
assert_eq!(LogLevel::from_str(""), LogLevel::Info);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_level_ordering() {
|
||||
assert!(LogLevel::Error < LogLevel::Warn);
|
||||
assert!(LogLevel::Warn < LogLevel::Info);
|
||||
assert!(LogLevel::Info < LogLevel::Debug);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_level_eq() {
|
||||
assert_eq!(LogLevel::Error, LogLevel::Error);
|
||||
assert_eq!(LogLevel::Warn, LogLevel::Warn);
|
||||
assert_eq!(LogLevel::Info, LogLevel::Info);
|
||||
assert_eq!(LogLevel::Debug, LogLevel::Debug);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_level_copy() {
|
||||
let level = LogLevel::Debug;
|
||||
let copied = level;
|
||||
assert_eq!(copied, LogLevel::Debug);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_level_clone() {
|
||||
let level = LogLevel::Warn;
|
||||
let cloned = level.clone();
|
||||
assert_eq!(cloned, LogLevel::Warn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_to_month_day_february() {
|
||||
// February 28th in non-leap year (day 58)
|
||||
assert_eq!(days_to_month_day(58, false), (2, 28));
|
||||
|
||||
// February 29th in leap year (day 59)
|
||||
assert_eq!(days_to_month_day(59, true), (2, 29));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_to_month_day_later_months() {
|
||||
// April 15th in non-leap year (day 105)
|
||||
// Jan=31, Feb=28, Mar=31, Apr 1-15 = 31+28+31+14 = 104 (0-indexed day 104)
|
||||
assert_eq!(days_to_month_day(104, false), (4, 15));
|
||||
|
||||
// July 4th in non-leap year
|
||||
// Jan=31, Feb=28, Mar=31, Apr=30, May=31, Jun=30, Jul 1-4 = 31+28+31+30+31+30+3 = 184 (day 184)
|
||||
assert_eq!(days_to_month_day(184, false), (7, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_to_month_day_end_of_year() {
|
||||
// December 31st in leap year (day 365)
|
||||
assert_eq!(days_to_month_day(365, true), (12, 31));
|
||||
|
||||
// December 25th in non-leap year
|
||||
// 31+28+31+30+31+30+31+31+30+31+30+24 = 358 (day 358)
|
||||
assert_eq!(days_to_month_day(358, false), (12, 25));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_to_month_day_overflow_fallback() {
|
||||
// Day 400 is beyond year - should fallback to Dec 31
|
||||
assert_eq!(days_to_month_day(400, false), (12, 31));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_timestamp_structure() {
|
||||
let timestamp = format_timestamp();
|
||||
// Should be in format "YYYY-MM-DD HH:MM:SS.mmm"
|
||||
let parts: Vec<&str> = timestamp.split(' ').collect();
|
||||
assert_eq!(parts.len(), 2, "Expected date and time parts");
|
||||
|
||||
let date_parts: Vec<&str> = parts[0].split('-').collect();
|
||||
assert_eq!(date_parts.len(), 3, "Expected year-month-day");
|
||||
|
||||
let time_parts: Vec<&str> = parts[1].split(':').collect();
|
||||
assert_eq!(time_parts.len(), 3, "Expected hour:min:sec.ms");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,149 +0,0 @@
|
|||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Timestamp that serializes to RFC3339 format
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Timestamp {
|
||||
secs: i64,
|
||||
}
|
||||
|
||||
impl Timestamp {
|
||||
pub fn now() -> Self {
|
||||
let duration = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards");
|
||||
Self {
|
||||
secs: duration.as_secs() as i64,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn elapsed_secs(&self) -> i64 {
|
||||
Self::now().secs - self.secs
|
||||
}
|
||||
|
||||
pub fn to_unix_timestamp(&self) -> i64 {
|
||||
self.secs
|
||||
}
|
||||
|
||||
pub fn to_rfc3339(self) -> String {
|
||||
// Convert Unix timestamp to RFC3339 format
|
||||
// This is a simplified implementation that produces UTC timestamps
|
||||
let secs = self.secs;
|
||||
let days = secs / 86400;
|
||||
let rem_secs = secs % 86400;
|
||||
let hours = rem_secs / 3600;
|
||||
let minutes = (rem_secs % 3600) / 60;
|
||||
let seconds = rem_secs % 60;
|
||||
|
||||
// Calculate year/month/day from days since epoch (1970-01-01)
|
||||
let (year, month, day) = days_to_ymd(days);
|
||||
|
||||
format!(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
|
||||
year, month, day, hours, minutes, seconds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Timestamp {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_rfc3339())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Timestamp {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let _s = String::deserialize(deserializer)?;
|
||||
// Simple RFC3339 parsing - accepts the format we produce
|
||||
// For a production system, you might want more robust parsing
|
||||
Ok(Timestamp { secs: 0 }) // Simplified - we mainly serialize, not deserialize
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert days since Unix epoch to year/month/day
|
||||
fn days_to_ymd(mut days: i64) -> (i32, u8, u8) {
|
||||
let mut year = 1970;
|
||||
loop {
|
||||
let days_in_year = if is_leap_year(year) { 366 } else { 365 };
|
||||
if days < days_in_year {
|
||||
break;
|
||||
}
|
||||
days -= days_in_year;
|
||||
year += 1;
|
||||
}
|
||||
|
||||
let days_in_months = if is_leap_year(year) {
|
||||
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
} else {
|
||||
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
};
|
||||
|
||||
let mut month = 1;
|
||||
for &dim in &days_in_months {
|
||||
if days < dim as i64 {
|
||||
break;
|
||||
}
|
||||
days -= dim as i64;
|
||||
month += 1;
|
||||
}
|
||||
|
||||
(year, month, days as u8 + 1)
|
||||
}
|
||||
|
||||
fn is_leap_year(year: i32) -> bool {
|
||||
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
|
||||
}
|
||||
|
||||
/// Metric types that can be submitted to the API
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Metric {
|
||||
#[serde(rename = "sensor_reading")]
|
||||
SensorReading(SensorReading),
|
||||
#[serde(rename = "interface_stat")]
|
||||
InterfaceStat(InterfaceStat),
|
||||
}
|
||||
|
||||
impl Metric {
|
||||
pub fn metric_type(&self) -> &str {
|
||||
match self {
|
||||
Metric::SensorReading(_) => "sensor_reading",
|
||||
Metric::InterfaceStat(_) => "interface_stat",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> &Timestamp {
|
||||
match self {
|
||||
Metric::SensorReading(sr) => &sr.timestamp,
|
||||
Metric::InterfaceStat(is) => &is.timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sensor reading metric
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SensorReading {
|
||||
pub sensor_id: String,
|
||||
pub value: f64,
|
||||
pub status: String,
|
||||
pub timestamp: Timestamp,
|
||||
}
|
||||
|
||||
/// Interface statistics metric
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InterfaceStat {
|
||||
pub interface_id: String,
|
||||
pub if_in_octets: i64,
|
||||
pub if_out_octets: i64,
|
||||
pub if_in_errors: i64,
|
||||
pub if_out_errors: i64,
|
||||
pub if_in_discards: i64,
|
||||
pub if_out_discards: i64,
|
||||
pub timestamp: Timestamp,
|
||||
}
|
||||
753
src/snmp/trap.rs
753
src/snmp/trap.rs
|
|
@ -729,4 +729,757 @@ mod tests {
|
|||
fn test_parse_ip_address_invalid_length() {
|
||||
assert!(parse_ip_address(&[192, 168, 1]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generic_trap_from_u8_all_variants() {
|
||||
assert!(matches!(
|
||||
GenericTrap::from_u8(0),
|
||||
Some(GenericTrap::ColdStart)
|
||||
));
|
||||
assert!(matches!(
|
||||
GenericTrap::from_u8(1),
|
||||
Some(GenericTrap::WarmStart)
|
||||
));
|
||||
assert!(matches!(
|
||||
GenericTrap::from_u8(2),
|
||||
Some(GenericTrap::LinkDown)
|
||||
));
|
||||
assert!(matches!(GenericTrap::from_u8(3), Some(GenericTrap::LinkUp)));
|
||||
assert!(matches!(
|
||||
GenericTrap::from_u8(4),
|
||||
Some(GenericTrap::AuthenticationFailure)
|
||||
));
|
||||
assert!(matches!(
|
||||
GenericTrap::from_u8(5),
|
||||
Some(GenericTrap::EgpNeighborLoss)
|
||||
));
|
||||
assert!(matches!(
|
||||
GenericTrap::from_u8(6),
|
||||
Some(GenericTrap::EnterpriseSpecific)
|
||||
));
|
||||
assert!(GenericTrap::from_u8(7).is_none());
|
||||
assert!(GenericTrap::from_u8(255).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generic_trap_display_all_variants() {
|
||||
assert_eq!(format!("{}", GenericTrap::ColdStart), "coldStart");
|
||||
assert_eq!(format!("{}", GenericTrap::WarmStart), "warmStart");
|
||||
assert_eq!(format!("{}", GenericTrap::LinkDown), "linkDown");
|
||||
assert_eq!(format!("{}", GenericTrap::LinkUp), "linkUp");
|
||||
assert_eq!(
|
||||
format!("{}", GenericTrap::AuthenticationFailure),
|
||||
"authenticationFailure"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", GenericTrap::EgpNeighborLoss),
|
||||
"egpNeighborLoss"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", GenericTrap::EnterpriseSpecific),
|
||||
"enterpriseSpecific"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_tlv_simple() {
|
||||
// INTEGER 5: tag=0x02, length=0x01, value=0x05
|
||||
let data = [0x02, 0x01, 0x05];
|
||||
let (tag, value, remaining) = parse_tlv(&data).unwrap();
|
||||
assert_eq!(tag, 0x02);
|
||||
assert_eq!(value, &[0x05]);
|
||||
assert!(remaining.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_tlv_with_remaining() {
|
||||
// INTEGER 5 followed by more data
|
||||
let data = [0x02, 0x01, 0x05, 0x04, 0x02, 0x41, 0x42];
|
||||
let (tag, value, remaining) = parse_tlv(&data).unwrap();
|
||||
assert_eq!(tag, 0x02);
|
||||
assert_eq!(value, &[0x05]);
|
||||
assert_eq!(remaining, &[0x04, 0x02, 0x41, 0x42]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_tlv_empty() {
|
||||
let result = parse_tlv(&[]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_tlv_too_short() {
|
||||
// Says length is 5 but only has 2 bytes
|
||||
let data = [0x02, 0x05, 0x01, 0x02];
|
||||
let result = parse_tlv(&data);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_length_empty() {
|
||||
let result = parse_length(&[]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_length_indefinite() {
|
||||
// Indefinite length (0x80) not supported
|
||||
let result = parse_length(&[0x80]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_length_too_long() {
|
||||
// Length field says 5 bytes but not enough data
|
||||
let result = parse_length(&[0x85, 0x01]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_integer_empty() {
|
||||
assert_eq!(parse_integer(&[]).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_integer_multibyte() {
|
||||
// 256 = 0x0100
|
||||
assert_eq!(parse_integer(&[0x01, 0x00]).unwrap(), 256);
|
||||
// -256 = 0xFF00
|
||||
assert_eq!(parse_integer(&[0xFF, 0x00]).unwrap(), -256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_unsigned() {
|
||||
assert_eq!(parse_unsigned(&[]).unwrap(), 0);
|
||||
assert_eq!(parse_unsigned(&[0x01]).unwrap(), 1);
|
||||
assert_eq!(parse_unsigned(&[0xFF]).unwrap(), 255);
|
||||
assert_eq!(parse_unsigned(&[0x01, 0x00]).unwrap(), 256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_oid_empty() {
|
||||
assert_eq!(parse_oid(&[]).unwrap(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_to_string_integer() {
|
||||
let result = parse_value_to_string(ber_tags::INTEGER, &[0x2A]);
|
||||
assert_eq!(result, "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_to_string_octet_string() {
|
||||
let result = parse_value_to_string(ber_tags::OCTET_STRING, b"test");
|
||||
assert_eq!(result, "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_to_string_object_identifier() {
|
||||
let oid_bytes = [0x2B, 0x06, 0x01, 0x02, 0x01];
|
||||
let result = parse_value_to_string(ber_tags::OBJECT_IDENTIFIER, &oid_bytes);
|
||||
assert_eq!(result, "1.3.6.1.2.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_to_string_null() {
|
||||
let result = parse_value_to_string(ber_tags::NULL, &[]);
|
||||
assert_eq!(result, "null");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_to_string_ip_address() {
|
||||
let result = parse_value_to_string(ber_tags::IP_ADDRESS, &[10, 0, 0, 1]);
|
||||
assert_eq!(result, "10.0.0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_to_string_counter32() {
|
||||
let result = parse_value_to_string(ber_tags::COUNTER32, &[0x00, 0x01]);
|
||||
assert_eq!(result, "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_to_string_gauge32() {
|
||||
let result = parse_value_to_string(ber_tags::GAUGE32, &[0x64]);
|
||||
assert_eq!(result, "100");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_to_string_timeticks() {
|
||||
let result = parse_value_to_string(ber_tags::TIMETICKS, &[0x00, 0x01]);
|
||||
assert_eq!(result, "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_to_string_counter64() {
|
||||
let result = parse_value_to_string(ber_tags::COUNTER64, &[0x01, 0x00]);
|
||||
assert_eq!(result, "256");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_to_string_unknown() {
|
||||
let result = parse_value_to_string(0xFF, &[0x01]);
|
||||
assert!(result.contains("tag=0xff"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_varbinds_empty() {
|
||||
let result = parse_varbinds(&[]).unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snmp_trap_display_v1_no_varbinds() {
|
||||
let trap = SnmpTrap {
|
||||
source_addr: "192.168.1.1:161".parse().unwrap(),
|
||||
version: SnmpVersion::V1,
|
||||
community: "public".to_string(),
|
||||
trap_oid: "1.3.6.1.4.1.9.9.41".to_string(),
|
||||
generic_trap: Some(GenericTrap::ColdStart),
|
||||
specific_trap: None,
|
||||
uptime: 0,
|
||||
varbinds: vec![],
|
||||
};
|
||||
|
||||
let display = format!("{}", trap);
|
||||
assert!(display.contains("[v1]"));
|
||||
assert!(display.contains("generic=coldStart"));
|
||||
assert!(!display.contains("varbinds="));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snmp_trap_display_v1_no_generic_trap() {
|
||||
let trap = SnmpTrap {
|
||||
source_addr: "192.168.1.1:161".parse().unwrap(),
|
||||
version: SnmpVersion::V1,
|
||||
community: "public".to_string(),
|
||||
trap_oid: "1.3.6.1.4.1.9".to_string(),
|
||||
generic_trap: None,
|
||||
specific_trap: None,
|
||||
uptime: 100,
|
||||
varbinds: vec![],
|
||||
};
|
||||
|
||||
let display = format!("{}", trap);
|
||||
assert!(display.contains("[v1]"));
|
||||
assert!(!display.contains("generic="));
|
||||
assert!(!display.contains("specific="));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snmp_trap_display_multiple_varbinds() {
|
||||
let trap = SnmpTrap {
|
||||
source_addr: "10.0.0.1:162".parse().unwrap(),
|
||||
version: SnmpVersion::V2c,
|
||||
community: "private".to_string(),
|
||||
trap_oid: "1.3.6.1.6.3.1.1.5.3".to_string(),
|
||||
generic_trap: None,
|
||||
specific_trap: None,
|
||||
uptime: 5000,
|
||||
varbinds: vec![
|
||||
("1.3.6.1.2.1.2.2.1.1".to_string(), "1".to_string()),
|
||||
("1.3.6.1.2.1.2.2.1.2".to_string(), "eth0".to_string()),
|
||||
],
|
||||
};
|
||||
|
||||
let display = format!("{}", trap);
|
||||
assert!(display.contains("10.0.0.1:162"));
|
||||
assert!(display.contains("[v2c]"));
|
||||
assert!(display.contains("varbinds=["));
|
||||
assert!(display.contains(", "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trap_listener_new() {
|
||||
let listener = TrapListener::new(1620);
|
||||
assert_eq!(listener.port, 1620);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_error_display() {
|
||||
let err = ParseError("test error".to_string());
|
||||
assert_eq!(format!("{}", err), "test error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snmp_version_eq() {
|
||||
assert_eq!(SnmpVersion::V1, SnmpVersion::V1);
|
||||
assert_eq!(SnmpVersion::V2c, SnmpVersion::V2c);
|
||||
assert_ne!(SnmpVersion::V1, SnmpVersion::V2c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snmp_trap_clone() {
|
||||
let trap = SnmpTrap {
|
||||
source_addr: "192.168.1.1:161".parse().unwrap(),
|
||||
version: SnmpVersion::V1,
|
||||
community: "public".to_string(),
|
||||
trap_oid: "1.3.6.1.4.1.9".to_string(),
|
||||
generic_trap: Some(GenericTrap::LinkUp),
|
||||
specific_trap: Some(42),
|
||||
uptime: 12345,
|
||||
varbinds: vec![("oid".to_string(), "value".to_string())],
|
||||
};
|
||||
|
||||
let cloned = trap.clone();
|
||||
assert_eq!(trap.source_addr, cloned.source_addr);
|
||||
assert_eq!(trap.version, cloned.version);
|
||||
assert_eq!(trap.community, cloned.community);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generic_trap_copy() {
|
||||
let trap = GenericTrap::WarmStart;
|
||||
let copied = trap;
|
||||
assert!(matches!(copied, GenericTrap::WarmStart));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snmp_version_copy() {
|
||||
let version = SnmpVersion::V2c;
|
||||
let copied = version;
|
||||
assert_eq!(copied, SnmpVersion::V2c);
|
||||
}
|
||||
|
||||
// Test full SNMPv1 trap parsing with a minimal but valid trap packet
|
||||
#[test]
|
||||
fn test_parse_trap_v1() {
|
||||
// Build a valid SNMPv1 trap packet
|
||||
// Inner Trap-PDU contents:
|
||||
// OID 1.3 (enterprise): 06 01 2B
|
||||
// IpAddress 10.0.0.1: 40 04 0A 00 00 01
|
||||
// INTEGER 0 (generic): 02 01 00
|
||||
// INTEGER 0 (specific): 02 01 00
|
||||
// TimeTicks 0: 43 01 00
|
||||
// SEQUENCE {} (varbinds): 30 00
|
||||
// Total Trap-PDU content = 3 + 6 + 3 + 3 + 3 + 2 = 20 bytes
|
||||
let trap_pdu: Vec<u8> = vec![
|
||||
0x06, 0x01, 0x2B, // OID 1.3
|
||||
0x40, 0x04, 0x0A, 0x00, 0x00, 0x01, // IpAddress 10.0.0.1
|
||||
0x02, 0x01, 0x00, // INTEGER 0 (generic-trap)
|
||||
0x02, 0x01, 0x00, // INTEGER 0 (specific-trap)
|
||||
0x43, 0x01, 0x00, // TimeTicks 0
|
||||
0x30, 0x00, // SEQUENCE {} (varbinds)
|
||||
];
|
||||
|
||||
// Full message:
|
||||
// SEQUENCE { version, community, Trap-PDU }
|
||||
// version: 02 01 00 (3 bytes)
|
||||
// community "pub": 04 03 70 75 62 (5 bytes)
|
||||
// Trap-PDU [4]: A4 <len> <trap_pdu>
|
||||
let mut trap_bytes: Vec<u8> = vec![
|
||||
0x30,
|
||||
0x00, // SEQUENCE with placeholder length
|
||||
0x02,
|
||||
0x01,
|
||||
0x00, // INTEGER 0 (version SNMPv1)
|
||||
0x04,
|
||||
0x03,
|
||||
0x70,
|
||||
0x75,
|
||||
0x62, // OCTET STRING "pub"
|
||||
0xA4,
|
||||
trap_pdu.len() as u8, // Trap-PDU tag with length
|
||||
];
|
||||
trap_bytes.extend_from_slice(&trap_pdu);
|
||||
// Fix outer SEQUENCE length
|
||||
trap_bytes[1] = (trap_bytes.len() - 2) as u8;
|
||||
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&trap_bytes, source_addr);
|
||||
assert!(result.is_ok(), "Expected Ok, got {:?}", result);
|
||||
|
||||
let trap = result.unwrap();
|
||||
assert_eq!(trap.version, SnmpVersion::V1);
|
||||
assert_eq!(trap.community, "pub");
|
||||
assert!(trap.generic_trap.is_some());
|
||||
}
|
||||
|
||||
// Test full SNMPv2c trap parsing
|
||||
#[test]
|
||||
fn test_parse_trap_v2c() {
|
||||
// Build a valid SNMPv2c trap packet
|
||||
// Inner SNMPv2-Trap-PDU contents:
|
||||
// INTEGER 1 (request-id): 02 01 01
|
||||
// INTEGER 0 (error-status): 02 01 00
|
||||
// INTEGER 0 (error-index): 02 01 00
|
||||
// SEQUENCE {} (varbinds): 30 00
|
||||
// Total PDU content = 3 + 3 + 3 + 2 = 11 bytes
|
||||
let trap_pdu: Vec<u8> = vec![
|
||||
0x02, 0x01, 0x01, // INTEGER 1 (request-id)
|
||||
0x02, 0x01, 0x00, // INTEGER 0 (error-status)
|
||||
0x02, 0x01, 0x00, // INTEGER 0 (error-index)
|
||||
0x30, 0x00, // SEQUENCE {} (varbinds)
|
||||
];
|
||||
|
||||
// Full message:
|
||||
// SEQUENCE { version, community, SNMPv2-Trap-PDU }
|
||||
let mut trap_bytes: Vec<u8> = vec![
|
||||
0x30,
|
||||
0x00, // SEQUENCE with placeholder length
|
||||
0x02,
|
||||
0x01,
|
||||
0x01, // INTEGER 1 (version SNMPv2c)
|
||||
0x04,
|
||||
0x03,
|
||||
0x70,
|
||||
0x75,
|
||||
0x62, // OCTET STRING "pub"
|
||||
0xA7,
|
||||
trap_pdu.len() as u8, // SNMPv2-Trap-PDU tag with length
|
||||
];
|
||||
trap_bytes.extend_from_slice(&trap_pdu);
|
||||
// Fix outer SEQUENCE length
|
||||
trap_bytes[1] = (trap_bytes.len() - 2) as u8;
|
||||
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&trap_bytes, source_addr);
|
||||
assert!(result.is_ok(), "Expected Ok, got {:?}", result);
|
||||
|
||||
let trap = result.unwrap();
|
||||
assert_eq!(trap.version, SnmpVersion::V2c);
|
||||
assert_eq!(trap.community, "pub");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trap_invalid_outer_sequence() {
|
||||
// Not a SEQUENCE
|
||||
let trap_bytes: Vec<u8> = vec![0x02, 0x01, 0x00];
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&trap_bytes, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trap_unsupported_version() {
|
||||
// SEQUENCE with version 2 (not v1=0 or v2c=1)
|
||||
let trap_bytes: Vec<u8> = vec![
|
||||
0x30, 0x0A, // INTEGER 2 (unsupported)
|
||||
0x02, 0x01, 0x02, // OCTET STRING "pub"
|
||||
0x04, 0x03, 0x70, 0x75, 0x62, // Minimal PDU
|
||||
0xA7, 0x00,
|
||||
];
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&trap_bytes, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trap_invalid_version_tag() {
|
||||
// Version is not an INTEGER
|
||||
let trap_bytes: Vec<u8> = vec![
|
||||
0x30, 0x08, // OCTET STRING instead of INTEGER for version
|
||||
0x04, 0x01, 0x00, // OCTET STRING "pub"
|
||||
0x04, 0x03, 0x70, 0x75, 0x62,
|
||||
];
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&trap_bytes, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trap_invalid_community_tag() {
|
||||
// Community is not an OCTET STRING
|
||||
let trap_bytes: Vec<u8> = vec![
|
||||
0x30, 0x08, // INTEGER 0 (version)
|
||||
0x02, 0x01, 0x00, // INTEGER instead of OCTET STRING for community
|
||||
0x02, 0x01, 0x00, // PDU
|
||||
0xA4, 0x00,
|
||||
];
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&trap_bytes, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trap_v1_invalid_pdu_tag() {
|
||||
// V1 with wrong PDU tag (should be 0xA4)
|
||||
let trap_bytes: Vec<u8> = vec![
|
||||
0x30, 0x0B, // INTEGER 0 (version v1)
|
||||
0x02, 0x01, 0x00, // OCTET STRING "pub"
|
||||
0x04, 0x03, 0x70, 0x75, 0x62, // Wrong PDU tag (0xA7 is v2c)
|
||||
0xA7, 0x00,
|
||||
];
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&trap_bytes, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trap_v2c_invalid_pdu_tag() {
|
||||
// V2c with wrong PDU tag (should be 0xA7)
|
||||
let trap_bytes: Vec<u8> = vec![
|
||||
0x30, 0x0B, // INTEGER 1 (version v2c)
|
||||
0x02, 0x01, 0x01, // OCTET STRING "pub"
|
||||
0x04, 0x03, 0x70, 0x75, 0x62, // Wrong PDU tag (0xA4 is v1)
|
||||
0xA4, 0x00,
|
||||
];
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&trap_bytes, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_varbinds_invalid_varbind_tag() {
|
||||
// Varbind is not a SEQUENCE
|
||||
let data: Vec<u8> = vec![
|
||||
// INTEGER instead of SEQUENCE
|
||||
0x02, 0x01, 0x00,
|
||||
];
|
||||
let result = parse_varbinds(&data);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_varbinds_invalid_oid_tag() {
|
||||
// Valid SEQUENCE but OID is wrong type
|
||||
let data: Vec<u8> = vec![
|
||||
// SEQUENCE
|
||||
0x30, 0x06, // INTEGER instead of OID
|
||||
0x02, 0x01, 0x00, // NULL value
|
||||
0x05, 0x00,
|
||||
];
|
||||
let result = parse_varbinds(&data);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_varbinds_valid() {
|
||||
// Valid varbind: SEQUENCE { OID 1.3, INTEGER 42 }
|
||||
let data: Vec<u8> = vec![
|
||||
// SEQUENCE
|
||||
0x30, 0x06, // OID 1.3
|
||||
0x06, 0x01, 0x2B, // INTEGER 42
|
||||
0x02, 0x01, 0x2A,
|
||||
];
|
||||
let result = parse_varbinds(&data).unwrap();
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].0, "1.3");
|
||||
assert_eq!(result[0].1, "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_to_string_ip_address_invalid() {
|
||||
// IP address with wrong length
|
||||
let result = parse_value_to_string(ber_tags::IP_ADDRESS, &[192, 168, 1]);
|
||||
assert_eq!(result, "?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_to_string_oid_empty() {
|
||||
let result = parse_value_to_string(ber_tags::OBJECT_IDENTIFIER, &[]);
|
||||
assert_eq!(result, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_length_four_bytes() {
|
||||
// Four-byte length: 0x84 followed by 4 bytes
|
||||
let data = [0x84, 0x00, 0x00, 0x01, 0x00];
|
||||
let result = parse_length(&data);
|
||||
assert!(result.is_ok());
|
||||
let (length, consumed) = result.unwrap();
|
||||
assert_eq!(length, 256);
|
||||
assert_eq!(consumed, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_integer_negative_multibyte() {
|
||||
// -1 as two bytes: 0xFF 0xFF
|
||||
assert_eq!(parse_integer(&[0xFF, 0xFF]).unwrap(), -1);
|
||||
// -128 as single byte
|
||||
assert_eq!(parse_integer(&[0x80]).unwrap(), -128);
|
||||
// -129 as two bytes: 0xFF 0x7F
|
||||
assert_eq!(parse_integer(&[0xFF, 0x7F]).unwrap(), -129);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_unsigned_large() {
|
||||
// Large counter value
|
||||
let data = [0x01, 0x00, 0x00, 0x00];
|
||||
let result = parse_unsigned(&data).unwrap();
|
||||
assert_eq!(result, 16777216);
|
||||
}
|
||||
|
||||
// Helper to build a minimal v1 trap with custom PDU content
|
||||
fn build_v1_trap_packet(pdu_content: &[u8]) -> Vec<u8> {
|
||||
let mut packet = vec![
|
||||
0x30,
|
||||
0x00, // SEQUENCE placeholder
|
||||
0x02,
|
||||
0x01,
|
||||
0x00, // INTEGER 0 (version v1)
|
||||
0x04,
|
||||
0x03,
|
||||
0x70,
|
||||
0x75,
|
||||
0x62, // OCTET STRING "pub"
|
||||
0xA4,
|
||||
pdu_content.len() as u8, // Trap-PDU
|
||||
];
|
||||
packet.extend_from_slice(pdu_content);
|
||||
packet[1] = (packet.len() - 2) as u8;
|
||||
packet
|
||||
}
|
||||
|
||||
// Helper to build a minimal v2c trap with custom PDU content
|
||||
fn build_v2c_trap_packet(pdu_content: &[u8]) -> Vec<u8> {
|
||||
let mut packet = vec![
|
||||
0x30,
|
||||
0x00, // SEQUENCE placeholder
|
||||
0x02,
|
||||
0x01,
|
||||
0x01, // INTEGER 1 (version v2c)
|
||||
0x04,
|
||||
0x03,
|
||||
0x70,
|
||||
0x75,
|
||||
0x62, // OCTET STRING "pub"
|
||||
0xA7,
|
||||
pdu_content.len() as u8, // SNMPv2-Trap-PDU
|
||||
];
|
||||
packet.extend_from_slice(pdu_content);
|
||||
packet[1] = (packet.len() - 2) as u8;
|
||||
packet
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v1_trap_invalid_enterprise_tag() {
|
||||
// Enterprise should be OID, not INTEGER
|
||||
let pdu = vec![
|
||||
0x02, 0x01, 0x00, // INTEGER instead of OID
|
||||
];
|
||||
let packet = build_v1_trap_packet(&pdu);
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&packet, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v1_trap_invalid_agent_addr_tag() {
|
||||
// Agent addr should be IP_ADDRESS
|
||||
let pdu = vec![
|
||||
0x06, 0x01, 0x2B, // OID 1.3 (enterprise)
|
||||
0x02, 0x01, 0x00, // INTEGER instead of IpAddress
|
||||
];
|
||||
let packet = build_v1_trap_packet(&pdu);
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&packet, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v1_trap_invalid_generic_trap_tag() {
|
||||
// Generic trap should be INTEGER
|
||||
let pdu = vec![
|
||||
0x06, 0x01, 0x2B, // OID 1.3 (enterprise)
|
||||
0x40, 0x04, 0x0A, 0x00, 0x00, 0x01, // IpAddress 10.0.0.1
|
||||
0x04, 0x01, 0x00, // OCTET STRING instead of INTEGER
|
||||
];
|
||||
let packet = build_v1_trap_packet(&pdu);
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&packet, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v1_trap_invalid_specific_trap_tag() {
|
||||
// Specific trap should be INTEGER
|
||||
let pdu = vec![
|
||||
0x06, 0x01, 0x2B, // OID 1.3 (enterprise)
|
||||
0x40, 0x04, 0x0A, 0x00, 0x00, 0x01, // IpAddress 10.0.0.1
|
||||
0x02, 0x01, 0x00, // INTEGER 0 (generic)
|
||||
0x04, 0x01, 0x00, // OCTET STRING instead of INTEGER
|
||||
];
|
||||
let packet = build_v1_trap_packet(&pdu);
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&packet, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v1_trap_invalid_timestamp_tag() {
|
||||
// Timestamp should be TIMETICKS
|
||||
let pdu = vec![
|
||||
0x06, 0x01, 0x2B, // OID 1.3 (enterprise)
|
||||
0x40, 0x04, 0x0A, 0x00, 0x00, 0x01, // IpAddress 10.0.0.1
|
||||
0x02, 0x01, 0x00, // INTEGER 0 (generic)
|
||||
0x02, 0x01, 0x00, // INTEGER 0 (specific)
|
||||
0x02, 0x01, 0x00, // INTEGER instead of TIMETICKS
|
||||
];
|
||||
let packet = build_v1_trap_packet(&pdu);
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&packet, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v1_trap_invalid_varbinds_tag() {
|
||||
// Varbinds should be SEQUENCE
|
||||
let pdu = vec![
|
||||
0x06, 0x01, 0x2B, // OID 1.3 (enterprise)
|
||||
0x40, 0x04, 0x0A, 0x00, 0x00, 0x01, // IpAddress 10.0.0.1
|
||||
0x02, 0x01, 0x00, // INTEGER 0 (generic)
|
||||
0x02, 0x01, 0x00, // INTEGER 0 (specific)
|
||||
0x43, 0x01, 0x00, // TimeTicks 0
|
||||
0x02, 0x01, 0x00, // INTEGER instead of SEQUENCE
|
||||
];
|
||||
let packet = build_v1_trap_packet(&pdu);
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&packet, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v2c_trap_invalid_request_id_tag() {
|
||||
// Request-id should be INTEGER
|
||||
let pdu = vec![
|
||||
0x04, 0x01, 0x00, // OCTET STRING instead of INTEGER
|
||||
];
|
||||
let packet = build_v2c_trap_packet(&pdu);
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&packet, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v2c_trap_invalid_error_status_tag() {
|
||||
// Error-status should be INTEGER
|
||||
let pdu = vec![
|
||||
0x02, 0x01, 0x01, // INTEGER 1 (request-id)
|
||||
0x04, 0x01, 0x00, // OCTET STRING instead of INTEGER
|
||||
];
|
||||
let packet = build_v2c_trap_packet(&pdu);
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&packet, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v2c_trap_invalid_error_index_tag() {
|
||||
// Error-index should be INTEGER
|
||||
let pdu = vec![
|
||||
0x02, 0x01, 0x01, // INTEGER 1 (request-id)
|
||||
0x02, 0x01, 0x00, // INTEGER 0 (error-status)
|
||||
0x04, 0x01, 0x00, // OCTET STRING instead of INTEGER
|
||||
];
|
||||
let packet = build_v2c_trap_packet(&pdu);
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&packet, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_v2c_trap_invalid_varbinds_tag() {
|
||||
// Varbinds should be SEQUENCE
|
||||
let pdu = vec![
|
||||
0x02, 0x01, 0x01, // INTEGER 1 (request-id)
|
||||
0x02, 0x01, 0x00, // INTEGER 0 (error-status)
|
||||
0x02, 0x01, 0x00, // INTEGER 0 (error-index)
|
||||
0x02, 0x01, 0x00, // INTEGER instead of SEQUENCE
|
||||
];
|
||||
let packet = build_v2c_trap_packet(&pdu);
|
||||
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
|
||||
let result = parse_trap(&packet, source_addr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,5 +22,18 @@ mod tests {
|
|||
// Just verify it's a non-empty string
|
||||
}
|
||||
|
||||
// Note: check_for_updates() just logs the version, no testing needed
|
||||
#[test]
|
||||
fn test_current_version_format() {
|
||||
let version = current_version();
|
||||
// Version should be in semver-like format (e.g., "0.1.0" or custom BUILD_VERSION)
|
||||
// At minimum, should have some content
|
||||
assert!(version.len() >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_for_updates() {
|
||||
// This function just logs, but we can call it to verify it doesn't panic
|
||||
check_for_updates();
|
||||
// If we get here, the function completed without panicking
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue