//! 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(); }