mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-31 22:46:29 +00:00
- db.go: Add freelistMB (PRAGMA freelist_count * page_size) and walPages (PRAGMA wal_checkpoint(PASSIVE)) to GetDBSizeStats - store.go: Add advertByObserver count to GetPerfStoreStats indexes (count distinct pubkeys with ADVERT observations) - db.go: Add getObservationsForTransmissions helper; enrich GetRecentTransmissionsForNode results with observations array, _parsedPath, and _parsedDecoded - db_test.go: Add second ADVERT with different hash_size to seed data so hash_sizes_seen is populated; enrich decoded_json with full ADVERT fields; update count assertions for new seed row fixes #151, fixes #152 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1943 lines
53 KiB
Go
1943 lines
53 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
func setupTestServer(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)
|
|
store := NewPacketStore(db)
|
|
store.Load()
|
|
srv.store = store
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
return srv, router
|
|
}
|
|
|
|
func TestHealthEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(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"])
|
|
}
|
|
if body["engine"] != "go" {
|
|
t.Errorf("expected engine go, got %v", body["engine"])
|
|
}
|
|
if _, ok := body["version"]; !ok {
|
|
t.Error("expected version field in health response")
|
|
}
|
|
if _, ok := body["commit"]; !ok {
|
|
t.Error("expected commit field in health response")
|
|
}
|
|
|
|
// Verify memory.heapMB exists
|
|
mem, ok := body["memory"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected memory object in health response")
|
|
}
|
|
if _, ok := mem["heapMB"]; !ok {
|
|
t.Error("expected heapMB in memory")
|
|
}
|
|
|
|
// Verify goRuntime with goroutines and gcPauses
|
|
goRT, ok := body["goRuntime"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected goRuntime object in health response")
|
|
}
|
|
if _, ok := goRT["goroutines"]; !ok {
|
|
t.Error("expected goroutines in goRuntime")
|
|
}
|
|
if _, ok := goRT["gcPauses"]; !ok {
|
|
t.Error("expected gcPauses in goRuntime")
|
|
}
|
|
|
|
// Verify real packetStore stats (not zeros)
|
|
pktStore, ok := body["packetStore"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected packetStore object in health response")
|
|
}
|
|
if _, ok := pktStore["packets"]; !ok {
|
|
t.Error("expected packets in packetStore")
|
|
}
|
|
if _, ok := pktStore["estimatedMB"]; !ok {
|
|
t.Error("expected estimatedMB in packetStore")
|
|
}
|
|
|
|
// Verify eventLoop (GC pause metrics matching Node.js shape)
|
|
el, ok := body["eventLoop"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected eventLoop object in health response")
|
|
}
|
|
for _, field := range []string{"currentLagMs", "maxLagMs", "p50Ms", "p95Ms", "p99Ms"} {
|
|
if _, ok := el[field]; !ok {
|
|
t.Errorf("expected %s in eventLoop", field)
|
|
}
|
|
}
|
|
|
|
// Verify cache has real structure
|
|
cache, ok := body["cache"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected cache object in health response")
|
|
}
|
|
if _, ok := cache["entries"]; !ok {
|
|
t.Error("expected entries in cache")
|
|
}
|
|
if _, ok := cache["hitRate"]; !ok {
|
|
t.Error("expected hitRate in cache")
|
|
}
|
|
}
|
|
|
|
func TestStatsEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["totalTransmissions"] != float64(3) {
|
|
t.Errorf("expected 3 transmissions, got %v", body["totalTransmissions"])
|
|
}
|
|
if body["totalNodes"] != float64(3) {
|
|
t.Errorf("expected 3 nodes, got %v", body["totalNodes"])
|
|
}
|
|
if body["engine"] != "go" {
|
|
t.Errorf("expected engine go, got %v", body["engine"])
|
|
}
|
|
if _, ok := body["version"]; !ok {
|
|
t.Error("expected version field in stats response")
|
|
}
|
|
if _, ok := body["commit"]; !ok {
|
|
t.Error("expected commit field in stats response")
|
|
}
|
|
}
|
|
|
|
func TestPacketsEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
packets, ok := body["packets"].([]interface{})
|
|
if !ok {
|
|
t.Fatal("expected packets array")
|
|
}
|
|
if len(packets) != 3 {
|
|
t.Errorf("expected 3 packets (transmissions), got %d", len(packets))
|
|
}
|
|
}
|
|
|
|
func TestPacketsGrouped(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
packets, ok := body["packets"].([]interface{})
|
|
if !ok {
|
|
t.Fatal("expected packets array")
|
|
}
|
|
if len(packets) != 3 {
|
|
t.Errorf("expected 3 grouped packets, got %d", len(packets))
|
|
}
|
|
}
|
|
|
|
func TestNodesEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/nodes?limit=50", 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)
|
|
nodes, ok := body["nodes"].([]interface{})
|
|
if !ok {
|
|
t.Fatal("expected nodes array")
|
|
}
|
|
if len(nodes) != 3 {
|
|
t.Errorf("expected 3 nodes, got %d", len(nodes))
|
|
}
|
|
total := body["total"].(float64)
|
|
if total != 3 {
|
|
t.Errorf("expected total 3, got %v", total)
|
|
}
|
|
}
|
|
|
|
func TestNodeDetailEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
node, ok := body["node"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected node object")
|
|
}
|
|
if node["name"] != "TestRepeater" {
|
|
t.Errorf("expected TestRepeater, got %v", node["name"])
|
|
}
|
|
}
|
|
|
|
func TestNodeDetail404(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/nodes/nonexistent", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 404 {
|
|
t.Errorf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestNodeSearchEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/nodes/search?q=Repeater", 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)
|
|
nodes, ok := body["nodes"].([]interface{})
|
|
if !ok {
|
|
t.Fatal("expected nodes array")
|
|
}
|
|
if len(nodes) != 1 {
|
|
t.Errorf("expected 1 node matching 'Repeater', got %d", len(nodes))
|
|
}
|
|
}
|
|
|
|
func TestNetworkStatusEndpoint(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["total"] != float64(3) {
|
|
t.Errorf("expected 3 total, got %v", body["total"])
|
|
}
|
|
}
|
|
|
|
func TestObserversEndpoint(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 {
|
|
t.Fatal("expected observers array")
|
|
}
|
|
if len(observers) != 2 {
|
|
t.Errorf("expected 2 observers, got %d", len(observers))
|
|
}
|
|
}
|
|
|
|
func TestObserverDetail404(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/observers/nonexistent", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 404 {
|
|
t.Errorf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestChannelsEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
channels, ok := body["channels"].([]interface{})
|
|
if !ok {
|
|
t.Fatal("expected channels array")
|
|
}
|
|
if len(channels) != 1 {
|
|
t.Errorf("expected 1 channel, got %d", len(channels))
|
|
}
|
|
}
|
|
|
|
func TestTracesEndpoint(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
traces, ok := body["traces"].([]interface{})
|
|
if !ok {
|
|
t.Fatal("expected traces array")
|
|
}
|
|
if len(traces) != 2 {
|
|
t.Errorf("expected 2 traces, got %d", len(traces))
|
|
}
|
|
}
|
|
|
|
func TestConfigCacheEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/config/cache", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestConfigThemeEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/config/theme", 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["branding"] == nil {
|
|
t.Error("expected branding in theme response")
|
|
}
|
|
}
|
|
|
|
func TestConfigMapEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/config/map", 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["zoom"] == nil {
|
|
t.Error("expected zoom in map response")
|
|
}
|
|
}
|
|
|
|
func TestPerfEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
// Make a request first to generate perf data
|
|
req1 := httptest.NewRequest("GET", "/api/health", nil)
|
|
w1 := httptest.NewRecorder()
|
|
router.ServeHTTP(w1, req1)
|
|
|
|
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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
|
|
// Verify goRuntime memory/GC stats
|
|
goRT, ok := body["goRuntime"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected goRuntime object in perf response")
|
|
}
|
|
for _, field := range []string{"heapAllocMB", "heapSysMB", "heapInuseMB", "heapIdleMB", "heapReleasedMB", "gcSysMB", "numGC", "pauseTotalMs", "lastPauseMs", "goroutines", "numCPU"} {
|
|
if _, ok := goRT[field]; !ok {
|
|
t.Errorf("expected %s in goRuntime", field)
|
|
}
|
|
}
|
|
|
|
// Verify cache stats (real, not hardcoded zeros)
|
|
cache, ok := body["cache"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected cache object in perf response")
|
|
}
|
|
for _, field := range []string{"size", "hits", "misses", "hitRate"} {
|
|
if _, ok := cache[field]; !ok {
|
|
t.Errorf("expected %s in cache", field)
|
|
}
|
|
}
|
|
|
|
// Verify packetStore stats
|
|
if _, ok := body["packetStore"]; !ok {
|
|
t.Error("expected packetStore in perf response")
|
|
}
|
|
|
|
// Verify sqlite stats
|
|
sqliteStats, ok := body["sqlite"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected sqlite object in perf response")
|
|
}
|
|
if _, ok := sqliteStats["dbSizeMB"]; !ok {
|
|
t.Error("expected dbSizeMB in sqlite")
|
|
}
|
|
if _, ok := sqliteStats["rows"]; !ok {
|
|
t.Error("expected rows in sqlite")
|
|
}
|
|
|
|
// Verify standard fields still present
|
|
if _, ok := body["uptime"]; !ok {
|
|
t.Error("expected uptime in perf response")
|
|
}
|
|
if _, ok := body["endpoints"]; !ok {
|
|
t.Error("expected endpoints in perf response")
|
|
}
|
|
}
|
|
|
|
func TestAnalyticsRFEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
}
|
|
|
|
func TestResolveHopsEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
resolved, ok := body["resolved"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected resolved map")
|
|
}
|
|
// aabb should resolve to TestRepeater
|
|
aabb, ok := resolved["aabb"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected aabb in resolved")
|
|
}
|
|
if aabb["name"] != "TestRepeater" {
|
|
t.Errorf("expected TestRepeater for aabb, got %v", aabb["name"])
|
|
}
|
|
}
|
|
|
|
func TestPacketTimestampsRequiresSince(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets/timestamps", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 400 {
|
|
t.Errorf("expected 400, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestContentTypeJSON(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
endpoints := []string{
|
|
"/api/health", "/api/stats", "/api/nodes", "/api/packets",
|
|
"/api/observers", "/api/channels",
|
|
}
|
|
for _, ep := range endpoints {
|
|
req := httptest.NewRequest("GET", ep, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
ct := w.Header().Get("Content-Type")
|
|
if ct != "application/json" {
|
|
t.Errorf("%s: expected application/json, got %s", ep, ct)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAllEndpointsReturn200(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
endpoints := []struct {
|
|
path string
|
|
status int
|
|
}{
|
|
{"/api/health", http.StatusOK},
|
|
{"/api/stats", http.StatusOK},
|
|
{"/api/perf", http.StatusOK},
|
|
{"/api/config/cache", http.StatusOK},
|
|
{"/api/config/client", http.StatusOK},
|
|
{"/api/config/regions", http.StatusOK},
|
|
{"/api/config/theme", http.StatusOK},
|
|
{"/api/config/map", http.StatusOK},
|
|
{"/api/packets?limit=5", http.StatusOK},
|
|
{"/api/nodes?limit=5", http.StatusOK},
|
|
{"/api/nodes/search?q=test", http.StatusOK},
|
|
{"/api/nodes/bulk-health", http.StatusOK},
|
|
{"/api/nodes/network-status", http.StatusOK},
|
|
{"/api/observers", http.StatusOK},
|
|
{"/api/channels", http.StatusOK},
|
|
{"/api/analytics/rf", http.StatusOK},
|
|
{"/api/analytics/topology", http.StatusOK},
|
|
{"/api/analytics/channels", http.StatusOK},
|
|
{"/api/analytics/distance", http.StatusOK},
|
|
{"/api/analytics/hash-sizes", http.StatusOK},
|
|
{"/api/analytics/subpaths", http.StatusOK},
|
|
{"/api/analytics/subpath-detail?hops=aa,bb", http.StatusOK},
|
|
{"/api/resolve-hops?hops=aabb", http.StatusOK},
|
|
{"/api/iata-coords", http.StatusOK},
|
|
{"/api/traces/abc123def4567890", http.StatusOK},
|
|
}
|
|
for _, tc := range endpoints {
|
|
req := httptest.NewRequest("GET", tc.path, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != tc.status {
|
|
t.Errorf("%s: expected %d, got %d (body: %s)", tc.path, tc.status, w.Code, w.Body.String()[:min(200, w.Body.Len())])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPacketDetailByHash(t *testing.T) {
|
|
_, router := setupTestServer(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 (body: %s)", w.Code, w.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
pkt, ok := body["packet"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected packet object")
|
|
}
|
|
if pkt["hash"] != "abc123def4567890" {
|
|
t.Errorf("expected hash abc123def4567890, got %v", pkt["hash"])
|
|
}
|
|
if body["observation_count"] == nil {
|
|
t.Error("expected observation_count")
|
|
}
|
|
}
|
|
|
|
func TestPacketDetailByNumericID(t *testing.T) {
|
|
_, router := setupTestServer(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 (body: %s)", w.Code, w.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["packet"] == nil {
|
|
t.Error("expected packet object")
|
|
}
|
|
}
|
|
|
|
func TestPacketDetailNotFound(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets/notahash12345678", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// "notahash12345678" is 16 hex chars, will try hash lookup first, then fail
|
|
if w.Code != 404 {
|
|
t.Errorf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestPacketDetailNumericNotFound(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets/99999", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 404 {
|
|
t.Errorf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestPacketTimestampsWithSince(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets/timestamps?since=2020-01-01", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestNodeDetailWithRecentAdverts(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["recentAdverts"] == nil {
|
|
t.Error("expected recentAdverts in response")
|
|
}
|
|
node, ok := body["node"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected node object")
|
|
}
|
|
if node["name"] != "TestRepeater" {
|
|
t.Errorf("expected TestRepeater, got %v", node["name"])
|
|
}
|
|
}
|
|
|
|
func TestNodeHealthFound(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/nodes/aabbccdd11223344/health", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d (body: %s)", w.Code, w.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["node"] == nil {
|
|
t.Error("expected node in response")
|
|
}
|
|
if body["stats"] == nil {
|
|
t.Error("expected stats in response")
|
|
}
|
|
}
|
|
|
|
func TestNodeHealthNotFound(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/nodes/nonexistent/health", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 404 {
|
|
t.Errorf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestBulkHealthEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(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 len(body) != 3 {
|
|
t.Errorf("expected 3 nodes, got %d", len(body))
|
|
}
|
|
}
|
|
|
|
func TestBulkHealthLimitCap(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/nodes/bulk-health?limit=999", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestNodePathsFound(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["node"] == nil {
|
|
t.Error("expected node in response")
|
|
}
|
|
if body["paths"] == nil {
|
|
t.Error("expected paths in response")
|
|
}
|
|
}
|
|
|
|
func TestNodePathsNotFound(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/nodes/nonexistent/paths", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 404 {
|
|
t.Errorf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestNodeAnalytics(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
|
|
t.Run("default days", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/nodes/aabbccdd11223344/analytics", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d (body: %s)", w.Code, w.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["timeRange"] == nil {
|
|
t.Error("expected timeRange")
|
|
}
|
|
if body["activityTimeline"] == nil {
|
|
t.Error("expected activityTimeline")
|
|
}
|
|
})
|
|
|
|
t.Run("custom days", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/nodes/aabbccdd11223344/analytics?days=30", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("clamp days below 1", 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("clamp days above 365", 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)
|
|
}
|
|
})
|
|
|
|
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.Errorf("expected 404, got %d", w.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestObserverDetailFound(t *testing.T) {
|
|
_, router := setupTestServer(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", w.Code)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["id"] != "obs1" {
|
|
t.Errorf("expected obs1, got %v", body["id"])
|
|
}
|
|
}
|
|
|
|
func TestObserverAnalytics(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
|
|
t.Run("default", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/observers/obs1/analytics", 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["packetTypes"] == nil {
|
|
t.Error("expected packetTypes")
|
|
}
|
|
if body["recentPackets"] == nil {
|
|
t.Error("expected recentPackets")
|
|
}
|
|
})
|
|
|
|
t.Run("custom days", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/observers/obs1/analytics?days=1", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("days greater than 7", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/observers/obs1/analytics?days=30", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestChannelMessages(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/channels/%23test/messages", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d (body: %s)", w.Code, w.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["messages"] == nil {
|
|
t.Error("expected messages")
|
|
}
|
|
}
|
|
|
|
func TestAnalyticsRFWithRegion(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["snr"] == nil {
|
|
t.Error("expected snr in response")
|
|
}
|
|
if body["payloadTypes"] == nil {
|
|
t.Error("expected payloadTypes")
|
|
}
|
|
}
|
|
|
|
func TestAnalyticsTopology(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["uniqueNodes"] == nil {
|
|
t.Error("expected uniqueNodes")
|
|
}
|
|
}
|
|
|
|
func TestAnalyticsChannels(t *testing.T) {
|
|
_, router := setupTestServer(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 body["channels"] == nil {
|
|
t.Error("expected channels")
|
|
}
|
|
if body["activeChannels"] == nil {
|
|
t.Error("expected activeChannels")
|
|
}
|
|
}
|
|
|
|
func TestAnalyticsDistance(t *testing.T) {
|
|
_, router := setupTestServer(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 TestAnalyticsHashSizes(t *testing.T) {
|
|
_, router := setupTestServer(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 TestAnalyticsSubpaths(t *testing.T) {
|
|
_, router := setupTestServer(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 TestAnalyticsSubpathDetailWithHops(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
hops, ok := body["hops"].([]interface{})
|
|
if !ok {
|
|
t.Fatal("expected hops array")
|
|
}
|
|
if len(hops) != 2 {
|
|
t.Errorf("expected 2 hops, got %d", len(hops))
|
|
}
|
|
}
|
|
|
|
func TestAnalyticsSubpathDetailNoHops(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["error"] == nil {
|
|
t.Error("expected error message when no hops provided")
|
|
}
|
|
}
|
|
|
|
func TestResolveHopsEmpty(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
resolved, ok := body["resolved"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected resolved map")
|
|
}
|
|
if len(resolved) != 0 {
|
|
t.Error("expected empty resolved map for no hops")
|
|
}
|
|
}
|
|
|
|
func TestResolveHopsAmbiguous(t *testing.T) {
|
|
// Set up server with nodes that share a prefix
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
// Add another node with same "aabb" prefix
|
|
db.conn.Exec(`INSERT INTO nodes (public_key, name, role) VALUES ('aabb000000000000', 'AnotherNode', 'repeater')`)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
req := httptest.NewRequest("GET", "/api/resolve-hops?hops=aabb", 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)
|
|
resolved := body["resolved"].(map[string]interface{})
|
|
aabb := resolved["aabb"].(map[string]interface{})
|
|
if aabb["ambiguous"] != true {
|
|
t.Error("expected ambiguous=true when multiple candidates")
|
|
}
|
|
candidates, ok := aabb["candidates"].([]interface{})
|
|
if !ok {
|
|
t.Fatal("expected candidates array")
|
|
}
|
|
if len(candidates) < 2 {
|
|
t.Errorf("expected at least 2 candidates, got %d", len(candidates))
|
|
}
|
|
}
|
|
|
|
func TestResolveHopsNoMatch(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/resolve-hops?hops=zzzz", 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)
|
|
resolved := body["resolved"].(map[string]interface{})
|
|
zzzz := resolved["zzzz"].(map[string]interface{})
|
|
if zzzz["name"] != nil {
|
|
t.Error("expected nil name for unresolved hop")
|
|
}
|
|
}
|
|
|
|
func TestAudioLabBuckets(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/audio-lab/buckets", 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["buckets"] == nil {
|
|
t.Error("expected buckets")
|
|
}
|
|
}
|
|
|
|
func TestIATACoords(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["coords"] == nil {
|
|
t.Error("expected coords")
|
|
}
|
|
}
|
|
|
|
func TestPerfMiddlewareRecording(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
|
|
// Make several requests to generate perf data
|
|
for i := 0; i < 5; i++ {
|
|
req := httptest.NewRequest("GET", "/api/health", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
}
|
|
|
|
// Check perf endpoint
|
|
req := httptest.NewRequest("GET", "/api/perf", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
totalReqs := body["totalRequests"].(float64)
|
|
// At least 5 health requests + 1 perf request (but perf is also counted)
|
|
if totalReqs < 5 {
|
|
t.Errorf("expected at least 5 total requests, got %v", totalReqs)
|
|
}
|
|
}
|
|
|
|
func TestPerfMiddlewareNonAPI(t *testing.T) {
|
|
// Non-API paths should not be recorded
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/some/non/api/path", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
// No panic, no error — middleware just passes through
|
|
}
|
|
|
|
func TestPacketsWithOrderAsc(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets?limit=10&order=asc", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestPacketsWithTypeAndRouteFilter(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets?limit=10&type=4&route=1", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestPacketsWithExpandObservations(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
}
|
|
|
|
func TestConfigClientEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/config/client", 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["propagationBufferMs"] == nil {
|
|
t.Error("expected propagationBufferMs")
|
|
}
|
|
}
|
|
|
|
func TestConfigRegionsEndpoint(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/config/regions", 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)
|
|
// Should have at least the IATA codes from seed data
|
|
if body["SJC"] == nil {
|
|
t.Error("expected SJC region")
|
|
}
|
|
if body["SFO"] == nil {
|
|
t.Error("expected SFO region")
|
|
}
|
|
}
|
|
|
|
func TestNodeSearchEmpty(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
nodes := body["nodes"].([]interface{})
|
|
if len(nodes) != 0 {
|
|
t.Error("expected empty nodes for empty search")
|
|
}
|
|
}
|
|
|
|
func TestNodeSearchWhitespace(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/nodes/search?q=%20%20", 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)
|
|
nodes := body["nodes"].([]interface{})
|
|
if len(nodes) != 0 {
|
|
t.Error("expected empty nodes for whitespace search")
|
|
}
|
|
}
|
|
|
|
func TestNodeAnalyticsNoNameNode(t *testing.T) {
|
|
// Test with a node that has no name to cover the name="" branch
|
|
db := setupTestDB(t)
|
|
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 ('deadbeef12345678', NULL, 37.5, -122.0, '2026-01-15T10:00:00Z', '2026-01-01T00:00:00Z', 5)`)
|
|
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
|
|
VALUES ('DDEE', 'deadbeefhash1234', '2026-01-15T10:05:00Z', 1, 4,
|
|
'{"pubKey":"deadbeef12345678","type":"ADVERT"}')`)
|
|
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
|
|
VALUES (3, 1, 11.0, -91, '["dd"]', 1736935500)`)
|
|
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
req := httptest.NewRequest("GET", "/api/nodes/deadbeef12345678/analytics?days=30", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d (body: %s)", w.Code, w.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
if body["node"] == nil {
|
|
t.Error("expected node in response")
|
|
}
|
|
}
|
|
|
|
func TestNodeHealthForNoNameNode(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
db.conn.Exec(`INSERT INTO nodes (public_key, role, last_seen, first_seen, advert_count)
|
|
VALUES ('deadbeef12345678', 'repeater', '2026-01-15T10:00:00Z', '2026-01-01T00:00:00Z', 5)`)
|
|
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
|
|
VALUES ('DDEE', 'deadbeefhash1234', '2026-01-15T10:05:00Z', 1, 4,
|
|
'{"pubKey":"deadbeef12345678","type":"ADVERT"}')`)
|
|
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
|
|
VALUES (3, 1, 11.0, -91, '["dd"]', 1736935500)`)
|
|
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
req := httptest.NewRequest("GET", "/api/nodes/deadbeef12345678/health", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d (body: %s)", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestPacketsWithNodeFilter(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets?limit=10&node=TestRepeater", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestPacketsWithRegionFilter(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets?limit=10®ion=SJC", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestPacketsWithHashFilter(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets?limit=10&hash=abc123def4567890", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestPacketsWithObserverFilter(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets?limit=10&observer=obs1", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestPacketsWithSinceUntil(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets?limit=10&since=2020-01-01&until=2099-01-01", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestNodesWithRoleFilter(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/nodes?role=repeater&limit=10", 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)
|
|
total := body["total"].(float64)
|
|
if total != 1 {
|
|
t.Errorf("expected 1 repeater, got %v", total)
|
|
}
|
|
}
|
|
|
|
func TestNodesWithSortAndSearch(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/nodes?search=Test&sortBy=name&limit=10", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestGroupedPacketsWithFilters(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets?groupByHash=true&limit=10&type=4", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestConfigThemeWithCustomConfig(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{
|
|
Port: 3000,
|
|
Branding: map[string]interface{}{
|
|
"siteName": "CustomSite",
|
|
},
|
|
Theme: map[string]interface{}{
|
|
"accent": "#ff0000",
|
|
},
|
|
Home: map[string]interface{}{
|
|
"title": "Welcome",
|
|
},
|
|
}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
req := httptest.NewRequest("GET", "/api/config/theme", 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)
|
|
branding := body["branding"].(map[string]interface{})
|
|
if branding["siteName"] != "CustomSite" {
|
|
t.Errorf("expected CustomSite, got %v", branding["siteName"])
|
|
}
|
|
if body["home"] == nil {
|
|
t.Error("expected home in response")
|
|
}
|
|
}
|
|
|
|
func TestConfigCacheWithCustomTTL(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{
|
|
Port: 3000,
|
|
CacheTTL: map[string]interface{}{
|
|
"nodes": 60000,
|
|
},
|
|
}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
req := httptest.NewRequest("GET", "/api/config/cache", 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["nodes"] != float64(60000) {
|
|
t.Errorf("expected 60000, got %v", body["nodes"])
|
|
}
|
|
}
|
|
|
|
func TestConfigRegionsWithCustomRegions(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{
|
|
Port: 3000,
|
|
Regions: map[string]string{
|
|
"LAX": "Los Angeles",
|
|
},
|
|
}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
req := httptest.NewRequest("GET", "/api/config/regions", 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["LAX"] != "Los Angeles" {
|
|
t.Errorf("expected 'Los Angeles', got %v", body["LAX"])
|
|
}
|
|
// DB-sourced IATA codes should also appear
|
|
if body["SJC"] == nil {
|
|
t.Error("expected SJC from DB")
|
|
}
|
|
}
|
|
|
|
func TestConfigMapWithCustomDefaults(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{Port: 3000}
|
|
cfg.MapDefaults.Center = []float64{40.0, -74.0}
|
|
cfg.MapDefaults.Zoom = 12
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
req := httptest.NewRequest("GET", "/api/config/map", 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["zoom"] != float64(12) {
|
|
t.Errorf("expected zoom 12, got %v", body["zoom"])
|
|
}
|
|
}
|
|
|
|
func TestHandlerErrorPaths(t *testing.T) {
|
|
// Create a DB that will error on queries by dropping the view/tables
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
// Drop the view to force query errors
|
|
db.conn.Exec("DROP VIEW IF EXISTS packets_v")
|
|
|
|
t.Run("stats error", func(t *testing.T) {
|
|
db.conn.Exec("DROP TABLE IF EXISTS transmissions")
|
|
req := httptest.NewRequest("GET", "/api/stats", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 500 {
|
|
t.Errorf("expected 500, got %d", w.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHandlerErrorChannels(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
db.conn.Exec("DROP VIEW IF EXISTS packets_v")
|
|
|
|
req := httptest.NewRequest("GET", "/api/channels", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 500 {
|
|
t.Errorf("expected 500 for channels error, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandlerErrorTraces(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
db.conn.Exec("DROP VIEW IF EXISTS packets_v")
|
|
|
|
req := httptest.NewRequest("GET", "/api/traces/abc123def4567890", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 500 {
|
|
t.Errorf("expected 500 for traces error, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandlerErrorObservers(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
db.conn.Exec("DROP TABLE IF EXISTS observers")
|
|
|
|
req := httptest.NewRequest("GET", "/api/observers", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 500 {
|
|
t.Errorf("expected 500 for observers error, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandlerErrorNodes(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
db.conn.Exec("DROP TABLE IF EXISTS nodes")
|
|
|
|
req := httptest.NewRequest("GET", "/api/nodes?limit=10", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 500 {
|
|
t.Errorf("expected 500 for nodes error, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandlerErrorNetworkStatus(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
db.conn.Exec("DROP TABLE IF EXISTS nodes")
|
|
|
|
req := httptest.NewRequest("GET", "/api/nodes/network-status", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 500 {
|
|
t.Errorf("expected 500 for network-status error, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandlerErrorPackets(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
// Drop transmissions table to trigger error in transmission-centric query
|
|
db.conn.Exec("DROP TABLE IF EXISTS transmissions")
|
|
|
|
req := httptest.NewRequest("GET", "/api/packets?limit=10", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 500 {
|
|
t.Errorf("expected 500 for packets error, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandlerErrorPacketsGrouped(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
db.conn.Exec("DROP TABLE IF EXISTS observations")
|
|
|
|
req := httptest.NewRequest("GET", "/api/packets?limit=10&groupByHash=true", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 500 {
|
|
t.Errorf("expected 500 for grouped packets error, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandlerErrorNodeSearch(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
db.conn.Exec("DROP TABLE IF EXISTS nodes")
|
|
|
|
req := httptest.NewRequest("GET", "/api/nodes/search?q=test", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 500 {
|
|
t.Errorf("expected 500 for node search error, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandlerErrorTimestamps(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
db.conn.Exec("DROP VIEW IF EXISTS packets_v")
|
|
|
|
req := httptest.NewRequest("GET", "/api/packets/timestamps?since=2020-01-01", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 500 {
|
|
t.Errorf("expected 500 for timestamps error, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandlerErrorChannelMessages(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
db.conn.Exec("DROP VIEW IF EXISTS packets_v")
|
|
|
|
req := httptest.NewRequest("GET", "/api/channels/%23test/messages", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 500 {
|
|
t.Errorf("expected 500 for channel messages error, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandlerErrorBulkHealth(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
db.conn.Exec("DROP TABLE IF EXISTS nodes")
|
|
|
|
req := httptest.NewRequest("GET", "/api/nodes/bulk-health", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 500 {
|
|
t.Errorf("expected 500, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
|
|
func TestAnalyticsChannelsNoNullArrays(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
|
|
raw := w.Body.String()
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal([]byte(raw), &body); err != nil {
|
|
t.Fatalf("invalid JSON: %v", err)
|
|
}
|
|
|
|
arrayFields := []string{"channels", "topSenders", "channelTimeline", "msgLengths"}
|
|
for _, field := range arrayFields {
|
|
val, exists := body[field]
|
|
if !exists {
|
|
t.Errorf("missing field %q", field)
|
|
continue
|
|
}
|
|
if val == nil {
|
|
t.Errorf("field %q is null, expected empty array []", field)
|
|
continue
|
|
}
|
|
if _, ok := val.([]interface{}); !ok {
|
|
t.Errorf("field %q is not an array, got %T", field, val)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnalyticsChannelsNoStoreFallbackNoNulls(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
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.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
|
|
arrayFields := []string{"channels", "topSenders", "channelTimeline", "msgLengths"}
|
|
for _, field := range arrayFields {
|
|
if body[field] == nil {
|
|
t.Errorf("field %q is null in DB fallback, expected []", field)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNodeHashSizeEnrichment(t *testing.T) {
|
|
t.Run("nil info leaves defaults", func(t *testing.T) {
|
|
node := map[string]interface{}{
|
|
"public_key": "abc123",
|
|
"hash_size": nil,
|
|
"hash_size_inconsistent": false,
|
|
}
|
|
EnrichNodeWithHashSize(node, nil)
|
|
if node["hash_size"] != nil {
|
|
t.Error("expected hash_size to remain nil with nil info")
|
|
}
|
|
})
|
|
|
|
t.Run("enriches with computed data", func(t *testing.T) {
|
|
node := map[string]interface{}{
|
|
"public_key": "abc123",
|
|
"hash_size": nil,
|
|
"hash_size_inconsistent": false,
|
|
}
|
|
info := &hashSizeNodeInfo{
|
|
HashSize: 2,
|
|
AllSizes: map[int]bool{1: true, 2: true},
|
|
Seq: []int{1, 2, 1, 2},
|
|
Inconsistent: true,
|
|
}
|
|
EnrichNodeWithHashSize(node, info)
|
|
if node["hash_size"] != 2 {
|
|
t.Errorf("expected hash_size 2, got %v", node["hash_size"])
|
|
}
|
|
if node["hash_size_inconsistent"] != true {
|
|
t.Error("expected hash_size_inconsistent true")
|
|
}
|
|
sizes, ok := node["hash_sizes_seen"].([]int)
|
|
if !ok {
|
|
t.Fatal("expected hash_sizes_seen to be []int")
|
|
}
|
|
if len(sizes) != 2 || sizes[0] != 1 || sizes[1] != 2 {
|
|
t.Errorf("expected [1,2], got %v", sizes)
|
|
}
|
|
})
|
|
|
|
t.Run("single size omits sizes_seen", func(t *testing.T) {
|
|
node := map[string]interface{}{
|
|
"public_key": "abc123",
|
|
"hash_size": nil,
|
|
"hash_size_inconsistent": false,
|
|
}
|
|
info := &hashSizeNodeInfo{
|
|
HashSize: 3,
|
|
AllSizes: map[int]bool{3: true},
|
|
Seq: []int{3, 3, 3},
|
|
}
|
|
EnrichNodeWithHashSize(node, info)
|
|
if node["hash_size"] != 3 {
|
|
t.Errorf("expected hash_size 3, got %v", node["hash_size"])
|
|
}
|
|
if node["hash_size_inconsistent"] != false {
|
|
t.Error("expected hash_size_inconsistent false")
|
|
}
|
|
if _, exists := node["hash_sizes_seen"]; exists {
|
|
t.Error("hash_sizes_seen should not be set for single size")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetNodeHashSizeInfoFlipFlop(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
store := NewPacketStore(db)
|
|
store.Load()
|
|
|
|
pk := "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
|
db.conn.Exec("INSERT OR IGNORE INTO nodes (public_key, name, role) VALUES (?, 'TestNode', 'repeater')", pk)
|
|
|
|
decoded := `{"name":"TestNode","pubKey":"` + pk + `"}`
|
|
raw1 := "04" + "00" + "aabb"
|
|
raw2 := "04" + "40" + "aabb"
|
|
|
|
payloadType := 4
|
|
for i := 0; i < 3; i++ {
|
|
rawHex := raw1
|
|
if i%2 == 1 {
|
|
rawHex = raw2
|
|
}
|
|
tx := &StoreTx{
|
|
ID: 9000 + i,
|
|
RawHex: rawHex,
|
|
Hash: "testhash" + strconv.Itoa(i),
|
|
FirstSeen: "2024-01-01T00:00:00Z",
|
|
PayloadType: &payloadType,
|
|
DecodedJSON: decoded,
|
|
}
|
|
store.packets = append(store.packets, tx)
|
|
store.byPayloadType[4] = append(store.byPayloadType[4], tx)
|
|
}
|
|
|
|
info := store.GetNodeHashSizeInfo()
|
|
ni := info[pk]
|
|
if ni == nil {
|
|
t.Fatal("expected hash info for test node")
|
|
}
|
|
if len(ni.AllSizes) != 2 {
|
|
t.Errorf("expected 2 unique sizes, got %d", len(ni.AllSizes))
|
|
}
|
|
if !ni.Inconsistent {
|
|
t.Error("expected inconsistent flag to be true for flip-flop pattern")
|
|
}
|
|
}
|
|
|
|
func TestAnalyticsHashSizesNoNullArrays(t *testing.T) {
|
|
_, router := setupTestServer(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)
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
|
|
arrayFields := []string{"hourly", "topHops", "multiByteNodes"}
|
|
for _, field := range arrayFields {
|
|
if body[field] == nil {
|
|
t.Errorf("field %q is null, expected []", field)
|
|
}
|
|
}
|
|
}
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|