mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-24 13:29:45 +00:00
Adds, per node, how many distinct FLOOD adverts it originated in the last 7 days. Zero-hop adverts (route_type DIRECT) are excluded, so a nearby observer hearing a node's cheap local adverts does not inflate the number - the existing advert_count mixes both kinds and cannot tell a chatty flooder (mesh-wide airtime) from the recommended 240-minute zero-hop cadence (local only). Consumers (the ArcScope repeater advisor) rate advert hygiene against the community practice of one flood advert every ~49h; with the mixed total, a correctly configured repeater looked chatty whenever an observer sat within zero-hop range. Implemented like the relay-liveness fields: a pure, unit-tested counter over (first_seen, route_type, hash) entries with the same timestamp parsing and hash dedup, fed by a from_pubkey-indexed query capped at the 2000 most recent advert rows. The flood route-type constant is named advertRouteTypeFlood so this merges independently of the open unscoped-relay PR (#1823). --------- Co-authored-by: Waydroid Builder <build@waydroid.local>
114 lines
4.4 KiB
Go
114 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// insertAdvertTx seeds one advert transmission row - the single place that
|
|
// knows the INSERT column list, shared by every test in this file.
|
|
func insertAdvertTx(t *testing.T, db *DB, pubkey, hash string, rt int, ts time.Time) {
|
|
t.Helper()
|
|
if _, err := db.conn.Exec(
|
|
`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, from_pubkey) VALUES ('00', ?, ?, ?, ?, ?)`,
|
|
hash, ts.Format("2006-01-02T15:04:05.000Z"), rt, payloadTypeAdvert, pubkey); err != nil {
|
|
t.Fatalf("insert transmission: %v", err)
|
|
}
|
|
}
|
|
|
|
func advertTS(hoursAgo float64) string {
|
|
return time.Now().UTC().Add(-time.Duration(hoursAgo * float64(time.Hour))).Format("2006-01-02T15:04:05.000Z")
|
|
}
|
|
|
|
// Flood adverts inside the window count; zero-hop (DIRECT) and out-of-window
|
|
// ones do not; duplicate hashes collapse; broken timestamps are skipped.
|
|
func TestCountFloodAdverts(t *testing.T) {
|
|
now := time.Now()
|
|
entries := []floodAdvertEntry{
|
|
{ts: advertTS(1), rt: advertRouteTypeFlood, hash: "a1"},
|
|
{ts: advertTS(2), rt: advertRouteTypeFlood, hash: "a1"}, // dup hash: one advert, two rows
|
|
{ts: advertTS(3), rt: advertRouteTypeFlood, hash: "a2"},
|
|
{ts: advertTS(4), rt: 0, hash: "a3"}, // zero-hop (DIRECT): excluded
|
|
{ts: advertTS(9 * 24), rt: advertRouteTypeFlood, hash: "a4"}, // outside 7d window
|
|
{ts: "not-a-time", rt: advertRouteTypeFlood, hash: "a5"}, // unparseable: skipped
|
|
{ts: advertTS(5), rt: -1, hash: "a6"}, // route type absent: excluded
|
|
}
|
|
if got := countFloodAdverts(entries, now, 7*24); got != 2 {
|
|
t.Fatalf("want 2 flood adverts in window, got %d", got)
|
|
}
|
|
// Hash-less entries dedup by timestamp instead of collapsing into one.
|
|
hashless := []floodAdvertEntry{
|
|
{ts: advertTS(1), rt: advertRouteTypeFlood},
|
|
{ts: advertTS(2), rt: advertRouteTypeFlood},
|
|
}
|
|
if got := countFloodAdverts(hashless, now, 7*24); got != 2 {
|
|
t.Fatalf("want 2 hash-less flood adverts, got %d", got)
|
|
}
|
|
}
|
|
|
|
// The wire contract: GET /api/nodes/{pubkey} carries flood_advert_count_7d,
|
|
// counting recent flood adverts only (zero-hop and out-of-window excluded).
|
|
func TestNodeDetailIncludesFloodAdvertCount(t *testing.T) {
|
|
srv, router := setupTestServer(t)
|
|
now := time.Now().UTC()
|
|
ins := func(hash string, rt int, ts time.Time) {
|
|
insertAdvertTx(t, srv.db, "aabbccdd11223344", hash, rt, ts)
|
|
}
|
|
// The shared fixture may already seed adverts for this node, so assert the
|
|
// DELTA our inserts cause rather than an absolute count.
|
|
fetch := func() float64 {
|
|
t.Helper()
|
|
req := httptest.NewRequest("GET", "/api/nodes/aabbccdd11223344", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("bad JSON: %v", err)
|
|
}
|
|
node, ok := body["node"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected node object")
|
|
}
|
|
got, ok := node["flood_advert_count_7d"].(float64)
|
|
if !ok {
|
|
t.Fatalf("flood_advert_count_7d missing or not a number: %v", node["flood_advert_count_7d"])
|
|
}
|
|
return got
|
|
}
|
|
before := fetch()
|
|
|
|
ins("fa1", advertRouteTypeFlood, now.Add(-2*time.Hour))
|
|
ins("fa2", advertRouteTypeFlood, now.Add(-30*time.Hour))
|
|
ins("za1", 0, now.Add(-1*time.Hour)) // zero-hop: excluded
|
|
ins("fa3", advertRouteTypeFlood, now.Add(-9*24*time.Hour)) // outside the 7d window
|
|
// Inside the SQL date floor (window + 1d slack) but outside the exact 7d
|
|
// window - only the Go-side check rejects this one.
|
|
ins("fa4", advertRouteTypeFlood, now.Add(-time.Duration(7.5*24)*time.Hour))
|
|
|
|
if got := fetch(); got != before+2 {
|
|
t.Fatalf("want flood_advert_count_7d = %v+2, got %v", before, got)
|
|
}
|
|
}
|
|
|
|
// The row cap saturates the count instead of failing: with a cap of 2, three
|
|
// qualifying flood adverts count as 2 (newest rows win via ORDER BY id DESC).
|
|
func TestCountFloodAdvertsForNode_RowCapSaturates(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
now := time.Now().UTC()
|
|
for i, h := range []string{"cap1", "cap2", "cap3"} {
|
|
insertAdvertTx(t, db, "capnode11223344", h, advertRouteTypeFlood, now.Add(-time.Duration(i+1)*time.Hour))
|
|
}
|
|
n, err := db.CountFloodAdvertsForNode("capnode11223344", 7*24, 2)
|
|
if err != nil {
|
|
t.Fatalf("count: %v", err)
|
|
}
|
|
if n != 2 {
|
|
t.Fatalf("want saturated count 2, got %d", n)
|
|
}
|
|
}
|