-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add response schema validation methods & test helpers (#18635)
This pull request adds 3 functions (and corresponding tests): `testhelpers/response_validation.go`: - `ValidateResponse` - `ValidateResponseData` field_data.go: - `ValidateStrict` (has the "strict" validation logic) The functions are primarily meant to be used in tests to ensure that the responses are consistent with the defined response schema. An example of how the functions can be used in tests can be found in #18636. ### Background This PR is part of the ongoing work to add structured responses in Vault OpenAPI (VLT-234)
- v1.19.0-rc1
- v1.18.5
- v1.18.4
- v1.18.3
- v1.18.2
- v1.18.1
- v1.18.0
- v1.18.0-rc1
- v1.17.6
- v1.17.5
- v1.17.4
- v1.17.3
- v1.17.2
- v1.17.1
- v1.17.0
- v1.17.0-rc1
- v1.16.3
- v1.16.2
- v1.16.1
- v1.16.0
- v1.16.0-rc3
- v1.16.0-rc2
- v1.16.0-rc1
- v1.15.8+ent
- v1.15.7+ent
- v1.15.6
- v1.15.5
- v1.15.4
- v1.15.3
- v1.15.2
- v1.15.1
- v1.15.0
- v1.15.0-rc1
- v1.14.12+ent
- v1.14.11+ent
- v1.14.10
- v1.14.9
- v1.14.8
- v1.14.7
- v1.14.6
- v1.14.5
- v1.14.4
- v1.14.3
- v1.14.2
- v1.14.1
- v1.14.0
- v1.14.0-rc1
- v1.13.13
- v1.13.12
- v1.13.11
- v1.13.10
- v1.13.9
- v1.13.8
- v1.13.7
- v1.13.6
- v1.13.5
- v1.13.4
- v1.13.3
- v1.13.2
- v1.13.1
- v1.13.0
- v1.13.0-rc1
- sdk/v0.15.2
- sdk/v0.15.1
- sdk/v0.15.0
- sdk/v0.14.1
- sdk/v0.14.0
- sdk/v0.13.0
- sdk/v0.12.0
- sdk/v0.11.1
- sdk/v0.11.0
- sdk/v0.10.2
- sdk/v0.10.1
- sdk/v0.10.0
- sdk/v0.9.2
- sdk/v0.9.1
- sdk/v0.9.0
- sdk/v0.8.1
- sdk/v0.8.0
- sdk/v0.7.0
- ent-changelog-1.15.9
- ent-changelog-1.15.8
- ent-changelog-1.15.7
- ent-changelog-1.14.13
- ent-changelog-1.14.12
- ent-changelog-1.14.11
- api/v1.16.0
- api/v1.15.0
- api/v1.14.0
- api/v1.13.0
- api/v1.12.2
- api/v1.12.1
- api/v1.12.0
- api/v1.11.0
- api/v1.10.0
- api/v1.9.2
- api/v1.9.1
- api/v1.9.0
- api/v1.8.3
- api/auth/userpass/v0.9.0
- api/auth/userpass/v0.8.0
- api/auth/userpass/v0.7.0
- api/auth/userpass/v0.6.0
- api/auth/userpass/v0.5.0
- api/auth/userpass/v0.4.1
- api/auth/userpass/v0.4.0
- api/auth/ldap/v0.9.0
- api/auth/ldap/v0.8.0
- api/auth/ldap/v0.7.0
- api/auth/ldap/v0.6.0
- api/auth/ldap/v0.5.0
- api/auth/ldap/v0.4.1
- api/auth/ldap/v0.4.0
- api/auth/kubernetes/v0.9.0
- api/auth/kubernetes/v0.8.0
- api/auth/kubernetes/v0.7.0
- api/auth/kubernetes/v0.6.0
- api/auth/kubernetes/v0.5.0
- api/auth/kubernetes/v0.4.1
- api/auth/kubernetes/v0.4.0
- api/auth/gcp/v0.9.0
- api/auth/gcp/v0.8.0
- api/auth/gcp/v0.7.0
- api/auth/gcp/v0.6.0
- api/auth/gcp/v0.5.0
- api/auth/gcp/v0.4.1
- api/auth/gcp/v0.4.0
- api/auth/azure/v0.8.0
- api/auth/azure/v0.7.0
- api/auth/azure/v0.6.0
- api/auth/azure/v0.5.0
- api/auth/azure/v0.4.1
- api/auth/azure/v0.4.0
- api/auth/aws/v0.9.0
- api/auth/aws/v0.8.0
- api/auth/aws/v0.7.0
- api/auth/aws/v0.6.0
- api/auth/aws/v0.5.0
- api/auth/aws/v0.4.1
- api/auth/aws/v0.4.0
- api/auth/approle/v0.9.0
- api/auth/approle/v0.8.0
- api/auth/approle/v0.7.0
- api/auth/approle/v0.6.0
- api/auth/approle/v0.5.0
- api/auth/approle/v0.4.1
- api/auth/approle/v0.4.0
Showing
5 changed files
with
499 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:improvement | ||
sdk: Add response schema validation method framework/FieldData.ValidateStrict and two test helpers (ValidateResponse, ValidateResponseData) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package schema | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/hashicorp/vault/sdk/framework" | ||
"github.com/hashicorp/vault/sdk/logical" | ||
) | ||
|
||
// ValidateResponseData is a test helper that validates whether the given | ||
// response data map conforms to the response schema (schema.Fields). It cycles | ||
// through the data map and validates conversions in the schema. In "strict" | ||
// mode, this function will also ensure that the data map has all schema's | ||
// requred fields and does not have any fields outside of the schema. | ||
func ValidateResponse(t *testing.T, schema *framework.Response, response *logical.Response, strict bool) { | ||
t.Helper() | ||
|
||
if response != nil { | ||
ValidateResponseData(t, schema, response.Data, strict) | ||
} else { | ||
ValidateResponseData(t, schema, nil, strict) | ||
} | ||
} | ||
|
||
// ValidateResponse is a test helper that validates whether the given response | ||
// object conforms to the response schema (schema.Fields). It cycles through | ||
// the data map and validates conversions in the schema. In "strict" mode, this | ||
// function will also ensure that the data map has all schema-required fields | ||
// and does not have any fields outside of the schema. | ||
func ValidateResponseData(t *testing.T, schema *framework.Response, data map[string]interface{}, strict bool) { | ||
t.Helper() | ||
|
||
if err := validateResponseDataImpl( | ||
schema, | ||
data, | ||
strict, | ||
); err != nil { | ||
t.Fatalf("validation error: %v; response data: %#v", err, data) | ||
} | ||
} | ||
|
||
// validateResponseDataImpl is extracted so that it can be tested | ||
func validateResponseDataImpl(schema *framework.Response, data map[string]interface{}, strict bool) error { | ||
// nothing to validate | ||
if schema == nil { | ||
return nil | ||
} | ||
|
||
// Marshal the data to JSON and back to convert the map's values into | ||
// JSON strings expected by Validate() and ValidateStrict(). This is | ||
// not efficient and is done for testing purposes only. | ||
jsonBytes, err := json.Marshal(data) | ||
if err != nil { | ||
return fmt.Errorf("failed to convert input to json: %w", err) | ||
} | ||
|
||
var dataWithStringValues map[string]interface{} | ||
if err := json.Unmarshal( | ||
jsonBytes, | ||
&dataWithStringValues, | ||
); err != nil { | ||
return fmt.Errorf("failed to unmashal data: %w", err) | ||
} | ||
|
||
// Validate | ||
fd := framework.FieldData{ | ||
Raw: dataWithStringValues, | ||
Schema: schema.Fields, | ||
} | ||
|
||
if strict { | ||
return fd.ValidateStrict() | ||
} | ||
|
||
return fd.Validate() | ||
} |
272 changes: 272 additions & 0 deletions
272
sdk/helper/testhelpers/schema/response_validation_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,272 @@ | ||
package schema | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/hashicorp/vault/sdk/framework" | ||
) | ||
|
||
func TestValidateResponse(t *testing.T) { | ||
cases := map[string]struct { | ||
schema *framework.Response | ||
response map[string]interface{} | ||
strict bool | ||
errorExpected bool | ||
}{ | ||
"nil schema, nil response, strict": { | ||
schema: nil, | ||
response: nil, | ||
strict: true, | ||
errorExpected: false, | ||
}, | ||
|
||
"nil schema, nil response, not strict": { | ||
schema: nil, | ||
response: nil, | ||
strict: false, | ||
errorExpected: false, | ||
}, | ||
|
||
"nil schema, good response, strict": { | ||
schema: nil, | ||
response: map[string]interface{}{ | ||
"foo": "bar", | ||
}, | ||
strict: true, | ||
errorExpected: false, | ||
}, | ||
|
||
"nil schema, good response, not strict": { | ||
schema: nil, | ||
response: map[string]interface{}{ | ||
"foo": "bar", | ||
}, | ||
strict: true, | ||
errorExpected: false, | ||
}, | ||
|
||
"nil schema fields, good response, strict": { | ||
schema: &framework.Response{}, | ||
response: map[string]interface{}{ | ||
"foo": "bar", | ||
}, | ||
strict: true, | ||
errorExpected: false, | ||
}, | ||
|
||
"nil schema fields, good response, not strict": { | ||
schema: &framework.Response{}, | ||
response: map[string]interface{}{ | ||
"foo": "bar", | ||
}, | ||
strict: true, | ||
errorExpected: false, | ||
}, | ||
|
||
"string schema field, string response, strict": { | ||
schema: &framework.Response{ | ||
Fields: map[string]*framework.FieldSchema{ | ||
"foo": { | ||
Type: framework.TypeString, | ||
}, | ||
}, | ||
}, | ||
response: map[string]interface{}{ | ||
"foo": "bar", | ||
}, | ||
strict: true, | ||
errorExpected: false, | ||
}, | ||
|
||
"string schema field, string response, not strict": { | ||
schema: &framework.Response{ | ||
Fields: map[string]*framework.FieldSchema{ | ||
"foo": { | ||
Type: framework.TypeString, | ||
}, | ||
}, | ||
}, | ||
response: map[string]interface{}{ | ||
"foo": "bar", | ||
}, | ||
strict: false, | ||
errorExpected: false, | ||
}, | ||
|
||
"string schema not required field, empty response, strict": { | ||
schema: &framework.Response{ | ||
Fields: map[string]*framework.FieldSchema{ | ||
"foo": { | ||
Type: framework.TypeString, | ||
Required: false, | ||
}, | ||
}, | ||
}, | ||
response: map[string]interface{}{}, | ||
strict: true, | ||
errorExpected: false, | ||
}, | ||
|
||
"string schema required field, empty response, strict": { | ||
schema: &framework.Response{ | ||
Fields: map[string]*framework.FieldSchema{ | ||
"foo": { | ||
Type: framework.TypeString, | ||
Required: true, | ||
}, | ||
}, | ||
}, | ||
response: map[string]interface{}{}, | ||
strict: true, | ||
errorExpected: true, | ||
}, | ||
|
||
"string schema required field, empty response, not strict": { | ||
schema: &framework.Response{ | ||
Fields: map[string]*framework.FieldSchema{ | ||
"foo": { | ||
Type: framework.TypeString, | ||
Required: true, | ||
}, | ||
}, | ||
}, | ||
response: map[string]interface{}{}, | ||
strict: false, | ||
errorExpected: false, | ||
}, | ||
|
||
"string schema required field, nil response, strict": { | ||
schema: &framework.Response{ | ||
Fields: map[string]*framework.FieldSchema{ | ||
"foo": { | ||
Type: framework.TypeString, | ||
Required: true, | ||
}, | ||
}, | ||
}, | ||
response: nil, | ||
strict: true, | ||
errorExpected: true, | ||
}, | ||
|
||
"string schema required field, nil response, not strict": { | ||
schema: &framework.Response{ | ||
Fields: map[string]*framework.FieldSchema{ | ||
"foo": { | ||
Type: framework.TypeString, | ||
Required: true, | ||
}, | ||
}, | ||
}, | ||
response: nil, | ||
strict: false, | ||
errorExpected: false, | ||
}, | ||
|
||
"empty schema, string response, strict": { | ||
schema: &framework.Response{ | ||
Fields: map[string]*framework.FieldSchema{}, | ||
}, | ||
response: map[string]interface{}{ | ||
"foo": "bar", | ||
}, | ||
strict: true, | ||
errorExpected: true, | ||
}, | ||
|
||
"empty schema, string response, not strict": { | ||
schema: &framework.Response{ | ||
Fields: map[string]*framework.FieldSchema{}, | ||
}, | ||
response: map[string]interface{}{ | ||
"foo": "bar", | ||
}, | ||
strict: false, | ||
errorExpected: false, | ||
}, | ||
|
||
"time schema, string response, strict": { | ||
schema: &framework.Response{ | ||
Fields: map[string]*framework.FieldSchema{ | ||
"time": { | ||
Type: framework.TypeTime, | ||
Required: true, | ||
}, | ||
}, | ||
}, | ||
response: map[string]interface{}{ | ||
"time": "2024-12-11T09:08:07Z", | ||
}, | ||
strict: true, | ||
errorExpected: false, | ||
}, | ||
|
||
"time schema, string response, not strict": { | ||
schema: &framework.Response{ | ||
Fields: map[string]*framework.FieldSchema{ | ||
"time": { | ||
Type: framework.TypeTime, | ||
Required: true, | ||
}, | ||
}, | ||
}, | ||
response: map[string]interface{}{ | ||
"time": "2024-12-11T09:08:07Z", | ||
}, | ||
strict: false, | ||
errorExpected: false, | ||
}, | ||
|
||
"time schema, time response, strict": { | ||
schema: &framework.Response{ | ||
Fields: map[string]*framework.FieldSchema{ | ||
"time": { | ||
Type: framework.TypeTime, | ||
Required: true, | ||
}, | ||
}, | ||
}, | ||
response: map[string]interface{}{ | ||
"time": time.Date(2024, 12, 11, 9, 8, 7, 0, time.UTC), | ||
}, | ||
strict: true, | ||
errorExpected: false, | ||
}, | ||
|
||
"time schema, time response, not strict": { | ||
schema: &framework.Response{ | ||
Fields: map[string]*framework.FieldSchema{ | ||
"time": { | ||
Type: framework.TypeTime, | ||
Required: true, | ||
}, | ||
}, | ||
}, | ||
response: map[string]interface{}{ | ||
"time": time.Date(2024, 12, 11, 9, 8, 7, 0, time.UTC), | ||
}, | ||
strict: false, | ||
errorExpected: false, | ||
}, | ||
} | ||
|
||
for name, tc := range cases { | ||
name, tc := name, tc | ||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
err := validateResponseDataImpl( | ||
tc.schema, | ||
tc.response, | ||
tc.strict, | ||
) | ||
if err == nil && tc.errorExpected == true { | ||
t.Fatalf("expected an error, got nil") | ||
} | ||
if err != nil && tc.errorExpected == false { | ||
t.Fatalf("unexpected error: %v", err) | ||
} | ||
}) | ||
} | ||
} |