edgefirst_client/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright © 2025 Au-Zone Technologies. All Rights Reserved.
3
4use crate::Progress;
5use tokio::sync::{AcquireError, watch};
6
7/// Comprehensive error type for EdgeFirst Studio Client operations.
8///
9/// This enum covers all possible error conditions that can occur when using
10/// the EdgeFirst Studio Client, from network issues to authentication problems
11/// and data validation errors.
12#[derive(Debug)]
13pub enum Error {
14    /// An I/O error occurred during file operations.
15    IoError(std::io::Error),
16    /// Configuration parsing or loading error.
17    ConfigError(config::ConfigError),
18    /// JSON serialization or deserialization error.
19    JsonError(serde_json::Error),
20    /// HTTP request error from the reqwest client.
21    HttpError(reqwest::Error),
22    /// Maximum number of retries exceeded for an operation.
23    MaxRetriesExceeded(u32),
24    /// URL parsing error.
25    UrlParseError(url::ParseError),
26    /// RPC error with error code and message from the server.
27    RpcError(i32, String),
28    /// Invalid RPC request ID format.
29    InvalidRpcId(String),
30    /// Environment variable error.
31    EnvError(std::env::VarError),
32    /// Semaphore acquisition error for concurrent operations.
33    SemaphoreError(AcquireError),
34    /// Async task join error.
35    JoinError(tokio::task::JoinError),
36    /// Error sending progress updates.
37    ProgressSendError(watch::error::SendError<Progress>),
38    /// Error receiving progress updates.
39    ProgressRecvError(watch::error::RecvError),
40    /// Path prefix stripping error.
41    StripPrefixError(std::path::StripPrefixError),
42    /// Integer parsing error.
43    ParseIntError(std::num::ParseIntError),
44    /// Server returned an invalid or unexpected response.
45    InvalidResponse,
46    /// Requested functionality is not yet implemented.
47    NotImplemented,
48    /// File part size exceeds the maximum allowed limit.
49    PartTooLarge,
50    /// Invalid file type provided.
51    InvalidFileType(String),
52    /// Invalid annotation type provided.
53    InvalidAnnotationType(String),
54    /// Unsupported file format.
55    UnsupportedFormat(String),
56    /// Required image files are missing from the dataset.
57    MissingImages(String),
58    /// Required annotation files are missing from the dataset.
59    MissingAnnotations(String),
60    /// Referenced label is missing or not found.
61    MissingLabel(String),
62    /// Invalid parameters provided to an operation.
63    InvalidParameters(String),
64    /// Attempted to use a feature that is not enabled.
65    FeatureNotEnabled(String),
66    /// Authentication token is empty or not provided.
67    EmptyToken,
68    /// Authentication token format is invalid.
69    InvalidToken,
70    /// Authentication token has expired.
71    TokenExpired,
72    /// User is not authorized to perform the requested operation.
73    Unauthorized,
74    /// Invalid or missing ETag header in HTTP response.
75    InvalidEtag(String),
76    /// Token storage operation error.
77    StorageError(String),
78    /// Polars dataframe operation error (only with "polars" feature).
79    #[cfg(feature = "polars")]
80    PolarsError(polars::error::PolarsError),
81    /// COCO format parsing or validation error.
82    CocoError(String),
83    /// ZIP archive read/write error.
84    ZipError(String),
85}
86
87impl From<std::io::Error> for Error {
88    fn from(err: std::io::Error) -> Self {
89        Error::IoError(err)
90    }
91}
92
93impl From<config::ConfigError> for Error {
94    fn from(err: config::ConfigError) -> Self {
95        Error::ConfigError(err)
96    }
97}
98
99impl From<serde_json::Error> for Error {
100    fn from(err: serde_json::Error) -> Self {
101        Error::JsonError(err)
102    }
103}
104
105impl From<reqwest::Error> for Error {
106    fn from(err: reqwest::Error) -> Self {
107        Error::HttpError(err)
108    }
109}
110
111impl From<url::ParseError> for Error {
112    fn from(err: url::ParseError) -> Self {
113        Error::UrlParseError(err)
114    }
115}
116
117impl From<std::env::VarError> for Error {
118    fn from(err: std::env::VarError) -> Self {
119        Error::EnvError(err)
120    }
121}
122
123impl From<AcquireError> for Error {
124    fn from(err: AcquireError) -> Self {
125        Error::SemaphoreError(err)
126    }
127}
128
129impl From<tokio::task::JoinError> for Error {
130    fn from(err: tokio::task::JoinError) -> Self {
131        Error::JoinError(err)
132    }
133}
134
135impl From<watch::error::SendError<Progress>> for Error {
136    fn from(err: watch::error::SendError<Progress>) -> Self {
137        Error::ProgressSendError(err)
138    }
139}
140
141impl From<watch::error::RecvError> for Error {
142    fn from(err: watch::error::RecvError) -> Self {
143        Error::ProgressRecvError(err)
144    }
145}
146
147impl From<std::path::StripPrefixError> for Error {
148    fn from(err: std::path::StripPrefixError) -> Self {
149        Error::StripPrefixError(err)
150    }
151}
152
153impl From<std::num::ParseIntError> for Error {
154    fn from(err: std::num::ParseIntError) -> Self {
155        Error::ParseIntError(err)
156    }
157}
158
159impl From<crate::storage::StorageError> for Error {
160    fn from(err: crate::storage::StorageError) -> Self {
161        Error::StorageError(err.to_string())
162    }
163}
164
165#[cfg(feature = "polars")]
166impl From<polars::error::PolarsError> for Error {
167    fn from(err: polars::error::PolarsError) -> Self {
168        Error::PolarsError(err)
169    }
170}
171
172impl From<zip::result::ZipError> for Error {
173    fn from(err: zip::result::ZipError) -> Self {
174        Error::ZipError(err.to_string())
175    }
176}
177
178impl std::fmt::Display for Error {
179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        match self {
181            Error::IoError(e) => write!(f, "I/O error: {}", e),
182            Error::ConfigError(e) => write!(f, "Configuration error: {}", e),
183            Error::JsonError(e) => write!(f, "JSON error: {}", e),
184            Error::HttpError(e) => write!(f, "HTTP error: {}", e),
185            Error::MaxRetriesExceeded(n) => write!(f, "Maximum retries ({}) exceeded", n),
186            Error::UrlParseError(e) => write!(f, "URL parse error: {}", e),
187            Error::RpcError(code, msg) => write!(f, "RPC error {}: {}", code, msg),
188            Error::InvalidRpcId(id) => write!(f, "Invalid RPC ID: {}", id),
189            Error::EnvError(e) => write!(f, "Environment variable error: {}", e),
190            Error::SemaphoreError(e) => write!(f, "Semaphore error: {}", e),
191            Error::JoinError(e) => write!(f, "Task join error: {}", e),
192            Error::ProgressSendError(e) => write!(f, "Progress send error: {}", e),
193            Error::ProgressRecvError(e) => write!(f, "Progress receive error: {}", e),
194            Error::StripPrefixError(e) => write!(f, "Path prefix error: {}", e),
195            Error::ParseIntError(e) => write!(f, "Integer parse error: {}", e),
196            Error::InvalidResponse => write!(f, "Invalid server response"),
197            Error::NotImplemented => write!(f, "Not implemented"),
198            Error::PartTooLarge => write!(f, "File part size exceeds maximum limit"),
199            Error::InvalidFileType(s) => write!(f, "Invalid file type: {}", s),
200            Error::InvalidAnnotationType(s) => write!(f, "Invalid annotation type: {}", s),
201            Error::UnsupportedFormat(s) => write!(f, "Unsupported format: {}", s),
202            Error::MissingImages(s) => write!(f, "Missing images: {}", s),
203            Error::MissingAnnotations(s) => write!(f, "Missing annotations: {}", s),
204            Error::MissingLabel(s) => write!(f, "Missing label: {}", s),
205            Error::InvalidParameters(s) => write!(f, "Invalid parameters: {}", s),
206            Error::FeatureNotEnabled(s) => write!(f, "Feature not enabled: {}", s),
207            Error::EmptyToken => write!(f, "Authentication token is empty"),
208            Error::InvalidToken => write!(f, "Invalid authentication token"),
209            Error::TokenExpired => write!(f, "Authentication token has expired"),
210            Error::Unauthorized => write!(f, "Unauthorized access"),
211            Error::InvalidEtag(s) => write!(f, "Invalid ETag header: {}", s),
212            Error::StorageError(s) => write!(f, "Token storage error: {}", s),
213            #[cfg(feature = "polars")]
214            Error::PolarsError(e) => write!(f, "Polars error: {}", e),
215            Error::CocoError(s) => write!(f, "COCO format error: {}", s),
216            Error::ZipError(s) => write!(f, "ZIP error: {}", s),
217        }
218    }
219}
220
221impl std::error::Error for Error {
222    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
223        match self {
224            Error::IoError(e) => Some(e),
225            Error::ConfigError(e) => Some(e),
226            Error::JsonError(e) => Some(e),
227            Error::HttpError(e) => Some(e),
228            Error::UrlParseError(e) => Some(e),
229            Error::EnvError(e) => Some(e),
230            Error::JoinError(e) => Some(e),
231            Error::StripPrefixError(e) => Some(e),
232            Error::ParseIntError(e) => Some(e),
233            #[cfg(feature = "polars")]
234            Error::PolarsError(e) => Some(e),
235            _ => None,
236        }
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use std::path::Path;
244
245    // Tests for wrapped error types - follow the pattern:
246    // 1. Create inner error
247    // 2. Capture inner error string
248    // 3. Wrap to custom Error type
249    // 4. Capture wrapped error string
250    // 5. Verify inner string is substring of wrapped string
251
252    #[test]
253    fn test_io_error_wrapping() {
254        // 1. Create inner error
255        let inner_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
256        // 2. Capture inner error string
257        let inner_str = inner_err.to_string();
258        // 3. Wrap to custom Error type
259        let wrapped_err: Error = inner_err.into();
260        // 4. Capture wrapped error string
261        let wrapped_str = wrapped_err.to_string();
262        // 5. Verify inner string is substring of wrapped string
263        assert!(
264            wrapped_str.contains(&inner_str),
265            "Wrapped error '{}' should contain inner error '{}'",
266            wrapped_str,
267            inner_str
268        );
269        assert!(wrapped_str.starts_with("I/O error: "));
270    }
271
272    #[test]
273    fn test_config_error_wrapping() {
274        // 1. Create inner error - Force a config error by trying to deserialize empty
275        //    config to a required struct
276        #[derive(Debug, serde::Deserialize)]
277        #[allow(dead_code)]
278        struct RequiredField {
279            required: String,
280        }
281
282        let inner_err = config::Config::builder()
283            .build()
284            .unwrap()
285            .try_deserialize::<RequiredField>()
286            .unwrap_err();
287        // 2. Capture inner error string
288        let inner_str = inner_err.to_string();
289        // 3. Wrap to custom Error type
290        let wrapped_err: Error = inner_err.into();
291        // 4. Capture wrapped error string
292        let wrapped_str = wrapped_err.to_string();
293        // 5. Verify inner string is substring of wrapped string
294        assert!(
295            wrapped_str.contains(&inner_str),
296            "Wrapped error '{}' should contain inner error '{}'",
297            wrapped_str,
298            inner_str
299        );
300        assert!(wrapped_str.starts_with("Configuration error: "));
301    }
302
303    #[test]
304    fn test_json_error_wrapping() {
305        // 1. Create inner error - invalid JSON
306        let inner_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
307        // 2. Capture inner error string
308        let inner_str = inner_err.to_string();
309        // 3. Wrap to custom Error type
310        let wrapped_err: Error = inner_err.into();
311        // 4. Capture wrapped error string
312        let wrapped_str = wrapped_err.to_string();
313        // 5. Verify inner string is substring of wrapped string
314        assert!(
315            wrapped_str.contains(&inner_str),
316            "Wrapped error '{}' should contain inner error '{}'",
317            wrapped_str,
318            inner_str
319        );
320        assert!(wrapped_str.starts_with("JSON error: "));
321    }
322
323    #[test]
324    fn test_url_parse_error_wrapping() {
325        // 1. Create inner error - invalid URL
326        let inner_err = url::Url::parse("not a valid url").unwrap_err();
327        // 2. Capture inner error string
328        let inner_str = inner_err.to_string();
329        // 3. Wrap to custom Error type
330        let wrapped_err: Error = inner_err.into();
331        // 4. Capture wrapped error string
332        let wrapped_str = wrapped_err.to_string();
333        // 5. Verify inner string is substring of wrapped string
334        assert!(
335            wrapped_str.contains(&inner_str),
336            "Wrapped error '{}' should contain inner error '{}'",
337            wrapped_str,
338            inner_str
339        );
340        assert!(wrapped_str.starts_with("URL parse error: "));
341    }
342
343    #[test]
344    fn test_env_error_wrapping() {
345        // 1. Create inner error - missing environment variable
346        let inner_err = std::env::var("NONEXISTENT_VAR_12345").unwrap_err();
347        // 2. Capture inner error string
348        let inner_str = inner_err.to_string();
349        // 3. Wrap to custom Error type
350        let wrapped_err: Error = inner_err.into();
351        // 4. Capture wrapped error string
352        let wrapped_str = wrapped_err.to_string();
353        // 5. Verify inner string is substring of wrapped string
354        assert!(
355            wrapped_str.contains(&inner_str),
356            "Wrapped error '{}' should contain inner error '{}'",
357            wrapped_str,
358            inner_str
359        );
360        assert!(wrapped_str.starts_with("Environment variable error: "));
361    }
362
363    #[test]
364    fn test_strip_prefix_error_wrapping() {
365        // 1. Create inner error - strip non-existent prefix
366        let path = Path::new("/foo/bar");
367        let prefix = Path::new("/baz");
368        let inner_err = path.strip_prefix(prefix).unwrap_err();
369        // 2. Capture inner error string
370        let inner_str = inner_err.to_string();
371        // 3. Wrap to custom Error type
372        let wrapped_err: Error = inner_err.into();
373        // 4. Capture wrapped error string
374        let wrapped_str = wrapped_err.to_string();
375        // 5. Verify inner string is substring of wrapped string
376        assert!(
377            wrapped_str.contains(&inner_str),
378            "Wrapped error '{}' should contain inner error '{}'",
379            wrapped_str,
380            inner_str
381        );
382        assert!(wrapped_str.starts_with("Path prefix error: "));
383    }
384
385    #[test]
386    fn test_parse_int_error_wrapping() {
387        // 1. Create inner error - invalid integer string
388        let inner_err = "not a number".parse::<i32>().unwrap_err();
389        // 2. Capture inner error string
390        let inner_str = inner_err.to_string();
391        // 3. Wrap to custom Error type
392        let wrapped_err: Error = inner_err.into();
393        // 4. Capture wrapped error string
394        let wrapped_str = wrapped_err.to_string();
395        // 5. Verify inner string is substring of wrapped string
396        assert!(
397            wrapped_str.contains(&inner_str),
398            "Wrapped error '{}' should contain inner error '{}'",
399            wrapped_str,
400            inner_str
401        );
402        assert!(wrapped_str.starts_with("Integer parse error: "));
403    }
404
405    #[cfg(feature = "polars")]
406    #[test]
407    fn test_polars_error_wrapping() {
408        // 1. Create inner error - duplicate column names cause an error
409        use polars::prelude::*;
410        let inner_err = DataFrame::new(vec![
411            Series::new("a".into(), &[1, 2, 3]).into(),
412            Series::new("a".into(), &[4, 5, 6]).into(),
413        ])
414        .unwrap_err();
415        // 2. Capture inner error string
416        let inner_str = inner_err.to_string();
417        // 3. Wrap to custom Error type
418        let wrapped_err: Error = inner_err.into();
419        // 4. Capture wrapped error string
420        let wrapped_str = wrapped_err.to_string();
421        // 5. Verify inner string is substring of wrapped string
422        assert!(
423            wrapped_str.contains(&inner_str),
424            "Wrapped error '{}' should contain inner error '{}'",
425            wrapped_str,
426            inner_str
427        );
428        assert!(wrapped_str.starts_with("Polars error: "));
429    }
430
431    // Tests for wrapped primitive types - follow the pattern:
432    // 1. Create random primitive value
433    // 2. Capture the primitive as string
434    // 3. Wrap to custom Error type
435    // 4. Capture wrapped error string
436    // 5. Verify primitive string is substring of wrapped string
437
438    #[test]
439    fn test_max_retries_exceeded() {
440        // 1. Create primitive value
441        let retry_count = 42u32;
442        // 2. Capture primitive as string
443        let primitive_str = retry_count.to_string();
444        // 3. Wrap to custom Error type
445        let wrapped_err = Error::MaxRetriesExceeded(retry_count);
446        // 4. Capture wrapped error string
447        let wrapped_str = wrapped_err.to_string();
448        // 5. Verify primitive string is substring of wrapped string
449        assert!(
450            wrapped_str.contains(&primitive_str),
451            "Wrapped error '{}' should contain retry count '{}'",
452            wrapped_str,
453            primitive_str
454        );
455        assert!(wrapped_str.starts_with("Maximum retries"));
456    }
457
458    #[test]
459    fn test_rpc_error() {
460        // 1. Create primitive values
461        let error_code = -32600;
462        let error_msg = "Invalid Request";
463        // 2. Capture primitives as strings
464        let code_str = error_code.to_string();
465        // 3. Wrap to custom Error type
466        let wrapped_err = Error::RpcError(error_code, error_msg.to_string());
467        // 4. Capture wrapped error string
468        let wrapped_str = wrapped_err.to_string();
469        // 5. Verify primitive strings are substrings of wrapped string
470        assert!(
471            wrapped_str.contains(&code_str),
472            "Wrapped error '{}' should contain error code '{}'",
473            wrapped_str,
474            code_str
475        );
476        assert!(
477            wrapped_str.contains(error_msg),
478            "Wrapped error '{}' should contain error message '{}'",
479            wrapped_str,
480            error_msg
481        );
482        assert!(wrapped_str.starts_with("RPC error"));
483    }
484
485    #[test]
486    fn test_invalid_rpc_id() {
487        // 1. Create primitive value
488        let invalid_id = "not-a-valid-id-123";
489        // 2. Capture primitive as string (already a string)
490        // 3. Wrap to custom Error type
491        let wrapped_err = Error::InvalidRpcId(invalid_id.to_string());
492        // 4. Capture wrapped error string
493        let wrapped_str = wrapped_err.to_string();
494        // 5. Verify primitive string is substring of wrapped string
495        assert!(
496            wrapped_str.contains(invalid_id),
497            "Wrapped error '{}' should contain invalid ID '{}'",
498            wrapped_str,
499            invalid_id
500        );
501        assert!(wrapped_str.starts_with("Invalid RPC ID: "));
502    }
503
504    #[test]
505    fn test_invalid_file_type() {
506        // 1. Create primitive value
507        let file_type = "unknown_format";
508        // 2. Capture primitive as string (already a string)
509        // 3. Wrap to custom Error type
510        let wrapped_err = Error::InvalidFileType(file_type.to_string());
511        // 4. Capture wrapped error string
512        let wrapped_str = wrapped_err.to_string();
513        // 5. Verify primitive string is substring of wrapped string
514        assert!(
515            wrapped_str.contains(file_type),
516            "Wrapped error '{}' should contain file type '{}'",
517            wrapped_str,
518            file_type
519        );
520        assert!(wrapped_str.starts_with("Invalid file type: "));
521    }
522
523    #[test]
524    fn test_invalid_annotation_type() {
525        // 1. Create primitive value
526        let annotation_type = "unsupported_annotation";
527        // 2. Capture primitive as string (already a string)
528        // 3. Wrap to custom Error type
529        let wrapped_err = Error::InvalidAnnotationType(annotation_type.to_string());
530        // 4. Capture wrapped error string
531        let wrapped_str = wrapped_err.to_string();
532        // 5. Verify primitive string is substring of wrapped string
533        assert!(
534            wrapped_str.contains(annotation_type),
535            "Wrapped error '{}' should contain annotation type '{}'",
536            wrapped_str,
537            annotation_type
538        );
539        assert!(wrapped_str.starts_with("Invalid annotation type: "));
540    }
541
542    #[test]
543    fn test_unsupported_format() {
544        // 1. Create primitive value
545        let format = "xyz_format";
546        // 2. Capture primitive as string (already a string)
547        // 3. Wrap to custom Error type
548        let wrapped_err = Error::UnsupportedFormat(format.to_string());
549        // 4. Capture wrapped error string
550        let wrapped_str = wrapped_err.to_string();
551        // 5. Verify primitive string is substring of wrapped string
552        assert!(
553            wrapped_str.contains(format),
554            "Wrapped error '{}' should contain format '{}'",
555            wrapped_str,
556            format
557        );
558        assert!(wrapped_str.starts_with("Unsupported format: "));
559    }
560
561    #[test]
562    fn test_missing_images() {
563        // 1. Create primitive value
564        let details = "image001.jpg, image002.jpg";
565        // 2. Capture primitive as string (already a string)
566        // 3. Wrap to custom Error type
567        let wrapped_err = Error::MissingImages(details.to_string());
568        // 4. Capture wrapped error string
569        let wrapped_str = wrapped_err.to_string();
570        // 5. Verify primitive string is substring of wrapped string
571        assert!(
572            wrapped_str.contains(details),
573            "Wrapped error '{}' should contain details '{}'",
574            wrapped_str,
575            details
576        );
577        assert!(wrapped_str.starts_with("Missing images: "));
578    }
579
580    #[test]
581    fn test_missing_annotations() {
582        // 1. Create primitive value
583        let details = "annotations.json";
584        // 2. Capture primitive as string (already a string)
585        // 3. Wrap to custom Error type
586        let wrapped_err = Error::MissingAnnotations(details.to_string());
587        // 4. Capture wrapped error string
588        let wrapped_str = wrapped_err.to_string();
589        // 5. Verify primitive string is substring of wrapped string
590        assert!(
591            wrapped_str.contains(details),
592            "Wrapped error '{}' should contain details '{}'",
593            wrapped_str,
594            details
595        );
596        assert!(wrapped_str.starts_with("Missing annotations: "));
597    }
598
599    #[test]
600    fn test_missing_label() {
601        // 1. Create primitive value
602        let label = "person";
603        // 2. Capture primitive as string (already a string)
604        // 3. Wrap to custom Error type
605        let wrapped_err = Error::MissingLabel(label.to_string());
606        // 4. Capture wrapped error string
607        let wrapped_str = wrapped_err.to_string();
608        // 5. Verify primitive string is substring of wrapped string
609        assert!(
610            wrapped_str.contains(label),
611            "Wrapped error '{}' should contain label '{}'",
612            wrapped_str,
613            label
614        );
615        assert!(wrapped_str.starts_with("Missing label: "));
616    }
617
618    #[test]
619    fn test_invalid_parameters() {
620        // 1. Create primitive value
621        let params = "batch_size must be positive";
622        // 2. Capture primitive as string (already a string)
623        // 3. Wrap to custom Error type
624        let wrapped_err = Error::InvalidParameters(params.to_string());
625        // 4. Capture wrapped error string
626        let wrapped_str = wrapped_err.to_string();
627        // 5. Verify primitive string is substring of wrapped string
628        assert!(
629            wrapped_str.contains(params),
630            "Wrapped error '{}' should contain params '{}'",
631            wrapped_str,
632            params
633        );
634        assert!(wrapped_str.starts_with("Invalid parameters: "));
635    }
636
637    #[test]
638    fn test_feature_not_enabled() {
639        // 1. Create primitive value
640        let feature = "polars";
641        // 2. Capture primitive as string (already a string)
642        // 3. Wrap to custom Error type
643        let wrapped_err = Error::FeatureNotEnabled(feature.to_string());
644        // 4. Capture wrapped error string
645        let wrapped_str = wrapped_err.to_string();
646        // 5. Verify primitive string is substring of wrapped string
647        assert!(
648            wrapped_str.contains(feature),
649            "Wrapped error '{}' should contain feature '{}'",
650            wrapped_str,
651            feature
652        );
653        assert!(wrapped_str.starts_with("Feature not enabled: "));
654    }
655
656    #[test]
657    fn test_invalid_etag() {
658        // 1. Create primitive value
659        let etag = "malformed-etag-value";
660        // 2. Capture primitive as string (already a string)
661        // 3. Wrap to custom Error type
662        let wrapped_err = Error::InvalidEtag(etag.to_string());
663        // 4. Capture wrapped error string
664        let wrapped_str = wrapped_err.to_string();
665        // 5. Verify primitive string is substring of wrapped string
666        assert!(
667            wrapped_str.contains(etag),
668            "Wrapped error '{}' should contain etag '{}'",
669            wrapped_str,
670            etag
671        );
672        assert!(wrapped_str.starts_with("Invalid ETag header: "));
673    }
674
675    // Tests for simple errors without wrapped content
676    // Just verify they can be created and displayed
677
678    #[test]
679    fn test_invalid_response() {
680        let err = Error::InvalidResponse;
681        let err_str = err.to_string();
682        assert_eq!(err_str, "Invalid server response");
683    }
684
685    #[test]
686    fn test_not_implemented() {
687        let err = Error::NotImplemented;
688        let err_str = err.to_string();
689        assert_eq!(err_str, "Not implemented");
690    }
691
692    #[test]
693    fn test_part_too_large() {
694        let err = Error::PartTooLarge;
695        let err_str = err.to_string();
696        assert_eq!(err_str, "File part size exceeds maximum limit");
697    }
698
699    #[test]
700    fn test_empty_token() {
701        let err = Error::EmptyToken;
702        let err_str = err.to_string();
703        assert_eq!(err_str, "Authentication token is empty");
704    }
705
706    #[test]
707    fn test_invalid_token() {
708        let err = Error::InvalidToken;
709        let err_str = err.to_string();
710        assert_eq!(err_str, "Invalid authentication token");
711    }
712
713    #[test]
714    fn test_token_expired() {
715        let err = Error::TokenExpired;
716        let err_str = err.to_string();
717        assert_eq!(err_str, "Authentication token has expired");
718    }
719
720    #[test]
721    fn test_unauthorized() {
722        let err = Error::Unauthorized;
723        let err_str = err.to_string();
724        assert_eq!(err_str, "Unauthorized access");
725    }
726}