Skip to content

Commit b820b25

Browse files
authored
test: harden integration tests against transient API blips (#47)
1 parent 51e0aae commit b820b25

5 files changed

Lines changed: 63 additions & 2 deletions

File tree

.github/workflows/integration-tests.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,27 @@ jobs:
9393
# --no-fail-fast runs every scenario binary even after one fails, so a
9494
# red run surfaces all failing scenarios at once rather than stopping at
9595
# the first.
96-
run: cargo test --all-features --test '*' --no-fail-fast -- --nocapture
96+
#
97+
# These tests hit the live production API, so a transient connectivity
98+
# blip (the suite has seen TCP connect timeouts to the API host) would
99+
# otherwise fail the whole run and need a manual re-run. Retry the suite
100+
# a few times with backoff so a brief outage self-heals; the bounded
101+
# connect timeout in tests/common/mod.rs keeps a down-API attempt to
102+
# seconds rather than minutes. A genuine failure still goes red after
103+
# exhausting the attempts.
104+
run: |
105+
for attempt in 1 2 3; do
106+
echo "::group::Integration test attempt ${attempt}/3"
107+
if cargo test --all-features --test '*' --no-fail-fast -- --nocapture; then
108+
echo "::endgroup::"
109+
exit 0
110+
fi
111+
echo "::endgroup::"
112+
if [ "${attempt}" -lt 3 ]; then
113+
backoff=$((attempt * 30))
114+
echo "Attempt ${attempt} failed; retrying in ${backoff}s..."
115+
sleep "${backoff}"
116+
fi
117+
done
118+
echo "::error::Integration tests failed after 3 attempts"
119+
exit 1

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ tokio = { version = "^1.46.0", features = ["rt-multi-thread", "macros", "time"]
4040
futures = "^0.3"
4141
wiremock = "0.6"
4242
uuid = { version = "1", features = ["v4"] }
43+
# Lets the integration harness build a reqwest client with a bounded connect
44+
# timeout (see tests/common/mod.rs). default-features = false mirrors the lib;
45+
# the TLS backend comes from the crate's own `native-tls`/`rustls` features via
46+
# Cargo feature unification.
47+
reqwest = { version = "^0.13", default-features = false }
4348

4449
[package.metadata.docs.rs]
4550
all-features = true

tests/auth_missing_token_401.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ async fn auth_missing_token_401() {
2020
let _client = skip_if_no_creds!();
2121
let env = common::load_env();
2222

23-
// No bearer token, no workspace header — just the API host.
23+
// No bearer token, no workspace header — just the API host. Share the
24+
// harness client so a down API host fails fast instead of stalling on the
25+
// OS-level connect timeout (see common::test_http_client).
2426
let mut config = Configuration::new();
2527
config.base_path = env.api_url.trim_end_matches('/').to_string();
28+
config.client = common::test_http_client();
2629

2730
let result = workspaces_api::list_workspaces(&config, None).await;
2831
match result {

tests/auth_unknown_workspace.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async fn auth_unknown_workspace() {
2727
.api_token(api_key)
2828
.workspace_id(fake_workspace.clone())
2929
.base_url(env.api_url)
30+
.reqwest_client(common::test_http_client())
3031
.build()
3132
.expect("Client::build should succeed");
3233

tests/common/mod.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,36 @@
1111
1212
#![allow(dead_code)]
1313

14+
use std::time::Duration;
15+
1416
use hotdata::Client;
1517

18+
/// Connect-phase ceiling for the shared test client.
19+
///
20+
/// `reqwest::Client::default()` (what the SDK uses when no client is supplied)
21+
/// has no connect timeout, so an unreachable API host blocks each call on the
22+
/// OS-level TCP timeout (~60s observed in CI). With ~20 scenario binaries run
23+
/// sequentially by `cargo test`, a transient connectivity blip turns into a
24+
/// ~20-minute red run. Bounding the connect phase fails fast — and lets hyper
25+
/// fall through to the next resolved address — so an outage is cheap to retry.
26+
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
27+
28+
/// Overall per-request ceiling. Generous enough for the tiny fixture upload and
29+
/// each poll request; purely a backstop against a hung socket.
30+
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
31+
32+
/// Build the reqwest client every scenario shares: identical to the SDK default
33+
/// except for the bounded [`CONNECT_TIMEOUT`]/[`REQUEST_TIMEOUT`]. Pass it via
34+
/// `ClientBuilder::reqwest_client` (or assign to `Configuration::client`) so a
35+
/// down API host can't stall the suite.
36+
pub fn test_http_client() -> reqwest::Client {
37+
reqwest::Client::builder()
38+
.connect_timeout(CONNECT_TIMEOUT)
39+
.timeout(REQUEST_TIMEOUT)
40+
.build()
41+
.expect("building the test reqwest client should not fail")
42+
}
43+
1644
/// Default API host. The auth-token -> JWT exchange and every endpoint live on
1745
/// the API host, so the ergonomic `Client` always points here unless overridden
1846
/// by `HOTDATA_SDK_TEST_API_URL`.
@@ -67,6 +95,7 @@ pub fn client_or_skip() -> Option<Client> {
6795
.api_token(env.api_key.expect("checked above"))
6896
.workspace_id(env.workspace_id.expect("checked above"))
6997
.base_url(env.api_url)
98+
.reqwest_client(test_http_client())
7099
.build()
71100
.expect("Client::build with valid credentials should not fail");
72101
Some(client)

0 commit comments

Comments
 (0)