You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
204 lines
6.6 KiB
204 lines
6.6 KiB
// Copyright The Prometheus Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// This file provides OpenAPI-specific test utilities for validating spec compliance.
|
|
package testhelpers
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/pb33f/libopenapi"
|
|
validator "github.com/pb33f/libopenapi-validator"
|
|
valerrors "github.com/pb33f/libopenapi-validator/errors"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var (
|
|
openAPIValidator31 validator.Validator
|
|
openAPIValidator32 validator.Validator
|
|
openAPIValidatorOnce sync.Once
|
|
openAPIValidatorErr error
|
|
)
|
|
|
|
// loadOpenAPIValidators loads and caches both OpenAPI 3.1 and 3.2 validators from golden files.
|
|
func loadOpenAPIValidators() (v31, v32 validator.Validator, err error) {
|
|
openAPIValidatorOnce.Do(func() {
|
|
// Load OpenAPI 3.1 validator.
|
|
goldenPath31 := filepath.Join("testdata", "openapi_3.1_golden.yaml")
|
|
specBytes31, err := os.ReadFile(goldenPath31)
|
|
if err != nil {
|
|
openAPIValidatorErr = fmt.Errorf("failed to read OpenAPI 3.1 spec from %s: %w", goldenPath31, err)
|
|
return
|
|
}
|
|
|
|
doc31, err := libopenapi.NewDocument(specBytes31)
|
|
if err != nil {
|
|
openAPIValidatorErr = fmt.Errorf("failed to parse OpenAPI 3.1 document: %w", err)
|
|
return
|
|
}
|
|
|
|
v31, errs := validator.NewValidator(doc31)
|
|
if len(errs) > 0 {
|
|
openAPIValidatorErr = fmt.Errorf("failed to create OpenAPI 3.1 validator: %v", errs)
|
|
return
|
|
}
|
|
|
|
openAPIValidator31 = v31
|
|
|
|
// Load OpenAPI 3.2 validator.
|
|
goldenPath32 := filepath.Join("testdata", "openapi_3.2_golden.yaml")
|
|
specBytes32, err := os.ReadFile(goldenPath32)
|
|
if err != nil {
|
|
openAPIValidatorErr = fmt.Errorf("failed to read OpenAPI 3.2 spec from %s: %w", goldenPath32, err)
|
|
return
|
|
}
|
|
|
|
doc32, err := libopenapi.NewDocument(specBytes32)
|
|
if err != nil {
|
|
openAPIValidatorErr = fmt.Errorf("failed to parse OpenAPI 3.2 document: %w", err)
|
|
return
|
|
}
|
|
|
|
v32, errs := validator.NewValidator(doc32)
|
|
if len(errs) > 0 {
|
|
openAPIValidatorErr = fmt.Errorf("failed to create OpenAPI 3.2 validator: %v", errs)
|
|
return
|
|
}
|
|
|
|
openAPIValidator32 = v32
|
|
})
|
|
|
|
if openAPIValidatorErr != nil {
|
|
return nil, nil, openAPIValidatorErr
|
|
}
|
|
|
|
return openAPIValidator31, openAPIValidator32, nil
|
|
}
|
|
|
|
// ValidateOpenAPI validates the request and response against both OpenAPI 3.1 and 3.2 specifications.
|
|
// This ensures API endpoints are compatible with both OpenAPI versions.
|
|
// Returns the response for chaining.
|
|
func (r *Response) ValidateOpenAPI() *Response {
|
|
r.t.Helper()
|
|
|
|
// Load both validators (cached after first call).
|
|
v31, v32, err := loadOpenAPIValidators()
|
|
require.NoError(r.t, err, "failed to load OpenAPI validators")
|
|
|
|
// Validate against OpenAPI 3.1 spec.
|
|
if r.request != nil {
|
|
r.validateRequestWithVersion(v31, "3.1")
|
|
}
|
|
r.validateResponseWithVersion(v31, "3.1")
|
|
|
|
// Validate against OpenAPI 3.2 spec.
|
|
if r.request != nil {
|
|
r.validateRequestWithVersion(v32, "3.2")
|
|
}
|
|
r.validateResponseWithVersion(v32, "3.2")
|
|
|
|
return r
|
|
}
|
|
|
|
// validateRequestWithVersion validates the HTTP request against a specific OpenAPI version's spec.
|
|
func (r *Response) validateRequestWithVersion(v validator.Validator, version string) {
|
|
r.t.Helper()
|
|
|
|
// Create a validation request from the original request.
|
|
validationReq := &http.Request{
|
|
Method: r.request.Method,
|
|
URL: r.request.URL,
|
|
Header: r.request.Header,
|
|
Body: io.NopCloser(bytes.NewReader(r.requestBody)),
|
|
}
|
|
|
|
// Validate the request.
|
|
valid, errors := v.ValidateHttpRequest(validationReq)
|
|
if !valid {
|
|
// Check if the error is because the path doesn't exist in this version.
|
|
// Some endpoints (like /notifications/live) only exist in 3.2, not 3.1.
|
|
if isPathNotFoundError(errors) && version == "3.1" && strings.Contains(r.request.URL.Path, "/notifications/live") {
|
|
// Expected: /notifications/live is only in OpenAPI 3.2.
|
|
return
|
|
}
|
|
|
|
var errorMessages []string
|
|
for _, e := range errors {
|
|
errorMessages = append(errorMessages, e.Error())
|
|
}
|
|
require.Fail(r.t, fmt.Sprintf("OpenAPI %s request validation failed", version),
|
|
"Request to %s %s failed OpenAPI %s validation:\n%v",
|
|
r.request.Method, r.request.URL.Path, version, errorMessages)
|
|
}
|
|
}
|
|
|
|
// validateResponseWithVersion validates the HTTP response against a specific OpenAPI version's spec.
|
|
func (r *Response) validateResponseWithVersion(v validator.Validator, version string) {
|
|
r.t.Helper()
|
|
|
|
// Create a validation request (needed for response validation context).
|
|
validationReq := &http.Request{
|
|
Method: r.request.Method,
|
|
URL: r.request.URL,
|
|
Header: r.request.Header,
|
|
}
|
|
|
|
// Create a response for validation.
|
|
validationResp := &http.Response{
|
|
StatusCode: r.StatusCode,
|
|
Header: r.responseHeader,
|
|
Body: io.NopCloser(bytes.NewReader([]byte(r.Body))),
|
|
Request: validationReq,
|
|
}
|
|
|
|
// Validate the response.
|
|
valid, errors := v.ValidateHttpResponse(validationReq, validationResp)
|
|
if !valid {
|
|
// Check if the error is because the path doesn't exist in this version.
|
|
// Some endpoints (like /notifications/live) only exist in 3.2, not 3.1.
|
|
if isPathNotFoundError(errors) && version == "3.1" && strings.Contains(r.request.URL.Path, "/notifications/live") {
|
|
// Expected: /notifications/live is only in OpenAPI 3.2.
|
|
return
|
|
}
|
|
|
|
var errorMessages []string
|
|
for _, e := range errors {
|
|
errorMessages = append(errorMessages, e.Error())
|
|
}
|
|
require.Fail(r.t, fmt.Sprintf("OpenAPI %s response validation failed", version),
|
|
"Response from %s %s (status %d) failed OpenAPI %s validation:\n%v",
|
|
r.request.Method, r.request.URL.Path, r.StatusCode, version, errorMessages)
|
|
}
|
|
}
|
|
|
|
// isPathNotFoundError checks if the validation errors indicate a path was not found in the spec.
|
|
func isPathNotFoundError(errors []*valerrors.ValidationError) bool {
|
|
for _, err := range errors {
|
|
errStr := err.Error()
|
|
// Check for common "path not found" error messages from libopenapi-validator.
|
|
if strings.Contains(errStr, "path") && (strings.Contains(errStr, "not found") || strings.Contains(errStr, "does not exist")) {
|
|
return true
|
|
}
|
|
if strings.Contains(errStr, "GET /notifications/live") || strings.Contains(errStr, "/notifications/live not found") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|