use crate::cli::{
actions::{
FallbackContext, FallbackServiceType, FallbackState, execute_fallback_command,
execute_fallback_http, get_fallback_state, metrics::ServiceMetrics, reset_fallback_state,
should_continue_fallback,
},
config::{BodyType, ServiceDetails},
telemetry,
};
use anyhow::{Result, anyhow};
use futures_util::StreamExt;
use regex::Regex;
use reqwest::{
Client, Method, RequestBuilder,
header::{HeaderMap, HeaderValue},
};
use serde_json::Value;
use std::{collections::HashMap, fmt::Write as _, sync::Arc};
use tokio::sync::Mutex;
use tracing::{debug, error, info, warn};
use std::hash::BuildHasher;
fn format_expected_status(status: Option<u16>) -> String {
status.map_or_else(|| "any".to_string(), |status| status.to_string())
}
fn format_headers(headers: &HeaderMap<HeaderValue>) -> String {
if headers.is_empty() {
return "(none)".to_string();
}
let mut output = String::new();
for (name, value) in headers {
let value = value.to_str().unwrap_or("<non-utf8>");
let _ = write!(output, "\n {}: {}", name.as_str(), value);
}
output
}
fn format_headers_block(headers: &HeaderMap<HeaderValue>) -> String {
if headers.is_empty() {
"\n (none)".to_string()
} else {
format_headers(headers)
}
}
fn format_http_response_success_log(
service_name: &str,
service_url: Option<&String>,
service_status: u16,
expected_status: Option<u16>,
matches: bool,
) -> String {
let service_url = service_url.map_or("(none)", String::as_str);
let expected_status = format_expected_status(expected_status);
format!(
"service_name: \"{service_name}\", service_url: \"{service_url}\", service_status: {service_status}, expected_status: {expected_status}, matches: {matches}"
)
}
fn format_http_response_failure_log(
service_name: &str,
service_url: Option<&String>,
service_status: u16,
expected_status: Option<u16>,
headers: &HeaderMap<HeaderValue>,
matches: bool,
) -> String {
let service_url = service_url.map_or("(none)", String::as_str);
let expected_status = format_expected_status(expected_status);
format!(
"service_name: \"{service_name}\", service_url: \"{service_url}\", service_status: {service_status}, expected_status: {expected_status}\nresponse_headers:{}\nmatches: {matches}",
format_headers_block(headers)
)
}
pub fn build_http_request(
client: &Client,
service_details: &ServiceDetails,
) -> Result<RequestBuilder> {
let url = service_details
.url
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No URL provided"))?;
let method = Method::from_bytes(service_details.method.to_string().as_bytes())?;
let mut request = client.request(method, url);
if let Some(body) = &service_details.body {
debug!("Building HTTP request with body: {:?}", body);
match body {
BodyType::Json(json) => {
request = request.json(json);
}
BodyType::Form(form_data) => {
request = request.form(form_data);
}
BodyType::Text(text) => {
request = request.body(text.clone()); }
}
}
Ok(request)
}
#[allow(clippy::too_many_lines)]
pub async fn handle_http_response<S: BuildHasher>(
service_name: &str,
service_details: &ServiceDetails,
response: reqwest::Response,
metrics: &ServiceMetrics,
counters: Arc<Mutex<HashMap<String, FallbackState, S>>>,
) -> Result<bool> {
let status = response.status();
let headers = response.headers().clone();
let actual_status = i32::from(status.as_u16());
let status_matches = service_details.expect.status_matches(status.as_u16());
let body_mismatch_reason =
match_response_expectations(response, service_details, service_details.max_bytes).await?;
let body_matches = body_mismatch_reason.is_none();
let is_match = status_matches && body_matches;
if is_match {
reset_fallback_state(service_name, &counters).await;
}
metrics
.epazote_status
.with_label_values(&[service_name])
.set(i64::from(is_match));
if telemetry::pretty_logs_enabled() {
let formatted = if is_match {
format_http_response_success_log(
service_name,
service_details.url.as_ref(),
status.as_u16(),
service_details.expect.status,
is_match,
)
} else {
format_http_response_failure_log(
service_name,
service_details.url.as_ref(),
status.as_u16(),
service_details.expect.status,
&headers,
is_match,
)
};
if is_match {
info!("{formatted}");
} else {
warn!("{formatted}");
}
} else if is_match {
info!(
service_name = service_name,
service_url = service_details.url,
service_status = status.as_u16(),
expected_status = %format_expected_status(service_details.expect.status),
response_headers = %format_headers(&headers),
matches = is_match
);
} else {
warn!(
service_name = service_name,
service_url = service_details.url,
service_status = status.as_u16(),
expected_status = %format_expected_status(service_details.expect.status),
response_headers = %format_headers(&headers),
matches = is_match
);
}
if !is_match
&& let Some(action) = &service_details.expect.if_not
&& should_continue_fallback(service_name, &counters, action).await
{
let state = get_fallback_state(service_name, &counters)
.await
.unwrap_or_default();
let context = FallbackContext {
service_name,
service_type: FallbackServiceType::Http,
expected_status: service_details.expect.expected_status_i32(),
actual_status: Some(actual_status),
error: if status_matches {
body_mismatch_reason.unwrap_or("request_error")
} else {
"status_mismatch"
},
failure_count: state.consecutive_failures,
threshold: action.threshold.unwrap_or(1),
url: service_details.url.as_deref(),
test: None,
};
if let Some(cmd) = &action.cmd {
let exit_code = execute_fallback_command(cmd, &context).await?;
info!(
"Executed fallback command for {} with exit code {}",
service_name, exit_code
);
}
if let Some(http) = &action.http {
let status = execute_fallback_http(http).await?;
info!(
"Executed fallback HTTP request for {} with status code {}",
service_name, status
);
}
}
Ok(is_match)
}
async fn match_response_expectations(
response: reqwest::Response,
service_details: &ServiceDetails,
max_bytes: Option<usize>,
) -> Result<Option<&'static str>> {
let expect = &service_details.expect;
if expect.body.is_none() && expect.json.is_none() && expect.body_not.is_none() {
return Ok(None);
}
let body = collect_response_bytes(response, max_bytes).await?;
if let Some(expected_body) = &expect.body
&& !match_response_body(&body, expected_body)?
{
return Ok(Some("body_mismatch"));
}
if let Some(expected_json) = &expect.json
&& !match_response_json(&body, expected_json)
{
return Ok(Some("json_mismatch"));
}
if let Some(forbidden_body) = &expect.body_not
&& match_response_body(&body, forbidden_body)?
{
return Ok(Some("body_not_match"));
}
Ok(None)
}
fn match_response_body(body: &[u8], expected_body: &str) -> Result<bool> {
let regex = generate_regex_pattern(expected_body).map_err(|e| {
error!(
"Invalid regex pattern in Expect body: {}, Error: {}",
expected_body, e
);
e
})?;
let text = String::from_utf8_lossy(body);
if regex.is_match(&text) {
debug!("Match found in response body");
return Ok(true);
}
Ok(false)
}
fn match_response_json(body: &[u8], expected_json: &Value) -> bool {
match serde_json::from_slice::<Value>(body) {
Ok(actual_json) => json_contains(expected_json, &actual_json),
Err(e) => {
error!("Failed to parse response body as JSON: {}", e);
false
}
}
}
async fn collect_response_bytes(
response: reqwest::Response,
max_bytes: Option<usize>,
) -> Result<Vec<u8>> {
let mut stream = response.bytes_stream();
let max_bytes = max_bytes.unwrap_or(usize::MAX);
let mut total_bytes_read = 0;
let mut buffer = Vec::new();
while let Some(chunk) = stream.next().await {
match chunk {
Ok(bytes) => {
let remaining_bytes = max_bytes.saturating_sub(total_bytes_read);
if remaining_bytes == 0 {
break;
}
let limited_chunk = if bytes.len() > remaining_bytes {
bytes.get(..remaining_bytes).unwrap_or(&bytes)
} else {
&bytes
};
buffer.extend_from_slice(limited_chunk);
total_bytes_read += limited_chunk.len();
if total_bytes_read >= max_bytes {
break;
}
}
Err(e) => {
error!("Failed to read chunk: {}", e);
return Ok(Vec::new());
}
}
}
Ok(buffer)
}
fn json_contains(expected: &Value, actual: &Value) -> bool {
match (expected, actual) {
(Value::Object(expected_map), Value::Object(actual_map)) => {
expected_map.iter().all(|(key, expected_value)| {
actual_map
.get(key)
.is_some_and(|actual_value| json_contains(expected_value, actual_value))
})
}
(Value::Array(expected_items), Value::Array(actual_items)) => {
expected_items.iter().all(|expected_item| {
actual_items
.iter()
.any(|actual_item| json_contains(expected_item, actual_item))
})
}
_ => expected == actual,
}
}
fn generate_regex_pattern(input: &str) -> Result<Regex> {
let trimmed_input = input.trim();
if trimmed_input.is_empty() {
return Err(anyhow!("Input regex pattern cannot be empty"));
}
let pattern = trimmed_input.strip_prefix("r\"").map_or_else(
|| regex::escape(trimmed_input),
|raw| raw.strip_suffix('"').unwrap_or(raw).to_string(),
);
debug!(
"Generated regex for: {}, pattern: {}",
trimmed_input, pattern
);
Regex::new(&pattern).map_err(|e| {
debug!("Regex compilation failed: {}", e);
e.into()
})
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
use crate::cli::{
actions::{FallbackState, client::build_client},
config::{Config, Expect, HttpMethod, ServiceDetails},
};
use mockito::Server;
use reqwest::StatusCode;
use serde_json::json;
use std::{fs, io::Write, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc};
use tokio::time::Duration;
fn create_config(yaml: &str) -> Config {
let mut tmp_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
tmp_file
.write_all(yaml.as_bytes())
.expect("Failed to write to temp file");
tmp_file.flush().expect("Failed to flush temp file");
Config::new(tmp_file.path().to_path_buf()).expect("Failed to load config")
}
fn generate_numbers(limit: usize, start: usize) -> String {
use std::fmt::Write;
let mut result = String::new();
let mut num = start;
while result.len() + 2 < limit {
let _ = write!(result, "{num} ");
num += 1;
}
result
}
fn create_env_capture_script(env_vars: &[&str]) -> (tempfile::TempDir, String, PathBuf) {
let tempdir = tempfile::Builder::new()
.prefix("epazote-http-env-")
.tempdir_in(".")
.expect("Failed to create temp dir");
let script_path = tempdir.path().join("capture.sh");
let output_path = tempdir.path().join("output.txt");
let body = env_vars
.iter()
.map(|key| format!("printenv {key}"))
.collect::<Vec<_>>()
.join("\n");
fs::write(
&script_path,
format!("#!/bin/sh\n{{\n{body}\n}} > {}\n", output_path.display()),
)
.expect("Failed to write capture script");
let mut permissions = fs::metadata(&script_path)
.expect("Failed to stat script")
.permissions();
permissions.set_mode(0o755);
fs::set_permissions(&script_path, permissions).expect("Failed to chmod script");
(
tempdir,
script_path
.to_str()
.expect("Invalid script path")
.to_string(),
output_path,
)
}
#[test]
fn test_generate_regex_pattern() {
let pattern = generate_regex_pattern("test").expect("Failed to generate regex pattern");
assert_eq!(pattern.as_str(), "test");
let pattern =
generate_regex_pattern(r#"r"test""#).expect("Failed to generate regex pattern");
assert_eq!(pattern.as_str(), "test");
let pattern =
generate_regex_pattern(r#"r"test"#).expect("Failed to generate regex pattern");
assert_eq!(pattern.as_str(), "test");
let pattern = generate_regex_pattern(r#"r"(?i).*hello.*""#)
.expect("Failed to generate regex pattern");
assert_eq!(pattern.as_str(), "(?i).*hello.*");
let pattern =
generate_regex_pattern("hello world").expect("Failed to generate regex pattern");
assert_eq!(pattern.as_str(), "hello world");
assert!(pattern.is_match("this is a hello world test"));
assert!(!pattern.is_match("this is a goodbye test"));
let pattern =
generate_regex_pattern("hello.world").expect("Failed to generate regex pattern");
assert_eq!(pattern.as_str(), "hello\\.world");
let pattern = generate_regex_pattern("a+b*").expect("Failed to generate regex pattern");
assert_eq!(pattern.as_str(), "a\\+b\\*");
}
#[test]
fn test_regex_matching_behavior() {
let pattern = generate_regex_pattern("findme").expect("Failed to generate regex");
assert!(pattern.is_match("here is findme in the middle"));
assert!(pattern.is_match("findme at the start"));
assert!(pattern.is_match("at the end findme"));
assert!(!pattern.is_match("nowhere to be found"));
let pattern = generate_regex_pattern(r#"r"^start""#).expect("Failed to generate regex");
assert!(pattern.is_match("start of string"));
assert!(!pattern.is_match("not the start of string"));
let pattern = generate_regex_pattern(r#"r"(?i)apple""#).expect("Failed to generate regex");
assert!(pattern.is_match("I like Apple"));
assert!(pattern.is_match("apple pie"));
assert!(!pattern.is_match("orange juice"));
let pattern = generate_regex_pattern(r#"r"\bcat\b""#).expect("Failed to generate regex");
assert!(pattern.is_match("the cat is here"));
assert!(!pattern.is_match("category is feline"));
}
#[test]
fn test_format_headers() {
let mut headers = HeaderMap::new();
headers.insert("content-type", HeaderValue::from_static("text/html"));
headers.insert(
"location",
HeaderValue::from_static("https://www.google.com/"),
);
let formatted = format_headers(&headers);
assert!(formatted.contains("\n content-type: text/html"));
assert!(formatted.contains("\n location: https://www.google.com/"));
}
#[test]
fn test_format_http_response_log() {
let mut headers = HeaderMap::new();
headers.insert("content-type", HeaderValue::from_static("text/html"));
headers.insert(
"location",
HeaderValue::from_static("https://www.google.com/"),
);
let formatted = format_http_response_failure_log(
"google",
Some(&"https://google.com".to_string()),
301,
Some(301),
&headers,
true,
);
assert!(formatted.contains(
"service_name: \"google\", service_url: \"https://google.com\", service_status: 301, expected_status: 301\nresponse_headers:"
));
assert!(formatted.contains("\n content-type: text/html"));
assert!(formatted.contains("\n location: https://www.google.com/"));
assert!(formatted.ends_with("\nmatches: true"));
}
#[test]
fn test_format_http_response_success_log() {
let formatted = format_http_response_success_log(
"google",
Some(&"https://google.com".to_string()),
301,
Some(301),
true,
);
assert_eq!(
formatted,
"service_name: \"google\", service_url: \"https://google.com\", service_status: 301, expected_status: 301, matches: true"
);
}
#[test]
fn test_json_contains_nested_objects_and_arrays() {
let expected = json!({
"status": "success",
"data": {
"activeTargets": [
{
"labels": {
"job": "DBMI-lab-nico"
},
"health": "up"
}
]
}
});
let actual = json!({
"status": "success",
"data": {
"activeTargets": [
{
"labels": {
"instance": "127.0.0.1:8429",
"job": "DBMI-lab-nico"
},
"health": "up",
"lastSamplesScraped": 932
},
{
"labels": {
"instance": "127.0.0.1:9080",
"job": "other"
},
"health": "down"
}
],
"droppedTargets": []
}
});
assert!(json_contains(&expected, &actual));
}
#[test]
fn test_json_contains_returns_false_for_missing_nested_match() {
let expected = json!({
"data": {
"activeTargets": [
{
"labels": {
"job": "DBMI-lab-nico"
},
"health": "down"
}
]
}
});
let actual = json!({
"data": {
"activeTargets": [
{
"labels": {
"job": "DBMI-lab-nico"
},
"health": "up"
}
]
}
});
assert!(!json_contains(&expected, &actual));
}
#[tokio::test]
async fn test_handle_http_response() {
let mut server = Server::new_async().await;
let _m = server
.mock("GET", "/health")
.with_status(200)
.create_async()
.await;
let service = ServiceDetails {
every: Duration::from_secs(1),
expect: Expect {
status: Some(200),
header: None,
body: None,
body_not: None,
json: None,
if_not: None,
},
follow_redirects: Some(true),
headers: None,
max_bytes: None,
test: None,
timeout: Duration::from_secs(5),
url: Some(format!("{}/health", server.url())),
method: HttpMethod::Get,
body: None,
};
let metrics = Arc::new(ServiceMetrics::new().expect("Failed to create metrics"));
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let (builder, _client_config) =
build_client(&service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, &service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let rs = handle_http_response("test", &service, response, &metrics, counters).await;
assert!(rs.is_ok());
}
#[tokio::test]
async fn test_build_http_request_json() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r"
---
services:
test:
url: {mock_url}/test
method: POST
body:
json:
key: value
oi: hola
every: 30s
headers:
X-Custom-Header: TestValue
expect:
status: 200
"
);
let config = create_config(&yaml);
let service = config.services.get("test").expect("Service not found");
let expected_json = json!({
"key": "value",
"oi": "hola"
});
let _ = env_logger::try_init();
let _mock = server
.mock("POST", "/test")
.match_header("X-Custom-Header", "TestValue")
.match_header("Content-Type", "application/json")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.match_body(mockito::Matcher::Json(expected_json.clone()))
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, service).expect("Failed to build request");
if let Some(body) = &config.services.get("test").expect("Service not found").body {
let json_body = serde_json::to_string(body).expect("Failed to serialize body");
assert_eq!(json_body, expected_json.to_string());
}
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_build_http_request_form() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r"
---
services:
test:
url: {mock_url}/test
method: POST
body:
form:
key: value
oi: hola
every: 30s
headers:
X-Custom-Header: TestValue
expect:
status: 200
"
);
let config = create_config(&yaml);
let service = config.services.get("test").expect("Service not found");
let expected_form = [
("key".to_string(), "value".to_string()),
("oi".to_string(), "hola".to_string()),
];
let _ = env_logger::try_init();
let _mock = server
.mock("POST", "/test")
.match_header("X-Custom-Header", "TestValue")
.match_header("Content-Type", "application/x-www-form-urlencoded")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.match_body(mockito::Matcher::UrlEncoded(
"key".to_string(),
"value".to_string(),
))
.match_body(mockito::Matcher::UrlEncoded(
"oi".to_string(),
"hola".to_string(),
))
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, service).expect("Failed to build request");
if let Some(BodyType::Form(body)) =
&config.services.get("test").expect("Service not found").body
{
for (key, value) in &expected_form {
assert_eq!(body.get(key), Some(value));
}
} else {
panic!("Expected BodyType::Form but found something else");
}
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_build_http_request_text() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r#"
---
services:
test:
url: {mock_url}/test
method: POST
body: "Hello, world!"
every: 30s
headers:
content-type: text/plain
X-Custom-Header: TestValue
expect:
status: 200
"#
);
let config = create_config(&yaml);
let service = config.services.get("test").expect("Service not found");
let expected_text = String::from("Hello, world!");
let _ = env_logger::try_init();
let _mock = server
.mock("POST", "/test")
.match_header("X-Custom-Header", "TestValue")
.match_header("Content-Type", "text/plain")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.match_body(mockito::Matcher::Exact(expected_text.clone()))
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, service).expect("Failed to build request");
if let Some(BodyType::Text(body)) =
&config.services.get("test").expect("Service not found").body
{
assert_eq!(body, &expected_text);
} else {
panic!("Expected BodyType::Text but found something else");
}
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_handle_http_response_expect_body() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r"
---
services:
test:
url: {mock_url}/test
every: 30s
expect:
status: 200
body: sopas
"
);
let config = create_config(&yaml);
let service = config.services.get("test").expect("Service not found");
let _ = env_logger::try_init();
let _mock = server
.mock("GET", "/test")
.with_body("world-sopas-hello")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs = handle_http_response(
"test",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
counters,
)
.await
.expect("Failed to handle response");
assert!(rs);
}
#[tokio::test]
async fn test_handle_http_response_expect_body_not() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r#"
---
services:
test:
url: {mock_url}/test
every: 30s
expect:
body_not: r"error|failure|Fatal"
"#
);
let config = create_config(&yaml);
let service = config.services.get("test").expect("Service not found");
let success_mock = server
.mock("GET", "/test")
.with_body("all components healthy")
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let rs = handle_http_response(
"test",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(rs);
success_mock.remove();
let _failure_mock = server
.mock("GET", "/test")
.with_body("Fatal writing output to destination")
.with_status(500)
.create_async()
.await;
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let rs = handle_http_response(
"test",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
counters,
)
.await
.expect("Failed to handle response");
assert!(!rs);
}
#[tokio::test]
async fn test_handle_http_response_expect_json() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r"
---
services:
test:
url: {mock_url}/test
every: 30s
expect:
status: 200
json:
status: success
data:
activeTargets:
- labels:
job: DBMI-lab-nico
health: up
"
);
let config = create_config(&yaml);
let service = config.services.get("test").expect("Service not found");
let _ = env_logger::try_init();
let _mock = server
.mock("GET", "/test")
.with_body(
r#"{"status":"success","data":{"activeTargets":[{"labels":{"instance":"127.0.0.1:8429","job":"DBMI-lab-nico"},"health":"up","lastSamplesScraped":932},{"labels":{"instance":"127.0.0.1:9080","job":"other"},"health":"down"}],"droppedTargets":[]}}"#,
)
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs = handle_http_response(
"test",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
counters,
)
.await
.expect("Failed to handle response");
assert!(rs);
}
#[tokio::test]
async fn test_handle_http_response_expect_json_invalid_body() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r"
---
services:
test:
url: {mock_url}/test
every: 30s
expect:
status: 200
json:
status: success
"
);
let config = create_config(&yaml);
let service = config.services.get("test").expect("Service not found");
let _ = env_logger::try_init();
let _mock = server
.mock("GET", "/test")
.with_body("not-json")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs = handle_http_response(
"test",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
counters,
)
.await
.expect("Failed to handle response");
assert!(!rs);
}
#[tokio::test]
async fn test_handle_http_response_if_not_cmd_sets_http_env_vars() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let (_tempdir, script_path, output_path) = create_env_capture_script(&[
"EPAZOTE_SERVICE_NAME",
"EPAZOTE_SERVICE_TYPE",
"EPAZOTE_EXPECTED_STATUS",
"EPAZOTE_ACTUAL_STATUS",
"EPAZOTE_ERROR",
"EPAZOTE_FAILURE_COUNT",
"EPAZOTE_THRESHOLD",
"EPAZOTE_URL",
]);
let yaml = format!(
r"
---
services:
test-env:
url: {mock_url}/test
every: 30s
expect:
status: 200
if_not:
threshold: 2
cmd: {script_path}
"
);
let config = create_config(&yaml);
let service = config.services.get("test-env").expect("Service not found");
let _mock = server
.mock("GET", "/test")
.with_status(503)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
for _ in 0..2 {
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let rs = handle_http_response(
"test-env",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!rs);
}
let output = fs::read_to_string(output_path).expect("Failed to read env capture");
assert_eq!(
output.lines().collect::<Vec<_>>(),
vec![
"test-env",
"http",
"200",
"503",
"status_mismatch",
"2",
"2",
&format!("{mock_url}/test"),
]
);
}
#[tokio::test]
async fn test_handle_http_response_body_not_sets_error() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let (_tempdir, script_path, output_path) = create_env_capture_script(&["EPAZOTE_ERROR"]);
let yaml = format!(
r"
---
services:
test-body-not:
url: {mock_url}/test
every: 30s
expect:
body_not: Failure
if_not:
cmd: {script_path}
"
);
let config = create_config(&yaml);
let service = config
.services
.get("test-body-not")
.expect("Service not found");
let _mock = server
.mock("GET", "/test")
.with_body("Failure writing output to destination")
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs = handle_http_response(
"test-body-not",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
counters,
)
.await
.expect("Failed to handle response");
assert!(!rs);
let output = fs::read_to_string(output_path).expect("Failed to read env capture");
assert_eq!(output.trim(), "body_not_match");
}
#[allow(clippy::too_many_lines)]
#[tokio::test]
async fn test_handle_http_response_if_not_cmd_resets_failure_count_after_success() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let (_tempdir, script_path, output_path) =
create_env_capture_script(&["EPAZOTE_FAILURE_COUNT", "EPAZOTE_ERROR"]);
let yaml = format!(
r"
---
services:
test-reset:
url: {mock_url}/test
every: 30s
expect:
status: 200
if_not:
threshold: 2
cmd: {script_path}
"
);
let config = create_config(&yaml);
let service = config
.services
.get("test-reset")
.expect("Service not found");
let failing_mock = server
.mock("GET", "/test")
.with_status(503)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
assert!(
!handle_http_response(
"test-reset",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response")
);
failing_mock.remove();
let _success_mock = server
.mock("GET", "/test")
.with_status(200)
.create_async()
.await;
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
assert!(
handle_http_response(
"test-reset",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response")
);
let failing_mock = server
.mock("GET", "/test")
.with_status(503)
.create_async()
.await;
for _ in 0..2 {
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
assert!(
!handle_http_response(
"test-reset",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response")
);
}
let output = fs::read_to_string(output_path).expect("Failed to read env capture");
assert_eq!(
output.lines().collect::<Vec<_>>(),
vec!["2", "status_mismatch"]
);
failing_mock.remove();
}
#[tokio::test]
async fn test_handle_http_response_threshold_delays_fallback() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r"
---
services:
test-threshold:
url: {mock_url}/test
every: 30s
expect:
status: 200
body: ok
if_not:
threshold: 3
cmd: echo threshold
"
);
let config = create_config(&yaml);
let service = config
.services
.get("test-threshold")
.expect("Service not found");
let _ = env_logger::try_init();
let _mock = server
.mock("GET", "/test")
.with_body("nope")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
for expected_executions in [0, 0, 1] {
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let rs = handle_http_response(
"test-threshold",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!rs);
let counters_locked = counters.lock().await;
let state = counters_locked
.get("test-threshold")
.expect("State not found");
assert_eq!(state.fallback_executions, expected_executions);
drop(counters_locked);
}
}
#[allow(clippy::too_many_lines)]
#[tokio::test]
async fn test_handle_http_response_success_resets_threshold_counter() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r"
---
services:
test-threshold:
url: {mock_url}/test
every: 30s
expect:
status: 200
body: ok
if_not:
threshold: 2
cmd: echo threshold
"
);
let config = create_config(&yaml);
let service = config
.services
.get("test-threshold")
.expect("Service not found");
let _ = env_logger::try_init();
let failing_mock = server
.mock("GET", "/test")
.with_body("nope")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let first_failure = handle_http_response(
"test-threshold",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!first_failure);
failing_mock.remove();
let _success_mock = server
.mock("GET", "/test")
.with_body("ok")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let success = handle_http_response(
"test-threshold",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(success);
let failing_mock = server
.mock("GET", "/test")
.with_body("still-nope")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let second_failure = handle_http_response(
"test-threshold",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!second_failure);
let counters_locked = counters.lock().await;
let state = counters_locked
.get("test-threshold")
.expect("State not found");
assert_eq!(state.consecutive_failures, 1);
assert_eq!(state.fallback_executions, 0);
failing_mock.remove();
}
#[tokio::test]
async fn test_handle_http_response_expect_body_regex_stop() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r#"
---
services:
test-stop:
url: {mock_url}/test
every: 30s
expect:
status: 200
body: r"\b(?:sopas|cit-02)\b" # match sopas or cit-02
if_not:
stop: 2
"#
);
let config = create_config(&yaml);
let service = config.services.get("test-stop").expect("Service not found");
let _ = env_logger::try_init();
let _mock = server
.mock("GET", "/test")
.with_body("---")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs1 = handle_http_response(
"test-stop",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!rs1);
let count1 = {
let counters_locked = counters.lock().await;
counters_locked
.get("test-stop")
.map_or(0, |state| state.fallback_executions)
};
assert_eq!(count1, 1, "Counter should be 1 after first attempt");
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let rs2 = handle_http_response(
"test-stop",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!rs2);
let count2 = {
let counters_locked = counters.lock().await;
counters_locked
.get("test-stop")
.map_or(0, |state| state.fallback_executions)
};
assert_eq!(count2, 2, "Counter should be 1 after first attempt");
}
#[tokio::test]
async fn test_handle_http_response_expect_if_not_http() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r"
---
services:
test-stop:
url: {mock_url}/test
every: 30s
expect:
status: 200
body: http
if_not:
stop: 2
http: {mock_url}/notify?milei=libra
"
);
let config = create_config(&yaml);
let service = config.services.get("test-stop").expect("Service not found");
let _ = env_logger::try_init();
let _mock = server
.mock("GET", "/test")
.with_body("---milei---")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs1 = handle_http_response(
"test-stop",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!rs1);
let count1 = {
let counters_locked = counters.lock().await;
counters_locked
.get("test-stop")
.map_or(0, |state| state.fallback_executions)
};
assert_eq!(count1, 1, "Counter should be 1 after first attempt");
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let rs2 = handle_http_response(
"test-stop",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!rs2);
let count2 = {
let counters_locked = counters.lock().await;
counters_locked
.get("test-stop")
.map_or(0, |state| state.fallback_executions)
};
assert_eq!(count2, 2, "Counter should be 1 after first attempt");
}
#[tokio::test]
async fn test_handle_http_response_expect_body_regex_example() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r#"
---
services:
test-stop:
url: {mock_url}/test
every: 30s
expect:
status: 200
body: r"success|ok"
"#
);
let config = create_config(&yaml);
let service = config.services.get("test-stop").expect("Service not found");
let _ = env_logger::try_init();
let mock = server
.mock("GET", "/test")
.with_body("success")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs1 = handle_http_response(
"test-stop",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(rs1);
mock.remove();
let _mock = server
.mock("GET", "/test")
.with_body("-- error --")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let rs2 = handle_http_response(
"test-stop",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!rs2);
mock.remove();
let _mock = server
.mock("GET", "/test")
.with_body("-- ok --")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let rs3 = handle_http_response(
"test-stop",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(rs3);
}
#[tokio::test]
async fn test_handle_http_response_expect_body_max_bytes_20() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r#"
---
services:
test-max_bytes:
url: {mock_url}/test
every: 30s
expect:
status: 200
body: "34917f37-72b9-403f-887c-20c5e93b7173"
max_bytes: 20
"#
);
let config = create_config(&yaml);
let service = config
.services
.get("test-max_bytes")
.expect("Service not found");
let _ = env_logger::try_init();
let _mock = server
.mock("GET", "/test")
.with_body("hello world 0123456789 34917f37-72b9-403f-887c-20c5e93b7173")
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs = handle_http_response(
"test-max_bytes",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!rs);
}
#[tokio::test]
async fn test_handle_http_response_expect_body_max_bytes_64k() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r#"
---
services:
test-max_bytes:
url: {mock_url}/test
every: 30s
expect:
status: 200
body: "34917f37-72b9-403f-887c-20c5e93b7173"
max_bytes: 64000
"#
);
let response_body = format!(
"{}{} --- FIN",
generate_numbers(64 * 1024, 0),
"34917f37-72b9-403f-887c-20c5e93b7173"
);
let config = create_config(&yaml);
let service = config
.services
.get("test-max_bytes")
.expect("Service not found");
let _ = env_logger::try_init();
let _mock = server
.mock("GET", "/test")
.with_body(response_body)
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs = handle_http_response(
"test-max_bytes",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!rs);
}
#[tokio::test]
async fn test_handle_http_response_expect_body_read_in_chunks() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r#"
---
services:
test-max_bytes:
url: {mock_url}/test
every: 30s
expect:
status: 200
body: "34917f37-72b9-403f-887c-20c5e93b7173"
"#
);
let response_body = format!(
"{} --- {} --- {} --- FIN",
generate_numbers(1024, 0),
"34917f37-72b9-403f-887c-20c5e93b7173",
generate_numbers(128 * 1024, 0)
);
let config = create_config(&yaml);
let service = config
.services
.get("test-max_bytes")
.expect("Service not found");
let _ = env_logger::try_init();
let _mock = server
.mock("GET", "/test")
.with_body(response_body)
.match_header(
"User-Agent",
mockito::Matcher::Regex("epazote.*".to_string()),
)
.with_status(200)
.create_async()
.await;
let (builder, _client_config) =
build_client(service).expect("Failed to build client builder");
let client = builder.build().expect("Failed to build client");
let request = build_http_request(&client, service).expect("Failed to build request");
let response = client
.execute(request.build().expect("Failed to build request"))
.await
.expect("Failed to execute request");
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs = handle_http_response(
"test-max_bytes",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(rs);
}
#[tokio::test]
async fn test_handle_http_response_expect_body_utf8_multibyte() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r#"
---
services:
test-utf8:
url: {mock_url}/test
every: 30s
expect:
status: 200
body: "epazote 🌿"
"#
);
let config = create_config(&yaml);
let service = config.services.get("test-utf8").expect("Service not found");
let _mock = server
.mock("GET", "/test")
.with_body("Automated HTTP supervisor: epazote 🌿")
.with_status(200)
.create_async()
.await;
let client = reqwest::Client::new();
let response = client.get(format!("{mock_url}/test")).send().await.unwrap();
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs = handle_http_response(
"test-utf8",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(rs);
}
#[tokio::test]
async fn test_handle_http_response_expect_body_max_bytes_exact_match() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let body_str = "0123456789";
let yaml = format!(
r#"
---
services:
test-exact:
url: {mock_url}/test
every: 30s
expect:
status: 200
body: "89"
max_bytes: 10
"#
);
let config = create_config(&yaml);
let service = config
.services
.get("test-exact")
.expect("Service not found");
let _mock = server
.mock("GET", "/test")
.with_body(body_str)
.with_status(200)
.create_async()
.await;
let client = reqwest::Client::new();
let response = client.get(format!("{mock_url}/test")).send().await.unwrap();
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs = handle_http_response(
"test-exact",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(rs);
}
#[tokio::test]
async fn test_handle_http_response_expect_body_max_bytes_too_small() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let body_str = "0123456789ABCDEF";
let yaml = format!(
r#"
---
services:
test-small:
url: {mock_url}/test
every: 30s
expect:
status: 200
body: "ABC"
max_bytes: 5
"#
);
let config = create_config(&yaml);
let service = config
.services
.get("test-small")
.expect("Service not found");
let _mock = server
.mock("GET", "/test")
.with_body(body_str)
.with_status(200)
.create_async()
.await;
let client = reqwest::Client::new();
let response = client.get(format!("{mock_url}/test")).send().await.unwrap();
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs = handle_http_response(
"test-small",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!rs);
}
#[tokio::test]
async fn test_handle_http_response_expect_json_max_bytes_limit() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let body_json =
r#"{"status":"success","data":"some long data that we might want to truncate"}"#;
let yaml = format!(
r"
---
services:
test-json-limit:
url: {mock_url}/test
every: 30s
expect:
status: 200
json:
status: success
max_bytes: 10
"
);
let config = create_config(&yaml);
let service = config
.services
.get("test-json-limit")
.expect("Service not found");
let _mock = server
.mock("GET", "/test")
.with_body(body_json)
.with_status(200)
.create_async()
.await;
let client = reqwest::Client::new();
let response = client.get(format!("{mock_url}/test")).send().await.unwrap();
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs = handle_http_response(
"test-json-limit",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!rs);
}
#[tokio::test]
async fn test_handle_http_response_expect_body_max_bytes_zero() {
let mut server = Server::new_async().await;
let mock_url = server.url();
let yaml = format!(
r#"
---
services:
test-zero:
url: {mock_url}/test
every: 30s
expect:
status: 200
body: "anything"
max_bytes: 0
"#
);
let config = create_config(&yaml);
let service = config.services.get("test-zero").expect("Service not found");
let _mock = server
.mock("GET", "/test")
.with_body("some content")
.with_status(200)
.create_async()
.await;
let client = reqwest::Client::new();
let response = client.get(format!("{mock_url}/test")).send().await.unwrap();
let counters: Arc<Mutex<HashMap<String, FallbackState>>> =
Arc::new(Mutex::new(HashMap::new()));
let rs = handle_http_response(
"test-zero",
service,
response,
&ServiceMetrics::new().expect("Failed to create metrics"),
Arc::clone(&counters),
)
.await
.expect("Failed to handle response");
assert!(!rs);
}
}