From f30ad7dd9795f5433d26a6b2bad922c05ce3045a Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:14:24 -0500 Subject: [PATCH] tests: fix flaky tests --- Cargo.toml | 1 + tests/integration_common/mod.rs | 24 +++++++++ tests/integration_lists.rs | 86 ++++++++++++++++++++++++--------- tests/integration_ping.rs | 9 ++-- 4 files changed, 92 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index db9826f..9be6781 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ serde_json = "1" dotenvy = "0.15" chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1", features = ["serde", "v4"] } +serial_test = "3" diff --git a/tests/integration_common/mod.rs b/tests/integration_common/mod.rs index 7a40337..b034cec 100644 --- a/tests/integration_common/mod.rs +++ b/tests/integration_common/mod.rs @@ -2,7 +2,9 @@ use onspring::OnspringClient; use std::collections::HashMap; +use std::future::Future; use std::sync::Once; +use std::time::Duration; static INIT: Once = Once::new(); @@ -64,3 +66,25 @@ pub async fn add_record() -> i32 { let response = client.save_record(request).await.unwrap(); response.id } + +pub async fn retry(max_attempts: u32, delay: Duration, f: F) -> T +where + F: Fn() -> Fut, + Fut: Future>>, +{ + let mut last_err = None; + for attempt in 0..max_attempts { + if attempt > 0 { + tokio::time::sleep(delay).await; + } + match f().await { + Ok(val) => return val, + Err(e) => last_err = Some(e), + } + } + panic!( + "retry failed after {} attempts: {}", + max_attempts, + last_err.unwrap() + ); +} diff --git a/tests/integration_lists.rs b/tests/integration_lists.rs index 8d39ee8..1c9d816 100644 --- a/tests/integration_lists.rs +++ b/tests/integration_lists.rs @@ -1,10 +1,14 @@ mod integration_common; +use std::time::Duration; + use integration_common::*; use onspring::SaveListItemRequest; +use serial_test::serial; #[tokio::test] #[ignore] +#[serial] async fn save_list_item_should_add_item() { let client = build_client(); let list_id = required_env_i32("TEST_LIST_ID"); @@ -20,17 +24,25 @@ async fn save_list_item_should_add_item() { let response = client.save_list_item(list_id, request).await.unwrap(); let item_id = response.id; - // Cleanup - let _ = client.delete_list_item(list_id, item_id).await; + retry(5, Duration::from_secs(1), || { + let client = build_client(); + async move { + client + .delete_list_item(list_id, item_id) + .await + .map_err(|e| e.into()) + } + }) + .await; } #[tokio::test] #[ignore] +#[serial] async fn save_list_item_should_update_item() { let client = build_client(); let list_id = required_env_i32("TEST_LIST_ID"); - // Create let name = format!("added_list_value_{}", chrono::Utc::now().timestamp_millis()); let request = SaveListItemRequest { id: None, @@ -40,28 +52,42 @@ async fn save_list_item_should_update_item() { }; let created = client.save_list_item(list_id, request).await.unwrap(); - // Update - let name = format!( - "updated_list_value_{}", - chrono::Utc::now().timestamp_millis() - ); - let update_request = SaveListItemRequest { - id: Some(created.id), - name, - numeric_value: Some(1.0), - color: Some("#000000".to_string()), - }; - let _response = client - .save_list_item(list_id, update_request) - .await - .unwrap(); + let updated = retry(5, Duration::from_secs(1), || { + let client = build_client(); + let name = format!( + "updated_list_value_{}", + chrono::Utc::now().timestamp_millis() + ); + async move { + let update_request = SaveListItemRequest { + id: Some(created.id), + name, + numeric_value: Some(1.0), + color: Some("#000000".to_string()), + }; + client + .save_list_item(list_id, update_request) + .await + .map_err(|e| e.into()) + } + }) + .await; - // Cleanup - let _ = client.delete_list_item(list_id, created.id).await; + retry(5, Duration::from_secs(1), || { + let client = build_client(); + async move { + client + .delete_list_item(list_id, updated.id) + .await + .map_err(|e| e.into()) + } + }) + .await; } #[tokio::test] #[ignore] +#[serial] async fn save_list_item_should_fail_with_invalid_api_key() { let client = build_client_with_key("invalid"); let request = SaveListItemRequest { @@ -76,6 +102,7 @@ async fn save_list_item_should_fail_with_invalid_api_key() { #[tokio::test] #[ignore] +#[serial] async fn save_list_item_should_fail_when_list_not_found() { let client = build_client(); let request = SaveListItemRequest { @@ -90,6 +117,7 @@ async fn save_list_item_should_fail_when_list_not_found() { #[tokio::test] #[ignore] +#[serial] async fn save_list_item_should_fail_when_item_not_found() { let client = build_client(); let list_id = required_env_i32("TEST_LIST_ID"); @@ -107,11 +135,11 @@ async fn save_list_item_should_fail_when_item_not_found() { #[tokio::test] #[ignore] +#[serial] async fn delete_list_item_should_succeed() { let client = build_client(); let list_id = required_env_i32("TEST_LIST_ID"); - // Create first let name = format!("to_delete_{}", chrono::Utc::now().timestamp_millis()); let request = SaveListItemRequest { id: None, @@ -121,12 +149,21 @@ async fn delete_list_item_should_succeed() { }; let created = client.save_list_item(list_id, request).await.unwrap(); - // Delete - client.delete_list_item(list_id, created.id).await.unwrap(); + retry(5, Duration::from_secs(1), || { + let client = build_client(); + async move { + client + .delete_list_item(list_id, created.id) + .await + .map_err(|e| e.into()) + } + }) + .await; } #[tokio::test] #[ignore] +#[serial] async fn delete_list_item_should_fail_with_invalid_api_key() { let client = build_client_with_key("invalid"); let fake_uuid = uuid::Uuid::parse_str("3fa85f64-5717-4562-b3fc-2c963f66afa6").unwrap(); @@ -136,6 +173,7 @@ async fn delete_list_item_should_fail_with_invalid_api_key() { #[tokio::test] #[ignore] +#[serial] async fn delete_list_item_should_fail_when_no_access() { let client = build_client(); let list_id = required_env_i32("TEST_LIST_ID_NO_ACCESS"); @@ -148,6 +186,7 @@ async fn delete_list_item_should_fail_when_no_access() { #[tokio::test] #[ignore] +#[serial] async fn delete_list_item_should_fail_when_list_not_found() { let client = build_client(); let fake_uuid = uuid::Uuid::parse_str("3fa85f64-5717-4562-b3fc-2c963f66afa6").unwrap(); @@ -157,6 +196,7 @@ async fn delete_list_item_should_fail_when_list_not_found() { #[tokio::test] #[ignore] +#[serial] async fn delete_list_item_should_fail_when_item_not_found() { let client = build_client(); let list_id = required_env_i32("TEST_LIST_ID"); diff --git a/tests/integration_ping.rs b/tests/integration_ping.rs index 846bb69..e7f3ba8 100644 --- a/tests/integration_ping.rs +++ b/tests/integration_ping.rs @@ -4,15 +4,14 @@ use integration_common::{build_client, build_client_with_key}; #[tokio::test] #[ignore] -async fn ping_should_succeed() { +async fn ping_should_succeed_with_api_key() { let client = build_client(); client.ping().await.unwrap(); } #[tokio::test] #[ignore] -async fn ping_should_fail_with_invalid_api_key() { - let client = build_client_with_key("invalid"); - let result = client.ping().await; - assert!(result.is_err()); +async fn ping_should_succeed_without_api_key() { + let client = build_client_with_key(""); + client.ping().await.unwrap(); }