Files
orbit/internal/transport/httpserver/server_test.go

89 lines
3.4 KiB
Go

package httpserver
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.yuntuo.hk/k2safe/orbit/internal/observability"
)
func TestHealthSeparatesLivenessAndReadiness(t *testing.T) {
server := New("127.0.0.1:0", func(context.Context) error { return errors.New("internal-database-details") })
for path, want := range map[string]int{"/health/live": 200, "/health/ready": 503, "/v1/tasks": 404} {
rr := httptest.NewRecorder()
server.Handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, path, nil))
if rr.Code != want || strings.Contains(rr.Body.String(), "internal-database-details") {
t.Fatalf("%s: %d %s", path, rr.Code, rr.Body.String())
}
if strings.HasPrefix(path, "/health/") {
if rr.Header().Get("Cache-Control") != "no-store" || rr.Header().Get("X-Content-Type-Options") != "nosniff" {
t.Fatalf("%s: missing anti-cache/security headers: %#v", path, rr.Header())
}
if path == "/health/ready" && rr.Header().Get("Retry-After") != "1" {
t.Fatalf("readiness failure missing retry hint: %#v", rr.Header())
}
}
}
}
func TestReadinessEntersDrainBeforeShutdown(t *testing.T) {
checks := 0
readiness := NewReadiness(func(context.Context) error {
checks++
return nil
})
server := New("127.0.0.1:0", readiness.Check)
ready := httptest.NewRecorder()
server.Handler.ServeHTTP(ready, httptest.NewRequest(http.MethodGet, "/health/ready", nil))
if ready.Code != http.StatusOK || checks != 1 {
t.Fatalf("before drain: status=%d checks=%d", ready.Code, checks)
}
readiness.BeginDrain()
draining := httptest.NewRecorder()
server.Handler.ServeHTTP(draining, httptest.NewRequest(http.MethodGet, "/health/ready", nil))
if draining.Code != http.StatusServiceUnavailable || checks != 1 || strings.Contains(draining.Body.String(), "server is draining") {
t.Fatalf("during drain: status=%d checks=%d body=%s", draining.Code, checks, draining.Body.String())
}
if draining.Header().Get("Retry-After") != "1" || draining.Header().Get("Cache-Control") != "no-store" {
t.Fatalf("draining readiness headers: %#v", draining.Header())
}
}
func TestVersionIsPublicAndBounded(t *testing.T) {
server := New("127.0.0.1:0", nil)
rr := httptest.NewRecorder()
server.Handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/version", nil))
if rr.Code != http.StatusOK || rr.Header().Get("Cache-Control") != "no-store" || rr.Header().Get("X-Content-Type-Options") != "nosniff" || rr.Header().Get("Content-Type") != "application/json" || !strings.Contains(rr.Body.String(), `"http_protocol_version":"1"`) || strings.Contains(rr.Body.String(), "ORBIT_MYSQL_DSN") {
t.Fatalf("unexpected version response: %d %s", rr.Code, rr.Body.String())
}
}
func TestMetricsListenerIsSeparateAndOptional(t *testing.T) {
metrics := observability.NewRegistry()
business := New("", nil)
WrapMetrics(business, metrics)
private := NewMetricsServer("", metrics)
for _, tc := range []struct {
handler http.Handler
path string
want int
}{
{business.Handler, "/metrics", 404}, {private.Handler, "/v1/tasks", 404},
{private.Handler, "/metrics", 200}, {NewMetricsServer("", nil).Handler, "/metrics", 404},
{private.Handler, "/metrics?secret=value", 400},
} {
rr := httptest.NewRecorder()
tc.handler.ServeHTTP(rr, httptest.NewRequest("GET", tc.path, nil))
if rr.Code != tc.want {
t.Fatalf("%s: %d", tc.path, rr.Code)
}
if strings.Contains(rr.Body.String(), "secret") {
t.Fatal("query leaked")
}
}
}