Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions db/node_location_integration_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
4 changes: 3 additions & 1 deletion internal/ingest/ingest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ type stubDB struct {
setCapabilityCalls []setCapabilityCall

upsertNodeCalls int
upsertNodeParams UpsertNodeParams
upsertChannelCalls int
upsertChannelHashOnlyCalls int
upsertChannelIATACalls int
Expand Down Expand Up @@ -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 }
Expand Down
3 changes: 2 additions & 1 deletion internal/ingest/side_effects.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 54 additions & 1 deletion internal/ingest/side_effects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package ingest
import (
"context"
"crypto/ed25519"
"encoding/binary"
"encoding/hex"
"encoding/json"
"testing"
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)
Expand Down
Loading