top_drives/tests/http_tests.rs
Jan-Peter von Hunnius 0b1a710ffa
All checks were successful
build / build-test (push) Successful in 1m47s
Retry transient 400s with body logging; garage reserve default 27
2026-09-29 09:25:47 +02:00

176 lines
6.4 KiB
Rust

//! Tests for the HTTP file download utility with mockito mock server.
use std::fs;
use mockito::{Mock, Server, ServerGuard};
use top_drives::get_file;
/// Helper: create a mock that returns a successful response with the given body.
fn mock_success(server: &mut ServerGuard, body: &str) -> Mock {
server.mock("GET", "/data.json").with_status(200).with_body(body).create()
}
/// Helper: create a mock that returns the given HTTP status.
fn mock_status(server: &mut ServerGuard, status: usize) -> Mock {
server.mock("GET", "/data.json").with_status(status).create()
}
#[tokio::test]
async fn test_get_file_success() {
let mut server = Server::new_async().await;
let m = mock_success(&mut server, "hello world");
let dir = tempfile::tempdir().unwrap();
let out_path = dir.path().join("output.txt");
let url = format!("{}/data.json", server.url());
get_file(&url, out_path.to_str().unwrap()).await.unwrap();
assert_eq!(fs::read_to_string(&out_path).unwrap(), "hello world");
m.assert();
}
#[tokio::test]
async fn test_get_file_empty_response_fails_immediately_without_retry() {
let mut server = Server::new_async().await;
// 200 with empty body means "no data available yet" — permanent, so exactly
// one request must be made (no retries) and a distinguishable error returned.
let m = server.mock("GET", "/data.json").with_status(200).with_body("").expect(1).create();
let dir = tempfile::tempdir().unwrap();
let out_path = dir.path().join("output.txt");
let url = format!("{}/data.json", server.url());
let result = get_file(&url, out_path.to_str().unwrap()).await;
let err = result.expect_err("Empty response should fail immediately");
assert!(
matches!(err, top_drives::AppError::EmptyResponse { .. }),
"Empty response should yield AppError::EmptyResponse, got: {err}"
);
assert!(!out_path.exists(), "No file should be written for an empty response");
m.assert();
}
#[tokio::test]
async fn test_get_file_server_error_retries() {
let mut server = Server::new_async().await;
// Return 500 three times, then 200
let m_fail = server.mock("GET", "/data.json").with_status(500).expect(3).create();
let m_ok = mock_success(&mut server, "recovered");
let dir = tempfile::tempdir().unwrap();
let out_path = dir.path().join("output.txt");
let url = format!("{}/data.json", server.url());
get_file(&url, out_path.to_str().unwrap()).await.unwrap();
assert_eq!(fs::read_to_string(&out_path).unwrap(), "recovered");
m_fail.assert();
m_ok.assert();
}
#[tokio::test]
async fn test_get_file_bad_request_retries_then_recovers() {
let mut server = Server::new_async().await;
// The records API occasionally serves transient 400s — return 400 three
// times, then 200. Three requests must hit the 400 mock.
let m_fail = server
.mock("GET", "/data.json")
.with_status(400)
.with_body("upstream overloaded")
.expect(3)
.create();
let m_ok = mock_success(&mut server, "recovered");
let dir = tempfile::tempdir().unwrap();
let out_path = dir.path().join("output.txt");
let url = format!("{}/data.json", server.url());
get_file(&url, out_path.to_str().unwrap()).await.unwrap();
assert_eq!(fs::read_to_string(&out_path).unwrap(), "recovered");
m_fail.assert();
m_ok.assert();
}
#[tokio::test]
async fn test_get_file_persistent_bad_request_fails_after_retries() {
let mut server = Server::new_async().await;
// 400 every time — should retry 3 times then fail with the body in the message
let m = server
.mock("GET", "/data.json")
.with_status(400)
.with_body("upstream overloaded")
.expect(4) // 1 initial + 3 retries
.create();
let dir = tempfile::tempdir().unwrap();
let out_path = dir.path().join("output.txt");
let url = format!("{}/data.json", server.url());
let result = get_file(&url, out_path.to_str().unwrap()).await;
assert!(result.is_err(), "Persistent 400 should fail after retries");
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("400"), "Error should mention HTTP 400: {err_msg}");
assert!(
err_msg.contains("upstream overloaded"),
"Error should carry the response body: {err_msg}"
);
assert!(!out_path.exists(), "No file should be written on persistent failure");
m.assert();
}
#[tokio::test]
async fn test_get_file_client_error_fails_immediately() {
let mut server = Server::new_async().await;
// 404 should fail immediately without retries
let m = mock_status(&mut server, 404).expect(1).create();
let dir = tempfile::tempdir().unwrap();
let out_path = dir.path().join("output.txt");
let url = format!("{}/data.json", server.url());
let result = get_file(&url, out_path.to_str().unwrap()).await;
assert!(result.is_err(), "404 should fail immediately");
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("404"), "Error should mention HTTP 404: {err_msg}");
m.assert();
}
#[tokio::test]
async fn test_get_file_persistent_server_error_fails() {
let mut server = Server::new_async().await;
// 503 every time — should retry 3 times then fail
let m = server
.mock("GET", "/data.json")
.with_status(503)
.expect(4) // 1 initial + 3 retries
.create();
let dir = tempfile::tempdir().unwrap();
let out_path = dir.path().join("output.txt");
let url = format!("{}/data.json", server.url());
let result = get_file(&url, out_path.to_str().unwrap()).await;
assert!(result.is_err(), "Persistent 503 should fail after retries");
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("503"), "Error should mention HTTP 503: {err_msg}");
m.assert();
}
#[tokio::test]
async fn test_get_file_atomic_write_no_partial_on_error() {
let mut server = Server::new_async().await;
// Return 404 — immediate failure, no file written
let m = mock_status(&mut server, 404).create();
let dir = tempfile::tempdir().unwrap();
let out_path = dir.path().join("output.txt");
let url = format!("{}/data.json", server.url());
let result = get_file(&url, out_path.to_str().unwrap()).await;
assert!(result.is_err());
// The output file should NOT exist (atomic write via temp + rename)
assert!(!out_path.exists(), "No partial file should be left on failure");
m.assert();
}