api calls
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.dcrubro.com/dcrubro/hammy-backend/internal/apikey"
|
||||
"git.dcrubro.com/dcrubro/hammy-backend/internal/ratelimit"
|
||||
)
|
||||
|
||||
func testRouter(t *testing.T, ready func() error) http.Handler {
|
||||
t.Helper()
|
||||
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
|
||||
rt := &Router{
|
||||
Auth: &Authenticator{
|
||||
Keys: &fakeStore{rec: activeRecord()},
|
||||
Limiter: ratelimit.NewMemory(),
|
||||
Logger: logger,
|
||||
},
|
||||
Logger: logger,
|
||||
Version: "test",
|
||||
Ready: ready,
|
||||
}
|
||||
|
||||
return rt.Handler()
|
||||
}
|
||||
|
||||
func TestHealthzNeedsNoKey(t *testing.T) {
|
||||
h := testRouter(t, nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/healthz", nil))
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status %d, want 200", w.Code)
|
||||
}
|
||||
|
||||
if id := w.Header().Get("X-Request-Id"); id == "" {
|
||||
t.Error("no X-Request-Id on the response")
|
||||
}
|
||||
}
|
||||
|
||||
// Liveness must not depend on Postgres. If it did, a database blip would get
|
||||
// the process killed by its supervisor and turn a wobble into an outage.
|
||||
func TestHealthzIgnoresDependencies(t *testing.T) {
|
||||
h := testRouter(t, func() error { return errors.New("postgres down") })
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/healthz", nil))
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("healthz status %d with a failing dependency, want 200", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadyzReflectsDependencies(t *testing.T) {
|
||||
h := testRouter(t, func() error { return errors.New("postgres down") })
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/readyz", nil))
|
||||
|
||||
if w.Code != http.StatusServiceUnavailable {
|
||||
t.Errorf("readyz status %d, want 503", w.Code)
|
||||
}
|
||||
|
||||
ok := testRouter(t, func() error { return nil })
|
||||
w = httptest.NewRecorder()
|
||||
ok.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/readyz", nil))
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("healthy readyz status %d, want 200", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV1RequiresKey(t *testing.T) {
|
||||
h := testRouter(t, nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/v1/whoami", nil))
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("status %d, want 401", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoamiEndToEnd(t *testing.T) {
|
||||
h := testRouter(t, nil)
|
||||
|
||||
g, err := apikey.Generate(apikey.Live)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/v1/whoami", nil)
|
||||
r.Header.Set("Authorization", "Bearer "+g.Plaintext)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var got struct {
|
||||
KeyID int64 `json:"key_id"`
|
||||
OwnerID int64 `json:"owner_id"`
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got.KeyID != 42 || got.OwnerID != 7 {
|
||||
t.Errorf("principal not propagated: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopeEnforcedOnRoute(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
|
||||
rec := activeRecord()
|
||||
rec.Scopes = []string{ScopeLogbook} // deliberately not reference
|
||||
|
||||
rt := &Router{
|
||||
Auth: &Authenticator{
|
||||
Keys: &fakeStore{rec: rec},
|
||||
Limiter: ratelimit.NewMemory(),
|
||||
Logger: logger,
|
||||
},
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
g, _ := apikey.Generate(apikey.Live)
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/v1/reference/ping", nil)
|
||||
r.Header.Set("Authorization", "Bearer "+g.Plaintext)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
rt.Handler().ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("status %d, want 403", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownPathIsJSON404(t *testing.T) {
|
||||
h := testRouter(t, nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/nope", nil))
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("status %d, want 404", w.Code)
|
||||
}
|
||||
|
||||
if ct := w.Header().Get("Content-Type"); ct[:16] != "application/json" {
|
||||
t.Errorf("Content-Type %q, want JSON so clients can parse errors uniformly", ct)
|
||||
}
|
||||
}
|
||||
|
||||
// A wrong method on a real path must 405, not 404.
|
||||
func TestMethodMismatch(t *testing.T) {
|
||||
h := testRouter(t, nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/healthz", nil))
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("status %d, want 405", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// A panicking handler must produce a 500, not kill the process.
|
||||
func TestRecoveryMiddleware(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
|
||||
boom := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
panic("kaboom")
|
||||
})
|
||||
|
||||
h := Chain(boom, WithRequestID, WithRecovery(logger))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/x", nil))
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("status %d, want 500", w.Code)
|
||||
}
|
||||
|
||||
if got := bodyCode(t, w); got != CodeInternal {
|
||||
t.Errorf("code %q, want %q", got, CodeInternal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestIDHonoursInbound(t *testing.T) {
|
||||
h := Chain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := RequestIDFrom(r.Context()); got != "abc123" {
|
||||
t.Errorf("request id %q, want abc123", got)
|
||||
}
|
||||
}), WithRequestID)
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
r.Header.Set("X-Request-Id", "abc123")
|
||||
|
||||
h.ServeHTTP(httptest.NewRecorder(), r)
|
||||
}
|
||||
Reference in New Issue
Block a user