mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-19 16:29:55 +00:00
GET /traces takes an optional cursorTag so tags sharing a millisecond survive paging. Migrations run under an advisory lock and record each file in the same implicit transaction, so a crash can't strand a fresh database. Overview windowHours counts the hours it covers, hourly observer activity fills unrolled hours from raw rows, and integration fixtures are UTC.
159 lines
4.4 KiB
Go
159 lines
4.4 KiB
Go
// Copyright 2026 Beacon Contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
|
|
mockdb "github.com/MeshCore-Beacon/beacon-server/db/sqlc/mock"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"go.uber.org/mock/gomock"
|
|
)
|
|
|
|
func TestListTraceTags_Empty(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
mock := mockdb.NewMockQuerier(ctrl)
|
|
|
|
mock.EXPECT().
|
|
ListTraceTags(gomock.Any(), gomock.Any()).
|
|
Return([]sqlc.ListTraceTagsRow{}, nil)
|
|
|
|
store := &Store{q: mock}
|
|
items, err := store.ListTraceTags(context.Background(), []string{"YVR"}, "", "", time.Time{}, time.Time{}, time.Time{}, "", 10)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(items) != 0 {
|
|
t.Errorf("expected 0 items, got %d", len(items))
|
|
}
|
|
}
|
|
|
|
func TestListTraceTags_WithPayload(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
mock := mockdb.NewMockQuerier(ctrl)
|
|
|
|
firstHeard := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}
|
|
lastHeard := pgtype.Timestamptz{Time: time.UnixMilli(1700000001000), Valid: true}
|
|
payload := []byte(`{"pathHashes":["aabb","ccdd"],"snrValues":[10,20]}`)
|
|
|
|
mock.EXPECT().
|
|
ListTraceTags(gomock.Any(), gomock.Any()).
|
|
Return([]sqlc.ListTraceTagsRow{
|
|
{
|
|
TraceTag: "trace-001",
|
|
FirstHeardAt: firstHeard,
|
|
LastHeardAt: lastHeard,
|
|
PacketCount: 3,
|
|
IataCount: 1,
|
|
TraceType: "trace",
|
|
BestPayload: payload,
|
|
},
|
|
}, nil)
|
|
|
|
store := &Store{q: mock}
|
|
items, err := store.ListTraceTags(context.Background(), []string{"YVR"}, "", "", time.Time{}, time.Time{}, time.Time{}, "", 10)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(items) != 1 {
|
|
t.Fatalf("expected 1 item, got %d", len(items))
|
|
}
|
|
if items[0].TraceTag != "trace-001" {
|
|
t.Errorf("expected TraceTag trace-001, got %s", items[0].TraceTag)
|
|
}
|
|
if len(items[0].PathHashes) != 2 {
|
|
t.Errorf("expected 2 path hashes, got %d", len(items[0].PathHashes))
|
|
}
|
|
if items[0].SNRValues[0] != 10 {
|
|
t.Errorf("expected SNR 10, got %f", items[0].SNRValues[0])
|
|
}
|
|
}
|
|
|
|
func TestGetTraceByTag_Empty(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
mock := mockdb.NewMockQuerier(ctrl)
|
|
|
|
mock.EXPECT().
|
|
GetPacketsByTraceTag(gomock.Any(), "trace-001").
|
|
Return([]sqlc.GetPacketsByTraceTagRow{}, nil)
|
|
|
|
store := &Store{q: mock}
|
|
detail, err := store.GetTraceByTag(context.Background(), "trace-001")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if detail != nil {
|
|
t.Errorf("expected nil for empty result, got %v", detail)
|
|
}
|
|
}
|
|
|
|
func TestGetTraceByTag_WithPacket(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
mock := mockdb.NewMockQuerier(ctrl)
|
|
|
|
firstHeard := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}
|
|
lastHeard := pgtype.Timestamptz{Time: time.UnixMilli(1700000001000), Valid: true}
|
|
// aabbccdd is valid hex for the packet hash
|
|
parsedPayload := []byte(`{"pathHashes":["aabb"],"snrValues":[15.0],"flags":0}`)
|
|
|
|
scopeName := "default"
|
|
mock.EXPECT().
|
|
GetPacketsByTraceTag(gomock.Any(), "trace-001").
|
|
Return([]sqlc.GetPacketsByTraceTagRow{
|
|
{
|
|
PacketHashHex: "aabbccdd",
|
|
RouteType: 1,
|
|
ScopeName: &scopeName,
|
|
FirstHeardAt: firstHeard,
|
|
LastHeardAt: lastHeard,
|
|
ParsedPayload: parsedPayload,
|
|
Iatas: []string{"YVR"},
|
|
},
|
|
}, nil)
|
|
|
|
mock.EXPECT().
|
|
ResolvePathHashesP2(gomock.Any(), sqlc.ResolvePathHashesP2Params{
|
|
Iata: "YVR",
|
|
Column2: [][]byte{{0xaa, 0xbb}},
|
|
}).
|
|
Return([]sqlc.ResolvePathHashesP2Row{}, nil)
|
|
|
|
store := &Store{q: mock}
|
|
detail, err := store.GetTraceByTag(context.Background(), "trace-001")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if detail == nil {
|
|
t.Fatal("expected detail, got nil")
|
|
}
|
|
if detail.TraceTag != "trace-001" {
|
|
t.Errorf("expected TraceTag trace-001, got %s", detail.TraceTag)
|
|
}
|
|
if len(detail.Packets) != 1 {
|
|
t.Fatalf("expected 1 packet, got %d", len(detail.Packets))
|
|
}
|
|
if len(detail.Packets[0].RawPath) != 1 {
|
|
t.Errorf("expected 1 raw hop, got %d", len(detail.Packets[0].RawPath))
|
|
}
|
|
}
|
|
|
|
func TestGetTraceByTag_DBError(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
mock := mockdb.NewMockQuerier(ctrl)
|
|
|
|
mock.EXPECT().
|
|
GetPacketsByTraceTag(gomock.Any(), "trace-001").
|
|
Return(nil, errors.New("db error"))
|
|
|
|
store := &Store{q: mock}
|
|
_, err := store.GetTraceByTag(context.Background(), "trace-001")
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
}
|