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}