The Prometheus monitoring system and time series database.
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.
 
 
 
 
 
prometheus/web/api/testhelpers/openapi.go

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
}