mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-15 22:39:56 +00:00
fix(observers): preserve advert pages with missing origin keys (#138)
* test(observers): cover adverts with a missing origin key * fix(observers): preserve advert pages with missing origin keys
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// Copyright 2026 Beacon Contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
|
||||
"github.com/MeshCore-Beacon/beacon-server/internal/api"
|
||||
"github.com/MeshCore-Beacon/beacon-server/internal/api/handlers"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func TestObserverAdvertsPostgres(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())
|
||||
_, err = tx.Exec(ctx, `
|
||||
CREATE TEMP TABLE packets (LIKE public.packets INCLUDING ALL) ON COMMIT DROP;
|
||||
CREATE TEMP TABLE packet_observations (LIKE public.packet_observations INCLUDING ALL) ON COMMIT DROP;
|
||||
CREATE TEMP TABLE nodes (LIKE public.nodes INCLUDING ALL) ON COMMIT DROP;
|
||||
INSERT INTO nodes(public_key,node_type,name) VALUES (decode(repeat('01',32),'hex'),1,'Known fixture node');
|
||||
INSERT INTO packets(packet_hash,payload_type,payload_version,route_type,origin_pubkey,raw_payload,raw_header,first_heard_at,last_heard_at) VALUES
|
||||
(decode(repeat('a1',32),'hex'),4,0,1,NULL,'\x00','\x00',now(),now()),
|
||||
(decode(repeat('b2',32),'hex'),4,0,1,decode(repeat('01',32),'hex'),'\x00','\x00',now(),now());
|
||||
INSERT INTO packet_observations(id,packet_hash,observer_id,iata,heard_at,path_length_byte,hash_size,hop_count) VALUES
|
||||
(1,decode(repeat('a1',32),'hex'),'00000000-0000-0000-0000-000000000106','YOW',now(),0,1,0),
|
||||
(2,decode(repeat('b2',32),'hex'),'00000000-0000-0000-0000-000000000106','YOW',now(),0,1,0);`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
router := handlers.ObserversRouter(&Store{q: sqlc.New(tx)})
|
||||
request := func(query string) api.Page[api.AdvertObservation] {
|
||||
t.Helper()
|
||||
response := httptest.NewRecorder()
|
||||
router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/00000000-0000-0000-0000-000000000106/adverts?"+query, nil).WithContext(ctx))
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("advert page HTTP %d", response.Code)
|
||||
}
|
||||
var page api.Page[api.AdvertObservation]
|
||||
if err := json.Unmarshal(response.Body.Bytes(), &page); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return page
|
||||
}
|
||||
page := request("limit=50")
|
||||
if len(page.Items) != 2 || page.Items[0].ID != 1 || page.Items[1].ID != 2 {
|
||||
t.Fatal("missing-origin advert was not preserved in the ordered page")
|
||||
}
|
||||
if key := page.Items[0].NodePublicKey; key == nil || *key != "" {
|
||||
t.Errorf("missing origin key = %v, want an empty string", key)
|
||||
}
|
||||
if key := page.Items[1].NodePublicKey; key == nil || *key != strings.Repeat("01", 32) {
|
||||
t.Error("known origin key changed")
|
||||
}
|
||||
first := request("limit=1")
|
||||
if !first.HasMore || first.NextCursor == nil || *first.NextCursor != 1 {
|
||||
t.Fatal("missing-origin advert broke the cursor")
|
||||
}
|
||||
last := request("limit=1&cursor=1")
|
||||
if len(last.Items) != 1 || last.Items[0].ID != 2 || last.HasMore {
|
||||
t.Fatal("continuing after the missing-origin advert lost the known advert")
|
||||
}
|
||||
}
|
||||
@@ -313,6 +313,7 @@ ORDER BY count DESC;
|
||||
-- name: ListObserverAdverts :many
|
||||
-- Returns advert packets (payload_type=4) heard by a specific observer.
|
||||
-- Pass cursor=0 to start from the beginning, or the last seen id for pagination.
|
||||
-- Keep missing-origin adverts; the generated key field expects a string, not NULL.
|
||||
SELECT
|
||||
po.id,
|
||||
encode(po.packet_hash, 'hex') AS packet_hash_hex,
|
||||
@@ -323,7 +324,7 @@ SELECT
|
||||
po.snr,
|
||||
po.hop_count,
|
||||
n.name AS node_name,
|
||||
encode(p.origin_pubkey, 'hex') AS node_public_key
|
||||
COALESCE(encode(p.origin_pubkey, 'hex'), '')::text AS node_public_key
|
||||
FROM packet_observations po
|
||||
JOIN packets p ON p.packet_hash = po.packet_hash
|
||||
LEFT JOIN nodes n ON n.public_key = p.origin_pubkey
|
||||
|
||||
@@ -151,6 +151,7 @@ type Querier interface {
|
||||
ListObservationsForPacket(ctx context.Context, packetHash []byte) ([]ListObservationsForPacketRow, error)
|
||||
// Returns advert packets (payload_type=4) heard by a specific observer.
|
||||
// Pass cursor=0 to start from the beginning, or the last seen id for pagination.
|
||||
// Keep missing-origin adverts; the generated key field expects a string, not NULL.
|
||||
ListObserverAdverts(ctx context.Context, arg ListObserverAdvertsParams) ([]ListObserverAdvertsRow, error)
|
||||
// Pass cursor=0 to start from the beginning, or the last seen observer's rownum for pagination.
|
||||
// Note: observers use UUID PKs so we order by last_seen and use a keyset on last_seen+id.
|
||||
|
||||
@@ -2860,7 +2860,7 @@ SELECT
|
||||
po.snr,
|
||||
po.hop_count,
|
||||
n.name AS node_name,
|
||||
encode(p.origin_pubkey, 'hex') AS node_public_key
|
||||
COALESCE(encode(p.origin_pubkey, 'hex'), '')::text AS node_public_key
|
||||
FROM packet_observations po
|
||||
JOIN packets p ON p.packet_hash = po.packet_hash
|
||||
LEFT JOIN nodes n ON n.public_key = p.origin_pubkey
|
||||
@@ -2892,6 +2892,7 @@ type ListObserverAdvertsRow struct {
|
||||
|
||||
// Returns advert packets (payload_type=4) heard by a specific observer.
|
||||
// Pass cursor=0 to start from the beginning, or the last seen id for pagination.
|
||||
// Keep missing-origin adverts; the generated key field expects a string, not NULL.
|
||||
func (q *Queries) ListObserverAdverts(ctx context.Context, arg ListObserverAdvertsParams) ([]ListObserverAdvertsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listObserverAdverts, arg.ObserverID, arg.Column2, arg.Limit)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user