openzeppelin_monitor/services/notification/
error.rs

1//! Notification error types and handling.
2//!
3//! Provides error types for notification-related operations,
4//! including network issues and configuration problems.
5
6use crate::utils::logging::error::{ErrorContext, TraceableError};
7use std::collections::HashMap;
8use thiserror::Error as ThisError;
9
10/// Represents errors that can occur during notification operations
11#[derive(ThisError, Debug)]
12pub enum NotificationError {
13	/// Errors related to network connectivity issues
14	#[error("Network error: {0}")]
15	NetworkError(Box<ErrorContext>),
16
17	/// Errors related to malformed requests or invalid responses
18	#[error("Config error: {0}")]
19	ConfigError(Box<ErrorContext>),
20
21	/// Errors related to internal processing errors
22	#[error("Internal error: {0}")]
23	InternalError(Box<ErrorContext>),
24
25	/// Errors related to script execution
26	#[error("Script execution error: {0}")]
27	ExecutionError(Box<ErrorContext>),
28
29	/// Error when Notifier `notify`` method fails (e.g., webhook failure, parsing error, invalid signature)
30	#[error("Notification failed: {0}")]
31	NotifyFailed(Box<ErrorContext>),
32}
33
34impl NotificationError {
35	// Network error
36	pub fn network_error(
37		msg: impl Into<String>,
38		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
39		metadata: Option<HashMap<String, String>>,
40	) -> Self {
41		Self::NetworkError(Box::new(ErrorContext::new_with_log(msg, source, metadata)))
42	}
43
44	// Config error
45	pub fn config_error(
46		msg: impl Into<String>,
47		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
48		metadata: Option<HashMap<String, String>>,
49	) -> Self {
50		Self::ConfigError(Box::new(ErrorContext::new_with_log(msg, source, metadata)))
51	}
52
53	// Internal error
54	pub fn internal_error(
55		msg: impl Into<String>,
56		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
57		metadata: Option<HashMap<String, String>>,
58	) -> Self {
59		Self::InternalError(Box::new(ErrorContext::new_with_log(msg, source, metadata)))
60	}
61
62	// Execution error
63	pub fn execution_error(
64		msg: impl Into<String>,
65		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
66		metadata: Option<HashMap<String, String>>,
67	) -> Self {
68		Self::ExecutionError(Box::new(ErrorContext::new_with_log(msg, source, metadata)))
69	}
70
71	// Notify failed error
72	pub fn notify_failed(
73		msg: impl Into<String>,
74		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
75		metadata: Option<HashMap<String, String>>,
76	) -> Self {
77		Self::NotifyFailed(Box::new(ErrorContext::new_with_log(msg, source, metadata)))
78	}
79}
80
81impl TraceableError for NotificationError {
82	fn trace_id(&self) -> String {
83		match self {
84			Self::NetworkError(ctx) => ctx.trace_id.clone(),
85			Self::ConfigError(ctx) => ctx.trace_id.clone(),
86			Self::InternalError(ctx) => ctx.trace_id.clone(),
87			Self::ExecutionError(ctx) => ctx.trace_id.clone(),
88			Self::NotifyFailed(ctx) => ctx.trace_id.clone(),
89		}
90	}
91}
92
93#[cfg(test)]
94mod tests {
95	use super::*;
96	use std::io::{Error as IoError, ErrorKind};
97
98	#[test]
99	fn test_network_error_formatting() {
100		let error = NotificationError::network_error("test error", None, None);
101		assert_eq!(error.to_string(), "Network error: test error");
102
103		let source_error = IoError::new(ErrorKind::NotFound, "test source");
104		let error = NotificationError::network_error(
105			"test error",
106			Some(Box::new(source_error)),
107			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
108		);
109		assert_eq!(error.to_string(), "Network error: test error [key1=value1]");
110	}
111
112	#[test]
113	fn test_config_error_formatting() {
114		let error = NotificationError::config_error("test error", None, None);
115		assert_eq!(error.to_string(), "Config error: test error");
116
117		let source_error = IoError::new(ErrorKind::NotFound, "test source");
118		let error = NotificationError::config_error(
119			"test error",
120			Some(Box::new(source_error)),
121			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
122		);
123		assert_eq!(error.to_string(), "Config error: test error [key1=value1]");
124	}
125
126	#[test]
127	fn test_internal_error_formatting() {
128		let error = NotificationError::internal_error("test error", None, None);
129		assert_eq!(error.to_string(), "Internal error: test error");
130
131		let source_error = IoError::new(ErrorKind::NotFound, "test source");
132		let error = NotificationError::internal_error(
133			"test error",
134			Some(Box::new(source_error)),
135			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
136		);
137		assert_eq!(
138			error.to_string(),
139			"Internal error: test error [key1=value1]"
140		);
141	}
142
143	#[test]
144	fn test_execution_error_formatting() {
145		let error = NotificationError::execution_error("test error", None, None);
146		assert_eq!(error.to_string(), "Script execution error: test error");
147
148		let source_error = IoError::new(ErrorKind::NotFound, "test source");
149		let error = NotificationError::execution_error(
150			"test error",
151			Some(Box::new(source_error)),
152			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
153		);
154		assert_eq!(
155			error.to_string(),
156			"Script execution error: test error [key1=value1]"
157		);
158	}
159
160	#[test]
161	fn test_notify_failed_error_formatting() {
162		let error = NotificationError::notify_failed("test error", None, None);
163		assert_eq!(error.to_string(), "Notification failed: test error");
164
165		let source_error = IoError::new(ErrorKind::NotFound, "test source");
166		let error = NotificationError::notify_failed(
167			"test error",
168			Some(Box::new(source_error)),
169			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
170		);
171		assert_eq!(
172			error.to_string(),
173			"Notification failed: test error [key1=value1]"
174		);
175	}
176
177	#[test]
178	fn test_error_source_chain() {
179		let io_error = std::io::Error::new(std::io::ErrorKind::Other, "while reading config");
180
181		let outer_error = NotificationError::network_error(
182			"Failed to initialize",
183			Some(Box::new(io_error)),
184			None,
185		);
186
187		// Just test the string representation instead of the source chain
188		assert!(outer_error.to_string().contains("Failed to initialize"));
189
190		// For NotificationError::NetworkError, we know the implementation details
191		if let NotificationError::NetworkError(ctx) = &outer_error {
192			// Check that the context has the right message
193			assert_eq!(ctx.message, "Failed to initialize");
194
195			// Check that the context has the source error
196			assert!(ctx.source.is_some());
197
198			if let Some(src) = &ctx.source {
199				assert_eq!(src.to_string(), "while reading config");
200			}
201		} else {
202			panic!("Expected NetworkError variant");
203		}
204	}
205
206	#[test]
207	fn test_all_error_variants_have_and_propagate_consistent_trace_id() {
208		let create_context_with_id = || {
209			let context = ErrorContext::new("test message", None, None);
210			let original_id = context.trace_id.clone();
211			(Box::new(context), original_id)
212		};
213
214		let errors_with_ids: Vec<(NotificationError, String)> = vec![
215			{
216				let (ctx, id) = create_context_with_id();
217				(NotificationError::NetworkError(ctx), id)
218			},
219			{
220				let (ctx, id) = create_context_with_id();
221				(NotificationError::ConfigError(ctx), id)
222			},
223			{
224				let (ctx, id) = create_context_with_id();
225				(NotificationError::InternalError(ctx), id)
226			},
227			{
228				let (ctx, id) = create_context_with_id();
229				(NotificationError::ExecutionError(ctx), id)
230			},
231			{
232				let (ctx, id) = create_context_with_id();
233				(NotificationError::NotifyFailed(ctx), id)
234			},
235		];
236
237		for (error, original_id) in errors_with_ids {
238			let propagated_id = error.trace_id();
239			assert!(
240				!propagated_id.is_empty(),
241				"Error {:?} should have a non-empty trace_id",
242				error
243			);
244			assert_eq!(
245				propagated_id, original_id,
246				"Trace ID for {:?} was not propagated consistently. Expected: {}, Got: {}",
247				error, original_id, propagated_id
248			);
249		}
250	}
251}