173 lines
5.6 KiB
Go
173 lines
5.6 KiB
Go
|
|
package server
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"net/url"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"scm.bstein.dev/bstein/soteria/internal/api"
|
||
|
|
"scm.bstein.dev/bstein/soteria/internal/config"
|
||
|
|
)
|
||
|
|
|
||
|
|
type policyTestKubeClient struct {
|
||
|
|
*fakeKubeClient
|
||
|
|
saveErr error
|
||
|
|
}
|
||
|
|
|
||
|
|
func (k *policyTestKubeClient) SaveSecretData(ctx context.Context, namespace, secretName, key string, value []byte, labels map[string]string) error {
|
||
|
|
if k.saveErr != nil {
|
||
|
|
return k.saveErr
|
||
|
|
}
|
||
|
|
return k.fakeKubeClient.SaveSecretData(ctx, namespace, secretName, key, value, labels)
|
||
|
|
}
|
||
|
|
|
||
|
|
func newPolicyHandlerServer(client kubeClient) *Server {
|
||
|
|
srv := &Server{
|
||
|
|
cfg: &config.Config{
|
||
|
|
AuthRequired: false,
|
||
|
|
BackupDriver: "restic",
|
||
|
|
Namespace: "maintenance",
|
||
|
|
PolicySecretName: "soteria-policies",
|
||
|
|
},
|
||
|
|
client: client,
|
||
|
|
longhorn: &fakeLonghornClient{},
|
||
|
|
metrics: newTelemetry(),
|
||
|
|
policies: map[string]api.BackupPolicy{},
|
||
|
|
}
|
||
|
|
srv.handler = http.HandlerFunc(srv.route)
|
||
|
|
return srv
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHandlePoliciesRejectsUnsupportedAndInvalidRequests(t *testing.T) {
|
||
|
|
srv := newPolicyHandlerServer(&policyTestKubeClient{fakeKubeClient: &fakeKubeClient{}})
|
||
|
|
|
||
|
|
t.Run("method not allowed", func(t *testing.T) {
|
||
|
|
req := httptest.NewRequest(http.MethodPut, "/v1/policies", nil)
|
||
|
|
res := httptest.NewRecorder()
|
||
|
|
srv.Handler().ServeHTTP(res, req)
|
||
|
|
|
||
|
|
if res.Code != http.StatusMethodNotAllowed {
|
||
|
|
t.Fatalf("expected 405, got %d: %s", res.Code, res.Body.String())
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("invalid json", func(t *testing.T) {
|
||
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/policies", strings.NewReader(`{"namespace":`))
|
||
|
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
res := httptest.NewRecorder()
|
||
|
|
srv.Handler().ServeHTTP(res, req)
|
||
|
|
|
||
|
|
if res.Code != http.StatusBadRequest || !strings.Contains(res.Body.String(), "invalid JSON") {
|
||
|
|
t.Fatalf("expected invalid JSON 400, got %d: %s", res.Code, res.Body.String())
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("validation error", func(t *testing.T) {
|
||
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/policies", strings.NewReader(`{"namespace":"","interval_hours":2}`))
|
||
|
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
res := httptest.NewRecorder()
|
||
|
|
srv.Handler().ServeHTTP(res, req)
|
||
|
|
|
||
|
|
if res.Code != http.StatusBadRequest || !strings.Contains(res.Body.String(), "namespace is required") {
|
||
|
|
t.Fatalf("expected namespace validation error, got %d: %s", res.Code, res.Body.String())
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHandlePoliciesSurfacesPersistFailure(t *testing.T) {
|
||
|
|
srv := newPolicyHandlerServer(&policyTestKubeClient{
|
||
|
|
fakeKubeClient: &fakeKubeClient{},
|
||
|
|
saveErr: errors.New("save exploded"),
|
||
|
|
})
|
||
|
|
|
||
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/policies", strings.NewReader(`{"namespace":"apps","interval_hours":2}`))
|
||
|
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
res := httptest.NewRecorder()
|
||
|
|
srv.Handler().ServeHTTP(res, req)
|
||
|
|
|
||
|
|
if res.Code != http.StatusBadRequest || !strings.Contains(res.Body.String(), "save exploded") {
|
||
|
|
t.Fatalf("expected persist failure surfaced as 400, got %d: %s", res.Code, res.Body.String())
|
||
|
|
}
|
||
|
|
if len(srv.policies) != 0 {
|
||
|
|
t.Fatalf("expected failed upsert to roll back in-memory policies, got %#v", srv.policies)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHandlePolicyByIDRejectsBadRequestsAndMissingPolicies(t *testing.T) {
|
||
|
|
srv := newPolicyHandlerServer(&policyTestKubeClient{fakeKubeClient: &fakeKubeClient{}})
|
||
|
|
|
||
|
|
t.Run("method not allowed", func(t *testing.T) {
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/policies/apps__data", nil)
|
||
|
|
res := httptest.NewRecorder()
|
||
|
|
srv.Handler().ServeHTTP(res, req)
|
||
|
|
|
||
|
|
if res.Code != http.StatusMethodNotAllowed {
|
||
|
|
t.Fatalf("expected 405, got %d: %s", res.Code, res.Body.String())
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("missing id", func(t *testing.T) {
|
||
|
|
req := httptest.NewRequest(http.MethodDelete, "/v1/policies/", nil)
|
||
|
|
res := httptest.NewRecorder()
|
||
|
|
srv.Handler().ServeHTTP(res, req)
|
||
|
|
|
||
|
|
if res.Code != http.StatusBadRequest || !strings.Contains(res.Body.String(), "policy id is required") {
|
||
|
|
t.Fatalf("expected missing id error, got %d: %s", res.Code, res.Body.String())
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("invalid escaped id", func(t *testing.T) {
|
||
|
|
req := &http.Request{
|
||
|
|
Method: http.MethodDelete,
|
||
|
|
URL: &url.URL{Path: "/v1/policies/%zz"},
|
||
|
|
Header: make(http.Header),
|
||
|
|
}
|
||
|
|
res := httptest.NewRecorder()
|
||
|
|
srv.Handler().ServeHTTP(res, req)
|
||
|
|
|
||
|
|
if res.Code != http.StatusBadRequest || !strings.Contains(res.Body.String(), "invalid policy id") {
|
||
|
|
t.Fatalf("expected invalid escaped id error, got %d: %s", res.Code, res.Body.String())
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("not found", func(t *testing.T) {
|
||
|
|
req := httptest.NewRequest(http.MethodDelete, "/v1/policies/apps__data", nil)
|
||
|
|
res := httptest.NewRecorder()
|
||
|
|
srv.Handler().ServeHTTP(res, req)
|
||
|
|
|
||
|
|
if res.Code != http.StatusNotFound || !strings.Contains(res.Body.String(), "policy not found") {
|
||
|
|
t.Fatalf("expected not found error, got %d: %s", res.Code, res.Body.String())
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHandlePolicyByIDSurfacesPersistFailure(t *testing.T) {
|
||
|
|
srv := newPolicyHandlerServer(&policyTestKubeClient{
|
||
|
|
fakeKubeClient: &fakeKubeClient{},
|
||
|
|
saveErr: errors.New("delete save exploded"),
|
||
|
|
})
|
||
|
|
srv.policies["apps__data"] = api.BackupPolicy{
|
||
|
|
ID: "apps__data",
|
||
|
|
Namespace: "apps",
|
||
|
|
PVC: "data",
|
||
|
|
IntervalHours: 6,
|
||
|
|
Enabled: true,
|
||
|
|
Dedupe: true,
|
||
|
|
}
|
||
|
|
|
||
|
|
req := httptest.NewRequest(http.MethodDelete, "/v1/policies/apps__data", nil)
|
||
|
|
res := httptest.NewRecorder()
|
||
|
|
srv.Handler().ServeHTTP(res, req)
|
||
|
|
|
||
|
|
if res.Code != http.StatusBadGateway || !strings.Contains(res.Body.String(), "delete save exploded") {
|
||
|
|
t.Fatalf("expected persist failure surfaced as 502, got %d: %s", res.Code, res.Body.String())
|
||
|
|
}
|
||
|
|
if _, ok := srv.policies["apps__data"]; !ok {
|
||
|
|
t.Fatalf("expected failed delete to roll back in-memory policies")
|
||
|
|
}
|
||
|
|
}
|