mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-30 04:57:57 +00:00
SaarMesh-Bot flagged on PR #1867 that the server's one-time detectSchema() PRAGMA snapshot racing the ingestor's ALTER TABLE migrations isn't specific to configured_scope -- it's a general property of every optional column, and offered to send a fix. Built it ourselves instead. hasResolvedPath/hasObsRawHex/hasScopeName/hasDefaultScope/ hasConfiguredScope/hasMultibyteSupCols/hasLastSeen are now schemaFlag (atomic.Bool) methods instead of plain bools, self-healed by a background ticker (healSchemaFlags) started once in OpenDB and stopped in Close(). Unlike an eager reprobe-on-read, the healer never nests inside another caller's already-open *sql.Rows cursor, so it can't self-deadlock a single-connection pool the way a naive "reprobe inside get()" version of this fix did (caught immediately by the existing test suite hanging). detectSchemaWithRetry's fixed 150ms budget is gone -- replaced by an unbounded self-heal that catches the migration whenever it actually lands. Every read site across db.go/store.go/chunked_load.go/main.go is now a method call; test fixtures that previously force-set the old bool fields now call .forceTrue() on the underlying schemaFlag.
316 lines
12 KiB
Go
316 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestHandleChannelMessages_EntryPointArea covers dborup's request: a
|
|
// channel message needs BOTH the scope it was sent with (already shown as
|
|
// "Scope: #dk (Danmark (alle))") AND the area the sender was actually
|
|
// physically in, resolved from the message's own path[0] entry-point
|
|
// repeater -- distinct because someone in Aarhus can still send with the
|
|
// broad #dk scope. Also confirms the internal "entryPrefix" field used to
|
|
// carry path[0] through to the resolution step never reaches the client.
|
|
func TestHandleChannelMessages_EntryPointArea(t *testing.T) {
|
|
srv, router := setupTestServer(t)
|
|
if _, err := srv.db.conn.Exec(`DELETE FROM transmissions`); err != nil {
|
|
t.Fatalf("clear transmissions: %v", err)
|
|
}
|
|
if _, err := srv.db.conn.Exec(`DELETE FROM observations`); err != nil {
|
|
t.Fatalf("clear observations: %v", err)
|
|
}
|
|
if !srv.db.hasScopeName() {
|
|
if _, err := srv.db.conn.Exec(`ALTER TABLE transmissions ADD COLUMN scope_name TEXT DEFAULT NULL`); err != nil {
|
|
t.Fatalf("add scope_name column: %v", err)
|
|
}
|
|
srv.db.hasScopeNameFlag.forceTrue()
|
|
}
|
|
|
|
f := func(v float64) *float64 { return &v }
|
|
srv.cfg.Areas = map[string]AreaEntry{
|
|
"AAR": {Label: "Aarhus by", LatMin: f(56.05), LatMax: f(56.25), LonMin: f(9.95), LonMax: f(10.35)},
|
|
}
|
|
|
|
// A repeater physically in Aarhus, resolvable only via a unique
|
|
// full-length prefix match (mirrors TestResolveHopsAPI_UniquePrefix).
|
|
if _, err := srv.db.conn.Exec("INSERT OR IGNORE INTO nodes (public_key, name, lat, lon, role) VALUES (?, ?, ?, ?, ?)",
|
|
"aar0011223344", "AarhusRepeater", 56.1503, 10.1965, "repeater"); err != nil {
|
|
t.Fatalf("insert node: %v", err)
|
|
}
|
|
srv.store.InvalidateNodeCache()
|
|
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
res, err := srv.db.conn.Exec(
|
|
`INSERT INTO transmissions (raw_hex,hash,first_seen,route_type,payload_type,channel_hash,decoded_json,scope_name) VALUES (?,?,?,0,5,'#dk',?,'#dk')`,
|
|
"aa", "chmsg1", now, `{"sender":"AarhusSender","text":"AarhusSender: hey from #dk"}`,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("insert tx: %v", err)
|
|
}
|
|
txID, _ := res.LastInsertId()
|
|
|
|
obsRes, err := srv.db.conn.Exec(`INSERT INTO observers (id, name, iata) VALUES (?,?,?)`, "obsAAR", "AarhusObs", "AAR")
|
|
if err != nil {
|
|
t.Fatalf("insert observer: %v", err)
|
|
}
|
|
obsIdx, _ := obsRes.LastInsertId()
|
|
if _, err := srv.db.conn.Exec(
|
|
`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp) VALUES (?,?,?,?,?,?)`,
|
|
txID, obsIdx, 1.0, -90.0, `["aar0011223344"]`, time.Now().Unix(),
|
|
); err != nil {
|
|
t.Fatalf("insert observation: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/api/channels/%23dk/messages?limit=10", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 200 {
|
|
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
messages, _ := body["messages"].([]interface{})
|
|
if len(messages) != 1 {
|
|
t.Fatalf("messages = %+v, want 1", messages)
|
|
}
|
|
msg, _ := messages[0].(map[string]interface{})
|
|
|
|
if msg["scope"] != "#dk" {
|
|
t.Errorf("scope = %v, want #dk", msg["scope"])
|
|
}
|
|
if msg["area"] != "Aarhus by" {
|
|
t.Errorf("area = %v, want \"Aarhus by\" (resolved from path[0], not the #dk scope)", msg["area"])
|
|
}
|
|
if _, present := msg["entryPrefix"]; present {
|
|
t.Error("entryPrefix must never reach the client -- it's an internal-only intermediate field")
|
|
}
|
|
}
|
|
|
|
// TestHandleChannelMessages_EntryPointArea_Unresolved confirms "area" is
|
|
// simply omitted (never guessed) when the entry-point prefix doesn't
|
|
// resolve unambiguously, or when no areas are configured at all.
|
|
func TestHandleChannelMessages_EntryPointArea_Unresolved(t *testing.T) {
|
|
srv, router := setupTestServer(t)
|
|
if _, err := srv.db.conn.Exec(`DELETE FROM transmissions`); err != nil {
|
|
t.Fatalf("clear transmissions: %v", err)
|
|
}
|
|
if _, err := srv.db.conn.Exec(`DELETE FROM observations`); err != nil {
|
|
t.Fatalf("clear observations: %v", err)
|
|
}
|
|
if !srv.db.hasScopeName() {
|
|
if _, err := srv.db.conn.Exec(`ALTER TABLE transmissions ADD COLUMN scope_name TEXT DEFAULT NULL`); err != nil {
|
|
t.Fatalf("add scope_name column: %v", err)
|
|
}
|
|
srv.db.hasScopeNameFlag.forceTrue()
|
|
}
|
|
// No areas configured at all.
|
|
srv.cfg.Areas = nil
|
|
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
res, err := srv.db.conn.Exec(
|
|
`INSERT INTO transmissions (raw_hex,hash,first_seen,route_type,payload_type,channel_hash,decoded_json,scope_name) VALUES (?,?,?,0,5,'#dk',?,'#dk')`,
|
|
"aa", "chmsg2", now, `{"sender":"Someone","text":"Someone: hi"}`,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("insert tx: %v", err)
|
|
}
|
|
txID, _ := res.LastInsertId()
|
|
obsRes, err := srv.db.conn.Exec(`INSERT INTO observers (id, name, iata) VALUES (?,?,?)`, "obsX", "ObsX", "XXX")
|
|
if err != nil {
|
|
t.Fatalf("insert observer: %v", err)
|
|
}
|
|
obsIdx, _ := obsRes.LastInsertId()
|
|
if _, err := srv.db.conn.Exec(
|
|
`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp) VALUES (?,?,?,?,?,?)`,
|
|
txID, obsIdx, 1.0, -90.0, `["deadbeef99"]`, time.Now().Unix(),
|
|
); err != nil {
|
|
t.Fatalf("insert observation: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/api/channels/%23dk/messages?limit=10", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 200 {
|
|
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
messages, _ := body["messages"].([]interface{})
|
|
if len(messages) != 1 {
|
|
t.Fatalf("messages = %+v, want 1", messages)
|
|
}
|
|
msg, _ := messages[0].(map[string]interface{})
|
|
if _, present := msg["area"]; present {
|
|
t.Errorf("area = %v, want absent (no areas configured)", msg["area"])
|
|
}
|
|
if _, present := msg["entryPrefix"]; present {
|
|
t.Error("entryPrefix must never reach the client")
|
|
}
|
|
}
|
|
|
|
// TestHandleChannelMessages_EntryPointArea_DirectObserverFallback covers a
|
|
// 0-hop (direct) reception: there's no relay path at all, so path[0]
|
|
// resolution has nothing to work with, but the hearing station's own GPS
|
|
// fix is a reasonable stand-in for "where this happened" at 0 hops. The
|
|
// observer here has role "client" (not repeater), deliberately proving the
|
|
// fallback bypasses the path-hop role filter that buildPrefixMap applies --
|
|
// a listening station doesn't need to be relay-eligible to have its own
|
|
// position count here.
|
|
func TestHandleChannelMessages_EntryPointArea_DirectObserverFallback(t *testing.T) {
|
|
srv, router := setupTestServer(t)
|
|
if _, err := srv.db.conn.Exec(`DELETE FROM transmissions`); err != nil {
|
|
t.Fatalf("clear transmissions: %v", err)
|
|
}
|
|
if _, err := srv.db.conn.Exec(`DELETE FROM observations`); err != nil {
|
|
t.Fatalf("clear observations: %v", err)
|
|
}
|
|
if !srv.db.hasScopeName() {
|
|
if _, err := srv.db.conn.Exec(`ALTER TABLE transmissions ADD COLUMN scope_name TEXT DEFAULT NULL`); err != nil {
|
|
t.Fatalf("add scope_name column: %v", err)
|
|
}
|
|
srv.db.hasScopeNameFlag.forceTrue()
|
|
}
|
|
|
|
f := func(v float64) *float64 { return &v }
|
|
srv.cfg.Areas = map[string]AreaEntry{
|
|
"DK_DJURS": {Label: "Djursland", LatMin: f(56.10), LatMax: f(56.55), LonMin: f(10.35), LonMax: f(10.90)},
|
|
}
|
|
|
|
// The hearing station itself: a plain client, not a repeater, sitting
|
|
// in Djursland.
|
|
if _, err := srv.db.conn.Exec("INSERT OR IGNORE INTO nodes (public_key, name, lat, lon, role) VALUES (?, ?, ?, ?, ?)",
|
|
"pkebeltoftobserver", "DK_8400_Ebeltoft Observer", 56.1959, 10.6801, "client"); err != nil {
|
|
t.Fatalf("insert node: %v", err)
|
|
}
|
|
srv.store.InvalidateNodeCache()
|
|
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
res, err := srv.db.conn.Exec(
|
|
`INSERT INTO transmissions (raw_hex,hash,first_seen,route_type,payload_type,channel_hash,decoded_json,scope_name) VALUES (?,?,?,0,5,'#test',?,'#dk')`,
|
|
"aa", "chmsgdirect1", now, `{"sender":"HSVI","text":"HSVI: Tak tak"}`,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("insert tx: %v", err)
|
|
}
|
|
txID, _ := res.LastInsertId()
|
|
|
|
obsRes, err := srv.db.conn.Exec(`INSERT INTO observers (id, name, iata) VALUES (?,?,?)`,
|
|
"pkebeltoftobserver", "DK_8400_Ebeltoft Observer", "EBT")
|
|
if err != nil {
|
|
t.Fatalf("insert observer: %v", err)
|
|
}
|
|
obsIdx, _ := obsRes.LastInsertId()
|
|
// path_json '[]' -- direct reception, no relay hops at all.
|
|
if _, err := srv.db.conn.Exec(
|
|
`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp) VALUES (?,?,?,?,?,?)`,
|
|
txID, obsIdx, 12.8, -70.0, `[]`, time.Now().Unix(),
|
|
); err != nil {
|
|
t.Fatalf("insert observation: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/api/channels/%23test/messages?limit=10", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 200 {
|
|
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
messages, _ := body["messages"].([]interface{})
|
|
if len(messages) != 1 {
|
|
t.Fatalf("messages = %+v, want 1", messages)
|
|
}
|
|
msg, _ := messages[0].(map[string]interface{})
|
|
|
|
if msg["hops"] != float64(0) {
|
|
t.Errorf("hops = %v, want 0 (direct)", msg["hops"])
|
|
}
|
|
if msg["area"] != "Djursland" {
|
|
t.Errorf("area = %v, want \"Djursland\" (resolved from the hearing station's own position, no relay path needed)", msg["area"])
|
|
}
|
|
if _, present := msg["entryObserverPubkey"]; present {
|
|
t.Error("entryObserverPubkey must never reach the client -- it's an internal-only intermediate field")
|
|
}
|
|
}
|
|
|
|
// TestHandleChannelMessages_EntryPointArea_DirectObserverFallback_NoGPS
|
|
// confirms the fallback stays silent (never guesses) when the hearing
|
|
// station of a 0-hop message has no GPS fix of its own on file -- the
|
|
// common case, since most observers aren't also positioned mesh nodes.
|
|
func TestHandleChannelMessages_EntryPointArea_DirectObserverFallback_NoGPS(t *testing.T) {
|
|
srv, router := setupTestServer(t)
|
|
if _, err := srv.db.conn.Exec(`DELETE FROM transmissions`); err != nil {
|
|
t.Fatalf("clear transmissions: %v", err)
|
|
}
|
|
if _, err := srv.db.conn.Exec(`DELETE FROM observations`); err != nil {
|
|
t.Fatalf("clear observations: %v", err)
|
|
}
|
|
if !srv.db.hasScopeName() {
|
|
if _, err := srv.db.conn.Exec(`ALTER TABLE transmissions ADD COLUMN scope_name TEXT DEFAULT NULL`); err != nil {
|
|
t.Fatalf("add scope_name column: %v", err)
|
|
}
|
|
srv.db.hasScopeNameFlag.forceTrue()
|
|
}
|
|
|
|
f := func(v float64) *float64 { return &v }
|
|
srv.cfg.Areas = map[string]AreaEntry{
|
|
"DK_DJURS": {Label: "Djursland", LatMin: f(56.20), LatMax: f(56.55), LonMin: f(10.35), LonMax: f(10.90)},
|
|
}
|
|
// Deliberately no nodes row for the observer -- no GPS fix on file.
|
|
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
res, err := srv.db.conn.Exec(
|
|
`INSERT INTO transmissions (raw_hex,hash,first_seen,route_type,payload_type,channel_hash,decoded_json,scope_name) VALUES (?,?,?,0,5,'#test',?,'#dk')`,
|
|
"aa", "chmsgdirect2", now, `{"sender":"Someone","text":"Someone: hi"}`,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("insert tx: %v", err)
|
|
}
|
|
txID, _ := res.LastInsertId()
|
|
obsRes, err := srv.db.conn.Exec(`INSERT INTO observers (id, name, iata) VALUES (?,?,?)`,
|
|
"pknogps", "No GPS Observer", "XXX")
|
|
if err != nil {
|
|
t.Fatalf("insert observer: %v", err)
|
|
}
|
|
obsIdx, _ := obsRes.LastInsertId()
|
|
if _, err := srv.db.conn.Exec(
|
|
`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp) VALUES (?,?,?,?,?,?)`,
|
|
txID, obsIdx, 5.0, -90.0, `[]`, time.Now().Unix(),
|
|
); err != nil {
|
|
t.Fatalf("insert observation: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/api/channels/%23test/messages?limit=10", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 200 {
|
|
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
messages, _ := body["messages"].([]interface{})
|
|
if len(messages) != 1 {
|
|
t.Fatalf("messages = %+v, want 1", messages)
|
|
}
|
|
msg, _ := messages[0].(map[string]interface{})
|
|
if _, present := msg["area"]; present {
|
|
t.Errorf("area = %v, want absent (observer has no GPS fix on file)", msg["area"])
|
|
}
|
|
if _, present := msg["entryObserverPubkey"]; present {
|
|
t.Error("entryObserverPubkey must never reach the client")
|
|
}
|
|
}
|