Ctrl K
ComunidadGuías…

tests/openapi_test.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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")
	})
}