// Copyright 2026 Beacon Contributors // SPDX-License-Identifier: AGPL-3.0-or-later package handlers import ( "context" "encoding/json" "time" "github.com/MeshCore-Beacon/beacon-server/internal/api" "github.com/google/uuid" ) // 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) getIATABorder func(ctx context.Context, iata string) (json.RawMessage, 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, iatas []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, pubkeyPrefix, name, scope string, cursor int64, limit int32, includeNeighbors bool) (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, payloadTypes, routeTypes []int16, iatas []string, scopes []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) getStatsTopAdvertisers func(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopAdvertiser, error) getStatsClockDrift func(ctx context.Context, iatas []string, limit int32) ([]api.ClockDriftEntry, error) getStatsTopTalkers func(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopTalker, 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 (s stubReader) ListIATAs(ctx context.Context) ([]api.IATA, error) { if s.listIATAs != nil { return s.listIATAs(ctx) } return nil, nil } 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 (s stubReader) GetIATABorder(ctx context.Context, iata string) (json.RawMessage, error) { if s.getIATABorder != nil { return s.getIATABorder(ctx, iata) } return nil, nil } func (s stubReader) ListRegions(ctx context.Context) ([]api.RegionSummary, error) { if s.listRegions != nil { return s.listRegions(ctx) } return nil, nil } 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 (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, iatas []string, cursor int64) (api.Page[api.ChannelSummary], error) { if s.listChannels != nil { return s.listChannels(ctx, limit, hash, iatas, cursor) } return api.Page[api.ChannelSummary]{}, nil } 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 (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 (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 (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 (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 (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 (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 (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 (s stubReader) GetObserverScopes(ctx context.Context, observerID uuid.UUID) ([]string, error) { if s.getObserverScopes != nil { return s.getObserverScopes(ctx, observerID) } return nil, nil } 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 (s stubReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) { if s.listNodes != nil { return s.listNodes(ctx, nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, pubkeyPrefix, name, scope, cursor, limit, includeNeighbors) } return api.Page[api.NodeSummary]{}, nil } 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 (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 (s stubReader) ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) { if s.listPackets != nil { return s.listPackets(ctx, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit) } return api.Page[api.PacketSummary]{}, nil } 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 (s stubReader) GetPacket(ctx context.Context, packetHash []byte) (*api.Packet, error) { if s.getPacket != nil { return s.getPacket(ctx, packetHash) } return nil, nil } 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 (s stubReader) GetStatsOverview(ctx context.Context, iatas []string) (*api.StatsOverview, error) { if s.getStatsOverview != nil { return s.getStatsOverview(ctx, iatas) } return nil, nil } 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 (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 (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 (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 (s stubReader) GetStatsTopAdvertisers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopAdvertiser, error) { if s.getStatsTopAdvertisers != nil { return s.getStatsTopAdvertisers(ctx, iatas, since, limit) } return nil, nil } func (s stubReader) GetStatsClockDrift(ctx context.Context, iatas []string, limit int32) ([]api.ClockDriftEntry, error) { if s.getStatsClockDrift != nil { return s.getStatsClockDrift(ctx, iatas, limit) } return nil, nil } func (s stubReader) GetStatsTopTalkers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopTalker, error) { if s.getStatsTopTalkers != nil { return s.getStatsTopTalkers(ctx, iatas, since, limit) } return nil, nil } func (s stubReader) GetScopeStats(ctx context.Context) ([]api.ScopeStats, error) { if s.getScopeStats != nil { return s.getScopeStats(ctx) } return nil, nil } 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 (s stubReader) GetScopeNames(ctx context.Context) ([]string, error) { if s.getScopeNames != nil { return s.getScopeNames(ctx) } return nil, nil } 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 (s stubReader) GetScopeByName(ctx context.Context, name string) (*api.ScopeDetail, error) { if s.getScopeByName != nil { return s.getScopeByName(ctx, name) } return nil, nil } 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 (s stubReader) GetTraceByTag(ctx context.Context, tag string) (*api.TraceDetail, error) { if s.getTraceByTag != nil { return s.getTraceByTag(ctx, tag) } return nil, nil } 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 (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 (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 (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 (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 (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 }