mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-17 13:40:05 +00:00
fix(ingest): honor explicit zero locations in adverts (#139)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
// Copyright 2026 Beacon Contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
|
||||
"github.com/MeshCore-Beacon/beacon-server/internal/ingest"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func TestNodeLocationResetPostgres(t *testing.T) {
|
||||
dsn := os.Getenv("BEACON_TEST_POSTGRES_DSN")
|
||||
if dsn == "" {
|
||||
t.Skip("set BEACON_TEST_POSTGRES_DSN for PostgreSQL regression")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
conn, err := pgx.Connect(ctx, dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close(context.Background())
|
||||
tx, err := conn.Begin(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tx.Rollback(context.Background())
|
||||
if _, err := tx.Exec(ctx, "CREATE TEMP TABLE nodes (LIKE public.nodes INCLUDING ALL) ON COMMIT DROP"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store := &Store{q: sqlc.New(tx)}
|
||||
lat, lon := 45.0, -75.0
|
||||
params := ingest.UpsertNodeParams{PublicKey: []byte{0x97}, NodeType: 2, Name: "location fixture", Latitude: &lat, Longitude: &lon}
|
||||
id, err := store.UpsertNode(ctx, params, ingest.RadioSettings{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
present bool
|
||||
lat, lon float64
|
||||
}{
|
||||
{"omission preserves location", false, 45, -75},
|
||||
{"explicit reset replaces location", true, 0, 0},
|
||||
{"omission preserves reset", false, 0, 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
params.Latitude, params.Longitude = nil, nil
|
||||
if tc.present {
|
||||
params.Latitude, params.Longitude = &tc.lat, &tc.lon
|
||||
}
|
||||
updatedID, err := store.UpsertNode(ctx, params, ingest.RadioSettings{})
|
||||
if err != nil || updatedID != id {
|
||||
t.Fatalf("node identity changed: %v", err)
|
||||
}
|
||||
var gotLat, gotLon float64
|
||||
if err := tx.QueryRow(ctx, "SELECT latitude,longitude FROM nodes WHERE id=$1", id).Scan(&gotLat, &gotLon); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotLat != tc.lat || gotLon != tc.lon {
|
||||
t.Fatalf("location = (%g,%g), want (%g,%g)", gotLat, gotLon, tc.lat, tc.lon)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -175,6 +175,7 @@ type stubDB struct {
|
||||
setCapabilityCalls []setCapabilityCall
|
||||
|
||||
upsertNodeCalls int
|
||||
upsertNodeParams UpsertNodeParams
|
||||
upsertChannelCalls int
|
||||
upsertChannelHashOnlyCalls int
|
||||
upsertChannelIATACalls int
|
||||
@@ -209,8 +210,9 @@ func (s *stubDB) InsertObservation(_ context.Context, _ InsertObservationParams)
|
||||
return s.observationInserted, nil
|
||||
}
|
||||
func (s *stubDB) SetNodeDefaultScope(_ context.Context, _ uuid.UUID, _ int32) error { return nil }
|
||||
func (s *stubDB) UpsertNode(_ context.Context, _ UpsertNodeParams, _ RadioSettings) (uuid.UUID, error) {
|
||||
func (s *stubDB) UpsertNode(_ context.Context, params UpsertNodeParams, _ RadioSettings) (uuid.UUID, error) {
|
||||
s.upsertNodeCalls++
|
||||
s.upsertNodeParams = params
|
||||
return uuid.Nil, nil
|
||||
}
|
||||
func (s *stubDB) UpsertNodeIATA(_ context.Context, _ uuid.UUID, _ string) error { return nil }
|
||||
|
||||
@@ -80,7 +80,8 @@ func (w *Worker) handlePayloadTypeSideEffects(ctx context.Context, packet *meshc
|
||||
return
|
||||
}
|
||||
var lat, lon *float64
|
||||
if advert.AppData().Lat != 0 || advert.AppData().Lon != 0 {
|
||||
// Presence, not value: an explicit 0/0 advert resets the stored location.
|
||||
if advert.Flags()&meshcore.AdvertLatLonMask != 0 {
|
||||
la := float64(advert.AppData().Lat) / 1e6
|
||||
lo := float64(advert.AppData().Lon) / 1e6
|
||||
lat = &la
|
||||
|
||||
@@ -6,6 +6,7 @@ package ingest
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
@@ -31,6 +32,10 @@ func (k *mapKeys) GetKey(hash []byte) []keystore.Entry {
|
||||
// buildAdvertPacket signs (or, if tamper is true, signs then mutates) an
|
||||
// advert payload and wraps it in a minimal Packet with no path (zero-hop).
|
||||
func buildAdvertPacket(t *testing.T, tamper bool) *meshcore.Packet {
|
||||
return buildAdvertPacketWithData(t, []byte{meshcore.AdvertTypeRepeater}, tamper)
|
||||
}
|
||||
|
||||
func buildAdvertPacketWithData(t *testing.T, data []byte, tamper bool) *meshcore.Packet {
|
||||
t.Helper()
|
||||
pub, priv, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
@@ -43,7 +48,7 @@ func buildAdvertPacket(t *testing.T, tamper bool) *meshcore.Packet {
|
||||
advert := &meshcore.Advert{
|
||||
PublicKey: id,
|
||||
Timestamp: 12345,
|
||||
RawAppData: []byte{meshcore.AdvertTypeRepeater}, // flags byte only, no optional fields
|
||||
RawAppData: data,
|
||||
}
|
||||
advert.Sign(priv)
|
||||
if tamper {
|
||||
@@ -61,6 +66,54 @@ func buildAdvertPacket(t *testing.T, tamper bool) *meshcore.Packet {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdvertLocationPresence(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
present, tamper bool
|
||||
lat, lon int32
|
||||
}{
|
||||
{"ordinary location", true, false, 45000000, -75000000},
|
||||
{"explicit reset", true, false, 0, 0},
|
||||
{"zero latitude", true, false, 0, -75000000},
|
||||
{"zero longitude", true, false, 45000000, 0},
|
||||
{"no location", false, false, 0, 0},
|
||||
{"tampered reset", true, true, 0, 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
data := []byte{meshcore.AdvertTypeRepeater}
|
||||
if tc.present {
|
||||
// Preserve the wire presence bit even for zero coordinates; the
|
||||
// library's AppData encoder omits location when both values are zero.
|
||||
data[0] |= meshcore.AdvertLatLonMask
|
||||
data = binary.LittleEndian.AppendUint32(data, uint32(tc.lat))
|
||||
data = binary.LittleEndian.AppendUint32(data, uint32(tc.lon))
|
||||
}
|
||||
worker, store := newTestWorker()
|
||||
packet := buildAdvertPacketWithData(t, data, tc.tamper)
|
||||
worker.handlePayloadTypeSideEffects(context.Background(), packet, "YOW", []byte{1}, RadioSettings{}, nil, nil, nil, 0)
|
||||
if tc.tamper {
|
||||
if store.upsertNodeCalls != 0 {
|
||||
t.Fatal("invalid signature updated the node")
|
||||
}
|
||||
return
|
||||
}
|
||||
if store.upsertNodeCalls != 1 {
|
||||
t.Fatal("signed advert did not update the node")
|
||||
}
|
||||
got := store.upsertNodeParams
|
||||
if !tc.present {
|
||||
if got.Latitude != nil || got.Longitude != nil {
|
||||
t.Fatal("absent location became an update")
|
||||
}
|
||||
return
|
||||
}
|
||||
if got.Latitude == nil || got.Longitude == nil || *got.Latitude != float64(tc.lat)/1e6 || *got.Longitude != float64(tc.lon)/1e6 {
|
||||
t.Fatal("advertised coordinates did not reach the node update")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePayloadTypeSideEffects_Advert_ValidSignature_UpsertsNode(t *testing.T) {
|
||||
w, db := newTestWorker()
|
||||
packet := buildAdvertPacket(t, false)
|
||||
|
||||
Reference in New Issue
Block a user