diff --git a/src-tauri/src/commands/credential.rs b/src-tauri/src/commands/credential.rs index 954c9f9..2334b25 100644 --- a/src-tauri/src/commands/credential.rs +++ b/src-tauri/src/commands/credential.rs @@ -1,6 +1,7 @@ use crate::error::AppError; use crate::models::credential::{TuleapCredentials, TuleapCredentialsSafe}; use crate::services::crypto; +use crate::services::tuleap_client::TuleapClient; use crate::AppState; use tauri::State; @@ -64,17 +65,8 @@ pub async fn test_tuleap_connection(state: State<'_, AppState>) -> Result Result { - let started_at = Instant::now(); - eprintln!("[tuleap] -> GET {}", url); + const MAX_ATTEMPTS: u32 = 3; + const BASE_DELAY_MS: u64 = 500; - let response = self - .http - .get(url) - .basic_auth(&self.username, Some(&self.password)) - .send() - .await; + for attempt in 1..=MAX_ATTEMPTS { + let started_at = Instant::now(); + eprintln!("[tuleap] -> GET {} (attempt {}/{})", url, attempt, MAX_ATTEMPTS); - match response { - Ok(resp) => { - eprintln!( - "[tuleap] <- GET {} | status={} | {}ms", - url, - resp.status(), - started_at.elapsed().as_millis() - ); - Ok(resp) - } - Err(err) => { - eprintln!( - "[tuleap] xx GET {} | error={} | {}ms", - url, - err, - started_at.elapsed().as_millis() - ); - Err(format!("request failed: {}", err)) + let response = self + .http + .get(url) + .basic_auth(&self.username, Some(&self.password)) + .send() + .await; + + match response { + Ok(resp) => { + let status = resp.status(); + eprintln!( + "[tuleap] <- GET {} | status={} | {}ms", + url, + status, + started_at.elapsed().as_millis() + ); + + // Retry transient HTTP failures. + if (status == reqwest::StatusCode::TOO_MANY_REQUESTS || status.is_server_error()) + && attempt < MAX_ATTEMPTS + { + let delay_ms = BASE_DELAY_MS * 2u64.pow(attempt - 1); + eprintln!( + "[tuleap] ~~ retry GET {} in {}ms (status={})", + url, delay_ms, status + ); + sleep(Duration::from_millis(delay_ms)).await; + continue; + } + + return Ok(resp); + } + Err(err) => { + eprintln!( + "[tuleap] xx GET {} | error={} | {}ms", + url, + err, + started_at.elapsed().as_millis() + ); + + if attempt < MAX_ATTEMPTS { + let delay_ms = BASE_DELAY_MS * 2u64.pow(attempt - 1); + eprintln!( + "[tuleap] ~~ retry GET {} in {}ms (error={})", + url, delay_ms, err + ); + sleep(Duration::from_millis(delay_ms)).await; + continue; + } + + return Err(format!("request failed: {}", err)); + } } } + + Err("request failed after retries".to_string()) } #[allow(dead_code)] @@ -211,6 +245,15 @@ pub fn extract_artifact_field_values( artifact: &serde_json::Value, field_label: &str, ) -> Vec { + fn push_unique(values: &mut Vec, value: String) { + if value.is_empty() { + return; + } + if !values.iter().any(|v| v == &value) { + values.push(value); + } + } + let values_arr = match artifact.get("values").and_then(|v| v.as_array()) { Some(a) => a, None => return vec![], @@ -233,31 +276,52 @@ pub fn extract_artifact_field_values( match field_type { "sb" | "rb" => { - // values[*].label + // values[*].id + values[*].label entry .get("values") .and_then(|v| v.as_array()) .map(|arr| { - arr.iter() - .filter_map(|v| v.get("label").and_then(|l| l.as_str()).map(str::to_string)) - .collect() + let mut out = Vec::new(); + for v in arr { + if let Some(id) = v.get("id") { + match id { + serde_json::Value::String(s) => push_unique(&mut out, s.clone()), + serde_json::Value::Number(n) => push_unique(&mut out, n.to_string()), + _ => {} + } + } + if let Some(label) = v.get("label").and_then(|l| l.as_str()) { + push_unique(&mut out, label.to_string()); + } + } + out }) .unwrap_or_default() } "msb" | "cb" => { - // bind_value_objects[*].display_name, fallback to label + // bind_value_objects[*].id + display_name/label entry .get("bind_value_objects") .and_then(|v| v.as_array()) .map(|arr| { - arr.iter() - .filter_map(|v| { - v.get("display_name") - .and_then(|d| d.as_str()) - .or_else(|| v.get("label").and_then(|l| l.as_str())) - .map(str::to_string) - }) - .collect() + let mut out = Vec::new(); + for v in arr { + if let Some(id) = v.get("id") { + match id { + serde_json::Value::String(s) => push_unique(&mut out, s.clone()), + serde_json::Value::Number(n) => push_unique(&mut out, n.to_string()), + _ => {} + } + } + if let Some(label) = v + .get("display_name") + .and_then(|d| d.as_str()) + .or_else(|| v.get("label").and_then(|l| l.as_str())) + { + push_unique(&mut out, label.to_string()); + } + } + out }) .unwrap_or_default() } @@ -376,7 +440,7 @@ mod tests { }); let result = extract_artifact_field_values(&artifact, "Status"); - assert_eq!(result, vec!["Nouveau"]); + assert_eq!(result, vec!["1", "Nouveau"]); } #[test] @@ -395,7 +459,7 @@ mod tests { }); let result = extract_artifact_field_values(&artifact, "Assigned to"); - assert_eq!(result, vec!["Team Maintenance"]); + assert_eq!(result, vec!["10", "Team Maintenance"]); } #[test]