Files
NianAIGC/backend/internal/httpapi/httpapi_test.go

181 lines
6.2 KiB
Go

package httpapi_test
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/httpapi"
)
func TestHealthReportsProcessAndDatabaseConfigurationWithoutReadinessProbe(t *testing.T) {
readiness := &readinessStub{
status: httpapi.DatabaseStatus{Backend: "postgres", Configured: true},
}
recorder := httptest.NewRecorder()
httpapi.NewHandler(readiness).ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/health", nil))
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK)
}
var response struct {
OK bool `json:"ok"`
AppID string `json:"appId"`
WebOnly bool `json:"webOnly"`
Database httpapi.DatabaseStatus `json:"database"`
}
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode response: %v", err)
}
if !response.OK || response.AppID != "zhinian-web-studio" || !response.WebOnly {
t.Fatalf("process response = %+v", response)
}
if response.Database != readiness.status {
t.Fatalf("database = %+v, want %+v", response.Database, readiness.status)
}
if readiness.readyCalls != 0 {
t.Fatalf("health made %d readiness probes, want 0", readiness.readyCalls)
}
}
func TestReadyReportsSuccessfulDatabaseProbe(t *testing.T) {
readiness := &readinessStub{
status: httpapi.DatabaseStatus{Backend: "postgres", Configured: true},
readyStatus: httpapi.DatabaseStatus{Backend: "postgres", Configured: true},
}
recorder := httptest.NewRecorder()
httpapi.NewHandler(readiness).ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/ready", nil))
assertReadyResponse(t, recorder, http.StatusOK, true, readiness.readyStatus)
}
func TestReadyReportsUnavailableDatabase(t *testing.T) {
readiness := &readinessStub{
status: httpapi.DatabaseStatus{Backend: "postgres", Configured: true},
readyStatus: httpapi.DatabaseStatus{Backend: "postgres", Configured: true},
readyErr: errors.New("database unavailable"),
}
recorder := httptest.NewRecorder()
httpapi.NewHandler(readiness).ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/ready", nil))
assertReadyResponse(t, recorder, http.StatusServiceUnavailable, false, readiness.readyStatus)
}
func TestReadyUsesConfiguredProbeTimeout(t *testing.T) {
readiness := &readinessStub{
status: httpapi.DatabaseStatus{Backend: "postgres", Configured: true},
readyStatus: httpapi.DatabaseStatus{Backend: "postgres", Configured: true},
}
recorder := httptest.NewRecorder()
timeout := 17 * time.Millisecond
httpapi.NewHandler(readiness, httpapi.WithReadinessTimeout(timeout)).ServeHTTP(
recorder,
httptest.NewRequest(http.MethodGet, "/api/ready", nil),
)
if readiness.deadlineRemaining <= 0 || readiness.deadlineRemaining > timeout {
t.Fatalf("probe deadline remaining = %s, want within (0, %s]", readiness.deadlineRemaining, timeout)
}
}
func TestReadyDefaultsToThreeSecondProbeTimeout(t *testing.T) {
readiness := &readinessStub{
readyStatus: httpapi.DatabaseStatus{Backend: "postgres", Configured: true},
}
recorder := httptest.NewRecorder()
httpapi.NewHandler(readiness).ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/ready", nil))
if readiness.deadlineRemaining < 2900*time.Millisecond || readiness.deadlineRemaining > 3*time.Second {
t.Fatalf("probe deadline remaining = %s, want approximately 3s", readiness.deadlineRemaining)
}
}
func TestHealthReflectsInvalidDatabaseConfiguration(t *testing.T) {
readiness := &readinessStub{
status: httpapi.DatabaseStatus{Backend: "invalid", Configured: false},
}
recorder := httptest.NewRecorder()
httpapi.NewHandler(readiness).ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/health", nil))
var response struct {
Database httpapi.DatabaseStatus `json:"database"`
}
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.Database != readiness.status {
t.Fatalf("database = %+v, want %+v", response.Database, readiness.status)
}
}
func TestHandlerRejectsUnsupportedMethodsAndUnknownPaths(t *testing.T) {
tests := []struct {
name string
method string
path string
wantStatus int
wantAllow string
}{
{name: "health post", method: http.MethodPost, path: "/api/health", wantStatus: http.StatusMethodNotAllowed, wantAllow: http.MethodGet},
{name: "ready post", method: http.MethodPost, path: "/api/ready", wantStatus: http.StatusMethodNotAllowed, wantAllow: http.MethodGet},
{name: "unknown", method: http.MethodGet, path: "/api/missing", wantStatus: http.StatusNotFound},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
httpapi.NewHandler(&readinessStub{}).ServeHTTP(recorder, httptest.NewRequest(test.method, test.path, nil))
if recorder.Code != test.wantStatus {
t.Fatalf("status = %d, want %d", recorder.Code, test.wantStatus)
}
if allow := recorder.Header().Get("Allow"); allow != test.wantAllow {
t.Fatalf("Allow = %q, want %q", allow, test.wantAllow)
}
})
}
}
func assertReadyResponse(t *testing.T, recorder *httptest.ResponseRecorder, wantStatus int, wantOK bool, wantDatabase httpapi.DatabaseStatus) {
t.Helper()
if recorder.Code != wantStatus {
t.Fatalf("status = %d, want %d", recorder.Code, wantStatus)
}
var response struct {
OK bool `json:"ok"`
Database httpapi.DatabaseStatus `json:"database"`
}
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.OK != wantOK || response.Database != wantDatabase {
t.Fatalf("response = %+v, want ok=%t database=%+v", response, wantOK, wantDatabase)
}
}
type readinessStub struct {
status httpapi.DatabaseStatus
readyStatus httpapi.DatabaseStatus
readyErr error
readyCalls int
deadlineRemaining time.Duration
}
func (s *readinessStub) Status() httpapi.DatabaseStatus { return s.status }
func (s *readinessStub) Ready(ctx context.Context) (httpapi.DatabaseStatus, error) {
s.readyCalls++
if deadline, ok := ctx.Deadline(); ok {
s.deadlineRemaining = time.Until(deadline)
}
return s.readyStatus, s.readyErr
}