Files
beacon-server/internal/ingest/packet_endpoints_test.go
MrAlders0n 98006a9364 feat(stats): serve historical stats from hourly rollups
Stats endpoints read hourly rollup tables filled once per closed hour by one
advisory-locked task, instead of materialized views that re-scanned every
observation on each refresh. IATA-set tables give exact distinct counts for any
IATA set or region. Cleanup holds raw rows for unrolled hours (up to 24h).

Adds /stats/series; ingest keeps packet observation counts, observer last IATA,
channel message counts and trace tag summaries. 001_baseline.sql is rewritten,
so pre-release 2.0 databases must be recreated. See docs/historical-stats.md.
2026-10-02 07:43:56 -04:00

100 lines
3.6 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package ingest
import (
"context"
"encoding/json"
"reflect"
"testing"
"time"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/MeshCore-Beacon/beacon-server/internal/hub"
"github.com/google/uuid"
"github.com/meshcore-go/meshcore-go"
)
type endpointCaptureDB struct {
*stubDB
node api.ResolvedNode
observed int
}
func (s *endpointCaptureDB) GetNodeByPubkey(context.Context, []byte) (uuid.UUID, error) {
return s.node.ID, nil
}
func (s *endpointCaptureDB) UpsertNode(context.Context, UpsertNodeParams, RadioSettings) (uuid.UUID, error) {
return s.node.ID, nil
}
func (s *endpointCaptureDB) GetNodesByIDs(context.Context, []uuid.UUID) (map[uuid.UUID]*api.ResolvedNode, error) {
return map[uuid.UUID]*api.ResolvedNode{s.node.ID: &s.node}, nil
}
func (s *endpointCaptureDB) ResolveEndpointHashes(_ context.Context, _ string, hashes [][]byte) (map[string][]api.ResolvedPathEntry, error) {
if len(hashes) == 1 && hashes[0][0] == 0xaa {
return map[string][]api.ResolvedPathEntry{"aa": {{NodeID: s.node.ID, Name: s.node.Name, PublicKey: []byte{0xaa}}}}, nil
}
return nil, nil
}
func (s *endpointCaptureDB) InsertObservation(context.Context, InsertObservationParams) (bool, int64, error) {
s.observed++
return true, 1, nil
}
// Stored rows resolve endpoints at read time, but live events still carry them.
func TestHandlePacketBroadcastsEndpoints(t *testing.T) {
name := "Companion"
for _, kind := range []string{"advert", "direct message"} {
t.Run(kind, func(t *testing.T) {
w, base := newTestWorker()
db := &endpointCaptureDB{stubDB: base, node: api.ResolvedNode{ID: uuid.New(), Name: &name, PublicKey: "aa"}}
w.db = db
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go w.hub.Run()
client := w.hub.NewClient()
w.hub.AddScope(client, "endpoints", hub.Scope{Events: []hub.EventType{hub.EventPacketObservation, hub.EventObserverStatus}})
defer w.hub.Remove(client)
waitForSummarySubscriber(t, ctx, w.hub, client)
packet := buildAdvertPacket(t, false)
wantSource := api.ResolveExactNode(&db.node)
var wantDestination *api.ResolvedHop
if kind == "direct message" {
packet = &meshcore.Packet{Header: meshcore.MakeHeader(meshcore.RouteTypeFlood, meshcore.PayloadTypeTxtMsg, 0), Payload: append([]byte{0xbb, 0xaa, 0, 0}, make([]byte, 16)...)}
wantSource = api.BuildResolvedPath([][]byte{{0xaa}}, map[string][]api.ResolvedPathEntry{"aa": {{NodeID: db.node.ID, Name: &name, PublicKey: []byte{0xaa}}}})[0]
d := api.BuildResolvedPath([][]byte{{0xbb}}, nil)[0]
wantDestination = &d
}
w.handlePacket(ctx, "YYZ", "0102", packetEnvelope(t, packet))
if db.observed != 1 {
t.Fatalf("observation written %d times", db.observed)
}
for {
select {
case event := <-client.Send:
if event.Type != hub.EventPacketObservation {
continue
}
var got struct {
Observation struct {
ResolvedSource *api.ResolvedHop `json:"resolvedSource"`
ResolvedDestination *api.ResolvedHop `json:"resolvedDestination"`
} `json:"observation"`
}
if err := json.Unmarshal(event.Payload, &got); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got.Observation.ResolvedSource, &wantSource) || !reflect.DeepEqual(got.Observation.ResolvedDestination, wantDestination) {
t.Fatalf("live endpoints: got %+v / %+v", got.Observation.ResolvedSource, got.Observation.ResolvedDestination)
}
return
case <-ctx.Done():
t.Fatal("packet observation event missing")
}
}
})
}
}