use crate::common::fixtures;
use crate::common::handler::TestHandler;
use async_snmp::handler::MibHandler;
use async_snmp::v3::{AuthProtocol, PrivProtocol};
use async_snmp::{Agent, Oid, Value, oid};
use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
pub struct TestAgent {
addr: SocketAddr,
handler: Arc<TestHandler>,
cancel: CancellationToken,
_task: JoinHandle<()>,
agent: Agent,
}
impl TestAgent {
pub async fn new() -> Self {
Self::with_data(fixtures::system_mib()).await
}
pub async fn with_data(initial: BTreeMap<Oid, Value>) -> Self {
let handler = Arc::new(TestHandler::new(initial));
Self::with_handler(handler).await
}
pub async fn with_handler(handler: Arc<TestHandler>) -> Self {
let cancel = CancellationToken::new();
let agent = Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.cancel(cancel.clone())
.handler(oid!(1, 3, 6), handler.clone())
.without_builtin_handlers()
.build()
.await
.expect("failed to build test agent");
let addr = agent.local_addr();
let agent_handle = agent.clone();
let task = tokio::spawn(async move {
if let Err(e) = agent.run().await {
if !e.to_string().contains("cancelled") {
eprintln!("TestAgent error: {e}");
}
}
});
tokio::task::yield_now().await;
Self {
addr,
handler,
cancel,
_task: task,
agent: agent_handle,
}
}
pub fn addr(&self) -> SocketAddr {
self.addr
}
pub fn handler(&self) -> &TestHandler {
&self.handler
}
pub fn set(&self, oid: Oid, value: Value) {
self.handler.set(oid, value);
}
pub fn remove(&self, oid: &Oid) -> Option<Value> {
self.handler.remove(oid)
}
pub fn get(&self, oid: &Oid) -> Option<Value> {
self.handler.get(oid)
}
pub fn len(&self) -> usize {
self.handler.len()
}
pub fn is_empty(&self) -> bool {
self.handler.is_empty()
}
pub fn stop(&self) {
self.cancel.cancel();
}
pub fn agent(&self) -> &Agent {
&self.agent
}
}
impl Drop for TestAgent {
fn drop(&mut self) {
self.cancel.cancel();
}
}
pub struct V3User {
pub username: Vec<u8>,
pub auth: Option<(AuthProtocol, Vec<u8>)>,
pub priv_: Option<(PrivProtocol, Vec<u8>)>,
}
impl V3User {
pub fn no_auth(username: impl Into<Vec<u8>>) -> Self {
Self {
username: username.into(),
auth: None,
priv_: None,
}
}
pub fn auth_only(
username: impl Into<Vec<u8>>,
protocol: AuthProtocol,
password: impl Into<Vec<u8>>,
) -> Self {
Self {
username: username.into(),
auth: Some((protocol, password.into())),
priv_: None,
}
}
pub fn auth_priv(
username: impl Into<Vec<u8>>,
auth_protocol: AuthProtocol,
auth_password: impl Into<Vec<u8>>,
priv_protocol: PrivProtocol,
priv_password: impl Into<Vec<u8>>,
) -> Self {
Self {
username: username.into(),
auth: Some((auth_protocol, auth_password.into())),
priv_: Some((priv_protocol, priv_password.into())),
}
}
}
pub struct TestAgentBuilder {
data: BTreeMap<Oid, Value>,
communities: Vec<Vec<u8>>,
usm_users: Vec<V3User>,
}
impl TestAgentBuilder {
pub fn new() -> Self {
Self {
data: fixtures::system_mib(),
communities: vec![b"public".to_vec()],
usm_users: Vec::new(),
}
}
pub fn data(mut self, data: BTreeMap<Oid, Value>) -> Self {
self.data = data;
self
}
pub fn community(mut self, community: &[u8]) -> Self {
self.communities.push(community.to_vec());
self
}
pub fn usm_user(mut self, user: V3User) -> Self {
self.usm_users.push(user);
self
}
pub async fn build(self) -> TestAgent {
let handler = Arc::new(TestHandler::new(self.data));
let cancel = CancellationToken::new();
let mut builder = Agent::builder()
.bind("127.0.0.1:0")
.cancel(cancel.clone())
.handler(oid!(1, 3, 6), handler.clone());
for community in &self.communities {
builder = builder.community(community);
}
for user in &self.usm_users {
builder = builder.usm_user(user.username.clone(), |u| {
let mut u = u;
if let Some((proto, pass)) = &user.auth {
u = u.auth(*proto, pass);
}
if let Some((proto, pass)) = &user.priv_ {
u = u.privacy(*proto, pass);
}
u
});
}
let agent = builder.build().await.expect("failed to build test agent");
let addr = agent.local_addr();
let agent_handle = agent.clone();
let task = tokio::spawn(async move {
let _ = agent.run().await;
});
TestAgent {
addr,
handler,
cancel,
_task: task,
agent: agent_handle,
}
}
}
impl Default for TestAgentBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_snmp::{Auth, Client};
#[tokio::test]
async fn test_agent_starts_and_responds() {
let agent = TestAgent::new().await;
let client = Client::builder(agent.addr().to_string(), Auth::v2c("public"))
.connect()
.await
.expect("failed to connect");
let result = client
.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0))
.await
.expect("GET failed");
assert_eq!(result.value.as_str(), Some("Test SNMP Agent"));
}
#[tokio::test]
async fn test_agent_dynamic_data() {
let agent = TestAgent::new().await;
agent.set(oid!(1, 3, 6, 1, 99, 1, 0), Value::Integer(12345));
let client = Client::builder(agent.addr().to_string(), Auth::v2c("public"))
.connect()
.await
.expect("failed to connect");
let result = client
.get(&oid!(1, 3, 6, 1, 99, 1, 0))
.await
.expect("GET failed");
assert_eq!(result.value, Value::Integer(12345));
}
#[tokio::test]
async fn test_agent_stops_cleanly() {
let agent = TestAgent::new().await;
let addr = agent.addr();
agent.stop();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
let result = Client::builder(addr.to_string(), Auth::v2c("public"))
.timeout(std::time::Duration::from_millis(100))
.connect()
.await;
if let Ok(client) = result {
let get_result = client.get(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)).await;
assert!(get_result.is_err());
}
}
}