1use std::fmt;
7
8#[derive(Debug, Clone)]
13pub enum DomainCheckError {
14 InvalidDomain { domain: String, reason: String },
16
17 NetworkError {
19 message: String,
20 source: Option<String>,
21 },
22
23 RdapError {
25 domain: String,
26 message: String,
27 status_code: Option<u16>,
28 },
29
30 WhoisError { domain: String, message: String },
32
33 BootstrapError { tld: String, message: String },
35
36 ParseError {
38 message: String,
39 content: Option<String>,
40 },
41
42 ConfigError { message: String },
44
45 FileError { path: String, message: String },
47
48 Timeout {
50 operation: String,
51 duration: std::time::Duration,
52 },
53
54 RateLimited {
56 service: String,
57 message: String,
58 retry_after: Option<std::time::Duration>,
59 },
60
61 InvalidPattern { pattern: String, reason: String },
63
64 Internal { message: String },
66}
67
68impl DomainCheckError {
69 pub fn invalid_domain<D: Into<String>, R: Into<String>>(domain: D, reason: R) -> Self {
71 Self::InvalidDomain {
72 domain: domain.into(),
73 reason: reason.into(),
74 }
75 }
76
77 pub fn network<M: Into<String>>(message: M) -> Self {
79 Self::NetworkError {
80 message: message.into(),
81 source: None,
82 }
83 }
84
85 pub fn network_with_source<M: Into<String>, S: Into<String>>(message: M, source: S) -> Self {
87 Self::NetworkError {
88 message: message.into(),
89 source: Some(source.into()),
90 }
91 }
92
93 pub fn rdap<D: Into<String>, M: Into<String>>(domain: D, message: M) -> Self {
95 Self::RdapError {
96 domain: domain.into(),
97 message: message.into(),
98 status_code: None,
99 }
100 }
101
102 pub fn rdap_with_status<D: Into<String>, M: Into<String>>(
104 domain: D,
105 message: M,
106 status_code: u16,
107 ) -> Self {
108 Self::RdapError {
109 domain: domain.into(),
110 message: message.into(),
111 status_code: Some(status_code),
112 }
113 }
114
115 pub fn whois<D: Into<String>, M: Into<String>>(domain: D, message: M) -> Self {
117 Self::WhoisError {
118 domain: domain.into(),
119 message: message.into(),
120 }
121 }
122
123 pub fn bootstrap<T: Into<String>, M: Into<String>>(tld: T, message: M) -> Self {
125 Self::BootstrapError {
126 tld: tld.into(),
127 message: message.into(),
128 }
129 }
130
131 pub fn timeout<O: Into<String>>(operation: O, duration: std::time::Duration) -> Self {
133 Self::Timeout {
134 operation: operation.into(),
135 duration,
136 }
137 }
138
139 pub fn invalid_pattern<P: Into<String>, R: Into<String>>(pattern: P, reason: R) -> Self {
141 Self::InvalidPattern {
142 pattern: pattern.into(),
143 reason: reason.into(),
144 }
145 }
146
147 pub fn internal<M: Into<String>>(message: M) -> Self {
149 Self::Internal {
150 message: message.into(),
151 }
152 }
153
154 pub fn file_error<P: Into<String>, M: Into<String>>(path: P, message: M) -> Self {
156 Self::FileError {
157 path: path.into(),
158 message: message.into(),
159 }
160 }
161
162 pub fn indicates_available(&self) -> bool {
166 match self {
167 Self::RdapError {
168 status_code: Some(404),
169 ..
170 } => true,
171 Self::WhoisError { message, .. } => {
172 let msg = message.to_lowercase();
173 msg.contains("not found")
174 || msg.contains("no match")
175 || msg.contains("no data found")
176 || msg.contains("domain available")
177 }
178 _ => false,
179 }
180 }
181
182 pub fn is_retryable(&self) -> bool {
184 matches!(
185 self,
186 Self::NetworkError { .. }
187 | Self::Timeout { .. }
188 | Self::RateLimited { .. }
189 | Self::RdapError {
190 status_code: Some(500..=599),
191 ..
192 }
193 )
194 }
196}
197
198impl fmt::Display for DomainCheckError {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 match self {
201 Self::InvalidDomain { domain, reason } => {
202 write!(f, "❌ '{}' is not a valid domain name: {}\n 💡 Try something like 'example.com' or use a different domain", domain, reason)
203 }
204 Self::NetworkError { message, source } => {
205 if message.to_lowercase().contains("connection") || message.to_lowercase().contains("connect") {
206 write!(f, "🌐 Cannot connect to the internet\n 💡 Please check your network connection and try again")
207 } else if message.to_lowercase().contains("timeout") {
208 write!(f, "⏱️ Request timed out\n 💡 Your internet connection may be slow. Try again or check fewer domains at once")
209 } else {
210 let _ = source; write!(f, "🌐 Network error: {}\n 💡 Please check your internet connection", message)
212 }
213 }
214 Self::RdapError { domain, message, status_code } => {
215 match status_code {
216 Some(404) => write!(f, "🔍 {}: RDAP returned no data, verifying via WHOIS", domain),
217 Some(429) => write!(f, "⏳ {}: Registry is rate limiting requests\n 💡 Please wait a moment and try again", domain),
218 Some(500..=599) => write!(f, "⚠️ {}: Registry server is temporarily unavailable\n 💡 Trying backup method...", domain),
219 Some(code) => write!(f, "⚠️ {}: Registry returned error (HTTP {})\n 💡 This domain registry may be temporarily unavailable", domain, code),
220 None => write!(f, "⚠️ {}: {}\n 💡 Trying alternative checking method...", domain, message),
221 }
222 }
223 Self::WhoisError { domain, message } => {
224 if message.to_lowercase().contains("not found") || message.to_lowercase().contains("no match") {
225 write!(f, "✅ {}: Domain appears to be available", domain)
226 } else if message.to_lowercase().contains("rate limit") || message.to_lowercase().contains("too many") {
227 write!(f, "⏳ {}: WHOIS server is rate limiting requests\n 💡 Please wait a moment and try again", domain)
228 } else if message.to_lowercase().contains("whois") && message.to_lowercase().contains("not found") {
229 write!(f, "⚠️ {}: WHOIS command not found on this system\n 💡 Please install whois or use online domain checkers", domain)
230 } else {
231 write!(f, "⚠️ {}: WHOIS lookup failed\n 💡 This may indicate the domain is available or the server is busy", domain)
232 }
233 }
234 Self::BootstrapError { tld, message: _ } => {
235 write!(f, "❓ Unknown domain extension '.{}'\n 💡 This TLD may not support automated checking. Try manually checking with a registrar", tld)
236 }
237 Self::ParseError { message: _, content: _ } => {
238 write!(f, "⚠️ Unable to understand server response\n 💡 The domain registry may be experiencing issues. Please try again later")
239 }
240 Self::ConfigError { message } => {
241 write!(f, "⚙️ Configuration error: {}\n 💡 Please check your command line arguments or configuration file values", message)
242 }
243 Self::FileError { path, message } => {
244 if message.to_lowercase().contains("not found") || message.to_lowercase().contains("no such file") {
245 write!(f, "📁 File not found: {}\n 💡 Please check the file path and make sure the file exists", path)
246 } else if message.to_lowercase().contains("permission") {
247 write!(f, "🔒 Permission denied: {}\n 💡 Please check file permissions or try running with appropriate access", path)
248 } else if message.to_lowercase().contains("no valid domains") {
249 write!(f, "📄 No valid domains found in: {}\n 💡 Make sure the file contains domain names (one per line) and check the format", path)
250 } else {
251 write!(f, "📁 File error ({}): {}\n 💡 Please check the file and try again", path, message)
252 }
253 }
254 Self::Timeout { operation, duration } => {
255 write!(f, "⏱️ Operation timed out after {:?}: {}\n 💡 Try reducing the number of domains or check your internet connection", duration, operation)
256 }
257 Self::RateLimited { service, message, retry_after } => {
258 match retry_after {
259 Some(retry) => write!(f, "⏳ Rate limited by {}: {}\n 💡 Please wait {:?} and try again", service, message, retry),
260 None => write!(f, "⏳ Rate limited by {}: {}\n 💡 Please wait a moment and try again", service, message),
261 }
262 }
263 Self::InvalidPattern { pattern, reason } => {
264 write!(f, "⚙️ Invalid pattern '{}': {}\n 💡 Supported: \\w (letters+hyphen), \\d (digits), ? (alphanumeric), literal characters", pattern, reason)
265 }
266 Self::Internal { message } => {
267 write!(f, "🔧 Internal error: {}\n 💡 This is unexpected. Please try again or report this issue", message)
268 }
269 }
270 }
271}
272
273impl std::error::Error for DomainCheckError {}
274
275impl From<reqwest::Error> for DomainCheckError {
277 fn from(err: reqwest::Error) -> Self {
278 if err.is_timeout() {
279 Self::timeout("HTTP request", std::time::Duration::from_secs(30))
280 } else if err.is_connect() {
281 Self::network_with_source("Connection failed", err.to_string())
282 } else {
283 Self::network_with_source("HTTP request failed", err.to_string())
284 }
285 }
286}
287
288impl From<serde_json::Error> for DomainCheckError {
289 fn from(err: serde_json::Error) -> Self {
290 Self::ParseError {
291 message: format!("JSON parsing failed: {}", err),
292 content: None,
293 }
294 }
295}
296
297impl From<std::io::Error> for DomainCheckError {
298 fn from(err: std::io::Error) -> Self {
299 Self::Internal {
300 message: format!("I/O error: {}", err),
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
312 fn test_invalid_domain_constructor() {
313 let err = DomainCheckError::invalid_domain("bad!", "contains special characters");
314 match err {
315 DomainCheckError::InvalidDomain { domain, reason } => {
316 assert_eq!(domain, "bad!");
317 assert_eq!(reason, "contains special characters");
318 }
319 _ => panic!("wrong variant"),
320 }
321 }
322
323 #[test]
324 fn test_network_constructor() {
325 let err = DomainCheckError::network("connection refused");
326 match err {
327 DomainCheckError::NetworkError { message, source } => {
328 assert_eq!(message, "connection refused");
329 assert!(source.is_none());
330 }
331 _ => panic!("wrong variant"),
332 }
333 }
334
335 #[test]
336 fn test_network_with_source_constructor() {
337 let err = DomainCheckError::network_with_source("failed", "dns lookup error");
338 match err {
339 DomainCheckError::NetworkError { message, source } => {
340 assert_eq!(message, "failed");
341 assert_eq!(source, Some("dns lookup error".to_string()));
342 }
343 _ => panic!("wrong variant"),
344 }
345 }
346
347 #[test]
348 fn test_rdap_constructor() {
349 let err = DomainCheckError::rdap("test.com", "lookup failed");
350 match err {
351 DomainCheckError::RdapError {
352 domain,
353 message,
354 status_code,
355 } => {
356 assert_eq!(domain, "test.com");
357 assert_eq!(message, "lookup failed");
358 assert!(status_code.is_none());
359 }
360 _ => panic!("wrong variant"),
361 }
362 }
363
364 #[test]
365 fn test_rdap_with_status_constructor() {
366 let err = DomainCheckError::rdap_with_status("test.com", "not found", 404);
367 match err {
368 DomainCheckError::RdapError {
369 domain,
370 message,
371 status_code,
372 } => {
373 assert_eq!(domain, "test.com");
374 assert_eq!(message, "not found");
375 assert_eq!(status_code, Some(404));
376 }
377 _ => panic!("wrong variant"),
378 }
379 }
380
381 #[test]
382 fn test_whois_constructor() {
383 let err = DomainCheckError::whois("test.com", "server unreachable");
384 match err {
385 DomainCheckError::WhoisError { domain, message } => {
386 assert_eq!(domain, "test.com");
387 assert_eq!(message, "server unreachable");
388 }
389 _ => panic!("wrong variant"),
390 }
391 }
392
393 #[test]
394 fn test_bootstrap_constructor() {
395 let err = DomainCheckError::bootstrap("xyz", "no endpoint found");
396 match err {
397 DomainCheckError::BootstrapError { tld, message } => {
398 assert_eq!(tld, "xyz");
399 assert_eq!(message, "no endpoint found");
400 }
401 _ => panic!("wrong variant"),
402 }
403 }
404
405 #[test]
406 fn test_timeout_constructor() {
407 let err = DomainCheckError::timeout("RDAP lookup", std::time::Duration::from_secs(10));
408 match err {
409 DomainCheckError::Timeout {
410 operation,
411 duration,
412 } => {
413 assert_eq!(operation, "RDAP lookup");
414 assert_eq!(duration, std::time::Duration::from_secs(10));
415 }
416 _ => panic!("wrong variant"),
417 }
418 }
419
420 #[test]
421 fn test_internal_constructor() {
422 let err = DomainCheckError::internal("unexpected state");
423 match err {
424 DomainCheckError::Internal { message } => {
425 assert_eq!(message, "unexpected state");
426 }
427 _ => panic!("wrong variant"),
428 }
429 }
430
431 #[test]
432 fn test_file_error_constructor() {
433 let err = DomainCheckError::file_error("/tmp/domains.txt", "permission denied");
434 match err {
435 DomainCheckError::FileError { path, message } => {
436 assert_eq!(path, "/tmp/domains.txt");
437 assert_eq!(message, "permission denied");
438 }
439 _ => panic!("wrong variant"),
440 }
441 }
442
443 #[test]
444 fn test_invalid_pattern_constructor() {
445 let err = DomainCheckError::invalid_pattern("test\\x", "unknown escape");
446 match err {
447 DomainCheckError::InvalidPattern { pattern, reason } => {
448 assert_eq!(pattern, "test\\x");
449 assert_eq!(reason, "unknown escape");
450 }
451 _ => panic!("wrong variant"),
452 }
453 }
454
455 #[test]
458 fn test_rdap_404_indicates_available() {
459 let err = DomainCheckError::rdap_with_status("test.com", "not found", 404);
460 assert!(err.indicates_available());
461 }
462
463 #[test]
464 fn test_rdap_200_not_available() {
465 let err = DomainCheckError::rdap_with_status("test.com", "ok", 200);
466 assert!(!err.indicates_available());
467 }
468
469 #[test]
470 fn test_rdap_no_status_not_available() {
471 let err = DomainCheckError::rdap("test.com", "generic error");
472 assert!(!err.indicates_available());
473 }
474
475 #[test]
476 fn test_whois_not_found_indicates_available() {
477 let err = DomainCheckError::whois("test.com", "No match for domain NOT FOUND");
478 assert!(err.indicates_available());
479 }
480
481 #[test]
482 fn test_whois_no_data_found_indicates_available() {
483 let err = DomainCheckError::whois("test.com", "No Data Found");
484 assert!(err.indicates_available());
485 }
486
487 #[test]
488 fn test_whois_domain_available_indicates_available() {
489 let err = DomainCheckError::whois("test.com", "Domain Available for registration");
490 assert!(err.indicates_available());
491 }
492
493 #[test]
494 fn test_whois_rate_limit_not_available() {
495 let err = DomainCheckError::whois("test.com", "rate limited");
496 assert!(!err.indicates_available());
497 }
498
499 #[test]
500 fn test_network_error_not_available() {
501 let err = DomainCheckError::network("connection refused");
502 assert!(!err.indicates_available());
503 }
504
505 #[test]
506 fn test_timeout_not_available() {
507 let err = DomainCheckError::timeout("test", std::time::Duration::from_secs(5));
508 assert!(!err.indicates_available());
509 }
510
511 #[test]
512 fn test_invalid_pattern_not_available() {
513 let err = DomainCheckError::invalid_pattern("bad", "reason");
514 assert!(!err.indicates_available());
515 }
516
517 #[test]
520 fn test_network_error_is_retryable() {
521 let err = DomainCheckError::network("connection refused");
522 assert!(err.is_retryable());
523 }
524
525 #[test]
526 fn test_network_with_source_is_retryable() {
527 let err = DomainCheckError::network_with_source("failed", "dns");
528 assert!(err.is_retryable());
529 }
530
531 #[test]
532 fn test_timeout_is_retryable() {
533 let err = DomainCheckError::timeout("test", std::time::Duration::from_secs(5));
534 assert!(err.is_retryable());
535 }
536
537 #[test]
538 fn test_rate_limited_is_retryable() {
539 let err = DomainCheckError::RateLimited {
540 service: "RDAP".to_string(),
541 message: "too many requests".to_string(),
542 retry_after: Some(std::time::Duration::from_secs(30)),
543 };
544 assert!(err.is_retryable());
545 }
546
547 #[test]
548 fn test_rdap_500_is_retryable() {
549 let err = DomainCheckError::rdap_with_status("test.com", "server error", 500);
550 assert!(err.is_retryable());
551 }
552
553 #[test]
554 fn test_rdap_502_is_retryable() {
555 let err = DomainCheckError::rdap_with_status("test.com", "bad gateway", 502);
556 assert!(err.is_retryable());
557 }
558
559 #[test]
560 fn test_rdap_599_is_retryable() {
561 let err = DomainCheckError::rdap_with_status("test.com", "error", 599);
562 assert!(err.is_retryable());
563 }
564
565 #[test]
566 fn test_rdap_403_not_retryable() {
567 let err = DomainCheckError::rdap_with_status("test.com", "forbidden", 403);
568 assert!(!err.is_retryable());
569 }
570
571 #[test]
572 fn test_rdap_404_not_retryable() {
573 let err = DomainCheckError::rdap_with_status("test.com", "not found", 404);
574 assert!(!err.is_retryable());
575 }
576
577 #[test]
578 fn test_config_error_not_retryable() {
579 let err = DomainCheckError::ConfigError {
580 message: "bad config".to_string(),
581 };
582 assert!(!err.is_retryable());
583 }
584
585 #[test]
586 fn test_invalid_domain_not_retryable() {
587 let err = DomainCheckError::invalid_domain("bad", "too short");
588 assert!(!err.is_retryable());
589 }
590
591 #[test]
592 fn test_invalid_pattern_not_retryable() {
593 let err = DomainCheckError::invalid_pattern("bad", "reason");
594 assert!(!err.is_retryable());
595 }
596
597 #[test]
598 fn test_file_error_not_retryable() {
599 let err = DomainCheckError::file_error("/tmp/x", "not found");
600 assert!(!err.is_retryable());
601 }
602
603 #[test]
604 fn test_bootstrap_error_not_retryable() {
605 let err = DomainCheckError::bootstrap("xyz", "no endpoint");
606 assert!(!err.is_retryable());
607 }
608
609 #[test]
610 fn test_internal_error_not_retryable() {
611 let err = DomainCheckError::internal("unexpected");
612 assert!(!err.is_retryable());
613 }
614
615 #[test]
618 fn test_display_invalid_domain() {
619 let err = DomainCheckError::invalid_domain("x", "too short");
620 let msg = format!("{}", err);
621 assert!(msg.contains("x"));
622 assert!(msg.contains("too short"));
623 }
624
625 #[test]
626 fn test_display_network_connection_error() {
627 let err = DomainCheckError::network("connection refused");
628 let msg = format!("{}", err);
629 assert!(msg.contains("connect"));
630 }
631
632 #[test]
633 fn test_display_network_timeout_error() {
634 let err = DomainCheckError::network("request timeout");
635 let msg = format!("{}", err);
636 assert!(msg.contains("timed out") || msg.contains("timeout"));
637 }
638
639 #[test]
640 fn test_display_network_generic() {
641 let err = DomainCheckError::network("something broke");
642 let msg = format!("{}", err);
643 assert!(msg.contains("something broke"));
644 }
645
646 #[test]
647 fn test_display_rdap_404() {
648 let err = DomainCheckError::rdap_with_status("avail.com", "not found", 404);
649 let msg = format!("{}", err);
650 assert!(msg.contains("avail.com"));
651 assert!(msg.contains("RDAP returned no data"));
652 assert!(msg.contains("WHOIS"));
653 }
654
655 #[test]
656 fn test_display_rdap_429() {
657 let err = DomainCheckError::rdap_with_status("test.com", "rate limited", 429);
658 let msg = format!("{}", err);
659 assert!(msg.contains("rate limiting"));
660 }
661
662 #[test]
663 fn test_display_rdap_5xx() {
664 let err = DomainCheckError::rdap_with_status("test.com", "error", 503);
665 let msg = format!("{}", err);
666 assert!(msg.contains("temporarily unavailable"));
667 }
668
669 #[test]
670 fn test_display_rdap_other_status() {
671 let err = DomainCheckError::rdap_with_status("test.com", "weird", 418);
672 let msg = format!("{}", err);
673 assert!(msg.contains("418"));
674 }
675
676 #[test]
677 fn test_display_rdap_no_status() {
678 let err = DomainCheckError::rdap("test.com", "lookup failed");
679 let msg = format!("{}", err);
680 assert!(msg.contains("lookup failed"));
681 }
682
683 #[test]
684 fn test_display_whois_not_found() {
685 let err = DomainCheckError::whois("test.com", "not found");
686 let msg = format!("{}", err);
687 assert!(msg.contains("available"));
688 }
689
690 #[test]
691 fn test_display_whois_rate_limit() {
692 let err = DomainCheckError::whois("test.com", "too many requests");
693 let msg = format!("{}", err);
694 assert!(msg.contains("rate limiting"));
695 }
696
697 #[test]
698 fn test_display_whois_generic() {
699 let err = DomainCheckError::whois("test.com", "server error");
700 let msg = format!("{}", err);
701 assert!(msg.contains("WHOIS lookup failed"));
702 }
703
704 #[test]
705 fn test_display_bootstrap_error() {
706 let err = DomainCheckError::bootstrap("xyz", "no endpoint");
707 let msg = format!("{}", err);
708 assert!(msg.contains(".xyz"));
709 }
710
711 #[test]
712 fn test_display_parse_error() {
713 let err = DomainCheckError::ParseError {
714 message: "bad json".to_string(),
715 content: None,
716 };
717 let msg = format!("{}", err);
718 assert!(msg.contains("server response"));
719 }
720
721 #[test]
722 fn test_display_config_error() {
723 let err = DomainCheckError::ConfigError {
724 message: "invalid value".to_string(),
725 };
726 let msg = format!("{}", err);
727 assert!(msg.contains("invalid value"));
728 }
729
730 #[test]
731 fn test_display_file_not_found() {
732 let err = DomainCheckError::file_error("/tmp/x.txt", "no such file");
733 let msg = format!("{}", err);
734 assert!(msg.contains("not found") || msg.contains("no such file"));
735 }
736
737 #[test]
738 fn test_display_file_permission() {
739 let err = DomainCheckError::file_error("/tmp/x.txt", "permission denied");
740 let msg = format!("{}", err);
741 assert!(msg.contains("Permission denied") || msg.contains("permission"));
742 }
743
744 #[test]
745 fn test_display_file_no_valid_domains() {
746 let err = DomainCheckError::file_error("/tmp/x.txt", "no valid domains found");
747 let msg = format!("{}", err);
748 assert!(msg.contains("No valid domains"));
749 }
750
751 #[test]
752 fn test_display_file_generic() {
753 let err = DomainCheckError::file_error("/tmp/x.txt", "corrupt data");
754 let msg = format!("{}", err);
755 assert!(msg.contains("corrupt data"));
756 }
757
758 #[test]
759 fn test_display_timeout() {
760 let err = DomainCheckError::timeout("RDAP", std::time::Duration::from_secs(5));
761 let msg = format!("{}", err);
762 assert!(msg.contains("timed out"));
763 assert!(msg.contains("RDAP"));
764 }
765
766 #[test]
767 fn test_display_rate_limited_with_retry() {
768 let err = DomainCheckError::RateLimited {
769 service: "RDAP".to_string(),
770 message: "slow down".to_string(),
771 retry_after: Some(std::time::Duration::from_secs(30)),
772 };
773 let msg = format!("{}", err);
774 assert!(msg.contains("RDAP"));
775 assert!(msg.contains("30"));
776 }
777
778 #[test]
779 fn test_display_rate_limited_without_retry() {
780 let err = DomainCheckError::RateLimited {
781 service: "WHOIS".to_string(),
782 message: "slow down".to_string(),
783 retry_after: None,
784 };
785 let msg = format!("{}", err);
786 assert!(msg.contains("WHOIS"));
787 assert!(msg.contains("wait a moment"));
788 }
789
790 #[test]
791 fn test_display_invalid_pattern() {
792 let err = DomainCheckError::invalid_pattern("test\\x", "unknown escape sequence '\\x'");
793 let msg = format!("{}", err);
794 assert!(msg.contains("test\\x"));
795 assert!(msg.contains("\\w")); assert!(msg.contains("\\d")); }
798
799 #[test]
800 fn test_display_internal() {
801 let err = DomainCheckError::internal("lock poisoned");
802 let msg = format!("{}", err);
803 assert!(msg.contains("lock poisoned"));
804 assert!(msg.contains("Internal error") || msg.contains("unexpected"));
805 }
806
807 #[test]
810 fn test_from_serde_json_error() {
811 let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
812 let err: DomainCheckError = json_err.into();
813 match err {
814 DomainCheckError::ParseError { message, content } => {
815 assert!(message.contains("JSON parsing failed"));
816 assert!(content.is_none());
817 }
818 _ => panic!("expected ParseError, got {:?}", err),
819 }
820 }
821
822 #[test]
823 fn test_from_io_error() {
824 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
825 let err: DomainCheckError = io_err.into();
826 match err {
827 DomainCheckError::Internal { message } => {
828 assert!(message.contains("I/O error"));
829 assert!(message.contains("file missing"));
830 }
831 _ => panic!("expected Internal, got {:?}", err),
832 }
833 }
834
835 #[test]
838 fn test_error_trait_implemented() {
839 let err = DomainCheckError::network("test");
840 let _: &dyn std::error::Error = &err;
842 }
843}