Validate response schema for integration tests (#19043)

* add RequestResponseCallback to core/options

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* pass in router and apply function on requests

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* add callback

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* cleanup

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Anton Averchenkov <84287187+averche@users.noreply.github.com>

* Update vault/core.go

* bad typo...

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* use pvt interface, can't downcast to child struct

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* finer grained errors

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* trim path for backend

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* remove entire mount point instead of just the first part of url

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* Update vault/testing.go

Co-authored-by: Anton Averchenkov <84287187+averche@users.noreply.github.com>

* add doc string

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* update docstring

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* reformat

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* added changelog

---------

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>
Co-authored-by: Anton Averchenkov <84287187+averche@users.noreply.github.com>
This commit is contained in:
Daniel Huckins 2023-02-15 14:57:57 -05:00 committed by GitHub
parent 020a3903ff
commit 7fde5ecb83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 59 additions and 0 deletions

3
changelog/19043.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:improvement
openapi: added ability to validate response structures against openapi schema for test clusters
```

View file

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"testing" "testing"
"github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/framework"
@ -126,3 +127,43 @@ func GetResponseSchema(t *testing.T, path *framework.Path, operation logical.Ope
return &schemaResponses[0] return &schemaResponses[0]
} }
// ResponseValidatingCallback can be used in setting up a [vault.TestCluster] that validates every response against the
// openapi specifications
//
// [vault.TestCluster]: https://pkg.go.dev/github.com/hashicorp/vault/vault#TestCluster
func ResponseValidatingCallback(t *testing.T) func(logical.Backend, *logical.Request, *logical.Response) {
type PathRouter interface {
Route(string) *framework.Path
}
return func(b logical.Backend, req *logical.Request, resp *logical.Response) {
t.Helper()
if b == nil {
t.Fatalf("non-nil backend required")
}
backend, ok := b.(PathRouter)
if !ok {
t.Fatalf("could not cast %T to have `Route(string) *framework.Path`", b)
}
// the full request path includes the backend
// but when passing to the backend, we have to trim the mount point
// `sys/mounts/secret` -> `mounts/secret`
// `auth/token/create` -> `create`
requestPath := strings.TrimPrefix(req.Path, req.MountPoint)
route := backend.Route(requestPath)
if route == nil {
t.Fatalf("backend %T could not find a route for %s", b, req.Path)
}
ValidateResponse(
t,
GetResponseSchema(t, route, req.Operation),
resp,
true,
)
}
}

View file

@ -688,6 +688,10 @@ type Core struct {
// contains absolute paths that we intend to forward (and template) when // contains absolute paths that we intend to forward (and template) when
// we're on a secondary cluster. // we're on a secondary cluster.
writeForwardedPaths *pathmanager.PathManager writeForwardedPaths *pathmanager.PathManager
// if populated, the callback is called for every request
// for testing purposes
requestResponseCallback func(logical.Backend, *logical.Request, *logical.Response)
} }
// c.stateLock needs to be held in read mode before calling this function. // c.stateLock needs to be held in read mode before calling this function.

View file

@ -675,6 +675,10 @@ func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request
resp, auth, err = c.handleRequest(ctx, req) resp, auth, err = c.handleRequest(ctx, req)
} }
if err == nil && c.requestResponseCallback != nil {
c.requestResponseCallback(c.router.MatchingBackend(ctx, req.Path), req, resp)
}
// If we saved the token in the request, we should return it in the response // If we saved the token in the request, we should return it in the response
// data. // data.
if resp != nil && resp.Data != nil { if resp != nil && resp.Data != nil {

View file

@ -1186,6 +1186,9 @@ type TestClusterOptions struct {
NoDefaultQuotas bool NoDefaultQuotas bool
Plugins *TestPluginConfig Plugins *TestPluginConfig
// if populated, the callback is called for every request
RequestResponseCallback func(logical.Backend, *logical.Request, *logical.Response)
} }
type TestPluginConfig struct { type TestPluginConfig struct {
@ -1936,6 +1939,10 @@ func (testCluster *TestCluster) newCore(t testing.T, idx int, coreConfig *CoreCo
handler = opts.HandlerFunc.Handler(&props) handler = opts.HandlerFunc.Handler(&props)
} }
if opts != nil && opts.RequestResponseCallback != nil {
c.requestResponseCallback = opts.RequestResponseCallback
}
// Set this in case the Seal was manually set before the core was // Set this in case the Seal was manually set before the core was
// created // created
if localConfig.Seal != nil { if localConfig.Seal != nil {