Files
MrAlders0n dff9c6cf00 perf(channels): track channel activity per IATA in its own table
The IATA filter ran a correlated EXISTS over packets/observations with
ILIKE, which skipped the iata index and took ~7s live. Keep a small
channel_iatas table at ingest (like node_iatas) and filter against it.
Also honor the iatas= param so multi-site regions stop getting the
global list. Filter now ages out with packet retention rather than
matching any retained packet.
2026-07-24 12:07:05 -07:00

330 lines
8.2 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 TestListChannels_Empty(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
ListChannels(gomock.Any(), sqlc.ListChannelsParams{
ChannelHash: nil,
Iatas: nil,
CursorTs: pgtype.Timestamptz{},
PageLimit: 11,
}).
Return([]sqlc.Channel{}, nil)
store := &Store{q: mock}
page, err := store.ListChannels(context.Background(), 10, nil, 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{
ChannelHash: nil,
Iatas: nil,
CursorTs: pgtype.Timestamptz{},
PageLimit: 3, // limit+1
}).
Return(rows, nil)
store := &Store{q: mock}
page, err := store.ListChannels(context.Background(), 2, nil, 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, nil, 0)
if err == nil {
t.Fatal("expected error, got nil")
}
}
func TestListChannels_IATAFilter(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
ListChannels(gomock.Any(), sqlc.ListChannelsParams{
ChannelHash: nil,
Iatas: []string{"YOW", "YYZ"},
CursorTs: pgtype.Timestamptz{},
PageLimit: 11,
}).
Return([]sqlc.Channel{}, nil)
store := &Store{q: mock}
_, err := store.ListChannels(context.Background(), 10, nil, []string{"YOW", "YYZ"}, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
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: []string{"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: []string{"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")
}
}