mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-22 14:11:27 +00:00
14 files changed
+278
-77
No files matched your search
+15
-15
@@ -58,9 +58,9 @@ func (s *Store) SetPacketDecrypted(ctx context.Context, hash []byte) error {
|
||||
return s.q.SetPacketDecrypted(ctx, hash)
|
||||
}
|
||||
|
||||
func (s *Store) 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 *Store) 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 len(iatas) > 0 {
|
||||
return s.listPacketsByIATAs(ctx, payloadType, routeType, iatas, scope, since, until, cursor, limit)
|
||||
return s.listPacketsByIATAs(ctx, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit)
|
||||
}
|
||||
var cursorTS pgtype.Timestamptz
|
||||
if cursor > 0 {
|
||||
@@ -75,13 +75,13 @@ func (s *Store) ListPackets(ctx context.Context, payloadType, routeType int16, i
|
||||
untilTS = pgtype.Timestamptz{Time: until, Valid: true}
|
||||
}
|
||||
rows, err := s.q.ListPackets(ctx, sqlc.ListPacketsParams{
|
||||
Column1: payloadType,
|
||||
Column2: routeType,
|
||||
Column1: payloadTypes,
|
||||
Column2: routeTypes,
|
||||
Column3: sinceTS,
|
||||
Column4: untilTS,
|
||||
Column5: cursorTS,
|
||||
Limit: limit + 1,
|
||||
Column7: scope,
|
||||
Column7: scopes,
|
||||
})
|
||||
if err != nil {
|
||||
return api.Page[api.PacketSummary]{}, err
|
||||
@@ -124,7 +124,7 @@ func (s *Store) ListPackets(ctx context.Context, payloadType, routeType int16, i
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) listPacketsByIATAs(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 *Store) listPacketsByIATAs(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) {
|
||||
var cursorTS pgtype.Timestamptz
|
||||
if cursor > 0 {
|
||||
cursorTS = pgtype.Timestamptz{Time: time.UnixMilli(cursor), Valid: true}
|
||||
@@ -141,15 +141,15 @@ func (s *Store) listPacketsByIATAs(ctx context.Context, payloadType, routeType i
|
||||
// ((packet_hash, observer_id) is unique), so scan deep enough per site
|
||||
// to still fill the page after collapsing those duplicates.
|
||||
rows, err := s.q.ListPacketsByIATAs(ctx, sqlc.ListPacketsByIATAsParams{
|
||||
Iatas: iatas,
|
||||
ScanDepth: (limit + 1) * 8,
|
||||
CursorTs: cursorTS,
|
||||
PayloadType: payloadType,
|
||||
RouteType: routeType,
|
||||
SinceTs: sinceTS,
|
||||
UntilTs: untilTS,
|
||||
ScopeName: scope,
|
||||
PageLimit: limit + 1,
|
||||
Iatas: iatas,
|
||||
ScanDepth: (limit + 1) * 8,
|
||||
CursorTs: cursorTS,
|
||||
PayloadTypes: payloadTypes,
|
||||
RouteTypes: routeTypes,
|
||||
SinceTs: sinceTS,
|
||||
UntilTs: untilTS,
|
||||
ScopeNames: scopes,
|
||||
PageLimit: limit + 1,
|
||||
})
|
||||
if err != nil {
|
||||
return api.Page[api.PacketSummary]{}, err
|
||||
|
||||
+5
-5
@@ -80,7 +80,7 @@ func TestListPackets_Pagination(t *testing.T) {
|
||||
Return(rows, nil)
|
||||
|
||||
store := &Store{q: mock}
|
||||
page, err := store.ListPackets(context.Background(), 0, 0, nil, "", time.Time{}, time.Time{}, 0, 2)
|
||||
page, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -113,7 +113,7 @@ func TestListPackets_LatestObserverNil(t *testing.T) {
|
||||
}, nil)
|
||||
|
||||
store := &Store{q: mock}
|
||||
page, err := store.ListPackets(context.Background(), 0, 0, nil, "", time.Time{}, time.Time{}, 0, 10)
|
||||
page, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -145,7 +145,7 @@ func TestListPackets_LatestObserverSet(t *testing.T) {
|
||||
}, nil)
|
||||
|
||||
store := &Store{q: mock}
|
||||
page, err := store.ListPackets(context.Background(), 0, 0, nil, "", time.Time{}, time.Time{}, 0, 10)
|
||||
page, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -422,7 +422,7 @@ func TestListPackets_IATAFilterRoutesToObservationIndex(t *testing.T) {
|
||||
})
|
||||
|
||||
store := &Store{q: mock}
|
||||
page, err := store.ListPackets(context.Background(), -1, -1, []string{"ALF"}, "", time.Time{}, time.Time{}, 0, 1)
|
||||
page, err := store.ListPackets(context.Background(), nil, nil, []string{"ALF"}, nil, time.Time{}, time.Time{}, 0, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -445,7 +445,7 @@ func TestListPackets_UnfilteredKeepsGlobalQuery(t *testing.T) {
|
||||
Return([]sqlc.ListPacketsRow{}, nil)
|
||||
|
||||
store := &Store{q: mock}
|
||||
if _, err := store.ListPackets(context.Background(), -1, -1, nil, "", time.Time{}, time.Time{}, 0, 50); err != nil {
|
||||
if _, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, 50); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -390,12 +390,12 @@ LEFT JOIN LATERAL (
|
||||
LEFT JOIN observers o ON o.id = po.observer_id
|
||||
LEFT JOIN transport_scopes ts ON ts.id = p.scope_id
|
||||
WHERE
|
||||
($1::smallint = -1 OR p.payload_type = $1::smallint)
|
||||
AND ($2::smallint = -1 OR p.route_type = $2::smallint)
|
||||
(COALESCE(cardinality($1::smallint[]), 0) = 0 OR p.payload_type = ANY($1::smallint[]))
|
||||
AND (COALESCE(cardinality($2::smallint[]), 0) = 0 OR p.route_type = ANY($2::smallint[]))
|
||||
AND ($3::timestamptz IS NULL OR p.first_heard_at >= $3)
|
||||
AND ($4::timestamptz IS NULL OR p.first_heard_at <= $4)
|
||||
AND ($5::timestamptz IS NULL OR p.last_heard_at < $5)
|
||||
AND ($7::text = '' OR ts.name = $7::text)
|
||||
AND (COALESCE(cardinality($7::text[]), 0) = 0 OR ts.name = ANY($7::text[]))
|
||||
ORDER BY p.last_heard_at DESC
|
||||
LIMIT $6;
|
||||
|
||||
@@ -431,13 +431,13 @@ FROM (
|
||||
JOIN packets p2 ON p2.packet_hash = po3.packet_hash
|
||||
WHERE po3.iata = req.iata
|
||||
AND po3.heard_at < COALESCE(@cursor_ts::timestamptz, 'infinity'::timestamptz)
|
||||
AND (@payload_type::smallint = -1 OR p2.payload_type = @payload_type::smallint)
|
||||
AND (@route_type::smallint = -1 OR p2.route_type = @route_type::smallint)
|
||||
AND (COALESCE(cardinality(@payload_types::smallint[]), 0) = 0 OR p2.payload_type = ANY(@payload_types::smallint[]))
|
||||
AND (COALESCE(cardinality(@route_types::smallint[]), 0) = 0 OR p2.route_type = ANY(@route_types::smallint[]))
|
||||
AND (@since_ts::timestamptz IS NULL OR p2.first_heard_at >= @since_ts)
|
||||
AND (@until_ts::timestamptz IS NULL OR p2.first_heard_at <= @until_ts)
|
||||
AND (@scope_name::text = '' OR EXISTS (
|
||||
AND (COALESCE(cardinality(@scope_names::text[]), 0) = 0 OR EXISTS (
|
||||
SELECT 1 FROM transport_scopes ts2
|
||||
WHERE ts2.id = p2.scope_id AND ts2.name = @scope_name::text))
|
||||
WHERE ts2.id = p2.scope_id AND ts2.name = ANY(@scope_names::text[])))
|
||||
ORDER BY po3.heard_at DESC
|
||||
LIMIT @scan_depth
|
||||
) hits
|
||||
|
||||
+22
-22
@@ -2558,24 +2558,24 @@ LEFT JOIN LATERAL (
|
||||
LEFT JOIN observers o ON o.id = po.observer_id
|
||||
LEFT JOIN transport_scopes ts ON ts.id = p.scope_id
|
||||
WHERE
|
||||
($1::smallint = -1 OR p.payload_type = $1::smallint)
|
||||
AND ($2::smallint = -1 OR p.route_type = $2::smallint)
|
||||
(COALESCE(cardinality($1::smallint[]), 0) = 0 OR p.payload_type = ANY($1::smallint[]))
|
||||
AND (COALESCE(cardinality($2::smallint[]), 0) = 0 OR p.route_type = ANY($2::smallint[]))
|
||||
AND ($3::timestamptz IS NULL OR p.first_heard_at >= $3)
|
||||
AND ($4::timestamptz IS NULL OR p.first_heard_at <= $4)
|
||||
AND ($5::timestamptz IS NULL OR p.last_heard_at < $5)
|
||||
AND ($7::text = '' OR ts.name = $7::text)
|
||||
AND (COALESCE(cardinality($7::text[]), 0) = 0 OR ts.name = ANY($7::text[]))
|
||||
ORDER BY p.last_heard_at DESC
|
||||
LIMIT $6
|
||||
`
|
||||
|
||||
type ListPacketsParams struct {
|
||||
Column1 int16 `json:"column_1"`
|
||||
Column2 int16 `json:"column_2"`
|
||||
Column1 []int16 `json:"column_1"`
|
||||
Column2 []int16 `json:"column_2"`
|
||||
Column3 pgtype.Timestamptz `json:"column_3"`
|
||||
Column4 pgtype.Timestamptz `json:"column_4"`
|
||||
Column5 pgtype.Timestamptz `json:"column_5"`
|
||||
Limit int32 `json:"limit"`
|
||||
Column7 string `json:"column_7"`
|
||||
Column7 []string `json:"column_7"`
|
||||
}
|
||||
|
||||
type ListPacketsRow struct {
|
||||
@@ -2745,13 +2745,13 @@ FROM (
|
||||
JOIN packets p2 ON p2.packet_hash = po3.packet_hash
|
||||
WHERE po3.iata = req.iata
|
||||
AND po3.heard_at < COALESCE($2::timestamptz, 'infinity'::timestamptz)
|
||||
AND ($3::smallint = -1 OR p2.payload_type = $3::smallint)
|
||||
AND ($4::smallint = -1 OR p2.route_type = $4::smallint)
|
||||
AND (COALESCE(cardinality($3::smallint[]), 0) = 0 OR p2.payload_type = ANY($3::smallint[]))
|
||||
AND (COALESCE(cardinality($4::smallint[]), 0) = 0 OR p2.route_type = ANY($4::smallint[]))
|
||||
AND ($5::timestamptz IS NULL OR p2.first_heard_at >= $5)
|
||||
AND ($6::timestamptz IS NULL OR p2.first_heard_at <= $6)
|
||||
AND ($7::text = '' OR EXISTS (
|
||||
AND (COALESCE(cardinality($7::text[]), 0) = 0 OR EXISTS (
|
||||
SELECT 1 FROM transport_scopes ts2
|
||||
WHERE ts2.id = p2.scope_id AND ts2.name = $7::text))
|
||||
WHERE ts2.id = p2.scope_id AND ts2.name = ANY($7::text[])))
|
||||
ORDER BY po3.heard_at DESC
|
||||
LIMIT $8
|
||||
) hits
|
||||
@@ -2778,15 +2778,15 @@ ORDER BY sh.site_heard_at DESC
|
||||
`
|
||||
|
||||
type ListPacketsByIATAsParams struct {
|
||||
Iatas []string `json:"iatas"`
|
||||
CursorTs pgtype.Timestamptz `json:"cursor_ts"`
|
||||
PayloadType int16 `json:"payload_type"`
|
||||
RouteType int16 `json:"route_type"`
|
||||
SinceTs pgtype.Timestamptz `json:"since_ts"`
|
||||
UntilTs pgtype.Timestamptz `json:"until_ts"`
|
||||
ScopeName string `json:"scope_name"`
|
||||
ScanDepth int32 `json:"scan_depth"`
|
||||
PageLimit int32 `json:"page_limit"`
|
||||
Iatas []string `json:"iatas"`
|
||||
CursorTs pgtype.Timestamptz `json:"cursor_ts"`
|
||||
PayloadTypes []int16 `json:"payload_types"`
|
||||
RouteTypes []int16 `json:"route_types"`
|
||||
SinceTs pgtype.Timestamptz `json:"since_ts"`
|
||||
UntilTs pgtype.Timestamptz `json:"until_ts"`
|
||||
ScopeNames []string `json:"scope_names"`
|
||||
ScanDepth int32 `json:"scan_depth"`
|
||||
PageLimit int32 `json:"page_limit"`
|
||||
}
|
||||
|
||||
type ListPacketsByIATAsRow struct {
|
||||
@@ -2817,11 +2817,11 @@ func (q *Queries) ListPacketsByIATAs(ctx context.Context, arg ListPacketsByIATAs
|
||||
rows, err := q.db.Query(ctx, listPacketsByIATAs,
|
||||
arg.Iatas,
|
||||
arg.CursorTs,
|
||||
arg.PayloadType,
|
||||
arg.RouteType,
|
||||
arg.PayloadTypes,
|
||||
arg.RouteTypes,
|
||||
arg.SinceTs,
|
||||
arg.UntilTs,
|
||||
arg.ScopeName,
|
||||
arg.ScopeNames,
|
||||
arg.ScanDepth,
|
||||
arg.PageLimit,
|
||||
)
|
||||
|
||||
@@ -968,6 +968,12 @@ const docTemplate = `{
|
||||
"name": "payloadType",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by multiple payload types, comma-separated e.g. 2,4",
|
||||
"name": "payloadTypes",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by payload type name (advert, grp_txt, txt_msg, trace, anon_req)",
|
||||
@@ -980,6 +986,12 @@ const docTemplate = `{
|
||||
"name": "routeType",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by multiple route types, comma-separated e.g. 0,1",
|
||||
"name": "routeTypes",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by single IATA code (case-insensitive)",
|
||||
@@ -998,6 +1010,12 @@ const docTemplate = `{
|
||||
"name": "scope",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by multiple transport scope names, comma-separated e.g. %23bc,%23west",
|
||||
"name": "scopes",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Filter by region ID, expands to member IATAs",
|
||||
|
||||
@@ -966,6 +966,12 @@
|
||||
"name": "payloadType",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by multiple payload types, comma-separated e.g. 2,4",
|
||||
"name": "payloadTypes",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by payload type name (advert, grp_txt, txt_msg, trace, anon_req)",
|
||||
@@ -978,6 +984,12 @@
|
||||
"name": "routeType",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by multiple route types, comma-separated e.g. 0,1",
|
||||
"name": "routeTypes",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by single IATA code (case-insensitive)",
|
||||
@@ -996,6 +1008,12 @@
|
||||
"name": "scope",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by multiple transport scope names, comma-separated e.g. %23bc,%23west",
|
||||
"name": "scopes",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Filter by region ID, expands to member IATAs",
|
||||
|
||||
@@ -1695,6 +1695,10 @@ paths:
|
||||
in: query
|
||||
name: payloadType
|
||||
type: integer
|
||||
- description: Filter by multiple payload types, comma-separated e.g. 2,4
|
||||
in: query
|
||||
name: payloadTypes
|
||||
type: string
|
||||
- description: Filter by payload type name (advert, grp_txt, txt_msg, trace,
|
||||
anon_req)
|
||||
in: query
|
||||
@@ -1704,6 +1708,10 @@ paths:
|
||||
in: query
|
||||
name: routeType
|
||||
type: integer
|
||||
- description: Filter by multiple route types, comma-separated e.g. 0,1
|
||||
in: query
|
||||
name: routeTypes
|
||||
type: string
|
||||
- description: Filter by single IATA code (case-insensitive)
|
||||
in: query
|
||||
name: iata
|
||||
@@ -1716,6 +1724,11 @@ paths:
|
||||
in: query
|
||||
name: scope
|
||||
type: string
|
||||
- description: Filter by multiple transport scope names, comma-separated e.g.
|
||||
%23bc,%23west
|
||||
in: query
|
||||
name: scopes
|
||||
type: string
|
||||
- description: Filter by region ID, expands to member IATAs
|
||||
in: query
|
||||
name: regionId
|
||||
|
||||
@@ -32,11 +32,14 @@ func PacketsRouter(reader api.Reader) http.Handler {
|
||||
// @Tags Packets
|
||||
// @Produce json
|
||||
// @Param payloadType query int false "Filter by payload type integer"
|
||||
// @Param payloadTypes query string false "Filter by multiple payload types, comma-separated e.g. 2,4"
|
||||
// @Param payloadTypeName query string false "Filter by payload type name (advert, grp_txt, txt_msg, trace, anon_req)"
|
||||
// @Param routeType query int false "Filter by route type (0=transport_flood, 1=flood, 2=direct, 3=transport_direct)"
|
||||
// @Param routeTypes query string false "Filter by multiple route types, comma-separated e.g. 0,1"
|
||||
// @Param iata query string false "Filter by single IATA code (case-insensitive)"
|
||||
// @Param iatas query string false "Filter by multiple IATA codes, comma-separated e.g. YVR,YYJ"
|
||||
// @Param scope query string false "Filter by transport scope name e.g. %23bc (URL-encoded #bc)"
|
||||
// @Param scopes query string false "Filter by multiple transport scope names, comma-separated e.g. %23bc,%23west"
|
||||
// @Param regionId query int false "Filter by region ID, expands to member IATAs"
|
||||
// @Param region query string false "Filter by region slug, expands to member IATAs"
|
||||
// @Param since query int false "Filter by first_heard_at >= since (epoch ms)"
|
||||
@@ -49,28 +52,20 @@ func PacketsRouter(reader api.Reader) http.Handler {
|
||||
// @Router /packets [get]
|
||||
func listPackets(reader api.Reader) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var payloadType int16 = -1
|
||||
if p := r.URL.Query().Get("payloadType"); p != "" {
|
||||
t, err := strconv.ParseInt(p, 10, 16)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "payloadType must be an integer")
|
||||
return
|
||||
}
|
||||
payloadType = int16(t)
|
||||
payloadTypes, err := parseInt16CSVOrSingle(r, "payloadTypes", "payloadType")
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "payloadType/payloadTypes must be integers")
|
||||
return
|
||||
}
|
||||
if payloadType == -1 {
|
||||
if len(payloadTypes) == 0 {
|
||||
if p := r.URL.Query().Get("payloadTypeName"); p != "" {
|
||||
payloadType = api.PayloadTypeFromString(p)
|
||||
payloadTypes = []int16{api.PayloadTypeFromString(p)}
|
||||
}
|
||||
}
|
||||
var routeType int16 = -1
|
||||
if p := r.URL.Query().Get("routeType"); p != "" {
|
||||
t, err := strconv.ParseInt(p, 10, 16)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "routeType must be an integer")
|
||||
return
|
||||
}
|
||||
routeType = int16(t)
|
||||
routeTypes, err := parseInt16CSVOrSingle(r, "routeTypes", "routeType")
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "routeType/routeTypes must be integers")
|
||||
return
|
||||
}
|
||||
var since, until time.Time
|
||||
if p := r.URL.Query().Get("since"); p != "" {
|
||||
@@ -116,8 +111,8 @@ func listPackets(reader api.Reader) http.HandlerFunc {
|
||||
}
|
||||
iatas = append(iatas, regionIATAs...)
|
||||
}
|
||||
scope := r.URL.Query().Get("scope")
|
||||
packets, err := reader.ListPackets(r.Context(), payloadType, routeType, iatas, scope, since, until, cursor, limit)
|
||||
scopes := parseCSVOrSingle(r, "scopes", "scope")
|
||||
packets, err := reader.ListPackets(r.Context(), payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
|
||||
@@ -127,7 +127,7 @@ func TestListPacketsBackfill_InvalidLimit(t *testing.T) {
|
||||
func TestListPackets_OK(t *testing.T) {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/packets", listPackets(stubReader{
|
||||
listPackets: func(_ context.Context, _, _ int16, _ []string, _ string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) {
|
||||
listPackets: func(_ context.Context, _, _ []int16, _ []string, _ []string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) {
|
||||
return api.Page[api.PacketSummary]{Items: []api.PacketSummary{{PacketHash: "deadbeef"}}}, nil
|
||||
},
|
||||
}))
|
||||
@@ -139,6 +139,108 @@ func TestListPackets_OK(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPackets_PluralParams_PassedThrough(t *testing.T) {
|
||||
var gotPayloadTypes, gotRouteTypes []int16
|
||||
var gotScopes []string
|
||||
r := chi.NewRouter()
|
||||
r.Get("/packets", listPackets(stubReader{
|
||||
listPackets: func(_ context.Context, payloadTypes, routeTypes []int16, _ []string, scopes []string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) {
|
||||
gotPayloadTypes = payloadTypes
|
||||
gotRouteTypes = routeTypes
|
||||
gotScopes = scopes
|
||||
return api.Page[api.PacketSummary]{}, nil
|
||||
},
|
||||
}))
|
||||
req := httptest.NewRequest(http.MethodGet, "/packets?payloadTypes=2,4&routeTypes=0,1&scopes=%23bc,%23west", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", w.Code)
|
||||
}
|
||||
if len(gotPayloadTypes) != 2 || gotPayloadTypes[0] != 2 || gotPayloadTypes[1] != 4 {
|
||||
t.Errorf("expected payloadTypes [2 4], got %v", gotPayloadTypes)
|
||||
}
|
||||
if len(gotRouteTypes) != 2 || gotRouteTypes[0] != 0 || gotRouteTypes[1] != 1 {
|
||||
t.Errorf("expected routeTypes [0 1], got %v", gotRouteTypes)
|
||||
}
|
||||
if len(gotScopes) != 2 || gotScopes[0] != "#bc" || gotScopes[1] != "#west" {
|
||||
t.Errorf("expected scopes [#bc #west], got %v", gotScopes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPackets_SingularParams_StillWork(t *testing.T) {
|
||||
var gotPayloadTypes, gotRouteTypes []int16
|
||||
var gotScopes []string
|
||||
r := chi.NewRouter()
|
||||
r.Get("/packets", listPackets(stubReader{
|
||||
listPackets: func(_ context.Context, payloadTypes, routeTypes []int16, _ []string, scopes []string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) {
|
||||
gotPayloadTypes = payloadTypes
|
||||
gotRouteTypes = routeTypes
|
||||
gotScopes = scopes
|
||||
return api.Page[api.PacketSummary]{}, nil
|
||||
},
|
||||
}))
|
||||
req := httptest.NewRequest(http.MethodGet, "/packets?payloadType=4&routeType=1&scope=%23bc", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", w.Code)
|
||||
}
|
||||
if len(gotPayloadTypes) != 1 || gotPayloadTypes[0] != 4 {
|
||||
t.Errorf("expected payloadTypes [4], got %v", gotPayloadTypes)
|
||||
}
|
||||
if len(gotRouteTypes) != 1 || gotRouteTypes[0] != 1 {
|
||||
t.Errorf("expected routeTypes [1], got %v", gotRouteTypes)
|
||||
}
|
||||
if len(gotScopes) != 1 || gotScopes[0] != "#bc" {
|
||||
t.Errorf("expected scopes [#bc], got %v", gotScopes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPackets_PluralParams_TakePrecedenceOverSingular(t *testing.T) {
|
||||
// Mirrors parseIATAs' precedence: when both are present, the plural param wins outright
|
||||
// rather than merging with the singular one.
|
||||
var gotPayloadTypes []int16
|
||||
r := chi.NewRouter()
|
||||
r.Get("/packets", listPackets(stubReader{
|
||||
listPackets: func(_ context.Context, payloadTypes, _ []int16, _ []string, _ []string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) {
|
||||
gotPayloadTypes = payloadTypes
|
||||
return api.Page[api.PacketSummary]{}, nil
|
||||
},
|
||||
}))
|
||||
req := httptest.NewRequest(http.MethodGet, "/packets?payloadType=9&payloadTypes=2,4", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", w.Code)
|
||||
}
|
||||
if len(gotPayloadTypes) != 2 || gotPayloadTypes[0] != 2 || gotPayloadTypes[1] != 4 {
|
||||
t.Errorf("expected payloadTypes [2 4] (plural wins), got %v", gotPayloadTypes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPackets_InvalidPayloadTypes(t *testing.T) {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/packets", listPackets(stubReader{}))
|
||||
req := httptest.NewRequest(http.MethodGet, "/packets?payloadTypes=2,notanint", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPackets_InvalidRouteTypes(t *testing.T) {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/packets", listPackets(stubReader{}))
|
||||
req := httptest.NewRequest(http.MethodGet, "/packets?routeTypes=0,notanint", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPacket_OK(t *testing.T) {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/packets/{packetHash}", getPacket(stubReader{
|
||||
|
||||
@@ -95,6 +95,59 @@ func parseIATAs(r *http.Request) []string {
|
||||
return iatas
|
||||
}
|
||||
|
||||
// parseInt16CSVOrSingle reads a comma-separated list from pluralParam, falling back to a
|
||||
// single value from singularParam if pluralParam is absent -- same precedence as parseIATAs.
|
||||
func parseInt16CSVOrSingle(r *http.Request, pluralParam, singularParam string) ([]int16, error) {
|
||||
raw := r.URL.Query().Get(pluralParam)
|
||||
if raw == "" {
|
||||
single := r.URL.Query().Get(singularParam)
|
||||
if single == "" {
|
||||
return nil, nil
|
||||
}
|
||||
v, err := strconv.ParseInt(single, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []int16{int16(v)}, nil
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]int16, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
v, err := strconv.ParseInt(p, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, int16(v))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// parseCSVOrSingle reads a comma-separated list from pluralParam, falling back to a single
|
||||
// value from singularParam if pluralParam is absent -- same precedence as parseIATAs, but
|
||||
// without the uppercasing (used for scope names, which are case-sensitive, e.g. "#bc").
|
||||
func parseCSVOrSingle(r *http.Request, pluralParam, singularParam string) []string {
|
||||
raw := r.URL.Query().Get(pluralParam)
|
||||
if raw == "" {
|
||||
if single := r.URL.Query().Get(singularParam); single != "" {
|
||||
return []string{single}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// resolveRegionIATAs expands a regionId or region slug to a slice of IATA codes.
|
||||
func resolveRegionIATAs(ctx context.Context, regionID, regionSlug string, reader api.Reader) ([]string, error) {
|
||||
var region *api.Region
|
||||
|
||||
@@ -35,7 +35,7 @@ type stubReader struct {
|
||||
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)
|
||||
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)
|
||||
@@ -201,9 +201,9 @@ func (s stubReader) ListNodeObservations(ctx context.Context, nodeID uuid.UUID,
|
||||
return api.Page[api.PacketObservationSummary]{}, nil
|
||||
}
|
||||
|
||||
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) {
|
||||
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, payloadType, routeType, iatas, scope, since, until, cursor, limit)
|
||||
return s.listPackets(ctx, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit)
|
||||
}
|
||||
return api.Page[api.PacketSummary]{}, nil
|
||||
}
|
||||
|
||||
@@ -119,7 +119,9 @@ type Reader interface {
|
||||
// Unfiltered lists order by global last_heard_at; when iatas are set, ordering
|
||||
// and the cursor are site-local (heard_at at the requested sites) instead.
|
||||
// cursor is epoch ms in either case; pass 0 to start from the beginning.
|
||||
ListPackets(ctx context.Context, payloadType, routeType int16, iatas []string, scope string, since, until time.Time, cursor int64, limit int32) (Page[PacketSummary], error)
|
||||
// payloadTypes/routeTypes/scopes are OR'd within each filter, ANDed across filters; pass
|
||||
// nil/empty to skip a filter.
|
||||
ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (Page[PacketSummary], error)
|
||||
|
||||
// ListPacketsAfterID returns packets with observations after the given observation ID,
|
||||
// ordered oldest first. Used for WS reconnect backfill.
|
||||
|
||||
Vendored
+1
-1
@@ -160,7 +160,7 @@ func (s *stubReader) ListObserverAdverts(_ context.Context, _ uuid.UUID, _ int64
|
||||
return api.Page[api.AdvertObservation]{}, nil
|
||||
}
|
||||
|
||||
func (s *stubReader) ListPackets(_ context.Context, _, _ int16, _ []string, _ string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) {
|
||||
func (s *stubReader) ListPackets(_ context.Context, _, _ []int16, _ []string, _ []string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) {
|
||||
return api.Page[api.PacketSummary]{}, nil
|
||||
}
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -394,8 +394,8 @@ func (cr *CachedReader) ListObserverAdverts(ctx context.Context, observerID uuid
|
||||
}
|
||||
|
||||
// ListPackets implements [api.Reader].
|
||||
func (cr *CachedReader) ListPackets(ctx context.Context, payloadType, routeType int16, iatas []string, scope string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) {
|
||||
return cr.inner.ListPackets(ctx, payloadType, routeType, iatas, scope, since, until, cursor, limit)
|
||||
func (cr *CachedReader) ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) {
|
||||
return cr.inner.ListPackets(ctx, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit)
|
||||
}
|
||||
|
||||
// ListPacketsAfterID implements [api.Reader].
|
||||
|
||||
Reference in new issue
Block a user