Files
meshcore-analyzer/cmd/server/bridge_handle_nodes_test.go
T

124 lines
4.0 KiB
Go

package main
import (
"encoding/json"
"net/http/httptest"
"testing"
"time"
"github.com/gorilla/mux"
)
// TestBridgeScore_HandleNodesSurface verifies that /api/nodes
// includes a `bridge_score` field on repeater rows after the bridge
// recomputer has run. Drives the line-graph A-B-C-D through the full
// pipeline: insert nodes, populate the neighbor graph, force a
// recompute, hit the handler, parse the response. Issue #672 axis 2.
func TestBridgeScore_HandleNodesSurface(t *testing.T) {
db := setupCapabilityTestDB(t)
defer db.conn.Close()
// handleNodes/db.GetNodes selects a foreign_advert column not in
// the minimal capability-test schema.
if _, err := db.conn.Exec(`ALTER TABLE nodes ADD COLUMN foreign_advert INTEGER DEFAULT 0`); err != nil {
t.Fatal(err)
}
// Four repeater nodes in a line.
pks := []string{
"aaaa000000000000000000000000000000000000000000000000000000000000",
"bbbb000000000000000000000000000000000000000000000000000000000000",
"cccc000000000000000000000000000000000000000000000000000000000000",
"dddd000000000000000000000000000000000000000000000000000000000000",
}
recent := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
for _, pk := range pks {
if _, err := db.conn.Exec(`INSERT INTO nodes
(public_key, name, role, lat, lon, last_seen, first_seen, advert_count)
VALUES (?, ?, 'repeater', 37.5, -122.0, ?, ?, 10)`,
pk, "node-"+pk[:4], recent, recent); err != nil {
t.Fatal(err)
}
}
store := NewPacketStore(db, nil)
// Build neighbor graph with the line A-B-C-D. Add each edge
// `count` times so its time-decayed Score saturates.
g := NewNeighborGraph()
now := time.Now()
obs := "obs-test"
snr := 5.0
for i := 0; i < 10; i++ {
g.upsertEdge(pks[0], pks[1], "aa", obs, &snr, now)
g.upsertEdge(pks[1], pks[2], "bb", obs, &snr, now)
g.upsertEdge(pks[2], pks[3], "cc", obs, &snr, now)
}
store.graph.Store(g)
// Direct invocation of the recomputer's compute path — bypassing
// StartBridgeScoreRecomputer's package-level once-flag (which is
// problematic across tests).
recomputeBridgeScoresSafe(store)
snap := store.GetBridgeScoreMap()
if len(snap) == 0 {
t.Fatalf("expected non-empty bridge score snapshot, got empty")
}
// Sanity: middle nodes b/c must be positive, ends must be zero.
if snap[pks[1]] <= 0 || snap[pks[2]] <= 0 {
t.Errorf("middle nodes should have positive bridge: b=%v c=%v",
snap[pks[1]], snap[pks[2]])
}
if snap[pks[0]] != 0 || snap[pks[3]] != 0 {
t.Errorf("end nodes should have zero bridge: a=%v d=%v",
snap[pks[0]], snap[pks[3]])
}
// Wire a Server, call handleNodes, parse the response.
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
srv.store = store
router := mux.NewRouter()
srv.RegisterRoutes(router)
req := httptest.NewRequest("GET", "/api/nodes?limit=100", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != 200 {
t.Fatalf("handleNodes status: want 200, got %d body=%s", rr.Code, rr.Body.String())
}
var resp struct {
Nodes []map[string]interface{} `json:"nodes"`
}
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v body=%s", err, rr.Body.String())
}
gotBy := map[string]map[string]interface{}{}
for _, n := range resp.Nodes {
if pk, _ := n["public_key"].(string); pk != "" {
gotBy[pk] = n
}
}
for _, pk := range pks {
n, ok := gotBy[pk]
if !ok {
t.Errorf("node %s missing from response", pk[:4])
continue
}
if _, has := n["bridge_score"]; !has {
t.Errorf("node %s: bridge_score field absent from response", pk[:4])
}
}
// Middle node B must report a non-zero bridge_score; end node A
// must report exactly zero. These two assertions together prevent
// a "field present but always 0" regression.
if v, _ := gotBy[pks[1]]["bridge_score"].(float64); v <= 0 {
t.Errorf("middle node B bridge_score in API response should be > 0, got %v", v)
}
if v, _ := gotBy[pks[0]]["bridge_score"].(float64); v != 0 {
t.Errorf("end node A bridge_score in API response should be 0, got %v", v)
}
}