Files
meshcore-analyzer/cmd/server/coverage_test.go
2026-03-28 15:04:54 -07:00

3715 lines
105 KiB
Go

package main
import (
"database/sql"
"encoding/json"
"fmt"
"math"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gorilla/mux"
_ "modernc.org/sqlite"
)
// --- helpers ---
func setupTestDBv2(t *testing.T) *DB {
t.Helper()
conn, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatal(err)
}
schema := `
CREATE TABLE nodes (
public_key TEXT PRIMARY KEY, name TEXT, role TEXT,
lat REAL, lon REAL, last_seen TEXT, first_seen TEXT, advert_count INTEGER DEFAULT 0,
battery_mv INTEGER, temperature_c REAL
);
CREATE TABLE observers (
id TEXT PRIMARY KEY, name TEXT, iata TEXT, last_seen TEXT, first_seen TEXT,
packet_count INTEGER DEFAULT 0, model TEXT, firmware TEXT,
client_version TEXT, radio TEXT, battery_mv INTEGER, uptime_secs INTEGER, noise_floor REAL
);
CREATE TABLE transmissions (
id INTEGER PRIMARY KEY AUTOINCREMENT, raw_hex TEXT NOT NULL,
hash TEXT NOT NULL UNIQUE, first_seen TEXT NOT NULL,
route_type INTEGER, payload_type INTEGER, payload_version INTEGER,
decoded_json TEXT, created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
transmission_id INTEGER NOT NULL REFERENCES transmissions(id),
observer_id TEXT, observer_name TEXT, direction TEXT,
snr REAL, rssi REAL, score INTEGER, path_json TEXT, timestamp INTEGER NOT NULL
);
`
if _, err := conn.Exec(schema); err != nil {
t.Fatal(err)
}
return &DB{conn: conn, isV3: false}
}
func seedV2Data(t *testing.T, db *DB) {
t.Helper()
now := time.Now().UTC()
recent := now.Add(-1 * time.Hour).Format(time.RFC3339)
epoch := now.Add(-1 * time.Hour).Unix()
db.conn.Exec(`INSERT INTO observers (id, name, iata, last_seen, first_seen, packet_count)
VALUES ('obs1', 'Obs One', 'SJC', ?, '2026-01-01T00:00:00Z', 100)`, recent)
db.conn.Exec(`INSERT INTO nodes (public_key, name, role, lat, lon, last_seen, first_seen, advert_count)
VALUES ('aabbccdd11223344', 'TestRepeater', 'repeater', 37.5, -122.0, ?, '2026-01-01T00:00:00Z', 50)`, recent)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('AABB', 'abc123def4567890', ?, 1, 4, '{"pubKey":"aabbccdd11223344","name":"TestRepeater","type":"ADVERT"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_id, observer_name, snr, rssi, path_json, timestamp)
VALUES (1, 'obs1', 'Obs One', 12.5, -90, '["aa","bb"]', ?)`, epoch)
}
func setupNoStoreServer(t *testing.T) (*Server, *mux.Router) {
t.Helper()
db := setupTestDB(t)
seedTestData(t, db)
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
// No store — forces DB fallback paths
router := mux.NewRouter()
srv.RegisterRoutes(router)
return srv, router
}
// --- detectSchema ---
func TestDetectSchemaV3(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
if !db.isV3 {
t.Error("expected v3 schema (observer_idx)")
}
}
func TestDetectSchemaV2(t *testing.T) {
db := setupTestDBv2(t)
defer db.Close()
db.detectSchema()
if db.isV3 {
t.Error("expected v2 schema (observer_id), got v3")
}
}
func TestDetectSchemaV2Queries(t *testing.T) {
db := setupTestDBv2(t)
defer db.Close()
seedV2Data(t, db)
// v2 schema should work with QueryPackets
result, err := db.QueryPackets(PacketQuery{Limit: 50, Order: "DESC"})
if err != nil {
t.Fatal(err)
}
if result.Total != 1 {
t.Errorf("expected 1 transmission in v2, got %d", result.Total)
}
// v2 grouped query
gResult, err := db.QueryGroupedPackets(PacketQuery{Limit: 50, Order: "DESC"})
if err != nil {
t.Fatal(err)
}
if gResult.Total != 1 {
t.Errorf("expected 1 grouped in v2, got %d", gResult.Total)
}
// v2 GetObserverPacketCounts
counts := db.GetObserverPacketCounts(0)
if counts["obs1"] != 1 {
t.Errorf("expected 1 obs count for obs1, got %d", counts["obs1"])
}
// v2 QueryMultiNodePackets
mResult, err := db.QueryMultiNodePackets([]string{"aabbccdd11223344"}, 50, 0, "DESC", "", "")
if err != nil {
t.Fatal(err)
}
if mResult.Total != 1 {
t.Errorf("expected 1 multi-node packet in v2, got %d", mResult.Total)
}
}
// --- buildPacketWhere ---
func TestBuildPacketWhere(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
tests := []struct {
name string
query PacketQuery
wantWhere int
}{
{"empty", PacketQuery{}, 0},
{"type filter", PacketQuery{Type: intPtr(4)}, 1},
{"route filter", PacketQuery{Route: intPtr(1)}, 1},
{"observer filter", PacketQuery{Observer: "obs1"}, 1},
{"hash filter", PacketQuery{Hash: "ABC123DEF4567890"}, 1},
{"since filter", PacketQuery{Since: "2025-01-01"}, 1},
{"until filter", PacketQuery{Until: "2099-01-01"}, 1},
{"region filter", PacketQuery{Region: "SJC"}, 1},
{"node filter", PacketQuery{Node: "TestRepeater"}, 1},
{"all filters", PacketQuery{
Type: intPtr(4), Route: intPtr(1), Observer: "obs1",
Hash: "abc123", Since: "2025-01-01", Until: "2099-01-01",
Region: "SJC", Node: "TestRepeater",
}, 8},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
where, args := db.buildPacketWhere(tc.query)
if len(where) != tc.wantWhere {
t.Errorf("expected %d where clauses, got %d", tc.wantWhere, len(where))
}
if len(where) != len(args) {
t.Errorf("where count (%d) != args count (%d)", len(where), len(args))
}
})
}
}
// --- DB.QueryMultiNodePackets ---
func TestDBQueryMultiNodePackets(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
t.Run("empty pubkeys", func(t *testing.T) {
result, err := db.QueryMultiNodePackets(nil, 50, 0, "DESC", "", "")
if err != nil {
t.Fatal(err)
}
if result.Total != 0 {
t.Errorf("expected 0 for empty pubkeys, got %d", result.Total)
}
})
t.Run("single pubkey match", func(t *testing.T) {
result, err := db.QueryMultiNodePackets([]string{"aabbccdd11223344"}, 50, 0, "DESC", "", "")
if err != nil {
t.Fatal(err)
}
if result.Total < 1 {
t.Errorf("expected >=1, got %d", result.Total)
}
})
t.Run("multiple pubkeys", func(t *testing.T) {
result, err := db.QueryMultiNodePackets(
[]string{"aabbccdd11223344", "eeff00112233aabb"}, 50, 0, "DESC", "", "")
if err != nil {
t.Fatal(err)
}
if result.Total < 1 {
t.Errorf("expected >=1, got %d", result.Total)
}
})
t.Run("with time filters", func(t *testing.T) {
result, err := db.QueryMultiNodePackets(
[]string{"aabbccdd11223344"}, 50, 0, "ASC",
"2020-01-01T00:00:00Z", "2099-01-01T00:00:00Z")
if err != nil {
t.Fatal(err)
}
if result.Total < 1 {
t.Errorf("expected >=1, got %d", result.Total)
}
})
t.Run("default limit and order", func(t *testing.T) {
result, err := db.QueryMultiNodePackets([]string{"aabbccdd11223344"}, 0, 0, "", "", "")
if err != nil {
t.Fatal(err)
}
if result.Total < 1 {
t.Errorf("expected >=1, got %d", result.Total)
}
})
t.Run("no match", func(t *testing.T) {
result, err := db.QueryMultiNodePackets([]string{"nonexistent"}, 50, 0, "DESC", "", "")
if err != nil {
t.Fatal(err)
}
if result.Total != 0 {
t.Errorf("expected 0, got %d", result.Total)
}
})
}
// --- Store.QueryMultiNodePackets ---
func TestStoreQueryMultiNodePackets(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
t.Run("empty pubkeys", func(t *testing.T) {
result := store.QueryMultiNodePackets(nil, 50, 0, "DESC", "", "")
if result.Total != 0 {
t.Errorf("expected 0, got %d", result.Total)
}
})
t.Run("matching pubkey", func(t *testing.T) {
result := store.QueryMultiNodePackets([]string{"aabbccdd11223344"}, 50, 0, "DESC", "", "")
if result.Total < 1 {
t.Errorf("expected >=1, got %d", result.Total)
}
})
t.Run("ASC order", func(t *testing.T) {
result := store.QueryMultiNodePackets([]string{"aabbccdd11223344"}, 50, 0, "ASC", "", "")
if result.Total < 1 {
t.Errorf("expected >=1, got %d", result.Total)
}
})
t.Run("with since/until", func(t *testing.T) {
result := store.QueryMultiNodePackets(
[]string{"aabbccdd11223344"}, 50, 0, "DESC",
"2020-01-01T00:00:00Z", "2099-01-01T00:00:00Z")
if result.Total < 1 {
t.Errorf("expected >=1, got %d", result.Total)
}
})
t.Run("offset beyond total", func(t *testing.T) {
result := store.QueryMultiNodePackets([]string{"aabbccdd11223344"}, 50, 9999, "DESC", "", "")
if len(result.Packets) != 0 {
t.Errorf("expected 0 packets, got %d", len(result.Packets))
}
})
t.Run("default limit", func(t *testing.T) {
result := store.QueryMultiNodePackets([]string{"aabbccdd11223344"}, 0, 0, "DESC", "", "")
if result.Total < 1 {
t.Errorf("expected >=1, got %d", result.Total)
}
})
}
// --- IngestNewFromDB ---
func TestIngestNewFromDB(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
initialMax := store.MaxTransmissionID()
// Insert a new transmission in DB
now := time.Now().UTC().Format(time.RFC3339)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('EEFF', 'newhash123456abcd', ?, 1, 4, '{"pubKey":"aabbccdd11223344","type":"ADVERT"}')`, now)
newTxID := 0
db.conn.QueryRow("SELECT MAX(id) FROM transmissions").Scan(&newTxID)
// Add observation for the new transmission
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (?, 1, 10.0, -92, '["cc"]', ?)`, newTxID, time.Now().Unix())
// Ingest
broadcastMaps, newMax := store.IngestNewFromDB(initialMax, 100)
if newMax <= initialMax {
t.Errorf("expected newMax > %d, got %d", initialMax, newMax)
}
if len(broadcastMaps) < 1 {
t.Errorf("expected >=1 broadcast maps, got %d", len(broadcastMaps))
}
// Verify broadcast map contains nested "packet" field (fixes #162)
if len(broadcastMaps) > 0 {
bm := broadcastMaps[0]
pkt, ok := bm["packet"]
if !ok || pkt == nil {
t.Error("broadcast map missing 'packet' field (required by packets.js)")
}
pktMap, ok := pkt.(map[string]interface{})
if ok {
for _, field := range []string{"id", "hash", "payload_type", "observer_id"} {
if _, exists := pktMap[field]; !exists {
t.Errorf("packet sub-object missing field %q", field)
}
}
}
// Verify decoded also present at top level (for live.js)
if _, ok := bm["decoded"]; !ok {
t.Error("broadcast map missing 'decoded' field (required by live.js)")
}
}
// Verify ingested into store
updatedMax := store.MaxTransmissionID()
if updatedMax < newMax {
t.Errorf("store max (%d) should be >= newMax (%d)", updatedMax, newMax)
}
t.Run("no new data", func(t *testing.T) {
maps, max := store.IngestNewFromDB(newMax, 100)
if maps != nil {
t.Errorf("expected nil for no new data, got %d maps", len(maps))
}
if max != newMax {
t.Errorf("expected same max %d, got %d", newMax, max)
}
})
t.Run("default limit", func(t *testing.T) {
_, _ = store.IngestNewFromDB(newMax, 0)
})
}
func TestIngestNewFromDBv2(t *testing.T) {
db := setupTestDBv2(t)
defer db.Close()
seedV2Data(t, db)
store := NewPacketStore(db)
store.Load()
initialMax := store.MaxTransmissionID()
now := time.Now().UTC().Format(time.RFC3339)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('EEFF', 'v2newhash12345678', ?, 1, 4, '{"pubKey":"aabbccdd11223344","type":"ADVERT"}')`, now)
newTxID := 0
db.conn.QueryRow("SELECT MAX(id) FROM transmissions").Scan(&newTxID)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_id, observer_name, snr, rssi, path_json, timestamp)
VALUES (?, 'obs1', 'Obs One', 10.0, -92, '["cc"]', ?)`, newTxID, time.Now().Unix())
broadcastMaps, newMax := store.IngestNewFromDB(initialMax, 100)
if newMax <= initialMax {
t.Errorf("expected newMax > %d, got %d", initialMax, newMax)
}
if len(broadcastMaps) < 1 {
t.Errorf("expected >=1 broadcast maps, got %d", len(broadcastMaps))
}
}
// --- MaxTransmissionID ---
func TestMaxTransmissionID(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
maxID := store.MaxTransmissionID()
if maxID <= 0 {
t.Errorf("expected maxID > 0, got %d", maxID)
}
t.Run("empty store", func(t *testing.T) {
emptyStore := NewPacketStore(db)
if emptyStore.MaxTransmissionID() != 0 {
t.Error("expected 0 for empty store")
}
})
}
// --- Route handler DB fallback (no store) ---
func TestHandleBulkHealthNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/nodes/bulk-health?limit=10", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
var body []interface{}
json.Unmarshal(w.Body.Bytes(), &body)
if body == nil {
t.Fatal("expected array response")
}
}
func TestHandleBulkHealthNoStoreMaxLimit(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/nodes/bulk-health?limit=500", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleAnalyticsRFNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
t.Run("basic", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/analytics/rf", 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{}
json.Unmarshal(w.Body.Bytes(), &body)
if _, ok := body["snr"]; !ok {
t.Error("expected snr field")
}
if _, ok := body["payloadTypes"]; !ok {
t.Error("expected payloadTypes field")
}
})
t.Run("with region", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/analytics/rf?region=SJC", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
}
func TestHandlePacketsNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
t.Run("basic packets", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets?limit=10", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("multi-node", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets?nodes=aabbccdd11223344,eeff00112233aabb", 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{}
json.Unmarshal(w.Body.Bytes(), &body)
if _, ok := body["packets"]; !ok {
t.Error("expected packets field")
}
})
t.Run("grouped", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets?groupByHash=true&limit=10", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
}
func TestHandlePacketsMultiNodeWithStore(t *testing.T) {
_, router := setupTestServer(t)
req := httptest.NewRequest("GET", "/api/packets?nodes=aabbccdd11223344&order=asc&limit=10&offset=0", 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{}
json.Unmarshal(w.Body.Bytes(), &body)
if _, ok := body["packets"]; !ok {
t.Error("expected packets field")
}
}
func TestHandlePacketDetailNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
t.Run("by hash", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets/abc123def4567890", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 404 {
t.Fatalf("expected 404 (no store), got %d: %s", w.Code, w.Body.String())
}
})
t.Run("by ID", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets/1", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 404 {
t.Fatalf("expected 404 (no store), got %d: %s", w.Code, w.Body.String())
}
})
t.Run("not found", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets/9999", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 404 {
t.Fatalf("expected 404, got %d", w.Code)
}
})
t.Run("non-numeric non-hash", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets/notahash", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 404 {
t.Fatalf("expected 404, got %d", w.Code)
}
})
}
func TestHandleAnalyticsChannelsNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/analytics/channels", 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{}
json.Unmarshal(w.Body.Bytes(), &body)
if _, ok := body["activeChannels"]; !ok {
t.Error("expected activeChannels field")
}
}
// --- transmissionsForObserver (byObserver index path) ---
func TestTransmissionsForObserverIndex(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
// Query packets for an observer — hits the byObserver index
result := store.QueryPackets(PacketQuery{Limit: 50, Observer: "obs1", Order: "DESC"})
if result.Total < 1 {
t.Errorf("expected >=1 packets for obs1, got %d", result.Total)
}
// Query with observer + type (uses from != nil path in transmissionsForObserver)
pt := 4
result2 := store.QueryPackets(PacketQuery{Limit: 50, Observer: "obs1", Type: &pt, Order: "DESC"})
if result2.Total < 1 {
t.Errorf("expected >=1 filtered packets, got %d", result2.Total)
}
}
// --- GetChannelMessages (dedup, observer, hops paths) ---
func TestGetChannelMessagesFromStore(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
// Test channel should exist from seed data
messages, total := store.GetChannelMessages("#test", 100, 0)
if total < 1 {
t.Errorf("expected >=1 messages for #test, got %d", total)
}
if len(messages) < 1 {
t.Errorf("expected >=1 message entries, got %d", len(messages))
}
t.Run("non-existent channel", func(t *testing.T) {
msgs, total := store.GetChannelMessages("nonexistent", 100, 0)
if total != 0 || len(msgs) != 0 {
t.Errorf("expected 0 for nonexistent channel, got %d/%d", total, len(msgs))
}
})
t.Run("default limit", func(t *testing.T) {
_, total := store.GetChannelMessages("#test", 0, 0)
if total < 1 {
t.Errorf("expected >=1 with default limit, got %d", total)
}
})
t.Run("with offset", func(t *testing.T) {
_, _ = store.GetChannelMessages("#test", 10, 9999)
})
}
func TestGetChannelMessagesDedupe(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
now := time.Now().UTC()
recent := now.Add(-1 * time.Hour).Format(time.RFC3339)
epoch := now.Add(-1 * time.Hour).Unix()
seedTestData(t, db)
// Insert a duplicate channel message with the same hash as existing
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('DDEE', 'dupchannelhash1234', ?, 1, 5, '{"type":"CHAN","channel":"#test","text":"Hello: World","sender":"TestUser"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (3, 1, 11.0, -91, '["aa"]', ?)`, epoch)
// Insert another dupe same hash as above (should dedup)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('DDFF', 'dupchannelhash5678', ?, 1, 5, '{"type":"CHAN","channel":"#test","text":"Hello: World","sender":"TestUser"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (4, 2, 9.0, -93, '[]', ?)`, epoch)
store := NewPacketStore(db)
store.Load()
msgs, total := store.GetChannelMessages("#test", 100, 0)
// Should have messages, with some deduped
if total < 1 {
t.Errorf("expected >=1 total messages, got %d", total)
}
_ = msgs
}
// --- GetChannels ---
func TestGetChannelsFromStore(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
channels := store.GetChannels("")
if len(channels) < 1 {
t.Errorf("expected >=1 channel, got %d", len(channels))
}
t.Run("with region", func(t *testing.T) {
ch := store.GetChannels("SJC")
_ = ch
})
t.Run("non-existent region", func(t *testing.T) {
ch := store.GetChannels("NONEXIST")
// Region filter may return 0 or fallback to unfiltered depending on DB content
_ = ch
})
}
// --- resolve (prefixMap) ---
func TestPrefixMapResolve(t *testing.T) {
nodes := []nodeInfo{
{PublicKey: "aabbccdd11223344", Name: "NodeA", HasGPS: true, Lat: 37.5, Lon: -122.0},
{PublicKey: "aabbccdd55667788", Name: "NodeB", HasGPS: false},
{PublicKey: "eeff0011aabbccdd", Name: "NodeC", HasGPS: true, Lat: 38.0, Lon: -121.0},
}
pm := buildPrefixMap(nodes)
t.Run("exact match", func(t *testing.T) {
n := pm.resolve("aabbccdd11223344")
if n == nil || n.Name != "NodeA" {
t.Errorf("expected NodeA, got %v", n)
}
})
t.Run("prefix match single", func(t *testing.T) {
n := pm.resolve("eeff")
if n == nil || n.Name != "NodeC" {
t.Errorf("expected NodeC, got %v", n)
}
})
t.Run("prefix match multiple — prefer GPS", func(t *testing.T) {
n := pm.resolve("aabbccdd")
if n == nil {
t.Fatal("expected non-nil")
}
if !n.HasGPS {
t.Error("expected GPS-preferred candidate")
}
if n.Name != "NodeA" {
t.Errorf("expected NodeA (has GPS), got %s", n.Name)
}
})
t.Run("no match", func(t *testing.T) {
n := pm.resolve("zzzzz")
if n != nil {
t.Errorf("expected nil, got %v", n)
}
})
t.Run("multiple candidates no GPS", func(t *testing.T) {
noGPSNodes := []nodeInfo{
{PublicKey: "aa11bb22", Name: "X", HasGPS: false},
{PublicKey: "aa11cc33", Name: "Y", HasGPS: false},
}
pm2 := buildPrefixMap(noGPSNodes)
n := pm2.resolve("aa11")
if n == nil {
t.Fatal("expected non-nil")
}
// Should return first candidate
})
}
// --- pathLen ---
func TestPathLen(t *testing.T) {
tests := []struct {
json string
want int
}{
{"", 0},
{"invalid", 0},
{`[]`, 0},
{`["aa"]`, 1},
{`["aa","bb","cc"]`, 3},
}
for _, tc := range tests {
got := pathLen(tc.json)
if got != tc.want {
t.Errorf("pathLen(%q) = %d, want %d", tc.json, got, tc.want)
}
}
}
// --- floatPtrOrNil ---
func TestFloatPtrOrNil(t *testing.T) {
v := 3.14
if floatPtrOrNil(&v) != 3.14 {
t.Error("expected 3.14")
}
if floatPtrOrNil(nil) != nil {
t.Error("expected nil")
}
}
// --- nullFloatPtr ---
func TestNullFloatPtr(t *testing.T) {
valid := sql.NullFloat64{Float64: 2.71, Valid: true}
p := nullFloatPtr(valid)
if p == nil || *p != 2.71 {
t.Errorf("expected 2.71, got %v", p)
}
invalid := sql.NullFloat64{Valid: false}
if nullFloatPtr(invalid) != nil {
t.Error("expected nil for invalid")
}
}
// --- nilIfEmpty ---
func TestNilIfEmpty(t *testing.T) {
if nilIfEmpty("") != nil {
t.Error("expected nil for empty")
}
if nilIfEmpty("hello") != "hello" {
t.Error("expected 'hello'")
}
}
// --- pickBestObservation ---
func TestPickBestObservation(t *testing.T) {
t.Run("empty observations", func(t *testing.T) {
tx := &StoreTx{}
pickBestObservation(tx)
if tx.ObserverID != "" {
t.Error("expected empty observer for no observations")
}
})
t.Run("single observation", func(t *testing.T) {
snr := 10.0
tx := &StoreTx{
Observations: []*StoreObs{
{ObserverID: "obs1", ObserverName: "One", SNR: &snr, PathJSON: `["aa"]`},
},
}
pickBestObservation(tx)
if tx.ObserverID != "obs1" {
t.Errorf("expected obs1, got %s", tx.ObserverID)
}
})
t.Run("picks longest path", func(t *testing.T) {
snr1, snr2 := 10.0, 5.0
tx := &StoreTx{
Observations: []*StoreObs{
{ObserverID: "obs1", SNR: &snr1, PathJSON: `["aa"]`},
{ObserverID: "obs2", SNR: &snr2, PathJSON: `["aa","bb","cc"]`},
},
}
pickBestObservation(tx)
if tx.ObserverID != "obs2" {
t.Errorf("expected obs2 (longest path), got %s", tx.ObserverID)
}
})
}
// --- indexByNode ---
func TestIndexByNode(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
store := NewPacketStore(db)
t.Run("empty decoded_json", func(t *testing.T) {
tx := &StoreTx{Hash: "h1"}
store.indexByNode(tx)
if len(store.byNode) != 0 {
t.Error("expected no index entries")
}
})
t.Run("valid decoded_json", func(t *testing.T) {
tx := &StoreTx{
Hash: "h2",
DecodedJSON: `{"pubKey":"aabbccdd11223344","destPubKey":"eeff00112233aabb"}`,
}
store.indexByNode(tx)
if len(store.byNode["aabbccdd11223344"]) != 1 {
t.Error("expected pubKey indexed")
}
if len(store.byNode["eeff00112233aabb"]) != 1 {
t.Error("expected destPubKey indexed")
}
})
t.Run("duplicate hash skipped", func(t *testing.T) {
tx := &StoreTx{
Hash: "h2",
DecodedJSON: `{"pubKey":"aabbccdd11223344"}`,
}
store.indexByNode(tx)
// Should not add duplicate
if len(store.byNode["aabbccdd11223344"]) != 1 {
t.Errorf("expected 1, got %d", len(store.byNode["aabbccdd11223344"]))
}
})
t.Run("invalid json", func(t *testing.T) {
tx := &StoreTx{Hash: "h3", DecodedJSON: "not json"}
store.indexByNode(tx)
// Should not panic or add anything
})
}
// --- resolveVersion ---
func TestResolveVersion(t *testing.T) {
old := Version
defer func() { Version = old }()
Version = "v1.2.3"
if resolveVersion() != "v1.2.3" {
t.Error("expected v1.2.3")
}
Version = ""
if resolveVersion() != "unknown" {
t.Error("expected unknown when empty")
}
}
// --- wsOrStatic ---
func TestWsOrStaticNonWebSocket(t *testing.T) {
hub := NewHub()
staticHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("static"))
})
handler := wsOrStatic(hub, staticHandler)
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
if w.Body.String() != "static" {
t.Errorf("expected 'static', got %s", w.Body.String())
}
}
// --- Poller.Start ---
func TestPollerStartStop(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
hub := NewHub()
poller := NewPoller(db, hub, 50*time.Millisecond)
go poller.Start()
time.Sleep(150 * time.Millisecond)
poller.Stop()
}
func TestPollerStartWithStore(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
hub := NewHub()
store := NewPacketStore(db)
store.Load()
poller := NewPoller(db, hub, 50*time.Millisecond)
poller.store = store
go poller.Start()
// Insert new data while poller running
now := time.Now().UTC().Format(time.RFC3339)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type)
VALUES ('FFEE', 'pollerhash12345678', ?, 1, 4)`, now)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES ((SELECT MAX(id) FROM transmissions), 1, 10.0, -92, '[]', ?)`, time.Now().Unix())
time.Sleep(200 * time.Millisecond)
poller.Stop()
}
// --- perfMiddleware slow query path ---
func TestPerfMiddlewareSlowQuery(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
store.Load()
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
// Add a slow handler
router.HandleFunc("/api/test-slow", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(110 * time.Millisecond)
writeJSON(w, map[string]string{"ok": "true"})
}).Methods("GET")
req := httptest.NewRequest("GET", "/api/test-slow", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if len(srv.perfStats.SlowQueries) < 1 {
t.Error("expected slow query to be recorded")
}
}
func TestPerfMiddlewareNonAPIPath(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
router := mux.NewRouter()
srv.RegisterRoutes(router)
// Non-API path should pass through without perf tracking
router.HandleFunc("/not-api", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}).Methods("GET")
initialReqs := srv.perfStats.Requests
req := httptest.NewRequest("GET", "/not-api", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if srv.perfStats.Requests != initialReqs {
t.Error("non-API request should not be tracked")
}
}
// --- writeJSON error path ---
func TestWriteJSONErrorPath(t *testing.T) {
w := httptest.NewRecorder()
// math.Inf cannot be marshaled to JSON — triggers the error path
writeJSON(w, math.Inf(1))
// Should not panic, just log the error
}
// --- GetObserverPacketCounts ---
func TestGetObserverPacketCountsV3(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
counts := db.GetObserverPacketCounts(0)
if len(counts) == 0 {
t.Error("expected some observer counts")
}
}
// --- Additional route fallback tests ---
func TestHandleAnalyticsTopologyNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/analytics/topology", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleAnalyticsDistanceNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/analytics/distance", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleAnalyticsHashSizesNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/analytics/hash-sizes", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleAnalyticsSubpathsNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/analytics/subpaths", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleAnalyticsSubpathDetailNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
t.Run("with hops", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/analytics/subpath-detail?hops=aa,bb", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("missing hops", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/analytics/subpath-detail", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("single hop", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/analytics/subpath-detail?hops=aa", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
}
func TestHandleChannelsNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/channels", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleChannelMessagesNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/channels/test/messages", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandlePacketTimestampsNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
t.Run("with since", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets/timestamps?since=2020-01-01T00:00:00Z", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("missing since", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets/timestamps", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 400 {
t.Fatalf("expected 400, got %d", w.Code)
}
})
}
func TestHandleStatsNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/stats", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleHealthNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/health", 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{}
json.Unmarshal(w.Body.Bytes(), &body)
if body["status"] != "ok" {
t.Errorf("expected status ok, got %v", body["status"])
}
}
// --- buildTransmissionWhere additional coverage ---
func TestBuildTransmissionWhereRFC3339(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
t.Run("RFC3339 since", func(t *testing.T) {
q := PacketQuery{Since: "2020-01-01T00:00:00Z"}
where, args := db.buildTransmissionWhere(q)
if len(where) != 1 {
t.Errorf("expected 1 clause, got %d", len(where))
}
if len(args) != 1 {
t.Errorf("expected 1 arg, got %d", len(args))
}
if !strings.Contains(where[0], "observations") {
t.Error("expected observations subquery for RFC3339 since")
}
})
t.Run("RFC3339 until", func(t *testing.T) {
q := PacketQuery{Until: "2099-01-01T00:00:00Z"}
where, args := db.buildTransmissionWhere(q)
if len(where) != 1 {
t.Errorf("expected 1 clause, got %d", len(where))
}
if len(args) != 1 {
t.Errorf("expected 1 arg, got %d", len(args))
}
})
t.Run("non-RFC3339 since", func(t *testing.T) {
q := PacketQuery{Since: "2020-01-01"}
where, _ := db.buildTransmissionWhere(q)
if len(where) != 1 {
t.Errorf("expected 1 clause, got %d", len(where))
}
if strings.Contains(where[0], "observations") {
t.Error("expected direct first_seen comparison for non-RFC3339")
}
})
t.Run("observer v3", func(t *testing.T) {
q := PacketQuery{Observer: "obs1"}
where, _ := db.buildTransmissionWhere(q)
if len(where) != 1 {
t.Errorf("expected 1 clause, got %d", len(where))
}
if !strings.Contains(where[0], "observer_idx") {
t.Error("expected observer_idx subquery for v3")
}
})
t.Run("region v3", func(t *testing.T) {
q := PacketQuery{Region: "SJC"}
where, _ := db.buildTransmissionWhere(q)
if len(where) != 1 {
t.Errorf("expected 1 clause, got %d", len(where))
}
if !strings.Contains(where[0], "iata") {
t.Error("expected iata subquery for region")
}
})
}
func TestBuildTransmissionWhereV2(t *testing.T) {
db := setupTestDBv2(t)
defer db.Close()
seedV2Data(t, db)
t.Run("observer v2", func(t *testing.T) {
q := PacketQuery{Observer: "obs1"}
where, _ := db.buildTransmissionWhere(q)
if len(where) != 1 {
t.Errorf("expected 1 clause, got %d", len(where))
}
if !strings.Contains(where[0], "observer_id") {
t.Error("expected observer_id subquery for v2")
}
})
t.Run("region v2", func(t *testing.T) {
q := PacketQuery{Region: "SJC"}
where, _ := db.buildTransmissionWhere(q)
if len(where) != 1 {
t.Errorf("expected 1 clause, got %d", len(where))
}
})
}
// --- GetMaxTransmissionID (DB) ---
func TestDBGetMaxTransmissionID(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
maxID := db.GetMaxTransmissionID()
if maxID <= 0 {
t.Errorf("expected > 0, got %d", maxID)
}
}
// --- GetNodeLocations ---
func TestGetNodeLocations(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
locs := db.GetNodeLocations()
if len(locs) == 0 {
t.Error("expected some node locations")
}
pk := strings.ToLower("aabbccdd11223344")
if entry, ok := locs[pk]; ok {
if entry["lat"] == nil {
t.Error("expected non-nil lat")
}
} else {
t.Error("expected node location for test repeater")
}
}
// --- Store edge cases ---
func TestStoreQueryPacketsEdgeCases(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
t.Run("hash filter", func(t *testing.T) {
result := store.QueryPackets(PacketQuery{Hash: "abc123def4567890", Limit: 50, Order: "DESC"})
if result.Total != 1 {
t.Errorf("expected 1, got %d", result.Total)
}
})
t.Run("non-existent hash", func(t *testing.T) {
result := store.QueryPackets(PacketQuery{Hash: "0000000000000000", Limit: 50, Order: "DESC"})
if result.Total != 0 {
t.Errorf("expected 0, got %d", result.Total)
}
})
t.Run("ASC order", func(t *testing.T) {
result := store.QueryPackets(PacketQuery{Limit: 50, Order: "ASC"})
if result.Total < 1 {
t.Error("expected results")
}
})
t.Run("offset beyond end", func(t *testing.T) {
result := store.QueryPackets(PacketQuery{Limit: 50, Offset: 9999, Order: "DESC"})
if len(result.Packets) != 0 {
t.Errorf("expected 0, got %d", len(result.Packets))
}
})
t.Run("node filter with index", func(t *testing.T) {
result := store.QueryPackets(PacketQuery{Node: "aabbccdd11223344", Limit: 50, Order: "DESC"})
if result.Total < 1 {
t.Error("expected >=1")
}
})
t.Run("route filter", func(t *testing.T) {
rt := 1
result := store.QueryPackets(PacketQuery{Route: &rt, Limit: 50, Order: "DESC"})
if result.Total < 1 {
t.Error("expected >=1")
}
})
t.Run("since filter", func(t *testing.T) {
result := store.QueryPackets(PacketQuery{Since: "2020-01-01", Limit: 50, Order: "DESC"})
if result.Total < 1 {
t.Error("expected >=1")
}
})
t.Run("until filter", func(t *testing.T) {
result := store.QueryPackets(PacketQuery{Until: "2099-01-01", Limit: 50, Order: "DESC"})
if result.Total < 1 {
t.Error("expected >=1")
}
})
}
// --- HandlePackets with various options ---
func TestHandlePacketsWithQueryOptions(t *testing.T) {
_, router := setupTestServer(t)
t.Run("with type filter", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets?type=4&limit=10", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("with route filter", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets?route=1&limit=10", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("expand observations", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets?limit=10&expand=observations", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("ASC order", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets?order=asc&limit=10", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
}
// --- handleObservers and handleObserverDetail ---
func TestHandleObserversNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/observers", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleObserverDetailNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/observers/obs1", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
}
func TestHandleObserverAnalyticsNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/observers/obs1/analytics", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 503 {
t.Fatalf("expected 503, got %d: %s", w.Code, w.Body.String())
}
}
// --- HandleTraces ---
func TestHandleTracesNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/traces/abc123def4567890", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
// --- HandleResolveHops ---
func TestHandleResolveHops(t *testing.T) {
_, router := setupTestServer(t)
t.Run("empty hops", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/resolve-hops", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("with hops", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/resolve-hops?hops=aabb,eeff", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
}
// --- HandlePerf ---
func TestHandlePerfNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/perf", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
// --- HandleIATACoords ---
func TestHandleIATACoordsNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/iata-coords", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
// --- Conversion helpers ---
func TestStrOrNil(t *testing.T) {
if strOrNil("") != nil {
t.Error("expected nil")
}
if strOrNil("abc") != "abc" {
t.Error("expected abc")
}
}
func TestIntPtrOrNil(t *testing.T) {
if intPtrOrNil(nil) != nil {
t.Error("expected nil")
}
v := 42
if intPtrOrNil(&v) != 42 {
t.Error("expected 42")
}
}
func TestNullIntPtr(t *testing.T) {
valid := sql.NullInt64{Int64: 7, Valid: true}
p := nullIntPtr(valid)
if p == nil || *p != 7 {
t.Error("expected 7")
}
invalid := sql.NullInt64{Valid: false}
if nullIntPtr(invalid) != nil {
t.Error("expected nil")
}
}
func TestNullStr(t *testing.T) {
valid := sql.NullString{String: "hello", Valid: true}
if nullStr(valid) != "hello" {
t.Error("expected hello")
}
invalid := sql.NullString{Valid: false}
if nullStr(invalid) != nil {
t.Error("expected nil")
}
}
func TestNullStrVal(t *testing.T) {
valid := sql.NullString{String: "test", Valid: true}
if nullStrVal(valid) != "test" {
t.Error("expected test")
}
invalid := sql.NullString{Valid: false}
if nullStrVal(invalid) != "" {
t.Error("expected empty string")
}
}
func TestNullFloat(t *testing.T) {
valid := sql.NullFloat64{Float64: 1.5, Valid: true}
if nullFloat(valid) != 1.5 {
t.Error("expected 1.5")
}
invalid := sql.NullFloat64{Valid: false}
if nullFloat(invalid) != nil {
t.Error("expected nil")
}
}
func TestNullInt(t *testing.T) {
valid := sql.NullInt64{Int64: 99, Valid: true}
if nullInt(valid) != 99 {
t.Error("expected 99")
}
invalid := sql.NullInt64{Valid: false}
if nullInt(invalid) != nil {
t.Error("expected nil")
}
}
// --- resolveCommit ---
func TestResolveCommit(t *testing.T) {
old := Commit
defer func() { Commit = old }()
Commit = "abc123"
if resolveCommit() != "abc123" {
t.Error("expected abc123")
}
Commit = ""
// With no .git-commit file and possibly no git, should return something
result := resolveCommit()
if result == "" {
t.Error("expected non-empty result")
}
}
// --- parsePathJSON ---
func TestParsePathJSON(t *testing.T) {
if parsePathJSON("") != nil {
t.Error("expected nil for empty")
}
if parsePathJSON("[]") != nil {
t.Error("expected nil for []")
}
if parsePathJSON("invalid") != nil {
t.Error("expected nil for invalid")
}
hops := parsePathJSON(`["aa","bb"]`)
if len(hops) != 2 {
t.Errorf("expected 2 hops, got %d", len(hops))
}
}
// --- Store.GetPerfStoreStats & GetCacheStats ---
func TestStorePerfAndCacheStats(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
stats := store.GetPerfStoreStats()
if _, ok := stats["totalLoaded"]; !ok {
t.Error("expected totalLoaded")
}
cacheStats := store.GetCacheStats()
if _, ok := cacheStats["size"]; !ok {
t.Error("expected size")
}
}
// --- enrichObs ---
func TestEnrichObs(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
// Find an observation from the loaded store
var obs *StoreObs
for _, o := range store.byObsID {
obs = o
break
}
if obs == nil {
t.Skip("no observations loaded")
}
enriched := store.enrichObs(obs)
if enriched["observer_id"] == nil {
t.Error("expected observer_id")
}
}
// --- HandleNodeSearch ---
func TestHandleNodeSearch(t *testing.T) {
_, router := setupTestServer(t)
t.Run("with query", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/nodes/search?q=Test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("empty query", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/nodes/search?q=", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
}
// --- HandleNodeDetail ---
func TestHandleNodeDetail(t *testing.T) {
_, router := setupTestServer(t)
t.Run("existing", func(t *testing.T) {
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)
}
})
t.Run("not found", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/nodes/nonexistent12345678", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 404 {
t.Fatalf("expected 404, got %d", w.Code)
}
})
}
// --- HandleNodeHealth ---
func TestHandleNodeHealth(t *testing.T) {
_, router := setupTestServer(t)
t.Run("not found", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/nodes/nonexistent12345678/health", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 404 {
t.Fatalf("expected 404, got %d", w.Code)
}
})
}
// --- HandleNodePaths ---
func TestHandleNodePaths(t *testing.T) {
_, router := setupTestServer(t)
t.Run("existing", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/nodes/aabbccdd11223344/paths", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("not found", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/nodes/nonexistent12345678/paths", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 404 {
t.Fatalf("expected 404, got %d", w.Code)
}
})
}
// --- HandleNodeAnalytics ---
func TestHandleNodeAnalytics(t *testing.T) {
_, router := setupTestServer(t)
t.Run("existing", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/nodes/aabbccdd11223344/analytics?days=7", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("not found", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/nodes/nonexistent/analytics", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 404 {
t.Fatalf("expected 404, got %d", w.Code)
}
})
t.Run("days bounds", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/nodes/aabbccdd11223344/analytics?days=0", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("days max", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/nodes/aabbccdd11223344/analytics?days=999", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
}
// --- HandleNetworkStatus ---
func TestHandleNetworkStatus(t *testing.T) {
_, router := setupTestServer(t)
req := httptest.NewRequest("GET", "/api/nodes/network-status", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
// --- HandleConfigEndpoints ---
func TestHandleConfigEndpoints(t *testing.T) {
_, router := setupTestServer(t)
endpoints := []string{
"/api/config/cache",
"/api/config/client",
"/api/config/regions",
"/api/config/theme",
"/api/config/map",
}
for _, ep := range endpoints {
t.Run(ep, func(t *testing.T) {
req := httptest.NewRequest("GET", ep, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d for %s", w.Code, ep)
}
})
}
}
// --- HandleAudioLabBuckets ---
func TestHandleAudioLabBuckets(t *testing.T) {
_, router := setupTestServer(t)
req := httptest.NewRequest("GET", "/api/audio-lab/buckets", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// May return 200 or 404 depending on implementation
if w.Code != 200 {
// Audio lab might not be fully implemented — just verify it doesn't crash
}
}
// --- txToMap ---
func TestTxToMap(t *testing.T) {
snr := 10.5
rssi := -90.0
pt := 4
rt := 1
tx := &StoreTx{
ID: 1,
RawHex: "AABB",
Hash: "abc123",
FirstSeen: "2025-01-01",
RouteType: &rt,
PayloadType: &pt,
DecodedJSON: `{"type":"ADVERT"}`,
ObservationCount: 2,
ObserverID: "obs1",
ObserverName: "Obs One",
SNR: &snr,
RSSI: &rssi,
PathJSON: `["aa"]`,
Direction: "RX",
}
m := txToMap(tx)
if m["id"] != 1 {
t.Error("expected id 1")
}
if m["hash"] != "abc123" {
t.Error("expected hash abc123")
}
if m["snr"] != 10.5 {
t.Error("expected snr 10.5")
}
}
// --- filterTxSlice ---
func TestFilterTxSlice(t *testing.T) {
txs := []*StoreTx{
{ID: 1, Hash: "a"},
{ID: 2, Hash: "b"},
{ID: 3, Hash: "a"},
}
result := filterTxSlice(txs, func(tx *StoreTx) bool {
return tx.Hash == "a"
})
if len(result) != 2 {
t.Errorf("expected 2, got %d", len(result))
}
}
// --- GetTimestamps ---
func TestStoreGetTimestamps(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
ts := store.GetTimestamps("2000-01-01")
if len(ts) < 1 {
t.Error("expected >=1 timestamps")
}
}
// Helper
func intPtr(v int) *int {
return &v
}
// setupRichTestDB creates a test DB with richer data including paths, multiple observers, channel data.
func setupRichTestDB(t *testing.T) *DB {
t.Helper()
db := setupTestDB(t)
now := time.Now().UTC()
recent := now.Add(-1 * time.Hour).Format(time.RFC3339)
yesterday := now.Add(-24 * time.Hour).Format(time.RFC3339)
recentEpoch := now.Add(-1 * time.Hour).Unix()
yesterdayEpoch := now.Add(-24 * time.Hour).Unix()
seedTestData(t, db)
// Add advert packet with raw_hex that has valid header + path bytes for hash size parsing
// route_type 1 = FLOOD, path byte at position 1 (hex index 2..3)
// header: 0x01 (route_type=1), path byte: 0x40 (hashSize bits=01 → size 2)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('0140aabbccdd', 'hash_with_path_01', ?, 1, 4, '{"pubKey":"aabbccdd11223344","name":"TestRepeater","type":"ADVERT"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (3, 1, 10.0, -91, '["aabb","ccdd"]', ?)`, recentEpoch)
// Another advert with 3-byte hash size: header 0x01, path byte 0x80 (bits=10 → size 3)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('0180eeff0011', 'hash_with_path_02', ?, 1, 4, '{"pubKey":"eeff00112233aabb","name":"TestCompanion","type":"ADVERT"}')`, yesterday)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (4, 2, 8.5, -94, '["eeff","0011","2233"]', ?)`, yesterdayEpoch)
// Another channel message with different sender for analytics
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('CC01', 'chan_msg_hash_001', ?, 1, 5, '{"type":"CHAN","channel":"#test","text":"User2: Another msg","sender":"User2","channelHash":"abc123"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (5, 1, 14.0, -88, '["aa"]', ?)`, recentEpoch)
return db
}
// --- Store-backed analytics tests ---
func TestStoreGetBulkHealthWithStore(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
results := store.GetBulkHealth(50, "")
if len(results) == 0 {
t.Error("expected bulk health results")
}
// Check that results have expected structure
for _, r := range results {
if _, ok := r["public_key"]; !ok {
t.Error("expected public_key field")
}
if _, ok := r["stats"]; !ok {
t.Error("expected stats field")
}
}
t.Run("with region filter", func(t *testing.T) {
results := store.GetBulkHealth(50, "SJC")
_ = results
})
}
func TestStoreGetAnalyticsHashSizes(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
result := store.GetAnalyticsHashSizes("")
if result["total"] == nil {
t.Error("expected total field")
}
dist, ok := result["distribution"].(map[string]int)
if !ok {
t.Error("expected distribution map")
}
_ = dist
t.Run("with region", func(t *testing.T) {
r := store.GetAnalyticsHashSizes("SJC")
_ = r
})
}
func TestStoreGetAnalyticsSubpaths(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
result := store.GetAnalyticsSubpaths("", 2, 8, 100)
if _, ok := result["subpaths"]; !ok {
t.Error("expected subpaths field")
}
t.Run("with region", func(t *testing.T) {
r := store.GetAnalyticsSubpaths("SJC", 2, 4, 50)
_ = r
})
}
func TestSubpathPrecomputedIndex(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
// After Load(), the precomputed index must be populated.
if len(store.spIndex) == 0 {
t.Fatal("expected spIndex to be populated after Load()")
}
if store.spTotalPaths == 0 {
t.Fatal("expected spTotalPaths > 0 after Load()")
}
// The rich test DB has paths ["aa","bb"], ["aabb","ccdd"], and
// ["eeff","0011","2233"]. That yields 5 unique raw subpaths.
expectedRaw := map[string]int{
"aa,bb": 1,
"aabb,ccdd": 1,
"eeff,0011": 1,
"0011,2233": 1,
"eeff,0011,2233": 1,
}
for key, want := range expectedRaw {
got, ok := store.spIndex[key]
if !ok {
t.Errorf("expected spIndex[%q] to exist", key)
} else if got != want {
t.Errorf("spIndex[%q] = %d, want %d", key, got, want)
}
}
if store.spTotalPaths != 3 {
t.Errorf("spTotalPaths = %d, want 3", store.spTotalPaths)
}
// Fast-path (no region) and slow-path (with region) must return the
// same shape.
fast := store.GetAnalyticsSubpaths("", 2, 8, 100)
slow := store.GetAnalyticsSubpaths("SJC", 2, 4, 50)
for _, r := range []map[string]interface{}{fast, slow} {
if _, ok := r["subpaths"]; !ok {
t.Error("missing subpaths in result")
}
if _, ok := r["totalPaths"]; !ok {
t.Error("missing totalPaths in result")
}
}
// Verify fast path totalPaths matches index.
if tp, ok := fast["totalPaths"].(int); ok && tp != store.spTotalPaths {
t.Errorf("fast totalPaths=%d, spTotalPaths=%d", tp, store.spTotalPaths)
}
}
func TestStoreGetAnalyticsRFCacheHit(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
// First call — cache miss
result1 := store.GetAnalyticsRF("")
if result1["totalPackets"] == nil {
t.Error("expected totalPackets")
}
// Second call — should hit cache
result2 := store.GetAnalyticsRF("")
if result2["totalPackets"] == nil {
t.Error("expected cached totalPackets")
}
// Verify cache hit was recorded
stats := store.GetCacheStats()
hits, _ := stats["hits"].(int64)
if hits < 1 {
t.Error("expected at least 1 cache hit")
}
}
func TestStoreGetAnalyticsTopology(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
result := store.GetAnalyticsTopology("")
if result == nil {
t.Error("expected non-nil result")
}
// #155: uniqueNodes must match DB 7-day active count, not hop resolution
stats, err := db.GetStats()
if err != nil {
t.Fatalf("GetStats failed: %v", err)
}
un, ok := result["uniqueNodes"].(int)
if !ok {
t.Fatalf("uniqueNodes is not int: %T", result["uniqueNodes"])
}
if un != stats.TotalNodes {
t.Errorf("uniqueNodes=%d should match stats totalNodes=%d", un, stats.TotalNodes)
}
t.Run("with region", func(t *testing.T) {
r := store.GetAnalyticsTopology("SJC")
_ = r
})
}
func TestStoreGetAnalyticsChannels(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
result := store.GetAnalyticsChannels("")
if _, ok := result["activeChannels"]; !ok {
t.Error("expected activeChannels")
}
if _, ok := result["topSenders"]; !ok {
t.Error("expected topSenders")
}
if _, ok := result["channelTimeline"]; !ok {
t.Error("expected channelTimeline")
}
t.Run("with region", func(t *testing.T) {
r := store.GetAnalyticsChannels("SJC")
_ = r
})
}
// Regression test for #154: channelHash is a number in decoded JSON from decoder.js,
// not a string. The Go struct must handle both types correctly.
func TestStoreGetAnalyticsChannelsNumericHash(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
recent := time.Now().Add(-1 * time.Hour).Format(time.RFC3339)
recentEpoch := time.Now().Add(-1 * time.Hour).Unix()
// Insert GRP_TXT packets with numeric channelHash (matches decoder.js output)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('DD01', 'grp_num_hash_1', ?, 1, 5, '{"type":"GRP_TXT","channelHash":97,"channelHashHex":"61","decryptionStatus":"no_key"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (4, 1, 10.0, -90, '[]', ?)`, recentEpoch)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('DD02', 'grp_num_hash_2', ?, 1, 5, '{"type":"GRP_TXT","channelHash":42,"channelHashHex":"2A","decryptionStatus":"no_key"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (5, 1, 10.0, -90, '[]', ?)`, recentEpoch)
// Also a decrypted CHAN with numeric channelHash
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('DD03', 'chan_num_hash_3', ?, 1, 5, '{"type":"CHAN","channel":"general","channelHash":97,"channelHashHex":"61","text":"hello","sender":"Alice"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (6, 1, 12.0, -88, '[]', ?)`, recentEpoch)
store := NewPacketStore(db)
store.Load()
result := store.GetAnalyticsChannels("")
channels := result["channels"].([]map[string]interface{})
if len(channels) < 2 {
t.Errorf("expected at least 2 channels (hash 97 + hash 42), got %d", len(channels))
}
// Verify the numeric-hash channels we inserted have proper hashes (not "?")
found97 := false
found42 := false
for _, ch := range channels {
if ch["hash"] == "97" {
found97 = true
}
if ch["hash"] == "42" {
found42 = true
}
}
if !found97 {
t.Error("expected to find channel with hash '97' (numeric channelHash parsing)")
}
if !found42 {
t.Error("expected to find channel with hash '42' (numeric channelHash parsing)")
}
// Verify the decrypted CHAN channel has the correct name
foundGeneral := false
for _, ch := range channels {
if ch["name"] == "general" {
foundGeneral = true
if ch["hash"] != "97" {
t.Errorf("expected hash '97' for general channel, got %v", ch["hash"])
}
}
}
if !foundGeneral {
t.Error("expected to find channel named 'general'")
}
}
func TestStoreGetAnalyticsDistance(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
result := store.GetAnalyticsDistance("")
if result == nil {
t.Error("expected non-nil result")
}
t.Run("with region", func(t *testing.T) {
r := store.GetAnalyticsDistance("SJC")
_ = r
})
}
func TestStoreGetSubpathDetail(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
result := store.GetSubpathDetail([]string{"aabb", "ccdd"})
if result == nil {
t.Error("expected non-nil result")
}
if _, ok := result["hops"]; !ok {
t.Error("expected hops field")
}
}
// --- Route handlers with store for analytics ---
func TestHandleAnalyticsRFWithStore(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
store.Load()
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
t.Run("basic", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/analytics/rf", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("with region", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/analytics/rf?region=SJC", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
}
func TestHandleBulkHealthWithStore(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
store.Load()
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
req := httptest.NewRequest("GET", "/api/nodes/bulk-health?limit=50&region=SJC", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleAnalyticsSubpathsWithStore(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
store.Load()
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
req := httptest.NewRequest("GET", "/api/analytics/subpaths?minLen=2&maxLen=4&limit=50", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleAnalyticsSubpathDetailWithStore(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
store.Load()
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
req := httptest.NewRequest("GET", "/api/analytics/subpath-detail?hops=aabb,ccdd", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleAnalyticsDistanceWithStore(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
store.Load()
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
req := httptest.NewRequest("GET", "/api/analytics/distance", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleAnalyticsHashSizesWithStore(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
store.Load()
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
req := httptest.NewRequest("GET", "/api/analytics/hash-sizes", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleAnalyticsTopologyWithStore(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
store.Load()
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
req := httptest.NewRequest("GET", "/api/analytics/topology", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
func TestHandleAnalyticsChannelsWithStore(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
store.Load()
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
req := httptest.NewRequest("GET", "/api/analytics/channels", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
// --- GetChannelMessages more paths ---
func TestGetChannelMessagesRichData(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
messages, total := store.GetChannelMessages("#test", 100, 0)
if total < 2 {
t.Errorf("expected >=2 messages for #test with rich data, got %d", total)
}
// Verify message fields
for _, msg := range messages {
if _, ok := msg["sender"]; !ok {
t.Error("expected sender field")
}
if _, ok := msg["hops"]; !ok {
t.Error("expected hops field")
}
}
}
// --- handleObservers with actual data ---
func TestHandleObserversWithData(t *testing.T) {
_, router := setupTestServer(t)
req := httptest.NewRequest("GET", "/api/observers", 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{}
json.Unmarshal(w.Body.Bytes(), &body)
observers, ok := body["observers"].([]interface{})
if !ok || len(observers) == 0 {
t.Error("expected non-empty observers")
}
}
// --- handleChannelMessages with store ---
func TestHandleChannelMessagesWithStore(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
store.Load()
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
req := httptest.NewRequest("GET", "/api/channels/%23test/messages?limit=10", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
// --- handleChannels with store ---
func TestHandleChannelsWithStore(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
store.Load()
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
req := httptest.NewRequest("GET", "/api/channels", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
// --- Traces via store path ---
func TestHandleTracesWithStore(t *testing.T) {
_, router := setupTestServer(t)
req := httptest.NewRequest("GET", "/api/traces/abc123def4567890", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
// --- Store.GetStoreStats ---
func TestStoreGetStoreStats(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
stats, err := store.GetStoreStats()
if err != nil {
t.Fatal(err)
}
if stats.TotalTransmissions < 1 {
t.Error("expected transmissions > 0")
}
}
// --- Store.QueryGroupedPackets ---
func TestStoreQueryGroupedPackets(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
result := store.QueryGroupedPackets(PacketQuery{Limit: 50, Order: "DESC"})
if result.Total < 1 {
t.Error("expected >=1 grouped packets")
}
}
// --- Store.GetPacketByHash / GetPacketByID / GetTransmissionByID ---
func TestStoreGetPacketByHash(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
pkt := store.GetPacketByHash("abc123def4567890")
if pkt == nil {
t.Fatal("expected packet")
}
if pkt["hash"] != "abc123def4567890" {
t.Errorf("wrong hash: %v", pkt["hash"])
}
t.Run("not found", func(t *testing.T) {
pkt := store.GetPacketByHash("0000000000000000")
if pkt != nil {
t.Error("expected nil for not found")
}
})
}
// --- Coverage gap-filling tests ---
func TestResolvePayloadTypeNameUnknown(t *testing.T) {
// nil → UNKNOWN
if got := resolvePayloadTypeName(nil); got != "UNKNOWN" {
t.Errorf("expected UNKNOWN for nil, got %s", got)
}
// known type
pt4 := 4
if got := resolvePayloadTypeName(&pt4); got != "ADVERT" {
t.Errorf("expected ADVERT, got %s", got)
}
// unknown type → UNK(N) format
pt99 := 99
if got := resolvePayloadTypeName(&pt99); got != "UNK(99)" {
t.Errorf("expected UNK(99), got %s", got)
}
}
func TestCacheHitTopology(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
// First call — cache miss
r1 := store.GetAnalyticsTopology("")
if r1 == nil {
t.Fatal("expected topology result")
}
// Second call — cache hit
r2 := store.GetAnalyticsTopology("")
if r2 == nil {
t.Fatal("expected cached topology result")
}
stats := store.GetCacheStats()
hits := stats["hits"].(int64)
if hits < 1 {
t.Errorf("expected cache hit, got %d hits", hits)
}
}
func TestCacheHitHashSizes(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
r1 := store.GetAnalyticsHashSizes("")
if r1 == nil {
t.Fatal("expected hash sizes result")
}
r2 := store.GetAnalyticsHashSizes("")
if r2 == nil {
t.Fatal("expected cached hash sizes result")
}
stats := store.GetCacheStats()
hits := stats["hits"].(int64)
if hits < 1 {
t.Errorf("expected cache hit, got %d", hits)
}
}
func TestCacheHitChannels(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
r1 := store.GetAnalyticsChannels("")
if r1 == nil {
t.Fatal("expected channels result")
}
r2 := store.GetAnalyticsChannels("")
if r2 == nil {
t.Fatal("expected cached channels result")
}
stats := store.GetCacheStats()
hits := stats["hits"].(int64)
if hits < 1 {
t.Errorf("expected cache hit, got %d", hits)
}
}
func TestGetChannelMessagesEdgeCases(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
// Channel not found — empty result
msgs, total := store.GetChannelMessages("nonexistent_channel", 10, 0)
if total != 0 {
t.Errorf("expected 0 total for nonexistent channel, got %d", total)
}
if len(msgs) != 0 {
t.Errorf("expected empty msgs, got %d", len(msgs))
}
// Default limit (0 → 100)
msgs, _ = store.GetChannelMessages("#test", 0, 0)
_ = msgs // just exercises the default limit path
// Offset beyond range
msgs, total = store.GetChannelMessages("#test", 10, 9999)
if len(msgs) != 0 {
t.Errorf("expected empty msgs for large offset, got %d", len(msgs))
}
if total == 0 {
t.Error("total should be > 0 even with large offset")
}
// Negative offset
msgs, _ = store.GetChannelMessages("#test", 10, -5)
_ = msgs // exercises the start < 0 path
}
func TestFilterPacketsEmptyRegion(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
// Region with no observers → empty result
results := store.QueryPackets(PacketQuery{Region: "NONEXISTENT", Limit: 100})
if results.Total != 0 {
t.Errorf("expected 0 results for nonexistent region, got %d", results.Total)
}
}
func TestFilterPacketsSinceUntil(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
// Since far future → empty
results := store.QueryPackets(PacketQuery{Since: "2099-01-01T00:00:00Z", Limit: 100})
if results.Total != 0 {
t.Errorf("expected 0 results for far future since, got %d", results.Total)
}
// Until far past → empty
results = store.QueryPackets(PacketQuery{Until: "2000-01-01T00:00:00Z", Limit: 100})
if results.Total != 0 {
t.Errorf("expected 0 results for far past until, got %d", results.Total)
}
// Route filter
rt := 1
results = store.QueryPackets(PacketQuery{Route: &rt, Limit: 100})
if results.Total == 0 {
t.Error("expected results for route_type=1 filter")
}
}
func TestFilterPacketsHashOnly(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
// Single hash fast-path — found
results := store.QueryPackets(PacketQuery{Hash: "abc123def4567890", Limit: 100})
if results.Total != 1 {
t.Errorf("expected 1 result for known hash, got %d", results.Total)
}
// Single hash fast-path — not found
results = store.QueryPackets(PacketQuery{Hash: "0000000000000000", Limit: 100})
if results.Total != 0 {
t.Errorf("expected 0 results for unknown hash, got %d", results.Total)
}
}
func TestFilterPacketsObserverWithType(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
// Observer + type filter (takes non-indexed path)
pt := 4
results := store.QueryPackets(PacketQuery{Observer: "obs1", Type: &pt, Limit: 100})
_ = results // exercises the combined observer+type filter path
}
func TestFilterPacketsNodeFilter(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
// Node filter — exercises DecodedJSON containment check
results := store.QueryPackets(PacketQuery{Node: "aabbccdd11223344", Limit: 100})
if results.Total == 0 {
t.Error("expected results for node filter")
}
// Node filter with hash combined
results = store.QueryPackets(PacketQuery{Node: "aabbccdd11223344", Hash: "abc123def4567890", Limit: 100})
_ = results
}
func TestGetNodeHashSizeInfoEdgeCases(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
now := time.Now().UTC()
recent := now.Add(-1 * time.Hour).Format(time.RFC3339)
recentEpoch := now.Add(-1 * time.Hour).Unix()
// Observers
db.conn.Exec(`INSERT INTO observers (id, name, iata, last_seen, first_seen, packet_count)
VALUES ('obs1', 'Obs', 'SJC', ?, '2026-01-01T00:00:00Z', 10)`, recent)
// Adverts with various edge cases
// 1. Valid advert with pubKey
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('0140aabbccdd', 'hs_valid_1', ?, 1, 4, '{"pubKey":"aabbccdd11223344","name":"NodeA","type":"ADVERT"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (1, 1, 10.0, -90, '[]', ?)`, recentEpoch)
// 2. Short raw_hex (< 4 chars)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('01', 'hs_short_hex', ?, 1, 4, '{"pubKey":"eeff00112233aabb","name":"NodeB","type":"ADVERT"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (2, 1, 10.0, -90, '[]', ?)`, recentEpoch)
// 3. Invalid hex in path byte position
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('01GGHHII', 'hs_bad_hex', ?, 1, 4, '{"pubKey":"1122334455667788","name":"NodeC","type":"ADVERT"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (3, 1, 10.0, -90, '[]', ?)`, recentEpoch)
// 4. Invalid JSON
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('0140aabb', 'hs_bad_json', ?, 1, 4, 'not-json')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (4, 1, 10.0, -90, '[]', ?)`, recentEpoch)
// 5. JSON with public_key field instead of pubKey
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('0180eeff', 'hs_alt_key', ?, 1, 4, '{"public_key":"aabbccdd11223344","name":"NodeA","type":"ADVERT"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (5, 1, 10.0, -90, '[]', ?)`, recentEpoch)
// 6. JSON with no pubKey at all
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('01C0ffee', 'hs_no_pk', ?, 1, 4, '{"name":"NodeZ","type":"ADVERT"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (6, 1, 10.0, -90, '[]', ?)`, recentEpoch)
// 7. Empty decoded_json
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('0140bbcc', 'hs_empty_json', ?, 1, 4, '')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (7, 1, 10.0, -90, '[]', ?)`, recentEpoch)
// 8-10. Multiple adverts for same node with different hash sizes (flip-flop test)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('0140dd01', 'hs_flip_1', ?, 1, 4, '{"pubKey":"ffff000011112222","name":"Flipper","type":"ADVERT"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (8, 1, 10.0, -90, '[]', ?)`, recentEpoch)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('0180dd02', 'hs_flip_2', ?, 1, 4, '{"pubKey":"ffff000011112222","name":"Flipper","type":"ADVERT"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (9, 1, 10.0, -90, '[]', ?)`, recentEpoch)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('0140dd03', 'hs_flip_3', ?, 1, 4, '{"pubKey":"ffff000011112222","name":"Flipper","type":"ADVERT"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (10, 1, 10.0, -90, '[]', ?)`, recentEpoch)
store := NewPacketStore(db)
store.Load()
info := store.GetNodeHashSizeInfo()
// Valid node should be present
if _, ok := info["aabbccdd11223344"]; !ok {
t.Error("expected aabbccdd11223344 in hash size info")
}
// Flipper should have inconsistent flag (2→3→2 = 2 transitions, 2 unique sizes, 3 obs)
if flipper, ok := info["ffff000011112222"]; ok {
if len(flipper.AllSizes) < 2 {
t.Errorf("expected 2+ unique sizes for flipper, got %d", len(flipper.AllSizes))
}
if !flipper.Inconsistent {
t.Error("expected Inconsistent=true for flip-flop node")
}
} else {
t.Error("expected ffff000011112222 in hash size info")
}
// Bad entries (short hex, bad hex, bad json, no pk) should not corrupt results
if _, ok := info["eeff00112233aabb"]; ok {
t.Error("short raw_hex node should not be in results")
}
if _, ok := info["1122334455667788"]; ok {
t.Error("bad hex node should not be in results")
}
}
func TestHandleResolveHopsEdgeCases(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
router := mux.NewRouter()
srv.RegisterRoutes(router)
// Empty hops param
req := httptest.NewRequest("GET", "/api/resolve-hops", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
var body map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &body)
resolved := body["resolved"].(map[string]interface{})
if len(resolved) != 0 {
t.Errorf("expected empty resolved for empty hops, got %d", len(resolved))
}
// Multiple hops with empty string included
req = httptest.NewRequest("GET", "/api/resolve-hops?hops=aabb,,eeff", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
json.Unmarshal(w.Body.Bytes(), &body)
resolved = body["resolved"].(map[string]interface{})
// Empty string should be skipped
if _, ok := resolved[""]; ok {
t.Error("empty hop should be skipped")
}
// Nonexistent prefix — zero candidates
req = httptest.NewRequest("GET", "/api/resolve-hops?hops=nonexistent_prefix_xyz", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestHandleObserversError(t *testing.T) {
// Use a closed DB to trigger an error from GetObservers
db := setupTestDB(t)
seedTestData(t, db)
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
router := mux.NewRouter()
srv.RegisterRoutes(router)
db.Close() // force error after routes registered
req := httptest.NewRequest("GET", "/api/observers", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 500 {
t.Errorf("expected 500 for closed DB, got %d", w.Code)
}
}
func TestHandleAnalyticsChannelsDBFallback(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
// Server with NO store — takes DB fallback path
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
router := mux.NewRouter()
srv.RegisterRoutes(router)
req := httptest.NewRequest("GET", "/api/analytics/channels", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
var body map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &body)
if _, ok := body["activeChannels"]; !ok {
t.Error("expected activeChannels in DB-fallback response")
}
if _, ok := body["channels"]; !ok {
t.Error("expected channels in DB-fallback response")
}
}
func TestGetChannelMessagesDedupeRepeats(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
now := time.Now().UTC()
recent := now.Add(-1 * time.Hour).Format(time.RFC3339)
recentEpoch := now.Add(-1 * time.Hour).Unix()
db.conn.Exec(`INSERT INTO observers (id, name, iata, last_seen, first_seen, packet_count)
VALUES ('obs1', 'Obs1', 'SJC', ?, '2026-01-01T00:00:00Z', 10)`, recent)
db.conn.Exec(`INSERT INTO observers (id, name, iata, last_seen, first_seen, packet_count)
VALUES ('obs2', 'Obs2', 'LAX', ?, '2026-01-01T00:00:00Z', 10)`, recent)
// Insert two copies of same CHAN message (same hash, different observers)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('CC01', 'dedup_chan_1', ?, 1, 5, '{"type":"CHAN","channel":"#general","text":"Alice: hello","sender":"Alice"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (1, 1, 12.0, -88, '["aa"]', ?)`, recentEpoch)
// Same sender + hash → different observation (simulates dedup)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('CC02', 'dedup_chan_1', ?, 1, 5, '{"type":"CHAN","channel":"#general","text":"Alice: hello","sender":"Alice"}')`, recent)
// Note: won't load due to UNIQUE constraint on hash → tests the code path with single tx having multiple obs
// Second different message
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('CC03', 'dedup_chan_2', ?, 1, 5, '{"type":"CHAN","channel":"#general","text":"Bob: world","sender":"Bob"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (2, 2, 10.0, -90, '["bb"]', ?)`, recentEpoch)
// GRP_TXT (not CHAN) — should be skipped by GetChannelMessages
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('DD01', 'grp_msg_hash_1', ?, 1, 5, '{"type":"GRP_TXT","channelHash":"42","text":"encrypted"}')`, recent)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (3, 1, 10.0, -90, '[]', ?)`, recentEpoch)
store := NewPacketStore(db)
store.Load()
msgs, total := store.GetChannelMessages("#general", 10, 0)
if total == 0 {
t.Error("expected messages for #general")
}
// Check message structure
for _, msg := range msgs {
if _, ok := msg["sender"]; !ok {
t.Error("expected sender field")
}
if _, ok := msg["text"]; !ok {
t.Error("expected text field")
}
if _, ok := msg["observers"]; !ok {
t.Error("expected observers field")
}
}
}
func TestTransmissionsForObserverFromSlice(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
// Test with from=nil (index path) — for non-existent observer
result := store.transmissionsForObserver("nonexistent_obs", nil)
if len(result) != 0 {
t.Errorf("expected nil/empty for nonexistent observer, got %d", len(result))
}
// Test with from=non-nil slice (filter path)
allPackets := store.packets
result = store.transmissionsForObserver("obs1", allPackets)
if len(result) == 0 {
t.Error("expected results for obs1 from filter path")
}
}
func TestGetPerfStoreStatsPublicKeyField(t *testing.T) {
db := setupRichTestDB(t)
defer db.Close()
store := NewPacketStore(db)
store.Load()
stats := store.GetPerfStoreStats()
indexes := stats["indexes"].(map[string]interface{})
// advertByObserver should count distinct pubkeys from advert packets
aboc := indexes["advertByObserver"].(int)
if aboc == 0 {
t.Error("expected advertByObserver > 0 for rich test DB")
}
}
func TestHandleAudioLabBucketsQueryError(t *testing.T) {
// Use closed DB to trigger query error
db := setupTestDB(t)
seedTestData(t, db)
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
router := mux.NewRouter()
srv.RegisterRoutes(router)
db.Close()
req := httptest.NewRequest("GET", "/api/audio-lab/buckets", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("expected 200 (empty buckets on error), got %d", w.Code)
}
var body map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &body)
buckets := body["buckets"].(map[string]interface{})
if len(buckets) != 0 {
t.Errorf("expected empty buckets on query error, got %d", len(buckets))
}
}
func TestStoreGetTransmissionByID(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
pkt := store.GetTransmissionByID(1)
if pkt == nil {
t.Fatal("expected packet")
}
t.Run("not found", func(t *testing.T) {
pkt := store.GetTransmissionByID(99999)
if pkt != nil {
t.Error("expected nil")
}
})
}
func TestStoreGetPacketByID(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
// Get an observation ID from the store
var obsID int
for id := range store.byObsID {
obsID = id
break
}
if obsID == 0 {
t.Skip("no observations")
}
pkt := store.GetPacketByID(obsID)
if pkt == nil {
t.Fatal("expected packet")
}
t.Run("not found", func(t *testing.T) {
pkt := store.GetPacketByID(99999)
if pkt != nil {
t.Error("expected nil")
}
})
}
// --- Store.GetObservationsForHash ---
func TestStoreGetObservationsForHash(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
obs := store.GetObservationsForHash("abc123def4567890")
if len(obs) < 1 {
t.Error("expected >=1 observation")
}
t.Run("not found", func(t *testing.T) {
obs := store.GetObservationsForHash("0000000000000000")
if len(obs) != 0 {
t.Errorf("expected 0, got %d", len(obs))
}
})
}
// --- Store.GetNewTransmissionsSince ---
func TestStoreGetNewTransmissionsSince(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
txs, err := db.GetNewTransmissionsSince(0, 100)
if err != nil {
t.Fatal(err)
}
if len(txs) < 1 {
t.Error("expected >=1 transmission")
}
}
// --- HandlePacketDetail with store (by hash, by tx ID, by obs ID) ---
func TestHandlePacketDetailWithStoreAllPaths(t *testing.T) {
_, router := setupTestServer(t)
t.Run("by hash", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets/abc123def4567890", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var body map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &body)
if body["observations"] == nil {
t.Error("expected observations")
}
})
t.Run("by tx ID", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets/1", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
})
t.Run("not found ID", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/packets/999999", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 404 {
t.Fatalf("expected 404, got %d", w.Code)
}
})
}
// --- Additional DB function coverage ---
func TestDBGetNewTransmissionsSince(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
txs, err := db.GetNewTransmissionsSince(0, 100)
if err != nil {
t.Fatal(err)
}
if len(txs) < 1 {
t.Error("expected >=1 transmissions")
}
}
func TestDBGetNetworkStatus(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
cfg := &Config{}
ht := cfg.GetHealthThresholds()
result, err := db.GetNetworkStatus(ht)
if err != nil {
t.Fatal(err)
}
if result == nil {
t.Error("expected non-nil result")
}
}
func TestDBGetObserverByID(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
obs, err := db.GetObserverByID("obs1")
if err != nil {
t.Fatal(err)
}
if obs == nil {
t.Error("expected non-nil observer")
}
if obs.ID != "obs1" {
t.Errorf("expected obs1, got %s", obs.ID)
}
t.Run("not found", func(t *testing.T) {
obs, err := db.GetObserverByID("nonexistent")
if err == nil && obs != nil {
t.Error("expected nil observer for nonexistent ID")
}
// Some implementations return (nil, err) — that's fine too
})
}
func TestDBGetTraces(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
traces, err := db.GetTraces("abc123def4567890")
if err != nil {
t.Fatal(err)
}
_ = traces
}
// --- DB queries with different filter combos ---
func TestDBQueryPacketsAllFilters(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
pt := 4
rt := 1
result, err := db.QueryPackets(PacketQuery{
Limit: 50,
Type: &pt,
Route: &rt,
Observer: "obs1",
Hash: "abc123def4567890",
Since: "2020-01-01",
Until: "2099-01-01",
Region: "SJC",
Node: "TestRepeater",
Order: "ASC",
})
if err != nil {
t.Fatal(err)
}
_ = result
}
// --- IngestNewFromDB dedup path ---
func TestIngestNewFromDBDuplicateObs(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
initialMax := store.MaxTransmissionID()
// Insert new transmission with same hash as existing (should merge into existing tx)
now := time.Now().UTC().Format(time.RFC3339)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('AABB', 'dedup_test_hash_01', ?, 1, 4, '{"pubKey":"aabbccdd11223344","type":"ADVERT"}')`, now)
newTxID := 0
db.conn.QueryRow("SELECT MAX(id) FROM transmissions").Scan(&newTxID)
// Add observation
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (?, 1, 11.0, -89, '["dd"]', ?)`, newTxID, time.Now().Unix())
// Add duplicate observation (same observer_id + path_json)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (?, 1, 11.0, -89, '["dd"]', ?)`, newTxID, time.Now().Unix())
_, newMax := store.IngestNewFromDB(initialMax, 100)
if newMax <= initialMax {
t.Errorf("expected newMax > %d, got %d", initialMax, newMax)
}
}
// --- IngestNewObservations (fixes #174) ---
func TestIngestNewObservations(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
// Get initial observation count for transmission 1 (hash abc123def4567890)
initialTx := store.byHash["abc123def4567890"]
if initialTx == nil {
t.Fatal("expected to find transmission abc123def4567890 in store")
}
initialObsCount := initialTx.ObservationCount
if initialObsCount != 2 {
t.Fatalf("expected 2 initial observations, got %d", initialObsCount)
}
// Record the max obs ID after initial load
maxObsID := db.GetMaxObservationID()
// Simulate a new observation arriving for the existing transmission AFTER
// the poller has already advanced past its transmission ID
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (1, 2, 5.0, -100, '["aa","bb","cc"]', ?)`, time.Now().Unix())
// Verify IngestNewFromDB does NOT pick up the new observation (tx id hasn't changed)
txMax := store.MaxTransmissionID()
_, newTxMax := store.IngestNewFromDB(txMax, 100)
if initialTx.ObservationCount != initialObsCount {
t.Errorf("IngestNewFromDB should not have changed obs count, was %d now %d",
initialObsCount, initialTx.ObservationCount)
}
_ = newTxMax
// IngestNewObservations should pick it up
newObsMax := store.IngestNewObservations(maxObsID, 500)
if newObsMax <= maxObsID {
t.Errorf("expected newObsMax > %d, got %d", maxObsID, newObsMax)
}
if initialTx.ObservationCount != initialObsCount+1 {
t.Errorf("expected obs count %d, got %d", initialObsCount+1, initialTx.ObservationCount)
}
if len(initialTx.Observations) != initialObsCount+1 {
t.Errorf("expected %d observations slice len, got %d", initialObsCount+1, len(initialTx.Observations))
}
// Best observation should have been re-picked (new obs has longer path)
if initialTx.PathJSON != `["aa","bb","cc"]` {
t.Errorf("expected best path to be updated to longer path, got %s", initialTx.PathJSON)
}
t.Run("no new observations", func(t *testing.T) {
max := store.IngestNewObservations(newObsMax, 500)
if max != newObsMax {
t.Errorf("expected same max %d, got %d", newObsMax, max)
}
})
t.Run("dedup by observer+path", func(t *testing.T) {
// Insert duplicate observation (same observer + path as existing)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (1, 1, 12.5, -90, '["aa","bb"]', ?)`, time.Now().Unix())
prevCount := initialTx.ObservationCount
newMax2 := store.IngestNewObservations(newObsMax, 500)
if initialTx.ObservationCount != prevCount {
t.Errorf("duplicate obs should not increase count, was %d now %d",
prevCount, initialTx.ObservationCount)
}
_ = newMax2
})
t.Run("default limit", func(t *testing.T) {
_ = store.IngestNewObservations(newObsMax, 0)
})
}
func TestIngestNewObservationsV2(t *testing.T) {
db := setupTestDBv2(t)
defer db.Close()
seedV2Data(t, db)
store := NewPacketStore(db)
store.Load()
tx := store.byHash["abc123def4567890"]
if tx == nil {
t.Fatal("expected to find transmission in store")
}
initialCount := tx.ObservationCount
maxObsID := db.GetMaxObservationID()
// Add new observation for existing transmission
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_id, observer_name, snr, rssi, path_json, timestamp)
VALUES (1, 'obs2', 'Obs Two', 6.0, -98, '["dd","ee"]', ?)`, time.Now().Unix())
newMax := store.IngestNewObservations(maxObsID, 500)
if newMax <= maxObsID {
t.Errorf("expected newMax > %d, got %d", maxObsID, newMax)
}
if tx.ObservationCount != initialCount+1 {
t.Errorf("expected obs count %d, got %d", initialCount+1, tx.ObservationCount)
}
}
func TestGetMaxObservationID(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
maxID := db.GetMaxObservationID()
if maxID != 0 {
t.Errorf("expected 0 for empty table, got %d", maxID)
}
seedTestData(t, db)
maxID = db.GetMaxObservationID()
if maxID <= 0 {
t.Errorf("expected positive max obs ID, got %d", maxID)
}
}
// --- perfMiddleware with endpoint normalization ---
func TestPerfMiddlewareEndpointNormalization(t *testing.T) {
_, router := setupTestServer(t)
// Hit a route with a hex hash — should normalize to :id
req := httptest.NewRequest("GET", "/api/packets/abc123def4567890", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// The hex id should have been normalized in perf stats
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
// --- handleNodeAnalytics edge cases ---
func TestHandleNodeAnalyticsNameless(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
// Insert a node without a name
db.conn.Exec(`INSERT INTO nodes (public_key, role, lat, lon, last_seen, first_seen, advert_count)
VALUES ('nameless_node_pk_1', 'repeater', 37.5, -122.0, ?, '2026-01-01', 1)`,
time.Now().UTC().Format(time.RFC3339))
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db)
store.Load()
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
req := httptest.NewRequest("GET", "/api/nodes/nameless_node_pk_1/analytics?days=1", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200, got %d", w.Code)
}
}
// --- PerfStats overflow (>100 recent entries) ---
func TestPerfStatsRecentOverflow(t *testing.T) {
_, router := setupTestServer(t)
// Hit an endpoint 120 times to overflow the Recent buffer (capped at 100)
for i := 0; i < 120; i++ {
req := httptest.NewRequest("GET", fmt.Sprintf("/api/health?i=%d", i), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
}
}
// --- handleAudioLabBuckets ---
func TestHandleAudioLabBucketsNoStore(t *testing.T) {
_, router := setupNoStoreServer(t)
req := httptest.NewRequest("GET", "/api/audio-lab/buckets", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Just verify no crash
}
// --- Store region filter paths ---
func TestStoreQueryPacketsRegionFilter(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
result := store.QueryPackets(PacketQuery{Region: "SJC", Limit: 50, Order: "DESC"})
_ = result
result2 := store.QueryPackets(PacketQuery{Region: "NONEXIST", Limit: 50, Order: "DESC"})
if result2.Total != 0 {
t.Errorf("expected 0 for non-existent region, got %d", result2.Total)
}
}
// --- DB.GetObserverIdsForRegion ---
func TestDBGetObserverIdsForRegion(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
ids, err := db.GetObserverIdsForRegion("SJC")
if err != nil {
t.Fatal(err)
}
if len(ids) == 0 {
t.Error("expected observer IDs for SJC")
}
ids2, err := db.GetObserverIdsForRegion("NONEXIST")
if err != nil {
t.Fatal(err)
}
if len(ids2) != 0 {
t.Errorf("expected 0 for NONEXIST, got %d", len(ids2))
}
}
// --- DB.GetDistinctIATAs ---
func TestDBGetDistinctIATAs(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
iatas, err := db.GetDistinctIATAs()
if err != nil {
t.Fatal(err)
}
if len(iatas) == 0 {
t.Error("expected at least one IATA code")
}
}
// --- DB.SearchNodes ---
func TestDBSearchNodes(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
nodes, err := db.SearchNodes("Test", 10)
if err != nil {
t.Fatal(err)
}
if len(nodes) == 0 {
t.Error("expected nodes matching 'Test'")
}
}
// --- Ensure non-panic on GetDBSizeStats with path ---
func TestGetDBSizeStatsMemory(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
stats := db.GetDBSizeStats()
if stats["dbSizeMB"] != float64(0) {
t.Errorf("expected 0 for in-memory, got %v", stats["dbSizeMB"])
}
}
// Regression test for #198: channel messages must include newly ingested packets.
// byPayloadType must maintain newest-first ordering after IngestNewFromDB so that
// GetChannelMessages reverse iteration returns the latest messages.
func TestGetChannelMessagesAfterIngest(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
seedTestData(t, db)
store := NewPacketStore(db)
store.Load()
initialMax := store.MaxTransmissionID()
// Get baseline message count
_, totalBefore := store.GetChannelMessages("#test", 100, 0)
// Insert a new channel message into the DB (newer than anything loaded)
now := time.Now().UTC()
nowStr := now.Format(time.RFC3339)
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
VALUES ('FF01', 'newchannelmsg19800', ?, 1, 5, '{"type":"CHAN","channel":"#test","text":"NewUser: brand new message","sender":"NewUser"}')`, nowStr)
newTxID := 0
db.conn.QueryRow("SELECT MAX(id) FROM transmissions").Scan(&newTxID)
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (?, 1, 12.0, -88, '[]', ?)`, newTxID, now.Unix())
// Ingest the new data
_, newMax := store.IngestNewFromDB(initialMax, 100)
if newMax <= initialMax {
t.Fatalf("ingest did not advance maxID: %d -> %d", initialMax, newMax)
}
// GetChannelMessages must now include the new message
msgs, totalAfter := store.GetChannelMessages("#test", 100, 0)
if totalAfter <= totalBefore {
t.Errorf("expected more messages after ingest: before=%d after=%d", totalBefore, totalAfter)
}
// The newest message (last in the returned slice) must be the one we just inserted
if len(msgs) == 0 {
t.Fatal("expected at least one message")
}
lastMsg := msgs[len(msgs)-1]
if lastMsg["text"] != "brand new message" {
t.Errorf("newest message should be 'brand new message', got %q", lastMsg["text"])
}
}