1mod handlers;
53mod types;
54mod varbind;
55
56use std::collections::HashMap;
57use std::net::SocketAddr;
58use std::sync::Arc;
59use std::sync::atomic::{AtomicU32, Ordering};
60use std::time::Instant;
61
62use bytes::Bytes;
63use tokio::net::UdpSocket;
64use tracing::instrument;
65
66use crate::ber::Decoder;
67use crate::error::internal::DecodeErrorKind;
68use crate::error::{Error, Result};
69use crate::oid::Oid;
70use crate::pdu::TrapV1Pdu;
71use crate::util::bind_udp_socket;
72use crate::v3::SaltCounter;
73use crate::varbind::VarBind;
74use crate::version::Version;
75
76pub use types::{DerivedKeys, UsmConfig};
78pub use varbind::validate_notification_varbinds;
79
80pub mod oids {
82 use crate::oid;
83
84 pub fn sys_uptime() -> crate::Oid {
86 oid!(1, 3, 6, 1, 2, 1, 1, 3, 0)
87 }
88
89 pub fn snmp_trap_oid() -> crate::Oid {
91 oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0)
92 }
93
94 pub fn snmp_trap_enterprise() -> crate::Oid {
96 oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 3, 0)
97 }
98
99 pub fn snmp_trap_address() -> crate::Oid {
101 oid!(1, 3, 6, 1, 6, 3, 18, 1, 3, 0)
102 }
103
104 pub fn snmp_traps() -> crate::Oid {
106 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5)
107 }
108
109 pub fn cold_start() -> crate::Oid {
111 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)
112 }
113
114 pub fn warm_start() -> crate::Oid {
116 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2)
117 }
118
119 pub fn link_down() -> crate::Oid {
121 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3)
122 }
123
124 pub fn link_up() -> crate::Oid {
126 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4)
127 }
128
129 pub fn auth_failure() -> crate::Oid {
131 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5)
132 }
133
134 pub fn egp_neighbor_loss() -> crate::Oid {
136 oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6)
137 }
138}
139
140pub struct NotificationReceiverBuilder {
144 bind_addr: String,
145 usm_users: HashMap<Bytes, UsmConfig>,
146 engine_id: Option<Vec<u8>>,
147 engine_boots: u32,
148}
149
150impl NotificationReceiverBuilder {
151 pub fn new() -> Self {
157 Self {
158 bind_addr: "0.0.0.0:162".to_string(),
159 usm_users: HashMap::new(),
160 engine_id: None,
161 engine_boots: 1,
162 }
163 }
164
165 pub fn bind(mut self, addr: impl Into<String>) -> Self {
169 self.bind_addr = addr.into();
170 self
171 }
172
173 pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
194 where
195 F: FnOnce(UsmConfig) -> UsmConfig,
196 {
197 let username_bytes: Bytes = username.into();
198 let config = configure(UsmConfig::new(username_bytes.clone()));
199 self.usm_users.insert(username_bytes, config);
200 self
201 }
202
203 pub fn engine_id(mut self, engine_id: impl Into<Vec<u8>>) -> Self {
208 self.engine_id = Some(engine_id.into());
209 self
210 }
211
212 pub fn engine_boots(mut self, boots: u32) -> Self {
217 self.engine_boots = boots;
218 self
219 }
220
221 pub async fn build(self) -> Result<NotificationReceiver> {
223 let bind_addr: SocketAddr = self.bind_addr.parse().map_err(|_| {
224 Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
225 })?;
226
227 let socket = bind_udp_socket(bind_addr, None, None, false)
228 .await
229 .map_err(|e| Error::Network {
230 target: bind_addr,
231 source: e,
232 })?;
233
234 let local_addr = socket.local_addr().map_err(|e| Error::Network {
235 target: bind_addr,
236 source: e,
237 })?;
238
239 let engine_id: Bytes = self.engine_id.map(Bytes::from).unwrap_or_else(|| {
240 let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01];
241 let timestamp = std::time::SystemTime::now()
242 .duration_since(std::time::UNIX_EPOCH)
243 .unwrap_or_default()
244 .as_secs();
245 id.extend_from_slice(×tamp.to_be_bytes());
246 Bytes::from(id)
247 });
248
249 Ok(NotificationReceiver {
250 inner: Arc::new(ReceiverInner {
251 socket,
252 local_addr,
253 usm_users: self.usm_users,
254 engine_id,
255 salt_counter: SaltCounter::new(),
256 engine_boots_base: self.engine_boots,
257 engine_start: Instant::now(),
258 usm_unknown_engine_ids: AtomicU32::new(0),
259 }),
260 })
261 }
262}
263
264impl Default for NotificationReceiverBuilder {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270#[derive(Debug, Clone)]
277pub enum Notification {
278 TrapV1 {
280 community: Bytes,
282 trap: TrapV1Pdu,
284 },
285
286 TrapV2c {
288 community: Bytes,
290 uptime: u32,
292 trap_oid: Oid,
294 varbinds: Vec<VarBind>,
296 request_id: i32,
298 },
299
300 TrapV3 {
302 username: Bytes,
304 context_engine_id: Bytes,
306 context_name: Bytes,
308 uptime: u32,
310 trap_oid: Oid,
312 varbinds: Vec<VarBind>,
314 request_id: i32,
316 },
317
318 InformV2c {
322 community: Bytes,
324 uptime: u32,
326 trap_oid: Oid,
328 varbinds: Vec<VarBind>,
330 request_id: i32,
332 },
333
334 InformV3 {
338 username: Bytes,
340 context_engine_id: Bytes,
342 context_name: Bytes,
344 uptime: u32,
346 trap_oid: Oid,
348 varbinds: Vec<VarBind>,
350 request_id: i32,
352 },
353}
354
355impl Notification {
356 pub fn trap_oid(&self) -> Result<Oid> {
361 match self {
362 Notification::TrapV1 { trap, .. } => trap.v2_trap_oid(),
363 Notification::TrapV2c { trap_oid, .. }
364 | Notification::TrapV3 { trap_oid, .. }
365 | Notification::InformV2c { trap_oid, .. }
366 | Notification::InformV3 { trap_oid, .. } => Ok(trap_oid.clone()),
367 }
368 }
369
370 pub fn uptime(&self) -> u32 {
372 match self {
373 Notification::TrapV1 { trap, .. } => trap.time_stamp,
374 Notification::TrapV2c { uptime, .. }
375 | Notification::TrapV3 { uptime, .. }
376 | Notification::InformV2c { uptime, .. }
377 | Notification::InformV3 { uptime, .. } => *uptime,
378 }
379 }
380
381 pub fn varbinds(&self) -> &[VarBind] {
383 match self {
384 Notification::TrapV1 { trap, .. } => &trap.varbinds,
385 Notification::TrapV2c { varbinds, .. }
386 | Notification::TrapV3 { varbinds, .. }
387 | Notification::InformV2c { varbinds, .. }
388 | Notification::InformV3 { varbinds, .. } => varbinds,
389 }
390 }
391
392 pub fn is_confirmed(&self) -> bool {
394 matches!(
395 self,
396 Notification::InformV2c { .. } | Notification::InformV3 { .. }
397 )
398 }
399
400 pub fn version(&self) -> Version {
402 match self {
403 Notification::TrapV1 { .. } => Version::V1,
404 Notification::TrapV2c { .. } | Notification::InformV2c { .. } => Version::V2c,
405 Notification::TrapV3 { .. } | Notification::InformV3 { .. } => Version::V3,
406 }
407 }
408}
409
410pub struct NotificationReceiver {
436 inner: Arc<ReceiverInner>,
437}
438
439struct ReceiverInner {
440 socket: UdpSocket,
441 local_addr: SocketAddr,
442 usm_users: HashMap<Bytes, UsmConfig>,
444 engine_id: Bytes,
446 salt_counter: SaltCounter,
448 engine_boots_base: u32,
450 engine_start: Instant,
452 usm_unknown_engine_ids: AtomicU32,
454}
455
456impl NotificationReceiver {
457 pub fn builder() -> NotificationReceiverBuilder {
461 NotificationReceiverBuilder::new()
462 }
463
464 pub async fn bind(addr: impl AsRef<str>) -> Result<Self> {
484 let addr_str = addr.as_ref();
485 let bind_addr: SocketAddr = addr_str
486 .parse()
487 .map_err(|_| Error::Config(format!("invalid bind address: {}", addr_str).into()))?;
488
489 let socket = bind_udp_socket(bind_addr, None, None, false)
490 .await
491 .map_err(|e| Error::Network {
492 target: bind_addr,
493 source: e,
494 })?;
495
496 let local_addr = socket.local_addr().map_err(|e| Error::Network {
497 target: bind_addr,
498 source: e,
499 })?;
500
501 let engine_id: Bytes = {
502 let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01];
503 let timestamp = std::time::SystemTime::now()
504 .duration_since(std::time::UNIX_EPOCH)
505 .unwrap_or_default()
506 .as_secs();
507 id.extend_from_slice(×tamp.to_be_bytes());
508 Bytes::from(id)
509 };
510
511 Ok(Self {
512 inner: Arc::new(ReceiverInner {
513 socket,
514 local_addr,
515 usm_users: HashMap::new(),
516 engine_id,
517 salt_counter: SaltCounter::new(),
518 engine_boots_base: 1,
519 engine_start: Instant::now(),
520 usm_unknown_engine_ids: AtomicU32::new(0),
521 }),
522 })
523 }
524
525 pub fn local_addr(&self) -> SocketAddr {
527 self.inner.local_addr
528 }
529
530 pub fn engine_id(&self) -> &[u8] {
532 &self.inner.engine_id
533 }
534
535 pub fn usm_unknown_engine_ids(&self) -> u32 {
537 self.inner.usm_unknown_engine_ids.load(Ordering::Relaxed)
538 }
539
540 #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
547 pub async fn recv(&self) -> Result<(Notification, SocketAddr)> {
548 let mut buf = vec![0u8; 65535];
549
550 loop {
551 let (len, source) =
552 self.inner
553 .socket
554 .recv_from(&mut buf)
555 .await
556 .map_err(|e| Error::Network {
557 target: self.inner.local_addr,
558 source: e,
559 })?;
560
561 let data = Bytes::copy_from_slice(&buf[..len]);
562
563 match self.parse_and_respond(data, source).await {
564 Ok(Some(notification)) => return Ok((notification, source)),
565 Ok(None) => continue, Err(e) => {
567 tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, error = %e }, "failed to parse notification");
569 continue;
570 }
571 }
572 }
573 }
574
575 async fn parse_and_respond(
579 &self,
580 data: Bytes,
581 source: SocketAddr,
582 ) -> Result<Option<Notification>> {
583 let mut decoder = Decoder::with_target(data.clone(), source);
585 let mut seq = decoder.read_sequence()?;
586 let version_num = seq.read_integer()?;
587 let version = Version::from_i32(version_num).ok_or_else(|| {
588 tracing::debug!(target: "async_snmp::notification", { source = %source, kind = %DecodeErrorKind::UnknownVersion(version_num) }, "unknown SNMP version");
589 Error::MalformedResponse { target: source }.boxed()
590 })?;
591 drop(seq);
592 drop(decoder);
593
594 match version {
595 Version::V1 => self.handle_v1(data, source).await,
596 Version::V2c => self.handle_v2c(data, source).await,
597 Version::V3 => self.handle_v3(data, source).await,
598 }
599 }
600}
601
602impl Clone for NotificationReceiver {
603 fn clone(&self) -> Self {
604 Self {
605 inner: Arc::clone(&self.inner),
606 }
607 }
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613 use crate::message::SecurityLevel;
614 use crate::oid;
615 use crate::pdu::GenericTrap;
616 use crate::v3::AuthProtocol;
617
618 #[test]
619 fn test_notification_trap_v1() {
620 let trap = TrapV1Pdu::new(
621 oid!(1, 3, 6, 1, 4, 1, 9999),
622 [192, 168, 1, 1],
623 GenericTrap::LinkDown,
624 0,
625 12345,
626 vec![],
627 );
628
629 let notification = Notification::TrapV1 {
630 community: Bytes::from_static(b"public"),
631 trap,
632 };
633
634 assert!(!notification.is_confirmed());
635 assert_eq!(notification.version(), Version::V1);
636 assert_eq!(notification.uptime(), 12345);
637 assert_eq!(notification.trap_oid().unwrap(), oids::link_down());
638 }
639
640 #[test]
641 fn test_notification_trap_v2c() {
642 let notification = Notification::TrapV2c {
643 community: Bytes::from_static(b"public"),
644 uptime: 54321,
645 trap_oid: oids::link_up(),
646 varbinds: vec![],
647 request_id: 1,
648 };
649
650 assert!(!notification.is_confirmed());
651 assert_eq!(notification.version(), Version::V2c);
652 assert_eq!(notification.uptime(), 54321);
653 assert_eq!(notification.trap_oid().unwrap(), oids::link_up());
654 }
655
656 #[test]
657 fn test_notification_inform() {
658 let notification = Notification::InformV2c {
659 community: Bytes::from_static(b"public"),
660 uptime: 11111,
661 trap_oid: oids::cold_start(),
662 varbinds: vec![],
663 request_id: 42,
664 };
665
666 assert!(notification.is_confirmed());
667 assert_eq!(notification.version(), Version::V2c);
668 }
669
670 #[test]
671 fn test_notification_receiver_builder_default() {
672 let builder = NotificationReceiverBuilder::new();
673 assert_eq!(builder.bind_addr, "0.0.0.0:162");
674 assert!(builder.usm_users.is_empty());
675 }
676
677 #[test]
678 fn test_notification_receiver_builder_with_user() {
679 let builder = NotificationReceiverBuilder::new()
680 .bind("0.0.0.0:1162")
681 .usm_user("trapuser", |u| u.auth(AuthProtocol::Sha1, b"authpass"));
682
683 assert_eq!(builder.bind_addr, "0.0.0.0:1162");
684 assert_eq!(builder.usm_users.len(), 1);
685
686 let user = builder
687 .usm_users
688 .get(&Bytes::from_static(b"trapuser"))
689 .unwrap();
690 assert_eq!(user.security_level(), SecurityLevel::AuthNoPriv);
691 }
692
693 #[test]
694 fn test_notification_v3_inform() {
695 let notification = Notification::InformV3 {
696 username: Bytes::from_static(b"testuser"),
697 context_engine_id: Bytes::from_static(b"engine123"),
698 context_name: Bytes::new(),
699 uptime: 99999,
700 trap_oid: oids::warm_start(),
701 varbinds: vec![],
702 request_id: 100,
703 };
704
705 assert!(notification.is_confirmed());
706 assert_eq!(notification.version(), Version::V3);
707 assert_eq!(notification.uptime(), 99999);
708 assert_eq!(notification.trap_oid().unwrap(), oids::warm_start());
709 }
710
711 #[test]
712 fn test_notification_trap_v1_enterprise_specific_oid() {
713 let trap = TrapV1Pdu::new(
714 oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
715 [192, 168, 1, 1],
716 GenericTrap::EnterpriseSpecific,
717 42,
718 12345,
719 vec![],
720 );
721
722 let notification = Notification::TrapV1 {
723 community: Bytes::from_static(b"public"),
724 trap,
725 };
726
727 assert_eq!(
728 notification.trap_oid().unwrap(),
729 oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42)
730 );
731 }
732
733 #[test]
734 fn test_compute_engine_boots_time_basic() {
735 let (boots, time) = crate::v3::compute_engine_boots_time(1, 1000);
736 assert_eq!(boots, 1);
737 assert_eq!(time, 1000);
738 }
739
740 #[test]
741 fn test_compute_engine_boots_time_zero_elapsed() {
742 let (boots, time) = crate::v3::compute_engine_boots_time(1, 0);
743 assert_eq!(boots, 1);
744 assert_eq!(time, 0);
745 }
746
747 #[test]
748 fn test_builder_engine_boots_default() {
749 let builder = NotificationReceiverBuilder::new();
750 assert_eq!(builder.engine_boots, 1);
751 }
752
753 #[test]
754 fn test_builder_engine_boots_custom() {
755 let builder = NotificationReceiverBuilder::new().engine_boots(5);
756 assert_eq!(builder.engine_boots, 5);
757 }
758
759 fn build_authed_v3_inform(
762 engine_id: &[u8],
763 engine_boots: u32,
764 engine_time: u32,
765 username: &[u8],
766 auth_password: &[u8],
767 auth_protocol: AuthProtocol,
768 ) -> Bytes {
769 use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
770 use crate::pdu::{Pdu, PduType};
771 use crate::v3::auth::authenticate_message;
772 use crate::v3::{LocalizedKey, UsmSecurityParams};
773 use crate::value::Value;
774
775 let auth_key =
776 LocalizedKey::from_password(auth_protocol, auth_password, engine_id).unwrap();
777 let mac_len = auth_key.mac_len();
778
779 let pdu = Pdu {
781 pdu_type: PduType::InformRequest,
782 request_id: 1,
783 error_status: 0,
784 error_index: 0,
785 varbinds: vec![
786 VarBind::new(oids::sys_uptime(), Value::TimeTicks(1000)),
787 VarBind::new(
788 oids::snmp_trap_oid(),
789 Value::ObjectIdentifier(oids::cold_start()),
790 ),
791 ],
792 };
793
794 let global = MsgGlobalData::new(1, 65507, MsgFlags::new(SecurityLevel::AuthNoPriv, false));
795
796 let usm_params = UsmSecurityParams::new(
797 Bytes::copy_from_slice(engine_id),
798 engine_boots,
799 engine_time,
800 Bytes::copy_from_slice(username),
801 )
802 .with_auth_placeholder(mac_len);
803
804 let scoped = ScopedPdu::new(Bytes::copy_from_slice(engine_id), Bytes::new(), pdu);
805 let msg = V3Message::new(global, usm_params.encode(), scoped);
806 let mut msg_bytes = msg.encode().to_vec();
807
808 let (auth_offset, auth_len) =
810 UsmSecurityParams::find_auth_params_offset(&msg_bytes).unwrap();
811 authenticate_message(&auth_key, &mut msg_bytes, auth_offset, auth_len).unwrap();
812
813 Bytes::from(msg_bytes)
814 }
815
816 #[tokio::test]
817 async fn test_v3_inform_outside_time_window_rejected() {
818 let receiver = NotificationReceiver::builder()
819 .bind("127.0.0.1:0")
820 .engine_id(b"test-engine".to_vec())
821 .engine_boots(1)
822 .usm_user("informuser", |u| {
823 u.auth(AuthProtocol::Sha1, b"authpass12345678")
824 })
825 .build()
826 .await
827 .unwrap();
828
829 let engine_id = b"test-engine";
830 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
831
832 let msg = build_authed_v3_inform(
834 engine_id,
835 1, 5000, b"informuser",
838 b"authpass12345678",
839 AuthProtocol::Sha1,
840 );
841
842 let result = receiver.handle_v3(msg, source).await;
843 assert!(
844 result.is_err(),
845 "message with engine_time=5000 should be rejected (outside 150s window)"
846 );
847 }
848
849 #[tokio::test]
850 async fn test_v3_inform_wrong_boots_rejected() {
851 let receiver = NotificationReceiver::builder()
852 .bind("127.0.0.1:0")
853 .engine_id(b"test-engine".to_vec())
854 .engine_boots(1)
855 .usm_user("informuser", |u| {
856 u.auth(AuthProtocol::Sha1, b"authpass12345678")
857 })
858 .build()
859 .await
860 .unwrap();
861
862 let engine_id = b"test-engine";
863 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
864
865 let msg = build_authed_v3_inform(
867 engine_id,
868 2, 0, b"informuser",
871 b"authpass12345678",
872 AuthProtocol::Sha1,
873 );
874
875 let result = receiver.handle_v3(msg, source).await;
876 assert!(
877 result.is_err(),
878 "message with wrong engine_boots should be rejected"
879 );
880 }
881
882 #[tokio::test]
883 async fn test_v3_inform_within_time_window_accepted() {
884 let receiver = NotificationReceiver::builder()
885 .bind("127.0.0.1:0")
886 .engine_id(b"test-engine".to_vec())
887 .engine_boots(1)
888 .usm_user("informuser", |u| {
889 u.auth(AuthProtocol::Sha1, b"authpass12345678")
890 })
891 .build()
892 .await
893 .unwrap();
894
895 let engine_id = b"test-engine";
896 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
897
898 let msg = build_authed_v3_inform(
900 engine_id,
901 1, 0, b"informuser",
904 b"authpass12345678",
905 AuthProtocol::Sha1,
906 );
907
908 let result = receiver.handle_v3(msg, source).await;
909 match result {
915 Ok(Some(_)) => {} Err(e) => {
917 let err_str = format!("{}", e);
918 assert!(
919 !err_str.contains("Auth"),
920 "should not be an auth error for valid time window, got: {}",
921 err_str
922 );
923 }
924 Ok(None) => panic!("should not return None for a valid InformRequest"),
925 }
926 }
927
928 fn build_v3_discovery_request(msg_id: i32, reportable: bool) -> Bytes {
930 use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
931 use crate::pdu::{Pdu, PduType};
932 use crate::v3::UsmSecurityParams;
933
934 let pdu = Pdu {
935 pdu_type: PduType::GetRequest,
936 request_id: 0,
937 error_status: 0,
938 error_index: 0,
939 varbinds: vec![],
940 };
941
942 let global = MsgGlobalData::new(
943 msg_id,
944 65507,
945 MsgFlags::new(SecurityLevel::NoAuthNoPriv, reportable),
946 );
947
948 let usm_params = UsmSecurityParams::new(
949 Bytes::new(), 0,
951 0,
952 Bytes::new(), );
954
955 let scoped = ScopedPdu::new(Bytes::new(), Bytes::new(), pdu);
956 let msg = V3Message::new(global, usm_params.encode(), scoped);
957 msg.encode()
958 }
959
960 #[tokio::test]
961 async fn test_v3_discovery_gets_response() {
962 let receiver = NotificationReceiver::builder()
963 .bind("127.0.0.1:0")
964 .engine_id(b"test-discovery-engine".to_vec())
965 .build()
966 .await
967 .unwrap();
968
969 let recv_addr = receiver.local_addr();
970
971 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
973 let client_addr = client.local_addr().unwrap();
974
975 let discovery_msg = build_v3_discovery_request(42, true);
976 client.send_to(&discovery_msg, recv_addr).await.unwrap();
977
978 let result = receiver.handle_v3(discovery_msg, client_addr).await;
981
982 assert!(result.is_ok());
984 assert!(result.unwrap().is_none());
985
986 assert_eq!(receiver.usm_unknown_engine_ids(), 1);
988 }
989
990 #[tokio::test]
991 async fn test_v3_discovery_non_reportable_ignored() {
992 let receiver = NotificationReceiver::builder()
993 .bind("127.0.0.1:0")
994 .engine_id(b"test-discovery-engine".to_vec())
995 .build()
996 .await
997 .unwrap();
998
999 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1000 let discovery_msg = build_v3_discovery_request(42, false);
1001
1002 let result = receiver.handle_v3(discovery_msg, source).await;
1003
1004 assert!(result.is_ok());
1006 assert!(result.unwrap().is_none());
1007 assert_eq!(receiver.usm_unknown_engine_ids(), 0);
1008 }
1009
1010 #[tokio::test]
1011 async fn test_v3_engine_id_mismatch_ignored() {
1012 let receiver = NotificationReceiver::builder()
1013 .bind("127.0.0.1:0")
1014 .engine_id(b"my-receiver-engine".to_vec())
1015 .engine_boots(1)
1016 .usm_user("informuser", |u| {
1017 u.auth(AuthProtocol::Sha1, b"authpass12345678")
1018 })
1019 .build()
1020 .await
1021 .unwrap();
1022
1023 let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1024
1025 let msg = build_authed_v3_inform(
1027 b"wrong-engine-id",
1028 1,
1029 0,
1030 b"informuser",
1031 b"authpass12345678",
1032 AuthProtocol::Sha1,
1033 );
1034
1035 let result = receiver.handle_v3(msg, source).await;
1036 assert!(result.is_ok());
1037 assert!(
1038 result.unwrap().is_none(),
1039 "engine ID mismatch should return None"
1040 );
1041 }
1042
1043 #[test]
1044 fn test_auto_generated_engine_id_non_empty() {
1045 let builder = NotificationReceiverBuilder::new();
1046 assert!(builder.engine_id.is_none());
1048 }
1049
1050 #[tokio::test]
1051 async fn test_bind_generates_engine_id() {
1052 let receiver = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
1053 assert!(!receiver.engine_id().is_empty());
1054 assert_eq!(receiver.engine_id()[0], 0x80);
1056 }
1057
1058 #[tokio::test]
1059 async fn test_builder_generates_engine_id() {
1060 let receiver = NotificationReceiver::builder()
1061 .bind("127.0.0.1:0")
1062 .build()
1063 .await
1064 .unwrap();
1065 assert!(!receiver.engine_id().is_empty());
1066 assert_eq!(receiver.engine_id()[0], 0x80);
1067 }
1068
1069 #[tokio::test]
1070 async fn test_builder_custom_engine_id() {
1071 let receiver = NotificationReceiver::builder()
1072 .bind("127.0.0.1:0")
1073 .engine_id(b"custom-engine".to_vec())
1074 .build()
1075 .await
1076 .unwrap();
1077 assert_eq!(receiver.engine_id(), b"custom-engine");
1078 }
1079}