Skip to main content

max / synckit-client

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