moniof 1.0.1

Actix middleware to monitor over-fetching and detect N+1 / overfetch database patterns (Mongo + SQL-agnostic)
Documentation
//! Tests for `core::task_ctx` — the task-local that carries per-request stats.

use moniof::core::stats::{QueryKind, QueryStatsHandle};
use moniof::core::task_ctx::{mark, mark_latency, MONIOF_HANDLE};

#[test]
fn mark_outside_a_request_scope_is_a_silent_noop() {
    // DB instrumentation fires from background tasks (pool warmup, migrations,
    // health checks) that never ran through the middleware. That must not panic.
    mark(QueryKind::Sql, "select 1");
    mark_latency(QueryKind::Mongo, "users/find", 12);
}

#[test]
fn mark_records_into_the_scoped_handle() {
    let handle = QueryStatsHandle::new();
    let read = handle.clone();

    MONIOF_HANDLE.sync_scope(handle, || {
        mark(QueryKind::Sql, "select 1");
        mark(QueryKind::Sql, "select 1");
    });

    let s = read.0.lock();
    assert_eq!(s.total, 2);
    assert_eq!(s.per_key["sql/select 1"], 2);
}

#[test]
fn keys_are_namespaced_by_query_kind() {
    let handle = QueryStatsHandle::new();
    let read = handle.clone();

    MONIOF_HANDLE.sync_scope(handle, || {
        mark(QueryKind::Sql, "k");
        mark(QueryKind::Mongo, "k");
        mark(QueryKind::Other, "k");
    });

    let s = read.0.lock();
    assert_eq!(s.total, 3);
    // Same logical name, three distinct buckets — a Mongo `users` and a SQL
    // `users` must never merge into one N+1 suspect.
    assert_eq!(s.per_key["sql/k"], 1);
    assert_eq!(s.per_key["mongo/k"], 1);
    assert_eq!(s.per_key["other/k"], 1);
}

#[test]
fn mark_latency_records_into_the_scoped_handle() {
    let handle = QueryStatsHandle::new();
    let read = handle.clone();

    MONIOF_HANDLE.sync_scope(handle, || {
        mark_latency(QueryKind::Mongo, "users/find", 8);
        mark_latency(QueryKind::Mongo, "users/find", 4);
    });

    let s = read.0.lock();
    assert_eq!(s.total_db_latency_ms, 12);
    assert_eq!(s.per_key_latency_ms["mongo/users/find"], 12);
    assert_eq!(s.per_key_max_latency_ms["mongo/users/find"], 8);
}

#[test]
fn scopes_are_isolated_from_each_other() {
    let a = QueryStatsHandle::new();
    let read_a = a.clone();
    let b = QueryStatsHandle::new();
    let read_b = b.clone();

    MONIOF_HANDLE.sync_scope(a, || mark(QueryKind::Sql, "in_a"));
    MONIOF_HANDLE.sync_scope(b, || mark(QueryKind::Sql, "in_b"));

    assert_eq!(read_a.0.lock().total, 1);
    assert!(read_a.0.lock().per_key.contains_key("sql/in_a"));
    assert_eq!(read_b.0.lock().total, 1);
    assert!(read_b.0.lock().per_key.contains_key("sql/in_b"));
}

#[tokio::test]
async fn stats_survive_across_await_points() {
    // A request handler awaits between queries; the task-local must follow it.
    let handle = QueryStatsHandle::new();
    let read = handle.clone();

    MONIOF_HANDLE
        .scope(handle, async {
            mark(QueryKind::Sql, "first");
            tokio::task::yield_now().await;
            mark(QueryKind::Sql, "second");
        })
        .await;

    assert_eq!(read.0.lock().total, 2);
}