use actix_web::{test, web, App, HttpResponse};
use moniof::config::MoniOFConfig;
use moniof::core::stats::QueryKind;
use moniof::core::task_ctx::{mark, mark_latency};
use moniof::MoniOF;
fn simulate_queries(key: &str, n: usize, ms: u128) {
for _ in 0..n {
mark(QueryKind::Sql, key);
mark_latency(QueryKind::Sql, key, ms);
}
}
fn header(res: &actix_web::dev::ServiceResponse, name: &str) -> Option<String> {
res.headers().get(name).map(|v| v.to_str().unwrap().to_string())
}
#[actix_web::test]
async fn records_totals_and_latency_for_a_handler() {
let app = test::init_service(App::new().wrap(MoniOF::new()).route(
"/",
web::get().to(|| async {
simulate_queries("users", 3, 4);
HttpResponse::Ok().finish()
}),
))
.await;
let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert_eq!(header(&res, "x-moniof-total").as_deref(), Some("3"));
assert_eq!(header(&res, "x-moniof-db-total-ms").as_deref(), Some("12"));
assert!(header(&res, "x-moniof-elapsed-ms").is_some());
}
#[actix_web::test]
async fn reports_the_slowest_key_by_max_single_latency() {
let app = test::init_service(App::new().wrap(MoniOF::new()).route(
"/",
web::get().to(|| async {
simulate_queries("orders", 4, 10);
simulate_queries("reports", 1, 90);
HttpResponse::Ok().finish()
}),
))
.await;
let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert_eq!(header(&res, "x-moniof-slowest-key").as_deref(), Some("sql/reports"));
assert_eq!(header(&res, "x-moniof-slowest-latency-ms").as_deref(), Some("90"));
}
#[actix_web::test]
async fn flags_an_n_plus_one_suspect() {
let app = test::init_service(App::new().wrap(MoniOF::new()).route(
"/",
web::get().to(|| async {
simulate_queries("users", 5, 2); HttpResponse::Ok().finish()
}),
))
.await;
let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert_eq!(header(&res, "x-moniof-n-plus-one-key").as_deref(), Some("sql/users"));
assert_eq!(header(&res, "x-moniof-n-plus-one-count").as_deref(), Some("5"));
assert_eq!(header(&res, "x-moniof-n-plus-one-total-ms").as_deref(), Some("10"));
}
#[actix_web::test]
async fn does_not_flag_below_the_repeat_threshold() {
let app = test::init_service(App::new().wrap(MoniOF::new()).route(
"/",
web::get().to(|| async {
simulate_queries("users", 4, 50); HttpResponse::Ok().finish()
}),
))
.await;
let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert_eq!(header(&res, "x-moniof-total").as_deref(), Some("4"));
assert!(header(&res, "x-moniof-n-plus-one-key").is_none());
}
#[actix_web::test]
async fn of_mode_off_suppresses_n_plus_one_headers_but_keeps_counters() {
let cfg = MoniOFConfig { of_mode: false, ..Default::default() };
let app = test::init_service(App::new().wrap(MoniOF::with_config(cfg)).route(
"/",
web::get().to(|| async {
simulate_queries("users", 20, 5);
HttpResponse::Ok().finish()
}),
))
.await;
let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert_eq!(header(&res, "x-moniof-total").as_deref(), Some("20"));
assert!(header(&res, "x-moniof-n-plus-one-key").is_none());
}
#[actix_web::test]
async fn add_response_headers_false_emits_nothing() {
let cfg = MoniOFConfig { add_response_headers: false, ..Default::default() };
let app = test::init_service(App::new().wrap(MoniOF::with_config(cfg)).route(
"/",
web::get().to(|| async {
simulate_queries("users", 9, 3);
HttpResponse::Ok().finish()
}),
))
.await;
let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert!(header(&res, "x-moniof-total").is_none());
assert!(header(&res, "x-moniof-db-total-ms").is_none());
assert!(header(&res, "x-moniof-slowest-key").is_none());
}
#[actix_web::test]
async fn a_handler_with_no_queries_reports_zeroes() {
let app = test::init_service(
App::new()
.wrap(MoniOF::new())
.route("/", web::get().to(|| async { HttpResponse::Ok().finish() })),
)
.await;
let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert_eq!(header(&res, "x-moniof-total").as_deref(), Some("0"));
assert_eq!(header(&res, "x-moniof-db-total-ms").as_deref(), Some("0"));
assert!(header(&res, "x-moniof-slowest-key").is_none());
}
#[actix_web::test]
async fn stats_do_not_leak_between_requests() {
let app = test::init_service(App::new().wrap(MoniOF::new()).route(
"/",
web::get().to(|| async {
simulate_queries("users", 2, 1);
HttpResponse::Ok().finish()
}),
))
.await;
for _ in 0..3 {
let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert_eq!(
header(&res, "x-moniof-total").as_deref(),
Some("2"),
"each request must start from a fresh QueryStats"
);
}
}
#[actix_web::test]
async fn stats_are_captured_across_await_points_in_the_handler() {
let app = test::init_service(App::new().wrap(MoniOF::new()).route(
"/",
web::get().to(|| async {
mark(QueryKind::Mongo, "users/find");
actix_web::rt::task::yield_now().await;
mark(QueryKind::Mongo, "users/find");
HttpResponse::Ok().finish()
}),
))
.await;
let res = test::call_service(&app, test::TestRequest::get().uri("/").to_request()).await;
assert_eq!(header(&res, "x-moniof-total").as_deref(), Some("2"));
}