Files
beacon-server/internal/api/handlers/scopes_test.go
MrAlders0n 5ccb1a2ce3 fix(api): warn on untrusted proxies and tighten config validation
Warn at startup when limits are on without trusted_proxies, and once when a
request carries forwarding headers Beacon ignores. Reject short or whitespace
admin keys and negative durations. Key default-window stats by hour, return
500 for scope lookup failures, and validate IATA path params.
2026-10-02 16:53:32 -04:00

114 lines
3.3 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"
"slices"
"testing"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/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
},
}, 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)
}
}
func TestGetScope_Errors(t *testing.T) {
for _, tc := range []struct {
name string
scope *api.ScopeDetail
err error
want int
}{
{"no rows", nil, pgx.ErrNoRows, http.StatusNotFound},
{"nil scope", nil, nil, http.StatusNotFound},
{"database failure", nil, context.DeadlineExceeded, http.StatusInternalServerError},
} {
t.Run(tc.name, func(t *testing.T) {
r := chi.NewRouter()
r.Get("/scopes/{name}", getScope(stubReader{
getScopeByName: func(context.Context, string) (*api.ScopeDetail, error) { return tc.scope, tc.err },
}))
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/scopes/%23bc", nil))
if w.Code != tc.want {
t.Fatalf("status = %d, want %d", w.Code, tc.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())
}
})
}
}
func TestListScopes_FiltersByMembership(t *testing.T) {
reader := stubReader{
getScopeNames: func(context.Context) ([]string, error) {
t.Fatal("filtered list must not fall back to every stored name")
return nil, nil
},
getRegionBySlug: func(_ context.Context, slug string) (*api.Region, error) {
if slug == "empty" {
return &api.Region{}, nil
}
return &api.Region{IATAs: []string{"YOW", "YGK"}}, nil
},
}
members := testScopes(map[string][]string{"YOW": {"#on", "#yow"}, "YGK": {"#on"}, "YUL": {"#qc"}})
for _, tc := range []struct {
query string
members ScopeMembership
want []string
}{
{"iatas=yow", members, []string{"#on", "#yow"}},
{"iatas=YUL,YYZ", members, []string{"#qc"}},
{"region=ottawa", members, []string{"#on", "#yow"}},
{"region=empty", members, []string{}},
{"iatas=YOW", nil, []string{}},
} {
w := httptest.NewRecorder()
listScopes(reader, tc.members)(w, httptest.NewRequest(http.MethodGet, "/scopes?"+tc.query, nil))
var got []string
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil || w.Code != http.StatusOK || got == nil {
t.Fatalf("%q: status %d body %s", tc.query, w.Code, w.Body.String())
}
if !slices.Equal(got, tc.want) {
t.Fatalf("%q: got %v, want %v", tc.query, got, tc.want)
}
}
}