tests(handlers): add some happy path tests everywhere

This commit is contained in:
Enot (ded) Skelly
2026-06-15 15:02:17 -07:00
parent fea747924d
commit e9d4a336ea
11 changed files with 612 additions and 90 deletions
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/MeshCore-Beacon/beacon-server/internal/ingest"
"github.com/go-chi/chi/v5"
)
func TestListBrokers_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/brokers", listBrokers([]*ingest.Worker{}))
req := httptest.NewRequest(http.MethodGet, "/brokers", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
var result []BrokerStatus
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
t.Fatalf("failed to decode: %v", err)
}
}
+32
View File
@@ -4,10 +4,12 @@
package handlers
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/go-chi/chi/v5"
)
@@ -109,3 +111,33 @@ func TestListChannelMessages_InvalidCursor(t *testing.T) {
t.Errorf("expected 400, got %d", w.Code)
}
}
func TestListChannels_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/channels", listChannels(stubReader{
listChannels: func(_ context.Context, _ int32, _ []byte, _ string, _ int64) (api.Page[api.ChannelSummary], error) {
return api.Page[api.ChannelSummary]{Items: []api.ChannelSummary{{ID: 1, ChannelHash: "ab"}}}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/channels", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestGetChannel_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/channels/{channelID}", getChannel(stubReader{
getChannel: func(_ context.Context, id int32) (*api.Channel, error) {
return &api.Channel{ChannelSummary: api.ChannelSummary{ID: int(id)}}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/channels/1", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package handlers
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/go-chi/chi/v5"
)
func TestListIATAs_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/iatas", listIATAs(stubReader{
listIATAs: func(_ context.Context) ([]api.IATA, error) {
return []api.IATA{{IATA: "YVR"}}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/iatas", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
var result []api.IATA
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
t.Fatalf("failed to decode: %v", err)
}
if len(result) != 1 || result[0].IATA != "YVR" {
t.Errorf("unexpected result: %v", result)
}
}
func TestGetIATA_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/iatas/{iata}", getIATA(stubReader{
getIATA: func(_ context.Context, iata string) (*api.IATA, error) {
return &api.IATA{IATA: iata}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/iatas/YVR", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
+35
View File
@@ -4,11 +4,14 @@
package handlers
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
func TestGetNode_InvalidUUID(t *testing.T) {
@@ -120,3 +123,35 @@ func TestListNodes_InvalidSupportsMultibyteTraces(t *testing.T) {
t.Errorf("expected 400, got %d", w.Code)
}
}
func TestListNodes_OK(t *testing.T) {
nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
r := chi.NewRouter()
r.Get("/nodes", listNodes(stubReader{
listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _ string, _ int64, _ int32) (api.Page[api.NodeSummary], error) {
return api.Page[api.NodeSummary]{Items: []api.NodeSummary{{ID: nodeID}}}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/nodes", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestGetNode_OK(t *testing.T) {
nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
r := chi.NewRouter()
r.Get("/nodes/{nodeId}", getNode(stubReader{
getNode: func(_ context.Context, id uuid.UUID) (*api.Node, error) {
return &api.Node{NodeSummary: api.NodeSummary{ID: id}}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/nodes/"+nodeID.String(), nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
+46
View File
@@ -4,11 +4,14 @@
package handlers
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
func TestGetObserverTelemetry_InvalidUUID(t *testing.T) {
@@ -62,3 +65,46 @@ func TestGetObserverTelemetry_InvalidInterval(t *testing.T) {
t.Errorf("expected 400, got %d", w.Code)
}
}
func TestListObservers_OK(t *testing.T) {
observerID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
r := chi.NewRouter()
r.Get("/observers", listObservers(stubReader{
listObservers: func(_ context.Context, _ []string, _, _, _, _, _ string, _ int64, _ int32) (api.Page[api.ObserverSummary], error) {
return api.Page[api.ObserverSummary]{Items: []api.ObserverSummary{{ID: observerID}}}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/observers", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestGetObserver_OK(t *testing.T) {
observerID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
r := chi.NewRouter()
r.Get("/observers/{observerId}", getObserver(stubReader{
getObserver: func(_ context.Context, id uuid.UUID) (*api.Observer, error) {
return &api.Observer{ObserverSummary: api.ObserverSummary{ID: id}}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/observers/"+observerID.String(), nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestGetObserver_InvalidUUID(t *testing.T) {
r := chi.NewRouter()
r.Get("/observers/{observerId}", getObserver(stubReader{}))
req := httptest.NewRequest(http.MethodGet, "/observers/not-a-uuid", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
+46
View File
@@ -4,9 +4,14 @@
package handlers
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/go-chi/chi/v5"
)
func TestParseIATAs_Single(t *testing.T) {
@@ -63,3 +68,44 @@ func TestParseIATAs_Uppercase(t *testing.T) {
t.Errorf("expected [YVR], got %v", result)
}
}
func TestListRegions_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/regions", listRegions(stubReader{
listRegions: func(_ context.Context) ([]api.RegionSummary, error) {
return []api.RegionSummary{{ID: 1, Slug: "bc", Name: "British Columbia"}}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/regions", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestGetRegion_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/regions/{regionId}", getRegion(stubReader{
getRegion: func(_ context.Context, id int32) (*api.Region, error) {
return &api.Region{RegionSummary: api.RegionSummary{ID: int(id), Slug: "bc"}}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/regions/1", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestGetRegion_InvalidID(t *testing.T) {
r := chi.NewRouter()
r.Get("/regions/{regionId}", getRegion(stubReader{}))
req := httptest.NewRequest(http.MethodGet, "/regions/notanint", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
+33
View File
@@ -4,10 +4,13 @@
package handlers
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/go-chi/chi/v5"
)
@@ -56,3 +59,33 @@ func TestSearchCrossIATARoutes_MissingParams(t *testing.T) {
}
}
}
func TestListKnownRoutes_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/routes", listKnownRoutes(stubReader{
listKnownRoutes: func(_ context.Context, _ string, _ int32, _ time.Time, _ int32) ([]api.KnownRoute, error) {
return []api.KnownRoute{{IATA: "YVR"}}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/routes", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestSearchKnownRoutes_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/routes/search", searchKnownRoutes(stubReader{
searchKnownRoutes: func(_ context.Context, _, _, _ string) ([]api.KnownRoute, error) {
return []api.KnownRoute{{IATA: "YVR"}}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/routes/search?iata=YVR&from=aa&to=bb", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package handlers
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/go-chi/chi/v5"
)
func TestListScopes_NoIATAs_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/scopes", listScopes(stubReader{
getScopeNames: func(_ context.Context) ([]string, error) {
return []string{"#bc", "#west"}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/scopes", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestGetScope_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/scopes/{name}", getScope(stubReader{
getScopeByName: func(_ context.Context, name string) (*api.ScopeDetail, error) {
return &api.ScopeDetail{Name: name}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/scopes/%23bc", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
+17
View File
@@ -4,10 +4,12 @@
package handlers
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/go-chi/chi/v5"
)
@@ -65,3 +67,18 @@ func TestGetStatsTopObservers_InvalidLimit(t *testing.T) {
t.Errorf("expected 400, got %d", w.Code)
}
}
func TestGetStatsOverview_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/stats/overview", getStatsOverview(stubReader{
getStatsOverview: func(_ context.Context, _ []string) (*api.StatsOverview, error) {
return &api.StatsOverview{TotalPackets: 100}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/stats/overview", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
+218 -90
View File
@@ -11,175 +11,303 @@ import (
"github.com/google/uuid"
)
// stubReader satisfies api.Reader with zero-value returns.
// Use it for handler tests that exercise validation paths where
// the reader is never actually called.
type stubReader struct{}
func (stubReader) ListIATAs(ctx context.Context) ([]api.IATA, error) {
return nil, nil
// stubReader satisfies api.Reader with configurable function fields.
// Unset fields return zero values. Use it for both validation tests
// (leave all fields nil) and happy path tests (set only what you need).
type stubReader struct {
listIATAs func(ctx context.Context) ([]api.IATA, error)
getIATA func(ctx context.Context, iata string) (*api.IATA, error)
listRegions func(ctx context.Context) ([]api.RegionSummary, error)
getRegion func(ctx context.Context, regionID int32) (*api.Region, error)
getRegionBySlug func(ctx context.Context, slug string) (*api.Region, error)
listChannels func(ctx context.Context, limit int32, hash []byte, iata string, cursor int64) (api.Page[api.ChannelSummary], error)
getChannel func(ctx context.Context, channelID int32) (*api.Channel, error)
listChannelMessages func(ctx context.Context, channelID *int32, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error)
listChannelMessagesByHash func(ctx context.Context, hash []byte, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error)
listMessagesAfterID func(ctx context.Context, afterID int64, iatas []string, scope string, limit int32) ([]api.ChannelMessage, error)
listObservers func(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error)
getObserver func(ctx context.Context, observerID uuid.UUID) (*api.Observer, error)
getObserverTelemetry func(ctx context.Context, observerID uuid.UUID, since, until time.Time, afterID int64) (*api.ObserverTelemetry, error)
getObserverTelemetryBucketed func(ctx context.Context, observerID uuid.UUID, since, until time.Time, bucketHours int32) ([]api.ObserverTelemetryPoint, error)
getObserverScopes func(ctx context.Context, observerID uuid.UUID) ([]string, error)
listObserverAdverts func(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (api.Page[api.AdvertObservation], error)
listNodes func(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name, scope string, cursor int64, limit int32) (api.Page[api.NodeSummary], error)
getNode func(ctx context.Context, nodeID uuid.UUID) (*api.Node, error)
getNodeNeighbors func(ctx context.Context, nodeID uuid.UUID) ([]api.NodeNeighbor, error)
listNodeObservations func(ctx context.Context, nodeID uuid.UUID, cursor int64, limit int32) (api.Page[api.PacketObservationSummary], error)
listPackets func(ctx context.Context, payloadType, routeType int16, iatas []string, scope string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error)
listPacketsAfterID func(ctx context.Context, afterObservationID int64, payloadType, routeType int16, iatas []string, scope string, limit int32) ([]api.PacketSummary, error)
getPacket func(ctx context.Context, packetHash []byte) (*api.Packet, error)
getRadioPresets func(ctx context.Context, preset string, iatas []string) ([]api.RadioPreset, error)
getStatsOverview func(ctx context.Context, iatas []string) (*api.StatsOverview, error)
getStatsObservations func(ctx context.Context, iatas []string, since time.Time) ([]api.ObservationPoint, error)
getStatsPayloadBreakdown func(ctx context.Context, iatas []string, since time.Time) ([]api.PayloadBreakdownItem, error)
getStatsTopNodes func(ctx context.Context, iatas []string, limit int32) ([]api.TopNode, error)
getStatsTopObservers func(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopObserver, error)
getScopeStats func(ctx context.Context) ([]api.ScopeStats, error)
getStatsNodeTypes func(ctx context.Context, iatas []string) ([]api.NodeTypeCount, error)
getScopeNames func(ctx context.Context) ([]string, error)
getScopesByIATAs func(ctx context.Context, iatas []string) ([]api.ScopeSummary, error)
getScopeByName func(ctx context.Context, name string) (*api.ScopeDetail, error)
listTraceTags func(ctx context.Context, iatas []string, scope, traceType string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error)
getTraceByTag func(ctx context.Context, tag string) (*api.TraceDetail, error)
listKnownRoutes func(ctx context.Context, iata string, hopCount int32, cursor time.Time, limit int32) ([]api.KnownRoute, error)
searchKnownRoutes func(ctx context.Context, iata, fromHash, toHash string) ([]api.KnownRoute, error)
getKnownRoutesByNode func(ctx context.Context, iata string, nodeID uuid.UUID) ([]api.KnownRoute, error)
getCrossIATANeighbors func(ctx context.Context, nodeID uuid.UUID, iata string) ([]api.NodeNeighbor, error)
searchCrossIATARoutes func(ctx context.Context, fromHash, fromIATA, toHash, toIATA string) ([]api.CrossIATARoute, error)
getNodesByIDs func(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*api.ResolvedNode, error)
}
func (stubReader) GetIATA(ctx context.Context, iata string) (*api.IATA, error) {
func (s stubReader) ListIATAs(ctx context.Context) ([]api.IATA, error) {
if s.listIATAs != nil {
return s.listIATAs(ctx)
}
return nil, nil
}
func (stubReader) ListRegions(ctx context.Context) ([]api.RegionSummary, error) {
func (s stubReader) GetIATA(ctx context.Context, iata string) (*api.IATA, error) {
if s.getIATA != nil {
return s.getIATA(ctx, iata)
}
return nil, nil
}
func (stubReader) GetRegion(ctx context.Context, regionID int32) (*api.Region, error) {
func (s stubReader) ListRegions(ctx context.Context) ([]api.RegionSummary, error) {
if s.listRegions != nil {
return s.listRegions(ctx)
}
return nil, nil
}
func (stubReader) GetRegionBySlug(ctx context.Context, slug string) (*api.Region, error) {
func (s stubReader) GetRegion(ctx context.Context, regionID int32) (*api.Region, error) {
if s.getRegion != nil {
return s.getRegion(ctx, regionID)
}
return nil, nil
}
func (stubReader) ListChannels(ctx context.Context, limit int32, hash []byte, iata string, cursor int64) (api.Page[api.ChannelSummary], error) {
func (s stubReader) GetRegionBySlug(ctx context.Context, slug string) (*api.Region, error) {
if s.getRegionBySlug != nil {
return s.getRegionBySlug(ctx, slug)
}
return nil, nil
}
func (s stubReader) ListChannels(ctx context.Context, limit int32, hash []byte, iata string, cursor int64) (api.Page[api.ChannelSummary], error) {
if s.listChannels != nil {
return s.listChannels(ctx, limit, hash, iata, cursor)
}
return api.Page[api.ChannelSummary]{}, nil
}
func (stubReader) GetChannel(ctx context.Context, channelID int32) (*api.Channel, error) {
func (s stubReader) GetChannel(ctx context.Context, channelID int32) (*api.Channel, error) {
if s.getChannel != nil {
return s.getChannel(ctx, channelID)
}
return nil, nil
}
func (stubReader) ListChannelMessages(ctx context.Context, channelID *int32, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
func (s stubReader) ListChannelMessages(ctx context.Context, channelID *int32, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
if s.listChannelMessages != nil {
return s.listChannelMessages(ctx, channelID, since, limit, iatas, scope, cursor)
}
return api.Page[api.ChannelMessage]{}, nil
}
func (stubReader) ListChannelMessagesByHash(ctx context.Context, hash []byte, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
func (s stubReader) ListChannelMessagesByHash(ctx context.Context, hash []byte, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
if s.listChannelMessagesByHash != nil {
return s.listChannelMessagesByHash(ctx, hash, since, limit, iatas, scope, cursor)
}
return api.Page[api.ChannelMessage]{}, nil
}
func (stubReader) ListMessagesAfterID(ctx context.Context, afterID int64, iatas []string, scope string, limit int32) ([]api.ChannelMessage, error) {
func (s stubReader) ListMessagesAfterID(ctx context.Context, afterID int64, iatas []string, scope string, limit int32) ([]api.ChannelMessage, error) {
if s.listMessagesAfterID != nil {
return s.listMessagesAfterID(ctx, afterID, iatas, scope, limit)
}
return nil, nil
}
func (stubReader) ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error) {
func (s stubReader) ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error) {
if s.listObservers != nil {
return s.listObservers(ctx, iatas, observerType, broker, status, name, scope, cursor, limit)
}
return api.Page[api.ObserverSummary]{}, nil
}
func (stubReader) GetObserver(ctx context.Context, observerID uuid.UUID) (*api.Observer, error) {
func (s stubReader) GetObserver(ctx context.Context, observerID uuid.UUID) (*api.Observer, error) {
if s.getObserver != nil {
return s.getObserver(ctx, observerID)
}
return nil, nil
}
func (stubReader) GetObserverTelemetry(ctx context.Context, observerID uuid.UUID, since, until time.Time, afterID int64) (*api.ObserverTelemetry, error) {
func (s stubReader) GetObserverTelemetry(ctx context.Context, observerID uuid.UUID, since, until time.Time, afterID int64) (*api.ObserverTelemetry, error) {
if s.getObserverTelemetry != nil {
return s.getObserverTelemetry(ctx, observerID, since, until, afterID)
}
return nil, nil
}
func (stubReader) GetObserverTelemetryBucketed(ctx context.Context, observerID uuid.UUID, since, until time.Time, bucketHours int32) ([]api.ObserverTelemetryPoint, error) {
func (s stubReader) GetObserverTelemetryBucketed(ctx context.Context, observerID uuid.UUID, since, until time.Time, bucketHours int32) ([]api.ObserverTelemetryPoint, error) {
if s.getObserverTelemetryBucketed != nil {
return s.getObserverTelemetryBucketed(ctx, observerID, since, until, bucketHours)
}
return nil, nil
}
func (stubReader) GetObserverScopes(ctx context.Context, observerID uuid.UUID) ([]string, error) {
func (s stubReader) GetObserverScopes(ctx context.Context, observerID uuid.UUID) ([]string, error) {
if s.getObserverScopes != nil {
return s.getObserverScopes(ctx, observerID)
}
return nil, nil
}
func (stubReader) ListObserverAdverts(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (api.Page[api.AdvertObservation], error) {
func (s stubReader) ListObserverAdverts(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (api.Page[api.AdvertObservation], error) {
if s.listObserverAdverts != nil {
return s.listObserverAdverts(ctx, observerID, cursor, limit)
}
return api.Page[api.AdvertObservation]{}, nil
}
func (stubReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name, scope string, cursor int64, limit int32) (api.Page[api.NodeSummary], error) {
func (s stubReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, name, scope string, cursor int64, limit int32) (api.Page[api.NodeSummary], error) {
if s.listNodes != nil {
return s.listNodes(ctx, nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, name, scope, cursor, limit)
}
return api.Page[api.NodeSummary]{}, nil
}
func (stubReader) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error) {
func (s stubReader) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error) {
if s.getNode != nil {
return s.getNode(ctx, nodeID)
}
return nil, nil
}
func (stubReader) ListNodeObservations(ctx context.Context, nodeID uuid.UUID, cursor int64, limit int32) (api.Page[api.PacketObservationSummary], error) {
func (s stubReader) GetNodeNeighbors(ctx context.Context, nodeID uuid.UUID) ([]api.NodeNeighbor, error) {
if s.getNodeNeighbors != nil {
return s.getNodeNeighbors(ctx, nodeID)
}
return nil, nil
}
func (s stubReader) ListNodeObservations(ctx context.Context, nodeID uuid.UUID, cursor int64, limit int32) (api.Page[api.PacketObservationSummary], error) {
if s.listNodeObservations != nil {
return s.listNodeObservations(ctx, nodeID, cursor, limit)
}
return api.Page[api.PacketObservationSummary]{}, nil
}
func (stubReader) ListPackets(ctx context.Context, payloadType, routeType int16, iatas []string, scope string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) {
func (s stubReader) ListPackets(ctx context.Context, payloadType, routeType int16, iatas []string, scope string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) {
if s.listPackets != nil {
return s.listPackets(ctx, payloadType, routeType, iatas, scope, since, until, cursor, limit)
}
return api.Page[api.PacketSummary]{}, nil
}
func (stubReader) ListPacketsAfterID(ctx context.Context, afterObservationID int64, payloadType, routeType int16, iatas []string, scope string, limit int32) ([]api.PacketSummary, error) {
func (s stubReader) ListPacketsAfterID(ctx context.Context, afterObservationID int64, payloadType, routeType int16, iatas []string, scope string, limit int32) ([]api.PacketSummary, error) {
if s.listPacketsAfterID != nil {
return s.listPacketsAfterID(ctx, afterObservationID, payloadType, routeType, iatas, scope, limit)
}
return nil, nil
}
func (stubReader) GetPacket(ctx context.Context, packetHash []byte) (*api.Packet, error) {
func (s stubReader) GetPacket(ctx context.Context, packetHash []byte) (*api.Packet, error) {
if s.getPacket != nil {
return s.getPacket(ctx, packetHash)
}
return nil, nil
}
func (stubReader) GetRadioPresets(ctx context.Context, preset string, iatas []string) ([]api.RadioPreset, error) {
func (s stubReader) GetRadioPresets(ctx context.Context, preset string, iatas []string) ([]api.RadioPreset, error) {
if s.getRadioPresets != nil {
return s.getRadioPresets(ctx, preset, iatas)
}
return nil, nil
}
func (stubReader) GetStatsOverview(ctx context.Context, iatas []string) (*api.StatsOverview, error) {
func (s stubReader) GetStatsOverview(ctx context.Context, iatas []string) (*api.StatsOverview, error) {
if s.getStatsOverview != nil {
return s.getStatsOverview(ctx, iatas)
}
return nil, nil
}
func (stubReader) GetStatsObservations(ctx context.Context, iatas []string, since time.Time) ([]api.ObservationPoint, error) {
func (s stubReader) GetStatsObservations(ctx context.Context, iatas []string, since time.Time) ([]api.ObservationPoint, error) {
if s.getStatsObservations != nil {
return s.getStatsObservations(ctx, iatas, since)
}
return nil, nil
}
func (stubReader) GetStatsPayloadBreakdown(ctx context.Context, iatas []string, since time.Time) ([]api.PayloadBreakdownItem, error) {
func (s stubReader) GetStatsPayloadBreakdown(ctx context.Context, iatas []string, since time.Time) ([]api.PayloadBreakdownItem, error) {
if s.getStatsPayloadBreakdown != nil {
return s.getStatsPayloadBreakdown(ctx, iatas, since)
}
return nil, nil
}
func (stubReader) GetStatsTopNodes(ctx context.Context, iatas []string, limit int32) ([]api.TopNode, error) {
func (s stubReader) GetStatsTopNodes(ctx context.Context, iatas []string, limit int32) ([]api.TopNode, error) {
if s.getStatsTopNodes != nil {
return s.getStatsTopNodes(ctx, iatas, limit)
}
return nil, nil
}
func (stubReader) GetStatsTopObservers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopObserver, error) {
func (s stubReader) GetStatsTopObservers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopObserver, error) {
if s.getStatsTopObservers != nil {
return s.getStatsTopObservers(ctx, iatas, since, limit)
}
return nil, nil
}
func (stubReader) GetScopeStats(ctx context.Context) ([]api.ScopeStats, error) {
func (s stubReader) GetScopeStats(ctx context.Context) ([]api.ScopeStats, error) {
if s.getScopeStats != nil {
return s.getScopeStats(ctx)
}
return nil, nil
}
func (stubReader) GetStatsNodeTypes(ctx context.Context, iatas []string) ([]api.NodeTypeCount, error) {
func (s stubReader) GetStatsNodeTypes(ctx context.Context, iatas []string) ([]api.NodeTypeCount, error) {
if s.getStatsNodeTypes != nil {
return s.getStatsNodeTypes(ctx, iatas)
}
return nil, nil
}
func (stubReader) GetScopeNames(ctx context.Context) ([]string, error) {
func (s stubReader) GetScopeNames(ctx context.Context) ([]string, error) {
if s.getScopeNames != nil {
return s.getScopeNames(ctx)
}
return nil, nil
}
func (stubReader) GetScopesByIATAs(ctx context.Context, iatas []string) ([]api.ScopeSummary, error) {
func (s stubReader) GetScopesByIATAs(ctx context.Context, iatas []string) ([]api.ScopeSummary, error) {
if s.getScopesByIATAs != nil {
return s.getScopesByIATAs(ctx, iatas)
}
return nil, nil
}
func (stubReader) GetScopeByName(ctx context.Context, name string) (*api.ScopeDetail, error) {
func (s stubReader) GetScopeByName(ctx context.Context, name string) (*api.ScopeDetail, error) {
if s.getScopeByName != nil {
return s.getScopeByName(ctx, name)
}
return nil, nil
}
func (stubReader) ListTraceTags(ctx context.Context, iatas []string, scope, traceType string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error) {
func (s stubReader) ListTraceTags(ctx context.Context, iatas []string, scope, traceType string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error) {
if s.listTraceTags != nil {
return s.listTraceTags(ctx, iatas, scope, traceType, since, until, cursor, limit)
}
return nil, nil
}
func (stubReader) GetTraceByTag(ctx context.Context, tag string) (*api.TraceDetail, error) {
func (s stubReader) GetTraceByTag(ctx context.Context, tag string) (*api.TraceDetail, error) {
if s.getTraceByTag != nil {
return s.getTraceByTag(ctx, tag)
}
return nil, nil
}
func (stubReader) ListKnownRoutes(ctx context.Context, iata string, hopCount int32, cursor time.Time, limit int32) ([]api.KnownRoute, error) {
func (s stubReader) ListKnownRoutes(ctx context.Context, iata string, hopCount int32, cursor time.Time, limit int32) ([]api.KnownRoute, error) {
if s.listKnownRoutes != nil {
return s.listKnownRoutes(ctx, iata, hopCount, cursor, limit)
}
return nil, nil
}
func (stubReader) SearchKnownRoutes(ctx context.Context, iata, fromHash, toHash string) ([]api.KnownRoute, error) {
func (s stubReader) SearchKnownRoutes(ctx context.Context, iata, fromHash, toHash string) ([]api.KnownRoute, error) {
if s.searchKnownRoutes != nil {
return s.searchKnownRoutes(ctx, iata, fromHash, toHash)
}
return nil, nil
}
func (stubReader) GetNodeNeighbors(ctx context.Context, nodeID uuid.UUID) ([]api.NodeNeighbor, error) {
func (s stubReader) GetKnownRoutesByNode(ctx context.Context, iata string, nodeID uuid.UUID) ([]api.KnownRoute, error) {
if s.getKnownRoutesByNode != nil {
return s.getKnownRoutesByNode(ctx, iata, nodeID)
}
return nil, nil
}
func (stubReader) GetKnownRoutesByNode(ctx context.Context, iata string, nodeID uuid.UUID) ([]api.KnownRoute, error) {
func (s stubReader) GetCrossIATANeighbors(ctx context.Context, nodeID uuid.UUID, iata string) ([]api.NodeNeighbor, error) {
if s.getCrossIATANeighbors != nil {
return s.getCrossIATANeighbors(ctx, nodeID, iata)
}
return nil, nil
}
func (stubReader) GetCrossIATANeighbors(ctx context.Context, nodeID uuid.UUID, iata string) ([]api.NodeNeighbor, error) {
func (s stubReader) SearchCrossIATARoutes(ctx context.Context, fromHash, fromIATA, toHash, toIATA string) ([]api.CrossIATARoute, error) {
if s.searchCrossIATARoutes != nil {
return s.searchCrossIATARoutes(ctx, fromHash, fromIATA, toHash, toIATA)
}
return nil, nil
}
func (stubReader) SearchCrossIATARoutes(ctx context.Context, fromHash, fromIATA, toHash, toIATA string) ([]api.CrossIATARoute, error) {
return nil, nil
}
func (stubReader) GetNodesByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*api.ResolvedNode, error) {
func (s stubReader) GetNodesByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*api.ResolvedNode, error) {
if s.getNodesByIDs != nil {
return s.getNodesByIDs(ctx, ids)
}
return nil, nil
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package handlers
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/go-chi/chi/v5"
)
func TestListTraceTags_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/traces", listTraceTags(stubReader{
listTraceTags: func(_ context.Context, _ []string, _, _ string, _, _ time.Time, _ time.Time, _ int32) ([]api.TraceTagSummary, error) {
return []api.TraceTagSummary{{TraceTag: "trace-001"}}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/traces", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestGetTrace_OK(t *testing.T) {
r := chi.NewRouter()
r.Get("/traces/{tag}", getTrace(stubReader{
getTraceByTag: func(_ context.Context, tag string) (*api.TraceDetail, error) {
return &api.TraceDetail{TraceTag: tag}, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/traces/trace-001", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestGetTrace_NotFound(t *testing.T) {
r := chi.NewRouter()
r.Get("/traces/{tag}", getTrace(stubReader{
getTraceByTag: func(_ context.Context, _ string) (*api.TraceDetail, error) {
return nil, nil
},
}))
req := httptest.NewRequest(http.MethodGet, "/traces/missing", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d", w.Code)
}
}