Files
beacon-server/internal/api/handlers/lookup_errors_test.go
MrAlders0n a1aff810e9 fix(api): answer 404 only for missing rows
Lookups turned every error into a 404, and region filters into a 400, so a
transient database failure looked final to clients that don't retry 4xx.
Missing rows stay 404/400; anything else is a 500. Empty /regions and
/iatas lists are [] rather than null.
2026-10-02 19:03:46 -04:00

117 lines
4.2 KiB
Go

// 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/google/uuid"
"github.com/jackc/pgx/v5"
)
// Lookups answer 404 only for a missing row; any other failure is a retryable 500.
func TestLookupErrors(t *testing.T) {
id := uuid.NewString()
for _, tc := range []struct {
name string
router func(error) http.Handler
path string
missing int // status for pgx.ErrNoRows
}{
{"region", func(err error) http.Handler {
return RegionsRouter(stubReader{getRegion: func(context.Context, int32) (*api.Region, error) { return nil, err }})
}, "/1", http.StatusNotFound},
{"regions", func(err error) http.Handler {
return RegionsRouter(stubReader{listRegions: func(context.Context) ([]api.RegionSummary, error) { return nil, err }})
}, "/", http.StatusInternalServerError},
{"iata", func(err error) http.Handler {
return IATAsRouter(stubReader{getIATA: func(context.Context, string) (*api.IATA, error) { return nil, err }})
}, "/YOW", http.StatusNotFound},
{"iata border", func(err error) http.Handler {
return IATAsRouter(stubReader{getIATABorder: func(context.Context, string) (json.RawMessage, error) { return nil, err }})
}, "/YOW/border", http.StatusNotFound},
{"iatas", func(err error) http.Handler {
return IATAsRouter(stubReader{listIATAs: func(context.Context) ([]api.IATA, error) { return nil, err }})
}, "/", http.StatusInternalServerError},
{"channel", func(err error) http.Handler {
return ChannelsRouter(stubReader{getChannel: func(context.Context, int32) (*api.Channel, error) { return nil, err }})
}, "/7", http.StatusNotFound},
{"observer", func(err error) http.Handler {
return ObserversRouter(stubReader{getObserver: func(context.Context, uuid.UUID) (*api.Observer, error) { return nil, err }})
}, "/" + id, http.StatusNotFound},
{"node", func(err error) http.Handler {
return NodesRouter(stubReader{getNode: func(context.Context, uuid.UUID) (*api.Node, error) { return nil, err }})
}, "/" + id, http.StatusNotFound},
{"packet", func(err error) http.Handler {
return PacketsRouter(stubReader{getPacket: func(context.Context, []byte) (*api.Packet, error) { return nil, err }})
}, "/abcd", http.StatusNotFound},
} {
for _, c := range []struct {
err error
want int
}{
{pgx.ErrNoRows, tc.missing},
{context.DeadlineExceeded, http.StatusInternalServerError},
} {
t.Run(tc.name+"/"+c.err.Error(), func(t *testing.T) {
w := httptest.NewRecorder()
tc.router(c.err).ServeHTTP(w, httptest.NewRequest(http.MethodGet, tc.path, nil))
if w.Code != c.want {
t.Fatalf("status = %d, want %d", w.Code, c.want)
}
var body map[string]APIError
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil || body["error"].Message == "" {
t.Fatalf("not a JSON error: %s", w.Body.String())
}
})
}
}
}
// An unknown region filter is the caller's mistake; a failed lookup is retryable.
func TestRegionFilterErrors(t *testing.T) {
for _, tc := range []struct {
err error
want int
}{
{pgx.ErrNoRows, http.StatusBadRequest},
{context.DeadlineExceeded, http.StatusInternalServerError},
} {
reader := stubReader{getRegionBySlug: func(context.Context, string) (*api.Region, error) { return nil, tc.err }}
for name, h := range map[string]http.Handler{
"packets": PacketsRouter(reader),
"nodes": NodesRouter(reader),
"traces": TracesRouter(reader),
} {
t.Run(name+"/"+tc.err.Error(), func(t *testing.T) {
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/?region=nowhere", nil))
if w.Code != tc.want {
t.Fatalf("status = %d, want %d", w.Code, tc.want)
}
})
}
}
}
func TestEmptyListsAreArrays(t *testing.T) {
for name, h := range map[string]http.Handler{
"regions": RegionsRouter(stubReader{}),
"iatas": IATAsRouter(stubReader{}),
} {
t.Run(name, func(t *testing.T) {
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil))
if w.Code != http.StatusOK || w.Body.String() != "[]\n" {
t.Fatalf("got %d %q, want 200 []", w.Code, w.Body.String())
}
})
}
}