openzeppelin_monitor/services/notification/
error.rs1use crate::utils::logging::error::{ErrorContext, TraceableError};
7use std::collections::HashMap;
8use thiserror::Error as ThisError;
9
10#[derive(ThisError, Debug)]
12pub enum NotificationError {
13 #[error("Network error: {0}")]
15 NetworkError(Box<ErrorContext>),
16
17 #[error("Config error: {0}")]
19 ConfigError(Box<ErrorContext>),
20
21 #[error("Internal error: {0}")]
23 InternalError(Box<ErrorContext>),
24
25 #[error("Script execution error: {0}")]
27 ExecutionError(Box<ErrorContext>),
28
29 #[error("Notification failed: {0}")]
31 NotifyFailed(Box<ErrorContext>),
32}
33
34impl NotificationError {
35 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 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 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 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 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 assert!(outer_error.to_string().contains("Failed to initialize"));
189
190 if let NotificationError::NetworkError(ctx) = &outer_error {
192 assert_eq!(ctx.message, "Failed to initialize");
194
195 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}