mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-16 03:00:29 +00:00
Storing a full candidate snapshot on every observation was the wrong call. The same node lists got copied thousands of times, about 22 GB on prod. Resolve endpoints live like path hops and drop the column.
141 lines
5.0 KiB
Go
141 lines
5.0 KiB
Go
// Copyright 2026 Beacon Contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sort"
|
|
"testing"
|
|
|
|
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
|
|
mockdb "github.com/MeshCore-Beacon/beacon-server/db/sqlc/mock"
|
|
"github.com/MeshCore-Beacon/beacon-server/internal/api"
|
|
"github.com/google/uuid"
|
|
"github.com/meshcore-go/meshcore-go"
|
|
"go.uber.org/mock/gomock"
|
|
)
|
|
|
|
func TestParsePacketEndpoints(t *testing.T) {
|
|
msg := append([]byte{0xbb, 0xaa, 0, 0}, make([]byte, 16)...)
|
|
key := []byte{1, 2, 3}
|
|
for _, tc := range []struct {
|
|
name string
|
|
kind uint8
|
|
raw, key []byte
|
|
want packetEndpoints
|
|
}{
|
|
{"direct message", meshcore.PayloadTypeTxtMsg, msg, nil, packetEndpoints{source: []byte{0xaa}, destination: []byte{0xbb}}},
|
|
{"request", meshcore.PayloadTypeReq, msg, nil, packetEndpoints{source: []byte{0xaa}, destination: []byte{0xbb}}},
|
|
{"advert", meshcore.PayloadTypeAdvert, nil, key, packetEndpoints{advert: true, advertKey: key}},
|
|
{"channel message", meshcore.PayloadTypeGrpTxt, msg, nil, packetEndpoints{}},
|
|
{"truncated", meshcore.PayloadTypeTxtMsg, []byte{0xbb}, nil, packetEndpoints{}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := parsePacketEndpoints(int16(tc.kind), tc.raw, tc.key)
|
|
if got.advert != tc.want.advert || string(got.advertKey) != string(tc.want.advertKey) ||
|
|
string(got.source) != string(tc.want.source) || string(got.destination) != string(tc.want.destination) {
|
|
t.Fatalf("got %+v, want %+v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func hopNames(h *api.ResolvedHop) []string {
|
|
if h == nil {
|
|
return nil
|
|
}
|
|
names := []string{h.Confidence}
|
|
for _, n := range h.Nodes {
|
|
names = append(names, *n.Name)
|
|
}
|
|
sort.Strings(names[1:])
|
|
return names
|
|
}
|
|
|
|
func TestResolveEndpointsBatchesAPage(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
mock := mockdb.NewMockQuerier(ctrl)
|
|
store := &Store{q: mock}
|
|
name := func(s string) *string { return &s }
|
|
alice, bob, carol := uuid.New(), uuid.New(), uuid.New()
|
|
advertKey := []byte{0xaa, 0x11}
|
|
|
|
mock.EXPECT().ResolveEndpointHashPairs(gomock.Any(), gomock.Any()).DoAndReturn(
|
|
func(_ context.Context, p sqlc.ResolveEndpointHashPairsParams) ([]sqlc.ResolveEndpointHashPairsRow, error) {
|
|
if len(p.Iatas) != 2 || len(p.Hashes) != 2 {
|
|
t.Errorf("want distinct IATAs and hashes, got %v %v", p.Iatas, p.Hashes)
|
|
}
|
|
return []sqlc.ResolveEndpointHashPairsRow{
|
|
{Iata: "YYZ", Hash: []byte{0xaa}, NodeID: alice, Name: name("Alice")},
|
|
{Iata: "YYZ", Hash: []byte{0xaa}, NodeID: bob, Name: name("Bob")},
|
|
{Iata: "YYZ", Hash: []byte{0xbb}, NodeID: carol, Name: name("Carol")},
|
|
{Iata: "YVR", Hash: []byte{0xaa}, NodeID: alice, Name: name("Alice")},
|
|
{Iata: "YVR", Hash: []byte{0xbb}, NodeID: carol, Name: name("Carol")}, // cross-product extra
|
|
}, nil
|
|
})
|
|
mock.EXPECT().GetNodesByPubkeys(gomock.Any(), [][]byte{advertKey}).Return(
|
|
[]sqlc.GetNodesByPubkeysRow{{ID: alice, PublicKey: advertKey, Name: name("Alice")}}, nil)
|
|
|
|
sources, destinations := store.resolveEndpoints(context.Background(), []endpointLookup{
|
|
{iata: "YYZ", ep: packetEndpoints{source: []byte{0xaa}, destination: []byte{0xbb}}},
|
|
{iata: "YVR", ep: packetEndpoints{source: []byte{0xaa}}},
|
|
{iata: "YYZ", ep: packetEndpoints{advert: true, advertKey: advertKey}},
|
|
{iata: "YYZ", ep: packetEndpoints{advert: true, advertKey: advertKey}},
|
|
{iata: "YYZ", ep: packetEndpoints{advert: true}},
|
|
{},
|
|
})
|
|
|
|
for i, tc := range []struct{ source, destination []string }{
|
|
{[]string{"ambiguous", "Alice", "Bob"}, []string{"high", "Carol"}},
|
|
{[]string{"high", "Alice"}, nil},
|
|
{[]string{"high", "Alice"}, nil},
|
|
{[]string{"high", "Alice"}, nil},
|
|
{[]string{"none"}, nil},
|
|
{nil, nil},
|
|
} {
|
|
if got := hopNames(sources[i]); !equalStrings(got, tc.source) {
|
|
t.Errorf("row %d source = %v, want %v", i, got, tc.source)
|
|
}
|
|
if got := hopNames(destinations[i]); !equalStrings(got, tc.destination) {
|
|
t.Errorf("row %d destination = %v, want %v", i, got, tc.destination)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolveEndpointsFailedLookupLeavesHopsEmpty(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
mock := mockdb.NewMockQuerier(ctrl)
|
|
store := &Store{q: mock}
|
|
mock.EXPECT().ResolveEndpointHashPairs(gomock.Any(), gomock.Any()).Return(nil, errors.New("boom"))
|
|
|
|
sources, destinations := store.resolveEndpoints(context.Background(), []endpointLookup{
|
|
{iata: "YYZ", ep: packetEndpoints{source: []byte{0xaa}, destination: []byte{0xbb}}},
|
|
})
|
|
if sources[0] != nil || destinations[0] != nil {
|
|
t.Fatal("a failed lookup must not report endpoints as unresolved")
|
|
}
|
|
}
|
|
|
|
func TestResolveEndpointsSkipsQueriesWhenNothingToResolve(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
store := &Store{q: mockdb.NewMockQuerier(ctrl)} // no EXPECT: any query fails the test
|
|
sources, _ := store.resolveEndpoints(context.Background(), []endpointLookup{{}, {iata: "YYZ"}})
|
|
if len(sources) != 2 || sources[0] != nil || sources[1] != nil {
|
|
t.Fatal("rows without endpoints must stay empty")
|
|
}
|
|
}
|
|
|
|
func equalStrings(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|