Skip to main content

max / makenotwork

6.8 KB · 194 lines History Blame Raw
1 //! Error types for the SyncKit client SDK.
2
3 use thiserror::Error;
4
5 /// All errors that can occur in the SyncKit client.
6 #[derive(Debug, Error)]
7 pub enum SyncKitError {
8 /// Network-level failure: connection refused, timeout, DNS resolution, TLS handshake.
9 #[error("HTTP request failed: {0}")]
10 Http(#[from] reqwest::Error),
11
12 /// Server returned a non-success HTTP status (4xx or 5xx). Status and message
13 /// extracted from response.
14 #[error("Server returned {status}: {message}")]
15 Server {
16 status: u16,
17 message: String,
18 /// Parsed `Retry-After` header value in seconds (429 responses only).
19 /// Hidden from public API — used internally by the retry loop.
20 #[doc(hidden)]
21 retry_after_secs: Option<u64>,
22 },
23
24 /// Response body could not be parsed as expected JSON type.
25 #[error("JSON serialization error: {0}")]
26 Json(#[from] serde_json::Error),
27
28 /// Encryption method called before `setup_encryption_new` or
29 /// `setup_encryption_existing`.
30 #[error("Encryption not initialized — call setup_encryption first")]
31 NoMasterKey,
32
33 /// `unwrap_master_key` or `decrypt_data`/`decrypt_bytes` failed. Wrong
34 /// password or corrupted ciphertext.
35 #[error("Wrong password or corrupted key envelope")]
36 DecryptionFailed,
37
38 /// Key envelope JSON has an unrecognized version or missing fields.
39 #[error("Invalid key envelope: {0}")]
40 InvalidEnvelope(String),
41
42 /// Argon2 key derivation or AEAD encryption/decryption failed (corrupt
43 /// data, wrong parameters).
44 #[error("Encryption error: {0}")]
45 Crypto(String),
46
47 /// Base64 decoding of encrypted payloads failed.
48 #[error("Base64 decode error: {0}")]
49 Base64(#[from] base64::DecodeError),
50
51 /// API method called before `authenticate` or `restore_session`.
52 #[error("Not authenticated — call authenticate first")]
53 NotAuthenticated,
54
55 /// JWT `exp` claim is within 30 seconds of current time. Caller should
56 /// re-authenticate.
57 #[error("Token expired — re-authenticate to continue syncing")]
58 TokenExpired,
59
60 /// Internal error that should not occur in normal operation.
61 #[error("Internal error: {0}")]
62 Internal(String),
63
64 /// OS keychain operation failed (store, load, or delete). Platform-specific.
65 #[cfg(feature = "keychain")]
66 #[error("Keychain error: {0}")]
67 Keychain(String),
68 }
69
70 #[cfg(feature = "keychain")]
71 impl From<keyring::Error> for SyncKitError {
72 fn from(e: keyring::Error) -> Self {
73 SyncKitError::Keychain(e.to_string())
74 }
75 }
76
77 /// Convenience alias.
78 pub type Result<T> = std::result::Result<T, SyncKitError>;
79
80 #[cfg(test)]
81 mod tests {
82 use super::*;
83 use std::error::Error;
84
85 #[test]
86 fn error_is_send_and_sync() {
87 fn assert_send_sync<T: Send + Sync>() {}
88 assert_send_sync::<SyncKitError>();
89 }
90
91 #[test]
92 fn display_all_variants() {
93 let cases: Vec<(SyncKitError, &str)> = vec![
94 (
95 SyncKitError::Server { status: 500, message: "boom".into(), retry_after_secs: None },
96 "Server returned 500: boom",
97 ),
98 (SyncKitError::NoMasterKey, "Encryption not initialized"),
99 (SyncKitError::DecryptionFailed, "Wrong password"),
100 (SyncKitError::InvalidEnvelope("bad".into()), "Invalid key envelope: bad"),
101 (SyncKitError::Crypto("aead".into()), "Encryption error: aead"),
102 (SyncKitError::NotAuthenticated, "Not authenticated"),
103 (SyncKitError::TokenExpired, "Token expired"),
104 (SyncKitError::Internal("oops".into()), "Internal error: oops"),
105 ];
106 for (err, expected_substring) in cases {
107 let msg = err.to_string();
108 assert!(
109 msg.contains(expected_substring),
110 "Expected '{expected_substring}' in '{msg}'"
111 );
112 }
113 }
114
115 #[test]
116 fn debug_format_no_panic() {
117 let variants: Vec<SyncKitError> = vec![
118 SyncKitError::Server { status: 500, message: "err".into(), retry_after_secs: None },
119 SyncKitError::NoMasterKey,
120 SyncKitError::DecryptionFailed,
121 SyncKitError::InvalidEnvelope("v".into()),
122 SyncKitError::Crypto("c".into()),
123 SyncKitError::NotAuthenticated,
124 SyncKitError::TokenExpired,
125 SyncKitError::Internal("i".into()),
126 ];
127 for v in variants {
128 let debug = format!("{v:?}");
129 assert!(!debug.is_empty());
130 }
131 }
132
133 #[test]
134 fn source_json_error() {
135 let inner = serde_json::from_str::<serde_json::Value>("bad").unwrap_err();
136 let err = SyncKitError::Json(inner);
137 assert!(err.source().is_some(), "Json variant should chain source");
138 }
139
140 #[test]
141 fn source_base64_error() {
142 use base64::Engine;
143 let inner = base64::engine::general_purpose::STANDARD
144 .decode("!!!invalid!!!")
145 .unwrap_err();
146 let err = SyncKitError::Base64(inner);
147 assert!(err.source().is_some(), "Base64 variant should chain source");
148 }
149
150 #[test]
151 fn source_none_for_leaf_variants() {
152 assert!(SyncKitError::NoMasterKey.source().is_none());
153 assert!(SyncKitError::DecryptionFailed.source().is_none());
154 assert!(SyncKitError::NotAuthenticated.source().is_none());
155 assert!(SyncKitError::TokenExpired.source().is_none());
156 assert!(SyncKitError::InvalidEnvelope("x".into()).source().is_none());
157 assert!(SyncKitError::Crypto("x".into()).source().is_none());
158 assert!(SyncKitError::Internal("x".into()).source().is_none());
159 let server = SyncKitError::Server { status: 500, message: "x".into(), retry_after_secs: None };
160 assert!(server.source().is_none());
161 }
162
163 #[test]
164 fn server_error_empty_message() {
165 let err = SyncKitError::Server { status: 503, message: String::new(), retry_after_secs: None };
166 let msg = err.to_string();
167 assert!(msg.contains("503"));
168 assert!(msg.contains(": "), "Should have colon separator even with empty message");
169 }
170
171 #[test]
172 fn server_error_very_long_message() {
173 let long = "x".repeat(1_000_000);
174 let err = SyncKitError::Server { status: 500, message: long, retry_after_secs: None };
175 let msg = err.to_string();
176 assert!(msg.contains("500"));
177 assert!(msg.len() > 1_000_000);
178 }
179
180 #[test]
181 fn invalid_envelope_preserves_detail() {
182 let detail = "unsupported version 99";
183 let err = SyncKitError::InvalidEnvelope(detail.into());
184 assert!(err.to_string().contains(detail));
185 }
186
187 #[test]
188 fn internal_preserves_detail() {
189 let detail = "unexpected state in push handler";
190 let err = SyncKitError::Internal(detail.into());
191 assert!(err.to_string().contains(detail));
192 }
193 }
194