open-standards/openpay/crates/op-fraud-graph/tests/velocity_counter.rs
Transaction Science 6bd1d3ed41
All checks were successful
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 8m28s
OpenPay at zero clippy findings, and held there in CI; FedNow's cap at the network's 10 million; a low-s bound that is N/2
Every OpenPay crate now passes clippy with -D warnings, and the openpay
entry of the workspace build sets clippy: true so it stays there. Most
of the findings were mechanical (clippy's own fixes, write! for
push_str(&format!), let-else); where a lint is wrong for the code it is
allowed at the narrowest scope with its reason: code tables keep one
arm per code, units_cents amounts keep their grouping, op-fraud-graph's
index casts rest on a bound upsert_entity now enforces (it used to hand
every vertex past 2^32 the same id).

Two findings were bugs. op-payouts capped FedNow at USD 1,000,000, the
limit before November 2025; the network maximum is USD 10,000,000 and a
test now sends five million. op-crypto-extra's EIP-7702 low-s bound was
N - 2^255, not N/2: it refused valid low-s signatures in a band just
below N/2. It is N/2 now, with tests at both edges.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 22:21:23 -05:00

68 lines
1.7 KiB
Rust

//! Integration tests for [`op_fraud_graph::VelocityCounter`].
use op_fraud_graph::{EntityKey, EntityKind, VelocityCounter, VelocityWindow};
fn k(s: &str) -> EntityKey {
EntityKey::from_raw(EntityKind::Account, s)
}
#[test]
fn counts_within_window() {
let mut c = VelocityCounter::new(VelocityWindow::default()).expect("ok");
let key = k("acc-A");
for t in 0..20 {
c.record(key, t);
}
assert_eq!(c.count(key, 19), 20);
}
#[test]
fn drops_events_outside_window() {
let mut c = VelocityCounter::new(VelocityWindow {
window_secs: 10,
bucket_secs: 1,
})
.expect("ok");
let key = k("acc-B");
for t in 0..10 {
c.record(key, t);
}
assert_eq!(c.count(key, 9), 10);
// Advance halfway: ts=14 → buckets at t=0..4 should have rolled off
// (window covers ts-9..=ts).
let later = c.count(key, 14);
assert!((5..10).contains(&later), "got {later}");
}
#[test]
fn unknown_key_is_zero() {
let mut c = VelocityCounter::new(VelocityWindow::default()).expect("ok");
assert_eq!(c.count(k("missing"), 0), 0);
}
#[test]
fn evict_stale_removes_old_entries() {
let mut c = VelocityCounter::new(VelocityWindow {
window_secs: 5,
bucket_secs: 1,
})
.expect("ok");
let key = k("acc-C");
c.record(key, 0);
assert_eq!(c.count(key, 0), 1);
c.evict_stale(1000);
// After eviction, the key should report zero (no state).
assert_eq!(c.count(key, 1000), 0);
}
#[test]
fn multiple_keys_isolated() {
let mut c = VelocityCounter::new(VelocityWindow::default()).expect("ok");
let a = k("acc-X");
let b = k("acc-Y");
c.record(a, 0);
c.record(a, 1);
c.record(b, 0);
assert_eq!(c.count(a, 1), 2);
assert_eq!(c.count(b, 1), 1);
}