239 lines
9.3 KiB
Go
239 lines
9.3 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/proxy-pool/proxy-pool/internal/platform/httpapi"
|
|
)
|
|
|
|
func TestHandlerReturnsStatusWithoutSensitiveDetails(t *testing.T) {
|
|
t.Parallel()
|
|
service := &stubService{status: Status{
|
|
ConfigVersion: "cfg-2",
|
|
SnapshotVersion: 7,
|
|
Upstreams: []UpstreamStatus{{Name: "provider-a", Enabled: true, Available: 11}},
|
|
Workers: []WorkerStatus{{ID: "worker-a", Zone: "cn-east", Connected: true, SnapshotVersion: 7}},
|
|
}}
|
|
handler := mustHandler(t, service)
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/v1/status", nil))
|
|
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
var response Status
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if response.ConfigVersion != "cfg-2" || response.SnapshotVersion != 7 || len(response.Upstreams) != 1 {
|
|
t.Fatalf("unexpected status response: %+v", response)
|
|
}
|
|
}
|
|
|
|
func TestNewHandlerRejectsMissingDependenciesAndInvalidLimit(t *testing.T) {
|
|
t.Parallel()
|
|
if _, err := NewHandler(nil, Options{MaxBodyBytes: 1024}); !errors.Is(err, ErrInvalidHandler) {
|
|
t.Fatalf("NewHandler(nil) error = %v, want %v", err, ErrInvalidHandler)
|
|
}
|
|
if _, err := NewHandler(&stubService{}, Options{}); !errors.Is(err, ErrInvalidHandler) {
|
|
t.Fatalf("NewHandler(zero limit) error = %v, want %v", err, ErrInvalidHandler)
|
|
}
|
|
}
|
|
|
|
func TestHandlerEnablesAndDisablesUpstream(t *testing.T) {
|
|
t.Parallel()
|
|
service := &stubService{mutation: MutationResult{Changed: true, Version: 8}}
|
|
handler := mustHandler(t, service)
|
|
|
|
for _, test := range []struct {
|
|
path string
|
|
enabled bool
|
|
}{
|
|
{path: "/api/v1/upstreams/provider-a/enable", enabled: true},
|
|
{path: "/api/v1/upstreams/provider-a/disable", enabled: false},
|
|
} {
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodPost, test.path, nil)
|
|
request.Header.Set(httpapi.HeaderRequestID, "req-admin")
|
|
|
|
handler.ServeHTTP(recorder, request)
|
|
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("%s status = %d, want %d; body=%s", test.path, recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
if service.lastUpstream.Name != "provider-a" || service.lastUpstream.Enabled != test.enabled || service.lastUpstream.RequestID != "req-admin" {
|
|
t.Fatalf("unexpected service call: %+v", service.lastUpstream)
|
|
}
|
|
if requestID := recorder.Header().Get(httpapi.HeaderRequestID); requestID != "req-admin" {
|
|
t.Fatalf("response request ID = %q, want req-admin", requestID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandlerSwitchesRoutingWithStrictJSON(t *testing.T) {
|
|
t.Parallel()
|
|
service := &stubService{mutation: MutationResult{Changed: true, Version: 9}}
|
|
handler := mustHandler(t, service)
|
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/routing/checkout/switch", strings.NewReader(
|
|
`{"expectedCurrent":"provider-a","target":"provider-b","reason":"capacity"}`,
|
|
))
|
|
request.Header.Set("Content-Type", httpapi.JSONContentType)
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(recorder, request)
|
|
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
if service.lastSwitch.Name != "checkout" || service.lastSwitch.ExpectedCurrent != "provider-a" || service.lastSwitch.Target != "provider-b" {
|
|
t.Fatalf("unexpected switch call: command=%+v", service.lastSwitch)
|
|
}
|
|
}
|
|
|
|
func TestHandlerReloadsConfigurationWithCommandRequestID(t *testing.T) {
|
|
t.Parallel()
|
|
service := &stubService{mutation: MutationResult{Changed: true, Version: 10}}
|
|
handler := mustHandler(t, service)
|
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/config/reload", nil)
|
|
request.Header.Set(httpapi.HeaderRequestID, "req-reload")
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(recorder, request)
|
|
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
if service.lastReload.RequestID != "req-reload" {
|
|
t.Fatalf("reload command = %+v", service.lastReload)
|
|
}
|
|
if !strings.Contains(recorder.Body.String(), `"requestId":"req-reload"`) {
|
|
t.Fatalf("unexpected reload response %q", recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandlerMapsServiceErrorsToProblemContract(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
serviceErr error
|
|
wantStatus int
|
|
wantCode string
|
|
}{
|
|
{name: "not found", serviceErr: ErrNotFound, wantStatus: http.StatusNotFound, wantCode: "NOT_FOUND"},
|
|
{name: "conflict", serviceErr: ErrConflict, wantStatus: http.StatusConflict, wantCode: "CONFLICT"},
|
|
{name: "invalid configuration", serviceErr: ErrInvalidConfiguration, wantStatus: http.StatusUnprocessableEntity, wantCode: "INVALID_CONFIGURATION"},
|
|
{name: "unavailable", serviceErr: ErrUnavailable, wantStatus: http.StatusServiceUnavailable, wantCode: "SERVICE_UNAVAILABLE"},
|
|
{name: "internal", serviceErr: errors.New("database password=secret"), wantStatus: http.StatusInternalServerError, wantCode: "INTERNAL_ERROR"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
handler := mustHandler(t, &stubService{err: test.serviceErr})
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/config/reload", nil)
|
|
request.Header.Set(httpapi.HeaderRequestID, "req-error")
|
|
|
|
handler.ServeHTTP(recorder, request)
|
|
|
|
if recorder.Code != test.wantStatus {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, test.wantStatus, recorder.Body.String())
|
|
}
|
|
if body := recorder.Body.String(); !strings.Contains(body, `"code":"`+test.wantCode+`"`) || strings.Contains(body, "password") {
|
|
t.Fatalf("unexpected problem body %q", body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandlerRejectsInvalidTransportRequests(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
path string
|
|
body string
|
|
content string
|
|
requestID string
|
|
wantStatus int
|
|
}{
|
|
{name: "method", method: http.MethodPut, path: "/api/v1/config/reload", wantStatus: http.StatusMethodNotAllowed},
|
|
{name: "unknown route", method: http.MethodGet, path: "/missing", wantStatus: http.StatusNotFound},
|
|
{name: "invalid name", method: http.MethodPost, path: "/api/v1/upstreams//enable", wantStatus: http.StatusNotFound},
|
|
{name: "unknown JSON field", method: http.MethodPost, path: "/api/v1/routing/r/switch", body: `{"expectedCurrent":"a","target":"b","extra":1}`, content: httpapi.JSONContentType, wantStatus: http.StatusBadRequest},
|
|
{name: "unsupported media type", method: http.MethodPost, path: "/api/v1/routing/r/switch", body: `{}`, content: "text/plain", wantStatus: http.StatusUnsupportedMediaType},
|
|
{name: "oversized body", method: http.MethodPost, path: "/api/v1/routing/r/switch", body: strings.Repeat(" ", 1100) + `{}`, content: httpapi.JSONContentType, wantStatus: http.StatusRequestEntityTooLarge},
|
|
{name: "invalid switch fields", method: http.MethodPost, path: "/api/v1/routing/r/switch", body: `{"expectedCurrent":"a"}`, content: httpapi.JSONContentType, wantStatus: http.StatusUnprocessableEntity},
|
|
{name: "invalid request ID", method: http.MethodPost, path: "/api/v1/config/reload", requestID: strings.Repeat("x", 129), wantStatus: http.StatusBadRequest},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
handler := mustHandler(t, &stubService{})
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(test.method, test.path, strings.NewReader(test.body))
|
|
request.Header.Set("Content-Type", test.content)
|
|
request.Header.Set(httpapi.HeaderRequestID, test.requestID)
|
|
|
|
handler.ServeHTTP(recorder, request)
|
|
|
|
if recorder.Code != test.wantStatus {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, test.wantStatus, recorder.Body.String())
|
|
}
|
|
if contentType := recorder.Header().Get("Content-Type"); contentType != httpapi.ProblemContentType {
|
|
t.Fatalf("Content-Type = %q, want %q", contentType, httpapi.ProblemContentType)
|
|
}
|
|
if requestID := recorder.Header().Get(httpapi.HeaderRequestID); requestID == "" {
|
|
t.Fatal("X-Request-ID response header is empty")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func mustHandler(t *testing.T, service Service) *Handler {
|
|
t.Helper()
|
|
handler, err := NewHandler(service, Options{MaxBodyBytes: 1024})
|
|
if err != nil {
|
|
t.Fatalf("NewHandler() error = %v", err)
|
|
}
|
|
return handler
|
|
}
|
|
|
|
type stubService struct {
|
|
status Status
|
|
mutation MutationResult
|
|
err error
|
|
lastUpstream SetUpstreamCommand
|
|
lastSwitch SwitchCommand
|
|
lastReload ReloadCommand
|
|
}
|
|
|
|
func (service *stubService) Status(context.Context) (Status, error) {
|
|
return service.status, service.err
|
|
}
|
|
|
|
func (service *stubService) SetUpstreamEnabled(_ context.Context, command SetUpstreamCommand) (MutationResult, error) {
|
|
service.lastUpstream = command
|
|
return service.mutation, service.err
|
|
}
|
|
|
|
func (service *stubService) SwitchRouting(_ context.Context, command SwitchCommand) (MutationResult, error) {
|
|
service.lastSwitch = command
|
|
return service.mutation, service.err
|
|
}
|
|
|
|
func (service *stubService) ReloadConfiguration(_ context.Context, command ReloadCommand) (MutationResult, error) {
|
|
service.lastReload = command
|
|
return service.mutation, service.err
|
|
}
|