Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate response schema for integration tests #19043

Merged
merged 17 commits into from
Feb 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/19043.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
openapi: added ability to validate response structures against openapi schema for test clusters
```
41 changes: 41 additions & 0 deletions sdk/helper/testhelpers/schema/response_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"

"github.com/hashicorp/vault/sdk/framework"
Expand Down Expand Up @@ -126,3 +127,43 @@ func GetResponseSchema(t *testing.T, path *framework.Path, operation logical.Ope

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) {
dhuckins marked this conversation as resolved.
Show resolved Hide resolved
type PathRouter interface {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting workaround here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sigh yeah... couldn't think of a better way to do it 🤷

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this implementation. It reminds of something similar I saw in the implementation of Go's context. 👍

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(
lursu marked this conversation as resolved.
Show resolved Hide resolved
t,
GetResponseSchema(t, route, req.Operation),
resp,
true,
)
}
}
4 changes: 4 additions & 0 deletions vault/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,10 @@ type Core struct {
// contains absolute paths that we intend to forward (and template) when
// we're on a secondary cluster.
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.
Expand Down
4 changes: 4 additions & 0 deletions vault/request_handling.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,10 @@ func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request
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
// data.
if resp != nil && resp.Data != nil {
Expand Down
7 changes: 7 additions & 0 deletions vault/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,9 @@ type TestClusterOptions struct {
NoDefaultQuotas bool

Plugins *TestPluginConfig

// if populated, the callback is called for every request
RequestResponseCallback func(logical.Backend, *logical.Request, *logical.Response)
}

type TestPluginConfig struct {
Expand Down Expand Up @@ -1936,6 +1939,10 @@ func (testCluster *TestCluster) newCore(t testing.T, idx int, coreConfig *CoreCo
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
// created
if localConfig.Seal != nil {
Expand Down