moniof 1.0.1

Actix middleware to monitor over-fetching and detect N+1 / overfetch database patterns (Mongo + SQL-agnostic)
Documentation
//! Unit tests for `observability::of` — the N+1 / over-fetch suspect finder.

use moniof::config::MoniOFConfig;
use moniof::core::stats::QueryStats;
use moniof::observability::of::find_suspects;

/// Record `key` `count` times, each contributing `ms` of latency.
fn seed(stats: &mut QueryStats, key: &str, count: usize, ms: u128) {
    for _ in 0..count {
        stats.record(key);
        stats.record_latency(key, ms);
    }
}

fn cfg() -> MoniOFConfig {
    MoniOFConfig {
        of_mode: true,
        n_plus_one_min_count: 5,
        n_plus_one_min_total_ms: Some(5),
        ..Default::default()
    }
}

#[test]
fn no_suspects_when_of_mode_disabled() {
    let mut s = QueryStats::new();
    seed(&mut s, "sql/select 1", 100, 10);

    let c = MoniOFConfig { of_mode: false, ..cfg() };
    assert!(find_suspects(&s, &c).is_empty());
}

#[test]
fn no_suspects_below_min_count() {
    let mut s = QueryStats::new();
    seed(&mut s, "sql/select 1", 4, 100); // 4 < min_count 5

    assert!(find_suspects(&s, &cfg()).is_empty());
}

#[test]
fn suspect_at_exactly_min_count() {
    let mut s = QueryStats::new();
    seed(&mut s, "sql/select 1", 5, 2); // 5 >= 5, total latency 10 >= 5

    let found = find_suspects(&s, &cfg());
    assert_eq!(found.len(), 1);
    assert_eq!(found[0].key, "sql/select 1");
    assert_eq!(found[0].count, 5);
    assert_eq!(found[0].total_latency_ms, 10);
}

#[test]
fn latency_gate_rejects_fast_repeated_queries() {
    let mut s = QueryStats::new();
    // Repeated 20x but each is sub-millisecond => total 0ms, under the 5ms floor.
    seed(&mut s, "sql/select 1", 20, 0);

    assert!(
        find_suspects(&s, &cfg()).is_empty(),
        "a key with zero recorded latency must not trip the latency gate"
    );
}

#[test]
fn latency_gate_is_skipped_when_unset() {
    let mut s = QueryStats::new();
    seed(&mut s, "sql/select 1", 20, 0);

    let c = MoniOFConfig { n_plus_one_min_total_ms: None, ..cfg() };
    let found = find_suspects(&s, &c);
    assert_eq!(found.len(), 1);
    assert_eq!(found[0].count, 20);
}

#[test]
fn suspects_sorted_by_count_descending() {
    let mut s = QueryStats::new();
    seed(&mut s, "sql/low", 6, 10);
    seed(&mut s, "sql/high", 30, 10);
    seed(&mut s, "sql/mid", 12, 10);

    let found = find_suspects(&s, &cfg());
    let keys: Vec<&str> = found.iter().map(|f| f.key.as_str()).collect();
    assert_eq!(keys, ["sql/high", "sql/mid", "sql/low"]);
}

#[test]
fn ties_on_count_broken_by_total_latency_descending() {
    let mut s = QueryStats::new();
    seed(&mut s, "sql/cheap", 10, 1); // total 10ms
    seed(&mut s, "sql/pricey", 10, 9); // total 90ms

    let found = find_suspects(&s, &cfg());
    assert_eq!(found[0].key, "sql/pricey");
    assert_eq!(found[1].key, "sql/cheap");
}

#[test]
fn suspects_are_capped_at_three() {
    let mut s = QueryStats::new();
    for i in 0..10 {
        seed(&mut s, &format!("sql/q{i}"), 6 + i, 10);
    }

    let found = find_suspects(&s, &cfg());
    assert_eq!(found.len(), 3, "finder must return at most the top 3 suspects");
    // And they must be the three worst, not an arbitrary three.
    assert_eq!(found[0].key, "sql/q9");
    assert_eq!(found[1].key, "sql/q8");
    assert_eq!(found[2].key, "sql/q7");
}

#[test]
fn counts_reported_are_the_recorded_counts() {
    let mut s = QueryStats::new();
    seed(&mut s, "mongo/users/find", 7, 3);

    let found = find_suspects(&s, &cfg());
    assert_eq!(found[0].count, 7);
    assert_eq!(found[0].total_latency_ms, 21);
}