async_snmp/client/builder.rs
1//! New unified client builder.
2//!
3//! This module provides the [`ClientBuilder`] type, a single entry point for
4//! constructing SNMP clients with any authentication mode (v1/v2c community
5//! or v3 USM).
6
7use std::net::{SocketAddr, ToSocketAddrs};
8use std::sync::Arc;
9use std::time::Duration;
10
11use bytes::Bytes;
12
13use crate::client::walk::{OidOrdering, WalkMode};
14use crate::client::{Auth, ClientConfig, CommunityVersion, V3SecurityConfig};
15use crate::error::Error;
16use crate::transport::{TcpTransport, Transport, UdpTransport};
17use crate::v3::EngineCache;
18use crate::version::Version;
19
20use super::Client;
21
22/// Builder for constructing SNMP clients.
23///
24/// This is the single entry point for client construction. It supports all
25/// SNMP versions (v1, v2c, v3) through the [`Auth`] enum.
26///
27/// # Example
28///
29/// ```rust,no_run
30/// use async_snmp::{Auth, ClientBuilder};
31/// use std::time::Duration;
32///
33/// # async fn example() -> async_snmp::Result<()> {
34/// // Simple v2c client
35/// let client = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
36/// .connect().await?;
37///
38/// // v3 client with authentication
39/// let client = ClientBuilder::new("192.168.1.1:161",
40/// Auth::usm("admin").auth(async_snmp::AuthProtocol::Sha256, "password"))
41/// .timeout(Duration::from_secs(10))
42/// .retries(5)
43/// .connect().await?;
44/// # Ok(())
45/// # }
46/// ```
47pub struct ClientBuilder {
48 target: String,
49 auth: Auth,
50 timeout: Duration,
51 retries: u32,
52 max_oids_per_request: usize,
53 max_repetitions: u32,
54 walk_mode: WalkMode,
55 oid_ordering: OidOrdering,
56 max_walk_results: Option<usize>,
57 engine_cache: Option<Arc<EngineCache>>,
58 /// Override context engine ID (V3 only, for proxy/routing scenarios).
59 context_engine_id: Option<Vec<u8>>,
60}
61
62impl ClientBuilder {
63 /// Create a new client builder.
64 ///
65 /// # Arguments
66 ///
67 /// * `target` - The target address (e.g., "192.168.1.1:161")
68 /// * `auth` - Authentication configuration (community or USM)
69 ///
70 /// # Example
71 ///
72 /// ```rust,no_run
73 /// use async_snmp::{Auth, ClientBuilder};
74 ///
75 /// // Using Auth::default() for v2c with "public" community
76 /// let builder = ClientBuilder::new("192.168.1.1:161", Auth::default());
77 ///
78 /// // Using Auth::v1() for SNMPv1
79 /// let builder = ClientBuilder::new("192.168.1.1:161", Auth::v1("private"));
80 ///
81 /// // Using Auth::usm() for SNMPv3
82 /// let builder = ClientBuilder::new("192.168.1.1:161",
83 /// Auth::usm("admin").auth(async_snmp::AuthProtocol::Sha256, "password"));
84 /// ```
85 pub fn new(target: impl Into<String>, auth: impl Into<Auth>) -> Self {
86 Self {
87 target: target.into(),
88 auth: auth.into(),
89 timeout: Duration::from_secs(5),
90 retries: 3,
91 max_oids_per_request: 10,
92 max_repetitions: 25,
93 walk_mode: WalkMode::Auto,
94 oid_ordering: OidOrdering::Strict,
95 max_walk_results: None,
96 engine_cache: None,
97 context_engine_id: None,
98 }
99 }
100
101 /// Set the request timeout (default: 5 seconds).
102 ///
103 /// This is the time to wait for a response before retrying or failing.
104 /// The total time for a request may be `timeout * (retries + 1)`.
105 ///
106 /// # Example
107 ///
108 /// ```rust
109 /// use async_snmp::{Auth, ClientBuilder};
110 /// use std::time::Duration;
111 ///
112 /// let builder = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
113 /// .timeout(Duration::from_secs(10));
114 /// ```
115 pub fn timeout(mut self, timeout: Duration) -> Self {
116 self.timeout = timeout;
117 self
118 }
119
120 /// Set the number of retries (default: 3).
121 ///
122 /// The client will retry failed requests up to this many times before
123 /// returning an error. Set to 0 to disable retries.
124 ///
125 /// # Example
126 ///
127 /// ```rust
128 /// use async_snmp::{Auth, ClientBuilder};
129 ///
130 /// // Increase retries for unreliable networks
131 /// let builder = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
132 /// .retries(5);
133 ///
134 /// // Disable retries for fast failure detection
135 /// let builder = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
136 /// .retries(0);
137 /// ```
138 pub fn retries(mut self, retries: u32) -> Self {
139 self.retries = retries;
140 self
141 }
142
143 /// Set the maximum OIDs per request (default: 10).
144 ///
145 /// Requests with more OIDs than this limit are automatically split
146 /// into multiple batches. Some devices have lower limits on the number
147 /// of OIDs they can handle in a single request.
148 ///
149 /// # Example
150 ///
151 /// ```rust
152 /// use async_snmp::{Auth, ClientBuilder};
153 ///
154 /// // For devices with limited request handling capacity
155 /// let builder = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
156 /// .max_oids_per_request(5);
157 ///
158 /// // For high-capacity devices, increase to reduce round-trips
159 /// let builder = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
160 /// .max_oids_per_request(50);
161 /// ```
162 pub fn max_oids_per_request(mut self, max: usize) -> Self {
163 self.max_oids_per_request = max;
164 self
165 }
166
167 /// Set max-repetitions for GETBULK operations (default: 25).
168 ///
169 /// Controls how many rows are requested per GETBULK PDU during walk
170 /// operations. Higher values reduce round-trips but increase response size.
171 ///
172 /// # Example
173 ///
174 /// ```rust
175 /// use async_snmp::{Auth, ClientBuilder};
176 ///
177 /// // Reduce max-repetitions for devices with limited buffer sizes
178 /// let builder = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
179 /// .max_repetitions(10);
180 ///
181 /// // Increase for faster walks over reliable networks
182 /// let builder = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
183 /// .max_repetitions(50);
184 /// ```
185 pub fn max_repetitions(mut self, max: u32) -> Self {
186 self.max_repetitions = max;
187 self
188 }
189
190 /// Override walk behavior for devices with buggy GETBULK (default: Auto).
191 ///
192 /// - `WalkMode::Auto`: Use GETNEXT for v1, GETBULK for v2c/v3
193 /// - `WalkMode::GetNext`: Always use GETNEXT (slower but more compatible)
194 /// - `WalkMode::GetBulk`: Always use GETBULK (faster, errors on v1)
195 ///
196 /// # Example
197 ///
198 /// ```rust
199 /// use async_snmp::{Auth, ClientBuilder, WalkMode};
200 ///
201 /// // Force GETNEXT for devices with broken GETBULK implementation
202 /// let builder = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
203 /// .walk_mode(WalkMode::GetNext);
204 ///
205 /// // Force GETBULK for faster walks (only v2c/v3)
206 /// let builder = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
207 /// .walk_mode(WalkMode::GetBulk);
208 /// ```
209 pub fn walk_mode(mut self, mode: WalkMode) -> Self {
210 self.walk_mode = mode;
211 self
212 }
213
214 /// Set OID ordering behavior for walk operations (default: Strict).
215 ///
216 /// - `OidOrdering::Strict`: Require strictly increasing OIDs. Most efficient.
217 /// - `OidOrdering::AllowNonIncreasing`: Allow non-increasing OIDs with cycle
218 /// detection. Uses O(n) memory to track seen OIDs.
219 ///
220 /// Use `AllowNonIncreasing` for buggy agents that return OIDs out of order.
221 ///
222 /// **Warning**: `AllowNonIncreasing` uses O(n) memory. Always pair with
223 /// [`max_walk_results`](Self::max_walk_results) to bound memory usage.
224 ///
225 /// # Example
226 ///
227 /// ```rust
228 /// use async_snmp::{Auth, ClientBuilder, OidOrdering};
229 ///
230 /// // Use relaxed ordering with a safety limit
231 /// let builder = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
232 /// .oid_ordering(OidOrdering::AllowNonIncreasing)
233 /// .max_walk_results(10_000);
234 /// ```
235 pub fn oid_ordering(mut self, ordering: OidOrdering) -> Self {
236 self.oid_ordering = ordering;
237 self
238 }
239
240 /// Set maximum results from a single walk operation (default: unlimited).
241 ///
242 /// Safety limit to prevent runaway walks. Walk terminates normally when
243 /// limit is reached.
244 ///
245 /// # Example
246 ///
247 /// ```rust
248 /// use async_snmp::{Auth, ClientBuilder};
249 ///
250 /// // Limit walks to at most 10,000 results
251 /// let builder = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
252 /// .max_walk_results(10_000);
253 /// ```
254 pub fn max_walk_results(mut self, limit: usize) -> Self {
255 self.max_walk_results = Some(limit);
256 self
257 }
258
259 /// Set shared engine cache (V3 only, for high-throughput polling).
260 ///
261 /// Allows multiple clients to share discovered engine state, reducing
262 /// the number of discovery requests. This is particularly useful when
263 /// polling many devices with SNMPv3.
264 ///
265 /// # Example
266 ///
267 /// ```rust
268 /// use async_snmp::{Auth, AuthProtocol, ClientBuilder, EngineCache};
269 /// use std::sync::Arc;
270 ///
271 /// // Create a shared engine cache
272 /// let cache = Arc::new(EngineCache::new());
273 ///
274 /// // Multiple clients can share the same cache
275 /// let builder1 = ClientBuilder::new("192.168.1.1:161",
276 /// Auth::usm("admin").auth(AuthProtocol::Sha256, "password"))
277 /// .engine_cache(cache.clone());
278 ///
279 /// let builder2 = ClientBuilder::new("192.168.1.2:161",
280 /// Auth::usm("admin").auth(AuthProtocol::Sha256, "password"))
281 /// .engine_cache(cache.clone());
282 /// ```
283 pub fn engine_cache(mut self, cache: Arc<EngineCache>) -> Self {
284 self.engine_cache = Some(cache);
285 self
286 }
287
288 /// Override the context engine ID (V3 only).
289 ///
290 /// By default, the context engine ID is the same as the authoritative
291 /// engine ID discovered during engine discovery. Use this to override for:
292 /// - Proxy scenarios where requests route through an intermediate agent
293 /// - Devices that require a specific context engine ID
294 /// - Pre-configured engine IDs from device documentation
295 ///
296 /// The engine ID should be provided as raw bytes (not hex-encoded).
297 pub fn context_engine_id(mut self, engine_id: impl Into<Vec<u8>>) -> Self {
298 self.context_engine_id = Some(engine_id.into());
299 self
300 }
301
302 /// Validate the configuration.
303 fn validate(&self) -> Result<(), Error> {
304 if let Auth::Usm(usm) = &self.auth {
305 // Privacy requires authentication
306 if usm.priv_protocol.is_some() && usm.auth_protocol.is_none() {
307 return Err(Error::Config("privacy requires authentication".into()));
308 }
309 // Protocol requires password (unless using master keys)
310 if usm.auth_protocol.is_some()
311 && usm.auth_password.is_none()
312 && usm.master_keys.is_none()
313 {
314 return Err(Error::Config("auth protocol requires password".into()));
315 }
316 if usm.priv_protocol.is_some()
317 && usm.priv_password.is_none()
318 && usm.master_keys.is_none()
319 {
320 return Err(Error::Config("priv protocol requires password".into()));
321 }
322 }
323
324 // Validate walk mode for v1
325 if let Auth::Community {
326 version: CommunityVersion::V1,
327 ..
328 } = &self.auth
329 && self.walk_mode == WalkMode::GetBulk
330 {
331 return Err(Error::Config("GETBULK not supported in SNMPv1".into()));
332 }
333
334 Ok(())
335 }
336
337 /// Resolve target address to SocketAddr.
338 fn resolve_target(&self) -> Result<SocketAddr, Error> {
339 self.target
340 .to_socket_addrs()
341 .map_err(|e| Error::Io {
342 target: None,
343 source: e,
344 })?
345 .next()
346 .ok_or_else(|| Error::Io {
347 target: None,
348 source: std::io::Error::new(
349 std::io::ErrorKind::NotFound,
350 "could not resolve address",
351 ),
352 })
353 }
354
355 /// Build ClientConfig from the builder settings.
356 fn build_config(&self) -> ClientConfig {
357 match &self.auth {
358 Auth::Community { version, community } => {
359 let snmp_version = match version {
360 CommunityVersion::V1 => Version::V1,
361 CommunityVersion::V2c => Version::V2c,
362 };
363 ClientConfig {
364 version: snmp_version,
365 community: Bytes::copy_from_slice(community.as_bytes()),
366 timeout: self.timeout,
367 retries: self.retries,
368 max_oids_per_request: self.max_oids_per_request,
369 v3_security: None,
370 walk_mode: self.walk_mode,
371 oid_ordering: self.oid_ordering,
372 max_walk_results: self.max_walk_results,
373 max_repetitions: self.max_repetitions,
374 }
375 }
376 Auth::Usm(usm) => {
377 let mut security =
378 V3SecurityConfig::new(Bytes::copy_from_slice(usm.username.as_bytes()));
379
380 // Prefer master_keys over passwords if available
381 if let Some(ref master_keys) = usm.master_keys {
382 security = security.with_master_keys(master_keys.clone());
383 } else {
384 if let (Some(auth_proto), Some(auth_pass)) =
385 (usm.auth_protocol, &usm.auth_password)
386 {
387 security = security.auth(auth_proto, auth_pass.as_bytes().to_vec());
388 }
389
390 if let (Some(priv_proto), Some(priv_pass)) =
391 (usm.priv_protocol, &usm.priv_password)
392 {
393 security = security.privacy(priv_proto, priv_pass.as_bytes().to_vec());
394 }
395 }
396
397 ClientConfig {
398 version: Version::V3,
399 community: Bytes::new(),
400 timeout: self.timeout,
401 retries: self.retries,
402 max_oids_per_request: self.max_oids_per_request,
403 v3_security: Some(security),
404 walk_mode: self.walk_mode,
405 oid_ordering: self.oid_ordering,
406 max_walk_results: self.max_walk_results,
407 max_repetitions: self.max_repetitions,
408 }
409 }
410 }
411 }
412
413 /// Build the client with the given transport.
414 fn build_inner<T: Transport>(self, transport: T) -> Client<T> {
415 let config = self.build_config();
416
417 if let Some(cache) = self.engine_cache {
418 Client::with_engine_cache(transport, config, cache)
419 } else {
420 Client::new(transport, config)
421 }
422 }
423
424 /// Connect via UDP (default).
425 ///
426 /// Creates a new UDP socket and connects to the target address. This is the
427 /// recommended connection method for most use cases due to UDP's lower
428 /// overhead compared to TCP.
429 ///
430 /// For high-throughput scenarios with many targets, consider using
431 /// [`SharedUdpTransport`](crate::SharedUdpTransport) with [`build()`](Self::build).
432 ///
433 /// # Errors
434 ///
435 /// Returns an error if the configuration is invalid or the connection fails.
436 ///
437 /// # Example
438 ///
439 /// ```rust,no_run
440 /// use async_snmp::{Auth, ClientBuilder};
441 ///
442 /// # async fn example() -> async_snmp::Result<()> {
443 /// let client = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
444 /// .connect()
445 /// .await?;
446 /// # Ok(())
447 /// # }
448 /// ```
449 pub async fn connect(self) -> Result<Client<UdpTransport>, Error> {
450 self.validate()?;
451 let addr = self.resolve_target()?;
452 let transport = UdpTransport::connect(addr).await?;
453 Ok(self.build_inner(transport))
454 }
455
456 /// Connect via TCP.
457 ///
458 /// Establishes a TCP connection to the target. Use this when:
459 /// - UDP is blocked by firewalls
460 /// - Messages exceed UDP's maximum datagram size
461 /// - Reliable delivery is required
462 ///
463 /// Note that TCP has higher overhead than UDP due to connection setup
464 /// and per-message framing.
465 ///
466 /// # Errors
467 ///
468 /// Returns an error if the configuration is invalid or the connection fails.
469 ///
470 /// # Example
471 ///
472 /// ```rust,no_run
473 /// use async_snmp::{Auth, ClientBuilder};
474 ///
475 /// # async fn example() -> async_snmp::Result<()> {
476 /// let client = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
477 /// .connect_tcp()
478 /// .await?;
479 /// # Ok(())
480 /// # }
481 /// ```
482 pub async fn connect_tcp(self) -> Result<Client<TcpTransport>, Error> {
483 self.validate()?;
484 let addr = self.resolve_target()?;
485 let transport = TcpTransport::connect(addr).await?;
486 Ok(self.build_inner(transport))
487 }
488
489 /// Build a client with a custom transport.
490 ///
491 /// Use this method when you need:
492 /// - A shared UDP transport for high-throughput polling of many targets
493 /// - A custom transport implementation
494 /// - To reuse an existing transport
495 ///
496 /// # Errors
497 ///
498 /// Returns an error if the configuration is invalid.
499 ///
500 /// # Example
501 ///
502 /// ```rust,no_run
503 /// use async_snmp::{Auth, ClientBuilder, SharedUdpTransport};
504 ///
505 /// # async fn example() -> async_snmp::Result<()> {
506 /// // Create a shared transport for polling many targets
507 /// let shared = SharedUdpTransport::bind("0.0.0.0:0").await?;
508 ///
509 /// // Create a handle for a specific target
510 /// let handle = shared.handle("192.168.1.1:161".parse().unwrap());
511 ///
512 /// // Build client with the shared transport handle
513 /// let client = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"))
514 /// .build(handle)?;
515 /// # Ok(())
516 /// # }
517 /// ```
518 pub fn build<T: Transport>(self, transport: T) -> Result<Client<T>, Error> {
519 self.validate()?;
520 Ok(self.build_inner(transport))
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use crate::v3::{AuthProtocol, MasterKeys, PrivProtocol};
528
529 #[test]
530 fn test_builder_defaults() {
531 let builder = ClientBuilder::new("192.168.1.1:161", Auth::default());
532 assert_eq!(builder.target, "192.168.1.1:161");
533 assert_eq!(builder.timeout, Duration::from_secs(5));
534 assert_eq!(builder.retries, 3);
535 assert_eq!(builder.max_oids_per_request, 10);
536 assert_eq!(builder.max_repetitions, 25);
537 assert_eq!(builder.walk_mode, WalkMode::Auto);
538 assert_eq!(builder.oid_ordering, OidOrdering::Strict);
539 assert!(builder.max_walk_results.is_none());
540 assert!(builder.engine_cache.is_none());
541 assert!(builder.context_engine_id.is_none());
542 }
543
544 #[test]
545 fn test_builder_with_options() {
546 let cache = Arc::new(EngineCache::new());
547 let builder = ClientBuilder::new("192.168.1.1:161", Auth::v2c("private"))
548 .timeout(Duration::from_secs(10))
549 .retries(5)
550 .max_oids_per_request(20)
551 .max_repetitions(50)
552 .walk_mode(WalkMode::GetNext)
553 .oid_ordering(OidOrdering::AllowNonIncreasing)
554 .max_walk_results(1000)
555 .engine_cache(cache.clone())
556 .context_engine_id(vec![0x80, 0x00, 0x01]);
557
558 assert_eq!(builder.timeout, Duration::from_secs(10));
559 assert_eq!(builder.retries, 5);
560 assert_eq!(builder.max_oids_per_request, 20);
561 assert_eq!(builder.max_repetitions, 50);
562 assert_eq!(builder.walk_mode, WalkMode::GetNext);
563 assert_eq!(builder.oid_ordering, OidOrdering::AllowNonIncreasing);
564 assert_eq!(builder.max_walk_results, Some(1000));
565 assert!(builder.engine_cache.is_some());
566 assert_eq!(builder.context_engine_id, Some(vec![0x80, 0x00, 0x01]));
567 }
568
569 #[test]
570 fn test_validate_community_ok() {
571 let builder = ClientBuilder::new("192.168.1.1:161", Auth::v2c("public"));
572 assert!(builder.validate().is_ok());
573 }
574
575 #[test]
576 fn test_validate_usm_no_auth_no_priv_ok() {
577 let builder = ClientBuilder::new("192.168.1.1:161", Auth::usm("readonly"));
578 assert!(builder.validate().is_ok());
579 }
580
581 #[test]
582 fn test_validate_usm_auth_no_priv_ok() {
583 let builder = ClientBuilder::new(
584 "192.168.1.1:161",
585 Auth::usm("admin").auth(AuthProtocol::Sha256, "authpass"),
586 );
587 assert!(builder.validate().is_ok());
588 }
589
590 #[test]
591 fn test_validate_usm_auth_priv_ok() {
592 let builder = ClientBuilder::new(
593 "192.168.1.1:161",
594 Auth::usm("admin")
595 .auth(AuthProtocol::Sha256, "authpass")
596 .privacy(PrivProtocol::Aes128, "privpass"),
597 );
598 assert!(builder.validate().is_ok());
599 }
600
601 #[test]
602 fn test_validate_priv_without_auth_error() {
603 // Manually construct UsmAuth with priv but no auth
604 let usm = crate::client::UsmAuth {
605 username: "user".to_string(),
606 auth_protocol: None,
607 auth_password: None,
608 priv_protocol: Some(PrivProtocol::Aes128),
609 priv_password: Some("privpass".to_string()),
610 context_name: None,
611 master_keys: None,
612 };
613 let builder = ClientBuilder::new("192.168.1.1:161", Auth::Usm(usm));
614 let err = builder.validate().unwrap_err();
615 assert!(
616 matches!(err, Error::Config(msg) if msg.contains("privacy requires authentication"))
617 );
618 }
619
620 #[test]
621 fn test_validate_auth_protocol_without_password_error() {
622 // Manually construct UsmAuth with auth protocol but no password
623 let usm = crate::client::UsmAuth {
624 username: "user".to_string(),
625 auth_protocol: Some(AuthProtocol::Sha256),
626 auth_password: None,
627 priv_protocol: None,
628 priv_password: None,
629 context_name: None,
630 master_keys: None,
631 };
632 let builder = ClientBuilder::new("192.168.1.1:161", Auth::Usm(usm));
633 let err = builder.validate().unwrap_err();
634 assert!(
635 matches!(err, Error::Config(msg) if msg.contains("auth protocol requires password"))
636 );
637 }
638
639 #[test]
640 fn test_validate_priv_protocol_without_password_error() {
641 // Manually construct UsmAuth with priv protocol but no password
642 let usm = crate::client::UsmAuth {
643 username: "user".to_string(),
644 auth_protocol: Some(AuthProtocol::Sha256),
645 auth_password: Some("authpass".to_string()),
646 priv_protocol: Some(PrivProtocol::Aes128),
647 priv_password: None,
648 context_name: None,
649 master_keys: None,
650 };
651 let builder = ClientBuilder::new("192.168.1.1:161", Auth::Usm(usm));
652 let err = builder.validate().unwrap_err();
653 assert!(
654 matches!(err, Error::Config(msg) if msg.contains("priv protocol requires password"))
655 );
656 }
657
658 #[test]
659 fn test_builder_with_usm_builder() {
660 // Test that UsmBuilder can be passed directly (via Into<Auth>)
661 let builder = ClientBuilder::new(
662 "192.168.1.1:161",
663 Auth::usm("admin").auth(AuthProtocol::Sha256, "pass"),
664 );
665 assert!(builder.validate().is_ok());
666 }
667
668 #[test]
669 fn test_validate_master_keys_bypass_auth_password() {
670 // When master keys are set, auth password is not required
671 let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpass");
672 let usm = crate::client::UsmAuth {
673 username: "user".to_string(),
674 auth_protocol: Some(AuthProtocol::Sha256),
675 auth_password: None, // No password
676 priv_protocol: None,
677 priv_password: None,
678 context_name: None,
679 master_keys: Some(master_keys),
680 };
681 let builder = ClientBuilder::new("192.168.1.1:161", Auth::Usm(usm));
682 assert!(builder.validate().is_ok());
683 }
684
685 #[test]
686 fn test_validate_master_keys_bypass_priv_password() {
687 // When master keys are set, priv password is not required
688 let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpass")
689 .with_privacy(PrivProtocol::Aes128, b"privpass");
690 let usm = crate::client::UsmAuth {
691 username: "user".to_string(),
692 auth_protocol: Some(AuthProtocol::Sha256),
693 auth_password: None, // No password
694 priv_protocol: Some(PrivProtocol::Aes128),
695 priv_password: None, // No password
696 context_name: None,
697 master_keys: Some(master_keys),
698 };
699 let builder = ClientBuilder::new("192.168.1.1:161", Auth::Usm(usm));
700 assert!(builder.validate().is_ok());
701 }
702}