maint(tests): start adding integration tests

first db <> api mapping

add filter to test coverage action to ignore generated
code like sqlc and swagger docs
This commit is contained in:
Enot (ded) Skelly
2026-06-15 13:26:03 -07:00
parent dfef16435c
commit 64ff9fc030
11 changed files with 2238 additions and 2 deletions
+6 -2
View File
@@ -29,13 +29,17 @@ jobs:
- name: Run tests with coverage
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
- name: Filter generated files from coverage
run: |
grep -v -e "db/sqlc" -e "docs" -e "iatadb/gen" coverage.out > coverage.filtered.out
- name: Display coverage
run: go tool cover -func=coverage.out
run: go tool cover -func=coverage.filtered.out
- name: Extract coverage percentage
id: coverage
run: |
COVERAGE=$(go tool cover -func=coverage.out | grep ^total | awk '{print $3}' | tr -d '%')
COVERAGE=$(go tool cover -func=coverage.filtered.out | grep ^total | awk '{print $3}' | tr -d '%')
echo "pct=$COVERAGE" >> $GITHUB_OUTPUT
- name: Update coverage badge
+1
View File
@@ -7,3 +7,4 @@ notes.md
roadmap.md
binaries/
cover.out
+306
View File
@@ -0,0 +1,306 @@
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 TestListChannels_Empty(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
ListChannels(gomock.Any(), sqlc.ListChannelsParams{
Column1: nil,
Column2: "",
Column3: pgtype.Timestamptz{},
Limit: 11,
}).
Return([]sqlc.Channel{}, nil)
store := &Store{q: mock}
page, err := store.ListChannels(context.Background(), 10, nil, "", 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(page.Items) != 0 {
t.Errorf("expected 0 items, got %d", len(page.Items))
}
if page.HasMore {
t.Error("expected HasMore false")
}
}
func TestListChannels_Pagination(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
isHashtag := false
keyKnown := true
lastSeen := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}
// return limit+1 rows to trigger HasMore
rows := make([]sqlc.Channel, 3)
for i := range rows {
rows[i] = sqlc.Channel{
ID: int32(i + 1),
ChannelHash: []byte{0xab, 0xcd},
LastSeen: lastSeen,
IsHashtag: &isHashtag,
KeyKnown: &keyKnown,
}
}
mock.EXPECT().
ListChannels(gomock.Any(), sqlc.ListChannelsParams{
Column1: nil,
Column2: "",
Column3: pgtype.Timestamptz{},
Limit: 3, // limit+1
}).
Return(rows, nil)
store := &Store{q: mock}
page, err := store.ListChannels(context.Background(), 2, nil, "", 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(page.Items) != 2 {
t.Errorf("expected 2 items, got %d", len(page.Items))
}
if !page.HasMore {
t.Error("expected HasMore true")
}
if page.NextCursor == nil {
t.Error("expected NextCursor to be set")
}
}
func TestListChannels_DBError(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
ListChannels(gomock.Any(), gomock.Any()).
Return(nil, errors.New("db error"))
store := &Store{q: mock}
_, err := store.ListChannels(context.Background(), 10, nil, "", 0)
if err == nil {
t.Fatal("expected error, got nil")
}
}
func TestGetChannel_Basic(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
isHashtag := false
keyKnown := true
msgCount := int64(5)
lastSeen := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}
mock.EXPECT().
GetChannelByID(gomock.Any(), int32(1)).
Return(sqlc.Channel{
ID: 1,
ChannelHash: []byte{0xab, 0xcd},
LastSeen: lastSeen,
IsHashtag: &isHashtag,
KeyKnown: &keyKnown,
MessageCount: &msgCount,
}, nil)
store := &Store{q: mock}
ch, err := store.GetChannel(context.Background(), 1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ch.ID != 1 {
t.Errorf("expected ID 1, got %d", ch.ID)
}
if ch.ChannelHash != "abcd" {
t.Errorf("expected ChannelHash abcd, got %s", ch.ChannelHash)
}
if ch.MessageCount != 5 {
t.Errorf("expected MessageCount 5, got %d", ch.MessageCount)
}
if ch.KeyFingerprint != nil {
t.Errorf("expected nil KeyFingerprint for non-hashtag channel")
}
}
func TestGetChannel_HashtagWithFingerprint(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
isHashtag := true
keyKnown := true
fp := []byte{0xde, 0xad, 0xbe, 0xef}
lastSeen := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}
mock.EXPECT().
GetChannelByID(gomock.Any(), int32(2)).
Return(sqlc.Channel{
ID: 2,
ChannelHash: []byte{0x01, 0x02},
LastSeen: lastSeen,
IsHashtag: &isHashtag,
KeyKnown: &keyKnown,
KeyFingerprint: fp,
}, nil)
store := &Store{q: mock}
ch, err := store.GetChannel(context.Background(), 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ch.KeyFingerprint == nil {
t.Fatal("expected KeyFingerprint to be set for hashtag channel")
}
if *ch.KeyFingerprint != "deadbeef" {
t.Errorf("expected KeyFingerprint deadbeef, got %s", *ch.KeyFingerprint)
}
}
func TestGetChannel_DBError(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
GetChannelByID(gomock.Any(), int32(1)).
Return(sqlc.Channel{}, errors.New("db error"))
store := &Store{q: mock}
_, err := store.GetChannel(context.Background(), 1)
if err == nil {
t.Fatal("expected error, got nil")
}
}
func TestListChannelMessages_AllChannels(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
sentAt := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}
senderName := "Alice"
content := "hello"
mock.EXPECT().
ListAllChannelMessages(gomock.Any(), sqlc.ListAllChannelMessagesParams{
Column1: pgtype.Timestamptz{},
Column2: "YVR",
Column3: "",
Column4: int64(0),
Limit: 3,
}).
Return([]sqlc.ListAllChannelMessagesRow{
{
ID: 1,
PacketHashHex: "deadbeef",
ChannelHash: []byte{0xab},
SenderName: &senderName,
Content: &content,
SentAt: sentAt,
ObservationCount: 2,
},
{
ID: 2,
PacketHashHex: "cafebabe",
ChannelHash: []byte{0xcd},
SenderName: &senderName,
Content: &content,
SentAt: sentAt,
ObservationCount: 1,
},
{
ID: 3,
PacketHashHex: "deadcafe",
ChannelHash: []byte{0xef},
SenderName: &senderName,
Content: &content,
SentAt: sentAt,
ObservationCount: 1,
},
}, nil)
store := &Store{q: mock}
page, err := store.ListChannelMessages(context.Background(), nil, time.Time{}, 2, []string{"YVR"}, "", 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(page.Items) != 2 {
t.Errorf("expected 2 items, got %d", len(page.Items))
}
if !page.HasMore {
t.Error("expected HasMore true")
}
if page.NextCursor == nil {
t.Error("expected NextCursor to be set")
}
}
func TestListChannelMessages_ByChannelID(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
channelID := int32(1)
sentAt := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}
senderName := "Bob"
content := "world"
mock.EXPECT().
ListChannelMessages(gomock.Any(), sqlc.ListChannelMessagesParams{
ChannelID: channelID,
Column2: pgtype.Timestamptz{},
Column3: "YVR",
Column4: "",
Column5: int64(0),
Limit: 3,
}).
Return([]sqlc.ListChannelMessagesRow{
{
ID: 1,
PacketHashHex: "deadbeef",
ChannelHash: []byte{0xab},
SenderName: &senderName,
Content: &content,
SentAt: sentAt,
ObservationCount: 1,
},
}, nil)
store := &Store{q: mock}
page, err := store.ListChannelMessages(context.Background(), &channelID, time.Time{}, 2, []string{"YVR"}, "", 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(page.Items) != 1 {
t.Errorf("expected 1 item, got %d", len(page.Items))
}
if page.HasMore {
t.Error("expected HasMore false")
}
}
func TestListChannelMessages_DBError(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
ListAllChannelMessages(gomock.Any(), gomock.Any()).
Return(nil, errors.New("db error"))
store := &Store{q: mock}
_, err := store.ListChannelMessages(context.Background(), nil, time.Time{}, 10, nil, "", 0)
if err == nil {
t.Fatal("expected error, got nil")
}
}
+148
View File
@@ -0,0 +1,148 @@
package db
import (
"context"
"errors"
"testing"
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
mockdb "github.com/MeshCore-Beacon/beacon-server/db/sqlc/mock"
"go.uber.org/mock/gomock"
)
func TestListIATAs(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
lat := 49.1967
lng := -123.1815
name := "Vancouver"
mock.EXPECT().
ListIATAs(gomock.Any()).
Return([]sqlc.IataCode{
{Iata: "YVR", DisplayName: &name, ApproxLat: &lat, ApproxLng: &lng},
}, nil)
store := &Store{q: mock}
iatas, err := store.ListIATAs(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(iatas) != 1 {
t.Fatalf("expected 1 iata, got %d", len(iatas))
}
if iatas[0].IATA != "YVR" {
t.Errorf("expected IATA YVR, got %s", iatas[0].IATA)
}
if *iatas[0].Lat != lat {
t.Errorf("expected Lat %f, got %f", lat, *iatas[0].Lat)
}
}
func TestGetIATA(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
lat := 49.1967
lng := -123.1815
name := "Vancouver"
mock.EXPECT().
GetIATA(gomock.Any(), "YVR").
Return(sqlc.IataCode{Iata: "YVR", DisplayName: &name, ApproxLat: &lat, ApproxLng: &lng}, nil)
store := &Store{q: mock}
iata, err := store.GetIATA(context.Background(), "YVR")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if iata.IATA != "YVR" {
t.Errorf("expected YVR, got %s", iata.IATA)
}
if *iata.Lng != lng {
t.Errorf("expected Lng %f, got %f", lng, *iata.Lng)
}
}
func TestGetRegion_WithZoomLevel(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
zoom := int32(10)
mock.EXPECT().
GetRegion(gomock.Any(), int32(1)).
Return(sqlc.GetRegionRow{
ID: 1,
Slug: "bc",
Name: "British Columbia",
ZoomLevel: &zoom,
}, nil)
mock.EXPECT().
GetRegionIATAs(gomock.Any(), int32(1)).
Return([]string{"YVR", "YYJ"}, nil)
store := &Store{q: mock}
region, err := store.GetRegion(context.Background(), 1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if region.Slug != "bc" {
t.Errorf("expected slug bc, got %s", region.Slug)
}
if region.ZoomLevel == nil {
t.Fatal("expected ZoomLevel to be set")
}
if *region.ZoomLevel != 10 {
t.Errorf("expected ZoomLevel 10, got %d", *region.ZoomLevel)
}
if len(region.IATAs) != 2 {
t.Errorf("expected 2 IATAs, got %d", len(region.IATAs))
}
}
func TestGetRegion_IATAError(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
GetRegion(gomock.Any(), int32(1)).
Return(sqlc.GetRegionRow{ID: 1, Slug: "bc", Name: "British Columbia"}, nil)
mock.EXPECT().
GetRegionIATAs(gomock.Any(), int32(1)).
Return(nil, errors.New("db error"))
store := &Store{q: mock}
_, err := store.GetRegion(context.Background(), 1)
if err == nil {
t.Fatal("expected error, got nil")
}
}
func TestGetRegionBySlug(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
GetRegionBySlug(gomock.Any(), "bc").
Return(sqlc.GetRegionBySlugRow{ID: 1, Slug: "bc", Name: "British Columbia"}, nil)
mock.EXPECT().
GetRegionIATAs(gomock.Any(), int32(1)).
Return([]string{"YVR"}, nil)
store := &Store{q: mock}
region, err := store.GetRegionBySlug(context.Background(), "bc")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if region.Slug != "bc" {
t.Errorf("expected slug bc, got %s", region.Slug)
}
if len(region.IATAs) != 1 {
t.Errorf("expected 1 IATA, got %d", len(region.IATAs))
}
}
+95
View File
@@ -0,0 +1,95 @@
package db
import (
"bytes"
"context"
"testing"
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
mockdb "github.com/MeshCore-Beacon/beacon-server/db/sqlc/mock"
"go.uber.org/mock/gomock"
)
func TestGetTransportScopes(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
GetTransportScopes(gomock.Any()).
Return([]sqlc.GetTransportScopesRow{
{
Name: "default",
TransportKey: []byte{0x01, 0x02},
KeyFingerprint: []byte{0xde, 0xad},
},
}, nil)
store := &Store{q: mock}
entries, err := store.GetTransportScopes(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(entries) != 1 {
t.Fatalf("expected 1 entry, got %d", len(entries))
}
if entries[0].Name != "default" {
t.Errorf("expected Name default, got %s", entries[0].Name)
}
if !bytes.Equal(entries[0].TransportKey, []byte{0x01, 0x02}) {
t.Errorf("expected TransportKey [0x01 0x02], got %v", entries[0].TransportKey)
}
}
func TestGetScopesByIATAs(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
GetScopesByIATAs(gomock.Any(), "YVR,YYJ").
Return([]sqlc.GetScopesByIATAsRow{
{Name: "default", ObserverCount: 3, NodeCount: 10, IataCount: 2},
}, nil)
store := &Store{q: mock}
items, err := store.GetScopesByIATAs(context.Background(), []string{"YVR", "YYJ"})
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].IATACount != 2 {
t.Errorf("expected IATACount 2, got %d", items[0].IATACount)
}
}
func TestGetScopeByName(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
GetScopeByName(gomock.Any(), "default").
Return(sqlc.GetScopeByNameRow{
Name: "default",
PacketCount: 100,
ObserverCount: 5,
NodeCount: 20,
IataCount: 3,
Iatas: []string{"YVR", "YYJ", "YYC"},
}, nil)
store := &Store{q: mock}
detail, err := store.GetScopeByName(context.Background(), "default")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if detail.Name != "default" {
t.Errorf("expected Name default, got %s", detail.Name)
}
if len(detail.IATAs) != 3 {
t.Errorf("expected 3 IATAs, got %d", len(detail.IATAs))
}
if detail.PacketCount != 100 {
t.Errorf("expected PacketCount 100, got %d", detail.PacketCount)
}
}
File diff suppressed because it is too large Load Diff
+162
View File
@@ -0,0 +1,162 @@
package db
import (
"context"
"testing"
"time"
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
mockdb "github.com/MeshCore-Beacon/beacon-server/db/sqlc/mock"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"go.uber.org/mock/gomock"
)
func TestGetStatsOverview(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
GetStatsOverview(gomock.Any(), "YVR").
Return(sqlc.GetStatsOverviewRow{
TotalPackets: 100,
TotalObservations: 500,
ActiveObservers: 10,
ActiveIatas: 3,
}, nil)
store := &Store{q: mock}
result, err := store.GetStatsOverview(context.Background(), []string{"YVR"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.TotalPackets != 100 {
t.Errorf("expected TotalPackets 100, got %d", result.TotalPackets)
}
if result.WindowHours != 24 {
t.Errorf("expected WindowHours 24, got %d", result.WindowHours)
}
}
func TestGetStatsTopNodes_NilObservationCount(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
lastHeard := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}
name := "test-node"
mock.EXPECT().
GetTopNodes(gomock.Any(), sqlc.GetTopNodesParams{
Column1: "YVR",
Limit: 5,
}).
Return([]sqlc.MvTopNodesByIatum{
{
NodeID: nodeID,
Name: &name,
NodeType: 1,
Iata: "YVR",
ObservationCount: nil,
LastHeard: lastHeard,
},
}, nil)
store := &Store{q: mock}
items, err := store.GetStatsTopNodes(context.Background(), []string{"YVR"}, 5)
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].ObservationCount != 0 {
t.Errorf("expected ObservationCount 0 for nil, got %d", items[0].ObservationCount)
}
}
func TestGetStatsTopObservers_IATATypeAssertion(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
observerID := uuid.MustParse("00000000-0000-0000-0000-000000000002")
displayName := "test-observer"
obsType := "fixed"
mock.EXPECT().
GetStatsTopObservers(gomock.Any(), gomock.Any()).
Return([]sqlc.GetStatsTopObserversRow{
{
ID: observerID,
DisplayName: &displayName,
ObserverType: &obsType,
Iata: "YVR",
ObservationCount: 42,
},
}, nil)
store := &Store{q: mock}
items, err := store.GetStatsTopObservers(context.Background(), []string{"YVR"}, time.Now().Add(-time.Hour), 5)
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].IATA != "YVR" {
t.Errorf("expected IATA YVR, got %s", items[0].IATA)
}
if *items[0].ObserverType != "fixed" {
t.Errorf("expected ObserverType fixed, got %s", *items[0].ObserverType)
}
}
func TestGetStatsNodeTypes_Mapping(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
GetStatsNodeTypes(gomock.Any(), "YVR").
Return([]sqlc.GetStatsNodeTypesRow{
{NodeType: 1, Count: 10},
{NodeType: 2, Count: 5},
}, nil)
store := &Store{q: mock}
result, err := store.GetStatsNodeTypes(context.Background(), []string{"YVR"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result) != 2 {
t.Fatalf("expected 2 items, got %d", len(result))
}
if result[0].NodeTypeName == "" {
t.Error("expected NodeTypeName to be set")
}
}
func TestGetScopeStats(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
GetScopeStats(gomock.Any()).
Return([]sqlc.GetScopeStatsRow{
{Name: "default", PacketCount: 100, ObserverCount: 5, NodeCount: 20},
}, nil)
store := &Store{q: mock}
items, err := store.GetScopeStats(context.Background())
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].Name != "default" {
t.Errorf("expected Name default, got %s", items[0].Name)
}
if items[0].PacketCount != 100 {
t.Errorf("expected PacketCount 100, got %d", items[0].PacketCount)
}
}
+90
View File
@@ -4,11 +4,16 @@
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/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"go.uber.org/mock/gomock"
)
func TestNullableUUID_Zero(t *testing.T) {
@@ -89,3 +94,88 @@ func TestToChannelMessage_NilFields(t *testing.T) {
t.Errorf("expected empty Content, got %s", msg.Content)
}
}
func TestResolvePathHashes_Empty(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
// no EXPECT — sqlc should never be called
store := &Store{q: mock}
result, err := store.ResolvePathHashes(context.Background(), "YVR", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != nil {
t.Errorf("expected nil, got %v", result)
}
}
func TestResolvePathHashes_DBError(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
hashes := [][]byte{{0x01, 0x02}}
mock.EXPECT().
ResolvePathHashes(gomock.Any(), sqlc.ResolvePathHashesParams{
Iata: "YVR",
Column2: hashes,
}).
Return(nil, errors.New("db error"))
store := &Store{q: mock}
result, err := store.ResolvePathHashes(context.Background(), "YVR", hashes)
if err == nil {
t.Fatal("expected error, got nil")
}
if result != nil {
t.Errorf("expected nil result on error, got %v", result)
}
}
func TestResolvePathHashes_Mapping(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
name := "test-node"
lat := 49.1967
lon := -123.1815
pubkey := []byte{0xde, 0xad}
hashes := [][]byte{{0xab, 0xcd}}
mock.EXPECT().
ResolvePathHashes(gomock.Any(), sqlc.ResolvePathHashesParams{
Iata: "YVR",
Column2: hashes,
}).
Return([]sqlc.ResolvePathHashesRow{
{
Hash: []byte{0xab, 0xcd},
NodeID: nodeID,
Name: &name,
Latitude: &lat,
Longitude: &lon,
PublicKey: pubkey,
},
}, nil)
store := &Store{q: mock}
result, err := store.ResolvePathHashes(context.Background(), "YVR", hashes)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
entries, ok := result["abcd"]
if !ok {
t.Fatal("expected key abcd in result")
}
if len(entries) != 1 {
t.Fatalf("expected 1 entry, got %d", len(entries))
}
if entries[0].NodeID != nodeID {
t.Errorf("expected NodeID %s, got %s", nodeID, entries[0].NodeID)
}
if entries[0].Name != &name {
t.Errorf("expected Name %s, got %v", name, entries[0].Name)
}
}
+160
View File
@@ -0,0 +1,160 @@
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,
},
}, nil)
mock.EXPECT().
ListObservationsForPacket(gomock.Any(), []byte{0xaa, 0xbb, 0xcc, 0xdd}).
Return([]sqlc.ListObservationsForPacketRow{
{Iata: "YVR"},
}, nil)
mock.EXPECT().
ResolvePathHashes(gomock.Any(), sqlc.ResolvePathHashesParams{
Iata: "YVR",
Column2: [][]byte{{0xaa, 0xbb}},
}).
Return([]sqlc.ResolvePathHashesRow{}, 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")
}
}
+1
View File
@@ -34,6 +34,7 @@ require (
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.53.0 // indirect
+2
View File
@@ -78,6 +78,8 @@ github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=