corteq_onepassword/
error.rs

1//! Error types for the corteq-onepassword crate.
2//!
3//! All error types are designed to be informative while ensuring
4//! sensitive data (tokens, secret values) is never included in error messages.
5
6use thiserror::Error;
7
8/// Result type alias using the crate's Error type.
9pub type Result<T> = std::result::Result<T, Error>;
10
11/// Errors that can occur when using the 1Password client.
12///
13/// Error messages are designed to be helpful for debugging while
14/// ensuring that sensitive data (tokens, secret values) is never exposed.
15#[derive(Error, Debug)]
16pub enum Error {
17    /// The `OP_SERVICE_ACCOUNT_TOKEN` environment variable is not set
18    /// and no explicit token was provided via `from_token()`.
19    #[error("missing authentication token: set OP_SERVICE_ACCOUNT_TOKEN or use from_token()")]
20    MissingAuthToken,
21
22    /// The provided token has an invalid format.
23    #[error("invalid authentication token format")]
24    InvalidToken,
25
26    /// Authentication with 1Password failed.
27    /// This typically means the token is expired or revoked.
28    #[error("authentication failed: {message}")]
29    AuthenticationFailed {
30        /// A message describing the authentication failure (never contains the token).
31        message: String,
32    },
33
34    /// Failed to establish or maintain the SDK session.
35    #[error("session error: {message}")]
36    SessionError {
37        /// A message describing the session error.
38        message: String,
39    },
40
41    /// The secret reference format is invalid.
42    ///
43    /// Valid format: `op://vault/item/field` or `op://vault/item/section/field`
44    #[error("invalid secret reference '{reference}': {reason}")]
45    InvalidReference {
46        /// The invalid reference string (safe to display, contains no secrets).
47        reference: String,
48        /// The reason the reference is invalid.
49        reason: String,
50    },
51
52    /// The requested secret was not found in 1Password.
53    #[error("secret not found: {reference}")]
54    SecretNotFound {
55        /// The reference that was not found.
56        reference: String,
57    },
58
59    /// Access to the specified vault was denied.
60    /// This typically means the service account lacks permission for this vault.
61    #[error("access denied to vault: {vault}")]
62    AccessDenied {
63        /// The vault name that access was denied to.
64        vault: String,
65    },
66
67    /// A network error occurred while communicating with 1Password.
68    #[error("network error: {message}")]
69    NetworkError {
70        /// A message describing the network error.
71        message: String,
72    },
73
74    /// An error occurred in the underlying 1Password SDK.
75    #[error("SDK error: {message}")]
76    SdkError {
77        /// A message describing the SDK error.
78        message: String,
79    },
80
81    /// Failed to load the native 1Password library.
82    #[error("failed to load native library: {message}")]
83    LibraryLoadError {
84        /// A message describing the library loading error.
85        message: String,
86    },
87
88    /// JSON serialization or deserialization error.
89    #[error("JSON error: {message}")]
90    JsonError {
91        /// A message describing the JSON error.
92        message: String,
93    },
94}
95
96impl Error {
97    /// Check if this error is retriable (e.g., transient network issues).
98    pub fn is_retriable(&self) -> bool {
99        matches!(
100            self,
101            Error::NetworkError { .. } | Error::SessionError { .. }
102        )
103    }
104
105    /// Check if this error indicates an authentication problem.
106    pub fn is_auth_error(&self) -> bool {
107        matches!(
108            self,
109            Error::MissingAuthToken
110                | Error::InvalidToken
111                | Error::AuthenticationFailed { .. }
112                | Error::AccessDenied { .. }
113        )
114    }
115}
116
117impl From<serde_json::Error> for Error {
118    fn from(err: serde_json::Error) -> Self {
119        Error::JsonError {
120            message: err.to_string(),
121        }
122    }
123}
124
125impl From<libloading::Error> for Error {
126    fn from(err: libloading::Error) -> Self {
127        Error::LibraryLoadError {
128            message: err.to_string(),
129        }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_error_is_send_sync() {
139        fn assert_send_sync<T: Send + Sync>() {}
140        assert_send_sync::<Error>();
141    }
142
143    #[test]
144    fn test_error_display_no_secrets() {
145        let error = Error::AuthenticationFailed {
146            message: "token expired".to_string(),
147        };
148        let display = error.to_string();
149        assert!(!display.contains("ops_"));
150    }
151
152    // ==========================================================================
153    // is_retriable() tests
154    // ==========================================================================
155
156    #[test]
157    fn test_is_retriable_network_error() {
158        let error = Error::NetworkError {
159            message: "connection reset".to_string(),
160        };
161        assert!(error.is_retriable());
162    }
163
164    #[test]
165    fn test_is_retriable_session_error() {
166        let error = Error::SessionError {
167            message: "session expired".to_string(),
168        };
169        assert!(error.is_retriable());
170    }
171
172    #[test]
173    fn test_is_retriable_non_retriable_errors() {
174        // All these should NOT be retriable
175        let non_retriable = vec![
176            Error::MissingAuthToken,
177            Error::InvalidToken,
178            Error::AuthenticationFailed {
179                message: "bad token".to_string(),
180            },
181            Error::InvalidReference {
182                reference: "op://x".to_string(),
183                reason: "too short".to_string(),
184            },
185            Error::SecretNotFound {
186                reference: "op://vault/item/field".to_string(),
187            },
188            Error::AccessDenied {
189                vault: "private".to_string(),
190            },
191            Error::SdkError {
192                message: "internal".to_string(),
193            },
194            Error::LibraryLoadError {
195                message: "not found".to_string(),
196            },
197            Error::JsonError {
198                message: "parse error".to_string(),
199            },
200        ];
201
202        for error in non_retriable {
203            assert!(!error.is_retriable(), "{error:?} should not be retriable");
204        }
205    }
206
207    // ==========================================================================
208    // is_auth_error() tests
209    // ==========================================================================
210
211    #[test]
212    fn test_is_auth_error_missing_token() {
213        assert!(Error::MissingAuthToken.is_auth_error());
214    }
215
216    #[test]
217    fn test_is_auth_error_invalid_token() {
218        assert!(Error::InvalidToken.is_auth_error());
219    }
220
221    #[test]
222    fn test_is_auth_error_auth_failed() {
223        let error = Error::AuthenticationFailed {
224            message: "token expired".to_string(),
225        };
226        assert!(error.is_auth_error());
227    }
228
229    #[test]
230    fn test_is_auth_error_access_denied() {
231        let error = Error::AccessDenied {
232            vault: "private-vault".to_string(),
233        };
234        assert!(error.is_auth_error());
235    }
236
237    #[test]
238    fn test_is_auth_error_non_auth_errors() {
239        // All these should NOT be auth errors
240        let non_auth = vec![
241            Error::SessionError {
242                message: "session expired".to_string(),
243            },
244            Error::InvalidReference {
245                reference: "op://x".to_string(),
246                reason: "too short".to_string(),
247            },
248            Error::SecretNotFound {
249                reference: "op://vault/item/field".to_string(),
250            },
251            Error::NetworkError {
252                message: "timeout".to_string(),
253            },
254            Error::SdkError {
255                message: "internal".to_string(),
256            },
257            Error::LibraryLoadError {
258                message: "not found".to_string(),
259            },
260            Error::JsonError {
261                message: "parse error".to_string(),
262            },
263        ];
264
265        for error in non_auth {
266            assert!(!error.is_auth_error(), "{error:?} should not be auth error");
267        }
268    }
269
270    // ==========================================================================
271    // From implementations tests
272    // ==========================================================================
273
274    #[test]
275    fn test_from_serde_json_error() {
276        // Create an actual serde_json error by trying to parse invalid JSON
277        let json_err = serde_json::from_str::<String>("not valid json").unwrap_err();
278        let error: Error = json_err.into();
279
280        assert!(matches!(error, Error::JsonError { .. }));
281        let display = error.to_string();
282        assert!(display.contains("JSON error"));
283    }
284
285    #[test]
286    fn test_from_libloading_error() {
287        // Create a libloading error by trying to load a non-existent library
288        let lib_err =
289            unsafe { libloading::Library::new("/nonexistent/path/to/lib.so") }.unwrap_err();
290        let error: Error = lib_err.into();
291
292        assert!(matches!(error, Error::LibraryLoadError { .. }));
293        let display = error.to_string();
294        assert!(display.contains("native library"));
295    }
296
297    // ==========================================================================
298    // Display message tests
299    // ==========================================================================
300
301    #[test]
302    fn test_error_display_missing_auth_token() {
303        let error = Error::MissingAuthToken;
304        let display = error.to_string();
305        assert!(display.contains("missing authentication token"));
306        assert!(display.contains("OP_SERVICE_ACCOUNT_TOKEN"));
307    }
308
309    #[test]
310    fn test_error_display_invalid_token() {
311        let error = Error::InvalidToken;
312        let display = error.to_string();
313        assert!(display.contains("invalid authentication token format"));
314    }
315
316    #[test]
317    fn test_error_display_authentication_failed() {
318        let error = Error::AuthenticationFailed {
319            message: "token expired".to_string(),
320        };
321        let display = error.to_string();
322        assert!(display.contains("authentication failed"));
323        assert!(display.contains("token expired"));
324    }
325
326    #[test]
327    fn test_error_display_session_error() {
328        let error = Error::SessionError {
329            message: "connection lost".to_string(),
330        };
331        let display = error.to_string();
332        assert!(display.contains("session error"));
333        assert!(display.contains("connection lost"));
334    }
335
336    #[test]
337    fn test_error_display_invalid_reference() {
338        let error = Error::InvalidReference {
339            reference: "op://vault".to_string(),
340            reason: "missing item and field".to_string(),
341        };
342        let display = error.to_string();
343        assert!(display.contains("invalid secret reference"));
344        assert!(display.contains("op://vault"));
345        assert!(display.contains("missing item and field"));
346    }
347
348    #[test]
349    fn test_error_display_secret_not_found() {
350        let error = Error::SecretNotFound {
351            reference: "op://vault/item/field".to_string(),
352        };
353        let display = error.to_string();
354        assert!(display.contains("secret not found"));
355        assert!(display.contains("op://vault/item/field"));
356    }
357
358    #[test]
359    fn test_error_display_access_denied() {
360        let error = Error::AccessDenied {
361            vault: "private-vault".to_string(),
362        };
363        let display = error.to_string();
364        assert!(display.contains("access denied"));
365        assert!(display.contains("private-vault"));
366    }
367
368    #[test]
369    fn test_error_display_network_error() {
370        let error = Error::NetworkError {
371            message: "connection timed out".to_string(),
372        };
373        let display = error.to_string();
374        assert!(display.contains("network error"));
375        assert!(display.contains("connection timed out"));
376    }
377
378    #[test]
379    fn test_error_display_sdk_error() {
380        let error = Error::SdkError {
381            message: "internal SDK failure".to_string(),
382        };
383        let display = error.to_string();
384        assert!(display.contains("SDK error"));
385        assert!(display.contains("internal SDK failure"));
386    }
387
388    #[test]
389    fn test_error_display_library_load_error() {
390        let error = Error::LibraryLoadError {
391            message: "library not found".to_string(),
392        };
393        let display = error.to_string();
394        assert!(display.contains("native library"));
395        assert!(display.contains("library not found"));
396    }
397
398    #[test]
399    fn test_error_display_json_error() {
400        let error = Error::JsonError {
401            message: "unexpected token".to_string(),
402        };
403        let display = error.to_string();
404        assert!(display.contains("JSON error"));
405        assert!(display.contains("unexpected token"));
406    }
407
408    // ==========================================================================
409    // Debug trait test
410    // ==========================================================================
411
412    #[test]
413    fn test_error_debug_impl() {
414        let error = Error::SdkError {
415            message: "test".to_string(),
416        };
417        let debug_str = format!("{error:?}");
418        assert!(debug_str.contains("SdkError"));
419    }
420}