#![cfg(all(feature = "tokio", feature = "flow"))]
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
use flowscope::Timestamp;
use netring::anomaly::Severity;
use netring::anomaly::shipped_sinks::ChannelSink;
use netring::anomaly::sink::AnomalySink;
use netring::layer::{DedupeAnomalies, Layer, MinSeverity, RateLimitAnomalies};
fn key() -> flowscope::extract::FiveTupleKey {
flowscope::extract::FiveTupleKey::new(
flowscope::L4Proto::Tcp,
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 12345),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), 80),
)
}
fn layer_chain(base: Box<dyn AnomalySink>, layers: Vec<Box<dyn Layer>>) -> Box<dyn AnomalySink> {
let mut sink = base;
for layer in layers.into_iter().rev() {
sink = layer.wrap(sink);
}
sink
}
#[tokio::test]
async fn min_severity_drops_info_emissions() {
let (channel_sink, mut rx) = ChannelSink::channel();
let base: Box<dyn AnomalySink> = Box::new(channel_sink);
let mut sink = layer_chain(base, vec![Box::new(MinSeverity::warning())]);
let k = key();
for _ in 0..100 {
sink.begin("InfoKind", Severity::Info, Timestamp::new(0, 0))
.with_key(&k)
.emit();
sink.begin("WarnKind", Severity::Warning, Timestamp::new(0, 0))
.with_key(&k)
.emit();
}
let mut received = 0;
while rx.try_recv().is_ok() {
received += 1;
}
assert_eq!(received, 100, "expected 100 Warning emissions to survive");
}
#[tokio::test]
async fn dedupe_same_kind_same_key_within_window() {
let (channel_sink, mut rx) = ChannelSink::channel();
let base: Box<dyn AnomalySink> = Box::new(channel_sink);
let mut sink = layer_chain(
base,
vec![Box::new(DedupeAnomalies::within(Duration::from_secs(60)))],
);
let k = key();
for _ in 0..50 {
sink.begin("Repeat", Severity::Warning, Timestamp::new(0, 0))
.with_key(&k)
.emit();
}
let mut received = 0;
while rx.try_recv().is_ok() {
received += 1;
}
assert_eq!(received, 1, "DedupeAnomalies should fold 50 → 1");
}
#[tokio::test]
async fn dedupe_distinct_kinds_pass_through() {
let (channel_sink, mut rx) = ChannelSink::channel();
let base: Box<dyn AnomalySink> = Box::new(channel_sink);
let mut sink = layer_chain(
base,
vec![Box::new(DedupeAnomalies::within(Duration::from_secs(60)))],
);
let k = key();
for i in 0..10 {
let kind: &'static str = match i {
0 => "K0",
1 => "K1",
2 => "K2",
3 => "K3",
4 => "K4",
5 => "K5",
6 => "K6",
7 => "K7",
8 => "K8",
_ => "K9",
};
sink.begin(kind, Severity::Warning, Timestamp::new(0, 0))
.with_key(&k)
.emit();
}
let mut received = 0;
while rx.try_recv().is_ok() {
received += 1;
}
assert_eq!(received, 10);
}
#[tokio::test]
async fn full_chain_min_severity_then_dedupe_then_ratelimit() {
let (channel_sink, mut rx) = ChannelSink::channel();
let base: Box<dyn AnomalySink> = Box::new(channel_sink);
let mut sink = layer_chain(
base,
vec![
Box::new(MinSeverity::warning()),
Box::new(DedupeAnomalies::within(Duration::from_secs(60))),
Box::new(RateLimitAnomalies::new(100, Duration::from_secs(1))),
],
);
let k = key();
for _ in 0..100 {
sink.begin("InfoSpam", Severity::Info, Timestamp::new(0, 0))
.with_key(&k)
.emit();
}
for i in 0..50 {
let kind: &'static str = match i {
0 => "D00",
1 => "D01",
2 => "D02",
3 => "D03",
4 => "D04",
5 => "D05",
6 => "D06",
7 => "D07",
8 => "D08",
9 => "D09",
10 => "D10",
11 => "D11",
12 => "D12",
13 => "D13",
14 => "D14",
15 => "D15",
16 => "D16",
17 => "D17",
18 => "D18",
19 => "D19",
20 => "D20",
21 => "D21",
22 => "D22",
23 => "D23",
24 => "D24",
25 => "D25",
26 => "D26",
27 => "D27",
28 => "D28",
29 => "D29",
30 => "D30",
31 => "D31",
32 => "D32",
33 => "D33",
34 => "D34",
35 => "D35",
36 => "D36",
37 => "D37",
38 => "D38",
39 => "D39",
40 => "D40",
41 => "D41",
42 => "D42",
43 => "D43",
44 => "D44",
45 => "D45",
46 => "D46",
47 => "D47",
48 => "D48",
_ => "D49",
};
sink.begin(kind, Severity::Warning, Timestamp::new(0, 0))
.with_key(&k)
.emit();
}
for _ in 0..100 {
sink.begin("Repeat", Severity::Warning, Timestamp::new(0, 0))
.with_key(&k)
.emit();
}
let mut received = 0;
while rx.try_recv().is_ok() {
received += 1;
}
assert_eq!(
received, 51,
"expected 0 Info + 50 distinct Warning + 1 deduped Warning = 51"
);
}