tests/openapi_test.go
Rama master · réplica de lectura del repositorio original.
package main
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"os"
"regexp"
"sort"
"strings"
"testing"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/getkin/kin-openapi/routers/gorillamux"
"github.com/shellhub-io/shellhub/pkg/api/authorizer"
"github.com/shellhub-io/shellhub/tests/environment"
"github.com/stretchr/testify/require"
)
const (
bundledSpec = "../openapi/static/openapi-community.json"
strictModeMismatchMarker = "the response does not match the OpenAPI schema"
strictModeEnabledMarker = "Enabling OpenAPI response validation in strict mode"
validationRanMarker = "OpenAPI response validation passed"
sweepObserver = "sweep-observer"
sweepObserverEmail = "sweep-observer@shellhub.io"
sweepObserverPassword = "password"
sweepRevoker = "sweep-revoker"
sweepRevokerEmail = "sweep-revoker@shellhub.io"
sweepRevokerPassword = "password"
unresolvablePathParam = "e2e-sweep"
)
var (
sweepPathParams = regexp.MustCompile(`\{[^}]+\}`)
pathsTheValidatorSkips = map[string]bool{
"/healthcheck": true,
"/api/healthcheck": true,
"/info": true,
"/api/info": true,
"/install.sh": true,
"/kickstart.sh": true,
"/api/install": true,
"/ssh/connection": true,
"/agent/connection": true,
"/ssh/revdial": true,
"/ws/ssh": true,
}
prefixesTheValidatorSkips = []string{"/metrics", "/internal"}
sweepMethods = map[string]bool{
http.MethodGet: true,
http.MethodPost: true,
http.MethodPut: true,
http.MethodPatch: true,
http.MethodDelete: true,
}
operationsThatRevokeTheCaller = map[sweepOperation]bool{
{method: http.MethodDelete, path: "/api/users/tokens"}: true,
}
)
type sweepOperation struct {
method string
path string
}
func validatorSkips(path string) bool {
if pathsTheValidatorSkips[path] {
return true
}
for _, prefix := range prefixesTheValidatorSkips {
if strings.HasPrefix(path, prefix) {
return true
}
}
return false
}
func sweepOperations(t *testing.T) []sweepOperation {
t.Helper()
content, err := os.ReadFile(bundledSpec)
require.NoError(t, err, "reading %s: run the openapi bundle before the suite", bundledSpec)
doc := struct {
Paths map[string]map[string]json.RawMessage `json:"paths"`
}{}
require.NoError(t, json.Unmarshal(content, &doc))
require.NotEmpty(t, doc.Paths, "the bundled spec declares no paths")
operations := make([]sweepOperation, 0, len(doc.Paths))
for path, methods := range doc.Paths {
if validatorSkips(path) {
continue
}
for method := range methods {
if !sweepMethods[strings.ToUpper(method)] {
continue
}
operations = append(operations, sweepOperation{method: strings.ToUpper(method), path: path})
}
}
sort.Slice(operations, func(i, j int) bool {
if operations[i].path != operations[j].path {
return operations[i].path < operations[j].path
}
return operations[i].method < operations[j].method
})
return operations
}
func TestOpenAPIDescribesSweptResponses(t *testing.T) {
t.Parallel()
ctx := context.Background()
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile(bundledSpec)
require.NoError(t, err)
router, err := gorillamux.NewRouter(doc)
require.NoError(t, err)
compose := environment.New(t, run).Up(ctx)
t.Cleanup(compose.Down)
compose.NewUser(t, ShellHubUsername, ShellHubEmail, ShellHubPassword)
compose.NewNamespace(t, ShellHubUsername, ShellHubNamespaceName, ShellHubNamespace)
compose.NewUser(t, sweepObserver, sweepObserverEmail, sweepObserverPassword)
compose.NewMember(t, sweepObserver, ShellHubNamespaceName, string(authorizer.RoleObserver))
compose.NewUser(t, sweepRevoker, sweepRevokerEmail, sweepRevokerPassword)
compose.NewMember(t, sweepRevoker, ShellHubNamespaceName, string(authorizer.RoleObserver))
observer := compose.AuthUser(t, sweepObserver, sweepObserverPassword)
revoker := compose.AuthUser(t, sweepRevoker, sweepRevokerPassword)
require.NotEmpty(t, observer.Token)
require.NotEmpty(t, revoker.Token)
compose.AwaitServerLog(t, strictModeEnabledMarker)
compose.AwaitServerLog(t, validationRanMarker)
callers := []struct {
name string
token string
}{
{name: "anonymous", token: ""},
{name: "observer", token: observer.Token},
}
for _, operation := range sweepOperations(t) {
path := sweepPathParams.ReplaceAllString(operation.path, unresolvablePathParam)
for _, caller := range callers {
t.Run(caller.name+" "+operation.method+" "+operation.path, func(t *testing.T) {
token := caller.token
if token != "" && operationsThatRevokeTheCaller[operation] {
token = revoker.Token
}
req := compose.Anonymous(ctx)
if token != "" {
req = req.SetAuthToken(token)
}
resp, err := req.Execute(operation.method, path)
require.NoError(t, err)
require.NotContains(t, string(resp.Body()), strictModeMismatchMarker,
"%s %s answered a shape the spec does not describe", operation.method, operation.path)
request := resp.Request.RawRequest
route, params, err := router.FindRoute(request)
require.NoError(t, err)
err = openapi3filter.ValidateResponse(ctx, &openapi3filter.ResponseValidationInput{
RequestValidationInput: &openapi3filter.RequestValidationInput{
Request: request, PathParams: params, Route: route,
},
Status: resp.StatusCode(), Header: resp.Header(),
Body: io.NopCloser(bytes.NewReader(resp.Body())),
Options: &openapi3filter.Options{IncludeResponseStatus: true},
})
require.NoError(t, err, "%s %s returned %d: %s", operation.method, operation.path, resp.StatusCode(), resp.Body())
})
}
}
t.Run("the observer token still authenticates", func(t *testing.T) {
resp, err := compose.Anonymous(ctx).SetAuthToken(observer.Token).Get("/api/auth/user")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode(),
"a swept operation changed the observer, so every observer pass before this ran unauthenticated")
})
}