ito_core/
backend_client.rs1use std::path::PathBuf;
8use std::time::Duration;
9
10use ito_config::types::BackendApiConfig;
11
12use crate::errors::{CoreError, CoreResult};
13
14#[derive(Debug, Clone)]
20pub struct BackendRuntime {
21 pub base_url: String,
23 pub token: String,
25 pub timeout: Duration,
27 pub max_retries: u32,
29 pub backup_dir: PathBuf,
31}
32
33pub fn resolve_backend_runtime(config: &BackendApiConfig) -> CoreResult<Option<BackendRuntime>> {
38 if !config.enabled {
39 return Ok(None);
40 }
41
42 let token = resolve_token(config)?;
43 let backup_dir = resolve_backup_dir(config);
44 let timeout = Duration::from_millis(config.timeout_ms);
45
46 Ok(Some(BackendRuntime {
47 base_url: config.url.clone(),
48 token,
49 timeout,
50 max_retries: config.max_retries,
51 backup_dir,
52 }))
53}
54
55fn resolve_token(config: &BackendApiConfig) -> CoreResult<String> {
57 if let Some(token) = &config.token {
58 let token = token.trim();
59 if !token.is_empty() {
60 return Ok(token.to_string());
61 }
62 }
63
64 let env_var = &config.token_env_var;
65 match std::env::var(env_var) {
66 Ok(val) if !val.trim().is_empty() => Ok(val.trim().to_string()),
67 Ok(_) => Err(CoreError::validation(format!(
68 "Backend mode is enabled but environment variable '{env_var}' is empty. \
69 Set the token via '{env_var}' or 'backend.token' in config."
70 ))),
71 Err(_) => Err(CoreError::validation(format!(
72 "Backend mode is enabled but environment variable '{env_var}' is not set. \
73 Set the token via '{env_var}' or 'backend.token' in config."
74 ))),
75 }
76}
77
78fn resolve_backup_dir(config: &BackendApiConfig) -> PathBuf {
80 if let Some(dir) = &config.backup_dir {
81 return PathBuf::from(dir);
82 }
83
84 let home = std::env::var("HOME")
85 .or_else(|_| std::env::var("USERPROFILE"))
86 .unwrap_or_else(|_| "/tmp".to_string());
87
88 PathBuf::from(home).join(".ito").join("backups")
89}
90
91pub fn is_retriable_status(status: u16) -> bool {
96 match status {
97 429 => true,
98 s if s >= 500 => true,
99 _ => false,
100 }
101}
102
103pub fn idempotency_key(operation: &str) -> String {
108 format!("{}-{operation}", uuid::Uuid::new_v4())
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn disabled_backend_returns_none() {
117 let config = BackendApiConfig::default();
118 assert!(!config.enabled);
119
120 let result = resolve_backend_runtime(&config).unwrap();
121 assert!(result.is_none());
122 }
123
124 #[test]
125 fn enabled_backend_with_explicit_token_resolves() {
126 let config = BackendApiConfig {
127 enabled: true,
128 token: Some("test-token-123".to_string()),
129 ..BackendApiConfig::default()
130 };
131
132 let runtime = resolve_backend_runtime(&config).unwrap().unwrap();
133 assert_eq!(runtime.token, "test-token-123");
134 assert_eq!(runtime.base_url, "http://127.0.0.1:9010");
135 assert_eq!(runtime.max_retries, 3);
136 assert_eq!(runtime.timeout, Duration::from_millis(30_000));
137 }
138
139 #[test]
140 fn enabled_backend_with_env_var_token_resolves() {
141 let env_var = "ITO_TEST_BACKEND_TOKEN_1";
142 unsafe { std::env::set_var(env_var, "env-token-456") };
144
145 let config = BackendApiConfig {
146 enabled: true,
147 token: None,
148 token_env_var: env_var.to_string(),
149 ..BackendApiConfig::default()
150 };
151
152 let runtime = resolve_backend_runtime(&config).unwrap().unwrap();
153 assert_eq!(runtime.token, "env-token-456");
154
155 unsafe { std::env::remove_var(env_var) };
157 }
158
159 #[test]
160 fn enabled_backend_missing_token_fails() {
161 let env_var = "ITO_TEST_NONEXISTENT_TOKEN_VAR";
162 unsafe { std::env::remove_var(env_var) };
164
165 let config = BackendApiConfig {
166 enabled: true,
167 token: None,
168 token_env_var: env_var.to_string(),
169 ..BackendApiConfig::default()
170 };
171
172 let err = resolve_backend_runtime(&config).unwrap_err();
173 let msg = err.to_string();
174 assert!(msg.contains(env_var), "error should mention env var: {msg}");
175 assert!(
176 msg.contains("not set"),
177 "error should mention 'not set': {msg}"
178 );
179 }
180
181 #[test]
182 fn enabled_backend_empty_token_fails() {
183 let env_var = "ITO_TEST_EMPTY_TOKEN_VAR";
184 unsafe { std::env::set_var(env_var, "") };
186
187 let config = BackendApiConfig {
188 enabled: true,
189 token: None,
190 token_env_var: env_var.to_string(),
191 ..BackendApiConfig::default()
192 };
193
194 let err = resolve_backend_runtime(&config).unwrap_err();
195 let msg = err.to_string();
196 assert!(msg.contains("empty"), "error should mention 'empty': {msg}");
197
198 unsafe { std::env::remove_var(env_var) };
200 }
201
202 #[test]
203 fn custom_backup_dir_is_used() {
204 let config = BackendApiConfig {
205 enabled: true,
206 token: Some("t".to_string()),
207 backup_dir: Some("/custom/backups".to_string()),
208 ..BackendApiConfig::default()
209 };
210
211 let runtime = resolve_backend_runtime(&config).unwrap().unwrap();
212 assert_eq!(runtime.backup_dir, PathBuf::from("/custom/backups"));
213 }
214
215 #[test]
216 fn default_backup_dir_uses_home() {
217 let config = BackendApiConfig {
218 enabled: true,
219 token: Some("t".to_string()),
220 backup_dir: None,
221 ..BackendApiConfig::default()
222 };
223
224 let runtime = resolve_backend_runtime(&config).unwrap().unwrap();
225 let path_str = runtime.backup_dir.to_string_lossy();
227 assert!(
228 path_str.ends_with(".ito/backups"),
229 "unexpected backup dir: {path_str}"
230 );
231 }
232
233 #[test]
234 fn is_retriable_status_checks() {
235 assert!(is_retriable_status(429));
236 assert!(is_retriable_status(500));
237 assert!(is_retriable_status(502));
238 assert!(is_retriable_status(503));
239 assert!(!is_retriable_status(400));
240 assert!(!is_retriable_status(401));
241 assert!(!is_retriable_status(404));
242 assert!(!is_retriable_status(200));
243 }
244
245 #[test]
246 fn idempotency_key_includes_operation() {
247 let key = idempotency_key("push");
248 assert!(key.ends_with("-push"));
249 assert!(key.len() > 5); }
251}