181 lines
6.2 KiB
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
|
|
}
|