#![allow(clippy::unwrap_used, clippy::panic)]
use std::time::Duration;
use super::*;
#[test]
fn an_unset_override_leaves_the_default() {
let budget = QueryFunctionBudget::from_getter(|_| None);
assert_eq!(budget.duration(), Duration::from_millis(QueryFunctionBudget::DEFAULT_MS));
assert!(budget.is_enforced());
}
#[test]
fn the_override_is_read_and_zero_disables_the_ceiling() {
let budget = QueryFunctionBudget::from_getter(|key| {
(key == QueryFunctionBudget::ENV).then(|| "250".into())
});
assert_eq!(budget.duration(), Duration::from_millis(250));
let disabled = QueryFunctionBudget::from_getter(|key| {
(key == QueryFunctionBudget::ENV).then(|| "0".into())
});
assert!(!disabled.is_enforced());
}
#[test]
fn an_unparseable_override_leaves_the_default() {
let budget = QueryFunctionBudget::from_getter(|key| {
(key == QueryFunctionBudget::ENV).then(|| "soon".into())
});
assert_eq!(budget.duration(), Duration::from_millis(QueryFunctionBudget::DEFAULT_MS));
}
#[test]
fn a_declared_timeout_wins_over_the_default_in_both_directions() {
let budget = QueryFunctionBudget::from_millis(5_000);
assert_eq!(budget.for_function(Some(30_000)).duration(), Duration::from_secs(30));
assert_eq!(budget.for_function(Some(250)).duration(), Duration::from_millis(250));
assert_eq!(budget.for_function(None).duration(), Duration::from_secs(5));
}
#[tokio::test(start_paused = true)]
async fn an_invocation_inside_the_budget_answers() {
let answer = run_within_budget("quotePreview", QueryFunctionBudget::from_millis(500), async {
tokio::time::sleep(Duration::from_millis(100)).await;
Ok(serde_json::json!({"id": "q-1"}))
})
.await
.unwrap();
assert_eq!(answer, serde_json::json!({"id": "q-1"}));
}
#[tokio::test(start_paused = true)]
async fn an_invocation_over_the_budget_times_out_naming_the_field() {
let error = run_within_budget("quotePreview", QueryFunctionBudget::from_millis(100), async {
tokio::time::sleep(Duration::from_millis(500)).await;
Ok(serde_json::json!({"id": "q-1"}))
})
.await
.expect_err("an invocation over the ceiling must not answer");
match error {
FraiseQLError::Timeout { timeout_ms, query } => {
assert_eq!(timeout_ms, 100);
assert_eq!(query.as_deref(), Some("quotePreview"));
},
other => panic!("expected a Timeout naming the field, got: {other}"),
}
}
#[tokio::test(start_paused = true)]
async fn a_zero_budget_means_no_ceiling() {
let answer = run_within_budget("quotePreview", QueryFunctionBudget::from_millis(0), async {
tokio::time::sleep(Duration::from_mins(1)).await;
Ok(serde_json::json!({"id": "q-1"}))
})
.await
.unwrap();
assert_eq!(answer, serde_json::json!({"id": "q-1"}));
}
#[tokio::test(start_paused = true)]
async fn an_invocation_failure_keeps_its_own_diagnosis() {
let error = run_within_budget("quotePreview", QueryFunctionBudget::from_millis(500), async {
Err(FraiseQLError::Validation {
message: "the quote engine is unavailable".to_string(),
path: None,
})
})
.await
.unwrap_err();
assert!(error.to_string().contains("quote engine is unavailable"), "got: {error}");
}
#[cfg(feature = "functions-runtime")]
#[test]
fn a_runtime_failure_is_attributed_to_the_field_and_keeps_its_message() {
let attributed = attribute(
"quotePreview",
"preview_quote",
&FraiseQLError::Validation {
message: "Failed to load WASM component".to_string(),
path: None,
},
);
let message = attributed.to_string();
assert!(message.contains("quotePreview"), "names the field: {message}");
assert!(message.contains("preview_quote"), "and the function: {message}");
assert!(
message.contains("Failed to load WASM component"),
"and keeps the only part an author can act on: {message}"
);
}