github.com/hashicorp/vault/sdk@v0.11.0/helper/testhelpers/schema/response_validation.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package schema 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "net/http" 10 "strings" 11 "testing" 12 13 "github.com/hashicorp/vault/sdk/framework" 14 "github.com/hashicorp/vault/sdk/logical" 15 ) 16 17 // ValidateResponse is a test helper that validates whether the given response 18 // object conforms to the response schema (schema.Fields). It cycles through 19 // the data map and validates conversions in the schema. In "strict" mode, this 20 // function will also ensure that the data map has all schema-required fields 21 // and does not have any fields outside of the schema. 22 func ValidateResponse(t *testing.T, schema *framework.Response, response *logical.Response, strict bool) { 23 t.Helper() 24 25 if response != nil { 26 ValidateResponseData(t, schema, response.Data, strict) 27 } else { 28 ValidateResponseData(t, schema, nil, strict) 29 } 30 } 31 32 // ValidateResponseData is a test helper that validates whether the given 33 // response data map conforms to the response schema (schema.Fields). It cycles 34 // through the data map and validates conversions in the schema. In "strict" 35 // mode, this function will also ensure that the data map has all schema's 36 // requred fields and does not have any fields outside of the schema. 37 func ValidateResponseData(t *testing.T, schema *framework.Response, data map[string]interface{}, strict bool) { 38 t.Helper() 39 40 if err := validateResponseDataImpl( 41 schema, 42 data, 43 strict, 44 ); err != nil { 45 t.Fatalf("validation error: %v; response data: %#v", err, data) 46 } 47 } 48 49 // validateResponseDataImpl is extracted so that it can be tested 50 func validateResponseDataImpl(schema *framework.Response, data map[string]interface{}, strict bool) error { 51 // nothing to validate 52 if schema == nil { 53 return nil 54 } 55 56 // Certain responses may come through with non-2xx status codes. While 57 // these are not always errors (e.g. 3xx redirection codes), we don't 58 // consider them for the purposes of schema validation 59 if status, exists := data[logical.HTTPStatusCode]; exists { 60 s, ok := status.(int) 61 if ok && (s < 200 || s > 299) { 62 return nil 63 } 64 } 65 66 // Marshal the data to JSON and back to convert the map's values into 67 // JSON strings expected by Validate() and ValidateStrict(). This is 68 // not efficient and is done for testing purposes only. 69 jsonBytes, err := json.Marshal(data) 70 if err != nil { 71 return fmt.Errorf("failed to convert input to json: %w", err) 72 } 73 74 var dataWithStringValues map[string]interface{} 75 if err := json.Unmarshal( 76 jsonBytes, 77 &dataWithStringValues, 78 ); err != nil { 79 return fmt.Errorf("failed to unmashal data: %w", err) 80 } 81 82 // these are special fields that will not show up in the final response and 83 // should be ignored 84 for _, field := range []string{ 85 logical.HTTPContentType, 86 logical.HTTPRawBody, 87 logical.HTTPStatusCode, 88 logical.HTTPRawBodyAlreadyJSONDecoded, 89 logical.HTTPCacheControlHeader, 90 logical.HTTPPragmaHeader, 91 logical.HTTPWWWAuthenticateHeader, 92 } { 93 delete(dataWithStringValues, field) 94 95 if _, ok := schema.Fields[field]; ok { 96 return fmt.Errorf("encountered a reserved field in response schema: %s", field) 97 } 98 } 99 100 // Validate 101 fd := framework.FieldData{ 102 Raw: dataWithStringValues, 103 Schema: schema.Fields, 104 } 105 106 if strict { 107 return fd.ValidateStrict() 108 } 109 110 return fd.Validate() 111 } 112 113 // FindResponseSchema is a test helper to extract response schema from the 114 // given framework path / operation. 115 func FindResponseSchema(t *testing.T, paths []*framework.Path, pathIdx int, operation logical.Operation) *framework.Response { 116 t.Helper() 117 118 if pathIdx >= len(paths) { 119 t.Fatalf("path index %d is out of range", pathIdx) 120 } 121 122 schemaPath := paths[pathIdx] 123 124 return GetResponseSchema(t, schemaPath, operation) 125 } 126 127 func GetResponseSchema(t *testing.T, path *framework.Path, operation logical.Operation) *framework.Response { 128 t.Helper() 129 130 schemaOperation, ok := path.Operations[operation] 131 if !ok { 132 t.Fatalf( 133 "could not find response schema: %s: %q operation does not exist", 134 path.Pattern, 135 operation, 136 ) 137 } 138 139 var schemaResponses []framework.Response 140 141 for _, status := range []int{ 142 http.StatusOK, // 200 143 http.StatusAccepted, // 202 144 http.StatusNoContent, // 204 145 } { 146 schemaResponses, ok = schemaOperation.Properties().Responses[status] 147 if ok { 148 break 149 } 150 } 151 152 if len(schemaResponses) == 0 { 153 // ListOperations have a default response schema that is implicit unless overridden 154 if operation == logical.ListOperation { 155 return &framework.Response{ 156 Description: "OK", 157 Fields: map[string]*framework.FieldSchema{ 158 "keys": { 159 Type: framework.TypeStringSlice, 160 }, 161 }, 162 } 163 } 164 165 t.Fatalf( 166 "could not find response schema: %s: %q operation: no responses found", 167 path.Pattern, 168 operation, 169 ) 170 } 171 172 return &schemaResponses[0] 173 } 174 175 // ResponseValidatingCallback can be used in setting up a [vault.TestCluster] 176 // that validates every response against the openapi specifications. 177 // 178 // [vault.TestCluster]: https://pkg.go.dev/github.com/hashicorp/vault/vault#TestCluster 179 func ResponseValidatingCallback(t *testing.T) func(logical.Backend, *logical.Request, *logical.Response) { 180 type PathRouter interface { 181 Route(string) *framework.Path 182 } 183 184 return func(b logical.Backend, req *logical.Request, resp *logical.Response) { 185 t.Helper() 186 187 if b == nil { 188 t.Fatalf("non-nil backend required") 189 } 190 191 backend, ok := b.(PathRouter) 192 if !ok { 193 t.Fatalf("could not cast %T to have `Route(string) *framework.Path`", b) 194 } 195 196 // The full request path includes the backend but when passing to the 197 // backend, we have to trim the mount point: 198 // `sys/mounts/secret` -> `mounts/secret` 199 // `auth/token/create` -> `create` 200 requestPath := strings.TrimPrefix(req.Path, req.MountPoint) 201 202 route := backend.Route(requestPath) 203 if route == nil { 204 t.Fatalf("backend %T could not find a route for %s", b, req.Path) 205 } 206 207 ValidateResponse( 208 t, 209 GetResponseSchema(t, route, req.Operation), 210 resp, 211 true, 212 ) 213 } 214 }