pax_global_header00006660000000000000000000000064152053404240014511gustar00rootroot0000000000000052 comment=a5d6833b6717a3acd491545ed03f987751f4d12c libopenapi-validator-0.13.8/000077500000000000000000000000001520534042400157075ustar00rootroot00000000000000libopenapi-validator-0.13.8/.github/000077500000000000000000000000001520534042400172475ustar00rootroot00000000000000libopenapi-validator-0.13.8/.github/dependabot.yml000066400000000000000000000001551520534042400221000ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "weekly" libopenapi-validator-0.13.8/.github/workflows/000077500000000000000000000000001520534042400213045ustar00rootroot00000000000000libopenapi-validator-0.13.8/.github/workflows/build.yaml000066400000000000000000000027721520534042400232770ustar00rootroot00000000000000name: Build on: push: branches: - main pull_request: branches: - main jobs: lint: runs-on: ubuntu-latest steps: - name: Checkout scm uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: version: v2.8 build: name: Build runs-on: ubuntu-latest steps: - name: Set up Go 1.x uses: actions/setup-go@v3 with: go-version: 1.25 id: go - name: Checkout code uses: actions/checkout@v3 - name: Get dependencies run: | go get -v -t -d ./... if [ -f Gopkg.toml ]; then curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh dep ensure fi - name: Test run: go test ./... - name: Coverage run: | go get github.com/axw/gocov/gocov go get github.com/AlekSi/gocov-xml go install github.com/axw/gocov/gocov go install github.com/AlekSi/gocov-xml - run: | go test -v -coverprofile cover.out ./... gocov convert cover.out | gocov-xml > coverage.xml - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml flags: unittests fail_ci_if_error: false verbose: true libopenapi-validator-0.13.8/.golangci.yml000066400000000000000000000007561520534042400203030ustar00rootroot00000000000000version: "2" linters: default: none enable: - asciicheck - bidichk - errcheck - govet - ineffassign - staticcheck - unused exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling paths: - third_party$ - builtin$ - examples$ formatters: enable: - gofumpt exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ libopenapi-validator-0.13.8/.pre-commit-config.yaml000066400000000000000000000006761520534042400222010ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: # Run golangci-lint as a pre-commit hook to catch issues before they are pushed # See https://golangci-lint.run/ for more information - repo: local hooks: - id: golangci-lint name: Lint Go code entry: go tool golangci-lint run language: system pass_filenames: false types: [go]libopenapi-validator-0.13.8/LICENSE.md000066400000000000000000000021301520534042400173070ustar00rootroot00000000000000MIT License Copyright (c) 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. libopenapi-validator-0.13.8/Makefile000066400000000000000000000005331520534042400173500ustar00rootroot00000000000000all: gofumpt import lint init: go install mvdan.cc/gofumpt@latest go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest go install github.com/daixiang0/gci@latest lint: golangci-lint run ./... gofumpt: gofumpt -l -w . import: gci write --skip-generated -s standard -s default -s localmodule -s blank -s dot -s alias . libopenapi-validator-0.13.8/README.md000066400000000000000000000103011520534042400171610ustar00rootroot00000000000000īģŋ

libopenapi

# Enterprise grade OpenAPI validation tools for golang. ![Pipeline](https://github.com/pb33f/libopenapi-validator/workflows/Build/badge.svg) [![codecov](https://codecov.io/gh/pb33f/libopenapi-validator/branch/main/graph/badge.svg?)](https://codecov.io/gh/pb33f/libopenapi-validator) [![discord](https://img.shields.io/discord/923258363540815912)](https://discord.gg/x7VACVuEGP) [![Docs](https://img.shields.io/badge/godoc-reference-5fafd7)](https://pkg.go.dev/github.com/pb33f/libopenapi-validator) A validation module for [libopenapi](https://github.com/pb33f/libopenapi). `libopenapi-validator` will validate the following elements against an OpenAPI 3+ specification - *http.Request* - Validates the request against the OpenAPI specification - *http.Response* - Validates the response against the OpenAPI specification - *libopenapi.Document* - Validates the OpenAPI document against the OpenAPI specification - *base.Schema* - Validates a schema against a JSON or YAML blob / unmarshalled object 👉👉 [Check out the full documentation](https://pb33f.io/libopenapi/validation/) 👈👈 --- ## Installation ```bash go get github.com/pb33f/libopenapi-validator ``` ## Validate OpenAPI Document ```bash go run github.com/pb33f/libopenapi-validator/cmd/validate@latest [--regexengine] [--yaml2json] ``` ## Install pre-commit hook To install the pre-commit hook, run the following command in your terminal: ```bash pre-commit install ``` ### Options #### --regexengine 🔍 Example: Use a custom regex engine/flag (e.g., ecmascript) ```bash go run github.com/pb33f/libopenapi-validator/cmd/validate@latest --regexengine=ecmascript ``` 🔧 Supported **--regexengine** flags/values (â„šī¸ Default: re2) - none - ignorecase - multiline - explicitcapture - compiled - singleline - ignorepatternwhitespace - righttoleft - debug - ecmascript - re2 - unicode #### --yaml2json 🔍 Convert YAML files to JSON before validation (â„šī¸ Default: false) [libopenapi](https://github.com/pb33f/libopenapi/blob/main/datamodel/spec_info.go#L115) passes `map[interface{}]interface{}` structures for deeply nested objects or complex mappings in the OpenAPI specification, which are not allowed in JSON. These structures cannot be properly converted to JSON by libopenapi and cannot be validated by jsonschema, resulting in ambiguous errors. This flag allows pre-converting from YAML to JSON to bypass this limitation of the libopenapi. **When does this happen?** - OpenAPI specs with deeply nested schema definitions - Complex `allOf`, `oneOf`, or `anyOf` structures with multiple levels - Specifications with intricate object mappings in examples or schema properties Enabling this flag pre-converts the YAML document from YAML to JSON, ensuring a clean JSON structure before validation. Example: ```bash go run github.com/pb33f/libopenapi-validator/cmd/validate@latest --yaml2json ``` ## Documentation - [The structure of the validator](https://pb33f.io/libopenapi/validation/#the-structure-of-the-validator) - [Validation errors](https://pb33f.io/libopenapi/validation/#validation-errors) - [Schema errors](https://pb33f.io/libopenapi/validation/#schema-errors) - [High-level validation](https://pb33f.io/libopenapi/validation/#high-level-validation) - [Validating http.Request](https://pb33f.io/libopenapi/validation/#validating-httprequest) - [Validating http.Request and http.Response](https://pb33f.io/libopenapi/validation/#validating-httprequest-and-httpresponse) - [Validating just http.Response](https://pb33f.io/libopenapi/validation/#validating-just-httpresponse) - [Validating HTTP Parameters](https://pb33f.io/libopenapi/validation/#validating-http-parameters) - [Validating an OpenAPI document](https://pb33f.io/libopenapi/validation/#validating-an-openapi-document) - [Validating Schemas](https://pb33f.io/libopenapi/validation/#validating-schemas) [libopenapi](https://github.com/pb33f/libopenapi) and [libopenapi-validator](https://github.com/pb33f/libopenapi-validator) are products of Princess Beef Heavy Industries, LLC libopenapi-validator-0.13.8/cache/000077500000000000000000000000001520534042400167525ustar00rootroot00000000000000libopenapi-validator-0.13.8/cache/cache.go000066400000000000000000000017171520534042400203520ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package cache import ( "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/santhosh-tekuri/jsonschema/v6" "go.yaml.in/yaml/v4" ) // SchemaCacheEntry holds a compiled schema and its intermediate representations. // This is stored in the cache to avoid re-rendering and re-compiling schemas on each request. type SchemaCacheEntry struct { Schema *base.Schema RenderedInline []byte ReferenceSchema string // String version of RenderedInline RenderedJSON []byte CompiledSchema *jsonschema.Schema RenderedNode *yaml.Node } // SchemaCache defines the interface for schema caching implementations. // The key is a uint64 hash of the schema (from schema.GoLow().Hash()). type SchemaCache interface { Load(key uint64) (*SchemaCacheEntry, bool) Store(key uint64, value *SchemaCacheEntry) Range(f func(key uint64, value *SchemaCacheEntry) bool) } libopenapi-validator-0.13.8/cache/cache_test.go000066400000000000000000000161261520534042400214110ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package cache import ( "testing" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/santhosh-tekuri/jsonschema/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewDefaultCache(t *testing.T) { cache := NewDefaultCache() assert.NotNil(t, cache) assert.NotNil(t, cache.m) } func TestDefaultCache_StoreAndLoad(t *testing.T) { cache := NewDefaultCache() // Create a test schema cache entry testSchema := &SchemaCacheEntry{ Schema: &base.Schema{}, RenderedInline: []byte("rendered"), RenderedJSON: []byte(`{"type":"object"}`), CompiledSchema: &jsonschema.Schema{}, } // Create a test key (uint64 hash) key := uint64(0x123456789abcdef0) // Store the schema cache.Store(key, testSchema) // Load the schema back loaded, ok := cache.Load(key) assert.True(t, ok, "Should find the cached schema") require.NotNil(t, loaded) assert.Equal(t, testSchema.RenderedInline, loaded.RenderedInline) assert.Equal(t, testSchema.RenderedJSON, loaded.RenderedJSON) assert.NotNil(t, loaded.CompiledSchema) } func TestDefaultCache_LoadMissing(t *testing.T) { cache := NewDefaultCache() // Try to load a key that doesn't exist key := uint64(0xdeadbeef) loaded, ok := cache.Load(key) assert.False(t, ok, "Should not find non-existent key") assert.Nil(t, loaded) } func TestDefaultCache_LoadNilCache(t *testing.T) { var cache *DefaultCache key := uint64(0) loaded, ok := cache.Load(key) assert.False(t, ok) assert.Nil(t, loaded) } func TestDefaultCache_StoreNilCache(t *testing.T) { var cache *DefaultCache // Should not panic key := uint64(0) cache.Store(key, &SchemaCacheEntry{}) // Verify nothing was stored (cache is nil) assert.Nil(t, cache) } func TestDefaultCache_Range(t *testing.T) { cache := NewDefaultCache() // Store multiple entries entries := make(map[uint64]*SchemaCacheEntry) for i := 0; i < 5; i++ { key := uint64(i) entry := &SchemaCacheEntry{ RenderedInline: []byte{byte(i)}, RenderedJSON: []byte{byte(i)}, } entries[key] = entry cache.Store(key, entry) } // Range over all entries count := 0 foundKeys := make(map[uint64]bool) cache.Range(func(key uint64, value *SchemaCacheEntry) bool { count++ foundKeys[key] = true // Verify the value matches what we stored expected, exists := entries[key] assert.True(t, exists, "Key should exist in original entries") assert.Equal(t, expected.RenderedInline, value.RenderedInline) return true }) assert.Equal(t, 5, count, "Should iterate over all 5 entries") assert.Equal(t, 5, len(foundKeys), "Should find all 5 unique keys") } func TestDefaultCache_RangeEarlyTermination(t *testing.T) { cache := NewDefaultCache() // Store multiple entries for i := 0; i < 10; i++ { key := uint64(i) cache.Store(key, &SchemaCacheEntry{}) } // Range but stop after 3 iterations count := 0 cache.Range(func(key uint64, value *SchemaCacheEntry) bool { count++ return count < 3 // Stop after 3 }) assert.Equal(t, 3, count, "Should stop after 3 iterations") } func TestDefaultCache_RangeNilCache(t *testing.T) { var cache *DefaultCache // Should not panic called := false cache.Range(func(key uint64, value *SchemaCacheEntry) bool { called = true return true }) assert.False(t, called, "Callback should not be called on nil cache") } func TestDefaultCache_RangeEmpty(t *testing.T) { cache := NewDefaultCache() // Range over empty cache count := 0 cache.Range(func(key uint64, value *SchemaCacheEntry) bool { count++ return true }) assert.Equal(t, 0, count, "Should not iterate over empty cache") } func TestDefaultCache_Overwrite(t *testing.T) { cache := NewDefaultCache() key := uint64(0x12345678) // Store first value first := &SchemaCacheEntry{ RenderedInline: []byte("first"), } cache.Store(key, first) // Store second value with same key second := &SchemaCacheEntry{ RenderedInline: []byte("second"), } cache.Store(key, second) // Load should return the second value loaded, ok := cache.Load(key) assert.True(t, ok) require.NotNil(t, loaded) assert.Equal(t, []byte("second"), loaded.RenderedInline) } func TestDefaultCache_MultipleKeys(t *testing.T) { cache := NewDefaultCache() // Store with different keys key1 := uint64(1) key2 := uint64(2) key3 := uint64(3) cache.Store(key1, &SchemaCacheEntry{RenderedInline: []byte("value1")}) cache.Store(key2, &SchemaCacheEntry{RenderedInline: []byte("value2")}) cache.Store(key3, &SchemaCacheEntry{RenderedInline: []byte("value3")}) // Load each one val1, ok1 := cache.Load(key1) val2, ok2 := cache.Load(key2) val3, ok3 := cache.Load(key3) assert.True(t, ok1) assert.True(t, ok2) assert.True(t, ok3) assert.Equal(t, []byte("value1"), val1.RenderedInline) assert.Equal(t, []byte("value2"), val2.RenderedInline) assert.Equal(t, []byte("value3"), val3.RenderedInline) } func TestDefaultCache_ThreadSafety(t *testing.T) { cache := NewDefaultCache() // Concurrent writes done := make(chan bool, 10) for i := 0; i < 10; i++ { go func(val int) { key := uint64(val) cache.Store(key, &SchemaCacheEntry{ RenderedInline: []byte{byte(val)}, }) done <- true }(i) } // Wait for all writes for i := 0; i < 10; i++ { <-done } // Concurrent reads for i := 0; i < 10; i++ { go func(val int) { key := uint64(val) loaded, ok := cache.Load(key) assert.True(t, ok) assert.NotNil(t, loaded) done <- true }(i) } // Wait for all reads for i := 0; i < 10; i++ { <-done } // Verify all entries exist count := 0 cache.Range(func(key uint64, value *SchemaCacheEntry) bool { count++ return true }) assert.Equal(t, 10, count, "All entries should be present") } func TestSchemaCache_Fields(t *testing.T) { // Test that SchemaCache properly holds all fields schema := &base.Schema{} compiled := &jsonschema.Schema{} sc := &SchemaCacheEntry{ Schema: schema, RenderedInline: []byte("rendered"), RenderedJSON: []byte(`{"type":"object"}`), CompiledSchema: compiled, } assert.Equal(t, schema, sc.Schema) assert.Equal(t, []byte("rendered"), sc.RenderedInline) assert.Equal(t, []byte(`{"type":"object"}`), sc.RenderedJSON) assert.Equal(t, compiled, sc.CompiledSchema) } func TestDefaultCache_RangeWithInvalidTypes(t *testing.T) { cache := NewDefaultCache() // Manually insert invalid types into the underlying sync.Map to test defensive programming // Store an entry with wrong key type cache.m.Store("invalid-key-type", &SchemaCacheEntry{}) // Store an entry with wrong value type validKey := uint64(1) cache.m.Store(validKey, "invalid-value-type") // Store a valid entry validKey2 := uint64(2) validEntry := &SchemaCacheEntry{RenderedInline: []byte("valid")} cache.Store(validKey2, validEntry) // Range should skip invalid entries and only process valid ones count := 0 var seenEntry *SchemaCacheEntry cache.Range(func(key uint64, value *SchemaCacheEntry) bool { count++ seenEntry = value return true }) assert.Equal(t, 1, count, "Should only process valid entry") assert.Equal(t, validEntry, seenEntry, "Should see the valid entry") } libopenapi-validator-0.13.8/cache/default_cache.go000066400000000000000000000022761520534042400220570ustar00rootroot00000000000000package cache import "sync" // DefaultCache is the default cache implementation using sync.Map for thread-safe concurrent access. type DefaultCache struct { m *sync.Map } var _ SchemaCache = &DefaultCache{} // NewDefaultCache creates a new DefaultCache with an initialized sync.Map. func NewDefaultCache() *DefaultCache { return &DefaultCache{m: &sync.Map{}} } // Load retrieves a schema from the cache. func (c *DefaultCache) Load(key uint64) (*SchemaCacheEntry, bool) { if c == nil || c.m == nil { return nil, false } val, ok := c.m.Load(key) if !ok { return nil, false } schemaCache, ok := val.(*SchemaCacheEntry) return schemaCache, ok } // Store saves a schema to the cache. func (c *DefaultCache) Store(key uint64, value *SchemaCacheEntry) { if c == nil || c.m == nil { return } c.m.Store(key, value) } // Range calls f for each entry in the cache (for testing/inspection). func (c *DefaultCache) Range(f func(key uint64, value *SchemaCacheEntry) bool) { if c == nil || c.m == nil { return } c.m.Range(func(k, v interface{}) bool { key, ok := k.(uint64) if !ok { return true } val, ok := v.(*SchemaCacheEntry) if !ok { return true } return f(key, val) }) } libopenapi-validator-0.13.8/cmd/000077500000000000000000000000001520534042400164525ustar00rootroot00000000000000libopenapi-validator-0.13.8/cmd/validate/000077500000000000000000000000001520534042400202435ustar00rootroot00000000000000libopenapi-validator-0.13.8/cmd/validate/main.go000066400000000000000000000160061520534042400215210ustar00rootroot00000000000000package main import ( "errors" "flag" "fmt" "log/slog" "os" "github.com/dlclark/regexp2" "github.com/goccy/go-yaml" "github.com/pb33f/libopenapi" "github.com/santhosh-tekuri/jsonschema/v6" validator "github.com/pb33f/libopenapi-validator" "github.com/pb33f/libopenapi-validator/config" ) type customRegexp regexp2.Regexp func (re *customRegexp) MatchString(s string) bool { matched, err := (*regexp2.Regexp)(re).MatchString(s) return err == nil && matched } func (re *customRegexp) String() string { return (*regexp2.Regexp)(re).String() } type regexEngine struct { runtimeOption regexp2.RegexOptions } func (e *regexEngine) run(s string) (jsonschema.Regexp, error) { re, err := regexp2.Compile(s, e.runtimeOption) if err != nil { return nil, err } return (*customRegexp)(re), nil } var regexParsingOptionsMap = map[string]regexp2.RegexOptions{ "none": regexp2.None, "ignorecase": regexp2.IgnoreCase, "multiline": regexp2.Multiline, "explicitcapture": regexp2.ExplicitCapture, "compiled": regexp2.Compiled, "singleline": regexp2.Singleline, "ignorepatternwhitespace": regexp2.IgnorePatternWhitespace, "righttoleft": regexp2.RightToLeft, "debug": regexp2.Debug, "ecmascript": regexp2.ECMAScript, "re2": regexp2.RE2, "unicode": regexp2.Unicode, } var ( defaultRegexEngine = "" regexParsingOptions = flag.String("regexengine", defaultRegexEngine, `Specify the regex parsing option to use. Supported values are: Engines: re2 (default), ecmascript Flags: ignorecase, multiline, explicitcapture, compiled, singleline, ignorepatternwhitespace, righttoleft, debug, unicode If not specified, the default libopenapi option is "re2". If not specified, the default libopenapi regex engine is "re2"".`) convertYAMLToJSON = flag.Bool("yaml2json", false, `Convert YAML files to JSON before validation. libopenapi passes map[interface{}]interface{} structures for deeply nested objects or complex mappings, which are not allowed in JSON and cannot be validated by jsonschema. This flag allows pre-converting from YAML to JSON to bypass this limitation of the libopenapi. Default is false.`) ) // main is the entry point for validating an OpenAPI Specification (OAS) document. // It uses the libopenapi-validator library to check if the provided OAS document // conforms to the OpenAPI specification. // // This tool accepts a single input file (YAML or JSON) and provides optional flags: // // `--regexengine` flag to customize the regex engine used during validation. // This is useful for cases where the spec uses regex patterns that require engines // like ECMAScript or RE2. // // Supported regex options include: // - Engines: re2 (default), ecmascript // - Flags: ignorecase, multiline, explicitcapture, compiled, singleline, // ignorepatternwhitespace, righttoleft, debug, unicode // // `--yaml2json` flag to convert YAML files to JSON before validation. // libopenapi passes map[interface{}]interface{} structures for deeply nested // objects or complex mappings, which are not allowed in JSON and cannot be // validated by jsonschema. This flag allows pre-converting from YAML to JSON // to bypass this limitation of the libopenapi. Default is false. // // Example usage: // // go run main.go --regexengine=ecmascript ./my-api-spec.yaml // go run main.go --yaml2json ./my-api-spec.yaml // // If validation passes, the tool logs a success message. // If the document is invalid or there is a processing error, it logs details and exits non-zero. func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, `Usage: validate [OPTIONS] Validates an OpenAPI document using libopenapi-validator. Options: --regexengine string Specify the regex parsing option to use. Supported values are: Engines: re2 (default), ecmascript Flags: ignorecase, multiline, explicitcapture, compiled, singleline, ignorepatternwhitespace, righttoleft, debug, unicode If not specified, the default libopenapi option is "re2". --yaml2json Convert YAML files to JSON before validation. libopenapi passes map[interface{}]interface{} structures for deeply nested objects or complex mappings, which are not allowed in JSON and cannot be validated by jsonschema. This flag allows pre-converting from YAML to JSON to bypass this limitation of the libopenapi. (default: false) -h, --help Show this help message and exit. `) } for _, arg := range os.Args[1:] { if arg == "--help" || arg == "-h" { flag.Usage() os.Exit(0) } } logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) flag.Parse() filename := flag.Arg(0) if len(flag.Args()) != 1 || filename == "" { logger.Error("missing file argument", slog.Any("args", os.Args)) flag.Usage() os.Exit(1) } validationOpts := []config.Option{} if *regexParsingOptions != "" { regexEngineOption, ok := regexParsingOptionsMap[*regexParsingOptions] if !ok { logger.Error("unsupported regex option provided", slog.String("provided", *regexParsingOptions), slog.Any("supported", []string{ "none", "ignorecase", "multiline", "explicitcapture", "compiled", "singleline", "ignorepatternwhitespace", "righttoleft", "debug", "ecmascript", "re2", "unicode", }), ) os.Exit(1) } reEngine := ®exEngine{ runtimeOption: regexEngineOption, } validationOpts = append(validationOpts, config.WithRegexEngine(reEngine.run)) } data, err := os.ReadFile(filename) if err != nil { logger.Error("error reading file", slog.String("provided", filename), slog.Any("error", err)) os.Exit(1) } if *convertYAMLToJSON { var v interface{} if err := yaml.Unmarshal(data, &v); err == nil { data, err = yaml.YAMLToJSON(data) if err != nil { logger.Error("invalid api spec: error converting yaml to json", slog.Any("error", err)) os.Exit(1) } } } doc, err := libopenapi.NewDocument(data) if err != nil { logger.Error("error creating new libopenapi document", slog.Any("error", err)) os.Exit(1) } docValidator, validatorErrs := validator.NewValidator(doc, validationOpts...) if len(validatorErrs) > 0 { logger.Error("error creating a new validator", slog.Any("errors", errors.Join(validatorErrs...))) os.Exit(1) } valid, validationErrs := docValidator.ValidateDocument() if !valid { logger.Error("validation errors", slog.Any("errors", validationErrs)) os.Exit(1) } logger.Info("document passes all validations", slog.String("filename", filename)) } libopenapi-validator-0.13.8/config/000077500000000000000000000000001520534042400171545ustar00rootroot00000000000000libopenapi-validator-0.13.8/config/config.go000066400000000000000000000311421520534042400207510ustar00rootroot00000000000000package config import ( "context" "log/slog" "net/http" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/santhosh-tekuri/jsonschema/v6" "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi-validator/radix" ) // RegexCache can be set to enable compiled regex caching. // It can be just a sync.Map, or a custom implementation with possible cleanup. // // Be aware that the cache should be thread safe type RegexCache interface { Load(key any) (value any, ok bool) // Get a compiled regex from the cache Store(key, value any) // Set a compiled regex to the cache } // AuthenticationFunc validates a security scheme for an HTTP request. // Return nil when the scheme is satisfied; return an error to fail the current security requirement. type AuthenticationFunc func(context.Context, *AuthenticationInput) error // AuthenticationInput contains the request and OpenAPI security scheme details passed to an AuthenticationFunc. type AuthenticationInput struct { Request *http.Request SecuritySchemeName string SecurityScheme *v3.SecurityScheme Scopes []string } // ValidationOptions A container for validation configuration. // // Generally fluent With... style functions are used to establish the desired behavior. type ValidationOptions struct { RegexEngine jsonschema.RegexpEngine RegexCache RegexCache // Enable compiled regex caching FormatAssertions bool ContentAssertions bool SecurityValidation bool AuthenticationFunc AuthenticationFunc OpenAPIMode bool // Enable OpenAPI-specific vocabulary validation AllowScalarCoercion bool // Enable string->boolean/number coercion Formats map[string]func(v any) error SchemaCache cache.SchemaCache // Optional cache for compiled schemas PathTree radix.PathLookup // O(k) path lookup via radix tree (built automatically) pathTreeDisabled bool // Internal: true if radix tree auto-build was disabled via DisablePathTree Logger *slog.Logger // Logger for debug/error output (nil = silent) AllowXMLBodyValidation bool // Allows to convert XML to JSON for validating a request/response body. AllowURLEncodedBodyValidation bool // Allows to convert URL Encoded to JSON for validating a request/response body. // strict mode options - detect undeclared properties even when additionalProperties: true StrictMode bool // Enable strict property validation StrictIgnorePaths []string // Instance JSONPath patterns to exclude from strict checks StrictIgnoredHeaders []string // Headers to always ignore in strict mode (nil = use defaults) strictIgnoredHeadersMerge bool // Internal: true if merging with defaults StrictRejectReadOnly bool // Reject readOnly properties in requests StrictRejectWriteOnly bool // Reject writeOnly properties in responses } // Option Enables an 'Options pattern' approach type Option func(*ValidationOptions) // NewValidationOptions creates a new ValidationOptions instance with default values. func NewValidationOptions(opts ...Option) *ValidationOptions { // create the set of default values o := &ValidationOptions{ FormatAssertions: false, ContentAssertions: false, SecurityValidation: true, OpenAPIMode: true, // Enable OpenAPI vocabulary by default SchemaCache: cache.NewDefaultCache(), // Enable caching by default } for _, opt := range opts { if opt != nil { opt(o) } } return o } // WithExistingOpts returns an Option that will copy the values from the supplied ValidationOptions instance func WithExistingOpts(options *ValidationOptions) Option { return func(o *ValidationOptions) { if options != nil { o.RegexEngine = options.RegexEngine o.RegexCache = options.RegexCache o.FormatAssertions = options.FormatAssertions o.ContentAssertions = options.ContentAssertions o.SecurityValidation = options.SecurityValidation o.AuthenticationFunc = options.AuthenticationFunc o.OpenAPIMode = options.OpenAPIMode o.AllowScalarCoercion = options.AllowScalarCoercion o.Formats = options.Formats o.SchemaCache = options.SchemaCache o.PathTree = options.PathTree o.pathTreeDisabled = options.pathTreeDisabled o.Logger = options.Logger o.AllowXMLBodyValidation = options.AllowXMLBodyValidation o.AllowURLEncodedBodyValidation = options.AllowURLEncodedBodyValidation o.StrictMode = options.StrictMode o.StrictIgnorePaths = options.StrictIgnorePaths o.StrictIgnoredHeaders = options.StrictIgnoredHeaders o.strictIgnoredHeadersMerge = options.strictIgnoredHeadersMerge o.StrictRejectReadOnly = options.StrictRejectReadOnly o.StrictRejectWriteOnly = options.StrictRejectWriteOnly } } } // WithLogger sets the logger for validation debug/error output. // If not set, logging is silent (nil logger is handled gracefully). func WithLogger(logger *slog.Logger) Option { return func(o *ValidationOptions) { o.Logger = logger } } // WithRegexEngine Assigns a custom regular-expression engine to be used during validation. func WithRegexEngine(engine jsonschema.RegexpEngine) Option { return func(o *ValidationOptions) { o.RegexEngine = engine } } // WithRegexCache assigns a cache for compiled regular expressions. // A sync.Map should be sufficient for most use cases. It does not implement any cleanup func WithRegexCache(regexCache RegexCache) Option { return func(o *ValidationOptions) { o.RegexCache = regexCache } } // WithFormatAssertions enables checks for 'format' assertions (such as date, date-time, uuid, etc) func WithFormatAssertions() Option { return func(o *ValidationOptions) { o.FormatAssertions = true } } // WithContentAssertions enables checks for contentType, contentEncoding, etc func WithContentAssertions() Option { return func(o *ValidationOptions) { o.ContentAssertions = true } } // WithoutSecurityValidation disables security validation for request validation func WithoutSecurityValidation() Option { return func(o *ValidationOptions) { o.SecurityValidation = false } } // WithAuthenticationFunc sets a custom function for validating security requirements. // When set, the function is authoritative for all security scheme types, including oauth2 and openIdConnect. func WithAuthenticationFunc(fn AuthenticationFunc) Option { return func(o *ValidationOptions) { o.AuthenticationFunc = fn } } // WithCustomFormat adds custom formats and their validators that checks for custom 'format' assertions // When you add different validators with the same name, they will be overridden, // and only the last registration will take effect. func WithCustomFormat(name string, validator func(v any) error) Option { return func(o *ValidationOptions) { if o.Formats == nil { o.Formats = make(map[string]func(v any) error) } o.Formats[name] = validator } } // WithOpenAPIMode enables OpenAPI-specific keyword validation (default: true) func WithOpenAPIMode() Option { return func(o *ValidationOptions) { o.OpenAPIMode = true } } // WithoutOpenAPIMode disables OpenAPI-specific keyword validation func WithoutOpenAPIMode() Option { return func(o *ValidationOptions) { o.OpenAPIMode = false } } // WithScalarCoercion enables string to boolean/number coercion (Jackson-style) func WithScalarCoercion() Option { return func(o *ValidationOptions) { o.AllowScalarCoercion = true } } // WithXmlBodyValidation enables converting an XML body to a JSON when validating the schema from a request and response body // The default option is set to false func WithXmlBodyValidation() Option { return func(o *ValidationOptions) { o.AllowXMLBodyValidation = true } } // WithURLEncodedBodyValidation enables converting an URL Encoded body to a JSON when validating the schema from a request and response body // The default option is set to false func WithURLEncodedBodyValidation() Option { return func(o *ValidationOptions) { o.AllowURLEncodedBodyValidation = true } } // WithSchemaCache sets a custom cache implementation or disables caching if nil. // Pass nil to disable schema caching and skip cache warming during validator initialization. // The default cache is a thread-safe sync.Map wrapper. func WithSchemaCache(schemaCache cache.SchemaCache) Option { return func(o *ValidationOptions) { o.SchemaCache = schemaCache } } // WithPathTree sets a custom radix tree for path matching. // The default is built automatically from the OpenAPI specification. func WithPathTree(pathTree radix.PathLookup) Option { return func(o *ValidationOptions) { o.PathTree = pathTree } } // DisablePathTree prevents automatic radix tree construction. // Use this to fall back to regex-based path matching only. func DisablePathTree() Option { return func(o *ValidationOptions) { o.pathTreeDisabled = true } } // WithStrictMode enables strict property validation. // In strict mode, undeclared properties are reported as errors even when // additionalProperties: true would normally allow them. // // This is useful for API governance scenarios where you want to ensure // clients only send properties that are explicitly documented in the // OpenAPI specification. func WithStrictMode() Option { return func(o *ValidationOptions) { o.StrictMode = true } } // WithStrictIgnorePaths sets JSONPath patterns for paths to exclude from strict validation. // Patterns use glob syntax: // - * matches a single path segment // - ** matches any depth (zero or more segments) // - [*] matches any array index // - \* escapes a literal asterisk // // Examples: // - "$.body.metadata.*" - any property under metadata // - "$.body.**.x-*" - any x-* property at any depth // - "$.headers.X-*" - any header starting with X- func WithStrictIgnorePaths(paths ...string) Option { return func(o *ValidationOptions) { o.StrictIgnorePaths = paths } } // WithStrictRejectReadOnly enables rejection of readOnly properties in requests. // When enabled, readOnly properties present in request bodies are reported as // validation errors instead of being silently skipped. func WithStrictRejectReadOnly() Option { return func(o *ValidationOptions) { o.StrictRejectReadOnly = true } } // WithStrictRejectWriteOnly enables rejection of writeOnly properties in responses. // When enabled, writeOnly properties present in response bodies are reported as // validation errors instead of being silently skipped. func WithStrictRejectWriteOnly() Option { return func(o *ValidationOptions) { o.StrictRejectWriteOnly = true } } // WithStrictIgnoredHeaders replaces the default ignored headers list entirely. // Use this to fully control which headers are ignored in strict mode. // For the default list, see the strict package's DefaultIgnoredHeaders. func WithStrictIgnoredHeaders(headers ...string) Option { return func(o *ValidationOptions) { o.StrictIgnoredHeaders = headers o.strictIgnoredHeadersMerge = false } } // WithStrictIgnoredHeadersExtra adds headers to the default ignored list. // Unlike WithStrictIgnoredHeaders, this merges with the defaults rather // than replacing them. func WithStrictIgnoredHeadersExtra(headers ...string) Option { return func(o *ValidationOptions) { o.StrictIgnoredHeaders = headers o.strictIgnoredHeadersMerge = true } } // defaultIgnoredHeaders contains standard HTTP headers ignored by default. // This is the fallback list used when no custom headers are configured. var defaultIgnoredHeaders = []string{ "content-type", "content-length", "accept", "authorization", "user-agent", "host", "connection", "accept-encoding", "accept-language", "cache-control", "pragma", "origin", "referer", "cookie", "date", "etag", "expires", "if-match", "if-none-match", "if-modified-since", "last-modified", "transfer-encoding", "vary", "x-forwarded-for", "x-forwarded-proto", "x-real-ip", "x-request-id", "request-start-time", // Added by some API clients for timing } // IsPathTreeDisabled returns true if radix tree auto-build was disabled via DisablePathTree. func (o *ValidationOptions) IsPathTreeDisabled() bool { return o.pathTreeDisabled } // GetEffectiveStrictIgnoredHeaders returns the list of headers to ignore // based on configuration. Returns defaults if not configured, merged list // if extra headers were added, or replaced list if headers were fully replaced. func (o *ValidationOptions) GetEffectiveStrictIgnoredHeaders() []string { if o.StrictIgnoredHeaders == nil { return defaultIgnoredHeaders } if o.strictIgnoredHeadersMerge { return append(defaultIgnoredHeaders, o.StrictIgnoredHeaders...) } return o.StrictIgnoredHeaders } libopenapi-validator-0.13.8/config/config_test.go000066400000000000000000000441421520534042400220140ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package config import ( "context" "log/slog" "sync" "testing" "github.com/santhosh-tekuri/jsonschema/v6" "github.com/stretchr/testify/assert" ) func TestNewValidationOptions_Defaults(t *testing.T) { opts := NewValidationOptions() assert.NotNil(t, opts) assert.False(t, opts.FormatAssertions) assert.False(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.False(t, opts.AllowXMLBodyValidation) // Default is false assert.False(t, opts.AllowURLEncodedBodyValidation) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } func TestNewValidationOptions_WithNilOption(t *testing.T) { opts := NewValidationOptions(nil) assert.NotNil(t, opts) assert.False(t, opts.FormatAssertions) assert.False(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.False(t, opts.AllowXMLBodyValidation) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } func TestWithFormatAssertions(t *testing.T) { opts := NewValidationOptions(WithFormatAssertions()) assert.True(t, opts.FormatAssertions) assert.False(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.False(t, opts.AllowXMLBodyValidation) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } func TestWithContentAssertions(t *testing.T) { opts := NewValidationOptions(WithContentAssertions()) assert.False(t, opts.FormatAssertions) assert.True(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.False(t, opts.AllowXMLBodyValidation) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } func TestWithoutSecurityValidation(t *testing.T) { opts := NewValidationOptions(WithoutSecurityValidation()) assert.False(t, opts.FormatAssertions) assert.False(t, opts.ContentAssertions) assert.False(t, opts.SecurityValidation) assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } func TestWithAuthenticationFunc(t *testing.T) { called := false authFn := func(ctx context.Context, input *AuthenticationInput) error { called = true assert.NotNil(t, ctx) assert.Equal(t, "ApiKeyAuth", input.SecuritySchemeName) return nil } opts := NewValidationOptions(WithAuthenticationFunc(authFn)) assert.True(t, opts.SecurityValidation) assert.NotNil(t, opts.AuthenticationFunc) assert.NoError(t, opts.AuthenticationFunc(context.Background(), &AuthenticationInput{ SecuritySchemeName: "ApiKeyAuth", })) assert.True(t, called) } func TestWithRegexEngine(t *testing.T) { // Test with nil regex engine (valid) var mockEngine jsonschema.RegexpEngine = nil opts := NewValidationOptions(WithRegexEngine(mockEngine)) assert.False(t, opts.FormatAssertions) assert.False(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } func TestWithExistingOpts(t *testing.T) { // Create original options with all settings enabled var testEngine jsonschema.RegexpEngine = nil original := &ValidationOptions{ RegexEngine: testEngine, RegexCache: &sync.Map{}, FormatAssertions: true, AllowXMLBodyValidation: true, AllowURLEncodedBodyValidation: true, ContentAssertions: true, SecurityValidation: false, } // Create new options using existing options opts := NewValidationOptions(WithExistingOpts(original)) assert.Nil(t, opts.RegexEngine) // Both should be nil assert.NotNil(t, opts.RegexCache) assert.Equal(t, original.AllowXMLBodyValidation, opts.AllowXMLBodyValidation) assert.Equal(t, original.AllowURLEncodedBodyValidation, opts.AllowURLEncodedBodyValidation) assert.Equal(t, original.FormatAssertions, opts.FormatAssertions) assert.Equal(t, original.ContentAssertions, opts.ContentAssertions) assert.Equal(t, original.SecurityValidation, opts.SecurityValidation) } func TestWithExistingOpts_NilSource(t *testing.T) { // Test with nil source options opts := NewValidationOptions(WithExistingOpts(nil)) assert.NotNil(t, opts) // Should not panic and should have default values assert.False(t, opts.FormatAssertions) assert.False(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.False(t, opts.AllowXMLBodyValidation) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } func TestMultipleOptions(t *testing.T) { opts := NewValidationOptions( WithFormatAssertions(), WithContentAssertions(), WithXmlBodyValidation(), ) assert.True(t, opts.FormatAssertions) assert.True(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) assert.True(t, opts.AllowXMLBodyValidation) assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } func TestOptionOverride(t *testing.T) { // Test that later options override earlier ones // First set format assertions, then turn them off by not setting them again opts := NewValidationOptions( WithFormatAssertions(), WithContentAssertions(), ) assert.True(t, opts.FormatAssertions) assert.True(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } func TestWithExistingOpts_PartialOverride(t *testing.T) { // Create original options var testEngine jsonschema.RegexpEngine = nil original := &ValidationOptions{ RegexEngine: testEngine, FormatAssertions: true, ContentAssertions: true, SecurityValidation: false, } // Create new options using existing options, then override one setting opts := NewValidationOptions( WithExistingOpts(original), WithContentAssertions(), // This should still be true (no change) ) assert.Nil(t, opts.RegexEngine) // Both should be nil assert.Nil(t, opts.RegexCache) assert.True(t, opts.FormatAssertions) // From original assert.True(t, opts.ContentAssertions) // Reapplied, but same value assert.False(t, opts.SecurityValidation) // From original } func TestWithUrlEncodedBodyValidation(t *testing.T) { opts := NewValidationOptions( WithURLEncodedBodyValidation(), ) assert.True(t, opts.AllowURLEncodedBodyValidation) } func TestComplexScenario(t *testing.T) { // Test a complex real-world scenario var mockEngine jsonschema.RegexpEngine = nil // Start with some base options baseOpts := &ValidationOptions{ FormatAssertions: true, SecurityValidation: false, // RegexEngine and ContentAssertions are defaults (nil/false) } // Create new options building on the base opts := NewValidationOptions( WithExistingOpts(baseOpts), WithContentAssertions(), WithRegexEngine(mockEngine), ) // Verify all settings are as expected assert.True(t, opts.FormatAssertions) // From base assert.True(t, opts.ContentAssertions) // Added assert.False(t, opts.SecurityValidation) // From base assert.Nil(t, opts.RegexEngine) // Should be nil assert.Nil(t, opts.RegexCache) } func TestMultipleOptionsWithSecurityDisabled(t *testing.T) { opts := NewValidationOptions( WithFormatAssertions(), WithContentAssertions(), WithoutSecurityValidation(), ) assert.True(t, opts.FormatAssertions) assert.True(t, opts.ContentAssertions) assert.False(t, opts.SecurityValidation) assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } func TestWithExistingOpts_SecurityValidationCopied(t *testing.T) { // Test that SecurityValidation is properly copied original := &ValidationOptions{ SecurityValidation: false, } opts := NewValidationOptions(WithExistingOpts(original)) assert.False(t, opts.SecurityValidation) // Test the opposite original2 := &ValidationOptions{ SecurityValidation: true, } opts2 := NewValidationOptions(WithExistingOpts(original2)) assert.True(t, opts2.SecurityValidation) } func TestWithExistingOpts_AuthenticationFuncCopied(t *testing.T) { called := false authFn := func(context.Context, *AuthenticationInput) error { called = true return nil } original := &ValidationOptions{ AuthenticationFunc: authFn, } opts := NewValidationOptions(WithExistingOpts(original)) assert.NotNil(t, opts.AuthenticationFunc) assert.NoError(t, opts.AuthenticationFunc(context.Background(), &AuthenticationInput{})) assert.True(t, called) } // Tests for new OpenAPI and scalar coercion configuration options func TestWithOpenAPIMode(t *testing.T) { opts := NewValidationOptions(WithOpenAPIMode()) assert.True(t, opts.OpenAPIMode) assert.False(t, opts.AllowScalarCoercion) // Should be default false assert.False(t, opts.FormatAssertions) // Should be default false assert.False(t, opts.ContentAssertions) // Should be default false assert.True(t, opts.SecurityValidation) // Should be default true } func TestWithoutOpenAPIMode(t *testing.T) { opts := NewValidationOptions(WithoutOpenAPIMode()) assert.False(t, opts.OpenAPIMode) assert.False(t, opts.AllowScalarCoercion) // Should be default false assert.False(t, opts.FormatAssertions) // Should be default false assert.False(t, opts.ContentAssertions) // Should be default false assert.True(t, opts.SecurityValidation) // Should be default true } func TestWithScalarCoercion(t *testing.T) { opts := NewValidationOptions(WithScalarCoercion()) assert.True(t, opts.AllowScalarCoercion) assert.True(t, opts.OpenAPIMode) // Should be default true assert.False(t, opts.FormatAssertions) // Should be default false assert.False(t, opts.ContentAssertions) // Should be default false assert.True(t, opts.SecurityValidation) // Should be default true } func TestWithOpenAPIModeAndScalarCoercion(t *testing.T) { opts := NewValidationOptions( WithOpenAPIMode(), WithScalarCoercion(), ) assert.True(t, opts.OpenAPIMode) assert.True(t, opts.AllowScalarCoercion) assert.False(t, opts.FormatAssertions) // Should be default false assert.False(t, opts.ContentAssertions) // Should be default false assert.True(t, opts.SecurityValidation) // Should be default true } func TestWithOpenAPIModeOverride(t *testing.T) { // Test that WithoutOpenAPIMode can override WithOpenAPIMode opts := NewValidationOptions( WithOpenAPIMode(), WithoutOpenAPIMode(), ) assert.False(t, opts.OpenAPIMode) // Should be false (last option wins) assert.False(t, opts.AllowScalarCoercion) } func TestComplexOpenAPIScenario(t *testing.T) { // Test a complex scenario with OpenAPI mode and other options opts := NewValidationOptions( WithFormatAssertions(), WithOpenAPIMode(), WithScalarCoercion(), WithContentAssertions(), WithoutSecurityValidation(), ) assert.True(t, opts.OpenAPIMode) assert.True(t, opts.AllowScalarCoercion) assert.True(t, opts.FormatAssertions) assert.True(t, opts.ContentAssertions) assert.False(t, opts.SecurityValidation) assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } func TestWithExistingOpts_OpenAPIFields(t *testing.T) { // Test that OpenAPI fields are properly copied from existing options original := &ValidationOptions{ OpenAPIMode: true, AllowScalarCoercion: true, FormatAssertions: false, ContentAssertions: false, SecurityValidation: true, } opts := NewValidationOptions(WithExistingOpts(original)) assert.True(t, opts.OpenAPIMode) assert.True(t, opts.AllowScalarCoercion) assert.False(t, opts.FormatAssertions) assert.False(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) } func TestWithCustomFormat(t *testing.T) { // Test WithCustomFormat option testFormatFunc := func(v any) error { return nil // Simple test format function } opts := NewValidationOptions(WithCustomFormat("test-format", testFormatFunc)) assert.NotNil(t, opts.Formats) assert.Contains(t, opts.Formats, "test-format") assert.NotNil(t, opts.Formats["test-format"]) } func TestWithSchemaCache(t *testing.T) { // Test with nil cache (disables caching) opts := NewValidationOptions(WithSchemaCache(nil)) assert.Nil(t, opts.SchemaCache) // Test with default cache by creating a new options object optsDefault := NewValidationOptions() assert.NotNil(t, optsDefault.SchemaCache, "Default options should have a cache") // Test setting a custom cache customCache := optsDefault.SchemaCache // Use default cache as custom optsCustom := NewValidationOptions(WithSchemaCache(customCache)) assert.Equal(t, customCache, optsCustom.SchemaCache) } func TestWithRegexpCache(t *testing.T) { syncMap := &sync.Map{} opts := NewValidationOptions(WithRegexCache(syncMap)) assert.NotNil(t, opts.RegexCache) } // Tests for strict mode configuration options func TestWithStrictMode(t *testing.T) { opts := NewValidationOptions(WithStrictMode()) assert.True(t, opts.StrictMode) assert.Nil(t, opts.StrictIgnorePaths) assert.Nil(t, opts.StrictIgnoredHeaders) } func TestWithStrictIgnorePaths(t *testing.T) { paths := []string{"$.body.metadata.*", "$.headers.X-*"} opts := NewValidationOptions(WithStrictIgnorePaths(paths...)) assert.Equal(t, paths, opts.StrictIgnorePaths) assert.False(t, opts.StrictMode) // Not enabled by default } func TestWithStrictIgnoredHeaders(t *testing.T) { headers := []string{"x-custom-header", "x-another-header"} opts := NewValidationOptions(WithStrictIgnoredHeaders(headers...)) assert.Equal(t, headers, opts.StrictIgnoredHeaders) assert.False(t, opts.strictIgnoredHeadersMerge) } func TestWithStrictIgnoredHeadersExtra(t *testing.T) { headers := []string{"x-extra-header"} opts := NewValidationOptions(WithStrictIgnoredHeadersExtra(headers...)) assert.Equal(t, headers, opts.StrictIgnoredHeaders) assert.True(t, opts.strictIgnoredHeadersMerge) } func TestGetEffectiveStrictIgnoredHeaders_Default(t *testing.T) { opts := NewValidationOptions() headers := opts.GetEffectiveStrictIgnoredHeaders() assert.NotNil(t, headers) assert.Contains(t, headers, "content-type") assert.Contains(t, headers, "authorization") } func TestGetEffectiveStrictIgnoredHeaders_Replace(t *testing.T) { customHeaders := []string{"x-only-this"} opts := NewValidationOptions(WithStrictIgnoredHeaders(customHeaders...)) headers := opts.GetEffectiveStrictIgnoredHeaders() assert.Equal(t, customHeaders, headers) assert.NotContains(t, headers, "content-type") // Default headers are replaced } func TestGetEffectiveStrictIgnoredHeaders_Merge(t *testing.T) { extraHeaders := []string{"x-extra-header"} opts := NewValidationOptions(WithStrictIgnoredHeadersExtra(extraHeaders...)) headers := opts.GetEffectiveStrictIgnoredHeaders() // Should have both defaults and extras assert.Contains(t, headers, "content-type") // From defaults assert.Contains(t, headers, "x-extra-header") // From extras assert.Contains(t, headers, "authorization") // From defaults } func TestWithLogger(t *testing.T) { logger := slog.New(slog.NewTextHandler(nil, nil)) opts := NewValidationOptions(WithLogger(logger)) assert.Equal(t, logger, opts.Logger) } func TestWithExistingOpts_StrictFields(t *testing.T) { original := &ValidationOptions{ StrictMode: true, StrictRejectReadOnly: true, StrictRejectWriteOnly: true, StrictIgnorePaths: []string{"$.body.*"}, StrictIgnoredHeaders: []string{"x-custom"}, strictIgnoredHeadersMerge: true, Logger: slog.New(slog.NewTextHandler(nil, nil)), } opts := NewValidationOptions(WithExistingOpts(original)) assert.True(t, opts.StrictMode) assert.True(t, opts.StrictRejectReadOnly) assert.True(t, opts.StrictRejectWriteOnly) assert.Equal(t, original.StrictIgnorePaths, opts.StrictIgnorePaths) assert.Equal(t, original.StrictIgnoredHeaders, opts.StrictIgnoredHeaders) assert.True(t, opts.strictIgnoredHeadersMerge) assert.Equal(t, original.Logger, opts.Logger) } func TestWithStrictRejectReadOnly(t *testing.T) { opts := NewValidationOptions(WithStrictRejectReadOnly()) assert.True(t, opts.StrictRejectReadOnly) } func TestWithStrictRejectWriteOnly(t *testing.T) { opts := NewValidationOptions(WithStrictRejectWriteOnly()) assert.True(t, opts.StrictRejectWriteOnly) } func TestStrictModeWithIgnorePaths(t *testing.T) { paths := []string{"$.body.metadata.*"} opts := NewValidationOptions( WithStrictMode(), WithStrictIgnorePaths(paths...), ) assert.True(t, opts.StrictMode) assert.Equal(t, paths, opts.StrictIgnorePaths) } func TestWithPathTree(t *testing.T) { // Use a mock/nil path tree — WithPathTree just sets the field opts := NewValidationOptions(WithPathTree(nil)) assert.Nil(t, opts.PathTree) // TestWithPathTree with a real value — use a custom implementation // We can verify the field is set using a simple check opts2 := &ValidationOptions{} WithPathTree(nil)(opts2) assert.Nil(t, opts2.PathTree) } func TestDisablePathTree(t *testing.T) { opts := NewValidationOptions(DisablePathTree()) assert.True(t, opts.IsPathTreeDisabled()) } func TestIsPathTreeDisabled_Default(t *testing.T) { opts := NewValidationOptions() assert.False(t, opts.IsPathTreeDisabled()) } func TestWithExistingOpts_PathTreeFields(t *testing.T) { original := NewValidationOptions(DisablePathTree()) opts := NewValidationOptions(WithExistingOpts(original)) assert.True(t, opts.IsPathTreeDisabled()) assert.Nil(t, opts.PathTree) } libopenapi-validator-0.13.8/errors/000077500000000000000000000000001520534042400172235ustar00rootroot00000000000000libopenapi-validator-0.13.8/errors/error_utilities.go000066400000000000000000000011621520534042400227760ustar00rootroot00000000000000package errors import ( "net/http" ) // PopulateValidationErrors mutates the provided validation errors with additional useful error information, that is // not necessarily available when the ValidationError was created and are standard for all errors. // Specifically, the RequestPath, SpecPath and RequestMethod are populated. func PopulateValidationErrors(validationErrors []*ValidationError, request *http.Request, path string) { for _, validationError := range validationErrors { validationError.SpecPath = path validationError.RequestMethod = request.Method validationError.RequestPath = request.URL.Path } } libopenapi-validator-0.13.8/errors/error_utilities_test.go000066400000000000000000000017411520534042400240400ustar00rootroot00000000000000// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package errors import ( "net/http" "testing" "github.com/stretchr/testify/require" ) // Helper function to create a mock ValidationError func createMockValidationError() *ValidationError { return &ValidationError{ Message: "Test validation error", } } func TestPopulateValidationErrors(t *testing.T) { // Create a mock request req, _ := http.NewRequest(http.MethodGet, "/test/path", nil) // Create mock validation errors validationErrors := []*ValidationError{ createMockValidationError(), createMockValidationError(), } // Call the function PopulateValidationErrors(validationErrors, req, "/spec/path") // Validate the results for _, validationError := range validationErrors { require.Equal(t, "/spec/path", validationError.SpecPath) require.Equal(t, http.MethodGet, validationError.RequestMethod) require.Equal(t, "/test/path", validationError.RequestPath) } } libopenapi-validator-0.13.8/errors/package.go000066400000000000000000000002651520534042400211500ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT // Package errors contains all the error types used by the validator package errors libopenapi-validator-0.13.8/errors/parameter_errors.go000066400000000000000000001445701520534042400231410ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package errors import ( "fmt" "net/url" "strings" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/helpers" ) func IncorrectFormEncoding(param *v3.Parameter, qp *helpers.QueryParam, i int) *ValidationError { specLine, specCol := paramExplodeLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query parameter '%s' is not exploded correctly", param.Name), Reason: fmt.Sprintf("The query parameter '%s' has a default or 'form' encoding defined, "+ "however the value '%s' is encoded as an object or an array using commas. The contract defines "+ "the explode value to set to 'true'", param.Name, qp.Values[i]), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: param, HowToFix: fmt.Sprintf(HowToFixParamInvalidFormEncode, helpers.CollapseCSVIntoFormStyle(param.Name, qp.Values[i])), } } func IncorrectSpaceDelimiting(param *v3.Parameter, qp *helpers.QueryParam) *ValidationError { specLine, specCol := paramStyleLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query parameter '%s' delimited incorrectly", param.Name), Reason: fmt.Sprintf("The query parameter '%s' has 'spaceDelimited' style defined, "+ "and explode is defined as false. There are multiple values (%d) supplied, instead of a single"+ " space delimited value", param.Name, len(qp.Values)), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: param, HowToFix: fmt.Sprintf(HowToFixParamInvalidSpaceDelimitedObjectExplode, helpers.CollapseCSVIntoSpaceDelimitedStyle(param.Name, qp.Values)), } } func IncorrectPipeDelimiting(param *v3.Parameter, qp *helpers.QueryParam) *ValidationError { specLine, specCol := paramStyleLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query parameter '%s' delimited incorrectly", param.Name), Reason: fmt.Sprintf("The query parameter '%s' has 'pipeDelimited' style defined, "+ "and explode is defined as false. There are multiple values (%d) supplied, instead of a single"+ " space delimited value", param.Name, len(qp.Values)), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: param, HowToFix: fmt.Sprintf(HowToFixParamInvalidPipeDelimitedObjectExplode, helpers.CollapseCSVIntoPipeDelimitedStyle(param.Name, qp.Values)), } } func InvalidDeepObject(param *v3.Parameter, qp *helpers.QueryParam) *ValidationError { specLine, specCol := paramStyleLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query parameter '%s' is not a valid deepObject", param.Name), Reason: fmt.Sprintf("The query parameter '%s' has the 'deepObject' style defined, "+ "There are multiple values (%d) supplied, instead of a single "+ "value", param.Name, len(qp.Values)), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: param, HowToFix: fmt.Sprintf(HowToFixParamInvalidDeepObjectMultipleValues, helpers.CollapseCSVIntoPipeDelimitedStyle(param.Name, qp.Values)), } } func deepObjectPathForError(qp *helpers.QueryParam) string { if qp == nil { return "" } if len(qp.PropertyPath) > 0 { return strings.Join(qp.PropertyPath, ".") } return qp.Property } func deepObjectBracketPathForError(qp *helpers.QueryParam) string { if qp == nil { return "" } if len(qp.PropertyPath) > 0 { return strings.Join(qp.PropertyPath, "][") } return qp.Property } func InvalidDeepObjectPathConflict(param *v3.Parameter, prefixParam, nestedParam *helpers.QueryParam) *ValidationError { specLine, specCol := paramStyleLineCol(param) prefixPath := deepObjectPathForError(prefixParam) nestedPath := deepObjectPathForError(nestedParam) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query parameter '%s' is not a valid deepObject", param.Name), Reason: fmt.Sprintf("The query parameter '%s' has the 'deepObject' style defined, "+ "but the property path '%s' is also used as a nested object prefix for '%s'", param.Name, prefixPath, nestedPath), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: param, HowToFix: fmt.Sprintf(HowToFixParamInvalidDeepObjectPathConflict, param.Name, deepObjectBracketPathForError(prefixParam), param.Name, deepObjectBracketPathForError(nestedParam)), } } func QueryParameterMissing(param *v3.Parameter, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "required") specLine, specCol := paramRequiredLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query parameter '%s' is missing", param.Name), Reason: fmt.Sprintf("The query parameter '%s' is defined as being required, "+ "however it's missing from the requests", param.Name), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, HowToFix: HowToFixMissingValue, SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Required query parameter '%s' is missing", param.Name), FieldName: param.Name, FieldPath: "", InstancePath: []string{}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func HeaderParameterMissing(param *v3.Parameter, pathTemplate string, operation string, renderedSchema string) *ValidationError { escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") escapedPath = strings.TrimPrefix(escapedPath, "~1") keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/required", escapedPath, strings.ToLower(operation), param.Name) specLine, specCol := paramRequiredLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, Message: fmt.Sprintf("Header parameter '%s' is missing", param.Name), Reason: fmt.Sprintf("The header parameter '%s' is defined as being required, "+ "however it's missing from the requests", param.Name), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, HowToFix: HowToFixMissingValue, SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Required header parameter '%s' is missing", param.Name), FieldName: param.Name, FieldPath: "", InstancePath: []string{}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func CookieParameterMissing(param *v3.Parameter, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "required") specLine, specCol := paramRequiredLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, Message: fmt.Sprintf("Cookie parameter '%s' is missing", param.Name), Reason: fmt.Sprintf("The cookie parameter '%s' is defined as being required, "+ "however it's missing from the request", param.Name), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, HowToFix: HowToFixMissingValue, SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Required cookie parameter '%s' is missing", param.Name), FieldName: param.Name, FieldPath: "", InstancePath: []string{}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func HeaderParameterCannotBeDecoded(param *v3.Parameter, val string, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") specLine, specCol := paramSchemaTypeLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, Message: fmt.Sprintf("Header parameter '%s' cannot be decoded", param.Name), Reason: fmt.Sprintf("The header parameter '%s' cannot be "+ "extracted into an object, '%s' is malformed", param.Name, val), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, HowToFix: HowToFixInvalidEncoding, SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Header value '%s' cannot be decoded as object (malformed encoding)", val), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectHeaderParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { var enums []string for i := range sch.Enum { enums = append(enums, fmt.Sprint(sch.Enum[i].Value)) } validEnums := strings.Join(enums, ", ") keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "enum") specLine, specCol := paramSchemaEnumLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, Message: fmt.Sprintf("Header parameter '%s' does not match allowed values", param.Name), Reason: fmt.Sprintf("The header parameter '%s' has pre-defined "+ "values set via an enum. The value '%s' is not one of those values.", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' does not match any enum values: [%s]", ef, validEnums), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectQueryParamArrayBoolean( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query array parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The query parameter (which is an array) '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid true/false value", param.Name, item), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array item '%s' is not a valid boolean", item), FieldName: param.Name, InstancePath: []string{param.Name, "[item]"}, KeywordLocation: keywordLocation, ReferenceSchema: renderedItemsSchema, }}, } } func IncorrectParamArrayMaxNumItems(param *v3.Parameter, sch *base.Schema, expected, actual int64, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "maxItems") specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query array parameter '%s' has too many items", param.Name), Reason: fmt.Sprintf("The query parameter (which is an array) '%s' has a maximum item length of %d, "+ "however the request provided %d items", param.Name, expected, actual), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixInvalidMaxItems, expected), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array has %d items, but maximum is %d", actual, expected), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectParamArrayMinNumItems(param *v3.Parameter, sch *base.Schema, expected, actual int64, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "minItems") specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query array parameter '%s' does not have enough items", param.Name), Reason: fmt.Sprintf("The query parameter (which is an array) '%s' has a minimum items length of %d, "+ "however the request provided %d items", param.Name, expected, actual), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixInvalidMinItems, expected), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array has %d items, but minimum is %d", actual, expected), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectParamArrayUniqueItems(param *v3.Parameter, sch *base.Schema, duplicates string, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "uniqueItems") specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query array parameter '%s' contains non-unique items", param.Name), Reason: fmt.Sprintf("The query parameter (which is an array) '%s' contains the following duplicates: '%s'", param.Name, duplicates), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: "Ensure the array values are all unique", SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array contains duplicate values: %s", duplicates), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectCookieParamArrayBoolean( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, Message: fmt.Sprintf("Cookie array parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The cookie parameter (which is an array) '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid true/false value", param.Name, item), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array item '%s' is not a valid boolean", item), FieldName: param.Name, InstancePath: []string{param.Name, "[item]"}, KeywordLocation: keywordLocation, ReferenceSchema: renderedItemsSchema, }}, } } func IncorrectQueryParamArrayInteger( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query array parameter '%s' is not a valid integer", param.Name), Reason: fmt.Sprintf("The query parameter (which is an array) '%s' is defined as being an integer, "+ "however the value '%s' is not a valid integer", param.Name, item), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, item), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array item '%s' is not a valid integer", item), FieldName: param.Name, InstancePath: []string{param.Name, "[item]"}, KeywordLocation: keywordLocation, ReferenceSchema: renderedItemsSchema, }}, } } func IncorrectQueryParamArrayNumber( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query array parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The query parameter (which is an array) '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, item), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array item '%s' is not a valid number", item), FieldName: param.Name, InstancePath: []string{param.Name, "[item]"}, KeywordLocation: keywordLocation, ReferenceSchema: renderedItemsSchema, }}, } } func IncorrectCookieParamArrayNumber( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, Message: fmt.Sprintf("Cookie array parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The cookie parameter (which is an array) '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, item), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array item '%s' is not a valid number", item), FieldName: param.Name, InstancePath: []string{param.Name, "[item]"}, KeywordLocation: keywordLocation, ReferenceSchema: renderedItemsSchema, }}, } } func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") escapedPath = strings.TrimPrefix(escapedPath, "~1") keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/content/application~1json/schema", escapedPath, strings.ToLower(operation), param.Name) specLine, specCol := paramContentLineCol(param, helpers.JSONContentType) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query parameter '%s' is not valid JSON", param.Name), Reason: fmt.Sprintf("The query parameter '%s' is defined as being a JSON object, "+ "however the value '%s' is not valid JSON", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: HowToFixInvalidJSON, SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' is not valid JSON", ef), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectQueryParamBool(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The query parameter '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid boolean", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, ef), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' is not a valid boolean", ef), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func InvalidQueryParamInteger(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query parameter '%s' is not a valid integer", param.Name), Reason: fmt.Sprintf("The query parameter '%s' is defined as being an integer, "+ "however the value '%s' is not a valid integer", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, ef), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' is not a valid integer", ef), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func InvalidQueryParamNumber(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The query parameter '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, ef), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' is not a valid number", ef), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectQueryParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { var enums []string for i := range sch.Enum { enums = append(enums, fmt.Sprint(sch.Enum[i].Value)) } validEnums := strings.Join(enums, ", ") keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "enum") specLine, specCol := paramSchemaEnumLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query parameter '%s' does not match allowed values", param.Name), Reason: fmt.Sprintf("The query parameter '%s' has pre-defined "+ "values set via an enum. The value '%s' is not one of those values.", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' does not match any enum values: [%s]", ef, validEnums), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectQueryParamEnumArray(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedItemsSchema string) *ValidationError { var enums []string if low := param.GoLow(); low != nil { if sv := low.Schema.Value; sv != nil { if s := sv.Schema(); s != nil { if iv := s.Items.Value; iv != nil { if as := iv.A.Schema(); as != nil { for i := range as.Enum.Value { enums = append(enums, fmt.Sprint(as.Enum.Value[i].Value.Value)) } } } } } } validEnums := strings.Join(enums, ", ") keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/enum") specLine, specCol := 1, 0 if low := param.GoLow(); low != nil { if sv := low.Schema.Value; sv != nil { if s := sv.Schema(); s != nil { if iv := s.Items.Value; iv != nil { if as := iv.A.Schema(); as != nil && as.Enum.KeyNode != nil { specLine = as.Enum.KeyNode.Line specCol = as.Enum.KeyNode.Column } } } } } return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query array parameter '%s' does not match allowed values", param.Name), Reason: fmt.Sprintf("The query array parameter '%s' has pre-defined "+ "values set via an enum. The value '%s' is not one of those values.", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array item '%s' does not match any enum values: [%s]", ef, validEnums), FieldName: param.Name, InstancePath: []string{param.Name, "[item]"}, KeywordLocation: keywordLocation, ReferenceSchema: renderedItemsSchema, }}, } } func IncorrectReservedValues(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") escapedPath = strings.TrimPrefix(escapedPath, "~1") keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/allowReserved", escapedPath, strings.ToLower(operation), param.Name) specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query parameter '%s' value contains reserved values", param.Name), Reason: fmt.Sprintf("The query parameter '%s' has 'allowReserved' set to false, "+ "however the value '%s' contains one of the following characters: :/?#[]@!$&'()*+,;=", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixReservedValues, url.QueryEscape(ef)), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' contains reserved characters but allowReserved is false", ef), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func InvalidHeaderParamInteger(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, Message: fmt.Sprintf("Header parameter '%s' is not a valid integer", param.Name), Reason: fmt.Sprintf("The header parameter '%s' is defined as being an integer, "+ "however the value '%s' is not a valid integer", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, ef), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' is not a valid integer", ef), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func InvalidHeaderParamNumber(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, Message: fmt.Sprintf("Header parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The header parameter '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, ef), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' is not a valid number", ef), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func InvalidCookieParamInteger(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, Message: fmt.Sprintf("Cookie parameter '%s' is not a valid integer", param.Name), Reason: fmt.Sprintf("The cookie parameter '%s' is defined as being an integer, "+ "however the value '%s' is not a valid integer", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, ef), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' is not a valid integer", ef), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func InvalidCookieParamNumber(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, Message: fmt.Sprintf("Cookie parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The cookie parameter '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, ef), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' is not a valid number", ef), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectHeaderParamBool(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, Message: fmt.Sprintf("Header parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The header parameter '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid boolean", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, ef), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' is not a valid boolean", ef), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectCookieParamBool(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, Message: fmt.Sprintf("Cookie parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The cookie parameter '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid boolean", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, ef), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' is not a valid boolean", ef), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectCookieParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { var enums []string for i := range sch.Enum { enums = append(enums, fmt.Sprint(sch.Enum[i].Value)) } validEnums := strings.Join(enums, ", ") keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "enum") specLine, specCol := paramSchemaEnumLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, Message: fmt.Sprintf("Cookie parameter '%s' does not match allowed values", param.Name), Reason: fmt.Sprintf("The cookie parameter '%s' has pre-defined "+ "values set via an enum. The value '%s' is not one of those values.", param.Name, ef), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' does not match any enum values: [%s]", ef, validEnums), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectHeaderParamArrayBoolean( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, Message: fmt.Sprintf("Header array parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The header parameter (which is an array) '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid true/false value", param.Name, item), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array item '%s' is not a valid boolean", item), FieldName: param.Name, InstancePath: []string{param.Name, "[item]"}, KeywordLocation: keywordLocation, ReferenceSchema: renderedItemsSchema, }}, } } func IncorrectHeaderParamArrayNumber( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, Message: fmt.Sprintf("Header array parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The header parameter (which is an array) '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, item), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array item '%s' is not a valid number", item), FieldName: param.Name, InstancePath: []string{param.Name, "[item]"}, KeywordLocation: keywordLocation, ReferenceSchema: renderedItemsSchema, }}, } } func IncorrectPathParamBool(param *v3.Parameter, item string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, Message: fmt.Sprintf("Path parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The path parameter '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid boolean", param.Name, item), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' is not a valid boolean", item), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectPathParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/enum", escapedPath, param.Name) var enums []string for i := range sch.Enum { enums = append(enums, fmt.Sprint(sch.Enum[i].Value)) } validEnums := strings.Join(enums, ", ") specLine, specCol := paramSchemaEnumLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, ParameterName: param.Name, Message: fmt.Sprintf("Path parameter '%s' does not match allowed values", param.Name), Reason: fmt.Sprintf("The path parameter '%s' has pre-defined "+ "values set via an enum. The value '%s' is not one of those values.", param.Name, ef), SpecLine: specLine, SpecCol: specCol, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' does not match any enum values: [%s]", ef, validEnums), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectPathParamInteger(param *v3.Parameter, item string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, Message: fmt.Sprintf("Path parameter '%s' is not a valid integer", param.Name), Reason: fmt.Sprintf("The path parameter '%s' is defined as being an integer, "+ "however the value '%s' is not a valid integer", param.Name, item), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, item), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' is not a valid integer", item), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectPathParamNumber(param *v3.Parameter, item string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, Message: fmt.Sprintf("Path parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The path parameter '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, item), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Value '%s' is not a valid number", item), FieldName: param.Name, InstancePath: []string{param.Name}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectPathParamArrayNumber( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, renderedSchema string, ) *ValidationError { escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, Message: fmt.Sprintf("Path array parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The path parameter (which is an array) '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, item), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array item '%s' is not a valid number", item), FieldName: param.Name, InstancePath: []string{param.Name, "[item]"}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectPathParamArrayInteger( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, renderedSchema string, ) *ValidationError { escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, Message: fmt.Sprintf("Path array parameter '%s' is not a valid integer", param.Name), Reason: fmt.Sprintf("The path parameter (which is an array) '%s' is defined as being an integer, "+ "however the value '%s' is not a valid integer", param.Name, item), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array item '%s' is not a valid integer", item), FieldName: param.Name, InstancePath: []string{param.Name, "[item]"}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func IncorrectPathParamArrayBoolean( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, renderedSchema string, ) *ValidationError { escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, Message: fmt.Sprintf("Path array parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The path parameter (which is an array) '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid boolean", param.Name, item), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Array item '%s' is not a valid boolean", item), FieldName: param.Name, InstancePath: []string{param.Name, "[item]"}, KeywordLocation: keywordLocation, ReferenceSchema: renderedSchema, }}, } } func PathParameterMissing(param *v3.Parameter, pathTemplate string, actualPath string) *ValidationError { actualSegments := strings.Split(strings.Trim(actualPath, "/"), "/") encodedPath := strings.ReplaceAll(pathTemplate, "~", "~0") encodedPath = strings.ReplaceAll(encodedPath, "/", "~1") encodedPath = strings.TrimPrefix(encodedPath, "~1") keywordLoc := fmt.Sprintf("/paths/%s/parameters/%s/required", encodedPath, param.Name) specLine, specCol := paramRequiredLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, Message: fmt.Sprintf("Path parameter '%s' is missing", param.Name), Reason: fmt.Sprintf("The path parameter '%s' is defined as being required, "+ "however it's missing from the requests", param.Name), SpecLine: specLine, SpecCol: specCol, ParameterName: param.Name, HowToFix: HowToFixMissingValue, SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: fmt.Sprintf("Required path parameter '%s' is missing from path '%s'", param.Name, actualPath), FieldName: param.Name, FieldPath: "", InstancePath: actualSegments, KeywordLocation: keywordLoc, }}, } } libopenapi-validator-0.13.8/errors/parameter_errors_test.go000066400000000000000000001676721520534042400242100ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package errors import ( "context" "testing" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v4" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" lowbase "github.com/pb33f/libopenapi/datamodel/low/base" lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" "github.com/pb33f/libopenapi-validator/helpers" ) // Helper to create a mock v3.Parameter object with a schema func createMockParameterWithSchema() *v3.Parameter { schemaProxy := &lowbase.SchemaProxy{} _ = schemaProxy.Build(context.Background(), &yaml.Node{}, &yaml.Node{}, nil) schemaProxy.Schema().Type = low.NodeReference[lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]]{ KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, Value: lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]{ A: "string", }, } schemaProxy.Schema().Enum = low.NodeReference[[]low.ValueReference[*yaml.Node]]{ KeyNode: &yaml.Node{Line: 10, Column: 20}, ValueNode: &yaml.Node{}, Value: []low.ValueReference[*yaml.Node]{ {Value: &yaml.Node{Value: "enum1"}}, {Value: &yaml.Node{Value: "enum2"}}, }, } param := &lowv3.Parameter{ Name: low.NodeReference[string]{Value: "testParam"}, Schema: low.NodeReference[*lowbase.SchemaProxy]{Value: schemaProxy}, Style: low.NodeReference[string]{Value: "form", KeyNode: &yaml.Node{Line: 15, Column: 25}, ValueNode: &yaml.Node{}}, Explode: low.NodeReference[bool]{Value: false, ValueNode: &yaml.Node{Line: 18, Column: 30}, KeyNode: &yaml.Node{}}, Required: low.NodeReference[bool]{ KeyNode: &yaml.Node{Line: 22, Column: 32}, ValueNode: &yaml.Node{}, }, KeyNode: &yaml.Node{}, } return v3.NewParameter(param) } func TestIncorrectFormEncoding(t *testing.T) { param := createMockParameterWithSchema() qp := &helpers.QueryParam{ Key: "testParam", Values: []string{"incorrect,value"}, } // Call the function err := IncorrectFormEncoding(param, qp, 0) // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testParam", err.ParameterName) require.Contains(t, err.Message, "Query parameter 'testParam' is not exploded correctly") require.Contains(t, err.Reason, "'testParam' has a default or 'form' encoding defined") require.Equal(t, 18, err.SpecLine) require.Equal(t, 30, err.SpecCol) require.Contains(t, err.HowToFix, "&testParam=incorrect&testParam=value'") } func TestIncorrectSpaceDelimiting(t *testing.T) { param := createMockParameterWithSchema() qp := &helpers.QueryParam{ Key: "testParam", Values: []string{"value1", "value2"}, } // create a low level query parameter // Call the function err := IncorrectSpaceDelimiting(param, qp) // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testParam", err.ParameterName) require.Contains(t, err.Message, "Query parameter 'testParam' delimited incorrectly") require.Contains(t, err.Reason, "'spaceDelimited' style defined") require.Contains(t, err.HowToFix, "testParam=value1%20value2") } func TestIncorrectPipeDelimiting(t *testing.T) { param := createMockParameterWithSchema() qp := &helpers.QueryParam{ Key: "testParam", Values: []string{"value1", "value2"}, } // Call the function err := IncorrectPipeDelimiting(param, qp) // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testParam", err.ParameterName) require.Contains(t, err.Message, "Query parameter 'testParam' delimited incorrectly") require.Contains(t, err.Reason, "'pipeDelimited' style defined") require.Contains(t, err.HowToFix, "testParam=value1|value2") } func TestQueryParameterMissing(t *testing.T) { param := createMockParameterWithSchema() // Call the function err := QueryParameterMissing(param, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testParam", err.ParameterName) require.Contains(t, err.Message, "Query parameter 'testParam' is missing") require.Contains(t, err.Reason, "'testParam' is defined as being required") require.Equal(t, HowToFixMissingValue, err.HowToFix) } func TestHeaderParameterMissing(t *testing.T) { param := createMockParameterWithSchema() // Call the function err := HeaderParameterMissing(param, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationHeader, err.ValidationSubType) require.Equal(t, "testParam", err.ParameterName) require.Contains(t, err.Message, "Header parameter 'testParam' is missing") require.Contains(t, err.Reason, "'testParam' is defined as being required") require.Equal(t, HowToFixMissingValue, err.HowToFix) } func TestCookieParameterMissing(t *testing.T) { param := createMockParameterWithSchema() // Call the function err := CookieParameterMissing(param, "/test", "get", "") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationCookie, err.ValidationSubType) require.Equal(t, "testParam", err.ParameterName) require.Contains(t, err.Message, "Cookie parameter 'testParam' is missing") require.Contains(t, err.Reason, "'testParam' is defined as being required") require.Equal(t, HowToFixMissingValue, err.HowToFix) } func TestHeaderParameterCannotBeDecoded(t *testing.T) { param := createMockParameterWithSchema() val := "malformed_header_value" // Call the function err := HeaderParameterCannotBeDecoded(param, val, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationHeader, err.ValidationSubType) require.Equal(t, "testParam", err.ParameterName) require.Contains(t, err.Message, "Header parameter 'testParam' cannot be decoded") require.Contains(t, err.Reason, "'malformed_header_value' is malformed") require.Equal(t, HowToFixInvalidEncoding, err.HowToFix) } func TestIncorrectHeaderParamEnum(t *testing.T) { param := createMockParameterWithSchema() schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), &yaml.Node{}, &yaml.Node{}, nil)) schemaProxy.Schema().Type = low.NodeReference[lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]]{ KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, Value: lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]{ A: "string", }, } schemaProxy.Schema().Enum = low.NodeReference[[]low.ValueReference[*yaml.Node]]{ KeyNode: &yaml.Node{Line: 10, Column: 20}, ValueNode: &yaml.Node{}, Value: []low.ValueReference[*yaml.Node]{ {Value: &yaml.Node{Value: "enum1"}}, {Value: &yaml.Node{Value: "enum2"}}, }, } s := schemaProxy.Schema() // build a high level schema from the low level one schema := base.NewSchema(s) // Call the function with an invalid enum value err := IncorrectHeaderParamEnum(param, "invalidEnum", schema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationHeader, err.ValidationSubType) require.Equal(t, "testParam", err.ParameterName) require.Contains(t, err.Message, "Header parameter 'testParam' does not match allowed values") require.Contains(t, err.Reason, "'invalidEnum' is not one of those values") require.Equal(t, 10, err.SpecLine) require.Equal(t, 20, err.SpecCol) require.Contains(t, err.HowToFix, "enum1, enum2") } func TestIncorrectQueryParamArrayBoolean(t *testing.T) { param := createMockParameterWithSchema() schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), &yaml.Node{}, &yaml.Node{}, nil)) schemaProxy.Schema().Type = low.NodeReference[lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]]{ KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, Value: lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]{ A: "string", }, } schemaProxy.Schema().Items = low.NodeReference[*lowbase.SchemaDynamicValue[*lowbase.SchemaProxy, bool]]{ KeyNode: &yaml.Node{Line: 30, Column: 40}, ValueNode: &yaml.Node{}, Value: &lowbase.SchemaDynamicValue[*lowbase.SchemaProxy, bool]{A: schemaProxy}, } s := schemaProxy.Schema() // build a high level schema from the low level one schema := base.NewSchema(s) // Call the function with an invalid boolean value in the array err := IncorrectQueryParamArrayBoolean(param, "notBoolean", schema, schema.Items.A.Schema(), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testParam", err.ParameterName) require.Contains(t, err.Message, "Query array parameter 'testParam' is not a valid boolean") require.Contains(t, err.Reason, "the value 'notBoolean' is not a valid true/false value") require.Contains(t, err.HowToFix, "true/false") } // Helper function to create a mock v3.Parameter with deepObject style func createMockParameterWithDeepObjectStyle() *v3.Parameter { param := &lowv3.Parameter{ Name: low.NodeReference[string]{Value: "testParam"}, Style: low.NodeReference[string]{Value: "deepObject", KeyNode: &yaml.Node{Line: 12, Column: 22}, ValueNode: &yaml.Node{}}, // Correct ValueNode set Explode: low.NodeReference[bool]{Value: false}, } return v3.NewParameter(param) } func TestInvalidDeepObject(t *testing.T) { param := createMockParameterWithDeepObjectStyle() // Create a mock query parameter with multiple values qp := &helpers.QueryParam{ Key: "testParam", Values: []string{"value1", "value2"}, } // Call the function err := InvalidDeepObject(param, qp) // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testParam", err.ParameterName) require.Contains(t, err.Message, "Query parameter 'testParam' is not a valid deepObject") require.Contains(t, err.Reason, "'testParam' has the 'deepObject' style defined") require.Contains(t, err.HowToFix, "testParam=value1|value2") } func TestInvalidDeepObjectPathConflict(t *testing.T) { param := createMockParameterWithDeepObjectStyle() prefixParam := &helpers.QueryParam{ Key: "testParam", Values: []string{"bad"}, Property: "nested", PropertyPath: []string{"nested"}, } nestedParam := &helpers.QueryParam{ Key: "testParam", Values: []string{"ok"}, Property: "nested", PropertyPath: []string{"nested", "child"}, } err := InvalidDeepObjectPathConflict(param, prefixParam, nestedParam) require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testParam", err.ParameterName) require.Contains(t, err.Message, "Query parameter 'testParam' is not a valid deepObject") require.Contains(t, err.Reason, "property path 'nested'") require.Contains(t, err.Reason, "'nested.child'") require.Contains(t, err.HowToFix, "testParam[nested]") require.Contains(t, err.HowToFix, "testParam[nested][child]") } func TestInvalidDeepObjectPathConflict_NilPaths(t *testing.T) { param := createMockParameterWithDeepObjectStyle() err := InvalidDeepObjectPathConflict(param, nil, nil) require.NotNil(t, err) require.Contains(t, err.Reason, "property path ''") require.Contains(t, err.HowToFix, "testParam[]") } func TestInvalidDeepObjectPathConflict_PropertyFallback(t *testing.T) { param := createMockParameterWithDeepObjectStyle() prefixParam := &helpers.QueryParam{ Key: "testParam", Values: []string{"bad"}, Property: "nested", } nestedParam := &helpers.QueryParam{ Key: "testParam", Values: []string{"ok"}, Property: "nested.child", } err := InvalidDeepObjectPathConflict(param, prefixParam, nestedParam) require.NotNil(t, err) require.Contains(t, err.Reason, "property path 'nested'") require.Contains(t, err.Reason, "'nested.child'") require.Contains(t, err.HowToFix, "testParam[nested]") require.Contains(t, err.HowToFix, "testParam[nested.child]") } func createMockParameterForBooleanArray() *v3.Parameter { param := &lowv3.Parameter{ Name: low.NodeReference[string]{Value: "testCookieParam"}, } return v3.NewParameter(param) } // Helper function to create a mock base.Schema with boolean items schema func createMockLowBaseSchemaForBooleanArray() *lowbase.Schema { itemsSchema := &lowbase.Schema{ Type: low.NodeReference[lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]]{ Value: lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]{ A: "boolean", }, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, } schemaProxy := &lowbase.SchemaProxy{} itemsSchema.Items = low.NodeReference[*lowbase.SchemaDynamicValue[*lowbase.SchemaProxy, bool]]{ Value: &lowbase.SchemaDynamicValue[*lowbase.SchemaProxy, bool]{ A: schemaProxy, }, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, } _ = schemaProxy.Build(context.Background(), &yaml.Node{}, &yaml.Node{}, nil) schemaProxy.Schema().Type = low.NodeReference[lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]]{ KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, Value: lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]{ A: "string", }, } return itemsSchema } func TestIncorrectCookieParamArrayBoolean(t *testing.T) { // Create mock parameter and schemas param := createMockParameterForBooleanArray() baseSchema := createMockLowBaseSchemaForBooleanArray() s := base.NewSchema(baseSchema) itemsSchema := base.NewSchema(baseSchema.Items.Value.A.Schema()) // Call the function with an invalid boolean value in the array err := IncorrectCookieParamArrayBoolean(param, "notBoolean", s, itemsSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationCookie, err.ValidationSubType) require.Equal(t, "testCookieParam", err.ParameterName) require.Contains(t, err.Message, "Cookie array parameter 'testCookieParam' is not a valid boolean") require.Contains(t, err.Reason, "the value 'notBoolean' is not a valid true/false value") require.Contains(t, err.HowToFix, "true/false") } // Helper function to create a mock v3.Parameter for number array validation func createMockParameterForNumberArray() *v3.Parameter { param := &lowv3.Parameter{ Name: low.NodeReference[string]{Value: "testQueryParam"}, } return v3.NewParameter(param) } // Helper function to create a mock base.Schema with number items schema func createMockLowBaseSchemaForNumberArray() *lowbase.Schema { itemsSchema := &lowbase.Schema{ Type: low.NodeReference[lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]]{ Value: lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]{ A: "number", }, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, } schemaProxy := &lowbase.SchemaProxy{} itemsSchema.Items = low.NodeReference[*lowbase.SchemaDynamicValue[*lowbase.SchemaProxy, bool]]{ Value: &lowbase.SchemaDynamicValue[*lowbase.SchemaProxy, bool]{ A: schemaProxy, }, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, } _ = schemaProxy.Build(context.Background(), &yaml.Node{}, &yaml.Node{}, nil) schemaProxy.Schema().Type = low.NodeReference[lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]]{ KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, Value: lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]{ A: "string", }, } return itemsSchema } func TestIncorrectQueryParamArrayInteger(t *testing.T) { // Create mock parameter and schemas param := createMockParameterForNumberArray() baseSchema := createMockLowBaseSchemaForNumberArray() s := base.NewSchema(baseSchema) itemsSchema := base.NewSchema(baseSchema.Items.Value.A.Schema()) // Call the function with an invalid number value in the array err := IncorrectQueryParamArrayInteger(param, "notNumber", s, itemsSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Query array parameter 'testQueryParam' is not a valid integer") require.Contains(t, err.Reason, "the value 'notNumber' is not a valid integer") require.Contains(t, err.HowToFix, "notNumber") } func TestIncorrectQueryParamArrayNumber(t *testing.T) { // Create mock parameter and schemas param := createMockParameterForNumberArray() baseSchema := createMockLowBaseSchemaForNumberArray() s := base.NewSchema(baseSchema) itemsSchema := base.NewSchema(baseSchema.Items.Value.A.Schema()) // Call the function with an invalid number value in the array err := IncorrectQueryParamArrayNumber(param, "notNumber", s, itemsSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Query array parameter 'testQueryParam' is not a valid number") require.Contains(t, err.Reason, "the value 'notNumber' is not a valid number") require.Contains(t, err.HowToFix, "notNumber") } // Helper function to create a mock v3.Parameter for cookie number array validation func createMockParameterForCookieNumberArray() *v3.Parameter { param := &lowv3.Parameter{ Name: low.NodeReference[string]{Value: "testCookieParam"}, } return v3.NewParameter(param) } // Helper function to create a mock base.Schema with number items schema func createMockLowBaseSchemaForCookieNumberArray() *lowbase.Schema { itemsSchema := &lowbase.Schema{ Type: low.NodeReference[lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]]{ Value: lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]{ A: "number", }, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, } schemaProxy := &lowbase.SchemaProxy{} itemsSchema.Items = low.NodeReference[*lowbase.SchemaDynamicValue[*lowbase.SchemaProxy, bool]]{ Value: &lowbase.SchemaDynamicValue[*lowbase.SchemaProxy, bool]{ A: schemaProxy, }, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, } _ = schemaProxy.Build(context.Background(), &yaml.Node{}, &yaml.Node{}, nil) schemaProxy.Schema().Type = low.NodeReference[lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]]{ KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, Value: lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]{ A: "string", }, } return itemsSchema } func TestIncorrectCookieParamArrayNumber(t *testing.T) { // Create mock parameter and schemas param := createMockParameterForCookieNumberArray() baseSchema := createMockLowBaseSchemaForCookieNumberArray() s := base.NewSchema(baseSchema) itemsSchema := base.NewSchema(baseSchema.Items.Value.A.Schema()) // Call the function with an invalid number value in the cookie array err := IncorrectCookieParamArrayNumber(param, "notNumber", s, itemsSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationCookie, err.ValidationSubType) require.Equal(t, "testCookieParam", err.ParameterName) require.Contains(t, err.Message, "Cookie array parameter 'testCookieParam' is not a valid number") require.Contains(t, err.Reason, "the value 'notNumber' is not a valid number") require.Contains(t, err.HowToFix, "notNumber") } // Helper function to create a mock v3.Parameter func createMockParameter() *v3.Parameter { schemaProxy := &lowbase.SchemaProxy{} _ = schemaProxy.Build(context.Background(), &yaml.Node{}, &yaml.Node{}, nil) m := orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]() m.Set(low.KeyReference[string]{Value: "application/json"}, low.ValueReference[*lowv3.MediaType]{ValueNode: &yaml.Node{}, Value: &lowv3.MediaType{}}) param := &lowv3.Parameter{ Name: low.NodeReference[string]{Value: "testQueryParam"}, Content: low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]]{ Value: m, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, Schema: low.NodeReference[*lowbase.SchemaProxy]{ Value: schemaProxy, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, Required: low.NodeReference[bool]{ KeyNode: &yaml.Node{}, }, } return v3.NewParameter(param) } // Helper function to create a mock base.Schema func createMockLowBaseSchema() *lowbase.Schema { itemsSchema := &lowbase.Schema{ Type: low.NodeReference[lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]]{ Value: lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]{ A: "boolean", }, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, } schemaProxy := &lowbase.SchemaProxy{} itemsSchema.Items = low.NodeReference[*lowbase.SchemaDynamicValue[*lowbase.SchemaProxy, bool]]{ Value: &lowbase.SchemaDynamicValue[*lowbase.SchemaProxy, bool]{ A: schemaProxy, }, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, } _ = schemaProxy.Build(context.Background(), &yaml.Node{}, &yaml.Node{}, nil) schemaProxy.Schema().Type = low.NodeReference[lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]]{ KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, Value: lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]{ A: "string", }, } return itemsSchema } func TestIncorrectParamEncodingJSON(t *testing.T) { param := createMockParameter() baseSchema := createMockLowBaseSchema() // Call the function with an invalid JSON value err := IncorrectParamEncodingJSON(param, "invalidJSON", base.NewSchema(baseSchema), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Query parameter 'testQueryParam' is not valid JSON") require.Contains(t, err.Reason, "the value 'invalidJSON' is not valid JSON") require.Equal(t, HowToFixInvalidJSON, err.HowToFix) } func TestIncorrectQueryParamBool(t *testing.T) { param := createMockParameter() baseSchema := createMockLowBaseSchema() lschemaProxy := &lowbase.SchemaProxy{} require.NoError(t, lschemaProxy.Build(context.Background(), &yaml.Node{}, &yaml.Node{}, nil)) lschemaProxy.Schema().Type = low.NodeReference[lowbase.SchemaDynamicValue[string, []low.ValueReference[string]]]{ KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, } param.GoLow().Schema.KeyNode = &yaml.Node{} param.Schema = base.NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{ Value: lschemaProxy, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }) // Call the function with an invalid boolean value err := IncorrectQueryParamBool(param, "notBoolean", base.NewSchema(baseSchema), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Query parameter 'testQueryParam' is not a valid boolean") require.Contains(t, err.Reason, "the value 'notBoolean' is not a valid boolean") require.Contains(t, err.HowToFix, "true/false") } func TestInvalidQueryParamNumber(t *testing.T) { param := createMockParameter() baseSchema := createMockLowBaseSchema() // Call the function with an invalid number value err := InvalidQueryParamNumber(param, "notNumber", base.NewSchema(baseSchema), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Query parameter 'testQueryParam' is not a valid number") require.Contains(t, err.Reason, "the value 'notNumber' is not a valid number") require.Contains(t, err.HowToFix, "notNumber") } func TestInvalidQueryParamInteger(t *testing.T) { param := createMockParameter() baseSchema := createMockLowBaseSchema() // Call the function with an invalid number value err := InvalidQueryParamInteger(param, "notNumber", base.NewSchema(baseSchema), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Query parameter 'testQueryParam' is not a valid integer") require.Contains(t, err.Reason, "the value 'notNumber' is not a valid integer") require.Contains(t, err.HowToFix, "notNumber") } func TestIncorrectQueryParamEnum(t *testing.T) { enum := `enum: [fish, crab, lobster]` var n yaml.Node _ = yaml.Unmarshal([]byte(enum), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.Value.Schema().Enum.KeyNode = &yaml.Node{} // Call the function with an invalid enum value err := IncorrectQueryParamEnum(param, "invalidEnum", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Query parameter 'testQueryParam' does not match allowed values") require.Contains(t, err.Reason, "'invalidEnum' is not one of those values") require.Contains(t, err.HowToFix, "fish, crab, lobster") } func TestIncorrectQueryParamEnumArray(t *testing.T) { enum := `items: enum: [fish, crab, lobster]` var n yaml.Node _ = yaml.Unmarshal([]byte(enum), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.Value = schemaProxy param.GoLow().Schema.Value.Schema().Items.Value.A.Schema().Enum.Value = []low.ValueReference[*yaml.Node]{ {Value: &yaml.Node{Value: "fish, crab, lobster"}}, } // Call the function with an invalid enum value err := IncorrectQueryParamEnumArray(param, "invalidEnum", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Query array parameter 'testQueryParam' does not match allowed values") require.Contains(t, err.Reason, "'invalidEnum' is not one of those values") require.Contains(t, err.HowToFix, "fish, crab, lobster") } func TestIncorrectReservedValues(t *testing.T) { enum := `name: bork` var n yaml.Node _ = yaml.Unmarshal([]byte(enum), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Name = "borked::?^&*" err := IncorrectReservedValues(param, "borked::?^&*", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "borked::?^&*", err.ParameterName) require.Contains(t, err.Message, "Query parameter 'borked::?^&*' value contains reserved values") require.Contains(t, err.Reason, "The query parameter 'borked::?^&*' has 'allowReserved' set to false") require.Contains(t, err.HowToFix, "borked%3A%3A%3F%5E%26%2A") } func TestInvalidHeaderParamInteger(t *testing.T) { enum := `name: blip` var n yaml.Node _ = yaml.Unmarshal([]byte(enum), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Name = "bunny" err := InvalidHeaderParamInteger(param, "bunmy", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationHeader, err.ValidationSubType) require.Equal(t, "bunny", err.ParameterName) require.Contains(t, err.Message, "Header parameter 'bunny' is not a valid integer") require.Contains(t, err.Reason, "The header parameter 'bunny' is defined as being an integer") require.Contains(t, err.HowToFix, "bunmy") } func TestInvalidHeaderParamNumber(t *testing.T) { enum := `name: blip` var n yaml.Node _ = yaml.Unmarshal([]byte(enum), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Name = "bunny" err := InvalidHeaderParamNumber(param, "bunmy", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationHeader, err.ValidationSubType) require.Equal(t, "bunny", err.ParameterName) require.Contains(t, err.Message, "Header parameter 'bunny' is not a valid number") require.Contains(t, err.Reason, "The header parameter 'bunny' is defined as being a number") require.Contains(t, err.HowToFix, "bunmy") } func TestInvalidCookieParamNumber(t *testing.T) { enum := `name: blip` var n yaml.Node _ = yaml.Unmarshal([]byte(enum), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Name = "cookies" err := InvalidCookieParamNumber(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationCookie, err.ValidationSubType) require.Equal(t, "cookies", err.ParameterName) require.Contains(t, err.Message, "Cookie parameter 'cookies' is not a valid number") require.Contains(t, err.Reason, "The cookie parameter 'cookies' is defined as being a number") require.Contains(t, err.HowToFix, "milky") } func TestInvalidCookieParamInteger(t *testing.T) { enum := `name: blip` var n yaml.Node _ = yaml.Unmarshal([]byte(enum), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Name = "cookies" err := InvalidCookieParamInteger(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationCookie, err.ValidationSubType) require.Equal(t, "cookies", err.ParameterName) require.Contains(t, err.Message, "Cookie parameter 'cookies' is not a valid integer") require.Contains(t, err.Reason, "The cookie parameter 'cookies' is defined as being an integer") require.Contains(t, err.HowToFix, "milky") } func TestIncorrectHeaderParamBool(t *testing.T) { enum := `name: blip` var n yaml.Node _ = yaml.Unmarshal([]byte(enum), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Name = "cookies" err := IncorrectHeaderParamBool(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationHeader, err.ValidationSubType) require.Equal(t, "cookies", err.ParameterName) require.Contains(t, err.Message, "Header parameter 'cookies' is not a valid boolean") require.Contains(t, err.Reason, "The header parameter 'cookies' is defined as being a boolean") require.Contains(t, err.HowToFix, "milky") } func TestIncorrectCookieParamBool(t *testing.T) { enum := `name: blip` var n yaml.Node _ = yaml.Unmarshal([]byte(enum), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Name = "cookies" err := IncorrectCookieParamBool(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationCookie, err.ValidationSubType) require.Equal(t, "cookies", err.ParameterName) require.Contains(t, err.Message, "Cookie parameter 'cookies' is not a valid boolean") require.Contains(t, err.Reason, "The cookie parameter 'cookies' is defined as being a boolean") require.Contains(t, err.HowToFix, "milky") } func TestIncorrectCookieParamEnum(t *testing.T) { enum := `enum: [fish, crab, lobster] items: enum: [fish, crab, lobster]` var n yaml.Node _ = yaml.Unmarshal([]byte(enum), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.Value = schemaProxy param.GoLow().Schema.Value.Schema().Enum.Value = []low.ValueReference[*yaml.Node]{ {Value: &yaml.Node{Value: "fish, crab, lobster"}}, } param.GoLow().Schema.Value.Schema().Enum.KeyNode = &yaml.Node{} err := IncorrectCookieParamEnum(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationCookie, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Cookie parameter 'testQueryParam' does not match allowed values") require.Contains(t, err.Reason, "The cookie parameter 'testQueryParam' has pre-defined values set via an enum") require.Contains(t, err.HowToFix, "milky") } func TestIncorrectHeaderParamArrayBoolean(t *testing.T) { items := `items: type: boolean` var n yaml.Node _ = yaml.Unmarshal([]byte(items), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) highSchema.GoLow().Items.Value.A.Schema() param := createMockParameter() param.Name = "bubbles" err := IncorrectHeaderParamArrayBoolean(param, "milky", highSchema, nil, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationHeader, err.ValidationSubType) require.Equal(t, "bubbles", err.ParameterName) require.Contains(t, err.Message, "Header array parameter 'bubbles' is not a valid boolean") require.Contains(t, err.Reason, "The header parameter (which is an array) 'bubbles' is defined as being a boolean") require.Contains(t, err.HowToFix, "milky") } func TestIncorrectHeaderParamArrayNumber(t *testing.T) { items := `items: type: number` var n yaml.Node _ = yaml.Unmarshal([]byte(items), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) highSchema.GoLow().Items.Value.A.Schema() param := createMockParameter() param.Name = "bubbles" err := IncorrectHeaderParamArrayNumber(param, "milky", highSchema, nil, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationHeader, err.ValidationSubType) require.Equal(t, "bubbles", err.ParameterName) require.Contains(t, err.Message, "Header array parameter 'bubbles' is not a valid number") require.Contains(t, err.Reason, "The header parameter (which is an array) 'bubbles' is defined as being a number") require.Contains(t, err.HowToFix, "milky") } func TestIncorrectPathParamBool(t *testing.T) { items := `items: type: number` var n yaml.Node _ = yaml.Unmarshal([]byte(items), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} err := IncorrectPathParamBool(param, "milky", highSchema, "/test-path", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationPath, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Path parameter 'testQueryParam' is not a valid boolean") require.Contains(t, err.Reason, "The path parameter 'testQueryParam' is defined as being a boolean") require.Contains(t, err.HowToFix, "milky") } func TestIncorrectPathParamEnum(t *testing.T) { items := `enum: [fish, crab, lobster] items: enum: [fish, crab, lobster]` var n yaml.Node _ = yaml.Unmarshal([]byte(items), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.Value = schemaProxy param.GoLow().Schema.Value.Schema().Enum.Value = []low.ValueReference[*yaml.Node]{ {Value: &yaml.Node{Value: "fish, crab, lobster"}}, } param.GoLow().Schema.Value.Schema().Enum.KeyNode = &yaml.Node{} err := IncorrectPathParamEnum(param, "milky", highSchema, "/test-path", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationPath, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Path parameter 'testQueryParam' does not match allowed values") require.Contains(t, err.Reason, "The path parameter 'testQueryParam' has pre-defined values set via an enum") require.Contains(t, err.HowToFix, "milky") } func TestIncorrectPathParamNumber(t *testing.T) { items := `items: type: number` var n yaml.Node _ = yaml.Unmarshal([]byte(items), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} err := IncorrectPathParamNumber(param, "milky", highSchema, "/test-path", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationPath, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Path parameter 'testQueryParam' is not a valid number") require.Contains(t, err.Reason, "The path parameter 'testQueryParam' is defined as being a number") require.Contains(t, err.HowToFix, "milky") } func TestIncorrectPathParamInteger(t *testing.T) { items := `items: type: integer` var n yaml.Node _ = yaml.Unmarshal([]byte(items), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} err := IncorrectPathParamInteger(param, "milky", highSchema, "/test-path", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationPath, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Path parameter 'testQueryParam' is not a valid integer") require.Contains(t, err.Reason, "The path parameter 'testQueryParam' is defined as being an integer") require.Contains(t, err.HowToFix, "milky") } func TestIncorrectPathParamArrayNumber(t *testing.T) { items := `items: type: number` var n yaml.Node _ = yaml.Unmarshal([]byte(items), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) highSchema.GoLow().Items.Value.A.Schema() param := createMockParameter() param.Name = "bubbles" err := IncorrectPathParamArrayNumber(param, "milky", highSchema, nil, "/test-path", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationPath, err.ValidationSubType) require.Equal(t, "bubbles", err.ParameterName) require.Contains(t, err.Message, "Path array parameter 'bubbles' is not a valid number") require.Contains(t, err.Reason, "The path parameter (which is an array) 'bubbles' is defined as being a number") require.Contains(t, err.HowToFix, "milky") } func TestIncorrectPathParamArrayInteger(t *testing.T) { items := `items: type: integer` var n yaml.Node _ = yaml.Unmarshal([]byte(items), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) highSchema.GoLow().Items.Value.A.Schema() param := createMockParameter() param.Name = "bubbles" err := IncorrectPathParamArrayInteger(param, "milky", highSchema, nil, "/test-path", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationPath, err.ValidationSubType) require.Equal(t, "bubbles", err.ParameterName) require.Contains(t, err.Message, "Path array parameter 'bubbles' is not a valid integer") require.Contains(t, err.Reason, "The path parameter (which is an array) 'bubbles' is defined as being an integer") require.Contains(t, err.HowToFix, "milky") } func TestIncorrectPathParamArrayBoolean(t *testing.T) { items := `items: type: number` var n yaml.Node _ = yaml.Unmarshal([]byte(items), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) highSchema.GoLow().Items.Value.A.Schema() param := createMockParameter() param.Name = "bubbles" err := IncorrectPathParamArrayBoolean(param, "milky", highSchema, nil, "/test-path", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationPath, err.ValidationSubType) require.Equal(t, "bubbles", err.ParameterName) require.Contains(t, err.Message, "Path array parameter 'bubbles' is not a valid boolean") require.Contains(t, err.Reason, "The path parameter (which is an array) 'bubbles' is defined as being a boolean") require.Contains(t, err.HowToFix, "milky") } func TestPathParameterMissing(t *testing.T) { items := `required: - testQueryParam` var n yaml.Node _ = yaml.Unmarshal([]byte(items), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} err := PathParameterMissing(param, "/test/{testQueryParam}", "/test/") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationPath, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Path parameter 'testQueryParam' is missing") require.Contains(t, err.Reason, "The path parameter 'testQueryParam' is defined as being required") require.Contains(t, err.HowToFix, "Ensure the value has been set") } func TestPathParameterMaxItems(t *testing.T) { items := `maxItems: 5 items: type: string` var n yaml.Node _ = yaml.Unmarshal([]byte(items), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} err := IncorrectParamArrayMaxNumItems(param, param.Schema.Schema(), 10, 25, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Query array parameter 'testQueryParam' has too many items") require.Contains(t, err.Reason, "The query parameter (which is an array) 'testQueryParam' has a maximum item length of 10, however the request provided 25 items") require.Contains(t, err.HowToFix, "Reduce the number of items in the array to 10 or less") } func TestPathParameterMinItems(t *testing.T) { items := `minItems: 5 items: type: string` var n yaml.Node _ = yaml.Unmarshal([]byte(items), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} err := IncorrectParamArrayMinNumItems(param, param.Schema.Schema(), 10, 5, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Query array parameter 'testQueryParam' does not have enough items") require.Contains(t, err.Reason, "The query parameter (which is an array) 'testQueryParam' has a minimum items length of 10, however the request provided 5 items") require.Contains(t, err.HowToFix, "Increase the number of items in the array to 10 or more") } func TestPathParameterUniqueItems(t *testing.T) { items := `uniqueItems: true items: type: string` var n yaml.Node _ = yaml.Unmarshal([]byte(items), &n) schemaProxy := &lowbase.SchemaProxy{} require.NoError(t, schemaProxy.Build(context.Background(), n.Content[0], n.Content[0], nil)) highSchema := base.NewSchema(schemaProxy.Schema()) param := createMockParameter() param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} err := IncorrectParamArrayUniqueItems(param, param.Schema.Schema(), "fish, cake", "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ParameterValidation, err.ValidationType) require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) require.Equal(t, "testQueryParam", err.ParameterName) require.Contains(t, err.Message, "Query array parameter 'testQueryParam' contains non-unique items") require.Contains(t, err.Reason, "The query parameter (which is an array) 'testQueryParam' contains the following duplicates: 'fish, cake'") require.Contains(t, err.HowToFix, "Ensure the array values are all unique") } // createMinimalParameter creates a parameter with nil GoLow nodes to test nil safety. func createMinimalParameter() *v3.Parameter { param := &lowv3.Parameter{ Name: low.NodeReference[string]{Value: "minParam"}, // All node references intentionally left with nil KeyNode/ValueNode } return v3.NewParameter(param) } func TestParameterErrors_NilGoLowNodes(t *testing.T) { // Tests that all parameter error constructors handle nil GoLow nodes // without panicking. This covers the crash scenario from wiretap #134. param := createMinimalParameter() qp := &helpers.QueryParam{ Key: "minParam", Values: []string{"value"}, } sch := &base.Schema{} t.Run("IncorrectFormEncoding", func(t *testing.T) { err := IncorrectFormEncoding(param, qp, 0) require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectSpaceDelimiting", func(t *testing.T) { err := IncorrectSpaceDelimiting(param, qp) require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectPipeDelimiting", func(t *testing.T) { err := IncorrectPipeDelimiting(param, qp) require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("InvalidDeepObject", func(t *testing.T) { err := InvalidDeepObject(param, qp) require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("InvalidDeepObjectPathConflict", func(t *testing.T) { err := InvalidDeepObjectPathConflict(param, qp, &helpers.QueryParam{ Key: "test", Values: []string{"ok"}, Property: "foo", PropertyPath: []string{"foo", "bar"}, }) require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("QueryParameterMissing", func(t *testing.T) { err := QueryParameterMissing(param, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("HeaderParameterMissing", func(t *testing.T) { err := HeaderParameterMissing(param, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("CookieParameterMissing", func(t *testing.T) { err := CookieParameterMissing(param, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("HeaderParameterCannotBeDecoded", func(t *testing.T) { err := HeaderParameterCannotBeDecoded(param, "bad", "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectHeaderParamEnum", func(t *testing.T) { err := IncorrectHeaderParamEnum(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectQueryParamEnum", func(t *testing.T) { err := IncorrectQueryParamEnum(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectCookieParamEnum", func(t *testing.T) { err := IncorrectCookieParamEnum(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectPathParamEnum", func(t *testing.T) { err := IncorrectPathParamEnum(param, "bad", sch, "/test", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectQueryParamBool", func(t *testing.T) { err := IncorrectQueryParamBool(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("InvalidQueryParamInteger", func(t *testing.T) { err := InvalidQueryParamInteger(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("InvalidQueryParamNumber", func(t *testing.T) { err := InvalidQueryParamNumber(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectReservedValues", func(t *testing.T) { err := IncorrectReservedValues(param, "a:b", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("InvalidHeaderParamInteger", func(t *testing.T) { err := InvalidHeaderParamInteger(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("InvalidHeaderParamNumber", func(t *testing.T) { err := InvalidHeaderParamNumber(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("InvalidCookieParamInteger", func(t *testing.T) { err := InvalidCookieParamInteger(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("InvalidCookieParamNumber", func(t *testing.T) { err := InvalidCookieParamNumber(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectHeaderParamBool", func(t *testing.T) { err := IncorrectHeaderParamBool(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectCookieParamBool", func(t *testing.T) { err := IncorrectCookieParamBool(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectPathParamBool", func(t *testing.T) { err := IncorrectPathParamBool(param, "bad", sch, "/test", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectPathParamInteger", func(t *testing.T) { err := IncorrectPathParamInteger(param, "bad", sch, "/test", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectPathParamNumber", func(t *testing.T) { err := IncorrectPathParamNumber(param, "bad", sch, "/test", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectParamEncodingJSON", func(t *testing.T) { err := IncorrectParamEncodingJSON(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectQueryParamEnumArray", func(t *testing.T) { err := IncorrectQueryParamEnumArray(param, "bad", sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("PathParameterMissing", func(t *testing.T) { err := PathParameterMissing(param, "/test/{id}", "/test/123") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) } func TestParameterErrors_NilSchemaItems(t *testing.T) { // Tests array parameter error constructors with nil Items in schema. param := createMinimalParameter() sch := &base.Schema{} // no Items set t.Run("IncorrectQueryParamArrayBoolean", func(t *testing.T) { err := IncorrectQueryParamArrayBoolean(param, "bad", sch, sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectParamArrayMaxNumItems", func(t *testing.T) { err := IncorrectParamArrayMaxNumItems(param, sch, 5, 10, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectParamArrayMinNumItems", func(t *testing.T) { err := IncorrectParamArrayMinNumItems(param, sch, 5, 2, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectParamArrayUniqueItems", func(t *testing.T) { err := IncorrectParamArrayUniqueItems(param, sch, "dup", "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectCookieParamArrayBoolean", func(t *testing.T) { err := IncorrectCookieParamArrayBoolean(param, "bad", sch, sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectQueryParamArrayInteger", func(t *testing.T) { err := IncorrectQueryParamArrayInteger(param, "bad", sch, sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectQueryParamArrayNumber", func(t *testing.T) { err := IncorrectQueryParamArrayNumber(param, "bad", sch, sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectCookieParamArrayNumber", func(t *testing.T) { err := IncorrectCookieParamArrayNumber(param, "bad", sch, sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectHeaderParamArrayBoolean", func(t *testing.T) { err := IncorrectHeaderParamArrayBoolean(param, "bad", sch, sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectHeaderParamArrayNumber", func(t *testing.T) { err := IncorrectHeaderParamArrayNumber(param, "bad", sch, sch, "/test", "get", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectPathParamArrayNumber", func(t *testing.T) { err := IncorrectPathParamArrayNumber(param, "bad", sch, sch, "/test", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectPathParamArrayInteger", func(t *testing.T) { err := IncorrectPathParamArrayInteger(param, "bad", sch, sch, "/test", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) t.Run("IncorrectPathParamArrayBoolean", func(t *testing.T) { err := IncorrectPathParamArrayBoolean(param, "bad", sch, sch, "/test", "{}") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) }) } libopenapi-validator-0.13.8/errors/parameters_howtofix.go000066400000000000000000000103121520534042400236410ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package errors const ( HowToFixReservedValues string = "parameter values need to URL Encoded to ensure reserved " + "values are correctly encoded, for example: '%s'" HowToFixParamInvalidInteger string = "Convert the value '%s' into an integer" HowToFixParamInvalidNumber string = "Convert the value '%s' into a number" HowToFixParamInvalidString string = "Convert the value '%s' into a string (cannot start with a number, or be a floating point)" HowToFixParamInvalidBoolean string = "Convert the value '%s' into a true/false value" HowToFixParamInvalidEnum string = "Instead of '%s', use one of the allowed values: '%s'" HowToFixParamInvalidFormEncode string = "Use a form style encoding for parameter values, for example: '%s'" HowToFixInvalidXml string = "Ensure xml is well-formed and matches schema structure" HowToFixXmlPrefix string = "Make sure to prepend the correct prefix '%s' to the declared fields" HowToFixXmlNamespace string = "Make sure to declare the 'xmlns:%s' with the correct namespace URI" HowToFixFormDataReservedCharacters string = "Make sure to correcly encode specials characters to percent encoding, or set allowReserved to true" HowToFixInvalidSchema string = "Ensure that the object being submitted, matches the schema correctly" HowToFixInvalidTypeEncoding string = "Ensure that the object being submitted matches the property encoding Content-Type" HowToFixParamInvalidSpaceDelimitedObjectExplode string = "When using 'explode' with space delimited parameters, " + "they should be separated by spaces. For example: '%s'" HowToFixParamInvalidPipeDelimitedObjectExplode string = "When using 'explode' with pipe delimited parameters, " + "they should be separated by pipes '|'. For example: '%s'" HowToFixParamInvalidDeepObjectMultipleValues string = "There can only be a single value per property name, " + "deepObject parameters should contain the property key in square brackets next to the parameter name. For example: '%s'" HowToFixParamInvalidDeepObjectPathConflict string = "Use either '%s[%s]' or nested properties like '%s[%s]', not both" HowToFixInvalidJSON string = "The JSON submitted is invalid, please check the syntax" HowToFixInvalidUrlEncoded string = "Ensure URL Encoded submitted is well-formed and matches schema structure" HowToFixDecodingError string = "The object can't be decoded, so make sure it's being encoded correctly according to the spec." HowToFixInvalidContentType string = "The content type is invalid, Use one of the %d supported types for this operation: %s" HowToFixInvalidResponseCode string = "The service is responding with a code that is not defined in the spec, fix the service or add the code to the specification" HowToFixInvalidEncoding string = "Ensure the correct encoding has been used on the object" HowToFixMissingValue string = "Ensure the value has been set" HowToFixPath string = "Check the path is correct, and check that the correct HTTP method has been used (e.g. GET, POST, PUT, DELETE)" HowToFixPathMethod string = "Add the missing operation to the contract for the path" HowToFixInvalidMaxItems string = "Reduce the number of items in the array to %d or less" HowToFixInvalidMinItems string = "Increase the number of items in the array to %d or more" HowToFixMissingHeader string = "Make sure the service responding sets the required headers with this response code" HowToFixInvalidRenderedSchema string = "Check the request schema for circular references or invalid structures" HowToFixInvalidJsonSchema string = "Check the request schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs" ) libopenapi-validator-0.13.8/errors/request_errors.go000066400000000000000000000046311520534042400226420ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package errors import ( "fmt" "net/http" "strings" "github.com/pb33f/libopenapi/orderedmap" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/helpers" ) func RequestContentTypeNotFound(op *v3.Operation, request *http.Request, specPath string) *ValidationError { ct := request.Header.Get(helpers.ContentTypeHeader) var ctypes []string var contentMap *orderedmap.Map[string, *v3.MediaType] specLine, specCol := 1, 0 if op.RequestBody != nil { contentMap = op.RequestBody.Content for pair := orderedmap.First(op.RequestBody.Content); pair != nil; pair = pair.Next() { ctypes = append(ctypes, pair.Key()) } if low := op.RequestBody.GoLow(); low != nil && low.Content.KeyNode != nil { specLine = low.Content.KeyNode.Line specCol = low.Content.KeyNode.Column } } return &ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.RequestBodyContentType, Message: fmt.Sprintf("%s operation request content type '%s' does not exist", request.Method, ct), Reason: fmt.Sprintf("The content type '%s' of the %s request submitted has not "+ "been defined, it's an unknown type", ct, request.Method), SpecLine: specLine, SpecCol: specCol, Context: op, HowToFix: fmt.Sprintf(HowToFixInvalidContentType, orderedmap.Len(contentMap), strings.Join(ctypes, ", ")), RequestPath: request.URL.Path, RequestMethod: request.Method, SpecPath: specPath, } } func OperationNotFound(pathItem *v3.PathItem, request *http.Request, method string, specPath string) *ValidationError { specLine, specCol := 1, 0 if low := pathItem.GoLow(); low != nil && low.KeyNode != nil { specLine = low.KeyNode.Line specCol = low.KeyNode.Column } return &ValidationError{ ValidationType: helpers.RequestValidation, ValidationSubType: helpers.ValidationMissingOperation, Message: fmt.Sprintf("%s operation request content type '%s' does not exist", request.Method, method), Reason: fmt.Sprintf("The path was found, but there was no '%s' method found in the spec", request.Method), SpecLine: specLine, SpecCol: specCol, Context: pathItem, HowToFix: HowToFixPathMethod, RequestPath: request.URL.Path, RequestMethod: request.Method, SpecPath: specPath, } } libopenapi-validator-0.13.8/errors/request_errors_test.go000066400000000000000000000100641520534042400236760ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package errors import ( "net/http" "testing" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v4" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" "github.com/pb33f/libopenapi-validator/helpers" ) // Helper to create a mock v3.Operation object with a RequestBody func createMockOperationWithRequestBody() *v3.Operation { content := orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]() content.Set(low.KeyReference[string]{ Value: "application/json", }, low.ValueReference[*lowv3.MediaType]{ Value: &lowv3.MediaType{}, }) reqBody := &lowv3.RequestBody{ Content: low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]]{ Value: content, KeyNode: &yaml.Node{Line: 10, Column: 20}, ValueNode: &yaml.Node{}, }, } // Create a lowv3.Operation object op := &lowv3.Operation{ RequestBody: low.NodeReference[*lowv3.RequestBody]{ Value: reqBody, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, } // Create a new v3.Operation object from the low return v3.NewOperation(op) } // Helper to create a mock v3.PathItem object func createMockPathItem() *v3.PathItem { pathItem := &lowv3.PathItem{ KeyNode: &yaml.Node{Line: 15, Column: 25}, } return v3.NewPathItem(pathItem) } func TestRequestContentTypeNotFound(t *testing.T) { // Create a mock operation with request body content types op := createMockOperationWithRequestBody() // Create a mock request with an invalid content type request, _ := http.NewRequest(http.MethodPost, "/test", nil) request.Header.Set(helpers.ContentTypeHeader, "application/xml") // Call the function err := RequestContentTypeNotFound(op, request, "/test") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.RequestBodyValidation, err.ValidationType) require.Equal(t, helpers.RequestBodyContentType, err.ValidationSubType) require.Contains(t, err.Message, "'application/xml' does not exist") require.Contains(t, err.Reason, "The content type 'application/xml' of the POST request submitted has not been defined") require.Equal(t, 10, err.SpecLine) require.Equal(t, 20, err.SpecCol) require.Contains(t, err.HowToFix, "application/json") } func TestOperationNotFound(t *testing.T) { // Create a mock path item pathItem := createMockPathItem() // Create a mock request request, _ := http.NewRequest(http.MethodPatch, "/test", nil) // Call the function err := OperationNotFound(pathItem, request, http.MethodPatch, "/test") // Validate the error require.NotNil(t, err) require.Equal(t, helpers.RequestValidation, err.ValidationType) require.Equal(t, helpers.ValidationMissingOperation, err.ValidationSubType) require.Contains(t, err.Message, "'PATCH' does not exist") require.Contains(t, err.Reason, "there was no 'PATCH' method found in the spec") require.Equal(t, 15, err.SpecLine) require.Equal(t, 25, err.SpecCol) require.Equal(t, HowToFixPathMethod, err.HowToFix) } func TestRequestContentTypeNotFound_NilContentKeyNode(t *testing.T) { // RequestBody exists but has no content KeyNode — should not panic reqBody := &lowv3.RequestBody{ Content: low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]]{ Value: orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]](), // KeyNode intentionally nil }, } op := &lowv3.Operation{ RequestBody: low.NodeReference[*lowv3.RequestBody]{ Value: reqBody, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, } highOp := v3.NewOperation(op) request, _ := http.NewRequest(http.MethodPost, "/test", nil) request.Header.Set(helpers.ContentTypeHeader, "application/xml") // Should not panic err := RequestContentTypeNotFound(highOp, request, "/test") require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) } libopenapi-validator-0.13.8/errors/response_errors.go000066400000000000000000000055511520534042400230120ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package errors import ( "fmt" "net/http" "strings" "github.com/pb33f/libopenapi/orderedmap" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/helpers" ) func ResponseContentTypeNotFound(op *v3.Operation, request *http.Request, response *http.Response, code string, isDefault bool, ) *ValidationError { ct := response.Header.Get(helpers.ContentTypeHeader) mediaTypeString, _, _ := helpers.ExtractContentType(ct) var ctypes []string specLine, specCol := 1, 0 var contentMap *orderedmap.Map[string, *v3.MediaType] // check for a default type (applies to all codes without a match) if !isDefault { resp := op.Responses.Codes.GetOrZero(code) if resp != nil { for pair := orderedmap.First(resp.Content); pair != nil; pair = pair.Next() { ctypes = append(ctypes, pair.Key()) } contentMap = resp.Content if low := resp.GoLow(); low != nil && low.Content.KeyNode != nil { specLine = low.Content.KeyNode.Line specCol = low.Content.KeyNode.Column } } } else { if op.Responses.Default != nil { for pair := orderedmap.First(op.Responses.Default.Content); pair != nil; pair = pair.Next() { ctypes = append(ctypes, pair.Key()) } contentMap = op.Responses.Default.Content if low := op.Responses.Default.GoLow(); low != nil && low.Content.KeyNode != nil { specLine = low.Content.KeyNode.Line specCol = low.Content.KeyNode.Column } } } return &ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.RequestBodyContentType, Message: fmt.Sprintf("%s / %s operation response content type '%s' does not exist", request.Method, code, mediaTypeString), Reason: fmt.Sprintf("The content type '%s' of the %s response received has not "+ "been defined, it's an unknown type", mediaTypeString, request.Method), SpecLine: specLine, SpecCol: specCol, Context: op, HowToFix: fmt.Sprintf(HowToFixInvalidContentType, orderedmap.Len(contentMap), strings.Join(ctypes, ", ")), } } func ResponseCodeNotFound(op *v3.Operation, request *http.Request, code int) *ValidationError { specLine, specCol := 1, 0 if low := op.GoLow(); low != nil && low.Responses.KeyNode != nil { specLine = low.Responses.KeyNode.Line specCol = low.Responses.KeyNode.Column } return &ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.ResponseBodyResponseCode, Message: fmt.Sprintf("%s operation request response code '%d' does not exist", request.Method, code), Reason: fmt.Sprintf("The response code '%d' of the %s request submitted has not "+ "been defined, it's an unknown type", code, request.Method), SpecLine: specLine, SpecCol: specCol, Context: op, HowToFix: HowToFixInvalidResponseCode, } } libopenapi-validator-0.13.8/errors/response_errors_test.go000066400000000000000000000206571520534042400240550ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package errors import ( "net/http" "testing" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v4" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" "github.com/pb33f/libopenapi-validator/helpers" ) // Helper to create a mock v3.Operation object func createMockOperation() *v3.Operation { content := orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]() content.Set(low.KeyReference[string]{ Value: "application/json", }, low.ValueReference[*lowv3.MediaType]{ Value: &lowv3.MediaType{}, }) r := &lowv3.Response{ Content: low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]]{ Value: content, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, } // resp := v3.NewResponse(r) // create a lowv3.Responses object responses := &lowv3.Responses{ Default: low.NodeReference[*lowv3.Response]{ Value: r, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, Codes: orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.Response]](), KeyNode: &yaml.Node{}, } // create a lowv3.Operation object op := &lowv3.Operation{ Responses: low.NodeReference[*lowv3.Responses]{ Value: responses, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, } // create a new v3.Operation object from the low highOp := v3.NewOperation(op) return highOp } func TestResponseContentTypeNotFound_Default(t *testing.T) { // Create a mock operation with a default response and content type op := createMockOperation() op.Responses.Default.Content.Set("application/json", &v3.MediaType{}) op.Responses.Default.GoLow().Content.KeyNode.Line = 12 op.Responses.Default.GoLow().Content.KeyNode.Column = 34 // Create a mock request and response request, _ := http.NewRequest(http.MethodGet, "/test", nil) response := &http.Response{ Header: http.Header{ helpers.ContentTypeHeader: {"application/xml"}, }, } // Call the function err := ResponseContentTypeNotFound(op, request, response, "200", true) // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ResponseBodyValidation, err.ValidationType) require.Equal(t, helpers.RequestBodyContentType, err.ValidationSubType) require.Contains(t, err.Message, "'application/xml' does not exist") require.Contains(t, err.Reason, "The content type 'application/xml' of the GET response received has not been defined") require.Equal(t, 12, err.SpecLine) require.Equal(t, 34, err.SpecCol) require.Contains(t, err.HowToFix, "application/json") } func TestResponseContentTypeNotFound_SpecificCode(t *testing.T) { // Create a mock operation with a response code and content type op := createMockOperation() responseContent := orderedmap.New[string, *v3.MediaType]() responseContent.Set("application/json", &v3.MediaType{}) content := orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]() content.Set(low.KeyReference[string]{ Value: "application/json", }, low.ValueReference[*lowv3.MediaType]{ Value: &lowv3.MediaType{}, }) r := &lowv3.Response{ Content: low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]]{ Value: content, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, } resp := v3.NewResponse(r) op.Responses.Codes.Set("200", resp) op.Responses.Codes.GetOrZero("200").GoLow().Content.KeyNode.Line = 15 op.Responses.Codes.GetOrZero("200").GoLow().Content.KeyNode.Column = 42 // Create a mock request and response request, _ := http.NewRequest(http.MethodPost, "/test", nil) response := &http.Response{ Header: http.Header{ helpers.ContentTypeHeader: {"application/xml"}, }, } // Call the function err := ResponseContentTypeNotFound(op, request, response, "200", false) // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ResponseBodyValidation, err.ValidationType) require.Equal(t, helpers.RequestBodyContentType, err.ValidationSubType) require.Contains(t, err.Message, "'application/xml' does not exist") require.Contains(t, err.Reason, "The content type 'application/xml' of the POST response received has not been defined") require.Equal(t, 15, err.SpecLine) require.Equal(t, 42, err.SpecCol) require.Contains(t, err.HowToFix, "application/json") } func TestResponseCodeNotFound(t *testing.T) { // Create a mock operation with responses op := createMockOperation() op.GoLow().Responses.KeyNode.Line = 22 op.GoLow().Responses.KeyNode.Column = 56 // Create a mock request request, _ := http.NewRequest(http.MethodDelete, "/test", nil) // Call the function with a response code that doesn't exist err := ResponseCodeNotFound(op, request, 404) // Validate the error require.NotNil(t, err) require.Equal(t, helpers.ResponseBodyValidation, err.ValidationType) require.Equal(t, helpers.ResponseBodyResponseCode, err.ValidationSubType) require.Contains(t, err.Message, "response code '404' does not exist") require.Contains(t, err.Reason, "The response code '404' of the DELETE request submitted has not been defined") require.Equal(t, 22, err.SpecLine) require.Equal(t, 56, err.SpecCol) require.Equal(t, HowToFixInvalidResponseCode, err.HowToFix) } func TestResponseContentTypeNotFound_NilContentKeyNode(t *testing.T) { // Response code exists but has no content KeyNode — should not panic r := &lowv3.Response{ Content: low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]]{ Value: orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]](), // KeyNode intentionally nil }, } resp := v3.NewResponse(r) responses := &lowv3.Responses{ Codes: orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.Response]](), KeyNode: &yaml.Node{}, } op := &lowv3.Operation{ Responses: low.NodeReference[*lowv3.Responses]{ Value: responses, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, } highOp := v3.NewOperation(op) highOp.Responses.Codes.Set("200", resp) request, _ := http.NewRequest(http.MethodGet, "/test", nil) response := &http.Response{ Header: http.Header{ helpers.ContentTypeHeader: {"application/xml"}, }, } // Should not panic err := ResponseContentTypeNotFound(highOp, request, response, "200", false) require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) } func TestResponseContentTypeNotFound_NilDefaultContentKeyNode(t *testing.T) { // Default response exists but has no content KeyNode — should not panic r := &lowv3.Response{ Content: low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]]{ Value: orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]](), // KeyNode intentionally nil }, } responses := &lowv3.Responses{ Default: low.NodeReference[*lowv3.Response]{ Value: r, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, Codes: orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.Response]](), KeyNode: &yaml.Node{}, } op := &lowv3.Operation{ Responses: low.NodeReference[*lowv3.Responses]{ Value: responses, KeyNode: &yaml.Node{}, ValueNode: &yaml.Node{}, }, } highOp := v3.NewOperation(op) request, _ := http.NewRequest(http.MethodGet, "/test", nil) response := &http.Response{ Header: http.Header{ helpers.ContentTypeHeader: {"application/xml"}, }, } // Should not panic err := ResponseContentTypeNotFound(highOp, request, response, "200", true) require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) } func TestResponseCodeNotFound_NilResponsesKeyNode(t *testing.T) { // Operation with nil Responses KeyNode — should not panic responses := &lowv3.Responses{ Codes: orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.Response]](), // KeyNode intentionally nil } op := &lowv3.Operation{ Responses: low.NodeReference[*lowv3.Responses]{ Value: responses, ValueNode: &yaml.Node{}, }, } highOp := v3.NewOperation(op) request, _ := http.NewRequest(http.MethodGet, "/test", nil) // Should not panic err := ResponseCodeNotFound(highOp, request, 404) require.NotNil(t, err) require.Equal(t, 1, err.SpecLine) require.Equal(t, 0, err.SpecCol) } libopenapi-validator-0.13.8/errors/spec_line_col.go000066400000000000000000000063451520534042400223600ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package errors import ( "go.yaml.in/yaml/v4" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi/datamodel/high/base" ) // SafeNodeLineCol safely extracts line and column from a yaml.Node pointer. // Returns (1, 0) if the node is nil. func SafeNodeLineCol(node *yaml.Node) (int, int) { if node == nil { return 1, 0 } return node.Line, node.Column } // paramExplodeLineCol safely extracts SpecLine/SpecCol from param.GoLow().Explode.ValueNode. func paramExplodeLineCol(param *v3.Parameter) (int, int) { if low := param.GoLow(); low != nil && low.Explode.ValueNode != nil { return low.Explode.ValueNode.Line, low.Explode.ValueNode.Column } return 1, 0 } // paramStyleLineCol safely extracts SpecLine/SpecCol from param.GoLow().Style.ValueNode. func paramStyleLineCol(param *v3.Parameter) (int, int) { if low := param.GoLow(); low != nil && low.Style.ValueNode != nil { return low.Style.ValueNode.Line, low.Style.ValueNode.Column } return 1, 0 } // paramRequiredLineCol safely extracts SpecLine/SpecCol from param.GoLow().Required.KeyNode. func paramRequiredLineCol(param *v3.Parameter) (int, int) { if low := param.GoLow(); low != nil && low.Required.KeyNode != nil { return low.Required.KeyNode.Line, low.Required.KeyNode.Column } return 1, 0 } // paramSchemaKeyLineCol safely extracts SpecLine/SpecCol from param.GoLow().Schema.KeyNode. func paramSchemaKeyLineCol(param *v3.Parameter) (int, int) { if low := param.GoLow(); low != nil && low.Schema.KeyNode != nil { return low.Schema.KeyNode.Line, low.Schema.KeyNode.Column } return 1, 0 } // paramSchemaTypeLineCol safely extracts SpecLine/SpecCol from // param.GoLow().Schema.Value.Schema().Type.KeyNode. func paramSchemaTypeLineCol(param *v3.Parameter) (int, int) { if low := param.GoLow(); low != nil { if sv := low.Schema.Value; sv != nil { if s := sv.Schema(); s != nil && s.Type.KeyNode != nil { return s.Type.KeyNode.Line, s.Type.KeyNode.Column } } } return 1, 0 } // paramSchemaEnumLineCol safely extracts SpecLine/SpecCol from // param.GoLow().Schema.Value.Schema().Enum.KeyNode. func paramSchemaEnumLineCol(param *v3.Parameter) (int, int) { if low := param.GoLow(); low != nil { if sv := low.Schema.Value; sv != nil { if s := sv.Schema(); s != nil && s.Enum.KeyNode != nil { return s.Enum.KeyNode.Line, s.Enum.KeyNode.Column } } } return 1, 0 } // paramContentLineCol safely extracts SpecLine/SpecCol from // param.GoLow().FindContent(ct).ValueNode. func paramContentLineCol(param *v3.Parameter, contentType string) (int, int) { if low := param.GoLow(); low != nil { if found := low.FindContent(contentType); found != nil && found.ValueNode != nil { return found.ValueNode.Line, found.ValueNode.Column } } return 1, 0 } // schemaItemsTypeLineCol safely extracts SpecLine/SpecCol from // sch.Items.A.GoLow().Schema().Type.KeyNode. func schemaItemsTypeLineCol(sch *base.Schema) (int, int) { if sch.Items != nil && sch.Items.A != nil { if low := sch.Items.A.GoLow(); low != nil { if s := low.Schema(); s != nil && s.Type.KeyNode != nil { return s.Type.KeyNode.Line, s.Type.KeyNode.Column } } } return 1, 0 } libopenapi-validator-0.13.8/errors/strict_errors.go000066400000000000000000000150551520534042400224640ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package errors import ( "fmt" "strings" ) // StrictValidationType is the validation type for strict mode errors. const StrictValidationType = "strict" // StrictValidationSubTypes for different kinds of strict validation errors. const ( StrictSubTypeProperty = "undeclared-property" StrictSubTypeHeader = "undeclared-header" StrictSubTypeQuery = "undeclared-query-param" StrictSubTypeCookie = "undeclared-cookie" StrictSubTypeReadOnlyProperty = "readonly-property" StrictSubTypeWriteOnlyProperty = "writeonly-property" ) // UndeclaredPropertyError creates a ValidationError for an undeclared property. func UndeclaredPropertyError( path string, name string, value any, declaredProperties []string, direction string, requestPath string, requestMethod string, specLine int, specCol int, ) *ValidationError { dirStr := direction if dirStr == "" { dirStr = "request" } return &ValidationError{ ValidationType: StrictValidationType, ValidationSubType: StrictSubTypeProperty, Message: fmt.Sprintf("%s property '%s' at '%s' is not declared in schema", dirStr, name, path), Reason: fmt.Sprintf("Strict mode: found property not in schema. "+ "Declared properties: [%s]", strings.Join(declaredProperties, ", ")), HowToFix: fmt.Sprintf("Add '%s' to the schema, remove it from the %s, "+ "or add '%s' to StrictIgnorePaths", name, dirStr, path), RequestPath: requestPath, RequestMethod: requestMethod, ParameterName: name, Context: truncateForContext(value), SpecLine: specLine, SpecCol: specCol, } } // UndeclaredHeaderError creates a ValidationError for an undeclared header. func UndeclaredHeaderError( name string, value string, declaredHeaders []string, direction string, requestPath string, requestMethod string, ) *ValidationError { dirStr := direction if dirStr == "" { dirStr = "request" } return &ValidationError{ ValidationType: StrictValidationType, ValidationSubType: StrictSubTypeHeader, Message: fmt.Sprintf("%s header '%s' is not declared in specification", dirStr, name), Reason: fmt.Sprintf("Strict mode: found header not in spec. "+ "Declared headers: [%s]", strings.Join(declaredHeaders, ", ")), HowToFix: fmt.Sprintf("Add '%s' to the operation's parameters, remove it from the %s, "+ "or add it to StrictIgnoredHeaders", name, dirStr), RequestPath: requestPath, RequestMethod: requestMethod, ParameterName: name, Context: value, } } // UndeclaredQueryParamError creates a ValidationError for an undeclared query parameter. func UndeclaredQueryParamError( path string, name string, value any, declaredParams []string, requestPath string, requestMethod string, ) *ValidationError { return &ValidationError{ ValidationType: StrictValidationType, ValidationSubType: StrictSubTypeQuery, Message: fmt.Sprintf("query parameter '%s' at '%s' is not declared in specification", name, path), Reason: fmt.Sprintf("Strict mode: found query parameter not in spec. "+ "Declared parameters: [%s]", strings.Join(declaredParams, ", ")), HowToFix: fmt.Sprintf("Add '%s' to the operation's query parameters, remove it from the request, "+ "or add '%s' to StrictIgnorePaths", name, path), RequestPath: requestPath, RequestMethod: requestMethod, ParameterName: name, Context: truncateForContext(value), } } // UndeclaredCookieError creates a ValidationError for an undeclared cookie. func UndeclaredCookieError( path string, name string, value any, declaredCookies []string, requestPath string, requestMethod string, ) *ValidationError { return &ValidationError{ ValidationType: StrictValidationType, ValidationSubType: StrictSubTypeCookie, Message: fmt.Sprintf("cookie '%s' at '%s' is not declared in specification", name, path), Reason: fmt.Sprintf("Strict mode: found cookie not in spec. "+ "Declared cookies: [%s]", strings.Join(declaredCookies, ", ")), HowToFix: fmt.Sprintf("Add '%s' to the operation's cookie parameters, remove it from the request, "+ "or add '%s' to StrictIgnorePaths", name, path), RequestPath: requestPath, RequestMethod: requestMethod, ParameterName: name, Context: truncateForContext(value), } } // ReadOnlyPropertyError creates a ValidationError for a readOnly property in a request. func ReadOnlyPropertyError( path string, name string, value any, requestPath string, requestMethod string, specLine int, specCol int, ) *ValidationError { return &ValidationError{ ValidationType: StrictValidationType, ValidationSubType: StrictSubTypeReadOnlyProperty, Message: fmt.Sprintf("request property '%s' at '%s' is readOnly and should not be sent in the request", name, path), Reason: fmt.Sprintf("Strict mode: property '%s' is marked readOnly in the schema", name), HowToFix: fmt.Sprintf("Remove the readOnly annotation from '%s' in the schema, "+ "remove it from the request, or add '%s' to StrictIgnorePaths", name, path), RequestPath: requestPath, RequestMethod: requestMethod, ParameterName: name, Context: truncateForContext(value), SpecLine: specLine, SpecCol: specCol, } } // WriteOnlyPropertyError creates a ValidationError for a writeOnly property in a response. func WriteOnlyPropertyError( path string, name string, value any, requestPath string, requestMethod string, specLine int, specCol int, ) *ValidationError { return &ValidationError{ ValidationType: StrictValidationType, ValidationSubType: StrictSubTypeWriteOnlyProperty, Message: fmt.Sprintf("response property '%s' at '%s' is writeOnly and should not be returned in the response", name, path), Reason: fmt.Sprintf("Strict mode: property '%s' is marked writeOnly in the schema", name), HowToFix: fmt.Sprintf("Remove the writeOnly annotation from '%s' in the schema, "+ "remove it from the response, or add '%s' to StrictIgnorePaths", name, path), RequestPath: requestPath, RequestMethod: requestMethod, ParameterName: name, Context: truncateForContext(value), SpecLine: specLine, SpecCol: specCol, } } // truncateForContext creates a truncated string representation for error context. func truncateForContext(v any) string { switch val := v.(type) { case string: if len(val) > 50 { return val[:47] + "..." } return val case map[string]any: return "{...}" case []any: return "[...]" default: s := fmt.Sprintf("%v", v) if len(s) > 50 { return s[:47] + "..." } return s } } libopenapi-validator-0.13.8/errors/strict_errors_test.go000066400000000000000000000157711520534042400235300ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package errors import ( "testing" "github.com/stretchr/testify/assert" ) func TestUndeclaredPropertyError(t *testing.T) { err := UndeclaredPropertyError( "$.body.user.extra", "extra", "some value", []string{"name", "email"}, "request", "/users", "POST", 42, 10, ) assert.NotNil(t, err) assert.Equal(t, StrictValidationType, err.ValidationType) assert.Equal(t, StrictSubTypeProperty, err.ValidationSubType) assert.Contains(t, err.Message, "request property 'extra' at '$.body.user.extra'") assert.Contains(t, err.Reason, "name, email") assert.Contains(t, err.HowToFix, "extra") assert.Contains(t, err.HowToFix, "$.body.user.extra") assert.Equal(t, "/users", err.RequestPath) assert.Equal(t, "POST", err.RequestMethod) assert.Equal(t, "extra", err.ParameterName) assert.Equal(t, 42, err.SpecLine) assert.Equal(t, 10, err.SpecCol) } func TestUndeclaredPropertyError_Response(t *testing.T) { err := UndeclaredPropertyError( "$.body.data.undeclared", "undeclared", map[string]any{"nested": "value"}, []string{"id", "name"}, "response", "/items/123", "GET", 100, 5, ) assert.NotNil(t, err) assert.Contains(t, err.Message, "response property 'undeclared'") assert.Contains(t, err.Reason, "id, name") assert.Equal(t, "{...}", err.Context) // Map truncated assert.Equal(t, 100, err.SpecLine) assert.Equal(t, 5, err.SpecCol) } func TestUndeclaredPropertyError_EmptyDirection(t *testing.T) { err := UndeclaredPropertyError( "$.body.prop", "prop", "value", nil, "", // Empty direction defaults to "request" "/test", "POST", 0, // Zero values for unknown location 0, ) assert.Contains(t, err.Message, "request property") assert.Equal(t, 0, err.SpecLine) assert.Equal(t, 0, err.SpecCol) } func TestUndeclaredHeaderError(t *testing.T) { err := UndeclaredHeaderError( "X-Custom-Header", "header-value", []string{"Content-Type", "Authorization"}, "request", "/api/endpoint", "GET", ) assert.NotNil(t, err) assert.Equal(t, StrictValidationType, err.ValidationType) assert.Equal(t, StrictSubTypeHeader, err.ValidationSubType) assert.Contains(t, err.Message, "request header 'X-Custom-Header'") assert.Contains(t, err.Reason, "Content-Type, Authorization") assert.Contains(t, err.HowToFix, "X-Custom-Header") assert.Equal(t, "/api/endpoint", err.RequestPath) assert.Equal(t, "GET", err.RequestMethod) assert.Equal(t, "X-Custom-Header", err.ParameterName) assert.Equal(t, "header-value", err.Context) } func TestUndeclaredHeaderError_Response(t *testing.T) { err := UndeclaredHeaderError( "X-Response-Header", "value", nil, "response", "/test", "POST", ) assert.Contains(t, err.Message, "response header") } func TestUndeclaredHeaderError_EmptyDirection(t *testing.T) { err := UndeclaredHeaderError( "X-Header", "value", nil, "", "/test", "GET", ) assert.Contains(t, err.Message, "request header") } func TestUndeclaredQueryParamError(t *testing.T) { err := UndeclaredQueryParamError( "$.query.debug", "debug", "true", []string{"page", "limit"}, "/items", "GET", ) assert.NotNil(t, err) assert.Equal(t, StrictValidationType, err.ValidationType) assert.Equal(t, StrictSubTypeQuery, err.ValidationSubType) assert.Contains(t, err.Message, "query parameter 'debug' at '$.query.debug'") assert.Contains(t, err.Reason, "page, limit") assert.Contains(t, err.HowToFix, "debug") assert.Contains(t, err.HowToFix, "$.query.debug") assert.Equal(t, "/items", err.RequestPath) assert.Equal(t, "GET", err.RequestMethod) assert.Equal(t, "debug", err.ParameterName) } func TestUndeclaredCookieError(t *testing.T) { err := UndeclaredCookieError( "$.cookies.tracking", "tracking", "abc123", []string{"session", "csrf"}, "/dashboard", "GET", ) assert.NotNil(t, err) assert.Equal(t, StrictValidationType, err.ValidationType) assert.Equal(t, StrictSubTypeCookie, err.ValidationSubType) assert.Contains(t, err.Message, "cookie 'tracking' at '$.cookies.tracking'") assert.Contains(t, err.Reason, "session, csrf") assert.Contains(t, err.HowToFix, "tracking") assert.Contains(t, err.HowToFix, "$.cookies.tracking") assert.Equal(t, "/dashboard", err.RequestPath) assert.Equal(t, "GET", err.RequestMethod) assert.Equal(t, "tracking", err.ParameterName) } func TestReadOnlyPropertyError(t *testing.T) { err := ReadOnlyPropertyError( "$.body.id", "id", "user-123", "/users", "POST", 10, 5, ) assert.NotNil(t, err) assert.Equal(t, StrictValidationType, err.ValidationType) assert.Equal(t, StrictSubTypeReadOnlyProperty, err.ValidationSubType) assert.Contains(t, err.Message, "readOnly") assert.Contains(t, err.Message, "'id'") assert.Contains(t, err.Message, "'$.body.id'") assert.Contains(t, err.Reason, "readOnly") assert.Contains(t, err.HowToFix, "id") assert.Equal(t, "/users", err.RequestPath) assert.Equal(t, "POST", err.RequestMethod) assert.Equal(t, "id", err.ParameterName) assert.Equal(t, 10, err.SpecLine) assert.Equal(t, 5, err.SpecCol) } func TestWriteOnlyPropertyError(t *testing.T) { err := WriteOnlyPropertyError( "$.body.password", "password", "secret", "/users/123", "GET", 20, 3, ) assert.NotNil(t, err) assert.Equal(t, StrictValidationType, err.ValidationType) assert.Equal(t, StrictSubTypeWriteOnlyProperty, err.ValidationSubType) assert.Contains(t, err.Message, "writeOnly") assert.Contains(t, err.Message, "'password'") assert.Contains(t, err.Message, "'$.body.password'") assert.Contains(t, err.Reason, "writeOnly") assert.Contains(t, err.HowToFix, "password") assert.Equal(t, "/users/123", err.RequestPath) assert.Equal(t, "GET", err.RequestMethod) assert.Equal(t, "password", err.ParameterName) assert.Equal(t, 20, err.SpecLine) assert.Equal(t, 3, err.SpecCol) } func TestTruncateForContext_String(t *testing.T) { // Short string should not be truncated short := truncateForContext("short") assert.Equal(t, "short", short) // Long string should be truncated long := truncateForContext("this is a very long string that exceeds fifty characters and should be truncated") assert.Len(t, long, 50) assert.True(t, len(long) <= 50) assert.Contains(t, long, "...") } func TestTruncateForContext_Map(t *testing.T) { m := map[string]any{"key": "value"} result := truncateForContext(m) assert.Equal(t, "{...}", result) } func TestTruncateForContext_Slice(t *testing.T) { s := []any{1, 2, 3} result := truncateForContext(s) assert.Equal(t, "[...]", result) } func TestTruncateForContext_Other(t *testing.T) { // Integer i := truncateForContext(12345) assert.Equal(t, "12345", i) // Boolean b := truncateForContext(true) assert.Equal(t, "true", b) // Long formatted value type customType struct { Field1 string Field2 string Field3 string } longValue := customType{ Field1: "this is a long value", Field2: "that will exceed fifty", Field3: "characters when formatted", } result := truncateForContext(longValue) assert.True(t, len(result) <= 50) assert.Contains(t, result, "...") } libopenapi-validator-0.13.8/errors/urlencoded_errors.go000066400000000000000000000042141520534042400232730ustar00rootroot00000000000000package errors import ( "fmt" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi/datamodel/high/base" ) func InvalidURLEncodedParsing(reason, referenceObject string) *ValidationError { return &ValidationError{ ValidationType: helpers.URLEncodedValidation, ValidationSubType: helpers.Schema, Message: "Unable to parse form-urlencoded body", Reason: fmt.Sprintf("failed to parse form-urlencoded: %s", reason), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: reason, ReferenceSchema: "", ReferenceObject: referenceObject, }}, HowToFix: HowToFixInvalidUrlEncoded, } } func InvalidTypeEncoding(schema *base.Schema, name, contentType string) *ValidationError { line := 1 col := 0 if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil { line = low.Type.KeyNode.Line col = low.Type.KeyNode.Column } return &ValidationError{ ValidationType: helpers.URLEncodedValidation, ValidationSubType: helpers.InvalidTypeEncoding, Message: fmt.Sprintf("The value '%s' could not be parsed to the defined encoding", name), Reason: fmt.Sprintf("The value '%s' is encoded as '%s' in the schema, however the value could not be parsed", name, contentType), SpecLine: line, SpecCol: col, Context: schema, HowToFix: HowToFixInvalidTypeEncoding, } } func ReservedURLEncodedValue(schema *base.Schema, name, value string) *ValidationError { line := 1 col := 0 if schema != nil { if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil { line = low.Type.KeyNode.Line col = low.Type.KeyNode.Column } } return &ValidationError{ ValidationType: helpers.URLEncodedValidation, ValidationSubType: helpers.ReservedValues, Message: fmt.Sprintf("Form value '%s' contains reserved characters", name), Reason: fmt.Sprintf("The form value '%s' contains reserved characters but allowReserved is false. Value: '%s'", name, value), SpecLine: line, SpecCol: col, Context: schema, HowToFix: HowToFixFormDataReservedCharacters, } } libopenapi-validator-0.13.8/errors/urlencoded_errors_test.go000066400000000000000000000032631520534042400243350ustar00rootroot00000000000000package errors import ( "testing" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/stretchr/testify/assert" ) func getURLEncodingTestSchema() *base.Schema { spec := `openapi: 3.0.0 paths: /pet: get: responses: '200': content: application/x-www-form-urlencoded: encoding: animal: contentType: application/json schema: type: object properties: animal: type: object` doc, _ := libopenapi.NewDocument([]byte(spec)) v3Doc, _ := doc.BuildV3Model() return v3Doc.Model.Paths.PathItems.GetOrZero("/pet").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/x-www-form-urlencoded").Schema.Schema() } func TestInvalidURLEncodedParsing(t *testing.T) { err := InvalidURLEncodedParsing("no data sent", "invalid-formdata") assert.NotNil(t, (*err)) assert.Equal(t, (*err).SchemaValidationErrors[0].Reason, "no data sent") assert.Equal(t, (*err).SchemaValidationErrors[0].ReferenceObject, "invalid-formdata") assert.Equal(t, helpers.Schema, (*err).ValidationSubType) } func TestInvalidTypeEncoding(t *testing.T) { err := InvalidTypeEncoding(getURLEncodingTestSchema(), "animal", helpers.JSONContentType) assert.NotNil(t, (*err)) assert.Equal(t, helpers.InvalidTypeEncoding, (*err).ValidationSubType) } func TestReservedURLEncodedValue(t *testing.T) { err := ReservedURLEncodedValue(getURLEncodingTestSchema(), "animal", "!") assert.NotNil(t, (*err)) assert.Equal(t, helpers.ReservedValues, (*err).ValidationSubType) } libopenapi-validator-0.13.8/errors/validation_error.go000066400000000000000000000146141520534042400231230ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package errors import ( "fmt" "github.com/pb33f/libopenapi-validator/helpers" "github.com/santhosh-tekuri/jsonschema/v6" ) // SchemaValidationFailure describes any failure that occurs when validating data // against either an OpenAPI or JSON Schema. It aims to be a more user-friendly // representation of the error than what is provided by the jsonschema library. type SchemaValidationFailure struct { // Reason is a human-readable message describing the reason for the error. Reason string `json:"reason,omitempty" yaml:"reason,omitempty"` // InstancePath is the raw path segments from the root to the failing field InstancePath []string `json:"instancePath,omitempty" yaml:"instancePath,omitempty"` // FieldName is the name of the specific field that failed validation (last segment of the path) FieldName string `json:"fieldName,omitempty" yaml:"fieldName,omitempty"` // FieldPath is the JSONPath representation of the field location that failed validation (e.g., "$.user.email") FieldPath string `json:"fieldPath,omitempty" yaml:"fieldPath,omitempty"` // KeywordLocation is the JSON Pointer (RFC 6901) path to the schema keyword that failed validation // (e.g., "/properties/age/minimum") KeywordLocation string `json:"keywordLocation,omitempty" yaml:"keywordLocation,omitempty"` // Line is the line number where the violation occurred. This may a local line number // if the validation is a schema (only schemas are validated locally, so the line number will be relative to // the Context object held by the ValidationError object). Line int `json:"line,omitempty" yaml:"line,omitempty"` // Column is the column number where the violation occurred. This may a local column number // if the validation is a schema (only schemas are validated locally, so the column number will be relative to // the Context object held by the ValidationError object). Column int `json:"column,omitempty" yaml:"column,omitempty"` // ReferenceSchema is the schema that was referenced in the validation failure. ReferenceSchema string `json:"referenceSchema,omitempty" yaml:"referenceSchema,omitempty"` // ReferenceObject is the object that failed schema validation ReferenceObject string `json:"referenceObject,omitempty" yaml:"referenceObject,omitempty"` // ReferenceExample is an example object generated from the schema that was referenced in the validation failure. ReferenceExample string `json:"referenceExample,omitempty" yaml:"referenceExample,omitempty"` // The original jsonschema.ValidationError object, if the schema failure originated from the jsonschema library. OriginalJsonSchemaError *jsonschema.ValidationError `json:"-" yaml:"-"` // Context is the raw schema object that failed validation (for programmatic access) Context interface{} `json:"-" yaml:"-"` } // Error returns a string representation of the error func (s *SchemaValidationFailure) Error() string { if s.FieldPath != "" { return fmt.Sprintf("Reason: %s, FieldPath: %s", s.Reason, s.FieldPath) } return fmt.Sprintf("Reason: %s", s.Reason) } // ValidationError is a struct that contains all the information about a validation error. type ValidationError struct { // Message is a human-readable message describing the error. Message string `json:"message" yaml:"message"` // Reason is a human-readable message describing the reason for the error. Reason string `json:"reason" yaml:"reason"` // ValidationType is a string that describes the type of validation that failed. ValidationType string `json:"validationType" yaml:"validationType"` // ValidationSubType is a string that describes the subtype of validation that failed. ValidationSubType string `json:"validationSubType" yaml:"validationSubType"` // SpecLine is the line number in the spec where the error occurred. SpecLine int `json:"specLine" yaml:"specLine"` // SpecCol is the column number in the spec where the error occurred. SpecCol int `json:"specColumn" yaml:"specColumn"` // HowToFix is a human-readable message describing how to fix the error. HowToFix string `json:"howToFix" yaml:"howToFix"` // RequestPath is the path of the request RequestPath string `json:"requestPath" yaml:"requestPath"` // SpecPath is the path from the specification that corresponds to the request SpecPath string `json:"specPath" yaml:"specPath"` // RequestMethod is the HTTP method of the request RequestMethod string `json:"requestMethod" yaml:"requestMethod"` // ParameterName is the name of the parameter that failed validation (for parameter validation errors) ParameterName string `json:"parameterName,omitempty" yaml:"parameterName,omitempty"` // SchemaValidationErrors is a slice of SchemaValidationFailure objects that describe the validation errors // This is only populated when the validation type is against a schema. SchemaValidationErrors []*SchemaValidationFailure `json:"validationErrors,omitempty" yaml:"validationErrors,omitempty"` // Context is the object that the validation error occurred on. This is usually a pointer to a schema // or a parameter object. Context interface{} `json:"-" yaml:"-"` } // Error returns a string representation of the error func (v *ValidationError) Error() string { if v.SchemaValidationErrors != nil { if v.SpecLine > 0 && v.SpecCol > 0 { return fmt.Sprintf("Error: %s, Reason: %s, Validation Errors: %s, Line: %d, Column: %d", v.Message, v.Reason, v.SchemaValidationErrors, v.SpecLine, v.SpecCol) } else { return fmt.Sprintf("Error: %s, Reason: %s, Validation Errors: %s", v.Message, v.Reason, v.SchemaValidationErrors) } } else { if v.SpecLine > 0 && v.SpecCol > 0 { return fmt.Sprintf("Error: %s, Reason: %s, Line: %d, Column: %d", v.Message, v.Reason, v.SpecLine, v.SpecCol) } else { return fmt.Sprintf("Error: %s, Reason: %s", v.Message, v.Reason) } } } // IsPathMissingError returns true if the error has a ValidationType of "path" and a ValidationSubType of "missing" func (v *ValidationError) IsPathMissingError() bool { return v.ValidationType == helpers.PathValidation && v.ValidationSubType == helpers.ValidationMissing } // IsOperationMissingError returns true if the error has a ValidationType of "request" and a ValidationSubType of "missingOperation" func (v *ValidationError) IsOperationMissingError() bool { return v.ValidationType == helpers.PathValidation && v.ValidationSubType == helpers.ValidationMissingOperation } libopenapi-validator-0.13.8/errors/validation_error_test.go000066400000000000000000000076771520534042400241750ustar00rootroot00000000000000// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package errors import ( "fmt" "testing" "github.com/pb33f/libopenapi-validator/helpers" "github.com/stretchr/testify/require" ) func TestSchemaValidationFailure_Error(t *testing.T) { // Test the Error method of SchemaValidationFailure s := &SchemaValidationFailure{ Reason: "Invalid type", FieldPath: "$.path.to.property", } expectedError := "Reason: Invalid type, FieldPath: $.path.to.property" require.Equal(t, expectedError, s.Error()) } func TestValidationError_Error_NoSchemaValidationErrors(t *testing.T) { // Test the Error method of ValidationError with no SchemaValidationErrors and no line/column info v := &ValidationError{ Message: "Missing required field", Reason: "The field 'id' is required but missing", } expectedError := "Error: Missing required field, Reason: The field 'id' is required but missing" require.Equal(t, expectedError, v.Error()) } func TestValidationError_Error_WithSpecLineAndColumn(t *testing.T) { // Test the Error method of ValidationError with spec line and column v := &ValidationError{ Message: "Invalid data type", Reason: "Expected 'string', got 'integer'", SpecLine: 10, SpecCol: 15, } expectedError := "Error: Invalid data type, Reason: Expected 'string', got 'integer', Line: 10, Column: 15" require.Equal(t, expectedError, v.Error()) } func TestValidationError_Error_WithSchemaValidationErrors(t *testing.T) { // Test the Error method of ValidationError with SchemaValidationErrors schemaError := &SchemaValidationFailure{ Reason: "Invalid enum value", FieldPath: "$.path.to.enum", } v := &ValidationError{ Message: "Enum validation failed", Reason: "Invalid enum value", SchemaValidationErrors: []*SchemaValidationFailure{schemaError}, } expectedError := fmt.Sprintf("Error: Enum validation failed, Reason: Invalid enum value, Validation Errors: %s", []*SchemaValidationFailure{schemaError}) require.Equal(t, expectedError, v.Error()) } func TestValidationError_Error_WithSchemaValidationErrors_AndSpecLineColumn(t *testing.T) { // Test the Error method of ValidationError with SchemaValidationErrors and SpecLine and SpecCol schemaError := &SchemaValidationFailure{ Reason: "Invalid enum value", FieldPath: "$.path.to.enum", } v := &ValidationError{ Message: "Enum validation failed", Reason: "Invalid enum value", SchemaValidationErrors: []*SchemaValidationFailure{schemaError}, SpecLine: 12, SpecCol: 5, } expectedError := fmt.Sprintf("Error: Enum validation failed, Reason: Invalid enum value, Validation Errors: %s, Line: 12, Column: 5", []*SchemaValidationFailure{schemaError}) require.Equal(t, expectedError, v.Error()) } func TestValidationError_IsPathMissingError(t *testing.T) { // Test the IsPathMissingError method v := &ValidationError{ ValidationType: helpers.PathValidation, ValidationSubType: helpers.ValidationMissing, } require.True(t, v.IsPathMissingError()) // Test with different ValidationSubType v.ValidationSubType = "wrongType" require.False(t, v.IsPathMissingError()) // Test with different ValidationType v.ValidationType = helpers.RequestValidation v.ValidationSubType = helpers.ValidationMissing require.False(t, v.IsPathMissingError()) } func TestValidationError_IsOperationMissingError(t *testing.T) { // Test the IsOperationMissingError method v := &ValidationError{ ValidationType: helpers.PathValidation, ValidationSubType: helpers.ValidationMissingOperation, } require.True(t, v.IsOperationMissingError()) // Test with different ValidationSubType v.ValidationSubType = "wrongOperation" require.False(t, v.IsOperationMissingError()) // Test with different ValidationType v.ValidationType = helpers.RequestValidation v.ValidationSubType = helpers.ValidationMissingOperation require.False(t, v.IsOperationMissingError()) } libopenapi-validator-0.13.8/errors/xml_errors.go000066400000000000000000000070731520534042400217550ustar00rootroot00000000000000package errors import ( "fmt" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi/datamodel/high/base" ) func MissingPrefix(schema *base.Schema, prefix string) *ValidationError { line := 1 col := 0 if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil { line = low.Type.KeyNode.Line col = low.Type.KeyNode.Column } return &ValidationError{ ValidationType: helpers.XmlValidation, ValidationSubType: helpers.XmlValidationPrefix, Message: fmt.Sprintf("The prefix '%s' is defined in the schema, however it's missing from the xml", prefix), Reason: fmt.Sprintf("The prefix '%s' is defined in the schema, however it's missing from the xml content", prefix), SpecLine: line, SpecCol: col, Context: schema, HowToFix: fmt.Sprintf(HowToFixXmlPrefix, prefix), } } func InvalidPrefix(schema *base.Schema, prefix string) *ValidationError { line := 1 col := 0 if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil { line = low.Type.KeyNode.Line col = low.Type.KeyNode.Column } return &ValidationError{ ValidationType: helpers.XmlValidation, ValidationSubType: helpers.XmlValidationPrefix, Message: fmt.Sprintf("The prefix '%s' defined in the schema differs from the xml", prefix), Reason: fmt.Sprintf("The prefix '%s' is defined in the schema, however the xml sent and invalid prefix", prefix), SpecCol: col, SpecLine: line, Context: schema, HowToFix: fmt.Sprintf(HowToFixXmlPrefix, prefix), } } func MissingNamespace(schema *base.Schema, namespace string) *ValidationError { line := 1 col := 0 if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil { line = low.Type.KeyNode.Line col = low.Type.KeyNode.Column } return &ValidationError{ ValidationType: helpers.XmlValidation, ValidationSubType: helpers.XmlValidationNamespace, Message: fmt.Sprintf("The namespace '%s' is defined in the schema, however it's missing from the xml", namespace), Reason: fmt.Sprintf("The namespace '%s' is defined in the schema, however it's missing from the xml content", namespace), SpecLine: line, SpecCol: col, Context: schema, HowToFix: fmt.Sprintf(HowToFixXmlNamespace, namespace), } } func InvalidNamespace(schema *base.Schema, namespace, expectedNamespace, prefix string) *ValidationError { line := 1 col := 0 if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil { line = low.Type.KeyNode.Line col = low.Type.KeyNode.Column } return &ValidationError{ ValidationType: helpers.XmlValidation, ValidationSubType: helpers.XmlValidationNamespace, Message: fmt.Sprintf("The namespace from prefix '%s' differs from the xml", prefix), Reason: fmt.Sprintf("The namespace from prefix '%s' is declared as '%s' in the schema, however in xml is declared as '%s'", prefix, expectedNamespace, namespace), SpecLine: line, SpecCol: col, Context: schema, HowToFix: fmt.Sprintf(HowToFixXmlNamespace, namespace), } } func InvalidXMLParsing(reason, referenceObject string) *ValidationError { return &ValidationError{ ValidationType: helpers.XmlValidation, ValidationSubType: helpers.Schema, Message: "xml example is malformed", Reason: fmt.Sprintf("failed to parse xml: %s", reason), SchemaValidationErrors: []*SchemaValidationFailure{{ Reason: reason, ReferenceSchema: "", ReferenceObject: referenceObject, }}, HowToFix: HowToFixInvalidXml, } } libopenapi-validator-0.13.8/errors/xml_errors_test.go000066400000000000000000000040761520534042400230140ustar00rootroot00000000000000// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package errors import ( "testing" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/stretchr/testify/assert" ) func getTestSchema() *base.Schema { spec := `openapi: 3.0.0 paths: /pet: get: responses: '200': content: application/xml: schema: type: object properties: age: type: integer xml: name: Cat` doc, _ := libopenapi.NewDocument([]byte(spec)) v3Doc, _ := doc.BuildV3Model() return v3Doc.Model.Paths.PathItems.GetOrZero("/pet").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() } func TestMissingPrefixError(t *testing.T) { schema := getTestSchema() err := MissingPrefix(schema, "prx") assert.NotNil(t, *err) assert.Equal(t, helpers.XmlValidationPrefix, (*err).ValidationSubType) } func TestMissingNamespaceError(t *testing.T) { schema := getTestSchema() err := MissingNamespace(schema, "http://ex.c") assert.NotNil(t, *err) assert.Equal(t, helpers.XmlValidationNamespace, (*err).ValidationSubType) } func TestInvalidPrefixError(t *testing.T) { schema := getTestSchema() err := InvalidPrefix(schema, "prx") assert.NotNil(t, *err) assert.Equal(t, helpers.XmlValidationPrefix, (*err).ValidationSubType) } func TestInvalidNamespaceError(t *testing.T) { schema := getTestSchema() err := InvalidNamespace(schema, "other", "http://ex.c", "prx") assert.NotNil(t, *err) assert.Equal(t, helpers.XmlValidationNamespace, (*err).ValidationSubType) } func TestInvalidParsing(t *testing.T) { err := InvalidXMLParsing("no data sent", "invalid-xml") assert.NotNil(t, (*err)) assert.Equal(t, (*err).SchemaValidationErrors[0].Reason, "no data sent") assert.Equal(t, (*err).SchemaValidationErrors[0].ReferenceObject, "invalid-xml") assert.Equal(t, helpers.Schema, (*err).ValidationSubType) } libopenapi-validator-0.13.8/go.mod000066400000000000000000000017721520534042400170240ustar00rootroot00000000000000module github.com/pb33f/libopenapi-validator go 1.25.0 require ( github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad github.com/dlclark/regexp2 v1.12.0 github.com/go-openapi/jsonpointer v0.23.1 github.com/goccy/go-yaml v1.19.2 github.com/pb33f/jsonpath v0.8.2 github.com/pb33f/libopenapi v0.36.6 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/stretchr/testify v1.11.1 go.yaml.in/yaml/v4 v4.0.0-rc.4 golang.org/x/text v0.37.0 ) require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-openapi/swag/jsonname v0.26.0 // indirect github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/sync v0.20.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) libopenapi-validator-0.13.8/go.sum000066400000000000000000000211041520534042400170400ustar00rootroot00000000000000github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad h1:3swAvbzgfaI6nKuDDU7BiKfZRdF+h2ZwKgMHd8Ha4t8= github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad/go.mod h1:9+nBLYNWkvPcq9ep0owWUsPTLgL9ZXTsZWcCSVGGLJ0= github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pb33f/jsonpath v0.8.2 h1:Ou4C7zjYClBm97dfZjDCjdZGusJoynv/vrtiEKNfj2Y= github.com/pb33f/jsonpath v0.8.2/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo= github.com/pb33f/libopenapi v0.36.6 h1:DRqWcgnMn8wiknMBKETIRZiY+GL05xEFtWwnK/Q+WOs= github.com/pb33f/libopenapi v0.36.6/go.mod h1:MsDdUlQ1CdrIDO5v26JfgBxQs7kcaOUEpMP3EqU6bI4= github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= libopenapi-validator-0.13.8/helpers/000077500000000000000000000000001520534042400173515ustar00rootroot00000000000000libopenapi-validator-0.13.8/helpers/constants.go000066400000000000000000000047021520534042400217170ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package helpers const ( ParameterValidation = "parameter" ParameterValidationPath = "path" ParameterValidationQuery = "query" ParameterValidationHeader = "header" ParameterValidationCookie = "cookie" RequestValidation = "request" RequestBodyValidation = "requestBody" XmlValidation = "xmlValidation" XmlValidationPrefix = "prefix" XmlValidationNamespace = "namespace" URLEncodedValidation = "urlEncodedValidation" InvalidTypeEncoding = "invalidTypeEncoding" ReservedValues = "reservedValues" Schema = "schema" ResponseBodyValidation = "response" RequestBodyContentType = "contentType" // Deprecated: use ValidationMissingOperation RequestMissingOperation = "missingOperation" PathValidation = "path" ValidationMissing = "missing" ValidationMissingOperation = "missingOperation" ResponseBodyResponseCode = "statusCode" SecurityValidation = "security" DocumentValidation = "document" SpaceDelimited = "spaceDelimited" PipeDelimited = "pipeDelimited" DefaultDelimited = "default" MatrixStyle = "matrix" LabelStyle = "label" Pipe = "|" Comma = "," Space = " " SemiColon = ";" Asterisk = "*" Period = "." Equals = "=" Integer = "integer" Number = "number" Slash = "/" Object = "object" String = "string" Array = "array" Boolean = "boolean" DeepObject = "deepObject" Header = "header" Cookie = "cookie" Path = "path" Form = "form" Query = "query" JSONContentType = "application/json" URLEncodedContentType = "application/x-www-form-urlencoded" JSONType = "json" ContentTypeHeader = "Content-Type" AuthorizationHeader = "Authorization" Charset = "charset" Boundary = "boundary" Preferred = "preferred" FailSegment = "**&&FAIL&&**" ) libopenapi-validator-0.13.8/helpers/ignore_regex.go000066400000000000000000000013261520534042400223570ustar00rootroot00000000000000// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package helpers import "regexp" var ( // Ignore generic poly errors that just say "none matched" since we get specific errors // But keep errors that say which subschemas matched (for multiple match scenarios) IgnorePattern = `^'?(anyOf|allOf|oneOf|validation)'? failed(, none matched)?$` IgnorePolyPattern = `^'?(anyOf|allOf|oneOf)'? failed(, none matched)?$` ) // IgnoreRegex is a regular expression that matches the IgnorePattern var IgnoreRegex = regexp.MustCompile(IgnorePattern) // IgnorePolyRegex is a regular expression that matches the IgnorePattern var IgnorePolyRegex = regexp.MustCompile(IgnorePolyPattern) libopenapi-validator-0.13.8/helpers/json_pointer.go000066400000000000000000000034401520534042400224120ustar00rootroot00000000000000// Copyright 2026 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package helpers import ( "fmt" "strings" "github.com/go-openapi/jsonpointer" ) // EscapeJSONPointerSegment escapes a single segment for use in a JSON Pointer (RFC 6901). // It replaces '~' with '~0' and '/' with '~1'. func EscapeJSONPointerSegment(segment string) string { return jsonpointer.Escape(segment) } // ConstructParameterJSONPointer constructs a full JSON Pointer path for a parameter // in the OpenAPI specification. // Format: /paths/{path}/{method}/parameters/{paramName}/schema/{keyword} // The path segment is automatically escaped according to RFC 6901. // The keyword can be a simple keyword like "type" or a nested path like "items/type". func ConstructParameterJSONPointer(pathTemplate, method, paramName, keyword string) string { escapedPath := EscapeJSONPointerSegment(pathTemplate) escapedPath = strings.TrimPrefix(escapedPath, "~1") // Remove leading slash encoding method = strings.ToLower(method) return fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/%s", escapedPath, method, paramName, keyword) } // ConstructResponseHeaderJSONPointer constructs a full JSON Pointer path for a response header // in the OpenAPI specification. // Format: /paths/{path}/{method}/responses/{statusCode}/headers/{headerName}/{keyword} // The path segment is automatically escaped according to RFC 6901. func ConstructResponseHeaderJSONPointer(pathTemplate, method, statusCode, headerName, keyword string) string { escapedPath := EscapeJSONPointerSegment(pathTemplate) escapedPath = strings.TrimPrefix(escapedPath, "~1") // Remove leading slash encoding method = strings.ToLower(method) return fmt.Sprintf("/paths/%s/%s/responses/%s/headers/%s/%s", escapedPath, method, statusCode, headerName, keyword) } libopenapi-validator-0.13.8/helpers/json_pointer_test.go000066400000000000000000000072471520534042400234620ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package helpers import ( "testing" "github.com/stretchr/testify/assert" ) func TestEscapeJSONPointerSegment(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "no special characters", input: "simple", expected: "simple", }, { name: "tilde only", input: "some~thing", expected: "some~0thing", }, { name: "slash only", input: "path/to/something", expected: "path~1to~1something", }, { name: "both tilde and slash", input: "path/with~special/chars~", expected: "path~1with~0special~1chars~0", }, { name: "path template", input: "/users/{id}", expected: "~1users~1{id}", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := EscapeJSONPointerSegment(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestConstructParameterJSONPointer(t *testing.T) { tests := []struct { name string pathTemplate string method string paramName string keyword string expected string }{ { name: "simple path with query parameter type", pathTemplate: "/users", method: "GET", paramName: "limit", keyword: "type", expected: "/paths/users/get/parameters/limit/schema/type", }, { name: "path with parameter and enum keyword", pathTemplate: "/users/{id}", method: "POST", paramName: "status", keyword: "enum", expected: "/paths/users~1{id}/post/parameters/status/schema/enum", }, { name: "path with tilde character", pathTemplate: "/some~path", method: "PUT", paramName: "value", keyword: "format", expected: "/paths/some~0path/put/parameters/value/schema/format", }, { name: "path with multiple slashes", pathTemplate: "/api/v1/users/{userId}/posts/{postId}", method: "DELETE", paramName: "filter", keyword: "required", expected: "/paths/api~1v1~1users~1{userId}~1posts~1{postId}/delete/parameters/filter/schema/required", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ConstructParameterJSONPointer(tt.pathTemplate, tt.method, tt.paramName, tt.keyword) assert.Equal(t, tt.expected, result) }) } } func TestConstructResponseHeaderJSONPointer(t *testing.T) { tests := []struct { name string pathTemplate string method string statusCode string headerName string keyword string expected string }{ { name: "simple response header", pathTemplate: "/health", method: "GET", statusCode: "200", headerName: "X-Request-ID", keyword: "required", expected: "/paths/health/get/responses/200/headers/X-Request-ID/required", }, { name: "path with parameter", pathTemplate: "/users/{id}", method: "POST", statusCode: "201", headerName: "Location", keyword: "schema", expected: "/paths/users~1{id}/post/responses/201/headers/Location/schema", }, { name: "path with tilde and slash", pathTemplate: "/some~path/to/resource", method: "PUT", statusCode: "204", headerName: "ETag", keyword: "type", expected: "/paths/some~0path~1to~1resource/put/responses/204/headers/ETag/type", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ConstructResponseHeaderJSONPointer(tt.pathTemplate, tt.method, tt.statusCode, tt.headerName, tt.keyword) assert.Equal(t, tt.expected, result) }) } } libopenapi-validator-0.13.8/helpers/operation_utilities.go000066400000000000000000000027251520534042400240010ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package helpers import ( "mime" "net/http" "github.com/pb33f/libopenapi/datamodel/high/v3" ) // ExtractOperation extracts the operation from the path item based on the request method. If there is no // matching operation found, then nil is returned. func ExtractOperation(request *http.Request, item *v3.PathItem) *v3.Operation { switch request.Method { case http.MethodGet: return item.Get case http.MethodPost: return item.Post case http.MethodPut: return item.Put case http.MethodDelete: return item.Delete case http.MethodOptions: return item.Options case http.MethodHead: if item.Head != nil { return item.Head } return item.Get case http.MethodPatch: return item.Patch case http.MethodTrace: return item.Trace } return nil } // ExtractContentType extracts the content type from the request header. First return argument is the content type // of the request.The second (optional) argument is the charset of the request. The third (optional) // argument is the boundary of the type (only used with forms really). func ExtractContentType(contentType string) (string, string, string) { // mime.ParseMediaType: "If there is an error parsing the optional parameter, // the media type will be returned along with the error ErrInvalidMediaParameter." ct, params, _ := mime.ParseMediaType(contentType) return ct, params["charset"], params["boundary"] } libopenapi-validator-0.13.8/helpers/operation_utilities_test.go000066400000000000000000000112051520534042400250310ustar00rootroot00000000000000// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package helpers import ( "mime" "net/http" "testing" "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/stretchr/testify/require" ) // Test ExtractOperation for each HTTP method func TestExtractOperation(t *testing.T) { pathItem := &v3.PathItem{ Get: &v3.Operation{Summary: "GET operation"}, Post: &v3.Operation{Summary: "POST operation"}, Put: &v3.Operation{Summary: "PUT operation"}, Delete: &v3.Operation{Summary: "DELETE operation"}, Options: &v3.Operation{Summary: "OPTIONS operation"}, Head: &v3.Operation{Summary: "HEAD operation"}, Patch: &v3.Operation{Summary: "PATCH operation"}, Trace: &v3.Operation{Summary: "TRACE operation"}, } // Test all HTTP methods tests := []struct { method string want string }{ {http.MethodGet, "GET operation"}, {http.MethodPost, "POST operation"}, {http.MethodPut, "PUT operation"}, {http.MethodDelete, "DELETE operation"}, {http.MethodOptions, "OPTIONS operation"}, {http.MethodHead, "HEAD operation"}, {http.MethodPatch, "PATCH operation"}, {http.MethodTrace, "TRACE operation"}, } for _, tt := range tests { req, _ := http.NewRequest(tt.method, "/", nil) operation := ExtractOperation(req, pathItem) require.NotNil(t, operation) require.Equal(t, tt.want, operation.Summary) } // Test an unsupported HTTP method req, _ := http.NewRequest("INVALID", "/", nil) operation := ExtractOperation(req, pathItem) require.Nil(t, operation) } // Test ExtractContentType for various input cases func TestExtractContentType(t *testing.T) { // Simple content type with no charset or boundary contentType, charset, boundary := ExtractContentType("application/json") require.Equal(t, "application/json", contentType) require.Empty(t, charset) require.Empty(t, boundary) // Content type with charset contentType, charset, boundary = ExtractContentType("text/html; charset=UTF-8") require.Equal(t, "text/html", contentType) require.Equal(t, "UTF-8", charset) require.Empty(t, boundary) // Content type with boundary contentType, charset, boundary = ExtractContentType("multipart/form-data; boundary=----WebKitFormBoundary") require.Equal(t, "multipart/form-data", contentType) require.Empty(t, charset) require.Equal(t, "----WebKitFormBoundary", boundary) // Content type with both charset and boundary contentType, charset, boundary = ExtractContentType("multipart/form-data; charset=UTF-8; boundary=----WebKitFormBoundary") require.Equal(t, "multipart/form-data", contentType) require.Equal(t, "UTF-8", charset) require.Equal(t, "----WebKitFormBoundary", boundary) // Content type with leading/trailing spaces contentType, charset, boundary = ExtractContentType(" application/xml ; charset=ISO-8859-1 ; boundary=myBoundary ") require.Equal(t, "application/xml", contentType) require.Equal(t, "ISO-8859-1", charset) require.Equal(t, "myBoundary", boundary) // Invalid content type (no key-value pair for charset/boundary) contentType, charset, boundary = ExtractContentType("application/xml; charset; boundary") require.Equal(t, "application/xml", contentType) require.Empty(t, charset) require.Empty(t, boundary) // Content type with custom parameter contentType, charset, boundary = ExtractContentType("text/html; version=2") require.Equal(t, "text/html", contentType) require.Empty(t, charset) require.Empty(t, boundary) // Content type with custom parameter, charset, and boundary contentType, charset, boundary = ExtractContentType("text/html; charset=UTF-8; version=2; boundary=myBoundary") require.Equal(t, "text/html", contentType) require.Equal(t, "UTF-8", charset) require.Equal(t, "myBoundary", boundary) // mime.ParseMediaType returns an error, but ExtractContentType still returns the content type. const ct = "text/plain;;" _, _, err := mime.ParseMediaType(ct) require.ErrorIs(t, err, mime.ErrInvalidMediaParameter) contentType, charset, boundary = ExtractContentType(ct) require.Equal(t, "text/plain", contentType) require.Empty(t, charset) require.Empty(t, boundary) } func TestExtractOperationHeadFallback(t *testing.T) { pathItem := &v3.PathItem{ Get: &v3.Operation{Summary: "GET operation"}, Head: nil, } req, _ := http.NewRequest(http.MethodHead, "/", nil) operation := ExtractOperation(req, pathItem) require.NotNil(t, operation) require.Equal(t, "GET operation", operation.Summary) } func TestExtractOperationHeadFallbackNoGet(t *testing.T) { pathItem := &v3.PathItem{ Head: nil, Get: nil, } req, _ := http.NewRequest(http.MethodHead, "/", nil) operation := ExtractOperation(req, pathItem) require.Nil(t, operation) } libopenapi-validator-0.13.8/helpers/package.go000066400000000000000000000006061520534042400212750ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT // Package helpers contains helper and utility functions used by the validator. Trying to avoid using the package // name utils anymore, as it's too generic and can cause conflicts with other packages - however I feel this pattern // will suffer the exact same fate with time. package helpers libopenapi-validator-0.13.8/helpers/parameter_utilities.go000066400000000000000000000525761520534042400237720ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package helpers import ( "fmt" "net/http" "slices" "strconv" "strings" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" ) // QueryParam is a struct that holds the key, values and property name for a query parameter // it's used for complex query types that need to be parsed and tracked differently depending // on the encoding styles used. type QueryParam struct { Key string Values []string Property string PropertyPath []string } // ExtractParamsForOperation will extract the parameters for the operation based on the request method. // Both the path level params and the method level params will be returned. func ExtractParamsForOperation(request *http.Request, item *v3.PathItem) []*v3.Parameter { params := item.Parameters switch request.Method { case http.MethodGet: if item.Get != nil { params = append(params, item.Get.Parameters...) } case http.MethodPost: if item.Post != nil { params = append(params, item.Post.Parameters...) } case http.MethodPut: if item.Put != nil { params = append(params, item.Put.Parameters...) } case http.MethodDelete: if item.Delete != nil { params = append(params, item.Delete.Parameters...) } case http.MethodOptions: if item.Options != nil { params = append(params, item.Options.Parameters...) } case http.MethodHead: if item.Head != nil { params = append(params, item.Head.Parameters...) } case http.MethodPatch: if item.Patch != nil { params = append(params, item.Patch.Parameters...) } case http.MethodTrace: if item.Trace != nil { params = append(params, item.Trace.Parameters...) } } return params } // ExtractSecurityForOperation will extract the security requirements for the operation based on the request method. // // Deprecated: use EffectiveSecurityForOperation instead, which also handles // document-level global security inheritance per the OpenAPI specification. func ExtractSecurityForOperation(request *http.Request, item *v3.PathItem) []*base.SecurityRequirement { var schemes []*base.SecurityRequirement switch request.Method { case http.MethodGet: if item.Get != nil { schemes = append(schemes, item.Get.Security...) } case http.MethodPost: if item.Post != nil { schemes = append(schemes, item.Post.Security...) } case http.MethodPut: if item.Put != nil { schemes = append(schemes, item.Put.Security...) } case http.MethodDelete: if item.Delete != nil { schemes = append(schemes, item.Delete.Security...) } case http.MethodOptions: if item.Options != nil { schemes = append(schemes, item.Options.Security...) } case http.MethodHead: if item.Head != nil { schemes = append(schemes, item.Head.Security...) } case http.MethodPatch: if item.Patch != nil { schemes = append(schemes, item.Patch.Security...) } case http.MethodTrace: if item.Trace != nil { schemes = append(schemes, item.Trace.Security...) } } return schemes } // ExtractSecurityHeaderNames extracts header names from applicable security schemes. // Returns header names from apiKey schemes with in:"header", plus "Authorization" // for http/oauth2/openIdConnect schemes. // // This function is used by strict mode validation to recognize security headers // as "declared" headers that should not trigger undeclared header errors. func ExtractSecurityHeaderNames( security []*base.SecurityRequirement, securitySchemes map[string]*v3.SecurityScheme, ) []string { if security == nil || securitySchemes == nil { return nil } seen := make(map[string]bool) var headers []string for _, sec := range security { if sec == nil || sec.ContainsEmptyRequirement { continue // No security required for this option } if sec.Requirements == nil { continue } for pair := sec.Requirements.First(); pair != nil; pair = pair.Next() { schemeName := pair.Key() scheme, ok := securitySchemes[schemeName] if !ok || scheme == nil { continue } var headerName string switch strings.ToLower(scheme.Type) { case "apikey": if strings.ToLower(scheme.In) == Header { headerName = scheme.Name } case "http", "oauth2", "openidconnect": headerName = "Authorization" } if headerName != "" && !seen[strings.ToLower(headerName)] { seen[strings.ToLower(headerName)] = true headers = append(headers, headerName) } } } return headers } // EffectiveSecurityForOperation returns the security requirements that apply to the // operation matched by request method. It implements OpenAPI's inheritance rule: // - If the operation defines security (even an empty array), use that. // - Otherwise, fall back to the document-level global security. // - Returns nil only when neither level defines security. func EffectiveSecurityForOperation(request *http.Request, item *v3.PathItem, docSecurity []*base.SecurityRequirement) []*base.SecurityRequirement { op := ExtractOperation(request, item) if op != nil && op.Security != nil { return op.Security // operation-level (may be empty [] = "no security") } return docSecurity // nil when no global security either } func cast(v string) any { if v == "true" || v == "false" { b, _ := strconv.ParseBool(v) return b } if i, err := strconv.ParseFloat(v, 64); err == nil { // check if this is an int or not if !strings.Contains(v, Period) { iv, _ := strconv.ParseInt(v, 10, 64) return iv } return i } return v } // getPropertySchema looks up a property's schema from an object schema's Properties map. // Returns nil if objectSchema is nil, has no Properties, or the property is not found. func getPropertySchema(objectSchema *base.Schema, propertyName string) *base.Schema { if objectSchema == nil || objectSchema.Properties == nil { return nil } proxy := objectSchema.Properties.GetOrZero(propertyName) if proxy == nil { return nil } return proxy.Schema() } func getAdditionalPropertiesSchema(objectSchema *base.Schema) *base.Schema { if objectSchema == nil || objectSchema.AdditionalProperties == nil || !objectSchema.AdditionalProperties.IsA() || objectSchema.AdditionalProperties.A == nil { return nil } return objectSchema.AdditionalProperties.A.Schema() } func getSchemaForObjectProperty(objectSchema *base.Schema, propertyName string) *base.Schema { if propSchema := getPropertySchema(objectSchema, propertyName); propSchema != nil { return propSchema } return getAdditionalPropertiesSchema(objectSchema) } // ParseDeepObjectKey splits a query-string key using qs-style deepObject bracket notation. // It returns ok=false for non-deepObject keys or malformed bracket paths. func ParseDeepObjectKey(key string) (baseName string, propertyPath []string, ok bool) { open := strings.IndexRune(key, '[') if open <= 0 { return "", nil, false } baseName = key[:open] rest := key[open:] for len(rest) > 0 { if rest[0] != '[' { return "", nil, false } close := strings.IndexRune(rest, ']') if close <= 1 { return "", nil, false } propertyPath = append(propertyPath, rest[1:close]) rest = rest[close+1:] } return baseName, propertyPath, len(propertyPath) > 0 } // castWithSchema casts a string value consulting the schema for the property's declared type. // If the schema says the property, or array property item, is a string, the value is returned // as-is (no numeric/bool guessing). // For other declared types, it falls back to cast() which produces correct results for integer, // number, and boolean values. The explicit string check prevents the most common miscast: numeric- // looking strings like "10" being converted to int64 when the schema declares type: string. func castWithSchema(v string, objectSchema *base.Schema, propertyName string) any { propSchema := schemaForPropertyPath(objectSchema, []string{propertyName}) if schemaPreservesStringValue(propSchema) { return v } if propSchema == nil && schemaPreservesStringValue(objectSchema) { return v } return cast(v) } func queryParamPropertyPath(v *QueryParam) []string { if len(v.PropertyPath) > 0 { return v.PropertyPath } return []string{v.Property} } func queryParamDeepObjectPath(v *QueryParam) []string { if v == nil { return nil } if len(v.PropertyPath) > 0 { return v.PropertyPath } if v.Property != "" { return []string{v.Property} } return nil } func schemaForPropertyPath(objectSchema *base.Schema, propertyPath []string) *base.Schema { current := objectSchema for _, propertyName := range propertyPath { current = getSchemaForObjectProperty(current, propertyName) if current == nil { return nil } } return current } func schemaTypeIncludes(sch *base.Schema, schemaType string) bool { return sch != nil && slices.Contains(sch.Type, schemaType) } func schemaArrayItems(sch *base.Schema) *base.Schema { if sch == nil || sch.Items == nil || !sch.Items.IsA() || sch.Items.A == nil { return nil } return sch.Items.A.Schema() } func schemaPreservesStringValue(sch *base.Schema) bool { if schemaTypeIncludes(sch, String) { return true } return schemaTypeIncludes(sch, Array) && schemaTypeIncludes(schemaArrayItems(sch), String) } // DeepObjectAllowsMultipleValues reports whether repeated values are allowed for a deepObject // property. It preserves existing top-level array/additionalProperties behavior and adds support // for nested properties declared as arrays. func DeepObjectAllowsMultipleValues(objectSchema *base.Schema, qp *QueryParam) bool { if objectSchema == nil { return false } if schemaTypeIncludes(objectSchema, Array) { return true } propertyPath := queryParamPropertyPath(qp) if schemaTypeIncludes(schemaForPropertyPath(objectSchema, propertyPath), Array) { return true } return schemaTypeIncludes(getAdditionalPropertiesSchema(objectSchema), Array) } func schemaForCastingPath(objectSchema *base.Schema, propertyPath []string) *base.Schema { propSchema := schemaForPropertyPath(objectSchema, propertyPath) if propSchema != nil { return propSchema } if schemaTypeIncludes(objectSchema, Array) { return objectSchema } return nil } func castWithSchemaPath(v string, objectSchema *base.Schema, propertyPath []string) any { if schemaPreservesStringValue(schemaForCastingPath(objectSchema, propertyPath)) { return v } if len(propertyPath) == 1 { return castWithSchema(v, objectSchema, propertyPath[0]) } return cast(v) } func deepObjectPathHasPrefix(prefix, path []string) bool { if len(prefix) >= len(path) { return false } for i := range prefix { if prefix[i] != path[i] { return false } } return true } // DeepObjectPathConflict reports whether any deepObject property path is also used // as a prefix for a nested path, such as obj[nested] and obj[nested][child]. func DeepObjectPathConflict(values []*QueryParam) (prefixParam, nestedParam *QueryParam, ok bool) { for i := range values { leftPath := queryParamDeepObjectPath(values[i]) if len(leftPath) == 0 { continue } for j := i + 1; j < len(values); j++ { rightPath := queryParamDeepObjectPath(values[j]) if len(rightPath) == 0 || slices.Equal(leftPath, rightPath) { continue } if deepObjectPathHasPrefix(leftPath, rightPath) { return values[i], values[j], true } if deepObjectPathHasPrefix(rightPath, leftPath) { return values[j], values[i], true } } } return nil, nil, false } func setNestedDeepObjectValue(target map[string]interface{}, propertyPath []string, value any) bool { if len(propertyPath) == 0 { target[""] = value return true } current := target for _, propertyName := range propertyPath[:len(propertyPath)-1] { next, ok := current[propertyName].(map[string]interface{}) if !ok { if existing, exists := current[propertyName]; exists { current[propertyName] = []interface{}{existing, value} return false } next = make(map[string]interface{}) current[propertyName] = next } current = next } propertyName := propertyPath[len(propertyPath)-1] if existing, exists := current[propertyName]; exists { if _, existingIsMap := existing.(map[string]interface{}); existingIsMap { if _, valueIsMap := value.(map[string]interface{}); !valueIsMap { current[propertyName] = []interface{}{existing, value} return false } } } current[propertyName] = value return true } // constructKVFromDelimited is the shared implementation for constructing key=value maps // from delimited strings (comma, period, semicolon). The delimiter determines how to split // entries, and each entry is further split on '=' to extract key-value pairs. func constructKVFromDelimited(values string, delimiter string, sch *base.Schema) map[string]interface{} { props := make(map[string]interface{}) exploded := strings.Split(values, delimiter) for i := range exploded { obK := strings.Split(exploded[i], Equals) if len(obK) == 2 { props[obK[0]] = castWithSchema(obK[1], sch, obK[0]) } } return props } // constructParamMapFromDelimitedEncoding is the shared implementation for constructing // parameter maps from pipe-delimited or space-delimited query parameter values. // Entries alternate between keys and values (key|value|key|value or key value key value). func constructParamMapFromDelimitedEncoding(values []*QueryParam, delimiter string, sch *base.Schema) map[string]interface{} { decoded := make(map[string]interface{}) for _, v := range values { props := make(map[string]interface{}) exploded := strings.Split(v.Values[0], delimiter) for i := range exploded { if i%2 == 0 && i+1 < len(exploded) { props[exploded[i]] = castWithSchema(exploded[i+1], sch, exploded[i]) } } decoded[v.Key] = props } return decoded } // ConstructParamMapFromDeepObjectEncoding will construct a map from the query parameters that are encoded as // deep objects. It's kind of a crazy way to do things, but hey, each to their own. func ConstructParamMapFromDeepObjectEncoding(values []*QueryParam, sch *base.Schema) map[string]interface{} { decoded := make(map[string]interface{}) for _, v := range values { propertyPath := queryParamPropertyPath(v) castForProp := func(val string) any { return castWithSchemaPath(val, sch, propertyPath) } props, ok := decoded[v.Key].(map[string]interface{}) if !ok { props = make(map[string]interface{}) decoded[v.Key] = props } rawValues := make([]interface{}, len(v.Values)) for i := range v.Values { rawValues[i] = castForProp(v.Values[i]) } if DeepObjectAllowsMultipleValues(sch, v) { setNestedDeepObjectValue(props, propertyPath, rawValues) continue } setNestedDeepObjectValue(props, propertyPath, castForProp(v.Values[0])) } return decoded } // ConstructParamMapFromQueryParamInput will construct a param map from an existing map of *QueryParam slices. // // Deprecated: use ConstructParamMapFromQueryParamInputWithSchema instead. func ConstructParamMapFromQueryParamInput(values map[string][]*QueryParam) map[string]interface{} { return ConstructParamMapFromQueryParamInputWithSchema(values, nil) } // ConstructParamMapFromPipeEncoding will construct a map from the query parameters that are encoded as // pipe separated values. // // Deprecated: use ConstructParamMapFromPipeEncodingWithSchema instead. func ConstructParamMapFromPipeEncoding(values []*QueryParam) map[string]interface{} { return ConstructParamMapFromPipeEncodingWithSchema(values, nil) } // ConstructParamMapFromSpaceEncoding will construct a map from the query parameters that are encoded as // space delimited values. // // Deprecated: use ConstructParamMapFromSpaceEncodingWithSchema instead. func ConstructParamMapFromSpaceEncoding(values []*QueryParam) map[string]interface{} { return ConstructParamMapFromSpaceEncodingWithSchema(values, nil) } // ConstructMapFromCSV will construct a map from a comma separated value string. // // Deprecated: use ConstructMapFromCSVWithSchema instead. func ConstructMapFromCSV(csv string) map[string]interface{} { return ConstructMapFromCSVWithSchema(csv, nil) } // ConstructKVFromCSV will construct a map from a comma separated value string that denotes key value pairs. // // Deprecated: use ConstructKVFromCSVWithSchema instead. func ConstructKVFromCSV(values string) map[string]interface{} { return ConstructKVFromCSVWithSchema(values, nil) } // ConstructKVFromLabelEncoding will construct a map from a period separated value string that denotes key value pairs. // // Deprecated: use ConstructKVFromLabelEncodingWithSchema instead. func ConstructKVFromLabelEncoding(values string) map[string]interface{} { return ConstructKVFromLabelEncodingWithSchema(values, nil) } // ConstructKVFromMatrixCSV will construct a map from a semicolon separated value string that denotes key value pairs. // // Deprecated: use ConstructKVFromMatrixCSVWithSchema instead. func ConstructKVFromMatrixCSV(values string) map[string]interface{} { return ConstructKVFromMatrixCSVWithSchema(values, nil) } // ConstructParamMapFromFormEncodingArray will construct a map from the query parameters that are encoded as // form encoded values. // // Deprecated: use ConstructParamMapFromFormEncodingArrayWithSchema instead. func ConstructParamMapFromFormEncodingArray(values []*QueryParam) map[string]interface{} { return ConstructParamMapFromFormEncodingArrayWithSchema(values, nil) } // DoesFormParamContainDelimiter will determine if a form parameter contains a delimiter. func DoesFormParamContainDelimiter(value, style string) bool { if strings.Contains(value, Comma) && (style == "" || style == Form) { return true } return false } // ExplodeQueryValue will explode a query value based on the style (space, pipe, or form/default). func ExplodeQueryValue(value, style string) []string { switch style { case SpaceDelimited: return strings.Split(value, Space) case PipeDelimited: return strings.Split(value, Pipe) default: return strings.Split(value, Comma) } } func CollapseCSVIntoFormStyle(key string, value string) string { return fmt.Sprintf("&%s=%s", key, strings.Join(strings.Split(value, ","), fmt.Sprintf("&%s=", key))) } func CollapseCSVIntoSpaceDelimitedStyle(key string, values []string) string { return fmt.Sprintf("%s=%s", key, strings.Join(values, "%20")) } func CollapseCSVIntoPipeDelimitedStyle(key string, values []string) string { return fmt.Sprintf("%s=%s", key, strings.Join(values, Pipe)) } // ConstructParamMapFromQueryParamInputWithSchema constructs a param map from an existing map of // *QueryParam slices, using the object schema to determine property types before casting. func ConstructParamMapFromQueryParamInputWithSchema(values map[string][]*QueryParam, sch *base.Schema) map[string]interface{} { decoded := make(map[string]interface{}) for _, q := range values { for _, v := range q { decoded[v.Key] = castWithSchema(v.Values[0], sch, v.Key) } } return decoded } // ConstructParamMapFromPipeEncodingWithSchema constructs a map from pipe-delimited query parameters, // using the object schema to determine property types before casting. func ConstructParamMapFromPipeEncodingWithSchema(values []*QueryParam, sch *base.Schema) map[string]interface{} { return constructParamMapFromDelimitedEncoding(values, Pipe, sch) } // ConstructParamMapFromSpaceEncodingWithSchema constructs a map from space-delimited query parameters, // using the object schema to determine property types before casting. func ConstructParamMapFromSpaceEncodingWithSchema(values []*QueryParam, sch *base.Schema) map[string]interface{} { return constructParamMapFromDelimitedEncoding(values, Space, sch) } // ConstructMapFromCSVWithSchema constructs a map from a comma separated value string, // using the object schema to determine property types before casting. func ConstructMapFromCSVWithSchema(csv string, sch *base.Schema) map[string]interface{} { decoded := make(map[string]interface{}) exploded := strings.Split(csv, Comma) for i := range exploded { if i%2 == 0 { if len(exploded) == i+1 { break } decoded[exploded[i]] = castWithSchema(exploded[i+1], sch, exploded[i]) } } return decoded } // ConstructKVFromCSVWithSchema constructs a map from a comma-separated key=value string, // using the object schema to determine property types before casting. func ConstructKVFromCSVWithSchema(values string, sch *base.Schema) map[string]interface{} { return constructKVFromDelimited(values, Comma, sch) } // ConstructKVFromLabelEncodingWithSchema constructs a map from a period-separated key=value string, // using the object schema to determine property types before casting. func ConstructKVFromLabelEncodingWithSchema(values string, sch *base.Schema) map[string]interface{} { return constructKVFromDelimited(values, Period, sch) } // ConstructKVFromMatrixCSVWithSchema constructs a map from a semicolon-separated key=value string, // using the object schema to determine property types before casting. func ConstructKVFromMatrixCSVWithSchema(values string, sch *base.Schema) map[string]interface{} { return constructKVFromDelimited(values, SemiColon, sch) } // ConstructParamMapFromFormEncodingArrayWithSchema constructs a map from form-encoded query parameters, // using the object schema to determine property types before casting. func ConstructParamMapFromFormEncodingArrayWithSchema(values []*QueryParam, sch *base.Schema) map[string]interface{} { decoded := make(map[string]interface{}) for _, v := range values { props := make(map[string]interface{}) exploded := strings.Split(v.Values[0], Comma) for i := range exploded { if i%2 == 0 { if len(exploded) > i+1 { props[exploded[i]] = castWithSchema(exploded[i+1], sch, exploded[i]) } } } decoded[v.Key] = props } return decoded } libopenapi-validator-0.13.8/helpers/parameter_utilities_test.go000066400000000000000000001336551520534042400250270ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package helpers import ( "net/http" "testing" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/require" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" ) // Test ExtractParamsForOperation with various HTTP methods func TestExtractParamsForOperation(t *testing.T) { pathItem := &v3.PathItem{ // Parameters: []*v3.Parameter{{Name: "param"}}, Get: &v3.Operation{Parameters: []*v3.Parameter{{Name: "getParam"}}}, Post: &v3.Operation{Parameters: []*v3.Parameter{{Name: "postParam"}}}, Put: &v3.Operation{Parameters: []*v3.Parameter{{Name: "putParam"}}}, Delete: &v3.Operation{Parameters: []*v3.Parameter{{Name: "deleteParam"}}}, Options: &v3.Operation{Parameters: []*v3.Parameter{{Name: "optionsParam"}}}, Head: &v3.Operation{Parameters: []*v3.Parameter{{Name: "headParam"}}}, Patch: &v3.Operation{Parameters: []*v3.Parameter{{Name: "patchParam"}}}, Trace: &v3.Operation{Parameters: []*v3.Parameter{{Name: "traceParam"}}}, } // Test all HTTP methods tests := []struct { method string expected []string // Expected parameter names }{ {http.MethodGet, []string{"getParam"}}, {http.MethodPost, []string{"postParam"}}, {http.MethodPut, []string{"putParam"}}, {http.MethodDelete, []string{"deleteParam"}}, {http.MethodOptions, []string{"optionsParam"}}, {http.MethodHead, []string{"headParam"}}, {http.MethodPatch, []string{"patchParam"}}, {http.MethodTrace, []string{"traceParam"}}, } for _, tt := range tests { // Create a new request with the specified method request, _ := http.NewRequest(tt.method, "/", nil) // Extract the parameters for the current request method params := ExtractParamsForOperation(request, pathItem) // Check if the number of parameters matches the expected count require.Len(t, params, len(tt.expected)) // Verify that the extracted parameter names match the expected ones for i, param := range params { require.Equal(t, tt.expected[i], param.Name) } } } // Test cast with different values (bool, int, float, string) func TestCast(t *testing.T) { require.Equal(t, true, cast("true")) require.Equal(t, int64(123), cast("123")) require.Equal(t, 123.45, cast("123.45")) require.Equal(t, "test", cast("test")) } // Test ExtractSecurityForOperation with various HTTP methods func TestExtractSecurityForOperation(t *testing.T) { // Create a PathItem with security requirements for each method pathItem := &v3.PathItem{ Get: &v3.Operation{Security: []*base.SecurityRequirement{{}}}, Post: &v3.Operation{Security: []*base.SecurityRequirement{{}}}, Put: &v3.Operation{Security: []*base.SecurityRequirement{{}}}, Delete: &v3.Operation{Security: []*base.SecurityRequirement{{}}}, Options: &v3.Operation{ Security: []*base.SecurityRequirement{{}}, }, Head: &v3.Operation{ Security: []*base.SecurityRequirement{{}}, }, Patch: &v3.Operation{ Security: []*base.SecurityRequirement{{}}, }, Trace: &v3.Operation{ Security: []*base.SecurityRequirement{{}}, }, } // Test all HTTP methods tests := []struct { method string }{ {http.MethodGet}, {http.MethodPost}, {http.MethodPut}, {http.MethodDelete}, {http.MethodOptions}, {http.MethodHead}, {http.MethodPatch}, {http.MethodTrace}, } for _, tt := range tests { // Create a new request with the specified method request, _ := http.NewRequest(tt.method, "/", nil) // Extract the security requirements for the current request method security := ExtractSecurityForOperation(request, pathItem) // Check if the number of security requirements matches the expected count (1 in all cases) require.Len(t, security, 1, "Failed for method: "+tt.method) } } // Test ExtractSecurityHeaderNames with various security scheme types func TestExtractSecurityHeaderNames(t *testing.T) { t.Run("nil inputs", func(t *testing.T) { require.Nil(t, ExtractSecurityHeaderNames(nil, nil)) require.Nil(t, ExtractSecurityHeaderNames([]*base.SecurityRequirement{}, nil)) require.Nil(t, ExtractSecurityHeaderNames(nil, map[string]*v3.SecurityScheme{})) }) t.Run("apiKey with in:header", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "ApiKeyAuth": { Type: "apiKey", In: "header", Name: "X-API-Key", }, } security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "ApiKeyAuth": {"read"}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Equal(t, []string{"X-API-Key"}, headers) }) t.Run("apiKey with in:query should not add header", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "ApiKeyQuery": { Type: "apiKey", In: "query", Name: "api_key", }, } security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "ApiKeyQuery": {}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Nil(t, headers) }) t.Run("apiKey with in:cookie should not add header", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "ApiKeyCookie": { Type: "apiKey", In: "cookie", Name: "session_id", }, } security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "ApiKeyCookie": {}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Nil(t, headers) }) t.Run("http bearer scheme", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "BearerAuth": { Type: "http", Scheme: "bearer", }, } security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "BearerAuth": {}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Equal(t, []string{"Authorization"}, headers) }) t.Run("http basic scheme", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "BasicAuth": { Type: "http", Scheme: "basic", }, } security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "BasicAuth": {}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Equal(t, []string{"Authorization"}, headers) }) t.Run("oauth2 scheme", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "OAuth2": { Type: "oauth2", }, } security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "OAuth2": {"read:users"}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Equal(t, []string{"Authorization"}, headers) }) t.Run("openIdConnect scheme", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "OpenID": { Type: "openIdConnect", OpenIdConnectUrl: "https://example.com/.well-known/openid-configuration", }, } security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "OpenID": {}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Equal(t, []string{"Authorization"}, headers) }) t.Run("empty security requirement (ContainsEmptyRequirement)", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "ApiKeyAuth": { Type: "apiKey", In: "header", Name: "X-API-Key", }, } security := []*base.SecurityRequirement{ { ContainsEmptyRequirement: true, }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Nil(t, headers) }) t.Run("nil security requirement in slice", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "ApiKeyAuth": { Type: "apiKey", In: "header", Name: "X-API-Key", }, } security := []*base.SecurityRequirement{nil} headers := ExtractSecurityHeaderNames(security, schemes) require.Nil(t, headers) }) t.Run("security requirement with nil Requirements map", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "ApiKeyAuth": { Type: "apiKey", In: "header", Name: "X-API-Key", }, } security := []*base.SecurityRequirement{ { Requirements: nil, }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Nil(t, headers) }) t.Run("multiple security options OR - different headers", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "ApiKeyAuth": { Type: "apiKey", In: "header", Name: "X-API-Key", }, "BearerAuth": { Type: "http", Scheme: "bearer", }, } // OR logic: separate security requirements security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "ApiKeyAuth": {}, }), }, { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "BearerAuth": {}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Len(t, headers, 2) require.Contains(t, headers, "X-API-Key") require.Contains(t, headers, "Authorization") }) t.Run("combined requirements AND - both headers", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "ApiKeyAuth": { Type: "apiKey", In: "header", Name: "X-API-Key", }, "BearerAuth": { Type: "http", Scheme: "bearer", }, } // AND logic: multiple schemes in one requirement security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "ApiKeyAuth": {}, "BearerAuth": {}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Len(t, headers, 2) require.Contains(t, headers, "X-API-Key") require.Contains(t, headers, "Authorization") }) t.Run("security scheme not found in schemes map", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "SomeOtherScheme": { Type: "apiKey", In: "header", Name: "X-Other", }, } security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "NonExistent": {}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Nil(t, headers) }) t.Run("nil scheme in schemes map", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "NilScheme": nil, } security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "NilScheme": {}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Nil(t, headers) }) t.Run("deduplication of Authorization header", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "BearerAuth": { Type: "http", Scheme: "bearer", }, "OAuth2": { Type: "oauth2", }, } // Both use Authorization header security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "BearerAuth": {}, }), }, { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "OAuth2": {"read"}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Equal(t, []string{"Authorization"}, headers) }) t.Run("case insensitive type matching", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "ApiKeyAuth": { Type: "APIKEY", // uppercase In: "HEADER", // uppercase Name: "X-API-Key", }, } security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "ApiKeyAuth": {}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Equal(t, []string{"X-API-Key"}, headers) }) t.Run("unknown security type is ignored", func(t *testing.T) { schemes := map[string]*v3.SecurityScheme{ "Unknown": { Type: "mutualTLS", // valid OpenAPI type but doesn't use headers }, } security := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "Unknown": {}, }), }, } headers := ExtractSecurityHeaderNames(security, schemes) require.Nil(t, headers) }) } func TestConstructParamMapFromDeepObjectEncoding(t *testing.T) { // Define mock values for testing values := []*QueryParam{ {Key: "key1", Values: []string{"value1"}, Property: "prop1"}, {Key: "key2", Values: []string{"123"}, Property: "prop2"}, {Key: "key3", Values: []string{"456", "789"}, Property: "prop3"}, } // Test case 1: Schema is nil decoded := ConstructParamMapFromDeepObjectEncoding(values, nil) require.NotNil(t, decoded) require.Equal(t, "value1", decoded["key1"].(map[string]interface{})["prop1"]) require.Equal(t, int64(123), decoded["key2"].(map[string]interface{})["prop2"]) require.Equal(t, int64(456), decoded["key3"].(map[string]interface{})["prop3"]) // Test case 2: Schema type contains array for the first param (Array handling) schema := &base.Schema{Type: []string{"array"}} decoded = ConstructParamMapFromDeepObjectEncoding(values, schema) require.NotNil(t, decoded) require.Equal(t, []interface{}{"value1"}, decoded["key1"].(map[string]interface{})["prop1"]) require.Equal(t, []interface{}{int64(123)}, decoded["key2"].(map[string]interface{})["prop2"]) require.Equal(t, []interface{}{int64(456), int64(789)}, decoded["key3"].(map[string]interface{})["prop3"]) // Test case 3: Schema with additional properties that is an array proxy := base.CreateSchemaProxy(&base.Schema{ Type: []string{"array"}, }) schema = &base.Schema{ AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{ A: proxy, }, } decoded = ConstructParamMapFromDeepObjectEncoding(values, schema) require.NotNil(t, decoded) require.Equal(t, []interface{}{"value1"}, decoded["key1"].(map[string]interface{})["prop1"]) require.Equal(t, []interface{}{int64(123)}, decoded["key2"].(map[string]interface{})["prop2"]) require.Equal(t, []interface{}{int64(456), int64(789)}, decoded["key3"].(map[string]interface{})["prop3"]) // Test case 4: Adding a value to an existing key in the decoded map valuesWithDup := []*QueryParam{ {Key: "key1", Values: []string{"value2"}, Property: "prop1"}, {Key: "key2", Values: []string{"456"}, Property: "prop2"}, } decoded = ConstructParamMapFromDeepObjectEncoding(valuesWithDup, nil) require.NotNil(t, decoded) require.Equal(t, "value2", decoded["key1"].(map[string]interface{})["prop1"]) require.Equal(t, int64(456), decoded["key2"].(map[string]interface{})["prop2"]) // Test case 5: Schema is not an array (standard object) nonArraySchema := &base.Schema{Type: []string{"object"}} decoded = ConstructParamMapFromDeepObjectEncoding(values, nonArraySchema) require.NotNil(t, decoded) require.Equal(t, "value1", decoded["key1"].(map[string]interface{})["prop1"]) require.Equal(t, int64(123), decoded["key2"].(map[string]interface{})["prop2"]) require.Equal(t, int64(456), decoded["key3"].(map[string]interface{})["prop3"]) } func TestConstructParamMapFromDeepObjectEncoding_ElseCase(t *testing.T) { arraySchema := &base.Schema{Type: []string{"array"}, AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{ A: base.CreateSchemaProxy(&base.Schema{Type: []string{"array"}}), }} newValues := []*QueryParam{ {Key: "key1", Values: []string{"456", "789"}, Property: "prop3"}, {Key: "key1", Values: []string{"999", "888"}, Property: "prop3"}, } decoded := ConstructParamMapFromDeepObjectEncoding(newValues, arraySchema) require.Equal(t, []interface{}{int64(999), int64(888)}, decoded["key1"].(map[string]interface{})["prop3"]) arraySchema = &base.Schema{Type: []string{"integer"}, AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{ A: base.CreateSchemaProxy(&base.Schema{Type: []string{"integer"}}), }} newValues = []*QueryParam{ {Key: "key1", Values: []string{"456", "789"}, Property: "prop3"}, {Key: "key1", Values: []string{"999", "888"}, Property: "prop3"}, } decoded = ConstructParamMapFromDeepObjectEncoding(newValues, arraySchema) require.Equal(t, int64(999), decoded["key1"].(map[string]interface{})["prop3"]) } func TestParseDeepObjectKey(t *testing.T) { tests := []struct { name string key string expectedBase string expectedPath []string expectedOK bool }{ { name: "flat", key: "obj[root]", expectedBase: "obj", expectedPath: []string{"root"}, expectedOK: true, }, { name: "nested", key: "obj[nested][child]", expectedBase: "obj", expectedPath: []string{"nested", "child"}, expectedOK: true, }, { name: "plain key", key: "obj", expectedOK: false, }, { name: "empty segment", key: "obj[]", expectedOK: false, }, { name: "trailing text", key: "obj[root]extra", expectedOK: false, }, { name: "missing closing bracket", key: "obj[root", expectedOK: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { baseName, propertyPath, ok := ParseDeepObjectKey(tc.key) require.Equal(t, tc.expectedOK, ok) require.Equal(t, tc.expectedBase, baseName) require.Equal(t, tc.expectedPath, propertyPath) }) } } func TestConstructParamMapFromDeepObjectEncoding_NestedObject(t *testing.T) { sch := &base.Schema{ Type: []string{"object"}, Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "root": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), "nested": base.CreateSchemaProxy(&base.Schema{ Type: []string{"object"}, Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "child": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), "count": base.CreateSchemaProxy(&base.Schema{Type: []string{"integer"}}), }), }), }), } values := []*QueryParam{ {Key: "obj", Values: []string{"test1"}, Property: "root", PropertyPath: []string{"root"}}, {Key: "obj", Values: []string{"10"}, Property: "nested", PropertyPath: []string{"nested", "child"}}, {Key: "obj", Values: []string{"42"}, Property: "nested", PropertyPath: []string{"nested", "count"}}, } decoded := ConstructParamMapFromDeepObjectEncoding(values, sch) obj := decoded["obj"].(map[string]interface{}) nested := obj["nested"].(map[string]interface{}) require.Equal(t, "test1", obj["root"]) require.Equal(t, "10", nested["child"]) require.Equal(t, int64(42), nested["count"]) } func TestConstructParamMapFromDeepObjectEncoding_NestedArray(t *testing.T) { sch := &base.Schema{ Type: []string{"object"}, Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "nested": base.CreateSchemaProxy(&base.Schema{ Type: []string{"object"}, Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "tags": base.CreateSchemaProxy(&base.Schema{ Type: []string{"array"}, Items: &base.DynamicValue[*base.SchemaProxy, bool]{ A: base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), }, }), }), }), }), } values := []*QueryParam{ {Key: "obj", Values: []string{"123", "456"}, Property: "nested", PropertyPath: []string{"nested", "tags"}}, } decoded := ConstructParamMapFromDeepObjectEncoding(values, sch) obj := decoded["obj"].(map[string]interface{}) nested := obj["nested"].(map[string]interface{}) require.Equal(t, []interface{}{"123", "456"}, nested["tags"]) require.True(t, DeepObjectAllowsMultipleValues(sch, values[0])) } func TestConstructParamMapFromDeepObjectEncoding_NestedAdditionalPropertiesArray(t *testing.T) { sch := &base.Schema{ Type: []string{"object"}, Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "filters": base.CreateSchemaProxy(&base.Schema{ Type: []string{"object"}, AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{ A: base.CreateSchemaProxy(&base.Schema{ Type: []string{"array"}, Items: &base.DynamicValue[*base.SchemaProxy, bool]{ A: base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), }, }), }, }), }), } values := []*QueryParam{ {Key: "obj", Values: []string{"123", "456"}, Property: "filters", PropertyPath: []string{"filters", "tag"}}, } decoded := ConstructParamMapFromDeepObjectEncoding(values, sch) obj := decoded["obj"].(map[string]interface{}) filters := obj["filters"].(map[string]interface{}) require.Equal(t, []interface{}{"123", "456"}, filters["tag"]) require.True(t, DeepObjectAllowsMultipleValues(sch, values[0])) } func TestDeepObjectPathConflict(t *testing.T) { tests := []struct { name string values []*QueryParam expect bool prefixPath []string nestedPath []string }{ { name: "scalar before nested", values: []*QueryParam{ {Key: "obj", Values: []string{"bad"}, Property: "nested", PropertyPath: []string{"nested"}}, {Key: "obj", Values: []string{"ok"}, Property: "nested", PropertyPath: []string{"nested", "child"}}, }, expect: true, prefixPath: []string{"nested"}, nestedPath: []string{"nested", "child"}, }, { name: "nested before scalar", values: []*QueryParam{ {Key: "obj", Values: []string{"ok"}, Property: "nested", PropertyPath: []string{"nested", "child"}}, {Key: "obj", Values: []string{"bad"}, Property: "nested", PropertyPath: []string{"nested"}}, }, expect: true, prefixPath: []string{"nested"}, nestedPath: []string{"nested", "child"}, }, { name: "same array path", values: []*QueryParam{ {Key: "obj", Values: []string{"alpha"}, Property: "nested", PropertyPath: []string{"nested", "tags"}}, {Key: "obj", Values: []string{"beta"}, Property: "nested", PropertyPath: []string{"nested", "tags"}}, }, }, { name: "sibling nested paths", values: []*QueryParam{ {Key: "obj", Values: []string{"ok"}, Property: "nested", PropertyPath: []string{"nested", "child"}}, {Key: "obj", Values: []string{"ok"}, Property: "nested", PropertyPath: []string{"nested", "other"}}, }, }, { name: "different roots", values: []*QueryParam{ {Key: "obj", Values: []string{"ok"}, Property: "nested", PropertyPath: []string{"nested"}}, {Key: "obj", Values: []string{"ok"}, Property: "other", PropertyPath: []string{"other", "child"}}, }, }, { name: "empty path is ignored", values: []*QueryParam{ {Key: "obj", Values: []string{"ignored"}}, {Key: "obj", Values: []string{"ok"}, Property: "nested", PropertyPath: []string{"nested", "child"}}, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { prefixParam, nestedParam, ok := DeepObjectPathConflict(tc.values) require.Equal(t, tc.expect, ok) if !tc.expect { return } require.Equal(t, tc.prefixPath, prefixParam.PropertyPath) require.Equal(t, tc.nestedPath, nestedParam.PropertyPath) }) } } func TestQueryParamDeepObjectPath(t *testing.T) { require.Nil(t, queryParamDeepObjectPath(nil)) require.Nil(t, queryParamDeepObjectPath(&QueryParam{})) require.Equal(t, []string{"root"}, queryParamDeepObjectPath(&QueryParam{Property: "root"})) require.Equal(t, []string{"root", "child"}, queryParamDeepObjectPath(&QueryParam{ Property: "root", PropertyPath: []string{"root", "child"}, })) } func TestSetNestedDeepObjectValue_PreservesConflicts(t *testing.T) { t.Run("empty path", func(t *testing.T) { target := make(map[string]interface{}) require.True(t, setNestedDeepObjectValue(target, nil, "root")) require.Equal(t, "root", target[""]) }) t.Run("scalar before nested", func(t *testing.T) { target := make(map[string]interface{}) require.True(t, setNestedDeepObjectValue(target, []string{"nested"}, "bad")) require.False(t, setNestedDeepObjectValue(target, []string{"nested", "child"}, "ok")) require.IsType(t, []interface{}{}, target["nested"]) }) t.Run("nested before scalar", func(t *testing.T) { target := make(map[string]interface{}) require.True(t, setNestedDeepObjectValue(target, []string{"nested", "child"}, "ok")) require.False(t, setNestedDeepObjectValue(target, []string{"nested"}, "bad")) require.IsType(t, []interface{}{}, target["nested"]) }) } func TestConstructParamMapFromDeepObjectEncoding_NestedPathConflict(t *testing.T) { tests := []struct { name string values []*QueryParam }{ { name: "scalar before nested", values: []*QueryParam{ {Key: "obj", Values: []string{"bad"}, Property: "nested", PropertyPath: []string{"nested"}}, {Key: "obj", Values: []string{"ok"}, Property: "nested", PropertyPath: []string{"nested", "child"}}, }, }, { name: "nested before scalar", values: []*QueryParam{ {Key: "obj", Values: []string{"ok"}, Property: "nested", PropertyPath: []string{"nested", "child"}}, {Key: "obj", Values: []string{"bad"}, Property: "nested", PropertyPath: []string{"nested"}}, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { decoded := ConstructParamMapFromDeepObjectEncoding(tc.values, nil) obj := decoded["obj"].(map[string]interface{}) require.IsType(t, []interface{}{}, obj["nested"]) }) } } func TestConstructKVFromLabelEncoding(t *testing.T) { // Test case 1: Empty input string values := "" props := ConstructKVFromLabelEncoding(values) require.NotNil(t, props) require.Empty(t, props) // Test case 2: Single valid key-value pair values = "key1=value1" props = ConstructKVFromLabelEncoding(values) require.NotNil(t, props) require.Equal(t, "value1", props["key1"]) // Test case 3: Multiple valid key-value pairs values = "key1=value1.key2=value2" props = ConstructKVFromLabelEncoding(values) require.NotNil(t, props) require.Equal(t, "value1", props["key1"]) require.Equal(t, "value2", props["key2"]) // Test case 4: Invalid key-value pair (missing equals) values = "key1=value1.key2" props = ConstructKVFromLabelEncoding(values) require.NotNil(t, props) require.Equal(t, "value1", props["key1"]) require.NotContains(t, props, "key2") // key2 should be ignored due to invalid format // Test case 5: Key-value pair where value needs to be cast to int and bool values = "key1=123.key2=true" props = ConstructKVFromLabelEncoding(values) require.NotNil(t, props) require.Equal(t, int64(123), props["key1"]) // cast to int require.Equal(t, true, props["key2"]) // cast to bool // Test case 6: Handle multiple valid and invalid key-value pairs values = "key1=value1.key2.key3=123.key4=true" props = ConstructKVFromLabelEncoding(values) require.NotNil(t, props) require.Equal(t, "value1", props["key1"]) // valid require.Equal(t, int64(123), props["key3"]) // valid require.Equal(t, true, props["key4"]) // valid require.NotContains(t, props, "key2") // invalid, missing value } func TestConstructParamMapFromQueryParamInput(t *testing.T) { // Test case 1: Empty input map values := map[string][]*QueryParam{} decoded := ConstructParamMapFromQueryParamInput(values) require.NotNil(t, decoded) require.Empty(t, decoded) // Test case 2: Single entry in the input map values = map[string][]*QueryParam{ "param1": { {Key: "param1", Values: []string{"value1"}}, }, } decoded = ConstructParamMapFromQueryParamInput(values) require.NotNil(t, decoded) require.Equal(t, "value1", decoded["param1"]) // Test case 3: Multiple entries in the input map values = map[string][]*QueryParam{ "param1": { {Key: "param1", Values: []string{"value1"}}, }, "param2": { {Key: "param2", Values: []string{"123"}}, }, "param3": { {Key: "param3", Values: []string{"true"}}, }, } decoded = ConstructParamMapFromQueryParamInput(values) require.NotNil(t, decoded) require.Equal(t, "value1", decoded["param1"]) require.Equal(t, int64(123), decoded["param2"]) // cast to int require.Equal(t, true, decoded["param3"]) // cast to bool // Test case 4: Handle multiple values but only the first value is used values = map[string][]*QueryParam{ "param1": { {Key: "param1", Values: []string{"first", "second"}}, }, } decoded = ConstructParamMapFromQueryParamInput(values) require.NotNil(t, decoded) require.Equal(t, "first", decoded["param1"]) // Only the first value is used // Test case 5: Handle different types of values values = map[string][]*QueryParam{ "intParam": { {Key: "intParam", Values: []string{"42"}}, }, "boolParam": { {Key: "boolParam", Values: []string{"false"}}, }, "stringParam": { {Key: "stringParam", Values: []string{"hello"}}, }, } decoded = ConstructParamMapFromQueryParamInput(values) require.NotNil(t, decoded) require.Equal(t, int64(42), decoded["intParam"]) require.Equal(t, false, decoded["boolParam"]) require.Equal(t, "hello", decoded["stringParam"]) } // Test ConstructParamMapFromPipeEncoding func TestConstructParamMapFromPipeEncoding(t *testing.T) { params := []*QueryParam{ {Key: "key1", Values: []string{"name|value"}}, } result := ConstructParamMapFromPipeEncoding(params) require.Equal(t, "value", result["key1"].(map[string]interface{})["name"]) } // Test ConstructParamMapFromSpaceEncoding func TestConstructParamMapFromSpaceEncoding(t *testing.T) { params := []*QueryParam{ {Key: "key1", Values: []string{"name value"}}, } result := ConstructParamMapFromSpaceEncoding(params) require.Equal(t, "value", result["key1"].(map[string]interface{})["name"]) } // Test ConstructMapFromCSV func TestConstructMapFromCSV(t *testing.T) { result := ConstructMapFromCSV("key1,value1,key2,value2") require.Equal(t, "value1", result["key1"]) require.Equal(t, "value2", result["key2"]) // add odd number of keys/values result = ConstructMapFromCSV("key1,value1,key2") require.Equal(t, "value1", result["key1"]) } // Test ConstructKVFromCSV func TestConstructKVFromCSV(t *testing.T) { result := ConstructKVFromCSV("key1=value1,key2=value2") require.Equal(t, "value1", result["key1"]) require.Equal(t, "value2", result["key2"]) } // Test CollapseCSVIntoFormStyle func TestCollapseCSVIntoFormStyle(t *testing.T) { result := CollapseCSVIntoFormStyle("key", "value1,value2") require.Equal(t, "&key=value1&key=value2", result) } // Test CollapseCSVIntoSpaceDelimitedStyle func TestCollapseCSVIntoSpaceDelimitedStyle(t *testing.T) { result := CollapseCSVIntoSpaceDelimitedStyle("key", []string{"value1", "value2"}) require.Equal(t, "key=value1%20value2", result) } // Test CollapseCSVIntoPipeDelimitedStyle func TestCollapseCSVIntoPipeDelimitedStyle(t *testing.T) { result := CollapseCSVIntoPipeDelimitedStyle("key", []string{"value1", "value2"}) require.Equal(t, "key=value1|value2", result) } // Test DoesFormParamContainDelimiter func TestDoesFormParamContainDelimiter(t *testing.T) { require.True(t, DoesFormParamContainDelimiter("value1,value2", "")) require.False(t, DoesFormParamContainDelimiter("value1 value2", "")) } // Test ExplodeQueryValue func TestExplodeQueryValue(t *testing.T) { require.Equal(t, []string{"value1", "value2"}, ExplodeQueryValue("value1,value2", "")) require.Equal(t, []string{"value1", "value2"}, ExplodeQueryValue("value1 value2", "spaceDelimited")) require.Equal(t, []string{"value1", "value2"}, ExplodeQueryValue("value1|value2", "pipeDelimited")) } func TestConstructKVFromMatrixCSV(t *testing.T) { // Test case 1: Empty input string values := "" props := ConstructKVFromMatrixCSV(values) require.NotNil(t, props) require.Empty(t, props) // Test case 2: Single valid key-value pair values = "key1=value1" props = ConstructKVFromMatrixCSV(values) require.NotNil(t, props) require.Equal(t, "value1", props["key1"]) // Test case 3: Multiple valid key-value pairs values = "key1=value1;key2=value2" props = ConstructKVFromMatrixCSV(values) require.NotNil(t, props) require.Equal(t, "value1", props["key1"]) require.Equal(t, "value2", props["key2"]) // Test case 4: Invalid key-value pair (missing equals) values = "key1=value1;key2" props = ConstructKVFromMatrixCSV(values) require.NotNil(t, props) require.Equal(t, "value1", props["key1"]) require.NotContains(t, props, "key2") // key2 should be ignored due to invalid format // Test case 5: Key-value pair where value needs to be cast to int and bool values = "key1=123;key2=true" props = ConstructKVFromMatrixCSV(values) require.NotNil(t, props) require.Equal(t, int64(123), props["key1"]) // cast to int require.Equal(t, true, props["key2"]) // cast to bool // Test case 6: Handle multiple valid and invalid key-value pairs values = "key1=value1;key2;key3=456;key4=false" props = ConstructKVFromMatrixCSV(values) require.NotNil(t, props) require.Equal(t, "value1", props["key1"]) // valid require.Equal(t, int64(456), props["key3"]) // valid require.Equal(t, false, props["key4"]) // valid require.NotContains(t, props, "key2") // invalid, missing value } func TestConstructParamMapFromFormEncodingArray(t *testing.T) { // Test case 1: Empty input values := []*QueryParam{} decoded := ConstructParamMapFromFormEncodingArray(values) require.NotNil(t, decoded) require.Empty(t, decoded) // Test case 2: Single QueryParam with valid key-value pairs values = []*QueryParam{ { Key: "param1", Values: []string{"key1,value1,key2,value2"}, }, } decoded = ConstructParamMapFromFormEncodingArray(values) require.NotNil(t, decoded) require.Contains(t, decoded, "param1") require.Equal(t, "value1", decoded["param1"].(map[string]interface{})["key1"]) require.Equal(t, "value2", decoded["param1"].(map[string]interface{})["key2"]) // Test case 3: Multiple QueryParam entries values = []*QueryParam{ { Key: "param1", Values: []string{"key1,value1"}, }, { Key: "param2", Values: []string{"key3,value3,key4,value4"}, }, } decoded = ConstructParamMapFromFormEncodingArray(values) require.NotNil(t, decoded) require.Contains(t, decoded, "param1") require.Equal(t, "value1", decoded["param1"].(map[string]interface{})["key1"]) require.Equal(t, "value3", decoded["param2"].(map[string]interface{})["key3"]) require.Equal(t, "value4", decoded["param2"].(map[string]interface{})["key4"]) // Test case 4: Odd number of values (incomplete key-value pair) values = []*QueryParam{ { Key: "param1", Values: []string{"key1,value1,key2"}, }, } decoded = ConstructParamMapFromFormEncodingArray(values) require.NotNil(t, decoded) require.Contains(t, decoded, "param1") require.Equal(t, "value1", decoded["param1"].(map[string]interface{})["key1"]) require.NotContains(t, decoded["param1"].(map[string]interface{}), "key2") // Invalid, no value for key2 // Test case 5: Casting different types (int, bool, string) values = []*QueryParam{ { Key: "param1", Values: []string{"key1,123,key2,true,key3,hello"}, }, } decoded = ConstructParamMapFromFormEncodingArray(values) require.NotNil(t, decoded) require.Contains(t, decoded, "param1") require.Equal(t, int64(123), decoded["param1"].(map[string]interface{})["key1"]) // cast to int require.Equal(t, true, decoded["param1"].(map[string]interface{})["key2"]) // cast to bool require.Equal(t, "hello", decoded["param1"].(map[string]interface{})["key3"]) // string remains string } func TestCastWithSchema(t *testing.T) { t.Run("returns string unchanged when schema property type is string", func(t *testing.T) { sch := &base.Schema{ Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "item_count": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), }), } result := castWithSchema("10", sch, "item_count") require.Equal(t, "10", result) }) t.Run("casts to int64 when schema property type is integer", func(t *testing.T) { sch := &base.Schema{ Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "count": base.CreateSchemaProxy(&base.Schema{Type: []string{"integer"}}), }), } result := castWithSchema("10", sch, "count") require.Equal(t, int64(10), result) }) t.Run("falls back to cast when no schema provided", func(t *testing.T) { result := castWithSchema("10", nil, "anything") require.Equal(t, int64(10), result) }) t.Run("falls back to cast when property not found in schema", func(t *testing.T) { sch := &base.Schema{ Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "other": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), }), } result := castWithSchema("10", sch, "missing") require.Equal(t, int64(10), result) }) t.Run("falls back to cast when schema has no properties", func(t *testing.T) { sch := &base.Schema{Type: []string{"object"}} result := castWithSchema("10", sch, "anything") require.Equal(t, int64(10), result) }) } func TestConstructParamMapFromQueryParamInputWithSchema(t *testing.T) { t.Run("preserves string '10' when schema says string", func(t *testing.T) { sch := &base.Schema{ Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "item_count": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), "search_term": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), }), } values := map[string][]*QueryParam{ "item_count": { {Key: "item_count", Values: []string{"10"}}, }, "search_term": { {Key: "search_term", Values: []string{"foo"}}, }, } decoded := ConstructParamMapFromQueryParamInputWithSchema(values, sch) require.Equal(t, "10", decoded["item_count"]) require.Equal(t, "foo", decoded["search_term"]) }) t.Run("casts numeric values when schema says integer", func(t *testing.T) { sch := &base.Schema{ Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "count": base.CreateSchemaProxy(&base.Schema{Type: []string{"integer"}}), }), } values := map[string][]*QueryParam{ "count": { {Key: "count", Values: []string{"42"}}, }, } decoded := ConstructParamMapFromQueryParamInputWithSchema(values, sch) require.Equal(t, int64(42), decoded["count"]) }) t.Run("falls back to heuristic when no schema", func(t *testing.T) { values := map[string][]*QueryParam{ "count": { {Key: "count", Values: []string{"42"}}, }, } decoded := ConstructParamMapFromQueryParamInputWithSchema(values, nil) require.Equal(t, int64(42), decoded["count"]) }) } func TestConstructParamMapFromDeepObjectEncoding_WithSchema(t *testing.T) { t.Run("preserves string values when schema property type is string", func(t *testing.T) { sch := &base.Schema{ Type: []string{"object"}, Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "prop1": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), }), } values := []*QueryParam{ {Key: "key1", Values: []string{"123"}, Property: "prop1"}, } decoded := ConstructParamMapFromDeepObjectEncoding(values, sch) require.Equal(t, "123", decoded["key1"].(map[string]interface{})["prop1"]) }) t.Run("preserves string values when root array items are strings", func(t *testing.T) { sch := &base.Schema{ Type: []string{"array"}, Items: &base.DynamicValue[*base.SchemaProxy, bool]{ A: base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), }, } require.Equal(t, "123", castWithSchema("123", sch, "unknown")) }) } func TestConstructParamMapFromPipeEncodingWithSchema(t *testing.T) { sch := &base.Schema{ Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "name": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), "count": base.CreateSchemaProxy(&base.Schema{Type: []string{"integer"}}), }), } params := []*QueryParam{ {Key: "key1", Values: []string{"name|123|count|42"}}, } result := ConstructParamMapFromPipeEncodingWithSchema(params, sch) props := result["key1"].(map[string]interface{}) require.Equal(t, "123", props["name"]) // string because schema says string require.Equal(t, int64(42), props["count"]) // int because schema says integer } func TestConstructParamMapFromSpaceEncodingWithSchema(t *testing.T) { sch := &base.Schema{ Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "name": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), }), } params := []*QueryParam{ {Key: "key1", Values: []string{"name 456"}}, } result := ConstructParamMapFromSpaceEncodingWithSchema(params, sch) props := result["key1"].(map[string]interface{}) require.Equal(t, "456", props["name"]) // string because schema says string } func TestConstructMapFromCSVWithSchema(t *testing.T) { sch := &base.Schema{ Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "id": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), "rank": base.CreateSchemaProxy(&base.Schema{Type: []string{"number"}}), }), } result := ConstructMapFromCSVWithSchema("id,99,rank,3.5", sch) require.Equal(t, "99", result["id"]) // string require.Equal(t, 3.5, result["rank"]) // number // odd number of values result = ConstructMapFromCSVWithSchema("id,99,rank", sch) require.Equal(t, "99", result["id"]) require.NotContains(t, result, "rank") } func TestConstructKVFromCSVWithSchema(t *testing.T) { sch := &base.Schema{ Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "key1": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), "key2": base.CreateSchemaProxy(&base.Schema{Type: []string{"integer"}}), }), } result := ConstructKVFromCSVWithSchema("key1=100,key2=200", sch) require.Equal(t, "100", result["key1"]) // string require.Equal(t, int64(200), result["key2"]) // integer } func TestConstructKVFromLabelEncodingWithSchema(t *testing.T) { sch := &base.Schema{ Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "key1": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), "key2": base.CreateSchemaProxy(&base.Schema{Type: []string{"boolean"}}), }), } result := ConstructKVFromLabelEncodingWithSchema("key1=true.key2=true", sch) require.Equal(t, "true", result["key1"]) // string because schema says string require.Equal(t, true, result["key2"]) // bool because schema says boolean // invalid pair (missing equals) is ignored result = ConstructKVFromLabelEncodingWithSchema("key1=val.key2", sch) require.Equal(t, "val", result["key1"]) require.NotContains(t, result, "key2") } func TestConstructKVFromMatrixCSVWithSchema(t *testing.T) { sch := &base.Schema{ Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "key1": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), }), } result := ConstructKVFromMatrixCSVWithSchema("key1=456;key2=789", sch) require.Equal(t, "456", result["key1"]) // string require.Equal(t, int64(789), result["key2"]) // no schema for key2, falls back to cast // invalid pair result = ConstructKVFromMatrixCSVWithSchema("key1=val;key2", sch) require.Equal(t, "val", result["key1"]) require.NotContains(t, result, "key2") } func TestConstructParamMapFromFormEncodingArrayWithSchema(t *testing.T) { sch := &base.Schema{ Properties: orderedmap.ToOrderedMap(map[string]*base.SchemaProxy{ "key1": base.CreateSchemaProxy(&base.Schema{Type: []string{"string"}}), "key2": base.CreateSchemaProxy(&base.Schema{Type: []string{"integer"}}), }), } values := []*QueryParam{ {Key: "param1", Values: []string{"key1,123,key2,456"}}, } decoded := ConstructParamMapFromFormEncodingArrayWithSchema(values, sch) props := decoded["param1"].(map[string]interface{}) require.Equal(t, "123", props["key1"]) // string require.Equal(t, int64(456), props["key2"]) // integer // odd number of values — incomplete pair ignored values = []*QueryParam{ {Key: "param1", Values: []string{"key1,val,key2"}}, } decoded = ConstructParamMapFromFormEncodingArrayWithSchema(values, sch) props = decoded["param1"].(map[string]interface{}) require.Equal(t, "val", props["key1"]) require.NotContains(t, props, "key2") } func TestEffectiveSecurityForOperation(t *testing.T) { globalSecurity := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "GlobalAuth": {}, }), }, } opSecurity := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "OpAuth": {}, }), }, } t.Run("operation-level security wins over global", func(t *testing.T) { pathItem := &v3.PathItem{ Get: &v3.Operation{Security: opSecurity}, } request, _ := http.NewRequest(http.MethodGet, "/", nil) result := EffectiveSecurityForOperation(request, pathItem, globalSecurity) require.Equal(t, opSecurity, result) }) t.Run("nil operation security falls back to global", func(t *testing.T) { pathItem := &v3.PathItem{ Get: &v3.Operation{}, // Security is nil } request, _ := http.NewRequest(http.MethodGet, "/", nil) result := EffectiveSecurityForOperation(request, pathItem, globalSecurity) require.Equal(t, globalSecurity, result) }) t.Run("empty operation security means no security (opt-out)", func(t *testing.T) { pathItem := &v3.PathItem{ Get: &v3.Operation{Security: []*base.SecurityRequirement{}}, } request, _ := http.NewRequest(http.MethodGet, "/", nil) result := EffectiveSecurityForOperation(request, pathItem, globalSecurity) require.NotNil(t, result) require.Len(t, result, 0) }) t.Run("both nil returns nil", func(t *testing.T) { pathItem := &v3.PathItem{ Get: &v3.Operation{}, } request, _ := http.NewRequest(http.MethodGet, "/", nil) result := EffectiveSecurityForOperation(request, pathItem, nil) require.Nil(t, result) }) t.Run("nil operation falls back to global", func(t *testing.T) { pathItem := &v3.PathItem{} // no Get operation request, _ := http.NewRequest(http.MethodGet, "/", nil) result := EffectiveSecurityForOperation(request, pathItem, globalSecurity) require.Equal(t, globalSecurity, result) }) t.Run("HEAD falls back to GET operation security", func(t *testing.T) { pathItem := &v3.PathItem{ Get: &v3.Operation{Security: opSecurity}, } request, _ := http.NewRequest(http.MethodHead, "/", nil) result := EffectiveSecurityForOperation(request, pathItem, globalSecurity) require.Equal(t, opSecurity, result) }) t.Run("HEAD with explicit Head security uses Head", func(t *testing.T) { headSecurity := []*base.SecurityRequirement{ { Requirements: orderedmap.ToOrderedMap(map[string][]string{ "HeadAuth": {}, }), }, } pathItem := &v3.PathItem{ Get: &v3.Operation{Security: opSecurity}, Head: &v3.Operation{Security: headSecurity}, } request, _ := http.NewRequest(http.MethodHead, "/", nil) result := EffectiveSecurityForOperation(request, pathItem, globalSecurity) require.Equal(t, headSecurity, result) }) } libopenapi-validator-0.13.8/helpers/path_finder.go000066400000000000000000000106171520534042400221700ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package helpers import ( "fmt" "strings" "unicode" "github.com/santhosh-tekuri/jsonschema/v6" ) // ExtractJSONPathFromValidationError traverses and processes a ValidationError to construct a JSONPath string representation of its instance location. func ExtractJSONPathFromValidationError(e *jsonschema.ValidationError) string { if len(e.Causes) > 0 { for _, cause := range e.Causes { ExtractJSONPathFromValidationError(cause) } } if len(e.InstanceLocation) > 0 { var b strings.Builder b.WriteString("$") for _, seg := range e.InstanceLocation { switch { case isNumeric(seg): fmt.Fprintf(&b, "[%s]", seg) case isSimpleIdentifier(seg): b.WriteByte('.') b.WriteString(seg) default: esc := escapeBracketString(seg) b.WriteString("['") b.WriteString(esc) b.WriteString("']") } } return b.String() } return "" } // isNumeric returns true if s is a non‐empty string of digits. func isNumeric(s string) bool { if s == "" { return false } for _, r := range s { if r < '0' || r > '9' { return false } } return true } // isSimpleIdentifier returns true if s matches [A-Za-z_][A-Za-z0-9_]*. func isSimpleIdentifier(s string) bool { for i, r := range s { if i == 0 { if !unicode.IsLetter(r) && r != '_' { return false } } else { if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' { return false } } } return len(s) > 0 } // escapeBracketString escapes backslashes and single‐quotes for inside ['...'] func escapeBracketString(s string) string { s = strings.ReplaceAll(s, `\`, `\\`) s = strings.ReplaceAll(s, `'`, `\'`) return s } // ExtractJSONPathsFromValidationErrors takes a slice of ValidationError pointers and returns a slice of JSONPath strings func ExtractJSONPathsFromValidationErrors(errors []*jsonschema.ValidationError) []string { var paths []string for _, err := range errors { path := ExtractJSONPathFromValidationError(err) if path != "" { paths = append(paths, path) } } return paths } // ExtractFieldNameFromInstanceLocation returns the last segment of the instance location as the field name func ExtractFieldNameFromInstanceLocation(instanceLocation []string) string { if len(instanceLocation) == 0 { return "" } return instanceLocation[len(instanceLocation)-1] } // ExtractFieldNameFromStringLocation returns the last segment of the instance location as the field name // when the location is provided as a string path func ExtractFieldNameFromStringLocation(instanceLocation string) string { if instanceLocation == "" { return "" } // Handle string format like "/properties/email" or "/0/name" segments := strings.Split(strings.Trim(instanceLocation, "/"), "/") if len(segments) == 0 || (len(segments) == 1 && segments[0] == "") { return "" } return segments[len(segments)-1] } // ExtractJSONPathFromInstanceLocation creates a JSONPath string from instance location segments func ExtractJSONPathFromInstanceLocation(instanceLocation []string) string { if len(instanceLocation) == 0 { return "" } var b strings.Builder b.WriteString("$") for _, seg := range instanceLocation { switch { case isNumeric(seg): fmt.Fprintf(&b, "[%s]", seg) case isSimpleIdentifier(seg): b.WriteByte('.') b.WriteString(seg) default: esc := escapeBracketString(seg) b.WriteString("['") b.WriteString(esc) b.WriteString("']") } } return b.String() } // ExtractJSONPathFromStringLocation creates a JSONPath string from string-based instance location func ExtractJSONPathFromStringLocation(instanceLocation string) string { if instanceLocation == "" { return "" } // Convert string format like "/properties/email" to array format segments := strings.Split(strings.Trim(instanceLocation, "/"), "/") if len(segments) == 0 || (len(segments) == 1 && segments[0] == "") { return "" } return ExtractJSONPathFromInstanceLocation(segments) } // ConvertStringLocationToPathSegments converts a string-based instance location to path segments array // Handles edge cases like empty strings and root-only paths func ConvertStringLocationToPathSegments(instanceLocation string) []string { if instanceLocation == "" { return []string{} } segments := strings.Split(strings.Trim(instanceLocation, "/"), "/") if len(segments) == 1 && segments[0] == "" { return []string{} } return segments } libopenapi-validator-0.13.8/helpers/path_finder_test.go000066400000000000000000000334601520534042400232300ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package helpers import ( "testing" "github.com/santhosh-tekuri/jsonschema/v6" "github.com/stretchr/testify/assert" ) func TestDiveIntoValidationError(t *testing.T) { tests := []struct { name string error *jsonschema.ValidationError expected string }{ { name: "empty instance location", error: &jsonschema.ValidationError{ InstanceLocation: []string{}, }, expected: "", }, { name: "numeric path segments", error: &jsonschema.ValidationError{ InstanceLocation: []string{"root", "array", "0", "1"}, }, expected: "$.root.array[0][1]", }, { name: "simple identifier path segments", error: &jsonschema.ValidationError{ InstanceLocation: []string{"user", "name", "first"}, }, expected: "$.user.name.first", }, { name: "complex path segments requiring escaping", error: &jsonschema.ValidationError{ InstanceLocation: []string{"user", "name-with-dash", "special'quote", "back\\slash"}, }, expected: "$.user['name-with-dash']['special\\'quote']['back\\\\slash']", }, { name: "mixed path segments", error: &jsonschema.ValidationError{ InstanceLocation: []string{"users", "0", "address", "street-name", "123"}, }, expected: "$.users[0].address['street-name'][123]", }, { name: "with nested causes", error: &jsonschema.ValidationError{ InstanceLocation: []string{"root"}, Causes: []*jsonschema.ValidationError{ { InstanceLocation: []string{"nested", "error"}, }, }, }, expected: "$.root", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ExtractJSONPathFromValidationError(tt.error) assert.Equal(t, tt.expected, result) }) } } func TestIsNumeric(t *testing.T) { tests := []struct { input string expected bool }{ {"123", true}, {"0", true}, {"01", true}, {"", false}, {"abc", false}, {"123abc", false}, {"12.3", false}, {"-123", false}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { result := isNumeric(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestIsSimpleIdentifier(t *testing.T) { tests := []struct { input string expected bool }{ {"abc", true}, {"a123", true}, {"_abc", true}, {"_123", true}, {"abc_123", true}, {"", false}, {"123abc", false}, {"abc-def", false}, {"abc.def", false}, {"abc def", false}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { result := isSimpleIdentifier(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestEscapeBracketString(t *testing.T) { tests := []struct { input string expected string }{ {"normal", "normal"}, {"with'quote", "with\\'quote"}, {"with\\backslash", "with\\\\backslash"}, {"with'quote\\and\\backslash", "with\\'quote\\\\and\\\\backslash"}, {"", ""}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { result := escapeBracketString(tt.input) assert.Equal(t, tt.expected, result) }) } } // TestDiveIntoValidationErrorRecursion tests that the function properly handles // recursive traversal through nested validation errors. func TestDiveIntoValidationErrorRecursion(t *testing.T) { childError1 := &jsonschema.ValidationError{ InstanceLocation: []string{"child1", "prop"}, } childError2 := &jsonschema.ValidationError{ InstanceLocation: []string{"child2", "0", "name"}, } parentError := &jsonschema.ValidationError{ InstanceLocation: []string{"parent"}, Causes: []*jsonschema.ValidationError{childError1, childError2}, } // The parent error should return its own path result := ExtractJSONPathFromValidationError(parentError) assert.Equal(t, "$.parent", result) // Verify the child errors return their paths correctly when called directly assert.Equal(t, "$.child1.prop", ExtractJSONPathFromValidationError(childError1)) assert.Equal(t, "$.child2[0].name", ExtractJSONPathFromValidationError(childError2)) } // TestDiveIntoValidationErrorEdgeCases tests edge cases including empty strings and unusual characters func TestDiveIntoValidationErrorEdgeCases(t *testing.T) { tests := []struct { name string error *jsonschema.ValidationError expected string }{ { name: "empty strings as elements", error: &jsonschema.ValidationError{ InstanceLocation: []string{"", "property"}, }, expected: "$[''].property", }, { name: "Unicode characters", error: &jsonschema.ValidationError{ InstanceLocation: []string{"🙂", "unicode_property"}, }, expected: "$['🙂'].unicode_property", }, { name: "null causes", error: &jsonschema.ValidationError{ InstanceLocation: []string{"root"}, Causes: nil, }, expected: "$.root", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ExtractJSONPathFromValidationError(tt.error) assert.Equal(t, tt.expected, result) }) } } // TestExtractJSONPathsFromValidationErrors tests the ExtractJSONPathsFromValidationErrors function func TestExtractJSONPathsFromValidationErrors(t *testing.T) { tests := []struct { name string errors []*jsonschema.ValidationError expected []string }{ { name: "nil errors", errors: nil, expected: nil, }, { name: "empty errors", errors: []*jsonschema.ValidationError{}, expected: nil, }, { name: "single error with empty path", errors: []*jsonschema.ValidationError{ { InstanceLocation: []string{}, }, }, expected: nil, }, { name: "single error with path", errors: []*jsonschema.ValidationError{ { InstanceLocation: []string{"root", "property"}, }, }, expected: []string{"$.root.property"}, }, { name: "multiple errors with paths", errors: []*jsonschema.ValidationError{ { InstanceLocation: []string{"users", "0", "name"}, }, { InstanceLocation: []string{"users", "1", "address", "street"}, }, }, expected: []string{"$.users[0].name", "$.users[1].address.street"}, }, { name: "mixed errors - some with empty paths", errors: []*jsonschema.ValidationError{ { InstanceLocation: []string{}, }, { InstanceLocation: []string{"users", "0", "name"}, }, { InstanceLocation: []string{}, }, }, expected: []string{"$.users[0].name"}, }, { name: "complex paths with special characters", errors: []*jsonschema.ValidationError{ { InstanceLocation: []string{"data", "special-field", "nested"}, }, { InstanceLocation: []string{"data", "array", "0", "item's", "property"}, }, }, expected: []string{"$.data['special-field'].nested", "$.data.array[0]['item\\'s'].property"}, }, { name: "with nested causes", errors: []*jsonschema.ValidationError{ { InstanceLocation: []string{"parent"}, Causes: []*jsonschema.ValidationError{ { InstanceLocation: []string{"child", "property"}, }, }, }, }, expected: []string{"$.parent"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ExtractJSONPathsFromValidationErrors(tt.errors) assert.Equal(t, tt.expected, result) }) } } func TestExtractFieldNameFromInstanceLocation(t *testing.T) { testCases := []struct { name string instancePath []string expected string }{ { name: "Empty path", instancePath: []string{}, expected: "", }, { name: "Single field", instancePath: []string{"name"}, expected: "name", }, { name: "Nested field", instancePath: []string{"user", "profile", "email"}, expected: "email", }, { name: "Array index", instancePath: []string{"users", "0", "name"}, expected: "name", }, { name: "Complex path", instancePath: []string{"root", "nested", "array", "1", "field"}, expected: "field", }, { name: "Field with special characters", instancePath: []string{"user", "email-address", "value"}, expected: "value", }, { name: "Numeric field name", instancePath: []string{"data", "123"}, expected: "123", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := ExtractFieldNameFromInstanceLocation(tc.instancePath) assert.Equal(t, tc.expected, result) }) } } func TestExtractJSONPathFromInstanceLocation(t *testing.T) { testCases := []struct { name string instancePath []string expected string }{ { name: "Empty path", instancePath: []string{}, expected: "", }, { name: "Simple field", instancePath: []string{"name"}, expected: "$.name", }, { name: "Nested object fields", instancePath: []string{"user", "profile", "email"}, expected: "$.user.profile.email", }, { name: "Array access", instancePath: []string{"users", "0", "name"}, expected: "$.users[0].name", }, { name: "Mixed array and object", instancePath: []string{"data", "items", "1", "properties", "value"}, expected: "$.data.items[1].properties.value", }, { name: "Field with dashes", instancePath: []string{"user", "email-address"}, expected: "$.user['email-address']", }, { name: "Field with spaces", instancePath: []string{"user", "full name"}, expected: "$.user['full name']", }, { name: "Field with special characters", instancePath: []string{"data", "field'with'quotes"}, expected: "$.data['field\\'with\\'quotes']", }, { name: "Field with backslash", instancePath: []string{"data", "field\\with\\backslash"}, expected: "$.data['field\\\\with\\\\backslash']", }, { name: "Unicode field name", instancePath: []string{"🙂", "unicode_field"}, expected: "$['🙂'].unicode_field", }, { name: "Numeric array indices", instancePath: []string{"matrix", "0", "1", "value"}, expected: "$.matrix[0][1].value", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := ExtractJSONPathFromInstanceLocation(tc.instancePath) assert.Equal(t, tc.expected, result) }) } } func TestExtractFieldNameFromStringLocation(t *testing.T) { testCases := []struct { name string instancePath string expected string }{ { name: "Empty path", instancePath: "", expected: "", }, { name: "Single field", instancePath: "/name", expected: "name", }, { name: "Nested field", instancePath: "/user/profile/email", expected: "email", }, { name: "Array index", instancePath: "/users/0/name", expected: "name", }, { name: "Complex path", instancePath: "/root/nested/array/1/field", expected: "field", }, { name: "Field with special characters", instancePath: "/user/email-address/value", expected: "value", }, { name: "Root path only", instancePath: "/", expected: "", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := ExtractFieldNameFromStringLocation(tc.instancePath) assert.Equal(t, tc.expected, result) }) } } func TestExtractJSONPathFromStringLocation(t *testing.T) { testCases := []struct { name string instancePath string expected string }{ { name: "Empty path", instancePath: "", expected: "", }, { name: "Simple field", instancePath: "/name", expected: "$.name", }, { name: "Nested object fields", instancePath: "/user/profile/email", expected: "$.user.profile.email", }, { name: "Array access", instancePath: "/users/0/name", expected: "$.users[0].name", }, { name: "Mixed array and object", instancePath: "/data/items/1/properties/value", expected: "$.data.items[1].properties.value", }, { name: "Root path only", instancePath: "/", expected: "", }, { name: "Complex nested path", instancePath: "/matrix/0/1/value", expected: "$.matrix[0][1].value", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := ExtractJSONPathFromStringLocation(tc.instancePath) assert.Equal(t, tc.expected, result) }) } } func TestConvertStringLocationToPathSegments(t *testing.T) { testCases := []struct { name string instancePath string expected []string }{ { name: "Empty string", instancePath: "", expected: []string{}, }, { name: "Root path only", instancePath: "/", expected: []string{}, }, { name: "Single field", instancePath: "/name", expected: []string{"name"}, }, { name: "Multiple fields", instancePath: "/user/profile/email", expected: []string{"user", "profile", "email"}, }, { name: "Array index", instancePath: "/users/0/name", expected: []string{"users", "0", "name"}, }, { name: "Multiple array indices", instancePath: "/matrix/0/1/value", expected: []string{"matrix", "0", "1", "value"}, }, { name: "Field with special characters", instancePath: "/user/email-address", expected: []string{"user", "email-address"}, }, { name: "Complex nested path", instancePath: "/data/items/1/properties/field-name", expected: []string{"data", "items", "1", "properties", "field-name"}, }, { name: "Leading and trailing slashes", instancePath: "///user/name///", expected: []string{"user", "name"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := ConvertStringLocationToPathSegments(tc.instancePath) assert.Equal(t, tc.expected, result) }) } } libopenapi-validator-0.13.8/helpers/regex_maker.go000066400000000000000000000077201520534042400221770ustar00rootroot00000000000000package helpers import ( "bytes" "fmt" "regexp" "strings" ) var ( baseDefaultPattern = "[^/]*" DefaultPatternRegex = regexp.MustCompile("^([^/]*)$") DefaultPatternRegexString = DefaultPatternRegex.String() ) // GetRegexForPath returns a compiled regular expression for the given path template. // // This function takes a path template string `tpl` and generates a regular expression // that matches the structure of the template. The template can include placeholders // enclosed in braces `{}` with optional custom patterns. // // Placeholders in the template can be defined as: // - `{name}`: Matches any sequence of characters except '/' // - `{name:pattern}`: Matches the specified custom pattern // // The function ensures that the template is well-formed, with balanced and properly // nested braces. If the template is invalid, an error is returned. // // Parameters: // - tpl: The path template string to convert into a regular expression. // // Returns: // - *regexp.Regexp: A compiled regular expression that matches the template. // - error: An error if the template is invalid or the regular expression cannot be compiled. // // Example: // // regex, err := GetRegexForPath("/orders/{id:[0-9]+}/items/{itemId}") // // regex: ^/orders/([0-9]+)/items/([^/]+)$ // // err: nil func GetRegexForPath(tpl string) (*regexp.Regexp, error) { // Check if it is well-formed. idxs, errBraces := BraceIndices(tpl) if errBraces != nil { return nil, errBraces } // Backup the original. template := tpl pattern := bytes.NewBufferString("^") var end int for i := 0; i < len(idxs); i += 2 { // Set all values we are interested in. raw := tpl[end:idxs[i]] end = idxs[i+1] parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) name := parts[0] patt := baseDefaultPattern if len(parts) == 2 { patt = parts[1] } // Name or pattern can't be empty. if name == "" || patt == "" { return nil, fmt.Errorf("missing name or pattern in %q", tpl[idxs[i]:end]) } // Build the regexp pattern. _, err := fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt) if err != nil { return nil, err } } // Add the remaining. raw := tpl[end:] pattern.WriteString(regexp.QuoteMeta(raw)) pattern.WriteByte('$') patternString := pattern.String() if patternString == DefaultPatternRegexString { return DefaultPatternRegex, nil } // Compile full regexp. reg, errCompile := regexp.Compile(patternString) if errCompile != nil { return nil, errCompile } // Check for capturing groups which used to work in older versions if reg.NumSubexp() != len(idxs)/2 { return nil, fmt.Errorf("route %s contains capture groups in its regexp. Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)", template) } // Done! return reg, nil } // BraceIndices returns the indices of the opening and closing braces in a string. // // It scans the input string `s` and identifies the positions of matching pairs // of braces ('{' and '}'). The function ensures that the braces are balanced // and properly nested. // // If the braces are unbalanced or improperly nested, an error is returned. // // Parameters: // - s: The input string to scan for braces. // // Returns: // - []int: A slice of integers where each pair of indices represents the // start and end positions of a matching pair of braces. // - error: An error if the braces are unbalanced or improperly nested. // // Example: // // indices, err := BraceIndices("/orders/{id}/items/{itemId}") // // indices: [8, 12, 19, 26] // // err: nil func BraceIndices(s string) ([]int, error) { var level, idx int var idxs []int for i := 0; i < len(s); i++ { switch s[i] { case '{': if level++; level == 1 { idx = i } case '}': if level--; level == 0 { idxs = append(idxs, idx, i+1) } else if level < 0 { return nil, fmt.Errorf("unbalanced braces in %q", s) } } } if level != 0 { return nil, fmt.Errorf("unbalanced braces in %q", s) } return idxs, nil } libopenapi-validator-0.13.8/helpers/regex_maker_test.go000066400000000000000000000064411520534042400232350ustar00rootroot00000000000000package helpers import ( "testing" "github.com/stretchr/testify/assert" ) func TestGetRegexForPath(t *testing.T) { tests := []struct { name string tpl string wantErr bool wantExpr string }{ { name: "well-formed template with default pattern", tpl: "/orders/{id}", wantErr: false, wantExpr: "^/orders/([^/]*)$", }, { name: "well-formed template with custom pattern", tpl: "/orders/{id:[0-9]+}", wantErr: false, wantExpr: "^/orders/([0-9]+)$", }, { name: "missing name in template", tpl: "/orders/{:pattern}", wantErr: true, }, { name: "missing pattern in template", tpl: "/orders/{name:}", wantErr: true, }, { name: "unbalanced braces in template", tpl: "/orders/{id", wantErr: true, }, { name: "unbalanced braces in template", tpl: "/orders/id}", wantErr: true, }, { name: "template with multiple variables", tpl: "/orders/{id:[0-9]+}/items/{itemId}", wantErr: false, wantExpr: "^/orders/([0-9]+)/items/([^/]*)$", }, { name: "OData formatted URL with single quotes", tpl: "/entities('{id}')", wantErr: false, wantExpr: "^/entities\\('([^/]*)'\\)$", }, { name: "OData formatted URL with custom pattern", tpl: "/entities('{id:[0-9]+}')", wantErr: false, wantExpr: "^/entities\\('([0-9]+)'\\)$", }, { name: "get default pattern", tpl: "/{param}", wantErr: false, wantExpr: "^/([^/]*)$", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := GetRegexForPath(tt.tpl) if (err != nil) != tt.wantErr { t.Errorf("GetRegexForPath() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && got.String() != tt.wantExpr { t.Errorf("GetRegexForPath() = %v, want %v", got.String(), tt.wantExpr) } }) } } func TestBraceIndices(t *testing.T) { tests := []struct { name string s string want []int wantErr bool }{ { name: "well-formed braces", s: "/orders/{id}/items/{itemId}", want: []int{8, 12, 19, 27}, wantErr: false, }, { name: "unbalanced braces", s: "/orders/{id/items/{itemId}", wantErr: true, }, { name: "unbalanced braces", s: "/orders/{id}/items/{itemId", wantErr: true, }, { name: "no braces", s: "/orders/id/items/itemId", want: []int{}, wantErr: false, }, { name: "OData formatted URL with single quotes", s: "/entities('{id}')", want: []int{11, 15}, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := BraceIndices(tt.s) if (err != nil) != tt.wantErr { t.Errorf("BraceIndices() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && !equal(got, tt.want) { t.Errorf("BraceIndices() = %v, want %v", got, tt.want) } }) } } func TestDefaultPatternCompileCache(t *testing.T) { res, err := GetRegexForPath("{param}") assert.Nil(t, err) assert.Equal(t, res, DefaultPatternRegex) assert.Equal(t, res.String(), DefaultPatternRegexString) } func equal(a, b []int) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } libopenapi-validator-0.13.8/helpers/schema_compiler.go000066400000000000000000000231321520534042400230330ustar00rootroot00000000000000package helpers import ( "bytes" "encoding/json" "fmt" "github.com/santhosh-tekuri/jsonschema/v6" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/openapi_vocabulary" ) // ConfigureCompiler configures a JSON Schema compiler with the desired behavior. func ConfigureCompiler(c *jsonschema.Compiler, o *config.ValidationOptions) { if o == nil { // Sanity return } // nil is the default so this is OK. c.UseRegexpEngine(o.RegexEngine) if o.FormatAssertions { c.AssertFormat() } if o.ContentAssertions { c.AssertContent() } for n, v := range o.Formats { c.RegisterFormat(&jsonschema.Format{ Name: n, Validate: v, }) } } // NewCompilerWithOptions mints a new JSON schema compiler with custom configuration. func NewCompilerWithOptions(o *config.ValidationOptions) *jsonschema.Compiler { c := jsonschema.NewCompiler() ConfigureCompiler(c, o) return c } // NewCompiledSchema establishes a programmatic representation of a JSON Schema document that is used for validation. // Defaults to OpenAPI 3.1+ behavior (strict JSON Schema compliance). func NewCompiledSchema(name string, jsonSchema []byte, o *config.ValidationOptions) (*jsonschema.Schema, error) { return NewCompiledSchemaWithVersion(name, jsonSchema, o, 3.1) } // NewCompiledSchemaWithVersion establishes a programmatic representation of a JSON Schema document that is used for validation. // The version parameter determines which OpenAPI keywords are allowed: // - version 3.0: Allows OpenAPI 3.0 keywords like 'nullable' // - version 3.1+: Rejects OpenAPI 3.0 keywords like 'nullable' (strict JSON Schema compliance) func NewCompiledSchemaWithVersion(name string, jsonSchema []byte, options *config.ValidationOptions, version float32) (*jsonschema.Schema, error) { compiler := NewCompilerWithOptions(options) compiler.UseLoader(NewCompilerLoader()) // register OpenAPI vocabulary with appropriate version and coercion settings if options != nil && options.OpenAPIMode { var vocabVersion openapi_vocabulary.VersionType if version >= 3.15 { // use 3.15 to avoid floating point precision issues (3.2+) vocabVersion = openapi_vocabulary.Version32 } else if version >= 3.05 { // use 3.05 to avoid floating point precision issues (3.1) vocabVersion = openapi_vocabulary.Version31 } else { vocabVersion = openapi_vocabulary.Version30 } vocab := openapi_vocabulary.NewOpenAPIVocabularyWithCoercion(vocabVersion, options.AllowScalarCoercion) compiler.RegisterVocabulary(vocab) compiler.AssertVocabs() if version < 3.05 { jsonSchema = transformOpenAPI30Schema(jsonSchema) } if options.AllowScalarCoercion { jsonSchema = transformSchemaForCoercion(jsonSchema) } } decodedSchema, err := jsonschema.UnmarshalJSON(bytes.NewReader(jsonSchema)) if err != nil { return nil, fmt.Errorf("failed to unmarshal JSON schema: %w", err) } if err = compiler.AddResource(name, decodedSchema); err != nil { return nil, fmt.Errorf("failed to add resource to schema compiler: %w", err) } jsch, err := compiler.Compile(name) if err != nil { return nil, fmt.Errorf("JSON schema compile failed: %s", err.Error()) } return jsch, nil } // transformOpenAPI30Schema transforms OpenAPI 3.0 schemas to JSON Schema 2020-12 compatible format. // Handles OAS 3.0-specific keywords: // - nullable: true → type array with "null" // - exclusiveMinimum/exclusiveMaximum: bool → numeric (draft-04 → 2020-12) func transformOpenAPI30Schema(jsonSchema []byte) []byte { var schema map[string]interface{} if err := json.Unmarshal(jsonSchema, &schema); err != nil { return jsonSchema } transformed := transformOAS30Keywords(schema) result, err := json.Marshal(transformed) if err != nil { return jsonSchema } return result } // transformOAS30Keywords recursively transforms OAS 3.0-specific keywords in a schema object func transformOAS30Keywords(schema interface{}) interface{} { switch s := schema.(type) { case map[string]interface{}: result := make(map[string]interface{}) // copy all properties first, recursing into nested schemas for key, value := range s { result[key] = transformOAS30Keywords(value) } // handle nullable keyword if nullable, ok := s["nullable"]; ok { if nullableBool, ok := nullable.(bool); ok { if nullableBool { result = transformNullableSchema(result) } else { delete(result, "nullable") } } } // handle exclusiveMinimum: bool → numeric transformExclusiveBound(result, "exclusiveMinimum", "minimum") // handle exclusiveMaximum: bool → numeric transformExclusiveBound(result, "exclusiveMaximum", "maximum") return result case []interface{}: result := make([]interface{}, len(s)) for i, item := range s { result[i] = transformOAS30Keywords(item) } return result default: return schema } } // transformExclusiveBound converts OAS 3.0 boolean exclusiveMinimum/exclusiveMaximum // to JSON Schema 2020-12 numeric form. // // OAS 3.0 (draft-04): minimum: 10, exclusiveMinimum: true → value must be > 10 // JSON Schema 2020-12: exclusiveMinimum: 10 → value must be > 10 func transformExclusiveBound(schema map[string]interface{}, exclusiveKey, boundKey string) { exVal, ok := schema[exclusiveKey] if !ok { return } exBool, isBool := exVal.(bool) if !isBool { return // already numeric (3.1 style), leave as-is } if exBool { // exclusiveMinimum: true + minimum: X → exclusiveMinimum: X (remove minimum) if bound, hasBound := schema[boundKey]; hasBound { schema[exclusiveKey] = bound delete(schema, boundKey) } else { // boolean true without a corresponding bound is invalid, just remove it delete(schema, exclusiveKey) } } else { // exclusiveMinimum: false is a no-op, just remove the keyword delete(schema, exclusiveKey) } } // transformNullableSchema transforms a schema with nullable: true to JSON Schema compatible format func transformNullableSchema(schema map[string]interface{}) map[string]interface{} { delete(schema, "nullable") // get the current type currentType, hasType := schema["type"] if hasType { // if there's already a type, convert it to include null switch t := currentType.(type) { case string: // convert "string" to ["string", "null"] schema["type"] = []interface{}{t, "null"} case []interface{}: // if it's already an array, add null if not present found := false for _, item := range t { if str, ok := item.(string); ok && str == "null" { found = true break } } if !found { newTypes := make([]interface{}, len(t)+1) copy(newTypes, t) newTypes[len(t)] = "null" schema["type"] = newTypes } } } allOf, hasAllOf := schema["allOf"] if hasAllOf { delete(schema, "allOf") oneOfAdditions := []interface{}{ map[string]interface{}{ "allOf": allOf, }, map[string]interface{}{ "type": "null", }, } var oneOfSlice []interface{} oneOf, hasOneOf := schema["oneOf"] if hasOneOf { oneOfSlice, _ = oneOf.([]interface{}) } oneOfSlice = append(oneOfSlice, oneOfAdditions...) schema["oneOf"] = oneOfSlice } // Handle enum values - add null if nullable but not already in enum enum, hasEnum := schema["enum"] if hasEnum { if enumSlice, ok := enum.([]interface{}); ok { // Check if null is already in enum hasNull := false for _, v := range enumSlice { if v == nil { hasNull = true break } } // Add null if not present if !hasNull { enumSlice = append(enumSlice, nil) schema["enum"] = enumSlice } } } return schema } // transformSchemaForCoercion transforms schemas to allow scalar coercion (string->boolean/number) func transformSchemaForCoercion(jsonSchema []byte) []byte { var schema map[string]interface{} if err := json.Unmarshal(jsonSchema, &schema); err != nil { // If we can't parse it, return as-is return jsonSchema } transformed := transformCoercionInSchema(schema) result, err := json.Marshal(transformed) if err != nil { return jsonSchema } return result } // transformCoercionInSchema recursively transforms schemas to support scalar coercion func transformCoercionInSchema(schema interface{}) interface{} { switch s := schema.(type) { case map[string]interface{}: result := make(map[string]interface{}) // copy all properties first for key, value := range s { result[key] = transformCoercionInSchema(value) } // transform type to allow string coercion for coercible types if schemaType, hasType := s["type"]; hasType { result["type"] = transformTypeForCoercion(schemaType) } return result case []interface{}: result := make([]interface{}, len(s)) for i, item := range s { result[i] = transformCoercionInSchema(item) } return result default: return schema } } // transformTypeForCoercion transforms type fields to allow string coercion func transformTypeForCoercion(schemaType interface{}) interface{} { switch t := schemaType.(type) { case string: // transform scalar types to include string for coercion if t == "boolean" || t == "number" || t == "integer" { return []interface{}{t, "string"} } return t case []interface{}: // if already an array, add string if it contains coercible types and doesn't already have string hasCoercibleType := false hasString := false for _, item := range t { if str, ok := item.(string); ok { if str == "boolean" || str == "number" || str == "integer" { hasCoercibleType = true } if str == "string" { hasString = true } } } if hasCoercibleType && !hasString { newTypes := make([]interface{}, len(t)+1) copy(newTypes, t) newTypes[len(t)] = "string" return newTypes } return t default: return schemaType } } libopenapi-validator-0.13.8/helpers/schema_compiler_test.go000066400000000000000000000570231520534042400241000ustar00rootroot00000000000000package helpers import ( "encoding/json" "fmt" "testing" "unicode" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pb33f/libopenapi-validator/config" ) // A few simple JSON Schemas const stringSchema = `{ "type": "string", "format": "date", "minLength": 10 }` const objectSchema = `{ "type": "object", "title" : "Fish", "properties" : { "name" : { "type": "string", "description": "The given name of the fish" }, "name" : { "type": "string", "format": "capital", "description": "The given name of the fish" }, "species" : { "type" : "string", "enum" : [ "OTHER", "GUPPY", "PIKE", "BASS" ] } } }` func Test_SchemaWithNilOptions(t *testing.T) { jsch, err := NewCompiledSchema("test", []byte(stringSchema), nil) require.NoError(t, err, "Failed to compile Schema") require.NotNil(t, jsch, "Did not return a compiled schema") } func Test_SchemaWithDefaultOptions(t *testing.T) { valOptions := config.NewValidationOptions() jsch, err := NewCompiledSchema("test", []byte(stringSchema), valOptions) require.NoError(t, err, "Failed to compile Schema") require.NotNil(t, jsch, "Did not return a compiled schema") } func Test_SchemaWithOptions(t *testing.T) { valOptions := config.NewValidationOptions( config.WithFormatAssertions(), config.WithContentAssertions(), config.WithCustomFormat("capital", func(v any) error { s, ok := v.(string) if !ok { return fmt.Errorf("expected string") } if s == "" { return nil } r := []rune(s)[0] if !unicode.IsUpper(r) { return fmt.Errorf("expected first latter to be uppercase") } return nil }), ) jsch, err := NewCompiledSchema("test", []byte(stringSchema), valOptions) require.NoError(t, err, "Failed to compile Schema") require.NotNil(t, jsch, "Did not return a compiled schema") } func Test_ObjectSchema(t *testing.T) { valOptions := config.NewValidationOptions() jsch, err := NewCompiledSchema("test", []byte(objectSchema), valOptions) require.NoError(t, err, "Failed to compile Schema") require.NotNil(t, jsch, "Did not return a compiled schema") } func Test_ValidJSONSchemaWithInvalidContent(t *testing.T) { // An example of a dubious JSON Schema const badSchema = `{ "type": "you-dont-know-me", "format": "date", "minLength": 10 }` jsch, err := NewCompiledSchema("test", []byte(badSchema), nil) assert.Error(t, err, "Expected an error to be thrown") assert.Nil(t, jsch, "invalid schema compiled!") } func Test_MalformedSONSchema(t *testing.T) { // An example of a JSON schema with malformed JSON const badSchema = `{ "type": "you-dont-know-me", "format": "date" "minLength": 10 }` jsch, err := NewCompiledSchema("test", []byte(badSchema), nil) assert.Error(t, err, "Expected an error to be thrown") assert.Nil(t, jsch, "invalid schema compiled!") } func Test_ValidJSONSchemaWithIncompleteContent(t *testing.T) { // An example of a dJSON schema with references to non-existent stuff const incompleteSchema = `{ "type": "object", "title" : "unresolvable", "properties" : { "name" : { "type": "string", }, "species" : { "$ref": "#/$defs/speciesEnum" } } }` jsch, err := NewCompiledSchema("test", []byte(incompleteSchema), nil) assert.Error(t, err, "Expected an error to be thrown") assert.Nil(t, jsch, "invalid schema compiled!") } // Additional comprehensive tests for version-aware schema compilation func TestNewCompiledSchemaWithVersion_OpenAPIMode_Version30(t *testing.T) { schemaJSON := `{ "type": "string", "nullable": true }` options := config.NewValidationOptions( config.WithOpenAPIMode(), ) // Test version 3.0 (< 3.05) jsch, err := NewCompiledSchemaWithVersion("test", []byte(schemaJSON), options, 3.0) require.NoError(t, err, "Should compile OpenAPI 3.0 schema with nullable") require.NotNil(t, jsch, "Should return compiled schema") } func TestNewCompiledSchemaWithVersion_OpenAPIMode_Version31(t *testing.T) { schemaJSON := `{ "type": "string" }` options := config.NewValidationOptions( config.WithOpenAPIMode(), ) // Test version 3.1 (>= 3.05) jsch, err := NewCompiledSchemaWithVersion("test", []byte(schemaJSON), options, 3.1) require.NoError(t, err, "Should compile OpenAPI 3.1 schema") require.NotNil(t, jsch, "Should return compiled schema") } func TestNewCompiledSchemaWithVersion_OpenAPIMode_Version32(t *testing.T) { schemaJSON := `{ "type": "string" }` options := config.NewValidationOptions( config.WithOpenAPIMode(), ) // Test version 3.2 (>= 3.15) jsch, err := NewCompiledSchemaWithVersion("test", []byte(schemaJSON), options, 3.2) require.NoError(t, err, "Should compile OpenAPI 3.2 schema") require.NotNil(t, jsch, "Should return compiled schema") } func TestNewCompiledSchemaWithVersion_OpenAPIMode_Version32_NullableRejected(t *testing.T) { schemaJSON := `{ "type": "string", "nullable": true }` options := config.NewValidationOptions( config.WithOpenAPIMode(), ) // Test version 3.2 (>= 3.15) with nullable should fail (same as 3.1+) jsch, err := NewCompiledSchemaWithVersion("test", []byte(schemaJSON), options, 3.2) assert.Error(t, err, "Should fail for nullable in OpenAPI 3.2") assert.Nil(t, jsch, "Should not return compiled schema") assert.Contains(t, err.Error(), "The `nullable` keyword is not supported in OpenAPI 3.1+") } func TestNewCompiledSchemaWithVersion_OpenAPIMode_Version31_NullableRejected(t *testing.T) { schemaJSON := `{ "type": "string", "nullable": true }` options := config.NewValidationOptions( config.WithOpenAPIMode(), ) // Test version 3.1 (>= 3.05) with nullable should fail jsch, err := NewCompiledSchemaWithVersion("test", []byte(schemaJSON), options, 3.1) assert.Error(t, err, "Should fail for nullable in OpenAPI 3.1") assert.Nil(t, jsch, "Should not return compiled schema") assert.Contains(t, err.Error(), "The `nullable` keyword is not supported in OpenAPI 3.1+") } func TestNewCompiledSchemaWithVersion_OpenAPIMode_ScalarCoercion(t *testing.T) { schemaJSON := `{ "type": "boolean" }` options := config.NewValidationOptions( config.WithOpenAPIMode(), config.WithScalarCoercion(), ) // Test with scalar coercion enabled jsch, err := NewCompiledSchemaWithVersion("test", []byte(schemaJSON), options, 3.0) require.NoError(t, err, "Should compile with scalar coercion") require.NotNil(t, jsch, "Should return compiled schema") // Test that coercion works err = jsch.Validate("true") assert.NoError(t, err, "Should allow string 'true' for boolean with coercion") err = jsch.Validate("invalid") assert.Error(t, err, "Should reject invalid boolean string") } func TestNewCompiledSchemaWithVersion_OpenAPIMode_NoScalarCoercion(t *testing.T) { schemaJSON := `{ "type": "boolean" }` options := config.NewValidationOptions( config.WithOpenAPIMode(), ) // Test with scalar coercion disabled (default) jsch, err := NewCompiledSchemaWithVersion("test", []byte(schemaJSON), options, 3.0) require.NoError(t, err, "Should compile without scalar coercion") require.NotNil(t, jsch, "Should return compiled schema") // Test that coercion doesn't work err = jsch.Validate("true") assert.Error(t, err, "Should reject string 'true' for boolean without coercion") err = jsch.Validate(true) assert.NoError(t, err, "Should accept actual boolean value") } func TestNewCompiledSchemaWithVersion_NonOpenAPIMode(t *testing.T) { schemaJSON := `{ "type": "string", "nullable": true }` options := config.NewValidationOptions() // OpenAPIMode is false by default // Test that OpenAPI keywords are ignored when not in OpenAPI mode jsch, err := NewCompiledSchemaWithVersion("test", []byte(schemaJSON), options, 3.0) require.NoError(t, err, "Should compile in non-OpenAPI mode") require.NotNil(t, jsch, "Should return compiled schema") // String values should pass when OpenAPI mode is disabled err = jsch.Validate("test") assert.NoError(t, err, "Should accept string values") // When OpenAPI mode is disabled, nullable is ignored by JSON Schema // The behavior with null depends on the JSON Schema version and mode } func TestTransformOpenAPI30Schema_ValidJSON(t *testing.T) { input := []byte(`{ "type": "string", "nullable": true }`) result := transformOpenAPI30Schema(input) var schema map[string]interface{} err := json.Unmarshal(result, &schema) require.NoError(t, err, "Result should be valid JSON") // Check that nullable was transformed schemaType, ok := schema["type"] assert.True(t, ok, "Should have type field") typeArray, ok := schemaType.([]interface{}) assert.True(t, ok, "Type should be an array") assert.Contains(t, typeArray, "string") assert.Contains(t, typeArray, "null") _, hasNullable := schema["nullable"] assert.False(t, hasNullable, "Should not have nullable field after transformation") } func TestTransformOpenAPI30Schema_InvalidJSON(t *testing.T) { input := []byte(`{invalid json}`) result := transformOpenAPI30Schema(input) // Should return original when invalid assert.Equal(t, input, result) } func TestTransformNullableInSchema_MapWithNullableTrue(t *testing.T) { schema := map[string]interface{}{ "type": "string", "nullable": true, } result := transformOAS30Keywords(schema) resultMap, ok := result.(map[string]interface{}) require.True(t, ok) schemaType, ok := resultMap["type"] require.True(t, ok) typeArray, ok := schemaType.([]interface{}) require.True(t, ok) assert.Contains(t, typeArray, "string") assert.Contains(t, typeArray, "null") _, hasNullable := resultMap["nullable"] assert.False(t, hasNullable) } func TestTransformNullableInSchema_MapWithNullableFalse(t *testing.T) { schema := map[string]interface{}{ "type": "string", "nullable": false, } result := transformOAS30Keywords(schema) resultMap, ok := result.(map[string]interface{}) require.True(t, ok) // nullable: false should just remove nullable, keep type as is schemaType, ok := resultMap["type"] require.True(t, ok) assert.Equal(t, "string", schemaType) _, hasNullable := resultMap["nullable"] assert.False(t, hasNullable) } func TestTransformNullableInSchema_Array(t *testing.T) { schema := []interface{}{ map[string]interface{}{ "type": "string", "nullable": true, }, "other-item", } result := transformOAS30Keywords(schema) resultArray, ok := result.([]interface{}) require.True(t, ok) assert.Len(t, resultArray, 2) firstItem, ok := resultArray[0].(map[string]interface{}) require.True(t, ok) schemaType := firstItem["type"].([]interface{}) assert.Contains(t, schemaType, "string") assert.Contains(t, schemaType, "null") _, hasNullable := firstItem["nullable"] assert.False(t, hasNullable) } func TestTransformNullableInSchema_OtherTypes(t *testing.T) { stringSchema := "string-value" result := transformOAS30Keywords(stringSchema) assert.Equal(t, stringSchema, result) numberSchema := 123 result = transformOAS30Keywords(numberSchema) assert.Equal(t, numberSchema, result) var nilSchema interface{} = nil result = transformOAS30Keywords(nilSchema) assert.Equal(t, nilSchema, result) } func TestTransformExclusiveBound_TrueWithBound(t *testing.T) { schema := map[string]interface{}{ "type": "number", "minimum": float64(10), "exclusiveMinimum": true, } transformExclusiveBound(schema, "exclusiveMinimum", "minimum") assert.Equal(t, float64(10), schema["exclusiveMinimum"]) _, hasMin := schema["minimum"] assert.False(t, hasMin) } func TestTransformExclusiveBound_TrueWithoutBound(t *testing.T) { schema := map[string]interface{}{ "type": "number", "exclusiveMinimum": true, } transformExclusiveBound(schema, "exclusiveMinimum", "minimum") _, hasExMin := schema["exclusiveMinimum"] assert.False(t, hasExMin) } func TestTransformExclusiveBound_False(t *testing.T) { schema := map[string]interface{}{ "type": "number", "minimum": float64(10), "exclusiveMinimum": false, } transformExclusiveBound(schema, "exclusiveMinimum", "minimum") _, hasExMin := schema["exclusiveMinimum"] assert.False(t, hasExMin) assert.Equal(t, float64(10), schema["minimum"]) } func TestTransformExclusiveBound_NotPresent(t *testing.T) { schema := map[string]interface{}{ "type": "number", "minimum": float64(10), } transformExclusiveBound(schema, "exclusiveMinimum", "minimum") assert.Equal(t, float64(10), schema["minimum"]) } func TestTransformExclusiveBound_AlreadyNumeric(t *testing.T) { schema := map[string]interface{}{ "type": "number", "exclusiveMinimum": float64(10), } transformExclusiveBound(schema, "exclusiveMinimum", "minimum") assert.Equal(t, float64(10), schema["exclusiveMinimum"]) } func TestTransformExclusiveBound_Maximum(t *testing.T) { schema := map[string]interface{}{ "type": "number", "maximum": float64(100), "exclusiveMaximum": true, } transformExclusiveBound(schema, "exclusiveMaximum", "maximum") assert.Equal(t, float64(100), schema["exclusiveMaximum"]) _, hasMax := schema["maximum"] assert.False(t, hasMax) } func TestTransformOAS30Keywords_ExclusiveMinMaxRecursive(t *testing.T) { schema := map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "price": map[string]interface{}{ "type": "number", "minimum": float64(0), "exclusiveMinimum": true, "maximum": float64(1000), "exclusiveMaximum": true, }, }, } result := transformOAS30Keywords(schema) resultMap := result.(map[string]interface{}) props := resultMap["properties"].(map[string]interface{}) price := props["price"].(map[string]interface{}) assert.Equal(t, float64(0), price["exclusiveMinimum"]) assert.Equal(t, float64(1000), price["exclusiveMaximum"]) _, hasMin := price["minimum"] assert.False(t, hasMin) _, hasMax := price["maximum"] assert.False(t, hasMax) } func TestTransformNullableSchema_ArrayType(t *testing.T) { schema := map[string]interface{}{ "type": []interface{}{"string", "number"}, "nullable": true, } result := transformNullableSchema(schema) schemaType, ok := result["type"] require.True(t, ok) typeArray, ok := schemaType.([]interface{}) require.True(t, ok) assert.Contains(t, typeArray, "string") assert.Contains(t, typeArray, "number") assert.Contains(t, typeArray, "null") _, hasNullable := result["nullable"] assert.False(t, hasNullable) } func TestTransformNullableSchema_ArrayTypeWithNull(t *testing.T) { schema := map[string]interface{}{ "type": []interface{}{"string", "null"}, "nullable": true, } result := transformNullableSchema(schema) schemaType, ok := result["type"] require.True(t, ok) typeArray, ok := schemaType.([]interface{}) require.True(t, ok) assert.Contains(t, typeArray, "string") assert.Contains(t, typeArray, "null") // Should not have duplicate "null" nullCount := 0 for _, item := range typeArray { if item == "null" { nullCount++ } } assert.Equal(t, 1, nullCount) _, hasNullable := result["nullable"] assert.False(t, hasNullable) } func TestTransformNullableSchema_NullableAllOf(t *testing.T) { schema := map[string]interface{}{ "type": []interface{}{"object"}, "allOf": []interface{}{ map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "id": map[string]interface{}{ "type": "string", }, }, }, }, "nullable": true, } result := transformNullableSchema(schema) schemaType, ok := result["type"] require.True(t, ok) typeArray, ok := schemaType.([]interface{}) require.True(t, ok) assert.Contains(t, typeArray, "object") assert.Contains(t, typeArray, "null") oneOf, ok := result["oneOf"] require.True(t, ok) oneOfSlice, ok := oneOf.([]interface{}) require.True(t, ok) assert.Len(t, oneOfSlice, 2) assert.Contains(t, oneOfSlice, map[string]interface{}{ "allOf": []interface{}{ map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "id": map[string]interface{}{ "type": "string", }, }, }, }, }) assert.Contains(t, oneOfSlice, map[string]interface{}{ "type": "null", }) _, hasNullable := result["nullable"] assert.False(t, hasNullable) } func TestTransformNullableSchema_NullableAllOfWithExistingOneOf(t *testing.T) { schema := map[string]interface{}{ "type": []interface{}{"object"}, "allOf": []interface{}{ map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "id": map[string]interface{}{ "type": "string", }, }, }, }, "oneOf": []interface{}{ map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "id": map[string]interface{}{ "type": "string", "const": []any{"val"}, }, }, }, }, "nullable": true, } result := transformNullableSchema(schema) schemaType, ok := result["type"] require.True(t, ok) typeArray, ok := schemaType.([]interface{}) require.True(t, ok) assert.Contains(t, typeArray, "object") assert.Contains(t, typeArray, "null") oneOf, ok := result["oneOf"] require.True(t, ok) oneOfSlice, ok := oneOf.([]interface{}) require.True(t, ok) assert.Len(t, oneOfSlice, 3) assert.Contains(t, oneOfSlice, map[string]interface{}{ "allOf": []interface{}{ map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "id": map[string]interface{}{ "type": "string", }, }, }, }, }) assert.Contains(t, oneOfSlice, map[string]interface{}{ "type": "null", }) assert.Contains(t, oneOfSlice, map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "id": map[string]interface{}{ "type": "string", "const": []any{"val"}, }, }, }) _, hasNullable := result["nullable"] assert.False(t, hasNullable) } func TestTransformSchemaForCoercion_ValidJSON(t *testing.T) { input := []byte(`{ "type": "boolean" }`) result := transformSchemaForCoercion(input) var schema map[string]interface{} err := json.Unmarshal(result, &schema) require.NoError(t, err, "Result should be valid JSON") // Check that type was transformed to include string schemaType, ok := schema["type"] assert.True(t, ok, "Should have type field") typeArray, ok := schemaType.([]interface{}) assert.True(t, ok, "Type should be an array") assert.Contains(t, typeArray, "boolean") assert.Contains(t, typeArray, "string") } func TestTransformSchemaForCoercion_InvalidJSON(t *testing.T) { input := []byte(`{invalid json}`) result := transformSchemaForCoercion(input) // Should return original when invalid assert.Equal(t, input, result) } func TestTransformOpenAPI30Schema_MarshalError(t *testing.T) { // Create a transformation that could potentially cause marshal issues // This is hard to test because Go's json.Marshal rarely fails // The error path exists for defensive programming input := []byte(`{ "type": "string", "nullable": true }`) result := transformOpenAPI30Schema(input) // Should return valid transformed JSON even if marshal could theoretically fail var schema map[string]interface{} err := json.Unmarshal(result, &schema) assert.NoError(t, err) } func TestTransformSchemaForCoercion_MarshalError(t *testing.T) { // Create a transformation that could potentially cause marshal issues // This is hard to test because Go's json.Marshal rarely fails // The error path exists for defensive programming input := []byte(`{ "type": "boolean" }`) result := transformSchemaForCoercion(input) // Should return valid transformed JSON even if marshal could theoretically fail var schema map[string]interface{} err := json.Unmarshal(result, &schema) assert.NoError(t, err) } func TestTransformCoercionInSchema_Array(t *testing.T) { schema := []interface{}{ map[string]interface{}{ "type": "number", }, "other-item", } result := transformCoercionInSchema(schema) resultArray, ok := result.([]interface{}) require.True(t, ok) assert.Len(t, resultArray, 2) firstItem, ok := resultArray[0].(map[string]interface{}) require.True(t, ok) schemaType := firstItem["type"].([]interface{}) assert.Contains(t, schemaType, "number") assert.Contains(t, schemaType, "string") } func TestTransformCoercionInSchema_OtherTypes(t *testing.T) { stringSchema := "string-value" result := transformCoercionInSchema(stringSchema) assert.Equal(t, stringSchema, result) } func TestTransformTypeForCoercion_ArrayWithNonStringItems(t *testing.T) { input := []interface{}{"boolean", 123, "null"} result := transformTypeForCoercion(input) typeArray, ok := result.([]interface{}) require.True(t, ok) assert.Contains(t, typeArray, "boolean") assert.Contains(t, typeArray, 123) assert.Contains(t, typeArray, "null") assert.Contains(t, typeArray, "string") } func TestTransformTypeForCoercion_OtherTypes(t *testing.T) { result := transformTypeForCoercion(123) assert.Equal(t, 123, result) result = transformTypeForCoercion(nil) assert.Equal(t, nil, result) result = transformTypeForCoercion(map[string]interface{}{}) assert.Equal(t, map[string]interface{}{}, result) } func TestTransformTypeForCoercion_EdgeCases(t *testing.T) { result := transformTypeForCoercion("fudge") assert.Equal(t, "fudge", result) result = transformTypeForCoercion([]interface{}{"string"}) assert.Equal(t, []interface{}{"string"}, result) } func TestTransformNullableSchema_EnumWithoutNull(t *testing.T) { // Test case: nullable: true with enum that doesn't contain null // Expected: null should be automatically added to the enum schema := map[string]interface{}{ "type": "string", "enum": []interface{}{ "active", "inactive", "pending", "archived", }, "nullable": true, } result := transformNullableSchema(schema) // nullable keyword should be removed _, hasNullable := result["nullable"] assert.False(t, hasNullable) // type should be converted to array including null schemaType, ok := result["type"] require.True(t, ok) typeArray, ok := schemaType.([]interface{}) require.True(t, ok) assert.Contains(t, typeArray, "string") assert.Contains(t, typeArray, "null") // enum should contain null enum, ok := result["enum"] require.True(t, ok) enumSlice, ok := enum.([]interface{}) require.True(t, ok) assert.Len(t, enumSlice, 5) // original 4 values + null assert.Contains(t, enumSlice, "active") assert.Contains(t, enumSlice, "inactive") assert.Contains(t, enumSlice, "pending") assert.Contains(t, enumSlice, "archived") assert.Contains(t, enumSlice, nil) } func TestTransformNullableSchema_EnumWithNull(t *testing.T) { // Test case: nullable: true with enum that already contains null // Expected: null should NOT be added twice schema := map[string]interface{}{ "type": "string", "enum": []interface{}{ "active", "inactive", "pending", "archived", nil, }, "nullable": true, } result := transformNullableSchema(schema) // nullable keyword should be removed _, hasNullable := result["nullable"] assert.False(t, hasNullable) // type should be converted to array including null schemaType, ok := result["type"] require.True(t, ok) typeArray, ok := schemaType.([]interface{}) require.True(t, ok) assert.Contains(t, typeArray, "string") assert.Contains(t, typeArray, "null") // enum should still contain only one null (not duplicated) enum, ok := result["enum"] require.True(t, ok) enumSlice, ok := enum.([]interface{}) require.True(t, ok) assert.Len(t, enumSlice, 5) // original 5 values (no duplication) assert.Contains(t, enumSlice, "active") assert.Contains(t, enumSlice, "inactive") assert.Contains(t, enumSlice, "pending") assert.Contains(t, enumSlice, "archived") // Count how many nulls are in the enum nullCount := 0 for _, v := range enumSlice { if v == nil { nullCount++ } } assert.Equal(t, 1, nullCount, "enum should contain exactly one null value") } libopenapi-validator-0.13.8/helpers/url_loader.go000066400000000000000000000030031520534042400220240ustar00rootroot00000000000000// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package helpers import ( "crypto/tls" "fmt" "net/http" "time" "github.com/santhosh-tekuri/jsonschema/v6" ) // HTTPURLLoader is a type that implements the Loader interface for loading schemas from HTTP URLs. // this change was made in jsonschema v6. The httploader package was removed and the HTTPURLLoader // type was introduced. // https://github.com/santhosh-tekuri/jsonschema/blob/boon/example_http_test.go // TODO: make all this stuff configurable, right now it's all hard wired and not very flexible. // // use interfaces and abstractions on all this. type HTTPURLLoader http.Client func (l *HTTPURLLoader) Load(url string) (any, error) { client := (*http.Client)(l) resp, err := client.Get(url) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { _ = resp.Body.Close() return nil, fmt.Errorf("%s returned status code %d", url, resp.StatusCode) } defer resp.Body.Close() return jsonschema.UnmarshalJSON(resp.Body) } func NewHTTPURLLoader(insecure bool) *HTTPURLLoader { httpLoader := HTTPURLLoader(http.Client{ Timeout: 15 * time.Second, }) if insecure { httpLoader.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } } return &httpLoader } func NewCompilerLoader() jsonschema.SchemeURLLoader { return jsonschema.SchemeURLLoader{ "file": jsonschema.FileLoader{}, "http": NewHTTPURLLoader(false), "https": NewHTTPURLLoader(false), } } libopenapi-validator-0.13.8/helpers/url_loader_test.go000066400000000000000000000060041520534042400230670ustar00rootroot00000000000000// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package helpers import ( "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/require" ) // Test the Load function for a successful case func TestHTTPURLLoader_Load_Success(t *testing.T) { // Create a mock HTTP server that returns a 200 response server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"success": true}`) })) defer server.Close() loader := NewHTTPURLLoader(false) // Test the Load function _, err := loader.Load(server.URL) require.NoError(t, err) } // Test the Load function when the server returns a non-200 response func TestHTTPURLLoader_Load_NonOKStatus(t *testing.T) { // Create a mock HTTP server that returns a 404 response server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "not found", http.StatusNotFound) })) defer server.Close() loader := NewHTTPURLLoader(false) // Test the Load function _, err := loader.Load(server.URL) require.Error(t, err) require.Contains(t, err.Error(), "returned status code 404") } // Test the Load function when the server returns an error func TestHTTPURLLoader_Load_Error(t *testing.T) { loader := NewHTTPURLLoader(false) // Test the Load function with an invalid URL _, err := loader.Load("http://invalid-url") require.Error(t, err) } // Test the Load function with an insecure TLS config func TestHTTPURLLoader_Load_Insecure(t *testing.T) { // Create a mock HTTPS server server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"secure": true}`) })) defer server.Close() loader := NewHTTPURLLoader(true) // Test the Load function _, err := loader.Load(server.URL) require.NoError(t, err) } // Test the NewHTTPURLLoader function with insecure set to false func TestNewHTTPURLLoader_Secure(t *testing.T) { loader := NewHTTPURLLoader(false) require.NotNil(t, loader) // Assert that the loader has the correct timeout and secure transport client := (*http.Client)(loader) require.Equal(t, 15*time.Second, client.Timeout) require.Nil(t, client.Transport) // Transport should be nil when secure } // Test the NewHTTPURLLoader function with insecure set to true func TestNewHTTPURLLoader_Insecure(t *testing.T) { loader := NewHTTPURLLoader(true) require.NotNil(t, loader) // Assert that the loader has an insecure transport configuration client := (*http.Client)(loader) transport, ok := client.Transport.(*http.Transport) require.True(t, ok) require.NotNil(t, transport.TLSClientConfig) require.True(t, transport.TLSClientConfig.InsecureSkipVerify) } // Test the NewCompilerLoader function func TestNewCompilerLoader(t *testing.T) { loader := NewCompilerLoader() require.NotNil(t, loader) // Assert that the loader contains the correct schemes require.NotNil(t, loader["http"]) require.NotNil(t, loader["https"]) require.NotNil(t, loader["file"]) } libopenapi-validator-0.13.8/helpers/version.go000066400000000000000000000004021520534042400213610ustar00rootroot00000000000000package helpers import ( "strings" ) // VersionToFloat converts a version string to a float32 for easier comparison. func VersionToFloat(version string) float32 { switch { case strings.HasPrefix(version, "3.0"): return 3.0 default: return 3.1 } } libopenapi-validator-0.13.8/helpers/version_test.go000066400000000000000000000017671520534042400224370ustar00rootroot00000000000000package helpers import ( "testing" "github.com/stretchr/testify/assert" ) func TestVersionToFloat(t *testing.T) { tests := []struct { name string version string expected float32 }{ { name: "OpenAPI 3.0", version: "3.0", expected: 3.0, }, { name: "OpenAPI 3.0.0", version: "3.0.0", expected: 3.0, }, { name: "OpenAPI 3.0.3", version: "3.0.3", expected: 3.0, }, { name: "OpenAPI 3.1", version: "3.1", expected: 3.1, }, { name: "OpenAPI 3.1.0", version: "3.1.0", expected: 3.1, }, { name: "OpenAPI 3.1.1", version: "3.1.1", expected: 3.1, }, { name: "default to 3.1 for unknown version", version: "4.0", expected: 3.1, }, { name: "default to 3.1 for empty string", version: "", expected: 3.1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := VersionToFloat(tt.version) assert.Equal(t, tt.expected, result) }) } } libopenapi-validator-0.13.8/libopenapi-logo.png000066400000000000000000002532571520534042400215130ustar00rootroot00000000000000‰PNG  IHDRÍŪ`å6)gAMAą üa IiCCPsRGB IEC61966-2.1H‰SwX“÷>ß÷eVBØđą—l"#ŦČYĸ’a„@҈ VœHUÄ‚Õ Hˆâ (¸gAŠˆZ‹U\8îܧĩ}zīííû×ûŧįœįüÎyĪ€&‘æĸj9R…<:؏OHÄÉŊ€Hā æËÂgÅđyx~t°?ü¯opÕ.$Įá˙ƒēP&W ‘ā"į RČ.TČȰSŗd ”ly|B"Ē ėôI>ØŠ“ÜØĸŠ™(G$@ģ`UR,Ā Ŧ@".ĀŽ€Yļ2G€ŊvŽX@`€™B,Ė 8CÍ L 0ŌŋāŠ_p…¸HĀ˕͗KŌ3¸•Đwōđāâ!âÂląBa)f ä"œ—›#HįLÎ ųŅÁū8?įæäáæfįlīôÅĸūkđo">!ņßūŧŒNĪīÚ_ååÖpĮ°uŋkŠ[ÚVhßų]3Û  Z Đzų‹y8ü@žĄPČ< í%bĄŊ0ã‹>˙3áoā‹~öü@ūÛzđqš@™­ĀŖƒũqanvŽRŽįËB1n÷į#ūĮ…ũŽ)Ņâ4ą\,ŠņX‰¸P"MĮyšR‘D!ɕâé2ņ–ũ “w Ŧ†OĀNļĩËlĀ~î‹XŌv@~ķ-Œ ‘g42y÷“ŋų@+͗¤ãŧč\¨”LÆD *°A ÁŦĀœÁŧĀaD@ $Ā<Bä€ Ą–ATĀ:Øĩ° šá´Á18 įā\ëp`žÂŧ† AČa!:ˆbŽØ"ΙŽ"aH4’€¤ éˆQ"ÅČr¤ŠBj‘]H#ō-r9\@úÛČ 2ŠüŠŧG1”˛QÔu@š¨ŠÆ sŅt4]€–ĸkŅ´=€ļĸ§ŅKčut}ŠŽc€Ņ1fŒŲa\Œ‡E`‰X&ĮcåX5V5cX7vĀžaī$‹€ė^„Âl‚GXLXC¨%ė#´ēW ƒ„1Â'"“¨O´%zųÄxb:ąXFŦ&î!!ž%^'_“H$ɒäN !%2I IkHÛH-¤S¤>ŌiœL&ëmÉŪ䲀Ŧ —‘ˇO’ûÉÃäˇ:ňâL ĸ$R¤”J5e?åĨŸ2B™ ĒQÍŠžÔLj:ŸZIm vP/S‡Š4uš%͛Cˤ-ŖÕКigi÷h/étē ŨƒE—Đ—Ōkčéįéƒôw † ƒĮHb(k{§ˇ/™LĻĶ—™ČT0×2™g˜˜oUX*ö*|‘Ę•:•V•~•įĒTUsU?ÕyĒ TĢUĢ^V}ĻFUŗP㊠ÔĢÕŠUģŠ6ŽÎRwRPĪQ_Ŗž_ũ‚úc ˛†…F †HŖTcˇÆ!Æ2eņXBÖrVë,k˜Mb[˛ųėLvûv/{LSCsĒfŦf‘fæqÍÆąāđ9؜JÎ!Î Î{--?-ąÖj­f­~­7ÚzÚžÚbírííëÚīup@,õ:m:÷u ē6ēQē…ēÛuĪę>Ķcëyé õĘõéŨŅGõmôŖõęīÖīŅ7046l18cđːcčk˜i¸Ņđ„á¨Ëhē‘ÄhŖŅIŖ'¸&î‡gã5x>fŦobŦ4ŪeÜkVyVõV×ŦIÖ\ë,ëmÖWlPW› ›:›Ë›­Äv›mßâ)Ō)õSnÚ1ėüė ėšėí9öaö%ömöĪĖÖ;t;|rtuĖvlpŧë¤á4ÊĊÃéWggĄsķ5ĻKË—v—Sm§Š§nŸz˕åîēŌĩĶõŖ›ģ›Ü­ŲmÔŨĖ=Å}ĢûM.›É]Ã=īAôđ÷XâqĖ㝧›§Âķį/^v^Y^ûŊOŗœ&žÖ0mČÛÄ[āŊË{`:>=eúÎé>Æ>ŸzŸ‡žĻž"ß=ž#~Ö~™~üžû;úËũøŋáyōņN`ÁåŊŗk™Ĩ5ģ/ >B Yr“oĀōųc3Üg,šŅʝZú0Ė&LÖކĪß~oĻųLéĖļˆāGlˆ¸i™ų})*2Ē.ęQ´Stqt÷,ÖŦäYûgŊŽņŠŒš;ÛjļrvgŦjlRlc뛏€¸Ē¸x‡øEņ—t$ í‰äÄØÄ=‰ãsįlš3œäšT–tcŽåÜĸšæéÎ˞wį|ūü/÷„ķû-G8Ī cHRMz&€„ú€ču0ę`:˜pœēQ< pHYs  šœKĐIDATxœėŨwx\×qđáßšm;z#ĀŪĢ(R%BŊBÕr…ĮŽ{‰“¸$.‰ķŲN\ãÛąc'qKlËEp‘d ęĸ(ŠIą÷ĸÛ÷Ū{Î÷Į)É&% į}žE ܝŨ…ŗŗsf„R MĶ4MĶ4MĶNĖ(všĻišĻiš6Úé¤YĶ4MĶ4MĶ^…Nš5MĶ4MĶ4íUč¤YĶ4MĶ4MĶ^…Uė4MĶ´ņŠąŠŲ ø95U)j)(U ÔĨąčp!v›|[k‹_ŧČ5MtŌŦišĻ‡a,UŌ›ëō }ˇPâ{~B l„ˆŽR¤„ g;ጉt a<Ĩ¤|x¸Øákš6ž=rNĶ4M)MÍį!nDˆĢŨlfv6ŲW.„ 'Jp"1ėp„xE †ecŲū2É>ōé$™ž^,ĮņĸĨžSR~š­ĩåébß'MĶÆ4kšĻiÃŽąŠų5ņfŋŋÂÍeËĨôÃ%UĩTOŸGåä™TLœJYí$â•a ËJy¸9ŽŊ8´e-ûׯĄk×Ėp'–čJ}I)õŸm­-˛¸÷PĶ´ŗNš5MĶ´aĶØÔ|ƒâcžë^˜KõĮÂņRQ7{!Ķ–]Â„Ų (ŸPąņ<遒đ˛K"¸Ø6ŽK˛#ÅæGaŨ}-dúēˆ”Tä„_QJ}ĩ­ĩ%_ŦûŠiÚŲO'͚Ļiڐjlj6y(õY)ũ› ™Lć™~ūĨĖŊôęfÍ%ĩJāå@é{ĨG"(¤|ųųt˛ÔŽÆ˜~^#ķ¯ŧ‰‰ æOŋr‚üŠ Q6nįž˙ø*É΃„ã%‡•R—ļĩļėĒûĸišöR:iÖ4M͆Dãuož‡R_ņ …[ š u3įqŪkßˌķąČöƒīŋX5>]JaAI%ŦģīqũņˇŌĮGž‚RŸmkmņ†æišĻŊH/7Ņ4MĶÎØE×Ŋųz”úcn īéûœ{ũ¸åĶ_aŪĨ¸Hõ€”gž0CpŌ…læ\r)“ĪYŽ›Ëĸ|ī}@Ũ™ß‚ĻiڟĶIŗĻišvF.ēîÍo•Jũ4ŨÛ=%\ZĄŽxī‡šōũ \e dah’å—äRŠÂŒå+pĸq¤īW—765ņ­išĻé¤YĶ4M;]÷æˇKé˙(ÕĶY]V?•ë?ōObÉM×PČBļ/HnĻV(d nöbJĢëQR\ ˜Ãs‹šĻg:iÖ4MĶNKãuožA)ųíL_o¤ĸa×}䙚b1É.(¤æá$Ās!QUNi] @ŠŲč¤YĶ´a “fMĶ4í”5^÷æ(ū#ÛßWQR]Ī5ú$SĪÅ@ø.ˆH[Á\gĶ„˛ A ”’€=üˇŽiÚxŖ“fMĶ4í”465‡âÛ^>3Ķ E¸čmÍôesč *ŋÃQaVę嗗ũš€P,(”ÂBWš5MVąĐ4MĶÆœ÷Hˇpĩ[(°ė–ˇąāęFŌ}A…Ųę„y0AÅĀ´däg=ũŌ÷Q„ĀôČ9M͆œNš5MĶ´“ÖØÔøßĘ’Ũ8ƒįĐĩxų`5č—Ę‚ž[ƒߤä¯CŧyÉ~}ī⎀ ‹ !<d ’đ 0ø{o8ˇá565WĢŧB~F͌yÜôņ/aGLō顝ÃŦ$DJ!;įą˙û.›š‡Å×ŧŽ%7ŋ˛ēJ:ōa”‚ŌjxááĮxôGßAIĶvnnkmš{čĸŅ4M čJŗĻEåā:āÍ(Õ(ĨQRÚRúBų>Ō÷PRN”Ž– Ĩ‡!,<7Ov „Ā0L 膚P˜Æĩ†a~P#‹Ģ›š˙ÜC@ŠŠŪāãcŧöÍ$Éķ€Eo*lĀD)C ūŠ’ƒ‡Ī 58ŲėĖ BˆŖ™æ”sī—˙B‰@ †<öįÁ_’$ˆphlj>LPå>@đ B×āĨsđׂÄ[Ŋ´ĩļœėšXAŊ›Ī1cŲÅÄ+L’ŨC›0K ąHõ$š÷[Ÿgīš'šėŊį‚7Ū‚īY¸Ų ŠpĪžuk)¤“8ąÄāŲĄ‹FĶ4íE:iÖ´ŗDcSs đnāũJÉ)n>køŽK8VBŦĸŠ’ēz*꧑¨Ē!/!’(Ű”ô†…t äŌ)Ülšd×ēÛé;°‡to—™Ī¤L7— ›ĄP“m‡Ž†ņ9āūÆĻæ˙VļĩļdŠ{īOŦąŠŲ!¨ŽO.ŧĖ&H–KQJ¨ā‚R éģ(ođͅ’˜NĶašĻÂ0 ÃÂtœāN'wāģ.žīž¤ŽŊ|)}„0PJž[0¤”B˜^Ą€ôÜcol a%Qo Ã0„@_„‘BmÃé&¨`īö665īv´Žä ’jī8oŠŽ’nÁ‰—U1aÎb¤$°C5-C)ˆ–B˛§Ÿ{ŋņ9rÉ$oüâ÷™ēt šȗÖĐDJ`˙ [Ų÷ü3ËÂ0­˙Õ;4ŅhšĻŊœNš5mŒkljŽW_“ŌŸįģ.žįQV×Ā„9įP?w1ÕĶæŠ&0mĶļ0LžBųk2ļ@Iđ žë!Ŋ}éÜŗŽ=ÛčØĩ™ŽvÃ0ŒÃ˛ßfÆÛ„aŪŲØÔü?ĀĶm­-}Å|,Žė?.Î.n@ˆšHi+%QJĄäŅĒąDJ‰ō%Ļ퐨Ŧ#V^E´´œHi)ąōJ"%„ĸ1‰2ėp '&\RzzÕæÁ,9Ÿ ŸIĢÎŌ'Õ߃t ƒ™¨$Ûߋ›Īc˜&šd…\ å+rÉJ) ™4…\/Ÿ%ŸIáå˛äŌ)!„@Aڌ!‘@ˆŠAeØB1A‚-rqHą!vŖØÖØÔ| EgA ÉŌB.kÖĪ]LõÔI¸0m+ā^,šŸ%ƒ•ØĘ/đČŋˎgãŌŋú0ŗ.œM!<<†$ÎĘĒĖn^˛õÉ'ÉôâDb9hģ¯ĨpꡎišöętŌŦicXcSķD!Ä'¤’vŗ”””N˜ÄŦ ¯búōK)Ģ€ôÁ-äQž‡›ĪPČĒcÉŪK›r_ß%0„@aYTM™EŨėyāCīáŪ˛Ž=Ī?Mįî-äĶIL˚Š…o†ņģÆĻæokÚZG>qiljN´Ļ,nąÁT”Bzžīĸ|‰’´‰$Ępb1JĒꨘ4ĘIS(ŠŽ&œ('’¨$’áDƒDĐlfP>Č ĪæŒē3„ŖUŖęčoŠ5§kwPėŽū^ÁÁ8åA>´2xy7—Åws˛üB–B.E.™!—N“íī#ŨßCžŋTn6‹WČãō¸…n.‹”>†a„bēbē&Â40 Ã0 ÆF‚>ęs•’Xá0^ĄߋŽG0L)ƒĮEúā&ļAĐ'÷˜V0Vîą˙ũ=ûvŗėĩÉŽįžĸ÷_1ķÂK¨›5xU-–íPpÁ‰ÂĄ­{ÔŽgŸ@˜Ļ0,ëŋ•R[ĪāYŅ4M{Eú  ĻQMÍ „˙áûŪUšÔąō*æ]~s.ē–ŌÚZ|W‘M&QĘą’yĘÔ`õPa˜áDĶd“yŽėXĪΧa˙ ĢÉĨúąB,ÛI#ÄWQęGm­-‡‡ú>˙ŠÆĻæÁŊeĀ5q“€:)%Ō*îJJ„00M“ŌēITNšAiŨD*&MŖĸ~ ąŠ œ¨I(ląķōā‚‹‚#uCŅÂ{Ú¯ŧ 0‚-xBI´aŧ˜Pf€ú^P•=–Äz ĨO!!ÕßEn —t_Šž#äŌ¤z{Čôu’ęé&7ЇB‚ Ē𠆉1xŖĻe+¯ĸrŌ,Ę&L¤Ŧnå ”Ö•ā„c´ŽČÁ$ZúA­8ņˇaŦv´­åŪo~Ž+Ūû1ßpíÛŗõ‰?rp͞=L:g9Knx-åõ5(Ĩxú×ŋTĪŪūSáDĸ)Ķ]ŌvßmΟîÃĒišöjtŌŦicPcSķ"„øß-ŦČĨúŠ›šˆåoz'“-Á/(réŌķC<LŠ ĩܨƒ_PØø,ē‹C›×āģ.Ąx)†aüQ)õ™ļ֖į†4€AMÍeĀBāā2„8_ đ yŧ|%Ļc.)Ĩ¤ēžē™‹¨›9›’Ú‰Ä*jqĸ6J‰'•dé0;ZÕÂÃmÃJũI>ø1XĄæ‹Õjq4Ņ6€Á_…Ü÷lŋ$›îÅÍô‘čĨ÷P'ũGÚé;ŧt_…L’\j?ŸĄPČSȤQŌ!ˆ•UPR;‰Xy •§Q?o UDK+ —”1Ø~ī› ’ht…€ ĒÆP åĶ'Q5?ū( Lúõŗ{͓ė\ĩŠE×ÜČš7,įāÆNîųæ—čØõŅԊ_jkmŅĮ^Ķ´qE'͚6Æ465× Ä~Á]‘Mö2}ŲĨ\üŽQZ[Kē'…įēC>4÷厚ŗC"%a˛ļ>ŲĘÆn§˙Č!‰L;´GI˙ãm­-ŋĒÛmljŽŽŽCˆëĸ\*/ŸĮÍĨ1 “PŦ”šési˜w.ÕĶįR1iv$Šeƒ—Wxų,;BT NŗwLøĶ¤ú8„ļô'ÛĄ b}Ŧâî) Š:Đ×~ˆdGŊíé;´ÎCxš šLŠ\˛Ĩ$N$F8ž VQKÕä™TM™EÕ´YÔ͜H´´;lāfƒ•Ûԃpē÷b͝ŋaé-¯ŖjōdrŠ[`"qpbKB>ã̰Ųųô*îûΗņŨ<áxé۟ēīסīŠiÚx§“fMC`ü”ū{ōÉ$S–ŽāŌw˜xy%éî¤ Ļ.Œ”Ŗ•įP4†éXtėÜÂŗŋû)û6<‹‰āDâ=JúŸmkmųŪ™ÜNcSķ<āz  ¸Vž[ I!•"SZ?™‰ –1qū9”M˜L¸¤ßķņrY|×ÅrėHcđ1Ō//:öX VÛM;HžMë%Uk|].é‘M!ŨĶMīÁN:voŖ÷āRŊ]¤ē“čÃw]Ŧp˜ŌšzâU¨š4Iį,Ąjr=‰ęi„BįÁ@ĮÂ0ˆ•Įņō/n” nÛ1ĄāŠ_ÜĒÖüá§"K6,ëúļûZ֍ôcĻiÚøĸ“fMC›šßü,—ėŖzú|ŽũÛΐ¨Ž$ŲŲ7øqwqĘĨJ)LĶ"R'ÕĶÅę;~Áχî‰ÄEy)ũ/_9ÕōMÍŗŋnBˆĨō=˛Š~¤ī“¨ĒĨaŪR&-^΄Ų퉕W# ps ŲJI”ô1L 'Á§_öNÎK§—F4í`z…0ƒ^e߅\ĒžƒGč;tˆîũ{8˛s}‡÷’č#Ķ׃”>Ņxv,JĸĒžÚ™ķ™0{> ķįQ6a2ą˛ "žėė%?Nž’AUÚÍf¸÷[_UģŸ{\$ĒjīTRž¯­ĩĨs„MĶÆ)==CĶÆˆÁ1jŸ÷ yÂ%åœ˙ēwRRSÉ@WĐZPÄ&\!žī‘î RRÁEo}?ņĘjžûũOÉô÷†"ĨeŸAŠhcSķįÛZ[˛¯v}ƒÉō{÷-1„›ĪRČĻPJP5iS—\Ääs/¤´n"v$JRČdqķųc­J),'Œ #˜uÂ|ōŽ÷-Ĩ$xš 7‚J´å@¤´Œ†Ō2æĪÁÍIrŠR=Ŋ´oŲÉĄ-ëé;ŧî;Iõt‘îéĻkīVv?û‘ōJęf,`ʒsŠ›Ŋō 5fĐ’Oŋ|´ėÅNöô’íīĻel XØĸiš6ŦtĨYĶÆˆÆĻæ Ôˇķ™‹Ž{ņ^2ũ|ˇl—5v8Še[l~ėŦŧíxų‘ŌŠ‚’ō‹ žxĸ tMÍåĀßo憁”>éŪ„€ÚY ˜žė2&/^NŲ„†`2„[Āw]”ô§3ˆc=׎Ǝ†ƒ…zúĨnب?9|h˜`Ų/&ģn.Eߑ>ŽlßÎáí/pdû ôØCv Ķļ ÅâĶĄbÂd.fÚyR7sŅ2?™$Įf‰‡°kõ:úūqķYėHėMm÷ŨöÛĸŨyMĶÆ ]iÖ´1`p´Ú‡ ų,%UuĖšč:ü‚Â/ä‡|Bƙ2ˆDXpՍ˜–Å“ˇ~—l))û¤Rt˙ũ§ĢąŠų]Āß "¤|I:Ųô%5Ķf3įŌ뙲äBbĨUH Íá\ęØJjÁŅõÖ 'T˜Ņ ķ°;öžmpÁ‰/ƒJąA%Ú°ãTMŽS5y"ŗ/ēė@7ncĪķOsdįfúîÁ÷SÉ&é9´›íO?BՔ™Ė\~ “ŸGÅÄJŧ\ĐļáíÛ6‘MöŠ•vĄÔ°6Ô4M4kÚXŅ„RõĘķŠ_°”˛ d’Ŗ0aCPČeQŌcöÅ׀aĐö‹ī‘K'ãáXâkMÍûÛZ[îhljžøG`ˆ„‚|ǝ§vÖf]tĶ–^@¤¤7īRČfŌGŠ Y/{qô“3;Å 9(%Î` ˛v&Ž&Ōž÷br0O:BŦ<ÂÜ+&0÷˛ķé9ĐËĄ-ŲģöimŨ@Ēģ/Ÿ§Jrđ…5”ÕOaڒ ˜šâJ&/žÄ@G'{Ö´Ą|ĶļV+ŠvõŽjš6nč¤YĶÆ†›”’ŽO0iáų(D°´d'eœ*!nÁÃbîeW‘čgÕo~„›Ë&ėpäķMÍ‚ņqī#XyīæÉĨS”ÕÖŗāĒ[˜yáDKËp ųtúekĢ˙Ŧ%åXÂÆ ‡t˛<Ú¨`>3ƒ›… ĻĨrR”ĒiõĖŊô:wd÷ęĮŲŊv%Ũûwb˜=vŅŊ[ž|š—\…Аė<ˆ‰ƒâN Ŋ˜wKĶ´ņC÷4kÚĐØÔüôŧ+Ã%e\˙Ņ/-¯ÂÍfÆÄĨ$v$ŠiY<ũë°éÛącqLË. ”žK!“"œ(eÎÅMĖģâFJëjɧōxîIläVA•Ų ‡°#‘cmÚØ DpvĶ –ôˇ÷ąwũķėXų‡ˇn ŸIa9ácËTBŅBˆĮ•’ījkmŅ•fMĶF„Nš5m”kljŽžtķŲyuŗqÅ{?…e;xnžą˛‘CJI´´”Lo'˙āktė܌VŖsȂĮäs/`ŅĩodœyøŽG6 ōę÷QI‰ ãD"ÁīõËÚØ¤@ E‚:7āŗ˙…Õlk{Œũëž!ŸIc‡"–0Ė,đGāÛšļ֖tqƒ×4íl§Û34môĢʔ”ÄĘ+1l)_˛īųŒüiv9‹1¸€ääH&V$Ša(yFáiŖ”a[“]Á•úy͍Ÿ˙aæn¸†u­wŗīųgÉôb…C1'ũBŧŠąŠųÛĀoÛZ[ô8:M͆ŒNš5môëŌÂ0Iuĩ#=ĶļÁ?õäŲ0-ģŸ{ŠÍŪM΁Ũ¸š žįbÚU“fR3s.õsÎĨfÆ"‰2”ôÉg2HĪ‚w ¯ Š™ą€’Ē:ēöng˙ Ξđę×aXNpĐëd¯I‰`YĻN˜Į!Ā/@˛;Xã=ųœyÔĪ›ĮŪįײūžģŲŗv%™žnBņ’é–úŽR˛ŠąŠų;m­-÷;vMĶÎ:iִҝč6 kZǧŋÃ…đ•:åé†e‘K °ëš'PR˛čš×a…"ôu cĮf:vldΚ'‰”–S;}>3¯bōĸe$Ē' ¤G>Eúg< A!_ ´n"ņŠzî!7ĐOĮ!Ē&Í ËœÔÕ()ąļcëŅrã‰C€ô‚Ęŗ…éį/Ą~Îļ¯|’u÷ŨIĮÎÍD†pĸôFC565˙øq[kËÖb‡¯iÚØĻ§ghÚĐØÔü[%åëAˆËŪũ÷L>÷˛ũũ§‘ŧ ”īáyy"‰2BҐ ÜtŠƒ[6°wMģV?N˙áũ(!h˜w.‹¯3.¸'%Ķ—Ä÷ÜSlĨ8JaX–âŠ[ŋËö§Ā‡šøcÚŌ‹Č ôŋęĨ†aŽ'WfŸFÚYáčsŠA8ŨÚŲøāÃlxā t"VVŽ }˙āßÛZ[~SԀ5MĶtĨYĶÆ†§…7įĶ)įĐļõL?Åi]‰RÃ˛I””âKŸĖ@ ”LdÃL=wõsÎeîå7qdĮö=ŋšî}Ûxęįße{ۃ,šé­L[ļ”ė€K>Lø:™‘p/()1 ƒHIYp+—ցŸĖhšŖ L"„!t[Æ8wô[&Ÿ/ĨÕu\ØüV&-:—ĩwßÁŽÕĪ¤ˆ–V^ ?lljžøˇļ֖ŊE \Ķ´1IOĪĐ´ąáQĀ´oŲ@ĒĢ—P,18zîä•YI6•¤É ×Qû…šTŠljajĻÍfáU¯ãÆOū3ŗ.ž–Ė@[˙#üÖ˙cíŨ°mĸ%eœō§UJ‚P,Ž1qá2ǧÎϐÍŊzģ‰RXŽƒeŲ:a֎F°m0Ũn&Ÿ3Ÿk?üa.yĮߓ¨Ŧ#ŲyßķJ Ķú p[cSķMŎYĶ´ąGˇghÚŅØÔü€’ūՅl–Kßũ,¸ō*ŽôųhåŖ¯ %Õe´īØÂ=_˙gōÉæ^~=vŗûš'XpÕk¸č/˙†xe™žPōúĢÂä3iōŠž M$V‚į^ąÚŦ¤B‚p<Ž0LŨËŦ—RAßŗŽ;iûÕĪŲūĖCDb Bņ”Rí(õũļ֖/;^MĶÆ4kÚŅØÔÜ üēËŠęiŗšæC˙PŦ„\j!†æC#ĨÂ0ˆW–Đŗ?w}ådz¸äcáÕWîMŗōW˙Ëēû~EŨœÅ\õÁ˙Gå¤éä’ũYėÉ&Πô1måûxŽË+fÁJĄP8áVČŅ ŗöęT0eÃC>díŊ­Ŧšķg¸š4ҞJ„9ĨÔ€ĩĩļ*v¸šĻ~:iÖ´1ĸąŠ9<̤ŋ¨Ë˛üõīæÜ›ßHēįTÖãSRb‡#ØŅ0{׮䩟})}Žx˙§˜¸`1…LÃ˛1LÅē{ĪŖ?ū:uŗĪáúŋ˙2ņō* Ų,ÕÍ*)1-›P,ÆXYŽƒ˙[8Q°lŸĪn䩟˙{ˇ--Į°l…Rë€ŋnkmYYėp5MŨtŌŦicHcSķë€ß˛i/¯æŠ|Š ŗæ‘ėí?ŖTRJI8ž@z.kūđ Vßųsæ-æĘ÷˙‰ę:réH‰R;Å[l|čūÁW˜ÅÍ\ōŽRøž;Twõ˜Ŗ¯QĄh Ëļõ´ íÔ)0L% y¤‹ļ_ū”-Oļb9!ėp ü-pk[k‹î–×4í¸tŌŦicHcSsøJŊ)—`âÂe\ųūOŠÅÉ $Oul3ô ‡b1Ü|žĮ~ōu6?r7 ¯yŊíīˆ$d^ÖkŦ¤ VpG#l{ę!úÚ0īŌpb1¤į Ũ=z{Jb˜áh ac+iƒuņÁ‡īØÃ(^Ŧ—˙YgÔāyI5XģŧĪĮîûŸüšvĢÎŅp]Ÿg{kūđ+@aEĸđ€ĪŒĻ+ŧÚÕ565 ^ūą‡jkmŅĪˆĻÅtŌŦicLcSķ4!ÄSŌs'ä2)æ]voũƒ|*yjŗ›•ÂĮp úū—Øĩæ –ŋá=,{Ũ;( ™ôņø)…N, é$j2¸Ŗ¯ON4†=ÚĢ˃I™0‚ĒĻ0‚Đ 2+¯āâ<¤įãy>(‰—÷H÷ö!•˙âĨ†IŦĸĶ2†i™˜ļ‰íØXŽ…RÁ´)ƒĶRĸ§‰œ$ĨĀŽ@8 ›y‚'nũŠî‰Â0%J}ø\[kK Ž%Įa 8@%P Lü3Iđ€@?pp<mkmú`4Mq:iÖ´1¨ąŠųMBˆ˙+ä2Q/Ÿįŧ[ūŠķ^ûf¤G0Jî$ˇ*Ĩ'JéÚŗ'~úmf_t-į4ŊŽB6‡›ĪžúÃŖ¯§Sâ> JŠ —9}‹LŽŪõÁÄØ0˙]€aæÉĨō  Ũ×M˛ģ“T×’]ídzÉĻú)¤SH¯€īyÁĖcĄa`Z6B„â ĸ‰r"eå$Ē&¯¨!Q]Kŧ˛šDu'B)åķb2=øīÃô´ŒyJ‡ã°kÍ <ōƒ˙¤÷Đn"‰ŌŖ_ōCāķ@™bpŽRę|)Õ ĨT™RĘގŨG !\!DŋaˆƒBˆmĀĨÔ.`#ĐôL%[Ķ´ŅG'͚6565[§…aüs>r¤īqŪkŪÎâ_‡iE)dŌxŽ{R C„0đ=)%ą˛ ŧ|ßͧo`d)HEŖXŽ3zÚc0°œāaō=(dúČô pd×>Úˇm ŋũŠŽvŌũ=ä’ũωaøžB*$¸™­’rŧq°}C$͆!0…ô=¤”DĘ*ˆWT-¯ĨzŌTĒgΧfj‘Ō2BŅx0ˇØ fK_'ĪĮsôMEŦz´ķäĪ~ÂŪuĪ`ÚÁ›!Äķjķ…„\ž€Tƒ=3ŠWüT%øN Ÿ[Ë2 9–eukPęY÷ÛZ[öÔũÕ4íĖé¤YĶÆ¨Áiß0 ķos™~>ĪÜËŽįœë_OEũ<×§Í"ũ ĪøD t°–ÚÄ …pķšāŗūQe)%1 ‹Pltô2Ģ Œå€ rį\j€ÎŨ‡8ŧm3‡ˇŽ§kßrŊäRÉ ÷;AYR |éãų†aba`˜ņâ<'RMÍR*|)‘ŌĮ2-lÛÂP~PĨ.äąl'#^^Kõ´9ÔĪYLũü9TMnž-<Ü\ØÃ¨xjGJ@( åu°ūūhũîŋ | ‹l.G6—ÃŽãP[Sɤ‰õT–—QY^Šm˙éBŨā9îéé§ģˇ#]>ŌI&“Å÷}¤”8!‡H8„eYyÃ̤Tw›ÚZ[r#ūhšvJtŌŦicXcSs)đ-!Œwšš šLŠĒÉ3˜wųÍL9÷|båuc°Ÿ6—96‡ųx^Ū"P|J)ėP'˜nP´¤YÉ Gųč˛ 7'é=¸ƒũëˇŗwũJē÷í$Û߅’Š‚įâI0ėPĐ¯ĄąH„˛Ō"‘0ąh„ēÚ*Ē*+HDŖÄbQĘËK1 ãÅ͊B }IwO/š‚Kr őŽ.ēēēIĨŗ¤ŌYú’d29 |ĪGy.!SāØ6˜%5Š›Ŋ˜įŸĪ„Ų3‰–U!,pŗA=ŠžæĸP2xķNĀ@w|€ŨĢŖ§ũÉJAyY S§Ndųš‹Y8oęĒ™P[CyY ĻyŧΌÁëVŠŪžēē{9ŌŲEû‘N6oŨÉĻí;9p°îŪ^¤”XĻI"Ãļ­=ž/īîžjkmąBĶ´Sĸ“fM㛚#Ā'…a|Lú~i6Ų‡jfĖgÆō˘|Îr5 ØļĀÍšä32¨nŽŌėI 4 Įb–Y”CnG_C1p"KØ˙ÂzvŽ\ÉūV‘ėlĮw XĄ0_Îæ°›xO6›Ŗ¯?IWw/ûbßÁÃėÚŗ­ÛwĶŅŲM*›ÅÍģ$â,éá{BŅĒgĖeÆōK™yÁų”××!%ä’ Ŋĸwā…ôƒį3RûÖoeeË­ėūiR™,žé0yR=W^t!—]t>‹ĖĨ˛ĸlHnˇŗģ—­ÛwąaĶVV>û<6oĄ¯?…m™TU–cFRJõGĨÔ¯{tßŗĻ>:iÖ´ŗDcSķk…ƒWģŲ …\;ĸ´v"uŗ2åÜTM™E$Q‚ō™,~!T—GYōŦ”´mB‘hÛHžL ļŽ:1pîMąoŨZļĩ=ÎĄ-Ī“OöĄä¤Â“A‹EUe9sgOgŲšį0îLęĒ+™P[CiibXCíëāĀĄ#tõôđėsx~Ã&víŨOWOļōŲ+Ŗ¤ĻéË.aū•—S9Šß ’įņt`PÉāƒp Ö?đ+õczí%ã)J+ĢxÍu—ķšë¯fņ‚š˜æđŊŖ8t¸ƒíģöđĐãm<õĖvī=€m۔•&pl{@Jy¯RęÖļ֖{†-MĶN™Nš5í,ŌØÔ< ø !Ä{”Rŗ}ˇ@!ŸE@Puœ>‡‰ –2éœå”7LDIČõ÷#Ĩ<ĩQuÃIŗÛėp;äŒh[†ôÁ˛ ZĸÛģf5ëīû7¯ĮĪĨ10É\T:KeE‹ÎeÅō%,9g>3ĻLĸǞbä‚ũžįĶÕĶÃî=ûyúŲu<öôŗlÛą‹L&KI,BČúŖ+ĻĖbŪeW3ī˛&UQrPČŨ$g3%ƒ•Úą2XsĪC<õķī“ęíĄ/[āÜsōŅžƒ‹W,#äœÚ§g"—ËŗsĪ>î}āQūđĮ‡Øŗ÷ %%q*ĘËPJ–Rūš`nôÁ JĶ´ŌIŗĻ…›šį7"ÄkĸŅ÷\ŧ|éy˜ļMĸĻž‰‹Îgî%MTML!ã‘O§FEOsp0҉Æ0Msd’æÁęr$$V‡ļíaíŨˇŗ{õS¸™$Ę0Éš’L.ĪäI¸ōâ šę˛Ėž1ÚšĒđÔxžĪÁÃGØē}'ŋŋû~ÚV­!™Î’ˆ„°PCP7įÎ{M3ĶĪ;/8ÔØĪŅ‘g%Á -ëî{„'~úR}Ŋ¤=¸Šé >úÁw0súÔĸÅįzÛvėæŪûĨåÎ?r¤Ŗ‹šĒ "‘0ž/×_ikmš­hjšč¤YĶÎZMÍP\\+„xôũ*é{HßÃ÷<•ĩĖžäZæ^z#%UedúSxˇ¨UgĨ$ĻmŽÄ'I ÷íũâéĪąöî;Yßz'؁œP˜d6Gžā2súTnēö ŽēôBĻLn ‰ o`C¤§¯Ÿ]ģöŅrįyđą6z{û(+‰c`Ú!fޏŠå¯oĻrbŠž`ĘÆYÕë<øüFË`ûĘÕ<øŸ˙Æ@o7ÉÛŪt ûëwR]UŧO^*_(°uû.nŊíîøãC€ ēĒŨJŠ_Ÿmkmé-vœš6^é¤YĶÆÆĻæP Üâ¯@§¤›Ëâ{.53æŗøú71eÉEH_âfSEŠ4x=˛CaėPxøĢĖ ŦppØoßúÍŦüõ˙qhķķ˜–…4-ŌÔTWōÖ7ŨĖ_s=eĮ566¤Ķ6nŨÁ/~s'<ÚJu,|ĪĨrō ßōnf5.#Ÿ†BöėęsĮĄ˙H;÷~ķË´o]OŋͯŊžOėƒ”—•žúŒ°t:Ãã+WķīüˆŨûP]YŽe™žRŦŪßÖÚ˛ŠØ1jÚx¤“fMG›šM  X|¸æhō ‚Ų]ÃĸëŪDy}=^Ū§Ml€G×sGŖ˜–5ŧIŗ‚p xnž5wŪɚģ~›Kc…ĸôĨŌÄ#anžū*ŪūĻ×2eJļ56“å?•Íæxō™Õüøgŋa͆MDC!lCesîõoä‚74c…2}Á׏åäY)°F'~úŦšį—ôį%—6.į+Ÿû&ÕO(vˆ'¤”âĀá#üį˙üŒģî{'d‹FQJ>ŌÖÚōûbĮ¨iãNš5mœjljޝūIĀ,Ī+XųLŠĘúŠĖģę5L\x>%U5¸ųŌwM°6ķ ×ë†BĄh ÞĩŲBũ­ũG:yâ§?d[ÛÃØáÂŅߟbņĸš|äīāÂe‹ąÎ’dųOĨĶ~ôķßpk˝drYĸŽM!“föŠk¸äīĄ´ļšė@ĐKgg “§đÍ/ū˖,*vx'Åķ|nģũūãŋJ.—#ôŸ~ÔÖÚâ7BM?tŌŦiã\cSsAÕųũ ęü|V¸ųË#?ü:šd/vŧœū~ΝÃĮ˙ö]4^pŪßđč—ÉdųÆū˜Ûnŋ!Ļ—Ĩ´f"—ŋīī™ą| ų4øÆTÅY°#phķvîûö?sp˙.ŧ`9_ûü§˜ü[[kKŽØņiÚŲėėlÖĶ4ítŊHxšÕ3æ0aöBzîĮwķxų<Ļc#=ęiŗQž4|‚U$CA!„Ā0Í` ā%ÍJÛũlGąúŽģxęÖīc˜&9,„įņwīxį[p{hnp {ķëndʤ‰üËŋũÛwíCuwđā÷žę“ĖšxyĐĒá2&gÀ|:Åž ĢČģ>sįLæÂķĪ-vXgŦ瞂¯}ūųاŋĖC=EÄZá8ög}_f›šŋÕÖÚâ;FM;[éJŗĻi465×O)%gzš,—ŊûĖŋęr:R¸ų<ž[‰Dp" 0 IĄ¤ē…(JaØÖÎg>ēΉ(žģķ.žúÅ˙`˜™‚O4áŖũNūōM7Ÿų e^ØŧũęwXŊv#ͧŦzW~ā˜Ŋb°â<zœíŪļƒģŋūIē;:xË_ŧ‰/ü͇GÅŸĄpøH'Ÿųâ7yāą6ęj̰-;-Ĩü(đ“ļÖYėø4ílt6°×4íĖ\ Ô(ß#^UKõŒš¤{= Ã$O¯ŦÁEđŨ¸‹Gō=R}) ~ą#<>ÄlŌŖŋã0&ęĒih¨+vXÃbÎŦé|úī˙šĒĘrēē{B,ūŊąŠ9TėØ4ílŖ“fMĶ…ķŠ$Ĩĩ TOM!?’Ŗ_ՋIų@ŧē÷ĩķäĪ˙‹Lo.Ų\ŽO~ä}\sųÅgî82yb=ßøÂ?2cúd˛Øė[÷ OüôĮ3˜w-Ga÷lpĐ'“ėĮ´fL›|t)ČYéŌįķ×īz+Žį“Ęd0„¸ø›ÆĻæŗ§EĶF4kš°T)ĒŊBžōú)”ÔTáįGpz•âXEûL ÍJB(ųTާnũ9]ģ6aĮJčOĻøģ÷ŋæ×^?T+ĶĻLä_˙ņ#”—•â›6۞ŧįīų#Ņ0MF]̆0ĀÍeÉ%û1,‹šĒĘą0đãŒüÅnĻų–ëI%͏žâŗĀ…ÅŽKĶÎ&:iÖ4 `ąTžá„"ÔL›#š['Äéŋ$)–\žŋ¯•­OŪŠĐŅÕĮëož–ŋú‹×Ÿĩ+ąGÂųKņĪû Ę0ņ¤bõíˇ˛wŨ6ĸeƒĢļG!ĀÍeČϰ,›Š˛Ōb‡4ė"á˙7īfŅ‚9ôöõĄ”*žŲØÔ<>V[jÚĐIŗĻsMÍŗbĻt]ŦPˆę™ķđ]9tƒ’Oš†8íĒĨa@8û7læų{Z0-ƒî4‹ĖáÃx%ņøĐ†;ŨtŨ•ŧë­o /îédeËĪČ ¸Aķ¨KœEđ-Ŧ ‹;œQ]UÁßũÄĸQ2™,•æ˙Wä°4íŦĄ“fMĶf ¨ķ=DÕ•ĩ¨"d@gPdÖoG Õ7ĀĒÛ[HuÆ7Jq>ņá÷1ebũĐ:Ž™ĻÁ‡ŪķVV,?dÁåЖĩŦšį÷ÁļE‡Q×Ļq´'c<í#¸æ˛‹xãkŽ'_(āûācMÍˊ—Ļ tŌŦiÚl Ōws”7LÁŽÄ‹’dã4_Ž˜Cąųá'Øˇî)ŦPŒžÕüZ.\ļxhįbŅ(˙üąRYUE&“cãwą˙…­DKŠđá„ög Ãā¯ßũĖ5L6‹"|ĩąŠY÷&iÚŌIŗĻcƒ§ë—€°|×Ĩ´v"áh5ښT_‰' =°áūߥ|ŸžT†Ĩ‹įķ†[š°mŊ{¨Íž1ŋ~į[H\ēŗöŪ;Č&=B1FMĩYI|br-49Yåeŧķ/߀‚|0į"āEKĶÆ<4kÚøVÔJŋ€Ž‘¨Ž;ũŠī;ŊÛĩlB˛ņÁGčŪŋeG‰FBŧ÷íob˛nˆiđ†[ŽãĘKWĐ;fߚ§Øųė„ŒŠŸ*AģNŒpŦéšã1oæ–ë¯æŠK.$•Î !ā“M͉bĮĨicŲ(xyĶ4­ˆ& !&˛YJĒj(­iĀwũĸ|Î.§5˰Ąc÷~ļĩŨ‡“Éå¸tÅų4^pېĮ¨Ŋ(‹ņwū‰˛ úûzŲôĐ td'ŠßĻĄ8áĄhß÷ŠøF°¸>ôîŋ¤Ŧ$AĄā"„˜üMącŌ´ąl|ž’hšvÔd„¨wķ9ėhœxEJųcæāTPAôŲüØÃ$;áa‹„šå†Ģ)+ŅEĩáļd҆e¯¨!ÆL?ŗa‘ģéŪˇ 3Į4 .i<Ÿ’„Ūü7ŌĖ›ÍU—6’u%û7=Kûļ]˜ˆ"U› =AYŨdœH”ũûMĮĨͧpŅōĨtöôbšf đšbĮ¤ic‘Nš5müĒǤīcZ6ҊĒā`ŨXHš€d˙ú6|×Ĩāy”$b\xūšE,ĐŨĶĮæm;YõÜz6nŪNww/RJ ×ķŠې ‡ŽžlÕĩ8rp6<Wđ1ŠĩNCīCeC N$ÁÁIí ã’i™\qɅÂĀķ|„`acSķ”bĮĨicŪ¤iãWPŠP„ãĨ؎ƒ#­† ™ū<‡6¯G*đ\sg3o֌ĸÄSp]Ö>ŋ‘gž[ΆM[ééë'›ÉQ(p…įŋš‰7žîzJĪÂjø‚yŗX˛x?đ {Ö=Ãü̝&^Q‹_¤¯ō!ZĨrŌTúömæp{eĨÃ~ģŲl–]{ŌŲÕM2•ÁqlĒ+˙:ЁŠŠ˛aŋũãž%‹˜>e"ûĸļērļ¯äĀ˙% MŖtŌŦiãW H $ŅŌ L;„-ëÜ^‰'6oc ë0Â0ÁW,_z–5˛/ižīķā#mÜöû{x~Ũ&|é# @`ƒžŪ>**ĘyĮ_žž+.ŊD,:ĸ1Ž”šę*.ģh9O?ũ íģļĐž}ŗ.ŦEPŦ“Ât¨š<žë9ÜŪ΂šÃ÷ĻjãæíÜ÷ĀcŦ^ˇ‘]ģ÷“Ëæ0mßŪ0M›:‘Ķ&qÅ%+¸úōFgd7U–&â,^8—ģ÷"•Š “fM;%:iÖ´ņĢ TLIE$^Š ;ž“ĸ͆#;6’íëÆˆ–RUcūœ‘­2ī?p˜o|÷GÜ˙đ“HŠ(IÄ™Bˆc—TŠķfķųūį,˜‹ey€ņ02„`é9ķ™2u*;ˇldßÚg˜šüL+‚Wųx”Ëv¨›6—õĖŗk÷>¸üĸ!ŋ\ŽĀz-ˇßÃáöNBĄ‘p˜P8(,ĶBÁÚu›XŊfąh”Ĩ‹PW[5äąŧ’p8ĊåK¸÷ÁĮČ „gAcSķÄļ֖#ˆĻa:iÖ´ņĢ…‰K`ÚĄĸö3+urunہLŸĸ{īN ;„”’IõĩĖÁ֌gž[Į?ū›ėÜĩ—ęNJãV¸ŗš<‰X”ĪúÃ,]ŧ`Äb+ĻšŗĻ3gÖ 6ŋ°žŽŊÛHõψWFŠĖā7Sõäé˜ŅRļnێâôļNžČūCí|ūßžËãOŽ"QUY~ė ĶQĻaĐĶÛĪ´Š üŋOü-_xļ=ō?z Ãā܅ķ¨(-!™ÎrœÉĀL@'͚v’ôA@M‡›šPƒ9ĮÁ0M(j{ÆĢßļR`… ÕÛC÷ū]XĄJJ&Ml ŦŦdb„ĩë7ō‘O~Ũ{öĶP_w܄Y)Hg˛ŧá–ëXzî‰k4ˆF#,]<Dy%Göî¤kĪ!, D~Ō(\JkËŠl˜Ęž={čėî˛ëOĻ2|úsßāĄĮÚ¨¨(#r0 ãe ŗ‚ū–mņŲOũ-W^z!ŽcŋėkFRii‚Ķ&“/äPL.J š6Fé¤YĶÆ'ˆ+@IE(^´gŒ–fÄto'éžn ĶIJ-fÍ™AûæĶŸ˙&í]4L¨=îx>äōy&ÔVņē›Ž‘¸F“9ŗĻSZZF_×ē÷lAú>EĘQ>$*ã”O˜ĘÎ]{ØģohŠĒžįķ­ī˙„ļUkŠ­ŽÂ0Ž˙ŖT)I*“æÚĢ.aéâECrÛg"‰0uRų‚ BD€ÅŎIĶÆ4kÚøb(…0 LËFPܜYžĖ&B*h?Rĸ”Â4 fMū¤9ÉđÍīū˜6nerCžīĻĩōĻNžČôŠ“†=ŽŅf꤉L™\0-īØL6™-JĨ‚ąsÂ°ŠŸŗLž];wÉõ>šr5ŋģã>ĘĘˆĖ6BËˆF"\y酔$ŠŋĨ2 3}Úd¤¯ŽÆÜĐØÔ<63hÚ( “fMŸLĀö};!O?D‹9Ŗų$nZ>$ģڏ-b1L“ēÚęaīą'žáŽ{¤ŽŽ˙Õb0ĄĻ ŒŦ$JuĩÕĖ›5aŲtîßE.•-Z,Ę.“ÎŽ—ą~ũ øū™='ŽįōëßŨM6—Įą_y†” Ķ4ŠĢūīĪ“!„ ĄžŽŌŌ8š|!DÁŧvMĶN‚Nš5m|J D•R Ã00LŠ]kVŠW;Š%*ÍŠŪn”RH)IÄĸ”&ÃZ6›įįˇũPČ!rP'‘ įōyŒbõ%‘m™L™2'"Ũ×M˙‘#EĢ4Ŗ@zP^[Mų¤lظ•îž3ëkŪļ}k×m&‹ž°-ãe!(E.—;ŖÛJjĢŠ(-%—/Ô#;ÆCĶÆ04kÚødÎąY0 f4+ÔĢ ôƒy}ϧ%%ž'Š­Ē ŪO˜×mØÄÆ-ÛŠ(/=ŠJĨ‚CGēNĢԜÉfŲąsĪÉĩ̌R uĩ”——áæŗô؅aČâô5 đlÛzÅĘâK ‰’8÷>đwŨ÷ĐIß֏<Åw=€@ą{ßūĶ šč%q*ËK1íŲd/Ų~ŖHIŗ 8˛=T•ĢáDúÁĖæ)ķfaÅ+xüɕ\zé%A2{Šęë‚ëTK˜'5ĨŅ÷%ĄPÃ4éééãą§Va ˇā’ĘdX˛h•Ã&/ødIŊtPNą?dŌ´1C'ÍšĻ"˜!_<xb !ĻmĐŅŲ=ŦQIyú$•RÄb —û|ŒļgÖ0erņX€l6Įî}čí ‹‹…)\””ČW™$2څ#! ĶÂËgqķŲWlģnB@! Ĩ5õL˜ŗˆUkÖĶŪŪÎ´Š§žgîėéDÂ!rų<ļõĘsš_J)…ešXsp\ĸÂ0B@œ3ĻOąĒo1ĮąkÚXĻ{š5MœÔOķ?]Â2ÜŖŲÂ!Ë6OēŸųO)Ĩ°m‹Ō’¤/Ųļ}ĢמĀęĩ/°iëN<×§ĸ´dpŌBp×,Û&ÛC *+Ę1m‹|&C>›fĪjž:^ė°ÁÜåËéčIą{÷ŪĶēĒÅ į˛lé"2™ėiO!0M!Ō—L™ÜĀE+Î;­ë:åÛÄ`˛ū’?Ō4í$č¤YĶ´ŅA”’'NDˇ†b ŦPT0žÎķŧaÛP_GIi×õÎčĐĄalĮ"E‰ĮĸÄĸĮFŧ$Ŗ”RE˜2Š~(Â/Ķ00L‹B6ƒ›I!Ėâ–7•߃é ã…bŦ\õžīŸōõ†Áko¸šh4BĄP8íx„zō ŽĮĩ—_ĖĖXƒ‡|ų˛÷#rÚvĐIŗĻO¨ÁÍÂåĻÁ6%OXûRBËvPRa&]=¤Ķéa‹ĘqlfL™ŒĄ֞į3eR•e#r{Ã%˜0!1 a˜ŖĸsÖ+@i}-“,áɧŸãđáöĶēžĢ.oäĻĻ+HĨ2øžĘoĻ„øRŅÛ7Āü93yÛ[n9­8NGĄār¤Ģ ×uąL͆ˇŋIĶÎ":iÖ´ņI†0žįã{…aŨvr! NN˜` ŽŲŽ”–!0L“ū˛ųá=bvų%`˜¯ŧĖb(!Čģîˆ}T?žˆÁ)ļ‹.šŒŨ‡:ØžíԗĪõąŋ}7˖,ĸŗģ÷čĸWũčč×øžOOO/ õ5|ö˙–ęĒŠĶŽãTųžO.—G)…" dGėÆ5mŒĶIŗĻOI]ĻåāfĶd‚ų°â4ĻD %Å+÷(+€Hi9B CĐÛ7@29ŧK!Î_ēˆi“HĻŌ§5Iãd!H§3TU–qéŠeÃr㝒ÁIsįa—Tņā#“ÍåNëē*ĘJųōŋü=×^yŠtšūdŠ\.ŧ5Œc rđ}L`)¸.ÉdŠŪū$ŗgLã+Ÿû8‹ÎÚ;ų*\Ī#›Íã86(Õ Œhš6†é¤YĶÆ§ Öė ŒbmžøSJ!剎Ļ)Ā0 Q]7Ø_R):2Ŧa•—•ō—͡ĐÛ׏īËaĢĘ÷ö đÆ[š˜Ø0aXŽ$ į–ÆĶ%ä’PVSÁÔÅįņÄĶĢé8ŌyÚ×7eŌDūã̟áCī}åĨô¤čėîĨĢģ—T:C:“%ÎŌÛ7@gWŊ}ũX–Åëož†ī|í3\xū’!ŧw''ÉԟLâ8 zūBĶÆ(=rNĶÆ1a¸ų^>; zšƒjĢ”Gˇ­ũyDJ‚aAyŨd¤”XļWđØŊo?—ą|XcģņÚ˸ãžXšj-S&OÄ?Cd'bš‡Û°hūlŪŪüÚ!ģŪbōũā Ļ‚aok9i<„į\rŋ|čÖŽÉ“'žöĄãđwø+V,_ÂĶĢž§í™5<ÔNÁķž FĘÅŖT”—rŪ’…Ŧ8 ×^qqŅfVt÷ö’LĻ9JŠN Ģ8‘hÚØŖ“fMŸPĒÃ0-ŧ\Ž\*F°i†.<-2˜akšâ„čbåU8áR'%{÷ö°JKKøÔGŪĮ>úYŽtt1ĄļĪ÷Ī8!´,‹žŪ>Įá}U#Øß:œúûPRbŲ6†eŠƒ€T›ķY˜<˙Jë&ķĐŖOqÃĩWᄜ3ēŪeKąlÉ"š_{=‡tĐŨĶGžāb™&Ĩe jĢ̘:šaˆîÅékoˇŸŠō2€ §¸iÚØĄ“fMŸ BāæŗøžOŅĪĒÁéJ üŸ$Z‚`B´ŧ†ō†itėڌ‚ŨûÉæˆFÂÃŪyKņ™Oū ŸøĖW9|¤“Úš*¤|…1y¯@eštt÷Mgøė?}„Ë.ŪjųHJĻ3xžG(%K eąŋšB€—ƒXŠÃš—]ÉSw˙Šũ‡3cˆFžÕÖVQ[[5$×5Ô<ĪcßÁv˛šŽcáûr{[kËéĪÍĶ´qfô5iš6ō@ZÂ4pŗi|ˇ@Ņ÷H¤g>{÷¤§§”:6%!˜šđ’{$Æā!Ŋ}ũėÜŗ—éS'ōŨ¯–wŧõ ØöYôÁŸ‚Cíø^P,ŽŽĒÕÍB@.u3Ļ0aöZ["Ÿ?û Ž™lŽ­;v …ŠCĀé­EÔ´qę,z•Ö4íí‡ Á+¤Søn;)vLÁá1)įËũyÖ,¸Y¨›ąxy%é~nĮ÷Žm9Øļ…Âs=˛ųB0Īh˜PËģŪöF^{ĶÕ˘:yŖŊũôôöãû>áX ĄŌ†đÜä™PČ@YÁ9—\Î}˙ũī<ŗúyޏôÂbG6Ŧö<ÄĄö#„‚„y[‘CŌ´1E'͚6~˛F$Ũߋ—ĪCĸؕæÁi žT*˜‰|œ e> UĶ(̟Bēw-ĻiķüúMtõôPU1ré&ÔōÁwũˇÜpO¯zžGžXÉŪ}éH‘J§ņ=Ã4‰E#ÔTW1qb-_x—_t u8ļ=bąŽ¤ÃG:HeŗČBP´„xy9ŧûgNK. 3—]FŦęÜyĪ}guŌ\(¸ŦZŊŽL&KEyJŠučÉšvJtŌŦiãW Éô⹅ĸ/7ÁJ3éû'žõĢĀr,&Î[ĘáÍĪãXáų [šú˛#¯m[LžXĪÄú:nžūJ˛Ų,ûϺ̇Bž€e›TUV2Ša‰x”PČ9åčąæp{Ɂ$Ļi¨ĒÁ ™2Ŏęå„…,”5T2oųE<öÄũlßš—Y3†æ@āh“ËįYŗ~#…‚‡eY}RĘÕm­-Ŗ¨iFĶF?4kÚøÕôĻUëģ9RŨ”×O Z ŠŲ€:xûJĘĪ%'! .ãų?Ū†”’l.OÛ3k¸ęŌ ‹˛Ü0 ĸ‘0ŅH˜ĘŠrāÅųÄÅ_Q>˛öė?DwON4Bé„I(ųį“PF)Á´`ö—ąæū;šëž‡øûŋywąÃûâ…ÍÛ))‰Ŗ”ÚŌAI"Ļ”RÛZ[v;.MkFĮOGM͊ĄC)–…īHvAˆŅQ=zPɏķ=ˆ–;L]rĘ÷ą ÅöŨ{YõÜú‘ Tû3‡t°s÷^¤ī+)ĨrŌ4¤âE5Å$&Ē掸œ­[ˇsĮ]÷;Ŧ!—Ëåyäɕ$bQPôŋ/vLš6é¤YĶÆŠļ֖¨†iáåķ$FnpøŽČAuLžBŌŦxyƒŠËÎŖ|Âd(dÉæķ<üxŠtzä‚Õ^fßūƒėÚŗå{TMžE¤4Vė^™ –Ė\~)eĩõÜy×ÉåķŎjHmÚē“5ë7’HÄQJí/vLš6é¤YĶÆˇ<Ã˛H÷téOĨ+Ĩž|Å}+^Jjʙēė2>‰h„Õk7đäĘ5#¨ö2›ļėd` ……ĸzę,"‰Č°T™•zņüAp9ög'{›F0ĨfÚ$æ.ŋ„6nâūGžú€‹č÷÷<@6›Ã˛,_Ám­-zj†Ļ4kÚøvĨŽXĄųt’Tw†iQė̀A‹ˆBJ?H€NŽ”ÍŦÆË‰”Vâ0ĘpWëÃd˛Ų‘ X É˛rõ:ōŲ %å•TM‹i[Áķ4D” Ú*ė„ĸŽA(Ąøā%NL›cIôÉ&ĖZq ˜6-ŋŋ‡Túėøū9p¨ĮŸZytÅ|;đË"‡¤ic–Nš5m|ÛĄ`Ÿސęë"Ų}Ķ1FE_3J!ĨJžpQĄ"ØX;c"SÎģ”l˛—h4BÛ3kxzÕķ#­lßą›{öáįĶ”ÕOĻzú|/˜RqĻ” ΨÆĘ R –žį2ĐÕE×ŪŊtíÛGמ}¤{{QRâD!VNčänßsaâ‚ųL]¸„§žx’m;ķ G–;îĨŊŊ“x,æ+ĨîokmŲTė˜4mŦŌ#į4m|;ˆR]†åāf3$;ÚQžŧܔ/‘RažÂÛ{߃HÂaūWŗë™ņ\—îūwÜķ.;—XŦø[Į‹ĩ/lĻũH'ŽiQ^?DUyĻßG 0‚DŲ´!ŲŲĮámÛißļū#Čô÷âæŌ€áX ‘Ō ǧÎ`ĘšįP=uV˛ƒ×u‚7`~Â% .ģ‘-+§åöģ¸öʋŽVhĮ¤Ã]Üy΃”––ô_+rHš6Ļé¤YĶÆąļÖˇąŠyJbØ!ú$ŸĘ ,5”ŸŠŸaA_ŗįË@‚Ž?Ŗü`IEũœŲĖžčFÖˇūŠŌDŒoãŠUĪqíxėãQgW<ņ Ё~ĒĘJ˜zŪ L3B!wú׊T(GK ¯Ŋ-?ĀÎgŸĻīā>ÜBéēÃ@ŧd Ž’…bįĒëî+gú˛‹Xzķ멜XMǤwüЊJ›L_v“į.bõęÕ<úä*n¸æŌĶŋEöŋˇū–öŽNĒ*+”Ręžļ֖-ŎIĶÆ2ŨžĄiÚ •ĩŊ‡öPȧŖdVsĐ×ėŊâŧf¸0m›EMM”ÔNÆ>ų‚ËĪ~};ũɑ xÜZŋq+kÖoÂ’ʉ3¨Ÿ;)ÅéUšUđfČ0!ƒĪ<ĮŊ˙ūyžúå9ŧu^!‹Š)-'œ(%Kģ„ĨDe˜ĻI˛ĢĩwũŠ{ŋųElÚNŦ,č]>QŸŗ_€DU9ķ.ē–L:ÍmˇßMwOī™<,Eŗ~ãVî¸į~ĸ‘JŠ đ•bĮ¤icŨhųɨiZņlEqØ´-ŌŨ¤ē:FGO3bđ0 ˙ŠÃ„/u3§°đĒ[đ=ŸōD”§žYÃo˙pž? zMÎbŠt†ÖGž `€’xŒIį\Hĸ˛ésÂ~ôã’ABk… ^ņrXûĮVîûö—hßūN$BŦŦ‚P,Žņ*ĢČM'DŦ´œHi‡ˇG×ŪÃ5‰ŗō%RŠWžį!Ā̓’ ¯šš s—âgĶ„‡ũŦ…í;÷ŒP´ãĶæm;xĸí9"Ä*ǘēt)VČDz'y āÄ!Q čŪ×Éŗŋ˙mŋøŧ|†HI–NrÍģR(Ĩ0í0Ҟ*:vnfõŋÃÍEO\mö\¨žÚĀâ+o!›āļßÜŞ}OōŽŒŋûC+O?ģ†x<Šb;đÕbĮ¤ig4kšÖ l1L7ŸĄįĀ.ėBŠâö4CPTJâģ¯ž} š$ĒĘXzĶë ÅJ‰‡mÚ;ģųî~N:“ˆĮŸd*Íīī~€öŽ#„ AŨŦsŠ›3ß?‰Š*hÁ' R}‡;X}ûŨÜ÷/ņûųOũō€O8Q:øõ§1đYILÛ&‹qpĶmڂaˇ{<ž `0īōK˜6o!Ī­z––;ūxr‰ú(°yÛNūį˙~T`[6§ÚZ[zŠ—Ļ tŌŦiã\[k‹6‚Č à¯ũéū4†å;4‚Nf’>JŠWũ¨_ĘāPāĖ –3īĘņ=ęōRZ~’_˙î<ŨĻ1ävėŪĮũ<……$RRÎÜK.Į‰F(d^Ŋ5΂•´īÜËÃ?üoîúˇOņÄ˙~‡]ĪÂH ¤ē;yė'˙˝_ū8ëīûÉî#ā„ņm‡h4zl‚Ę™S˜Ļ’>{žšŪƒ„b'X~)‚ †a1û’˘2wöîæGˇū†ö#CËđđ}Ÿüô6yęĘJXĻąYÁįÚZ[ÎŽāšVD:iÖ4 `JąéžúÚ÷aŽ–”B ĨDz'W%˛}P^[Nã_ü‘˛*"–Āu]>ûåoĶŪ1zŸąæ™įžįžĨ4!’(cîå×-áfOPeVÁR;ėŗåņg¸ķ‹Ÿb͝?ßʼn—’ņ ‹ņW7_ɡ?ų>ĻÔÕ0Ę „8ã{a„aÛʇøÃW>ĪÁƒ‡—8Ģ`"KÍô),ŧę&*ËK¸˙GųÅoū€?›Z†˜RŠßß}?˙ķŋÆąÂN8Ĩ”ú`[kK{ącĶ´ŗ‰Nš5MŖ­ĩĨXHĶ ŅŊw'ųt~pĨvq‰ ąéyĮ˙8ũ8”‚t?L[ēˆ ŪđN¤ÄŖ[wėæKßø>Š´îo>SG:ēø¯Ÿüˇāb ɤs.`ÚyKq OĐö`‡UāŲßŨÁũ˙ų9zfy)Čds\˛l_ûûwō7oš‘%ķgđÖë/% ͟Ęāzūi%ĪGŋ<_(ā8Ĩ•ĩėÛôüÖįØģv#ĄČq|ŒŸ˜séELY¸œ°üøŋåž?ՇjØ=úä3|ķ{?Á÷<âąhV*ųiāŠbĮĨigķ_ūå_ŠƒĻiŖĀoũMpĩ0P!›aę’Fœh éŸė„a&ĻeŸTŌ$D°^[)˜¸`&Éî7>G,ž`Ũæmäķ/XŠačēÁéPJņŊŨĘí÷áãŸų7úú“DL¨š2‡ËŪķœP˜\ú8ŖÜ8Čô đčŋIļŋ›hY%nĄ@.īrÃ%įņĄˇÜˆëúôö'1M#˜™"ž”ä S&T͏d>įΙJUY !ĮÆ2L žGŪuq=‰įK<$Ņž'ą-“ Õå,_0›ŋyˍÜ|Ųrzû“„,‹kYŊi]ÉŠÃģŠ™ļ˜ĘIĩøîËŋĪ…æ6‡cUSĻphËVüūNļī=ˁCíœŋd%‰øˆ=ö/õĐãmüķžÉî=û¨ŸP BÜĄ”úÛļ֖ąšÂPĶÆ€â7,jš6*´ĩļøMÍĪŌ÷Íîũģpŗ9„iŊō(„‘ĸ@ú ÅŠTž ’ŨPVWŕøwũŗôÚ F„¯÷GT”•qÕeÃ÷YÄõ<žō­˙fëŽŨT—ÆĻͅo~/Ĩĩe tžxöą‚›6Ņšg+ņĘj’l.OuE)oēöb"ŽÍáÎ>LķåŸ"Mœ;{û‰FÂ,7“sfO#•Éąīp'íŨ=ô%Ķtõ&ņ}˙Ø×Į#aj*˘R_ÃˉuD#!úSi2ŲŽī1ĩĄ–Ģ–ŸÃރídzØąō!Ļ-›Â>nß|Ē*&Õ°â/ŪĪ}ßújŦ~}ō>÷oßá˙ũǘ:šačėWđģģZųʡū›ŪŪ~ę'ÔĸāNĨÔßĩĩļth š6Îč¤YĶ´—zx—Š”öÜKĪÁŊTO›Ev !Šß˙+=‰’>†°NöLā1™^¨™>‘k>ôqú¯§÷ā.’)Égžüm<ß§éĘK†%æŗÉ˙ø—´>ō$UeĨä3I.|ķۘšbŠ@rÜŖåÂÕuīŨŧJJ<)™T[ɔ 5ä '0 ˛š<Ų\žãrlæLm`ūŒIä .RIØĻeYHŠp=/HǕĞL<ĪĮķ%s§M¤ŧ$NGǟî;É%]ːũg[ ?ä Õ 3–ͧņ­īįąŸ|“˛¨ÃcO=C˙@’OūŨû8éĸĄz˜O¨§ˇũŧ…Ÿßv'žįmÉø‰Rę˙ĩĩļö4mœ+ūOAMĶF“ËvH÷vŌŊo–mŧú–ŠĖ땸Ž<åWŽÁdúaĘâų\ķ7Ÿ$Q= ËËŌĶÛĪgŋümî¸įá ü,qÛíwķ_?ų%‰X ?—fÆ—ŗėõ¯Ã͂tƒäøx 3˜Ũsp/–ã ”BJ… ŧ$Ž! |_žę‡Göå ’é ũŠ4Ŋ)ú’i އīK<ßĮ—’LŽüˇTšt6‡Ba ~Bw]bŅ0ąHaøžG!ãđÛühâŸKÒ¯æŧ×ŧ å{”DCŦŨ°‰øĖWøÍí îĮ0yní üãŋ~ũŦ!U•åHĨžĸ”ú„N˜5mdč¤YĶ´cÚZ[€6„Gvlϐ-`ŲĄĶ[a<äžīĄŽ;ĶėU V Ķ}P?MųG*'NĮö2$“i>÷oßá'ŋøíG|6¸ũîøúw‚iÛČ\’ĘŠŗšüƒí„Č%Oœ0CĐí () ž„ qU(ú’i¤’ĻqŌã_:‚IžīãzÁĨāzĮf)í~Y¨iÚËüøÖߔ× Ã´ ™$S–^D8^ŠWČ˙@ Ãr0ŒSåčGíŌ…ĒÉÕTM[@ĮîäzÚņ…É“+Ÿ#É˛|é9˜æ tĮ™ģī„Īõģ\Ÿđ—”sí‡>MÍˉd{?„xĨ§bđ `ĮîÚôš\E3§0ĩž†t6?$hŧÚUHĨ(/Iđôē-<ôėzÜLšę‰SXxíĩ(uüžæc×=8ŋŲ´L&/š‹­āđÖM¨|;eëŽŨ<öÔ*6mŨAĄāR’ˆ“ˆĮNë~ $ĶÜ~Ī|í?~ĀŊ<†ëz”$âØļũ RęCm­-wŧįío%ķ 5m|Đ=͚ĻũЧŧaĄl˛î=ÛITÖ;Ļ€(АŌ˙ŗCc§pH´j4˛Š˙điųÁąŨ͘N˜ūŧ…ŊûđŲOũŨ¨™Å[,ŋģģ•/ãûä\>V8Æ5ûO4,˜Ev€ÁRîĢ_Đ0 /‡ uBt÷ đÛžbÁĖ)”•ÄčíOëėl)%Ue%tõ&yxÕz Ųļe2iņ ŦC!{÷ŀ|,ĮâÜާrŌTVūú'Ú˛Ž¨m“Īģ<ôØĶ<úä**ˢ7gŗgNĨĸ´”˛˛ņ8åĨ%DŖalËĸļϊ\Ž@Wo/Št†ÃílܲGŸ|†ŊĄ$”$âX–Õ|[)õŸm­-ŨÃö išvBBŠ\5M-›š āIĨä 7—eÁ•¯á¡|€ė@ąC‚×MÛ&qĮˆN@率'~úŋlyüĄŲ< æĖäŸ>ú.8īÜĸW؋ĄåÎ{ųęü7īâXA Õīũ$ ŽžŒ\Z NnĢųāŧėRJžųíØĐú[båU†Ižā’Éø‹.åÃoŊ%ũŠĖ°´ĐK)‰ĮĸÄ"!~đ›?ōw?‚›ė§zâTnúäŋQZ[ƒ›?ĩëœdûX×z۞ŧŸŪC{0,'GJIžāR(đ|ŸH8L$&á86–eRšHāzÉš|ŽŽ žK<Áļm„ĘâvŸ6éĨ%šV<ē=CĶ´—yĪÛߤ~|ëoøŽ+LÛfŌÂå˜Nå{ŖâP Ā´!‰ÅÍC´$ÂÔĨË0ŦÛ6ãŸÃ=<üÔ*ĸá0ŗgN;ĨųĐcY.Ÿį‡?oáĢßū!ž”D  .{×?°đšË)dŒ“N˜€‚B.ƒ‚ęisHuĩĶš{ –&䨀âų­ģéHrÎėŠ”%bøR!‡¨ ŖC”Æ#˜ĻÁĪîz„˙Ŋķ!ōŠ~Ëââw|”É‹įáæ8éžę—ō І˜ŧx“_F$‘ĀÍH÷t Ŋ–!p,“c!PxžK:•f ?Io_/ĩĶÕĶG6—#—Ī …ˆF#žmY9!ÄŖBˆlø;đžˇŋiĖ~Ô´ņKWš5Mû3MÍ×đō9ۉƸâ}ŸdŌÂ%$ģ{Ŗ`õ´RŠP4†eÛCr>Q)°CŠÁÖ'WōÔ/~@˙áũ(Ķ!įz\sÅÅüŨûŪÆĖéSÎęÕÛûæ›ßû w=đĨ‰2›$\RÉUø83–Ÿ›˙dfŽŽ3÷ÉgR(Špĸqō™$mŋøOvŽ|;!Sp=úS–ĖÆÛnž’ Í&lÛ¤sA…V)yĘĪŗÃ„‡HØaĪÁ#üėqĪĪáeˆFĸŦxëYrĶxy y ÷ëDė8QHvåčÚˇ“ÎŊ;é?´ŸTOųt/ŸCú.n>īšƒŸ`”ī"ũcĢsĀ€ŸteYĶF4kšög›šĀFßķ&šŲ4žųũ,šé $ģúGÃFm””XNˆP4:dC=”͆X9Ųq€§nũ!6ŦÁõx'šL; 29"a‡ÆsįsķĨËX4{!ËF"QJS8Ô`úOžw!'k †Āa@gO’Gž]Īí>ÉÖ]ûˆÛ&U “šāÍībū—ãfÁĒMņƒa9Qp =p]ĀŠ —Láåŗd“}¸š,Ļe‚lyô!ļĩĩb‡cÃxxk[kˆ!ˆHĶ´!¤“fMĶūLcSŗnUJŊÕÍf™|î…\ūŪO"˛™QŌã+ĮâĻ1tĶđ“žXYđŦģīnÖÜÕBĒû™ŧ‹'áŌ‹/ā=o#KÎ' Ņ ĪĄönģũ~úËߓÎå(…ÉgRL\x>Wžīī¨9‰T/(SN,…ˇPxŲ÷ŒR*¨0ĮBŪļ™ÜÁ Ģɧ0mƒūLž’x”…3§Đ¸x.ķfLĸĻŧœŌD”x4Bļ†Āõ|Ré,ŊŠûwņÜĻ<ųÜļîØCČ2¨™PĪÔsĪgŲëßJÍô˛§ņF⤠~FđÆá脑Ŗŋ7ŦāΌ`× Oūü7ŦúÍÅK0 k#¨ˇˇĩļŦâ¨4M;CãŖIOĶ´Ķqŋ0Œˇ Ķ g˙NŌ}”Õ5PȤGE_ŗR2čą6ĄģRBAĒ'8 xŪkoĸnÎlžģũˇė_ˇŠL&Åũ>ĘÚõ›¸ųē+yíW3ÎLÂĄĐĐÅ0BRé ĪŦ~žŨúVŽ~žhČĄ4dâš>‹šŪĀEų.1’ŨA~¨žr!^6ī˜0kÕSįppãsė^ũ$‡ļŦ#Ķ×̈́Ę2˛y^š†ļĩ)+M0câ&O¨ĻĻĸ„˛D‚PČœ¯lŽIertõôq¤ģ‡]ûqāHŠTĶ4™2m:Ķ–,göŗ1qŅb, Ō=Á•aųVƒųŧâeũßū`‚ô=ža‚0 xšėK1Đ;4mTŌ•fMĶŽĢąŠyB<įģų˜0L.yûG˜qÁĨdúGĪ Ûvp†°Eãe×/ƒ¤&Z…´Į–'eCëčÜŊ…d:MÎ44ÔsÃ՗rÃ5—ąpūė1‘<įry^ØŧßŨŨĘŊ­’Éåˆ;Ę÷¨˜8e¯} ķ¯ŧ! Ķ7ø—N3ą<^ĨųĨŽUhéA˙áėÛø<۞z€Tį! ĶĻŗg€‚ëS(\Ī÷ ;Žm![5Ę%ļMǎĄ<ĸą8eÕuT6LdâŧÅL?˙|jĻĪIJ!— 6žę|ép4i~Ļå6ÖÜõsBą†amŦ4?WÜč4MûSēŌŦiډėCŠ5†0/qķ9ÚwŧĀôķ/Á0ÍāĐŌ(āû>RĒaiFP)LvA8fąøúĢ™´p›{”­OÜO΁]´īßĮūīW´>ú$—Ž8Ÿk/ŋˆ…ķfS]U1äņœŠ‚ëąúų Ü}ßÃ<üøJÚ;:‰G–ÅĖKŽfųŪLíŒId“Oíg”XŠW¨änõsķ9 š,–ĸfú jfĖ`×ē6Ļ ūęīĄĢ§}qčđŽtuĶߟėōØt Ã4qsY\ˇĀĖÆ‹¨ą€De%ĩõ”OœJ8'/}ƒíŖĒŽ+0L+x˜u KĶF54kšv".°RÆ%JJzí#Ķ߇Mā{Šĸ÷5 !ŌĮ÷\lĮļ-߆ųt0ZŦlB-žųÍL;o)›y”ŊkŸĸûāėÜÁ¯öāĄĮŸfÁœ™\uŲ æĪžÁüyŗ ;CØ>rĩw°vũ&}r%OŦ|ŽîŪ~ %)ą!d™TO_Äâ^ËĖ /Ā;$ģƒ‰Æ-D4„!ƒK´˙ĖŅ•Øž›#ŨoaŲ&ļobŕįsĶuWP(¸ô 0J“Íæ1‚ũØX–Å/ūSnũíí\öžO1gųEx”§PžO!“Ã/8Ļ1d÷i¨-’B6RęčaIe‘jš:iÖ4íÄ|ℸ„0 úīgāČ>ęį/ϐĄčm ŌwQĘyŲĄ°Ą&Œā``ē,&Ė™EÍôYtĢVŗķ™GéÜŗÎ}ģyäđaÚV=Įä‰ ĖŸ;“Ĩį,āü%‹˜2ąXldéėfĶ–íŦzn=k7nfË֝ $S„m‹ˆ*`š•Ķ2˙˛ë˜ÕxåĒɧ!Õ=8…b¨*ą „iī<†g¤’ŖÁģ\Ȃ›ÆDÍDÎŊq"ŗ/š#Û°ũ*l\KĪÁ=ôwwĐ×q˜ƒ÷ŗjí,ͤǞ‚úÚ&ÔUSWSEuu5UUTU”cÛ6ՕeXļõ’›\—žž2š=Ŋũttvq¨Ŋƒö#éėáp{Ũ=(ĨŲŊ*d;! “¨Ÿw.ĶÎģ„I‹æ̍DÉ`ĸš6œ}žĻåāŠ<ÁG'úžQ˜6Øū5åqfΚyR×]p=Ž>„Š-‰ĸŽn)Q Ã4GԌ@ŧüÁ÷>åŅ4m”í¯&šĻW¸ß°Ŧë”ōéŪģ“|f€hi%…\ļØą”Bzۈ'GGß/ønĐēáY`‡J™ŧ¸”†ųŗČgnáČöŊØ´–Ž};čÚŗŽN Jq0=ĀÁ}{Q†‰c;„"alĮÁ2M –mūɁ’ߗøRâK×õ(äķ¸ž‹PåûĀ2‚ęjŧφšisŠ_°ŒÉ RZ[ŠÉr|īØU+ĨĀ´LlĮÁÍįƉoĐvāĀŽÍ”ÆÂ”•–Ôõ÷ôĶŅÕI(ZvP~°iZ °Â4Pckų´9§iŖ”Nš5M;ĄļÖ¯ąŠy­LĶ$ĶßK˙‘vĸeUÁ:íŅĐ(*Āķ<,uldīČŪü`(Ŋāô;8‘ ĻžWÁŒ ’éËĶ{¸ƒūÃißš‘îŊ;Huw‘Τ(ä3äzÉø>†i" ô8Ū8P!‚äYIå{a‰ÅEb8Ņ8‰ĒZǧÎĸfú\*ĻPV_I(ÂÍH?¨Ž#T€í„‘ž‹īË?_ÅŽ–"3ā“ę<Ėĸ3äøžūž>ēzzˆÍœ‰’ —Ų´m,ÛŨ ŗ‚`zÆËÎũéĖM͆‰Nš5M{5{”b§i‡f˛Iē÷ī nöB ÃDË:ĩS§TH ŗøCŽ-´đ+ĐÂÆÛL˜§~Î4f6ŽĀw}2}iúېę9BēįŠž>ÜLšB6E.9đį‰%Áũ´CœX'!V^EĸĒ–xEe “‰–Æ0m+dnrÉQđŪF0 ėH ? ZH^RIW€‹Ōy`~˛‹ /¸’ČIn[ėëëŖ IUŧ0QŌ †"EyuJ }rŠ䋋Z\ôA@M•tŌŦiÚĢéĩΰŦųt’žÃû‘ž$u§ąZyČŠ ™ôĨa™ŖjÖ­RÁE‚‘uAķ°…ņĘeuØvĐ.áæ…œ¤Icg>šRĶáDlėXļĀ÷‚ždĪé˙ž/Œø]}UA›†…‰ĪfáĨ‰ŗRX!H÷Ļīđ>‰’ãŪ˙ãYŗv-ų\žōę  ‚7N$†a™Ŗ;afđ`ŠrÉôu#}! ”ōģŅąAHĶ´—ŅIŗĻi¯&ŧ€¯Hu!7ĐG¨¤ŋ/rh Vį$ʓ`;˜“0˜H+ …ÁzbNņ C`;&ĄHɉ˙ē Ö?R;ŅđãQJŠÁ>cĀÍį^0Ā2 ŲĶɝwŨŞ .$cš&Ļi"ĨÄķŧc_īē.ĢV¯æŋūë{–ÃÄ9‹†Ā‰D1ÍQ:bî8ôī—‚āāhn*Ņ´qK'͚ĻŊĸļ֖|cSķaŌ×qt7Ҋ* ŒŽœM Ō;öą˙XI˜Žl#đÛ:xĨJą8ÁŋJå„†E!—ļK*E6éS9is/¸‚ß˙ļ…Ūž^&M™DUEU•Uô÷p¤ãÂ4°,‹ƒņØŖ“Îd¸ú]Ÿdúō‹ąûØ’ąâčr——0“ĪŦĻũtŌŦiÚÉØ*„č5 Qžîî ÕĶAÍô9¯°#yd)@I)%æ(kŅ8-Ŗãa6G'j„b ŧ|ßuÉ%ˆ–”qÅģ>E8–`՚§Yĩu޲,|ĪĮõ\”’†…Š1åüĢ9į˛ëū?{˙'Įužųâßs*tœ<ƒ˜ŗÄ,Rĸ$T–-[’%­ĶÚŪëŊžëũí]ß{íŨkŲģÖū$[˛,˖ĩ’ĩĻ$JĻ"$&‰$˜3A$rŽ;V8īũãTā O"qžŸOcfēĢĢNwUŖŸzë9Ī˅o˙ ~Á'iĖpķ‘(ˆš)iŌ›IŊ˜—pĮ‘8Ņėp8N„Aā%íų7˜$bäĀŌ$Åķ|L:ũs–c횧2 ãøˆ€VŠ0_Ā„9Ō8&jԙŗâ"îøÃŋâÆƒ;1i‚1ƒÖ Īķƒö<ōĨ2ĨŽš”:!ĒAŗūÆ;×P Ã)qŊ†:™¸oÍęģâc=ĪápLN4;ŽaX`“öü’fá=;Iãíû˜$žūŠs“`Lz¨ÆŠâx†ŌĒ kOŖŊž Iĸ˜°T`nĪ9‡Û,”Ũ­*ëĘ-’*ö÷é>OkĪĐc×ü4Įáp'šĮ‰PEdƒį$qĖāžHRĮ/ļ33Bį2ŨœĻ‡|ÍĶ= Į #Ų„FĨ~ š#öĸB!j´ķ´=?RoLÁŦTë I‚pXŸKÎp8f(ŽëÃá8.kVߕëP ĩGŗ2D}d$ëŊ<3‹clŪ­ãG+UDŊZ ´:tËTĻ‚ŦäüÆˇĄĄ>4LT¯ÚŽ–ļô>0ŨÃr8ããDŗÃá8Qv"2ā…!qŖNe⠈;iZQ€AŒ™):ŪqēČ˜Û›Ĩ¤IvĖŽ´oŌWëpŧņ™ßv‡ãĀ Ā6Ī÷‰ujƒAéĸP3_ŗãd‡ãĘ0Đ?Ũƒp8ããDŗÃá8Q†Ųíų!qŊJm Č&1Í•ĒD0ŖžæéÃq h-Ԇˆ5Ũ#=ļÚlŧã-ípL=JCšD4ë#hĪGACŖ‡§{\‡ãč8Ņėp8ŽJ§i˙'/ĐËōĨåC'íŧŊ÷fŽn^Éļ§4cööÚF4jĻ”t•BLŠI2˙õL›Ã ×Ē B€O]š­ņ.'šŽŒÍ‡ã¨ˆČ5q’.‹ĸ„8JÉÂŧ‡ōíēĖ…ë`öÁ”ûojđęŌ?0V3D<+›ƒ+ޟļc†ĄlFsŊ2L5ŠąGú ‚ÜŸÎúũmĶ=4‡Ãqt”¸ŽY‡cęŋ]7’#öKŪ~ҁ&,„yĨ4 ÕđėĨO\Úā`WŠČEĪ0m^Áķ}ÂBĨܜgĮ B”RžøÉ= íÜŲ÷..+^Xš°°ėOéŋāķÃŗįFl[ŗk–ĄÂXĄÅVŸ[ˇŠ@DPZ‘+–Į6p8ĻuøyŖ(hæ ĐH™˙WšvC7}ŗíƒ üÅ'ų"ģĻc¸‡ãč8Ņėp8ÆĨú‘æĮŖFōOGK•#ˆ€ö~Î#Ÿ đ”FÅ J°cVÂļ… % tĒCâ žŅx‰ EĄ'ŗiŸäKe´ī;OŗcB5æg&ŒĮûŊĩŒ({E&ˆaҏæA˜ģČĩV8ēęŋÄßōũŠy%‡ãDqžf‡Ã1.ZĢÕžīmJ’dŠVęušSi…ŒQ-!n¤x&=r5=lķyëS°ˇ×°qQĖūŪ”ŨŗR:RŌė„] he”ũ™ŨätmČ Á$)žī;Í|p´ĢcĨņ~?ŪãŖâ7ģOĩlōrčáŅģ”=ž uđ#ČG4!ŒėmÁ&¸āPU C+>D›s ßá˜y¸JŗÃá8*͏6/7ąųZ›e ņS†ņ§Õ‰ĪWøž&đ}|íᥠU‡Á.Ã]ķRvÍJnjyĄ AeĨ:?Qx†ÃĒŅ'-¤Åā9ÂBá(ŖvĖäÄ÷ŽŅ‡„ëØßEņ Fs¸š=|s€íĘ7ú¨ėąægĄã~^j×$ö§—ØíĢP¨ŲeJU{Ąb˙ÎW2ąœ€C؄00Ub „)–GĢÔI#ũÛ4M˙ˇü˙ gX˜ŖÃqfãDŗÃá8&>]\Ķīĩ,)`00Į9" ƀR(/Ё‡¯<üD[ĩ’“‡ũ])ģûRē ûzStĨTІTAꋭÜ…Ÿ‚N[•h[åV-ąuÄcV€;0W*áDķë9Uųx'/GŪwĖŋ˜#*ē2Ļ ņŽxLĮĘÉJŦĐmUw•0*Āuļ./ĩ~j¯h+fķuģÉBtl/Ôėzō5ČE R(Tív u+„=cEĀ6ŋ–1?[~|lĪŨą7›>ŪûŽėcidh G{R#h˙faÍ8K:ŽiÂŲ3Į1Ų:l¤ĻŊ0Đ0;0´kCÉžû…Ä(+`ÆæåiđkāÕ8ėÄ">YĶOívZÛ Rë —rČļpēd•\e˛ß[ŪāĖŪ0úûŅ*žcĮS|Bˎ]îX'BŲ•c“īil0АĻû˜{*ĘW÷¨Öž§ĐÚÃķÚĶøâáŊRSQj)š#g'ŽņÁ„TŲeŌąCv¸`īÃĒÖÖKļbĢÆ‰Ã3ŪŅ|’ą¤GMŲË^JžaũˆdB1¨#š@Xã°jŠ`Åŋ/‡ūnáĩŪˇ1ëįX÷K(Ē#î#Î[WȏėØ ėxË&q$G$YfŖ8Ņ›ņÖŦ1ȘŸŲU Á¤V›THA;365†45ļĘ,Ų•ũ hũ˚đcūŽü/ÅWOpô‡cŠpĸŲápŒË +7ÔRũž1ÚvøĘzšSĻFSĄėO>ŠJžā‹×ĶV4ŒÕd’M*´V€L8pH„BœZÕĻĀÆÜe7ßĶh_Ą…ŧĖfĒGśJí*ĩØI„ĐČndpâÂjĻ0^õôx"ķHA;öž#Ŋēã­ëXĸõT™¨÷]ņķTžoÆüVüf3 ÍčĨc'¸Ļ­™°ąąUâXĐikYŖ%D9ąVąÂXyv’)m+–íŖhMĖ—”S<5s8“‰Í‡c\"Q˙wŒĩ,GKķxc.īD)D(jDžR”|ȑâ)(yPō-2*ž5’UÅ6E{ąú° h’ŨŸHiÜ2+”­õ¨0ŅÚVĨĩÎÄJāĄZ>ácU:eĖíh—öOĩ!ËÉxe%TĮ{lŧ †cĮ>ĻR|d`’IŊ I2Ģ˜jĩGl.Ļ#yIyjÔ#˛ˆ“؃¨ĩk•dŪic_ŖŸZOwj@ĨBƒ—ŊūŅÂĩRĪūž„vŨQ(öŠ…§HB…  PU´ jšžĐĖ ZŠŅΈ2 ÁZ3Æ=ČKOéE:ŽIʼnf‡Ãņ:ž_Ųx_ŨčN`ŪÚ(-)3*ĸiÍĮ†c0h _ƒYqj(PÔb=ÄÆ÷ŦR÷…Ŋŧm2˙ŦĻ­ŸÁ€ĩ^˜ÔÎũĘ4ˆn•Č•˛T Jk+°ŗ(<´˛ŋ++´ÁÚFZ Ja=)cņ8ē =YŽĸ™ZŨĮ.g}¯ŲËû7ōÅĒ1ĪûxzčšcE˛}î8ƒkņ0¯ā¸rųZãq}4ŨhNėmöĖĄÍ+ÆDÁĩOm ÷Öûi끅JÁ„ហT‹B3gŊãq uŲIĨJĀ„$9ûœÄŗö •6Á˒d’PČÕ=ĄšxĸTzIčą)žŋvŪ]ŠŽģ¤Ãá˜2œhv8‡ą~UŖģnԟ§āŸŒh>’L¯Žūa×eÅ`$ PÄ)TŌ–b9 >ļšé =Čûāc˛˙´Ŧ(ÔƊtģ-E’­W1ļ:jE7)H’´î:$\˛*ŦÖÚŪ­Ŧ Ʉö•MEŽ:Įˇ2I G_Näu•_G4CV•l=mŒ ÄVô‹ü8bh‡ģ3ÔáÃ9ŌåŠĸ;ïĮƒ=fĨ’%‚ŒGęÛۑÂ9*ÚD”ŅÕ H’•)ąĪkėēĩąuZ!Ģ Ä!ėŸcÛˇ{‰§É˜Öķã<Ŗ'/ĸĐȃ6BĀ`—zÖz1öƒû—MņhEZB‰ĸ^†ûߛ˛yÜō]MĪ>MũÄãÂ'Ëãp8N'šĮ(ëV5T$ęß7ZzdõtQãüūē+íJh$P§Ĩ mØKmķ =FđBV•Ö€¯„6OhĶVŋÎRp䡭4LĐKËh ŒšŒo˛|` ¯.†{d´ŲĘx šHŅ1 Pæ(ĸR9˜ņ‡8îãęõ*ôt :šƒÖ‚ jEŗ%âƒå€Qi#RGZī[Ĩ õ˛ŊŌ rhĸéYB­(Ŗ•ä–įzô}ĖN|ŒčüA”­ˇŽņ ™Q2zNqäĪŅר^j[šŖJų¨uÅĸu‚ė „ÎŧõĐōũhm á*ģrĄb%lŊ6å}1īüŠaÖNEŖüÆŗŅ;‹Í‡c,WWŒú”(üé*syŖŠēuQFlôîX†HHEŅí =šÆYîDQcEl&ÂØfGExí|aŨ•ÂŽĨBĨCFģĶ‡QFBį Íc;āŊNŌŖ¨_ī$æ¸JúøûėØKØęŦ0ÔÉ1Eŗ:ō čx*PƈÕ#‹Ô†ŅŦėąxŠB™qĒų-Ņ­‰ĶąøÆÚpZä­s}ØUƒ–¸=ô24‡'#ĢŦĢߥÚüčyĘ1^¯Œųˆĸ{DĶ^ĀŠķî/ĨkŠ(wôįÚ¸Ãá˜i8Ņėp8XŋĒQŽõ§ąĐs:ļŒÉāu::#U6:mn˛0LŅ ’Ķ-ã)+ât ~âœbã%)/]%l]nˆrļÕrĮ~{iūh JaĢÍÃ%É4ņĄå”öŽXöõI{ãĩá;úžiU:­`¯/īą]ąBĩēåˇ––ī{Č.(´V|¨‚~ŧƒP˙žļž#xØ}ÂqÆØĒdŸä$P0ĐŅ{.ĘķĖ­5ŽûW{|×eņœpv8fN4;Â֌ēqēĮq"6'ÚW0/4Ė ÄĮ°wŲƒČŪ’6^ <mÂÖeBBĄŽ)4Ô¨%AyúPr›cCU6 ×ēÜ?Fđļ<ĶŖ#†ĮK‰ģ‰Sæx"Q„§š“āt^Îɞɘ2°`ŪčėĩĻ1˜ô¸į'Ov–‹ënĖqösulÕÔ Į|Ö0Мā‘8ŽĶĉf‡ÃÁËĢo­¤ęˇ™Ve>’–8.ia~hčõ ņá'ŋNm›ƒäëv׎ŗ„gŽKyõ’”zYŅ^ņioøčЇügĩft̝ÛöņsÅ7îÁxžã؈Ø@”\´†$I!W 0 DuNī`Ŗ _Á^ŸW¯ö˜ŋYŽWmčŧģäö´Ã1ÃpĸŲá8ÃyeUc^%UŸiĸf-“y& °Ö :=aQ˜PR‡î;ZųMėįۚđŌ[RúĨØgv- ũŅĘņ ­N=ĪŲ1iˆ€ö ß•ũũl}ūöm~ “æŸw1‹.9Ÿ|{͊]v"ĢÎ"PŦÁļKBúŠéÚchæĮ_Ø;q[v8…ÍĮĖ‹+šXø?kĸޝÜL"Ę*„sũ”š!PB|Ԝŗã#Ęú–ËÃ61â…k Oܔ°û,ČKĀŦfßķ‘Đnczy8&Đ>ÛaĮÚ ŦųÚßŗķ•gHĶ…bíũßaŅÅWsïüzÎŖ64ÁÛ×ÔĄÚëą­w€Î]yŽbÜŽāDŗÃ1#qĸŲá8ƒ‰áSUŖ?ĮŸw5´ĒË ™#0ôx6˛âtsĄfS1v-ž¸)eũ%†¤čŅįÉ Í¸pby 9Š'ũ4võaäK°oãvîûÛ˙Ξמ§ĐÖIŽÜ†BÕ+lxü~”†wũÁ“+ä­Uc"ĢÍŲ?ĪÉ:.āŽōŠĖn‡c†áDŗÃq†ōŌǯƚŅ’Bi& f€f&X{=ÂĐPÖB,`NQE‰ļ“˛Ú† ĘÁ“ˇž¸ŲĐ?ËЖäčŽō(­m÷A'–'•#ŖâDAچ#GîŪ Ę:ũeMH_Ņņˇ%b=ĖqŖÎãßügöŧöˆg”¨ru( ÛĻ1c#áL ˆ‹—ŗF3ą]^Gö§]ųҎĢ5ø9Øôä+l{ū1ÂB[&˜ V1Ĩ}”Ōl~úÎŋųx$Ņy›<ĸƒėŲÎú9GuS—Æ‘Ņy[°E‡Ã1Á8Ņėpœaŧ°˛9+ūļ.ęė ×šZąqŗ=an˜RŌ†uZųËFCą!h@>Ņä‹DĢ“Îáuœ Ų>mæ!*—@XƒÎŊ†ží†öũ†â¯ ųŠmyŨš¤ŠžwŽĄAE÷ †ęrEm‘Ļą@‘ļ ÁŨ`\ˇƒøyHbÃæ§Ÿ¤^¤ÔÕk;?‰Ī¨ÜËāŪ]ô.žGMØ[€!čg°ļũēŸķņŠŋ~Ņ×:ī.LĖVĮDâDŗÃqņÜĘFGbÔįĒĸŪ3kâŸÁú—KJ˜ē=ƒRËë[bŸ,ũ0Ü-<ô.áå+ FCĄb[+…Ę×N0Oĸ .@œ‡Ō,\—2{sĘė× ûS‚„uÁKŗŽ‰:KŽ ë¨a] ōÂŧo%T—*LAõ@åÅđĨ•ķQ/ø#ļú<öĀV < J…Á=ÛAíy‡U™Ä¤†$jLxfŗįC}p€ū‘Ŋôį‡ĮĶø)°ibˇęp8& 'šŽ3„W6ĘŠđßĢĸ>3gâŸÁļÁ•0'0Ėō ĄR9=_ąhČ5Ŧ%cÅÂĪVĻėŸo˙Sŧ $Cᯘ´FÉZ0foN9ë9Ãü—zˇŧTHF}I¨HZũ^ÔáëHÅH(’˛B<…Ž …mPÜ`čģ?ĨzŽG˙ĩšk4qŦüĖÖ%@}¸Bŗ:‚ĶcXđC¨¤^AŠã*ōmĀ+S<2‡Ãq‚8ŅėpœŧŧĒ‘7đŸĒĸ?)Öâ9#H<}~JŸoh÷d´â|ē*``ö†”ķMS4§šeEAé•>^‹q qģ­.ĪųnBq‹b×G]U’éŸbÕúgŋÕđčí†FJÃŲd˛Ņø2A) Ÿ…Ģ2Ÿ.b“IšĄ­.ŋõEXŧÃŪŋtŦ8}ȂW ĨACT8É}-ļ’iEۈ :ö‰Ž€É؊vĪĪ IIąã4ášīū„kŸ"ßÖÁ¸+Qš¸QĄĐŅÃų7Ũ†3q­´ũĸú#÷úyōäŽÆNāš‰ŲšÃᘠfŠĩŅápL0ëV5Ū1˜ĒLĀ›*Á,X!œˆĘæa)ڔ° 0,Χ, S:3ßr$vŽÖim¯Õ {ĒmđĶĻÜûACBÛ@æ_ķÚ­-#‡ö='˜'€$gŋDŪō2ŧ÷Xŧ֤͊ đā†Į@uhžž=ď_ßĐä¸(Đ~MĐM…ߎ ŧš0ržĸ•O>ëīũ9kūųo‰ęUÂB‘4Ž1ib'Š ÆĐ"MŽxĪ/ĶwÖ• L—+žëCũT*”T‰nŨqäRۀ ĩI‡Ã1ņ¸JŗÃņ&ä啍w õOĒäMĸ`ƒBƔĖ|žÚ=C§g()ČiģD"ļú &…ãÎĄyøÁÛ4ˇáÂįĀK!ÍÛ*ąįÃ-Â×îĐ<ķŽëîn2ÜŖOø€tMđ*Œ/š•Ŋꐚí°íS>ņ[;ü÷á3ÄQ…ļŪŲÔ+C4kU.[ų‹Üü‰_Eûģú1&ĻĐŅGĄŦ‰jØÄŒ 6.je_îāž¤&Ļ ķŌĄÚ0‡ōDļNėVĮDãDŗÃņ&aŨĘÆģūrS˜3QėVÆ@S@ŖčōRē}Ą]%(ž’lԟ"uX߈‰t†´äEû T:„W^ēJP ʃYvRøšüŽæĖCˆ;!Ü',ū۔Îg ÍYë.ú̘×~ÅįĮīņ¨ášGÁkdÂ9ûāÆgáwœõbÂė-)•nmSMŽ…ļ'Bh+ĀÕ8'<ĸl…9.(ļū†OúÅū5šīoū;ÃvŌÖ7‡¤Ņ 60ĀųˇŊë>ų1”ī҆ļžn”‚$úP6ņo‚ŗhšĩ„ęĀI)ŠBŌKwKZP¨xČY3Ž™›čpŧ xyUsՀQ s&ĒÂŦ°“õbvO8;L8'—2Ë7äĩ •mP‰"e;øMĐļD´­^ļ÷ÃŪųÂ÷?fxáZÁOl“Ŗå˙ŠÆŧ0ÄķŧIo§ÆÜÄLOãšIÁ@Üēg})ĄãŲ”¨[aBEÜ Ú(–9eū?$<|<đvkĄņęY‚J.xfEŠŸ0‡xš !˜ã|ŠĒí Ŗ~ĶŖ™˛Uh@HJŠ-ŸōIVi^ØÉũŸ˙ CvRîžEĮԆú9û-ˇpĶoũ:šŽ"ĩ~HchTĄ^¤‘ĩîž$˙ŋ—ƒĘƒ4F†IUž>ŨŖš$ƒĀ&gˇc"qĸŲáxƒŗvUķ×RõwMQķ54ÉN !ŠPÂ0eY.Ĩ/0ļ*'‹mDbĻ@m'üĩ ÂæķážO¤l:O(Ô l§a†ZûanRlK(ׁa  ”{ W|ø§SHK€ĀÂLhÆu)Ä;TųÛ–|ËpÎgž™?¸ÃĻkø50xĀ۞€ÁåĪßäSĖĒČĮØJ *€ĒC0$˜pėƒ Øū1Ÿøũš—wņĶŋúKlYOŠŖ1)ÕūũĖ?˙rnúO@ˇ´Ķų7)á^Á”˛ũ6QFûŖ‘ĩĪ9°‹úđ JA›*yælŽ [5“8‡Ã1A8ŅėpŧyqeãRūĒUažˆī~}žay.e~`aRĢÉã!Ų„ŋâåė0ύĖčĀ#"'•–?šÕÍīÁU†{?R+ŲH9%ĮĖ"í{ø?ŠB?4)ą‰›e ˇ‡OqQå!~üį˙?ũōgņ|đsŧņbQt?l˜õũ*LĀø¯Å،æftŊh¸čĪböí1|ķNØ7t đāĸ— [k~Ÿo̰Į¸Z  $9Eę9öxOØúq‘_ö¨īáŪŋüKļŋđ8ÅÎn”įҍŌÖ;—Ûū÷˙ĀÜå‹i˙lJ×ÃҊöį ēiĮ;ŲûE{ÄPéīĮH‚§|9OŸŨīŲė™!ā“;‡Ã1Q8ŅėpŧÁxöŽfG _2ęß§Pšˆq’ ãß°,Ÿ0ËOÔ¨Xžj”Ō4Jđã_LyäŨŅP¨`ƒ ą—íũ ‡ŌzŌ^DđH¸’ĩ\ʋäT“‘DsV‡áo?Ÿu?˙/Ũw…SwÆ1A(¸ ōۄšßLņëļâ|LZBģSQÚūEBķ…”{n…m‹JU¸z-ė¸<`ŨUÅ9ę>UÍĸÂĢØũ‰]ˇą‚ģßë3üKi%åįķ6<~?šr­šÕ*…ļ.nüíĮÜ+ÎĻüuÃŦ{L¤E(l5äöŲjķdî PúˇoEk”bŽęËe'ēĪáē:oœhv8Ū@Ŧ_՘“Â]UŖ~A 8Ũ°ÁNôķœϜK)*ÛŅoZŦ¸ŲåøŌ˜ ß˙hĘ × ųšmbrŧęr ´ā“¤€đ0\Æz–°•&!1ŠQÄxüʇoæļë.āž¯˙w oŗé oÄĻ^¨æÜ“RØ&¤…ēÂúœŊēpá˙H6|īXH ‹7Ā…ģO­ôîQFŲ4”CXr ē.C‚?*vŨé3đQĒ1˙ÍßōÚc÷’īčÄ r$Q íy\˙Éßåėˇ]Fū_„9˙’ ųVš‡Â„L8id~æ¨>ÂđŪ]¤JhķĘôčŽvA5¯uŪ]zŖ]ƒp8ÎXœhv8Ū Ŧ_ÕX6lÔ=ŖŪÁ$c™ĖŖÜá Ëķ s‚ĊÁé‹!_ƒmËāģŋš˛ņBĄ4 ~|‚93ŋúaĨô¤ø‰S E8›œÍ&<’1 žÍ(A+øØûo€Ę>ÖÜũuüü7Jȧﭰ;Ÿē1H59É÷2-+D+V|)ĄīĨüč*xö- \ūä:=WhÅed÷Xă}K<6\ëspžG´@ĶŊGm‰bī>ũŋá!ÄŦųëäųÕß&,ņïÄxą”*ĩ–Œ "äweĸy’ŋĩĩĄƒÔRWMÎŅgUfŅCDŌKÍp8ŪPŧQū w8ÎX^\ŲĐJq{%՟ŠV´%œ*‚Æž‚ųžan˜âc1ĻŗäåGY‡ŋK…ûߟ2Ô m™ßúd^°ÚГ¨P Ëk˛ÜlÅ3) •cėģ§”b¸Ōäüeķ¸õú ųÁũ?å-ī~s–Īgd˙ÄgO(bŗ?ŗ~”āW é<Å#ÎØŠuĸaáŋĻx#†ŧZV\ûS¸îYøŅu>KŸKXöLÂÁŚu7„lģĐŖžĻŌŖđ"đ^ŊÄ'8(Ũ ˜'ūéÛ<˙“ģȕ‹ųqŖFŌlpՇ>ÉŋpÁ“ÂâĪ%x‘wXÁ öXRØlg¯~’ĮÖIĸ yī4ŋākĮ(y°0Héö Šņd*‡ãĄ WŗâåÉ[„‡î0ÄtôÛ¸š“ō’52 ›#= gFĀ `AŽJšVĄ!aÖņđ‰q’rĮmWpī#/ķĖę{x÷˛ßÂĪAԜÁÂŲŗ–…ŽĮSĘ녴 ˆR§î ›œ!>,Xm(ėKxęw}ęw*Ūö\Ū§xáCķ<Ößāŗû >Đƒ—Ļ ĖRh“ōÄ?~‡Įžų÷ų<šRQŊFm ŸĢ>øë\ņŠāŋ$,ülB8 Qˇ˛—2`|E0 ^UHÚ&īØ÷<ëkÜŊ‹$PZ3OÍęō•Ž×Åüã¤mØápL 3õŋm‡ãŒįŕÍļDÔURõĨXTūt­—­\å>OXžKčö ą@:M‚Y2™¯BšwxOŠQBy˜ã6žw€ō<|?˜´ ^6>l€ĮW¯fhpˆbš†¸6De`/ĩÁ}$q•Đ$Ŧ]ũ}ķK(O“oī nÔh rÁÛîäŠ_˙EÂ}0÷ķ Å톨“ÃķčpčČzļ'm"`Ö Đ$0´{•d„Y~o˛Ü[ĸéŧģôô$mŲápLŽŌėpĖ@^ZŲčKDũįŠQŋk'œ:ļēŦ•a~`˜¤YˇŋéĢ.ˇ.‘—G`¤C¸˙ŊÂÚĢ šlÂߊælÍøAx¨<9Á( žgX÷ķxî[_æļŋøuÎnË34RĮį2@’¤tļYųļKųéĪū‘Gīū:īũŖ?"WPDõ™WmĪV…Û^6´­L˜ĩɛˆ÷RėúŖ^E÷Zá˛˙3fíųŦģÂī‡ö*”‹¤†­/¯cįÚčßž™j˙”įŅŪ7—bg¯Žší{”:{HĸˆĘÁŊŦ¸övnú÷ŋGžŗū2Ļ}õcÜtEĐÍIėb‰Í#‡¨ØG•: õb9×;›HŌ/ĘûUŧ;œąįO‡ãõ8ŅėpĖ0žŋŖyA‚úĶĒQī3œž`NÅ&d´kaA˜ŌéKÖŧDM[š(ۘ¤T}ķ…ûßkØpP¨BЌôīĨ2´ŸP‡\ŦĪÅSūō*¨NâfĮ$áDŗÃ1CxyeķŽaŖžŌ.ËžëO™f–Ŋŧ8LXĻä´gUįéÂh;á¯4 /^%üđc†= …ļ!đŌĶL1ČÔ§øöĩÉÍb×=īœĨ”úōŗG^dמa ųđ¨ĸŲĄ%Üø–s¸ķ×°îé'xčë_%#ëož)ŲÍŲ%â!ėÛ.{‚ĮĻ{Û÷NĄËáģ`ësëøéį˙‚Ŋ_ĻÜ=‹|{'ÚķmĨ9{[•ÖhĪC)ÛģÜ;Ÿ[ķw˜uņ\ĸ-B÷Ã)ZŦW9´mļŊaPCÖÖ\íSô¯PėžŪcû*ũˇhLIĄã‰}ˆ˜6ah÷>šÍžö9ß[ž-OøãđÛū :]r8'Šŗg83€—îhū֐Q˙)æj¤œÂÚ1b Íų)힌&fL’eá+6Vîņ[…‡ßŦ€ÖfbŋDО‡ö'ŗS…%M cn/ßü.îû‡ĪđÞã7~ņFÂĀ'ŠS´>üÅ(Í8Ĩøõ_¸‘u¯mįūo}ƒYgŸÍ[ŪŋŠ$‚$š~›†xāW…ōFTa&ú­đBušf˙Û=ŧ(ø0¸w?|ãË íŪB[ī´ÖČqēĀ“â!šB;i ’ŧ"ęTHŗ 1KCAHˊ´¤¨Č)CˆŨfįÁ”z¨Fc”:- ÔQ^*A•&ˇm%•”6ŋ QæŊbļė9—ŗ&x‹‡c*p•f‡cšyūŽÆõį§.˜ÁÚ1ŲžaY.ĨÃŗūåtÔ´JÀĀĪî4Ü˙Ū”(…Šm“<ķm@­&¯ev‹$ļUĪKnŊŽ…įđģ`ũĻ}tļ_'˜GĮ'ÂĐHĨ zøŖßzsfwōÃ/ū5Ö{?Ąų‡ēĘ˄…Å”s+ oy%âʧbŪōPÄU?jrÅŨ1ŗ7’p’š›d›ĩAúwnÆCSđ •Ī6ūņ+į~įŦɨm;Ž)‰f‡cšxęŽĻzōŨÍ/ũ‡ąĐîqō:Ĩĩ|l@)áŦ\ʒ0%‡0ŊŅf’]Y/AŊ ?üeÚw<ʼnttfŲĖ­*ķd‹O4kĐ9o7}č—Øļkŧëę̈́ba|›†ÂļĐŠsí%‹ų“?øž4ųÎ˙ø4{7m'ß6͕ælÛá~!ėˇâvÂ7!uÃĐ% ¯A†önãĩGî?ŸGNĐĢĸ=8jШ Ų,o–īŒšæ‰&7ŨSįļ˙ÕāšīGœ˙PÄŌĮbf¯OéÜn(‚ŠšęŅ4ķ 5 ö˜ÖšÛОũ4FŧŠ2ų­fįeŋ5‡Ã1U8ŅėpL¯ŽjūWMÔ¯Ĩžęåa4ĸč ËÔšíø7­…Kemå88WøŪĮR^ēڐkØHš MģSĘf3{Áņ— ŌLĒ8īĻ›8˙­ˇ°úgĪqßŖë<ĪĶãž÷JA'ŒTŧûæ ųÃßzC;6ņĶŋûõÁa‚ü” ˙uHæĪotSM¤›@tÕs4qˇõ°īŨ¸Ą};r…>ÛQ@š&ų"ĨÎn’‚*œõlÂŧ— ĨũvUbˆÂ(Eĸ!õm¸4:Cŗ´'8ŅΎ“FnÛAÔlā‡!՚4ō{í;>ü•ˇŪūá &ẋc pĸŲá˜bÖ¯j,2ę‡5Ŗ>„M>%RŦõĸĪ7ŦČ'tøb˜LāXO/ąö‹mįĀw>iØ|žP˛žæ ‡AkĨô”Y”˛Õæļž2ˇ|ä—PA™ŋũ§ŗ}Īåbu”(Ĩ¨G1Õ:žãJ~ã“wđę÷ķøŨßEédúr›ŗĘhĐ/öŒkĸLžŲÄIe@7…Æ\09ÛÛ¤†ƒÛˇa’?—;ņ}§IŗA/Ō=§ Ąm!h QAŲ8;Ī&ĩˆÎގVܜ‚°.ÔÚŲ7[OđÆZ…lœāū­hTFíqË5+äĘ ˇ5ŅĮ†+ÍGŽ~û˙âēÛ?<įúwūÂL t8ĮÁ‰f‡c yeUcéPĒžQ5ę&uŠWä[•d…b~h8;—*!’™ŅiÎK lĀĢ— ÷|"áĀ<Ąm0ĢčM‚<@koō:ģmģę#°čŌ‹šū}díú|ņŸ Šō…đ¨ĪĶJQĢG(1üę‡nāš+Īãņī}ƒk_%ČMá CĢ;c¸_l@Ÿ?˜ÆceŦõĄ•’Ą#Đ ÛHDEBs–„™PUBÚh ŠAŠ<‘$AŌ„žÅ+hŸ;˜Ŋ!Ĩ8(Į÷'‹=>Gz5#}jtĖŌ0rđ #ûļSoDh­ųĐģŽPųŪ/đ‰[9wéŦŽj5úߛqú ˆ|øēwūÂÔ]&q8§ŒÍĮņōĘÆ ÊūnͨËOõƒ'Ø s^ gį…)¨émV2:6a͊’'o~øË†ZJC“P]ƒR |ĘO”‚¨aŊĘWÜų^.ēæ:žˇú ~đāKrÁQ'h­čĒ1̧ŋ÷ŌÚ/>đ”ŠĐ“ōz˛ ¨:>ĘÉMöˇ2™ŽA7Ák€Wˇ7";ą/öĄYPÔú/ŌėzģfëĮ6ũaHõ\?’mO8éĶFĨÚAąķŽŋ™—#Ü KžKĐ ĮNüP6 < `ךš(§đãÉ96ĩ#{v3ŧoÍDčíjcnošĐW|×åú¯ūãåSš\č­Ž5žŠâ¯{ᇧҠãp8N9įpLkW6nũõēĐsĒsŦ V”tzÂÂ0Ĩ¤…fVå›V˛*eĄ Qž#劅°iS3&<ēėH´‡Öúu āĻ­Ą>m}]Üüņß`÷–M|áŸ~Âō%ŗšhÅ<‡j­Ā.Ã#u.8g.įŸģ„ë_¤w…ŽŲŨDéÔžQT¯aŖį”dãV%Ö0úæ ö’F’‡¨SuAÜqģĸ9GŅėS¨.E>T„eQ” ô Ãö ŧRļâ^čŅ9!ÚHĸ&aątˍ9Ĩ5IŗAÅ\rķ,ŧú-TÎ&aÖVC’?äÍ÷5^,4ÚģVø‚~…Âķė‰ÔÁ;>¸ šđœš‹9FĒM˜ļr^ũÖ/ŨČ9‹úäĪŋ´:Š˙_m…\øë‰Ãá˜Hœhv8&™ĩ+ˇŽõĨ†Đs* ­ę˛æ†y~J¨m“™P]öSÛ´d¸KxđNÃē+…|-k‰=ɂYDđŗ†Ķ…ԇaÉUpķ/~œī~îŋņßžø}>ũĮa^_;ũƒUyũ¤iJ>ôhīė`Ķ+ģ¨ ¤{~÷”ŋĨ@ÕÁŋĻj›Ņ¤yˆCû{ÜĨˆú >Ë#íĶŌĻ‘v ŠŽæVĄT…ōh¯AûˆĸXš@üķáåG ,ēč|:į.f˙æu„ÅJŠ×§dī[\ĢR¯Ž°ôŠëšö éđčZ+\đķŒäÕqΚt ûz ĖU¨É81Û:;i4Ųģq=f“|ĄÄyįĖ!đ<QŒįi*Õ&I’rû įŠWˇėãožū@ޘ ķ­ˇøž5ĢīÚ6 #s8€ÍĮ$ōüĘÆjŠú\5÷TôŖ‘ĸ'Ė }žMĮ˜‚Y[a\¨ĀÎĨÂO?˜˛m´ fūĻÄüe'ĸ§¯ŗžŌ7­p~Ë>ČāžŨ:ÉōÂ'Á>d'†Ķŋã5j˜žeŽŧđ,:{ÚîˇJŨƒRŠöļđˇífߖWhëčī?øÕF“ Ī™Owg ĪŗņûĢ|ų›đėËÛčé*bŒy xvFæp8&õē™Ę‡ã”xúŽæo4Dũy,tŸŦ`ŽÄÆpÍ Ræø‚§…ÄĖ -ØĘ_.TāÕK„ûßkØˇ@(ØÜ[3Miī"‚ŸËæ 3ãK6ž°hm{7nā…ŸŦæÕ§ÖPę'đ=ÚĘ%^t%Wž÷ô-YJŗbŖĘĻ‹f‹öÁ{˛žô4äÔŪWuȎņōEđėR˜ķXÄĨ÷ÆÖ‡*‚DxæŊ96į<ÚÖ†ŽÕ \ QڊdĨ ˆĄĢgíąˇŽĐbm$*…Įo€—wÎũ‹˜´]ąåc>…ŗ…ëūšIīCĩÛ6/) ›.÷YũÛyDCž:yŲĖžŸōĖ÷žĮšo|ŽB['Jû¯ WëĨR!œˇhN7g-ėaÁœ.jõ&/Ŋē›ĩ¯íÂ÷=rĄŋUDŪģfõ]ĪMüČĮDáDŗÃ1?%TM§ }ŠŽfŽ‘öˆßņŌÕļÛ_Ą2ÅūŽ5Â0†ã‘&öæy9ĘŊŗiŸ5›Ŧ?i QQ+Átĸ*ØĶ]ũŖ=kNeŦ`Ūˇx ø›nš;ĸwˇ!*)RŠÃ†īyų,ŸĨ˙#Ą´*s•Ë5—ÂâõXŖzve›­v5CŦG:.Ū ŋM3üŠĄŧ_ąā{)ûÂĪ۟įÚBƒsLxö]9v­Đ”&ī‚„Ō ØûęömZ‹æõšRęņ'îûöÆˇŪūĄĪĻŠū Ju‹Č< ĩÖUĨØ$Âæ5ĢīrŽ7N4;§Čē•ËęFž&ŧE)N¨ŅŸ`'û)ŊYŖ’ĸ†TŅ Š– BސœņxĄ}OüR™ũ+ „Ã)^–w73(”šŽžĶ§FšBZ?$ôeôĻ]0ƒŊrĐaë8w+č4ķ5ŸČs›`˙Q–]ķc:â":UÆ ĶÚ đTÉzwĖ0ɤÄŪļõÁî.lŋãŧŊ+˜‡Ë°úxŽĶpŨß7¸ę‡â)ĸĸŨO…!ÃæĢ=žž&dū?Ä´oƒ¤Ŧhæ`är…‘ĸķ m‚bsœÁŠ]V%pŲ:.ķXŽIģĸûC驔u7ųˆo'ĢN扞ÖPí¯ąû•gQÚGk?ZŗúŽqVN0;oL\ĨŲá8 ^ZÕX¨P<”čß0āīŦŗĨŒ@¨`VŌįBÅhŖ’™‚¤P4yũ!>Ûø*˙˙˜üP‰RÕCįg˜8AyzF‰ø7A#%Ø<æîŗÍGĖQž%ŧØŋ;—ĀĪ/Ú×&ŦúVDû PëЈ( ›¯ yüí!ķž’ĐžÉõ(üaĄ2W,S,ܤļ9Ę ŸL¤%hß—Ė…'ßéŅž%Ą°OēRŗ÷>…a+Ŧ'õʈ˛“÷mŲÁžk)´wbÄŧ<<‰[u8Ķ€ÍĮ đÂĘF{$ęļTôm + ĮŋLc°ļ _ ŗü”9ĄPR†XԌČ]FĢĻiP‰&YĀŖáĶ|õáģ™­ē(S"Ž›3Î<,€V:ë¤æ˜(übëÃŌV8ų-ĄÄŠéZ^¸^î5\đŊ˜ËīIB¨tg F ”ú…ŊhÖŦ čú—”î—Rĸ…JˆbøbÍÜP3gļíö)ėL áŧ—aÛMšĄKŪ °ã>Õ%аŸI?ą˛ĢOØúė#4Ē#”:{Aäš5ĢīZ;š[v8SÍĮ1x¯´ē2>ÕõÉ$s(K0§´o(ē=Ãė ĨÃŗj 93ĖĀŖBBbAš‚.i oÍÁ->gčHʄO’ÄÔû霺hzĮë˜DŲ.€mđüšĐ7~œÅĪaŞJaßlxôb¨ §Üō…&‹_1Ô: Îg‚Y 4(ė;_ķЇķ´ßc˜õ¨!괙Ú^Ē\æąä0Ļt*ļ6 oŽ{ž˙ŸuwņâŠĖ`'VGØūÂãļCĨR5ųŲäoŲápL5N4;GáŠw7/RšÆ)PŨļ‹đҰ‰ …Đæfû†nOđ”Íaž1ŅŦē,5AŒā/ņ)Ü,ņ ķũšQ‚¤)ĘpÖ¸a†ū ĨfVc“7 ÚXáüŌb˜ŋ.zŲzvUb'ûŊt<ģXč}:æÎģ#Š5šev(c‡”Fh~öŅ<<ķîM‰ģ”=ÛLm\āĐ=íŠų¯Øˆ99Ų¸°ÖˆĐЈĄąHQéR05ց\^}ø%úwl!×֎ ¯÷NÁÖĮãDŗÃqĪßŅŧßPŌ‚ĸŌū°yļ)PŗöxäŽĪ÷ ¨*Hvy€zŠô•ēŲW9ˆ— Õūũ˜8Ei…˜ķĘ“H+&š­ûÛáģˇÁų÷§ŧ˙īęxF1ԗŲ1ZMnST<øë9újÎųûĨI™ŅLE‚¤ÂČÕ>į6 pL+—ųx¨lbH`o…Ŗp8Sˆ͎3’gîhv Ü ę}‘¨ cQgˇžˇ%–S;ŅŋĪ3ô†vΊå-7#ČŧŖĻjÍÔ^Ÿ&<Ī'w‰îŌH 2LkÆĸE1´•Ëô–{ˆvžL^‡4F¨ö“/uE5fDÍY˛ŒfįΘ4$ŠA Q C 4Û/öYôbJž*4ĘŲ¤?šA|XķņģÅcéWc‚Ä !PÔæ)ôšË×ÛĮNČËŦ2+F q^8ž[Û [;ÉT–Æ@Ą ũ;°åš5øaŒi?\ŗúŽæÔÄápL%N4;Î(žŋŖqy‚zO$ęÎM„öÖcĮû΍D!]ž0'Hi×5ŗō–[rĢîõ,MîBŸā<¯×æ›vĀãĩ%U†9ŊÄiŠ |†÷īĨr`í}}Dõę ´r™"fØÄÄ7#--Û6ûÎŌŦūŨ<—}?âĸûcÚj¯!ũXŽ =g˙˙ ũb'ūY‰#|‹Ī| mHŖ æVe9Á6Z áÕĨđÂ2Ø6ۊírcr;ũ ­ ,ÁĢŪĪĀî6fö_Ú‘8ŽŠÄ‰fĮ›ž§îhÎņ•|,2ęęuE,jcŽũã}߯YōE›6Ė Rē<đŒšAyËÚÆĮQˇcõz5Áų>š le2FÂáÕå#Ŧ+ÜY=ķ)†¨õ3|` ‚‹f”N™I‘$onŒ‚\ÍFĘ=ņū;Īķšô' _Lņáé„ŧēĀcņß'”ö IįႹEŊS‘^ŦYą5[¯÷úePÖÆĄ"ûXš‡KaŨÛâ;ÕÄāgėŠĖ"PhƒÁ]^{ä>ÂBĨU‚ČÖŦžkĪԎÆápL%N4;Ū”ŧ´Ēą a¸DŖ>Õ0ę\P S(´?‘īYƒõ.į”0;zũ”œ˛ŒfŽ`KÁT h…?_œ,ķņēm›DŪ§ûX/^œ3ë,: mT›U<``Į6ĸš Ã&ŽĻŋĘĢČzQ;Ļ %ļuęÁö =,ÎŗäŲ”ō°aĶâ€ÅŸKčZo¨/Rˆæ1ûĶĢĀĐ՚۞bÖ3_ˇÂÆÚ> u–9°a!ėîļũr‘ĩ‹¨i¨.ŽđCØøøC ėÚFž\‘đwĶ3"‡Ã1U8ŅėxĶđėĘF¨ášF_RjU6œ¸ũĸ…Í[ļ.„9žaNRЙXž)ÅÍ$Ģ, č’&wa@¸ÂĮ[âĄō IA˛8Ž“&†ķgC{ĄĖHŖ‚¯<úˇo"nÖÂ<8š ŽfĮ4áĨ Ģ„Šõ×ųāAq“PģDC;v ^AĨØžQˆĸË=.Ūjŧ"‡]õH}č˛yWlëƒjÁ>Ũ3PhZq Ķ'˜ÅX[ÆČˆõÜÖšlT?ž›žQ9ŽŠÂ‰fĮ’W6´VxFÔėēQW{J>}A ķSČŖ´§N~nÁ0ģ<ÃÜ0Ĩ]ÛûĻŨ†Ņ ą ¨"ø‹<‚%>ūR¯WĄB0uúéoĢ\*˛¤g!ÛvŖ<ÍĀŪíŒėßMīâsNûĨ8Ūø(/ÎüÆ@cĄbÛ¯{á^Ci‹ß%ũ‚_pŋ0xž&œŖ(?ũs ™‡zŪÆÚh‡ƒíP)ĀHÁZ0r‰ŊĮö=]dœåÆŊ(ŌjđsÄōĒe[įą…ŦŊī^ön\‡†`ן]ŗúŽéū_ÂápL2N4;Ū0<ģ˛éĄ(0+§¸ĩnÔ[SxW$ô™1Ĩ'ÅÉÍUËēūbræ)ŊA3•ålP2&’Cy –‡w–G°ĀÃ[ Ņy…˛ æ‰Ûļ ā˛įsī+PÎįHęuúˇm¤÷ŦsljÅ @fFÍ˙ÍĮ˜4 Ĩŋīu"3ûÛĪÚk›v¨÷hįÚå[–‹pĐz“=ž{ Ôr—Áķlõ6Níąî „‰Ü§=›ƒŒ“BÚŽˆíR臠´ũ°ˆŅ$Í1ŸWąëđÂÄ (AĀž4ļëķ|PǁH xHšŗ3ūZĢ3Pꀑƒ1¯üė>$MĐ^ā)āū {˙ĮŒÅ‰fĮŒåĨU jEglÔr­šREĸW4DuřõāT*ĘpH,§ĸ”0+H™yeH˛TŒ)Ŗ%’Mö3ëķl×xŪ<ŋÔĮŸĨPm6ęËÔímĸ1ÚW\ļā4ĒCŧōŖŸR W,5ęTû÷‘ÆŊ‹WpūÛŪNŠŗ#PáÁ/˙wnÛĀŧsÃ'~\!OŌ°¯_{ CxuÍ}ėßŧž P@)m@>Ŋfõ]§b„r8o0œhvĖ(^YÕh6Ē3Uh×Üb`q*ŧ=BތS…ĶĐųd“ĪZ"šõ;žz}CˇohĶ‚VČ$6nUņĢ/Z~QŖUTYÎņņæiŧ9oއĘŲĮH° IŽ"|&ŒÎî;‹åŗ—°n÷JĘãĀÖM īŨCûŦŲÔG†Qę(ŽŠ'¯^.;LâQŗFë`SÚĮÛņBŸ49ŧj €†¨Y§ļ§cĀķėž­ThVG0’’/ļĶ9ŋ›rwi i^qŖÎ+=KåĀšõ&‰G ­!‰SęCũ¤IDÛŦy\ũODƒ<ôÕ`ßĻ—)´wrųʏ°ôĒ+‰V”ģā•Ÿ?ÉÃ_ûQŖĘüķ/ãíŋųŸđBEAą öoŨÁ}_üsly•4i2įė xīū ĨŽ.Ō‚l|â%ųÚ_ƒ‚BAŒ`ŒáåžO˙Ž-ÜúŠßĨŊ¯Ė†5OŗáŅ{‰5:g/$WĘÛĪöķVč„ūíƒŦŊīG€ ũ§ÕSą‹ĮôãDŗcZyúŽf>PôÅÂ" ´Ro3ÂåĀŧáTÍKŗŠ¯§īÁq22‡C&X}eÛ^wzB‡—RÔY_aâǎ-9dĩ8Ōī(t§ÆëPč>?ßÛåĄÚēdc⤑ų”eĖē'Ų!!”ģ \8wOm{ļ|ƒ{ļ1¸g3=‹æQžŪ ö’ģZMâI΀Vĩ7ßfE^`Ësrpû&$M0FȗژwŪ%,šâ*æŸˇĪ×4ĢXO°AÖ>ō(}õ3ä˨ŦL4$q"(Ĩ(õĖæŌw˜ķnēíx>T*ûxđī˙’Ą=[ EL’vp(Ĩđ‚€$ŠčY¸”Ģ?ø+Œčįåī!i4Hã͂ Ī%W*S‹íxv¯Ž[_à ĸj/°âÕíę7=ņ;7Sęę%Ē [qf ą•ᄁ][1´õĖ&#Ljđ‹!^Ã÷ļ<õ{o{/]ķĪe˙–WPÚŖsî"–\y-AęCv]ž† „Wų û6ŋB˜+X›’Č_‘…é8Ž7?N4;ϜV6–ÄĸæZŽNPgĨÂÅŠpc$ĘkMώc…VVäž(BV¸…ŅNcZC¨ ¨„6ĪPÔPŌ†PY_l"ŠĶ.܎ÉÆ6q ģZ=ēōT^Ŗ ļCŸ×ĢŅŊ¯GãuiČĒÉ 0M0ũGŦ{ EĒZiŽZt ßzîĮDĒŅ`ī†õ,žė­šiOŨ€Æ!53%Ędúh æ\ömÜĖcßü:[žyˆ¤i›Ōĩ|ßJ)vŽ{šĩ÷ßÃˇŪÉīy?…ļ2JvC ÍĘ0;ļŌ1{.&ķŅkßG{ Hã&û7­įū/ūŋ¤Q“Kīx7i …įųä˝´õÍĻuv*ƌ§5ętÎYˆöĩĄ*ÅļhīFLJ˙öרūâË,ŋî*‚D5h֛äŠmhß§cî|reEeĀvâ98ĖÖž@iMX(’Du’8ĻYkRîĩŸų¸ž2¸{ Pž2­5IÔ@€°TĸYĻ>4H}n߂ŸËáiŸŲË.°ö–,ÚŽØ ûˇėgŨĪîEIІ ˛øž›čpœ98Ņė˜žŋŖą(…Ģ@-KQīŒ KjF/l™Ļ՜nŦ>?[Ŗ:­pV…ōŗåhØJšČQļ5•čHqų’ ˜ÛŪĮŽĄũėzųYĒWQėę%‰ĸ雘ų˜E Go|~fPh‡}›vōÃŋú/ėßü2Ĩž>ōåŽQą €Ō4ĨYáŅo|‘ũ{¸åד ĐNÜP„Ĩ6JŨ}Úģ­Ø!‰š¤iJxÅų2ԆōäwžĘĸKŽdîōYėŨĄ}Mŗŗö•!âFÍZ4l5ĨQŖNPČä >âí‘´ĒĮ‚mAm@˛‹˛*T¨¨œFˇ+ŧNîÖxŨ ŨĻĐíUV(Y—Ufģ¨g‚ûČmΌp 0ĐÕŲÎÅķĪcËÁ]´įŠ íŨÅŪ ¯pÎ57 ĩÎDët X0bđĻiëĶ„yhVę<üÕ/p`ûktÎYZ#qŗn+Î"č $Čå)vuäōŦŊī;äJmÜüŋ…’MB5ŲäJc¯ĀDMk‡ō4ĒC(í‘/ˇQėčĸrp[ž~”yįŊ‡fĩIG “ĻäÛ;éY´Œ|šHbōyü|‘ŗ¯ēĪęØ4Á×[ŠBļŋøûˇŪÁâKÎahĪõĄ~JĮh]ÅW‡­܍ļˆARČ^ZVYD‡Ō.[1ŦÛ4ĒCáuktˇB­ŸDØ*ō‹†ÄYzÁz“O#z!ˇ-ģžŽ}‚¨:Âîõ/°øōkđ‚€¸ŲœŪæ€æĖŊ2.bm<ũŨŲöÂã´õôRˆšÕ FR:úæĄ<ÍČž=ԇ‡(´ĩ ģzY{ßwY|éÕ,ģîrLB֖Ü:âFž%+8÷ÆwRlīf`×v^]ŗšĄŊ;ņƒ°PdΆĩ4†ßƒŸ H“ˆ¨6Â%w|”ĢŪ+Ę˓D‚Rb…Ļh?$#ĒũûHĸˆ|ģ‡CXlcxßN6<öį\ĩ„¸9Dåā~´ОGÛŦy€ ;°u۞_ƒRäl“ĪHĨõ¨[†÷ī¤YĢâ!ŗâ…mD1DĩAâz…¨QŖgŅ2ēæ÷˛áąŸ ĘÚQzŸMƒZųvˆjuž˙ŅwŪŋ“Rwc€˙ļfõ]ōčp8Ū 8Ņė˜0ÖŨŲčkõÉȨD¨‹RĄĐJŸ:žŽj \[ȕ,'#hU‘•PTBŅŗâØŪođ‡%j´Z_•ąŪã17Iė†%ļĢ<¨œB•ŗjq—FwhtWöwQĄJ UPā‰ļÍü&­ |ãn{ åÃĐĸ¸ôŦœÕŗ€-wĐŽĨ4ũ;wņô=ß`˙ÆWȡu‘+ļQmāşÜÍĸK.#_Ę.hå7›Ė:ë<.}įm(üđrēæÍáū/ūwŌ$AiMŗV#MĀķ}P c i’Đ=˙,æ/ŸMŊIPŒĻb$ˆghTG0IŒö|$Iđ‚ĪØüäĪ9piœŌ¨ ãų>Ú)uöáe'Ą[ž~Ą=Ûȗ;č]ŧŒ¨VĄ>2H5ˆęU´‚41 ėŪŽ¤)ĸ…ŗßr9‹Vhúᕟ˙œū›A+æ.ŋ„ ,ŲŽ8ĸsŪBúÎ:æˆß đŌOįĩĮî'W*Ŗ}1æ¸*ŗÃqFâDŗã´yúŽĻō”üadÔ/7D/ļĶGĒ–¸<ÔŗcŒYŲ {-„ÚdBržā#Ö÷ŦŦ­ÃNôS´GNcEr Yīą‚Q#ĩō°B¸ŦŅڔMŗčĐčÎL k dëõ84鯕}Ûē¯ĩŨ7Š8>)ôövqũYW˛v÷kt:Úĩ•Ŋ¯­Í&ui$5ĶĨaL: |&ŠgȗaįË[Øš… ĖŲû͘ ĐÆ Ÿü=.¸å:ĸ†ũŧĖYą„Ūŗ–ō“Ī~šũ[֓/• ‹%öo}›7ŗøōĨäËm„…2iŖ”˜”¨n@iL Ũ –’+–¨ douöoj/ŸøžŸ/°õŲ‡øņĐnÃU@HĶ„ÚĐįŋm /ZAÜL‰5´įƒģ= ] ėÚĀϧ6â‡y´īaLB[÷lÂb ­ačĀ [_xŒ¤Ų 8 ‹/Ŋ†í/>‰!nÔiV‡QLlØˇqhŸË1´g}g5›žŪĀæ§!Žë”:zščöˇspÛ6†ö믈ĄkŪYt͟Mŗ…6Øq€gŋ÷mLÜ Ø11fđ×kVßuä)ąÃá8pĸŲqZŦ]Uk*úOĒŠžÁ@áhUåVė[ŠBc'ę…ZŦHVBArŲ߁uH}ļ*¸’­cT)’Æ §Võ8ÎĒĮJP[%Vyë1Ö]ā÷yč*ĒŦ˛l+Ė-ô’ĖNŅÄŅ{Ek c_đ›A(Á¤ī¸āzžõ¨Ĩ ~ŗõ™ĮXrÅuš<ÍZmZ^ļÂVš1Ö>sif‚œĩ TŠˆ’8eų5×pîõ×7 ™ĨcD ˜ģ|.—¯ú÷}é3¤i‚ö|ĸj…]ëŸgéÕK ‚~&‰õ"›”4‘Ņæ%Q}„8˛Ūeúa’DŗoĶ&­ķĖ]q1žŋ ž\ĮķĢŋ‰Ÿ+ ũ1æIāëkVßår™Ž3'š'ÍKĢKõ™ŠŅī‚VŽr,j4î-§ųK™9´säЈißÉ'ˇ›€‰m™;)¯`ĢĮŪ,kŠđēõ¨įXíÄ<’L_´ŦŒÅgZ™ōDQ@ ŨšŪžâ:Ūô$* Оũŧ˜wŪåx~@eŊ†§“åŸaöŒJ+ÔÜĘFōíí‡,D­h‘ĖWœ+†ô-YÁūͯ &Ekf­BÔíeŸLĨÉĘlņ žķg[ÅĐŪ=Tö„yD Ji掸„ iœĸ”ĘâqŗA5QZ#ÆÕj;:)vt7›YĒ˜$eÁ—RęęfûÚ§mÅØ÷ܡ‹ Ča’„rī,ōeØōü;_~’0_ĸPnįåûÄĢŨËОäJe›E„EÜš™úČ0ųļC6Z.â{!įŨ˛’k>ôqæ,›Įž ėŲđ J):į˧kŪ”C{xú;_#‰ę”ģg!vōßį€×Ļz;Ž™ƒ͎æå• eo­ĻúË5aXŅŠ°•äĪPÔBŅ3ôĄL䖨ąIéņÄ՘Y}ŖmĻ3 ÕĄgy6ŊĸSãõ(ŧU␗xŒyZblō(Ē&c@kÅ[–]˞'—đęÍ´‡9ļ?˙+Ž] –0m%w%Ŗžæ3e/FĢÃŲ{7ŒÛ­'{ÜoųŸåyDĩ:q ´Öö§4žP:ČĐž(/Â<(E­˙ /Ŋ†Å—^f›Ŗh+˜Ķ$Ay1ķĪģŒÎ9 ˆŖ&šb ?—ŖØŲM[ĪŌšÕ!Õa´ ĸ(vztĪģˆgŋˇ€ÁŊ[)´wášÔ¤øštsLY"ČYŗįqëōkyq÷z(Ųŋ›­Ī?FĪĸĨ(ĪGŌŠŋj­˛l`Œą•Í3HŌ¤Ú{ȡwbâíy(Ĩ8¸m+éUWāy‡'ō)ĪžֆúíÉ'Š4‰Čˇ•)´ƒˆBû!&ŽĪķQųbfŊP¤ILŖVĄmÖ<ŽųĐ¯+ųDuCŗ2‚1 I” ũ<×ūâ/pö[.ĸ2`#₂,[°'`qŖNܨŖ´"”éY2›ĨWŨČĶ÷|4Žđƒ“ÄÚ;iī›Įāî*Û_xæ0Flc’Ô ´Ũį Čår ī‡ū[ ōVäŋå}ŋĀ9×,eä€ĩŠ4k6ŲÆķ`īÆW‘4E{ŗĪ>Ÿr/ŧđÃyųŪo“/ulĀNūĢMĮ~v83'š'ÄÚ;w õÕĒĄ#§ĄĪOéÉ*ËÖģ(¤(šrœŠcĢ"ÖĘ.nũžMR9đúüŲ ŨĢđē=tŸF…Ųs=Ŧ9SáõņƒÎinģø:î~~5ûĒũ=ŸM?Ā9WßL[ß<ĸÚ4X=•H3Æāé3¨3 ˛“ûēæ-ĸŊoļn ôKhOŗõŲ'¸čļ;(tä¨@ĢKeŽũ;Øŋy=J{YŽąG)ŗMäÛ:Č•ÛŠėßE ŠÖ㜁“u^\|ņÕ\ųū_bÁįҍ‚į§4ĒC˜$É,JEÄQ¨5SŌũPŠņs!sģˆ›uĸz ßķņ‚AŽ„īÒ+¯âĩG~Jm¸?ȑ& …Ŧ5÷ļį׹oã˄ų"AžČ’˯Ŗ­§‡8NØūÜT€Røų<ĩĄ˜Ą=;PÚŖĐŪIĄŊá}P´íĩČ YOØņâ3TöŅ>{ /¸”ũ›ōČ?‘4Ž(tô bbāķkVßõØôíl‡Ã1SpĸŲq\Öž§yÃHĸŋR3t„JXĻôz2š‰,ŖŅqĮAlä[K0+OĄËX›Å,ŋĐÃīĶč˛Fĩž”'Ņá“ķģ9'˜§†,9䜹‹¸eųĩüĪ'ŋKGš›ƒ;6ŗáŅšâŊCkcĻ^8 X/lāŸ1Ĩl2Fįœvú–Ŧ`ī†u¨’B{>ģ×ŋČs?ú6×ôÔ:}Ō„Ŧë_ÂK?Ŋ‡á=;đœm*’/1ëė‹ˆŖÖšÅvlÔ阺€s¯ŋ•öšķ@ŎNú/',)ęC6ĨB)F+üšR íÖüËw)ūč!*ũûiÖFPĸÚˇƒE—^ÍĘ˙G4Ģ[ÁUkë ‹4Ģ0Å ^|ëü"“D::É ¯<ôâfc ŋãŊÜükŸ W„Fîú˙mdiVFhÖĒ ėØČāŽí(­č]|3…ö6âæaÖo´#ģШ –ģXrÅõtÎ)ņŖĪ|Žū¯ŅÖ3c'X~øü´ėd‡Ã1ãpĸŲqLÖßŲX2bԗG Ŋ‚ÅAJ'Är‚ļ‹ ‰ąŅ`žBĩC°ĀGĪņđf)ŧ>ëGV>ÖnŅĖĒČ)¯Ī;v"yzPöä%_Ęąę’[¸÷•‡Žëär6=ëe9mXú|+˜O˛ W÷:ô&o‘>,ō´7ĮŨĄräéĀ ˆ(KDQꔐŦ1ŠâP†cF *…‹Ī>—›–]Åŋ<ûCĘĨnwmeãpÅ{?F5˜Žr¯ISŌ4Å |ë$8Đ Xxá ÎŋåŨN4;ŽJEøĩÁTŋ§Ũ7,S:<ąvŒ{z <đŠį‚…úĨÜMšŨēMˆH•ŧ Ķ“5úëDŅt (ŊĀ|`0 čfg?͈i@Øpįˇqī+2Õ(„^yčĮœ}í-´õÎ&ĒT§ļC "ģœŸÚ–Îg Ę G“ÂUüwlaãc÷Qčė&W*äķ īŨA˙ö øaŽbG7ĘhTG¨ pîMwpî 7ÕŦuA€4މ›u$ĸFÍN˛$͜GœÄ* Š”ĘĀ#C;{­`ÎĐÚC{Jit0kY .ŧŠfÃcŪų—ŠöYsX|éÕö5Äöuy¸ęeōo#Čåiī[HEŦúã?Ã÷mE;ÉZrŖ@čYŧ€ŲË =kY1 „Ũ=ÉūNĮģ˛ëP 6>ž‹Ÿ~ū/IãÅCUæŋ[ŗúŽœô}čp8ŪPœAß0Ž“á…•Í‹ĢŠūÃYA. lKëŦ0o~<<ÕųŌÚņĘŽîn9ÚJĖj| + ;°ây °˜ ,–Å~QŽĶŖuųž—-=ŸÛÎŊŽģžûĨRÃ{w˛îūīsí/˙&ĘķϏ؉Xš$øf‰S¸ųiD)k™Čˇ•šõßüažČkŪKcx°X"ß֎ÖŨˆIiV+DõJ+Îģy7ũÚŋÁsD5‹/įņƒÍj?—'Čđƒđ¨īĨI ‘e×ŪJGīlÚį.ÄķäKE‚|HP)”C”öđÃ~ØIŗâqîuˇpŪ7ãI!Ē“Ų<Ŧm"W*1¯{bėdG?âųYde é"¸YÍ1Z]<ÄŒVŏĖ`…tšęÃUüĘįØĩ™RWOK0? üɄî(‡ÃņĻ@əōíâ8avŋˇZÜûŸ.(ųŨš 'Ražøđ°ļķîR}2ÆfVŖnlåy°x ppVP;&ē^Ú°ßūį˙ĖP4BA|”VÜūûÆėsÎĨ64dŊŽS…؈ˆ\Š„įŸ9Q ä; ĒÖX{˙ƒŧōЏØŊ$j`â­=‚\žÎųK8÷†Û9ɗK4†e'ÆIÚdĶSOS:HąĢ‡öž9ô.ZbÛfåũą^䖨ĩod6UŌž&ļšJƒØíĨc› Ŋ01æ˙šÖ‹ąąr§k[7)äÛÁķ…{˙æķŧxīwȡĩãųˆėn_ŗúŽįOo+‡ã͈͎ױëŊÕŅ÷´{ĻÃWV0…ā[Ā€§:ī.mŸĒ1ŽÅŦό­DwW—×r¨'šcĸl`^øËī~‰/¯ų]ÅĸáA]r o˙ˇ˙'I”ÄMÔxežI–„!aŪ^|8˙k VhV÷r`Ë>vn'ĒÛĘq÷ü…Ė:{ÅÎŲHjEėX”˛)Úŗ^a“dUāc‘%QhĪVzĨ•Ļ“ũÛGxĄŗsœ)GŒ}reXķõģxėŽ/ärų"b€¯Y}ס§~d‡ã€͎Ã8đžĒū$PüįV'ŪqØ ü3ļKֆÎģK#S6ĀĀŦĻtaĢĐWˇW=Ķ9Ž7tėŨw_ũ§˙ĀæƒÛi K$õ×üßrūÛŪMu`hJ‡$"hĪ#W(ĸ=īŒÍ"ādšæļ2k´5ą¤GVWąÕŲ)fKėžđ{ø.c›­;á™{îåĄ¯~IcÂR[ë…ūĮ5Ģīú§y˜‡cã<͎Ãđ+|ÆĖ[ŋžė™, Æéĸo§ Tf5_à æĢ€ˇgOßßh˜=ˇ‡^ķūėŸG”āĪ˙ø›Ė?ī2ÚúfS˛í™§ĨlwĀ$I}ΊÄ3L8+•uŊKėīÚct–Ÿ1V4ÛĮžIĮŋ˙ø>ÅįM­“‰|;Ŧ{ā1Öüķß’Æ m­6Ų_>7ÍÃt83WivÆāû̟ūáˆģ€Īf÷īîŧģô†lXmVŖ€<ļ }đ~ālEÚܝ,:qšđģ_ûŋyxĶStÚhV‡Y~ŨíÜđ‰? ĒWIcÛ-nJ†$‚ö|rĨ"ZYmĩGaŒ`~uÍŗÜ˙…OSé'ßŪŲŌú?>šfõ]ûĻuœ‡cÆã*͎Qß_íƒ˙¯ŊûŽãŧü˙yffûĸ,ö^Å^ĉǔ bŲĸQvėÄ>į"%ö9‰“ÜÉÉ]~é9õrņŖ;>Ÿ[(Y´lĢ@ĸ%‘”XÄ"Q¤ØÄ^ĸnߝy~ĖB‚ Ā.ĘbīÛ¯5`wæ™Ų)ßyžīķ<ÜŨéWQā1āaāXåĄ"Ė“ÜŒēŽąHgzžB¸é7ˇãv0ĖMŗ"Ž(7§ŌâË7~Žũį’ȤđųŪļ™ŅĶgŗā–;hojgđĸWĮÎbg˛>īˆ™!P\†vķŗũa8ķÎa^ũî˙"Ö|‘põ¨Žæ=ĀW$`Bô†Ô4‹´Ü[ŧxqGÁøĶĘ'B[ŠZ¨AäÔ3ˇ#á}Ā ¸)ūb–ІGŸũū~펉+qŌIB•5ÜüĨ˙Áč3iŋԂ1ˆĩÍĻeá ?˜âYŒLʄ@œ9p„įūåoh9÷>ĄHMGĀü6nĮŋw‹\L!D‰0˙ėĪūŦØeC@Ë=1X\üđåĘ'BīŗLƒMͤMÍ䐚Éú(ŗ¸ĩŌaÜô _Q 8DiĮ­–_0á*Ū9s„÷.§,&Ö|‰XSŦÄ “I&/MÃqPωiĘā)#•i‚/įåų˙ķˇ4ž:L¸ztĮŸ€ßØZŋqG‹(„(1RĶ,hš'nš*ŸŊRėō %N=Ķp&îæãŽ*f™†ĨA…áș“|ņ{Ņ–Š0ŧÄÛ[X¸n=kũAŌiM:”ņ›ĩv0 _(„!ĩÍ#ŠÖîĐyž Ûą—ũÚNŽ››tGŸ>ˇĩ~ãĢÅ.Ģĸ´HĐ,Dœzfwĩ¸ãAOƊZ¨!B)7p~rÛ g˙ėŠcSeĶØ™,Ģî€%wŪM2š$“HĄŒ­qÖšƒŊū ŋO‚æ‘Bƒ™ ˜oÛÍ ü ņæFÂ5c:މ“Āon­ßø\ą‹*„(=4 Q §žyĀjāNÜ8ŽÂMå‰ŅŧkxxĶv˛ĪŨûČWî~÷üŅß]^Cŧ­ Ĩ,nüĪŋΜn"z)N6“đT ­ eā †Q–)‡9Į@ ŧķÂËŧúũojo!T=ʝ[ë¸ŖdŧTė˛ !J“Œž!DŒ:€īæj kqĶ7ÖāÖ@O<Å+á€J§€ãĀĀ[¸™F ëßŦmZë9Z;7úÃåÄ[›yíßÂōy™ąúZb͊l*9 ŠJ8Ú!“Náŗ‚h…ÎÃQnöÁ`…›[ŋëɟŗí'߯ÉĻVÂąm€÷qs˜%`BLjš…čgN=“pkžgáŽÆ1 ˜ Ė,fšúÁQā=āđfîŋu4t÷æĢŽŊuÖ¨pd“†ų ˆ57(¯äē/ū3×Ŧ%Ņ’$“L xŽŗÖ_0„åņHšÆ0Ŗ7-(\Šdœ­?ø1{žyĨūp™ûŒ¤õ[ĀīJŗĸ¯$hb€9õŒÁ ĸ'SĀ`ƒĪÆ ĄŨšĀˇ_ŪZŋņí"W1 HĐ,Ä sę •@î(5ĀhÜ ēwÆÂęÜŋ˙ĸ˙:j  hΗpƒäK¸)Įpæv Á¨ŖŠ/+Ģ­ÛpđmĨÔ€D{+–åaņ÷ŗäÎ_ÅôÄ[ZÜR Pžŗv,¯_ €VJŌ4J™vįĘņ‡ XGwîįÕī‡ķīŊ/Âã tĖ›€?ÜZŋņX‘K,„&$hbˆČMķíÝPÅ×éŋƒ@7hî3z,n@Î}ÜÆ ÄĮãN{~!÷{•{8¤pƒáv †;ëcˆIŖîƒåõ›Úē øā_•R5ÉX;N6Ëėknaųú_ŖrüXĸ—ĸØŲĖ€Õ:kíāķ°ü~ܘJ”íž•î¤:{žy–m˙ņ=â-nęiZ—üā/ˇÖoŧpÅå !D$hĸD9õx€ŽđOãv:ôøHđĢĮ¨ŖhĄb.pžø0 Ca§Rd "&ŗøÎû™ĩú&´‚d{ԝœ¤ßk5 đC˜KįRŖÁ°Ü­cl˙ņw8đō3hĨ „Ęrŗ?ęđ߀īo­ß-v‘…ËÍBˆAS[ˇáāÛĀ\ĨŽíIÅQ†ÁŒ׹¨î>jĻM%ۚĀNĀčZŖL_0Œa(Éo.Zƒ7žÛu€­?üįŧƒ?ÆōųsßŖ> |Øŧĩ~Ŗ]Ü !†# š…ƒĒļnÃTā€O& ėtšL*A¸z, oš›ĢŽ­Ã HEch'‹[YŪ?´Ö˜Ļ_(„RHā<”iP–;v˛=Îî'Ęžú'Č$cøBåωvobO㎐q´ØEB _4 !]mŨˇVđOp;?MĨH'㌟ŗˆÅw|šI —€ŖHÅ8ŽŨo)Úq°ü~|ū@nX˛~YŦčOĘ­]6L›oāÍĮŋĮŲw÷â …rĩËÜ­üķÖúíÅ-°b¸“ YQ4ĩuV\„@á86ÉöVŧ ĶW\Į‚uwS=i2،ÆÎĻq˛ļ[=ÜĮZk×īĮëw›÷åR8hP&˜X^M[C {Ÿy’/ūœTŦ•@yUGîrx øŖ­õ_,vą…#ƒÍBˆĸĢ­Ûđ[ĀâNc(à ›J’ho%TYÍU×ŪÆU×ÕQ9vļíÎ&čØŲžĪ:8x|>Šq."­ŨË^?$cQoŨÆî_ lnF) ĮŠļb;6áĘQŒģjS–ŦfÜÜ„kÆ`Œ&ÉĻRn3§Z)…/ĻåĖ Ž|‡1Ŗk¸a^9Ŗ‰id°h'L•4P…ƝrQޚ=ëčÜgä‚eÂXs;§ß~‡#Û^ãÄūě.āņúĀĸ5–"ā5Y?åŖĮ3yŅ*&.\Jõ¤YxƒАNĻČ$(ĨTTpáČAžųÆ_Ņ|ņ<˙勎ķw/#LJŲ @Ąą1ÉâáŖŲŖįÂ‡§ŋgøFLøC`z!›†K'OræũŲž…†÷ID1,“”cOi|“yŗÆ˛ūÖ%Ŧ]3‡īũä5vŋ÷mîą¯{›„$hB” Úē 5Ā5¸5Īwƒ *Žv°Ķ)ˇfM¨˛†QSg1aÁ &Î[B¸z,ž…ã@{CO˙ķ_đîöWų_ģ¯~aŠDĘvkĀ šiN‡—ōSÉ´ƒ)3đŅN“–n*yô’Í…#û8ž{§öīĻåė ´v°ŧ^˛X4ĩÅ |,ž;‘[¯™ËÚŗ]UF8â—õģųŖŋûÁ€oŸiĒU[ë7&‹ˇ…BᒠYQ˛jë6D€™ĀgõĀdĨNãd38Žƒéņ¨¨dėŒųŒŸˇ„qŗįŗ˙ų'yũ§?äęĢįņđģ— Ŗ+¸Đؖˁî˛"~#Ë{į3ŧŦ—Q=~"ĘÁŖl¸#õa˜āõáq˙;…†÷qúĀ^Ūßõ&-gONŅa˜$mHĨmʂ>–/šBŨÚyŦ\4PĀ‹Ŗbņ4åa?g/ļō_ŋūSNŸoNũŪëˇÖo|ŗØ›,„Vą „…ÚZŋąØQ[ˇáā_€:­õgZĶãÃôxŅZã86‰ÖŽŧų2ĮvlĄrü$Z)¯Œ°áŽL_Åų†V Ŗûęc…ÂôøÎ÷ãBø0÷íIFAgqwG ĮąĪ €/vb-I.:Î؃{8˙ŪŽ!Ūv Ķō`X,oe(e0&čeÅĸ)Ŧ[3‡ŗÆã÷{‰'ŌDãIGŖ5´E“LaūĖq>~Ņđyk š…E'Aŗĸäm­ßŽÖÖmxøp5č{õJŠqĻiiaZ´ãĐtöņ´Í˛ĶŠŊz:ņD Ûvē šĩ† ß˙sŧ{ô<“Ž]‰åu‡Oöŗrk LíQļ3Đrá—NââŅÜyw-įN’IĨŅvÃãÁ*ÃPhM*Ą,āĄęXŗdŠl–tÚĻĩ=ãčjö•Ûv°LÅUĶÆ x•íč•ÅŨ BᒠY1ll­ßh-Ā‹ĩuļ\ |XĨ ŖZ†i ”åd¸nå,Fהqö|Ëek™MCá÷{xúå}Ä­KoēƒxĮÁ˛ §ôĶ4>HGQnœkyŨ—c;$Ûbm ZΝââŅw¸pô-įNŌzî v6…7Æ0-<~ĮØØČMhũāeūcs[lf&›ÅëņĐĐÅø`“ŽeQÄâiæĪO¤"H[4Y[[ˇĄlkũÆöAÛ!BŅ š…ÃŌÖúYā4đā'ĩu&w˙5“ą§NWŚ%ĶHÄĶ—]†Öû8đŪYžzųÆMž…ayˆ5Åđ-0L,åÍ3 všQđƒŸĄxãÔŠ\…¸Ę¨š•ƝĩĀІRYRí—N]ĸõÂ)ÚΟĨņäQ.>BŦĨ‰L2‰“McX<Á ^#ÜąąˆĀŗĀ/ļÖol¸æļ ŸÍdí{œVĩKgâ1 l§ûĸ¤3ÆFSSNs[ŦĖÉĀ;š›„ĸ'4 !F„­õO]sۆīŲļūÛv¸íú˘RCcsŦû´ GzIg˛<ō¯Ōؚ`”įĪūĶĮ_VAųč TŽ›Ldü$*ĮNĀcúŧxũŧ†iĸsĩĐ˙vĐÚéXɇuwĩÕŧ> zģč„?øo@äN?+ėl–l"C2™!“ːޯh>’Ös§h?†iá Q*ÜQh­Ŗ@nÎņ&`ËÖúį>ļš_„ž{ˇī9nŨ[w55‘íąäe§/˙úŖWx}÷Q*ʂd’ ŌÉK4ŸyŸs‡öáņú1,Ãã%T9Оš1„ĢFĒŦÁ_QI ŦY%ō2e>|!Ã˛@Y(ʰĐ(F7y ų'WŽnļHk4näEk­ŗØ›d[–T,M2%ŅŪLĸ­…xsņæFڛ/mŧ@ĸŊ•l*I6“&›Na /ˆŋŦôM§Q–ZŅē8 ėļĪn­ßØŌÃ~˙™iĮOŋ4kûŪãÜwÛÕ´E— š5  E(čAkm‘–/„N‚f!ĈP[ˇ!É:÷ųŧ~õS+‰T9ߨöąZf­5Ę0¨Ŧōäsoņƒ'ß ôa™Z+üár(ĢpGåČfŅŽÉĐ|î$M§âØîΆe⠆ņBø!ŧÁ2L¯o L <‚åņŦˆ`˜ĻûߑQšĀYįaWUãņ+´†L2KŧĨ íØîh(´ÖÄZ°Ķi´ãĐŪԈ“Mom&“Œį^ 2É8ŠxŒt<Šv˛–ôܗeb˜&>oŠˆ¤k7PÖډįqĶ.¯īûˇÖoŒövßo­ß[së}˙œI;ßxvËÛÜZ;‡ōp€Öhķ2šäưīi)„(%4 !FŠ’ŠĖUKįMdÅĸŠ4ĩÆē­éÔĀčĒ0ģŪ>Éŋūh ŽÖøŊÖˇ­ĪWi­¯Bë Ō0Ír,Ģ˅Ô͉ЎƒÍŽGIļ5cg2ØŲ ``xL e`&(ÃÚ˛>H1ū`1ĻĮ-ŖRhĮFÛŲĻ§ÖšÔ ;“ukš5šåkėŒíNĘâq‡}3,wäpõ(”a|,ÉÚũQ§pœFíĻ[œīoáÉąžė|ĨÔ˙ ø=ŋĩ÷āéO<ˇ‡?ŗ–D:C6cwķ=h ÃČm;ĀGwBƒÍBˆA)îs´cŦ]1‹PĀGSKôcÁšã8D*B´ļ%øæ^á\C Օe­wkũÆ4@mŨ†™Ā$`,0 ­gj¨ÆŖA×  ˜/–ĮËGzãåÆFkÛÎũJģņGKŒJēŠĘUÂ0 Üy u.Zaú|šĖ…Ī0AŠ\0ŪQk ¸ŸpkĮÁ Œ/g€Vā8px7`>žƯßl­ßØvÍm÷˙+c?ņ“§vW/˜ĖÕ &qö|‹ģĩų.ÜZôĶš;˛Vúŗ,BQ š…Ã^mŨ†JĮÖ×W–ÍÅs&bÛN7īԘĻI0āeãĶ;y띓T”QŠí˜ļÖo<é´lTāŅcŅšGĄõD Ŗ@WĸŠĘŊΝ” ĸ”Ī4͎Pđ]ĻôÁäå‡ßøā/Z§€ŦÖNM78n†īn.ōE 7íâĐēĩ~cę˛ īG¯?ûOŽšuÃ7.6ļ˙Î7ô ķ‡w3ĻϜķ nšŒRÉ:ŒUÆ;Gβ˙Đ<–•F&7B 4 !†=ĨÔ5ŅxrԊ…SS]F:“ũX-ŗmkjĒŧķŪ9žx~†Rx,ķ"ĩĩ~ŖÆ J[€ƒ˙V[ˇÁ‡(‡;ũëËŊ*ĐzÕ0>÷ˇŽhžŖķ›Ņ)$6€nÍpĮĪ8‹;ܛ[KœÍŊ/•{oĐžĩ~c[oöÕ@SŠ˙Yö¯ŪžįøĘo|˙ezđ6&Œ‰ĐĐԎŖƏ.Įv~đä4ˇÆ)+ ŧēĩ~ãûÅ.ˇBHĐ,„ –ĮSiߌ)Ŗ¨Ž ŅܖøČĩÖx=†2xaÛAŽh ,ėG)žÔš‹…Ž4Wƒ{1÷ēĸÚē ×c[ŊäŖšŧ HÉN?wŒG]2ļÖoŧpíí÷˙ˇö>öŗöŽĘd~ķĶ×2mb5 hn‰ķČÆWyņC^­ģĖB4 !FÛŅŗŧ–éUvĮOî2v›ÖPVæįRKŒí{ŽŖ”Âë1ҚŸ™Á(c7ÁZë-Aŋ÷@}û—/íķ=u‘VÍÆīķ˛uįQvŋ{¯×Äã1˙Ik^/vy…$hB s×ÔmđÚļ=)đǞ ŸLÆūØ{LCá1LŸ¸Č™‹ÍxŊ&J‡´Ö{séĸŊūėF][ˇáĮŸĨ<Ļņ­c§GN4@n¸?ŋ΃eßךŋÚZŋąģt!„t2ŒbxS”kÍËtƒą͆]Zk<‹t&Ë{Ž% øŧh­Ÿ.ĨĖ#ĀÖúY­ųe÷üŪŨ~¯ĮPÆī9īąŒ?CķÕ­õ›‹]N!„č 5ÍBˆaMk”eFk4AK[ĮrĮ:îÔĐīŗ8yމ]īœÄqžL´Ö›û{Ø5ņQšZägkë6ėą,cnįĮ]ZsjkũÆAI‹BˆŪ’ Y1ÜiĶ4´ãhģ@&“ÅīõÎfŅZc Ë29üūEŽē„eā)÷N‘Ë=bl­ßxwŽŖ_>ŋĩ~ã‰ĸV!Đ#5ÍBˆ‘âMZŗôüpĐ* ųĐZŸsũ<đ×0 !„čŽÔ4 !F”Úē ́ÕĀ4ÜŠ¨SĀ>ā9™ČD!ÄåHĐ,„B!DdČ9!„B!z AŗB!„= Y!„BˆHĐ,„B!D$hB!„ĸ4 !„BŅ š…B!„čÍB!„Bô@‚f!„B!z AŗB!„= Y!„BˆHĐ,„B!D$hB!„ĸ4 !„BŅ š…B!„čÍB!„Bô@‚f!„B!z AŗB!„=°Š]€áŦļnƒ”BĄ øŧ‚Üį´ŪZŋąE ˆkęî7€‚ļ ÜíxŊū?œ|?[[ˇÁPšĨs‡(ĀÆ!H?~ũtũ7‹VœU7ß­|ĄrÃë ŒM´ĩĖokŧPå †F{ƒÁZHš”2 ;“NĮۚĪË#íŪ`ø˜ĄÔ;“Ížöėûuĩu”Ĩ•ęų̓@kĮãÅ’JÄôĢŋüaŋīãÚÛî/ ­ŅÚÁ.įĨ'žÛëķšãÚHßŠÖC)^6˙kČåŦûô—TĒ­Yeí,J°ûĩF)…ŋŦRŋđØ#ƒz=Xŗî^CY ŋ*÷@k;ËöÍOôÛūîĩw~Ö°ŗ =Nú†`y9čûv˙Ÿ÷u”B)Ũ‡ÍUô‹† šÖ'}øīˇ°T!WĘÆļĶ$<ÖĪe+ȚēûĒß1-oĻÔԎŨžęĻģ˙õļ̐t㓖Īŋ‰V…4Â*4ZkĨ^žŲOeęQímÖX>˙īYž€ÖÚé÷#F)@;žd,ļgÕÍë˙ú6ŲũŊŽî\˙Š/L7LëŋX^īd”Ę ‰sAŖ-ŸŋŨļ3ËŽŊc›ŋŦ|åõÅĸ͍‰][žęs •RŋëņW*SŠBļW)”“ĩSŲtr+đH_Ë#>N‚æ¤aŧ՗>ŗH-  ų<<*>ŧäm6:ßjbˆÍ1ÚËÖ°î×Õx œ"˙k§‰ $øūÎáíúų˙ ´ôūŗVųd5ķŪĪĢß]čĮG‚4*¯ŊÚŋ4Õ­´ŅĖNqFŋ?ĮŠÃÂ˙oeŨúį@Ŋ÷fũЁX÷ōë>1Úôx~Ŋzâ´¯ÎXyÍø‰ –Ŧ¨Á´ûõ Ü2P† vŌqĒŌņDŨš÷öÔŨąíˇĪxëߗ¯Ŋã›;_}úH_WãņVŽŪđÅOO[y Šhĸ?J^8垛ŠXŒxk#mįi>s<ÛrūėŲ‹īŊûDíŸųi˛­ußîמn+tédŦfÁÚēßXyīgÉϞ ‰`á2LË$le÷Ο8|ūŊũ=ÍW¯šU*"ķVŨû…{g]s=‰öDŪ¤˛gîį•īūKpÉĒ›sĪ/D ܄9ēvõ§üˤ… Č$ķ,×âŌé3lûá7+¤ yõē{üáęąŋ^û™/n˜0w ÉhĸßĪxËcĸ‰ņú~|õģ¯üâāõū]C÷´?eiígVŪûŲą†å!›˛ōjÖ#ØY›xkŅĻ‹4Ÿ9NÛÅs­~Ũōú~´âúOnņø}§ˇÖ?Vđë/ÜžîˇūøÖŠ12yŪ9”_qrĪÛö››žWŽÍB‚椠ĩœ §š°a’ßųŽ?PF$Ģ0Ļ„ųĶ8/žKĘÅDƒîįÁ—ĩqōĒ•S¨Œ_S„A bõ @•Š ˜ĘÕ,E)‚i[|Tī_ŧ‹mąz×?.Šģã{{ęŸ>֟ë\ĩîž9•c'=˛čļO\7gí:å>2)°ĶŠCĸīáC^”Ķ„@y€Ų×Ŧaæę5‘ã;vüūŽ'˙cũōëīú=Ëc=ŗ}ķĻLáË7/ų˃ēŦÆR_Ų€ĩ@į# S=i †5ˋkÉLn8vø÷o{å÷ŽîxíĮĢoŲđ7۟߸ŋe;ŽÎxüūKe5ęLꃇŸ!Éō@:Ą1Ŋū4ZĮ{ķ™Ũ۞Ķ×Üö鸯,LŲ( Ķ“įwĒÁôÂĖ•K9øöOī|â{O/­­ûÁ[[ëûvd(Züá ]^VŠ|/.Ę}XM´—a\ėS9ōM%'†"ŖWMZ¸˜@™…é-ë÷Le€å 2}ųډĮŪ|y-ƒ4k'nų­ĄęŠąĻŲ$Åŋāc*Á˜ŽĮģ;KÅĨSî8ągû‡_ųPkÃŲ˙šėē;žÜĩåéÖB–mÆŲP¤Ú ׄŒLžõJ/ž˛HJC'fn$hX*I’'wMŗƒ‡4q /NũKa`cĢ8šYō`,LĀÁ!‹QĀĐ&Ģâ¤Ņø‰“D ĄžŦ ¨¤œëÔ5,ãšĐNļüÉSú§Ÿ˜SwĶīŦqKŦcõ-÷.Ŧš0ũ‡×˙§NZ4‹d;´5¸ĖŽ4Ėb¤˙Ų6ØYHÆÁōÁUׯ fúėi[ôīī|ų+ËÖŪū]¯>S`>¤Ŗ2I‡dŌąū-w_iížŧS¯žĮÄķ˜šēö3ģņŗÖÜō+œIÆŋŋķÕ§ķÚnĨNÖVÉØ)ЃšEšŸŦŌ mgɯŲGĢl:[đwęÄ!y7^ĮŠ};ŋzæŨ¯}}8UŲt’d 2qōšĩ ™¤ 8ƒvzĄ™Ŗ§_5×ōyˆˇC6Õ˙qĨŧTŒ­6C‘šÅK×Ŧķŋĩms˛ŸWķqJ)'›Q阍i™dS ‰ šCRƒ2ĄfōÆÎúŗk¯ģjũS˙÷—Ÿų÷Å+oú“Ŋožx&ßej­t"N:"“įVĘŊeĶqpīi†NÄ1LŠÜ¤ĐאēJtŌ—męë}¸œžíÛū~ÄHq‘ Í ę:T°t ĩߙ[wã}ÜlV۸ɉ•ãĻūĮŋņĨ…“Ėĸũ"¤ĸ`˜ĖEĨĀ0ĀICë¨WÁMüļoúŠëū&›JŨŲ×eĢbÁŨŧ”áî˙lĘŨæL Ļ-[LŨWū`ÜUkëūŨ°<_-x“‡Āöõ¸ũßË oŖRlƒšIcXrûúĨÁŠę˙´xÕÍũP ¤úžßÉŌ5ˇšpÅäE‹üĻĮmiˆcF)÷ \UÍØ™ķ—™–wŪāme˙oOžûhˆ6C{#„GE¸ö×>Įõ_øōĒ7å[KVß<ϰíU}úŽõ †Â­Vˆaà ä51Ō\ ÃL5™ß4~wæ|–˙ëÕuwÍ/tšK¯Š3 Ķü‡5ŸūÍšL§õĸ[Ģ0$‚åŽr×üč%”ųX}˙įĢĮĖ\ô— —]?ĄØE0Ę ž3I7x”‡šū?ũ&W]S÷÷+oøä=Å.ŪpŖ”Ûē‘l‡Ų×ŦfÚÕkžâ8öōb—k0eĶÉQáĒŅk+ÆN@;Û"Ą–…¨™2gœ2ŦEˇĻŌcîū‰^rSãÕÕrí¯éĄŠĒŋ_}ĶziÍf†â-Wˆ’g Đ8\$Ã$FqņÅ9UŒū‹ĨuŸđ˛<ŸœķŨĻ]=‡ļRČVËUvĚĄjB5ËīŪ°Ø.pÅ Ÿ4‹[°ĨrÁsŦ0`ÕũŋJÕ¤éŗdõÍSŠ]ļáFŒéEˇŨ[Q3yæŸ.ŋūŽ`ąË5X,¯oüøy זÕT’lg@īæŽíĻiŒ›=3\6jlíâ•7KėЅiē-MmaîÚ,¸åžĪ9Žķųb—Kô/9đ…  ‡Fl2—•\w‚[ÕŨ–wûYY͸/ĪŊū2)÷V -pJšeÍĻaĘŌ%L˜ģ|ŊMĪ,všƒ2 Ņ e5•,ũÄįf{á¯ģLÑ2 ŪįOfîõwŪúîb—i0Ŧ¸ūV(RsMÕø ~¯Ī=Įō’ā8nËVÕÄŅ”U›—M'&āęJ–a¸s*‹nûÕSfüIíퟮ)všD˙‘ Yˆd H“E×uT3î7üų,cÉʛ–ĖXąvaÕø2‰žå6e¸) ūpŠËVĪņ‡*n*v™‹ÂŊyN]ŧˆ‰ VÜštÍ­ãŠ]ĻáĻ#M#›‚ënaô´Ģūįĸ7–KZB<ūPY¸jĖcg-!•ø4-ĨÜ}lBLZ¸|q(Rŗt`×Xē”é”Õ™sŨ“SŅļģŠ]&Ņ$hb€) Œf,sՒ[C”OĪk†šfÔôšeϝ¸ŗ Ęθ7’ņsfX–×ŋpÎüŪb—iPäjÚ}e&/\5F;Έy`LJAŧ ĘjÂ,Ŋķ3S‚‘?*v™ša™5•ã§Ŧ¨]=āĩ˞i°<ŖĻMĻwå Ŧ˛t)Čf`Âŧ̉Œ›ô™kn˙´ÄZÄ|‘B 0…"M†0‹yÁV.-œ_ws¯īsՓgԖžčĪfcįKã΁Š**ÆOšŒT˜W퀝UŒ™9'\=yšÔ8  x+L_u53VŨđāŌ5ˇ ÛNËŽŊŨp˛Ų“.­ļ|ād´t-;åŖ'0zÆÜkWŨtˇäé_A&‘qåjĖĖų‹ÚΏ˜kŪp'Aŗƒ@ãāŖÕ”/ęU‡¸•7~ŌŽÔ, WW*§āéAŠKáÖRyũ•Œž2{nÍÔYc‹]ĻÁäd!2Ą\ųȨ̈Í]¸jXw„,Ĩ܀Î4 –ŪyŸŋbĖ„ŦŊõža9raš^eXĩ5“§b˜š>ƒ ã<Ž3šęIĶįĄ ų  –Ī bėŋRĖ.vyD˙ YŒ8Ē€˙õ‡PA5“™š7™Tž“°t,ÃrĪíŧZŽréFĘPŒ™1§:ĩ"˙5 ˛EåØÉūljSWģ8ĸ Ë'q!ēĶq°0°č}~°˜$pH‘)h&CdЄˆĻlĸF÷ĒÆQ+Ē Ëã1”ĸOJʝbÕÎÚd’š|î–ĻĮĀë3Ȥ”›Ÿ\ĀMÚąÁōƒ7čĩ4jđk[•(ä[tí|8r@Ą´(‹ō1**FŸEßgŽë•ŽŖdzĀÉî“ÕHŌ¸/įßx›uú]tuí-?ŨŊõųŗÅ.WĘÄbsĒ'OŦ2hģ˜įų¨íkiĮ´fŌÛJásÛáûųKĶZĄ”á/+Īŧú‹$ûwá…s˛ < oŊŖōÄŪ­ŧtÍēWŪÚļšĩØåę+ĨÅãæ,œá${wøŲ\?…ĻS-ņDkÃ[ąææQ‰ÖĖl¯ĪC:ŅûeéÜäJÆã •­]˛æ–ŸíŲö|{›3 4āņBķéãÉ#Û_úL 2˛Ī0Í[ø”2t6“ÔŅφkÚÎëÖ/˙ŅLo TĐŗã@¨:„vt¨€MCÍbŌž0yM0Ļėh”–‹ėIŲäŨ¯d3šTŦ5Ē”:^čb*ĮMNcp(ŪŌ0Ë7q:Y13t4ˇ+"‰`EEûöÍOäUo´ėúOüŲņ¯ŦŸ{CŨlĨ Īo6L Œ+§õŲĩ† ¨<íW´î|ųį6îPá#J* UFą¨nÒ-ßûûßū˛ØeęĢl&}ãø9‹ü†™KuĘ'h6CĶøūąÆĻ3ī˙E°ĸæúXËŲ?Ž™<…T>ĩÖʝžŧzâtÆLŸS—MÆūrA3¸įĒ'ŌeŖÆFßxņ‰ n%@oŊ°ęæõĪ´œģøĨ1ŗĻ™ØĘ¤ U„nb`ČčbDr ¨%SŽ@ĄķM$,2e*€tŦéRÁUֆĮk+ÃĖØŲlIÍHؙŖų¨ąyWėzå—ÉdŦũtŸ3ąE^´Æ‹fäÖpi°3s¯[ë›ēė†/.^ySIûuMŨ32qÚēęI3p │d{‚æ3GZMËķb´šáíK§/¸-!yœJšyÍÁr/Փ§MŌŽ3+˙Ō ĮÎ5Sgĩ–×Œģ”Œĩd žäé\Gā>u%C‰ÍBô’ƒŖTĸeF Ĩô ÷…SJĻĪSpu‡vCeCc”„iĮ.,f5-o)väŅ J÷Ûę™4Ęũ,šũŠĒ?^˛ęæŧ[˜†ŠD´męøŲ քG…ōĢÎQ´_jãü‘wŽŧŗgkļŊņÜŲSû÷7Ø6XžüÂ:í¸— ķ(˸eՍwéœ];SXo>e˜&E¸^‹ĄK‚f!zÉĀčsĸqƒo]ŒĒjšîD%Ú.Đ?´Š(Lœ7“ų7~ę´žŊØe*”iYwUOšiZVššÖsg3éxl7€vôąÖsīHļļį?4ĄvËP=q:eÕc֘oEžK( Úq”;毈>D'4‹‘H & Ļml]X­ƒÆƒ‡8G;™ * ‡Aˆ^QJŲ ‡LįŧbÉf@™æ¯ģĨlôôų_Y´ü† Å.S!*ÆMē}üüĨf*šëė™˜fš3īėnLDÛ_UToo8ˇŗņä)ˇ“`ž 3 7Ecâ‚ĨWg3)‡XŒ4‹GĄ21Zų~Ž…KŖ|økŧXä8kÜIUډ‘"}*@¸D'ÅCZGēŊŌ$ãm*Ų^ōFô™VIB͔ą,¸õŽĩ–?øųb—)_ĢožgRdܔųå5•d ¸r&$ãiNŧw1ÕŪ˛`Į–_fĸ—.îi>{&Ŗōޞ)wÂĸš)ĶŧĒvņę›d`1ėÉA.FŒŽFļ$ O3‹ÖÔŨĩđø¯ē…&ŨČŲËšáw–ĒÕA0°ķ§Yaā„>ËQŪŲŌF“Ô4—­uiŒL=CkƒōšqĶĮÍYüí;ŋđ_›Qô[NļazIÅZ}࿯?{ũ؟lí¯å…Ûq-›‚Ų×^gžÜˇûK‹WÜøÔŪ/ Ųņ…ģ˛íĖēIķ¯Ž6,ČwˆxĨĀã‡ŗĪØÍįNŧž÷–¯=JēpôŨãŗckf{|aŌÉŪįJ+åΞ8ję<ÆN›ŗūôÁ=˙ ̧4ôí8ä=‹Œļ$h#‚;!C Å,VÜŠ>û×ŧIÕC6ŸFë,Yʉ„—Š•ŖŸ÷Ä&‰…b?1ĸ/8îürb°(Đēđ1¯ĩõ—JNx6h/‹o[_6˙æÛoTųļå÷ Xnpčõ]lūæ_~ōA3|ā…"~ŨzĮ„‹Güî’Õë~ĪöÍ-Å.[oøBå××LëSŒX¨ĘĄņÄáöĪtū[ôRÃŪŗ÷īO´_š‡Éä“Ėc@*‘qåTŒ›põéw÷ŒcÍîķna'oÛĨsĻaZǎŗ ŠáO‚f1bdqČĸ™ŖķXPãĐsŨ;“ ø°Č`(`&@ ”arŽFŪĐ/īļÉŧąŋžžDB°Ō§ ,뤥TŪĶ)/Z~ãĒ…uëg™ú4ĨŽ2 Ĩ ™ų:ë§š,Œa…ŨŸûņh EĀ_^‘ĐvÁķ#…ÖnāʎˇĩT&ĩ°Ŧf´Õ—vĻ\ĩŒ¤Â4‹à Ũßœ\ėŌ›XÂÆ.`t{M/ļÖ<­wÎqâĪ÷Ö?͐÷‚DŸ8ļļžĄŸģũ×~/7§Ųå)ðíLÆĶ~éâÄņs}nÉí÷Eú2ą‰ÖÉx,‹RąÂ–?;ãžú[&™ÂKõmz÷ÁĻršÍŪ€ÁüuuŪSûw˙ŅÂe×Ŋĩזãî߇fmb*_V9~Je¨ŌCĸ€)D ÚŖúėŨ;,÷l×ŋˇ^8ûōéûŽĪXšxšáąpōœíS;0~Îb”ŠÖbßÂŊdwFPeú@˙ĘmŸũĘIe\yF@Ĩ”Ö ;ŪŌ\•JDëj?ķÛ7EƏR…žGʀd,‹27šĐp%Aŗq4ƒq]×@/>žäÉĖ›ŧô7Ųį|ÅâCĘ }ķnūä‚L<úˆ˛zžė)ĀÎf UVŠ1ŗ¨PEН:ŧ"(@gIĮĸ)o ÔRØRÄGč\ځ v§˛2 Ņ ÕGŗčöO-o9âū̝ŊíīvŋöėôēS=qÚ''ĖYZ‘M›…=´)hôöq^Ķ„D[ĢIÆ/ž1”HrēĀÄä‚>Į)}$Ļ<ąĒîŪŅÅ.͈§ |õuĩ 4iZΊ%ŖmGûžDĄL0M#ûŪĢĪÆo{ËŖ1ōŠŌoI Ļ3īĻģî1LĪĘt2vАŅ)’VÎĘņW-Ž4<žüS34ø‚pņč{ēéÔņ+Ļ…)ËÚqáØģį3Šxū§lƒšÉS¨;ņÎ%ĢÖ ŊŠĘ‹tî›>Mۅ3Š–ķ'÷÷}ib( Yˆ~æā Ë2ĩ†ŋ2ž1ņNĩá{a*˙m^ŨMWģlbđ&D›R$Ŗ-—ŊŗŗšØå4xeF6~iĪĶīmšØ€?˜ĮÆ;4_: ÖŨAŸ‰™IÄį*Ã2ãq/_{ģ¯zâŒe‘‰ŦŽQ*ōĄ”ûj<ųžŽ65Ô_éŊ;_ųÅū ‡NÅc˜Dv|¨™2c´7ē!˙% CnF-NeŖvģ8ĸHĐ,ÄpȒ"CîU÷ņEãwîšÅ’īÍĢģyYąË&R`zāŌÉŗ™xKcIŒi\ ÜQô´ĒĒŲŅxōĐ˙8đÂK ‘Į€~†‰(DƗŗüSŋvŊ“ÉüF&‘ĖģĻu Ä[[fUOš=§bt… 5lų ÖlsņčÁ“ĄšŅīôôūD{ķŽ GΤ ÎŋļYš!“¯VžPų'ōûôđdy ژĄåėɆĘq“†Lžˇč›!rybxQ8hIŅ‚Í ķŸ¯.œĖŦo­Ēģw|ąË'‰h‡s÷´´\8ķķbgxҘĻ5ęЁ]ŋ|gķĻgOŋ}?ŋ%( ŪŗV¯büÜ%5ņÖ&c¨t,¯sŨčsÆoŪŖ7h †bÍM\8zđųmĪnlėé3ŅÆķŋ8{pĪEeÚŊŽą˙@ŽÃl嘉*ŠŽ]sÛ}Á<—0ŧhđāŌéSúüáwžŪúÜã2zÆ0!AŗČD‘&ËE2ĖaŸ0~uš˙ģ\bāi ^4näøŽW÷Kšhû™ÖērÁ’k­–‹§ūįî_<~!“Jbųz˙yĨ ›—xÕ}ŋAÕÄ*į8Å%PYsŨøĢĻ…tÖ&/šabNė{Ãio<˙Bo>˛÷ĢģÎŪwļíbŗ€!ė,#^&/Z13ŅÚĀŧ~0Ŋ^´‘sĮ°iz˙áÍŋ¸kōâĢ—„Ģ"¤âųŋÁrąfo ŧ|ō‚åĶ<ū2ė4y?ųh “€EˇŨcÍŊáļĪ–ÕsļˇÖ8ļ“ WU™G“O'ĄfĘ4OÕ¤iŋücaK)]ځP5D/ÅŲõŗī7dņ?)v™D˙’ YŒ0î ŊY:’{sßwĢy,LxH`“!›w­ŗ" Ė`>|kž¯˙á:D˙qlđø!Ã[÷ōúyŅąŗđÖÖúAqN)w ũ5Û6hÛņ*(ëß%÷ͮמn]vŨöÖS?Ũxũį˙ŗ×0Ũ ĻWrz Įđ‡ËWUM˜2 EĻĀņ<*ĮŽ3ō|p˛˛É<ö[Ų” К‰3Ļ/^~ôŊ;_>^ؒJKG‹Nų(ˆ6EyņßžŲ|æĀŪOīzíéąũ#‰ÍbÄ01ņbđš~!ũ‚ūŲAŗÉ¤įŖUÅčĢV7Ž^ÂJ,,ė<§âV@M2&Ēé3¯­ûtÕkõ?i*t{Äĸà –1`_ũËŧņø˙{>Ūzé×Ū|ág=vÂęOĻivūėgņÃ[Ÿ;č—ˇcũÖÅ0QŠX<ŒÔœč¯eöÅG^5ŋ<øĘĶO_vÍg&/™Ģ- ‰üä|,Zqä+oZZ5q|AŖft–M÷[ązE8ލx˙­­÷?¸%dšīÆã‡`4ŋĀĢß˙vûņ]¯~a÷ëõ/ˇpb HĐ,FˇŽXaĄhâbË9N<ä#°Õä˛i~>ë`bĐĀŲʃÎ[’R_ūÂZuŖ•Dc“_•Œƒƒ…ÉĻO<ÉŅYĀ…m‘(6ÃÃr‡–BšĩÎ:ÂÛ/<ĨOîyķŸĶ‰Ø_žųâĪ÷ĄHįŌ ”CëÅŗ‡Ūīs•ã&žŅũ9*ÚŅĻ9äfĐÛõęĶö˛ëîüŖˇ~šņÖQĶ˙¸Æã÷"ûzË0­yՓfÎ UUí˜:ģTƝ!k+FO›ë÷‚+Š]œ Ô‡įŊ˛ÜZæØĨvö×ŋȁWęßh=wúĢÚqļģœb`HĐ,FˇBĐ´"ŒJlŽ˙n[žo›Vˇú¯÷°uu-+˜ķšu.h˛Ō$+ō\ŋ(€Öî˜ŧ2eéž7ÁįĻåNG5‰Ö4‰ö&§éĖ ûÄ[ۚÎzûõd[Ķ˙2PÛßxéÉAīø×9¸˛ŧ>ĨôĢOũ(ßãŧ¤íÚōÔÉU7¯øĀ‹ĪūŨōģīRédéĜc'­0oÁXPØJĢđäōz+ƒŒŊhųĘ?u՛/=yŲéģšŖÁrđú5N?¤Ū(åĻ $ÛÚ{Ã9ûˁˇŌ§öī<ŌrūėŖZÛ?ØņĘĪ[ûž&1TIĐ,F$.¨šē‚Ē&;ŅN–pwŗŽZoyŒ% fy!O˛}ãS™XķÅvĶëëķíSk;&ÖrŠņÂҝeâąũJŠgß(b€Đ ĨT˙Ĩe”;“ūƁ—~ųŠ)KW¯­™4ŠXĢûā4ÔÍ]¸˛fŌüU‹Ē&TcnŽüP蘘í@ "ĀØY *ß{Ŋ~1P´s"†ÃÛßÔ§÷īj3-_V2Ũa' Í’Œĩ§šĪŧŋĢåė‰Ũ Ūđ…Ę^ÚžųņģOŠR"Aŗy¨ ‚ŋęĒPÚũßév4Œiˇ)5›Nqđĩ§_?š÷¯xĄ¸R…~ƒƒF„ĢÆdå•M¯?ũãh?–Xôƒ[žJ-_{ĮßķË?ķƒ˙Åį %‘ĻŠŒZ8åęÕĢåc(ĩōhЎĸzŌ´ĒČØIuĀÆ"_Nī3ĩå˙ūËũūPųĶãé9ī ­ņ‡B#cĸĻR ožü š†04 ‘‡,YĨéĪ Q1Đ´ĨŖĮˇ„Ējī{ķåTąË$žÖÎöûvĸFÎčjĮ+ŋt2Éøãoo~ęéŗO(gȞļ_pņØYs§Z^pܕūĨŨWų¨•c'-]ļöŽHą‹$D&‹!Î@™!ĘŽ_V÷ɆAŋƒ3u}îąđÆÖúÛ`ųC‚ĘŊR$U;Í5uu–™>į ÃÆ) %mļŌjâBMŖŋ´Œk§{Īsr“/ģWõļ:7đnAëĐXAÅčisĘŪúÅ?ŗhų īÛųrŪŲąæÆ›*ĮMœŽ)#›*Ŋũ#LË;ÆŽxpÍ-ŋrBF`€VĨ@ųLuÚë<ûÂã˙6ä‚ô};^Š/^uķ?îyę§ĩ5ŋõå*oČ"s§:*æ_ŊÖW=aúŠqWÍ6LëÃ)Ŋķ5ĩčęƒ˙ˏļ!ŠfüÜ%ŗŽŊģÉ>Dɓ yˆŌhŌØŒf‚ņ9õ{ˇ9ØëLLc *Igmų¨ dRW{˙ųŽ÷{ü`šũ<+°ĶnĮž|9ŧ^‹Č¸éŖŊÁđJ$hÀÍCX›ŦTĢ Ā;ëP€8Âx^ŅĪ ‘[ȰąqĐ,Qk[9ģöēõ‰öØz;“î1hÎM/gĘ˨;o0H*FaĩNāļg¸ŸÖĮYąš9ėcfMc‚iV_§bžĶéD† GøZĪBuˇõÖļįR‹VÜđÍŊĪ>yĶ„ųs—„**ˆ Ą)(LŸwŌØ™ŗ†">âÍųׂ+Üq¨,dĶŲ‚¯M[¨Ö(Ķú`BŸ|8šq9ĮÍžę ŋbá˛üûwåßĘ%ÄP"AķæÖŽ:Ä0 Ī]‡E’¸­q†ũMãā'Hˆpîįž}xûŅ$°ąą ž)dphĨ%eáŊØĶûŗŠÄŲt<–D9‡šJA6 ĻĮ¤fĘԎåäu[Ö6d<Ĩ¯RnžĻuTUôĸ×ܜæ\­3 pМÄÎĻ3Úą‡t§Ã};^>˛üúO<úöæŋąōŪõ†Į™!j´ü†ģáęqˇUMšb(íĄÍˇ–Ų€hs3ģųD[ĶŠŖû=>_ڝäšpŽŖĶÚļkŽZ{ËâĢŽ]ëրįqæjíÎåcljŒ›:ûÂá}ŗĸ-Bô–ÍCžv›ÉˆlčH55€Y˛2†BņâG˜dIsV/FۑžŪ¨ˆdílštLcZô)Ú´3‚¨B˛4˜d’Meb†eIĮÖA5e@)GįōxeũĀą3ß?đŌSwL^´äcfL#“.v‰Ā4ŦĘōQãn3}žû`ZoZãíį6=ĸuöŸĘjF;ÚŅ}úFíčt,^ƒâÉYĩkg˜V~SN{ƒ~&-X|Õņ/­@‚fQâ$hĸ—úhĀšiá"įNŧYŋŠĮšũōš‰‰l:u6ڜœ[96%ØĢ^–’Ņm įŪO´5]*v™ÄČ´ûĩú؂Ģ×ūĶî§6-X÷āƒS}ūéE øMgZ͔™Kƒ•7(ÍįÚŌlšĪ6J´m{זsũUļĩw}Ž5kßßzáÂŒĒ cČäų¸kįĻ5uŧ'XY|ˇŋĘ&D1 Ų4!†Æŧ¯a“ŨŲ›ĪŧúÔ˙Ķß?¸ĩõüŠ´åĨ$jķ>Fƒå‡Tŧ™‹Įík8~XfæE㠄^>ąsËĻc;ŪÂô¸ĶŦËÕĩu–aZĢ&-Xâ7ŧšwų¤fŪ¤b1NîÛõ†iyŪîĪōe‰LŦõŌKŽŧĮOŪM Čf \=ŽŅ3į׎ŧéî…ũY>!›ÍB  …æmvĻ.qáGŊũ\ĸĨigã‰# Į.ÍÉE ĶM¸túŧÎχėÛ>ä†%#Įîןuí­˙°¯ūįo´]l.ÖŊŖL3WÔVOšæöĩË3˙J)÷üjkl åÜÉũ™dâũū,ßļÍ?ĩŖ6_:ųží8…eI;YV”?y†vėũY>!›ÍB ¨ĀÃŪįˆ~÷1sώŸ Fj^9s`÷éxkŖÄÎXĢeÎĻŽī|ķ\ŦéâËÅ.“ûvžtæÜÁ=ß~ûŗâ Ã,N9 Ķ;~îĸO”ÕT‘Œ’wK’aēíÎ:fg’ąŊöní÷,m;›šÔxâũ­įZ „ōÎNåFŅP0nÎŧPŲ¨qk—_W >ú á*ą[°ĨĮAÂK†,¯ę§ã œų—ˇëŸīuvōöįÚzáȁ'NŊŊoÁsú‡<^hx˙8įßÛûJí.v™„xkûsßyûųĮ_˜ŧ _ä ũīÛhúÛ=õOį=Æ[Û7_j:sōŋŊųø’v&E°Âm&ˆisû…ž°;ÖÛ/ŧčŧģåį˙ōÖļįŸ-vą„člߎ—vŸÜˇũ{‡^o€A­mļŗ™ę˛ęŅ+"ƑI0^´r'4i=ކc‡ļŊųŌ/. HAt2yāˁ}Gŗ ˗ßuG)H%Á€ ķæTúË"W/^y“¤hˆ’$Aķ͏“4ë!ũ?ča*énļ ÛŽžWæ å}ę Q„ņÁCš8O9ŋäGú_˙o—Ø[˙lŦĐíŪņōĪ7ŊģåéŋØúãí¤W}ØÉn(Īƒø@%X^Ø÷ÜKėøé~lgŌ˙Ôˇ…¸üB^CbåŠ/Û;دÂ6°¯ßi˙ėįl2ūõˇ~ņÃŊíÍxCžO]&­õšņs3-7ī7ßu™Ø›†ãīˇ:væūŲ—-ėĨXķŎZ/œÃČ͘WymwdĘą“¨;ųFeyF÷~ÕE86‹Ië>žĨ¸ŅĨCÆiXڃāÃ;$G S€rĨėyŽfÆĀp|( 9|,wÂW&瀺ÛĀĐž\imŧũ3]lšOž đāŪ7ۉņļ>ÄĢŧxt/¯'EâÔŋØįI=vŧüķŋYyã§ėT"ú'WâÎ𨩓ą3îÄ!vÖ튞+ÎāČ}ƒĘts—MxüĐvą‰ũõĪŗ÷ŲM¤âÍš÷ _‰ŌĻĮĀëw›yķ-ŸĮŸĨ”S Ïh­1 Ãņú)hD…ÁdzA;Ę0Ũ‚÷šŌĻĮÄë‡lžķj ^?X>ĨŒB.#ąûõúöĨknũ͎žúÅã×}á×-Ø_8¯ \šĮ™é3ÕĢO*ÃŧnŌâxƒ…ÍLč šC͝}wīxKķëų}:?v:Ņmē°åü‘ƒ÷›3; :ĪŠĩ,TO˸ŲW]{ūŊŊã€+׌k´2Líņ™tĖä÷p|~0LĨ•ęŨwRlJ)Įōú Ū¯ãôø@õ|/…‘ y@鰉ixpƒÅĄxģV¸ž‰i‚Šôü m*ŒQĀ.`‹Ü΃ōkwÕų0Ēƒ‰ģO!ąO5&M—¸ÄyÎęĶb˙ÁãúŨ_´Đ¸iOũ3Ûús}ožôäß.ŋî“ĮŽúԜĩ7ß0mŲ *ƌÅpkŸ´ãNvā>ķvī(wŌĶ‚lÖ]o´Š3vqčÕWœ~{÷ˇœlú;ģ_¯÷qMå†e*ĶãåyÉÕČ–”QjčGÍ , ŗÆ´Āņ ņ ŲĶōĸ 3`˜_o>ŗtõ:ŦŦ ĻI!ߊÖ`Y`˜^P*ēĪc_ŧĩíš'×ŪõkON]ēōŪ+įĘ7ĮXše˛L e˜Õ=Ŋ}ÕÍ÷Œ›´`Ųũ‘qcŅŽģō92•r?ĶzĄ…čĨķįŪŪķjŋMhŌ7_ųĨŊdõēŊ-įĪdí –'›]4κɂ຿•—wĶę›īygû O\ö1X)åW†Ynz>ŧŽå4›Ā0‚UāķReXŖãÜ·RškiPŊš—‹BHĐ<€4Ēų9ũĶķģ•H‘˛wkŅNŗ/AôbOī ĖįāŅīęoļq’ų>+ ĀņŸÖGN–Qž×lÖ6Ųė9}ōė÷ų?LŦh–l‘÷ЁƒmDi‹9d“Žv.âø›qÚßPpâú'öԚwnųųcËŽŊã͡Zšn9ôڋ÷Ž™9oåø9ķĒ"&ā†ņĘAč|gKčÆ0LPo‰’ŽĩĶzą‘Ķö¤OßÛxōČéDü靯üŧ_ĻĖÍfRMo?˙ԅ{ödl&M>¤ŨšĻL:Ž55œÎĻĶC8uY>úĖ;ģŽlū×XØqœ"ĪWweJ)egSÁĻĶ'NkĮîÕÃŅ[Û7ëÚē ­īŧđtÃéwögėL&EÛ¨5xŧ–NÆÚ*mMMū˛Hŋ|§éxėk[ø­YĮwĖmgu"ßzÃ0ŒTŦŨ—ŠļžčņÍڙ™ˆļú^˙ÁwÛ™LŪ߯RhÃ0ŊÍįĪϓҖ_äûųB+ĒNœÚŋëŠįžŅ~­åõļ9Y'īš ĶōčD[S•éņ^åņ\aŽS ņķīí;õŌˇŋi)Êj;ŋõiĐ^Ÿ/pöĐžLĸĩŠĪ­|ƒ!koÜū“G.xƒ¸S@­”i*Ŗ­ąt´}ĀōÛG: š֏Žņî+'õáŦĶ_ x@r°Í,éķ=Ŋ×ÂĶØÂĨOīŅÛũWĐÛą6…ļŌ¤â^|y5ŲktcŒļßŨ§ß,WÖE& [ĩr)c`f+ˆ$š¸p~_}}û%ģ^{úđoËŽŊũéļ‹gĻÚ÷Æ ;›īņĮ„"ÕCUŖF[^ŸOk§_>Ĩ ĨGĮ›/]joēp&ŅÖrÆ.kŒ57ž‘N&Ží|åGûs}ÚÉ>~ņØ{o6ž8šËŽĖŗŧ´GÛN3g2Āā3,ëũö‹įîŽ55zQ ĘąÔJ;ÚRЏåõĪãsĪ4žøāĨSī;š‘|ķd ĩãËĻ’–×Wp?ÎŪxá‰#Ģnŧ{Cĸ­%ˆ*āÚĻQ M”ŅcåƒayŪ‰6^¸9ŅԜą3éŧ¯dJŌĘL%Y;›Q3ēōøügcM'ŅŌ8ʰ<éŧ˛q> ´RfĀԚ´ã\ų!KëCŅĻ‹_ˆˇ4”"“oŋĨ +›IŲÚÎæslM6“ū‹3vCfVÔĤ”cg•É\ę÷ TažB!„#‡Œž!„B!D$hB!„ĸ4 !„BŅ š…B!„čÍB!„Bô@‚f!„B!z AŗB!„= Y!„BˆȌ€H Ų‰ŗ…Bˆâj^Ŧ"žÜŋ›f`sdSxPf<Ždōēū'3 –{bôâ­3zē04¯nîëŞĒ"›ÂÍŊ)ßPŌŧ>úđČeūüXdSxÃ`–g04¯><ÜÃÛ6G6…oŒō †^n3‘Má’|âėíöåė"Įpŋë’;w 5ÎųæõŅūŧÁ~-˛)üõ~\^ŅäžûpåžžŲ~t`K5øōŧ^€ģ/Žá^36G6…w]éÍßõ?IĪXõō}W ŦsOãŊ ˜+á›î•öÁ}š} Äp˛ ˇ–ía`#ĐÔŧ>ē1PŒrΏ0ÍëŖëš×Gâ>,õ&`˜<Ōŧ>z´y}´7÷ÁáŦŖfūa`gķúčķÍëŖëŠ\ĻE‚æŲî¨AęIO7ÉŪ^(z¤)š›cOБ~ą#Ã}¸ÂÎá|3”s~äi^}x7đ+Ät`cķúč#ÍëŖ‘ß=2Ŧž—}2x$§yā=ĘĮ›_6ņáM#Ōŧ>z_dSørAo× úAÜJį›ęą+|~¨ëē}_ÃmēŪȇ9nÃĸiō vá>øäĶ\Wę†û6īÂ=ž;ûāŧįÃÚæŽ–áŪ ‡M“|#ņœīØÆ|†ģ_:ī›e|xnt –5¯ŪRÂ-Ģ—ĶŨõ><ÖŅũõb8ī“!Ejš^wl7˜îŦÛZ•\sTį'ķcševŊ–ržWį‹éą\ĐąĻ7¯–üÍŖÍäS-uÃ}››s¯‡:Ŋ:ÃŊAVá>oîæķįj膛‘xÎßĮGƒŽW3n ÔŨßJ>Eå2s3ûAâ2Ümî ŋĖČũŊk ¸ ÷k¸éîzŅųšņu>ÜgŨí“ËõũDjšXdSøXķúčc|4(~¸…ā÷5¯NīĻC`×`úŅÜīē>}—d-sķļ<–ûŨ:Ümę|ą}7¸ĸ”DøhíPךĸc¸įõÜsûa>zN<Ôŧ>zl¸t„ÁįüåŽÔâPŌrŨēĖģpŋĶnāÛuģ;˙ŧ7P\ž{oį}¸Žy}ôĄaØĶõzA—Ÿ›šü>š¯‡VkŅGRĶ<8ēĀËpkŽXÛ|™€vķģĮJxXžËmß}Ũä„KŽŖŽĻ“ë؃{ŧßÂĮk‘F9‹rΏšû×C]~Ŋ ÷ø^‡›ßÜ̓BĮû:*šēļJ=<ĖZ#z#‚{ŊxwŸtŊ÷Į–Š!C‚æA{ęëz`?ĀĮƒéŽOäŨŨ\ē{ íŽYwČ˝ˇąclÎûpķŧāŖŋb8šŽ ,íuî,BŌFā9K7¯Ū¤#=ÚÍįJ­öđ!>ÚĸЌģ“`×14ÛH 7Ķ}>|Į°}]kڇ[Z͐"AķāézÁëŽVez—ķ]o]›.šK¸Ųļ§íëhŽíLjžDŠûZnęå¸7ÃŽ”ÎēÖڕĸuÎG6…7w}ņņV„îëæŗ%͚˜Ģež\öËå7w~@čîŧčģĻëŦæÃnŽl =˛)ŧœˇB=”ģ˙wŨWÃ.Õg¨ yđtŊvWĢšD.xūHĀܡë§TføčͰ÷fy_7ī|îH1BD6…wån†3čRģŠ w­AŠ ƒačäœē>člÎŊēÖ oƝÜëë]:rvģ{pė˜ÜŖŗRnčĩ\ ĐųēĐqMÎŠ‡ šIŽ– ë‰~ŋ(<ĐM&ĀŖŨt Ą›Ī—„\ķQį&¤Įpē6+ŨG÷ûMˆa#˛)ü Ŋņ=ģftŊ–lŗĢœķ#JׇģŽ…Î÷¯]Ā†Ë ‘Ųn͝…Ž4ĨŽ÷ŊR˜ĖGwr×ߍ¤ũ1¨$h\]›;Æ ė.ˇšģfĖŽ7Ž]ĨÔd×EoROĀ åŖMR#ĸVAŒ8 r5H]ƒĮ’ š‘s~$é.hîz˙úZ/ĮŅŨčĨ|^äĢ7­.2Vķ‘ yus#€î;vmÂęø{wKUįméH=éŽ6Š#ĪûŅn~'ÄpŌ] ĐõzQĘ#hČ9?2ËĮËēüŽWØģiq™žkũČįGÂņ‘ÛîîrÂģnģ¤k  šßĮr˜/3ēFgŨ=Ĩwä–œ\.wį›˙ŖŨüŽŗûFƒ„čÎejŨ†E‘œķ#G7Ákw÷ļ|[H‡S‹KžĻ7¯ŽËyŊ“.šW×ũ1,ŽC‘̓¯ë`z.WųrĩÆĶcŦiŗ„§ËėŽéîJÚûr?w~zžo[+Äp'įŧ…yw(ʇųxjƃ|H”s~dé&íĸģã:ßŅP$ũ⪚qæf>žÎųh W¨ y4Įåfė.ĮĢÛ™\đ]ŠēЍģß}lT‘ËüNˆáĸģĀ kĐ\Š7C9įGļŽäŽyÉŊĘAÎ=tu͇ūXoos¤KÜ1Ü!įfā^vvų{Į˜×b€HĐ\ņņ›_wĩÍá§ą™;oKĮÄ,û]îĸØy;—á^$>ō;ŋU #'/iÎÃĄ†MÎų‘§ģ!{ęđ~9tųųą\ëkgĨx^\NĮTã]_Uš1Ũà –ģÛJ-ķĀ’ šrõĮ†™ëftË ÉT’As7ļ<ÖÍī:o[w؏ČAíÅđÕŧ>i^}„ŸŨa\RÁœķ#Öåæ$č|[Öŧ>ēņryęÎ‹ŽŽŌ}KÅp1÷ēëëhķúčķ¸ĩÍŨíŗ[ēŠOô3ĢØÁēĝ‡Yzˆ\íKķúčŅ.Ÿ+å|Ĩî†Ėëz\—ģ0tįČĻpUķúč1>ŧéŪ‡4G‰”›ėcî9Đ9ˆlÆm‚í:mvs 6AË9?2=ÆGkB×á>~ŊËīīÃũūŋŽ;īĀæÜ}p?/ĀũŪģæČwŦo¸ˆpųÉIÖå^_ãã5đbHĐ\$š‹CᏂޛåŖŨÔČ@‰^ē™åpîs׋ߕ†ŠäšåãÀbzķúhwƒŨ=M|Ŧ„'¸Šn^ŊRĶôƒ¸į}×ā˛ë´ÚCšœķ#WdSøXķúhפGp§ÆîzlGČŌÍëŖWZėŖšW×ŦÍ%ÜĮ§ãîĮûøhpũnŽŗ@4×Ŗ|ôŠûÜ'Č]|XãÜY¯„‚ēNūXîįBŽûf%ú0Ņ ËøøMĸŗ¯QbÁ”¸Ŧc†ÜwũÎģæû–9įGļ¯ĶĨ5÷¸ž÷xîzģ’Gų°vĩëCÖpoučÚˇĄãĄãk|´#āôæõ҇"›Âr?@’Ķ\\ŨŨāÃĄd†ë €Đ}^ZoLĮŨ7ņ[E ;†{\Ž{s|žį,–b9įG°\ X׀ļŖāQÜāš§J cš÷}-÷šŽĮĪ×G@-sw#Ë<ˇi\=$įÅĀ’šæ"Šl 77¯víīČĻđ×/3[VIÖŦä:4u~Rî¨qęmīéîtŠī†Gk×āār:FC_įītv†ęø}ĮōFēĪe|°ÔR䜚{Y×tŒe¸5¤_ĮmYé8VĻį^ŗÜmÎũûîšŅõzˇ9˛)<Ük™;|ˇÜ<‚ģ˙:Į Üü‘˛_ÍÅ×5hžž ˜ģ›°TsWģ(ŸĻšî–×õfŲŨīJM„Ū7_?Ėđ˜J6Âđ؎Ëéî;íØŪé¸7ž+åŦ?˜U§ÔČ9ß{Ãũ¸Ŗķ^įãŧãę!Üāx­ęčØ5Pė°‹S™†“n¯‡š ļŽ(;:wM]z¨y}ôŅŽ†4 š‹,˛)üX7ģë[R5M]t}čZÛtŒžSO:z ƒ{QŲØåī˚×G—•xS]OųËÃŅ2†wĐ\čwēøZ ĪrÎ÷^_j߇ŧ\ZŅ-šáãēEķIÛy÷Ü(ĩtĨŪ¸ėõ°S tįXárĮÎ#¸i-ĸŸIĐ<4tíØUsŠ5Īv¸Ėā]=ÖSį…æõŅ]\šFÜ oŠß@ÅČöĨÚáWÎyŅ­ČĻđƒÍëŖ›qīu…LPĶL Ļ*õŗéŨCøēܨK%{Ǥ#āĐĐĶE ›g;ôfĒÔˇ/DôÔÜ$“ˆR˛9÷únSsUdSxC)Ė9r΋nE6…ËÍj÷ =ˇ::ÎásĮ9ŅÛ}0Ŧ[0ŠEi­‹]†aK)Uė"!„CRŽ“`GŪrį´„Žŗ#m æ~%ņ]˙“ Y!„BˆHz†B!„= Y!„BˆHĐ,„B!D$hB!„ĸ4 !„BŅ š…B!„čÍB!„Bô@‚f!„B!z AŗB!„= Y!„BˆHĐ,„B!D$hB!„ĸ4 !„B҃˙dO~īfˇ.IENDŽB`‚libopenapi-validator-0.13.8/openapi_vocabulary/000077500000000000000000000000001520534042400215715ustar00rootroot00000000000000libopenapi-validator-0.13.8/openapi_vocabulary/coercion.go000066400000000000000000000076001520534042400237240ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary import ( "regexp" "strconv" "github.com/santhosh-tekuri/jsonschema/v6" ) // coercionExtension handles Jackson-style scalar coercion (string->boolean/number) type coercionExtension struct { schemaType any // string, []string, or nil allowCoercion bool } var ( booleanRegex = regexp.MustCompile(`^(true|false)$`) numberRegex = regexp.MustCompile(`^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$`) integerRegex = regexp.MustCompile(`^-?(?:0|[1-9]\d*)$`) ) func (c *coercionExtension) Validate(ctx *jsonschema.ValidatorContext, v any) { if !c.allowCoercion { return // Coercion disabled - let normal validation handle it } str, ok := v.(string) if !ok { return // Not a string - let normal validation handle it } // check if we should coerce and validate the string format if c.shouldCoerceToBoolean() { if !c.isValidBooleanString(str) { ctx.AddError(&CoercionError{ SourceType: "string", TargetType: "boolean", Value: str, Message: "string value cannot be coerced to boolean, must be 'true' or 'false'", }) } return } if c.shouldCoerceToNumber() { if !c.isValidNumberString(str) { ctx.AddError(&CoercionError{ SourceType: "string", TargetType: "number", Value: str, Message: "string value cannot be coerced to number, must be a valid numeric string", }) } return } if c.shouldCoerceToInteger() { if !c.isValidIntegerString(str) { ctx.AddError(&CoercionError{ SourceType: "string", TargetType: "integer", Value: str, Message: "string value cannot be coerced to integer, must be a valid integer string", }) } return } } func (c *coercionExtension) shouldCoerceToBoolean() bool { return c.hasType("boolean") } func (c *coercionExtension) shouldCoerceToNumber() bool { return c.hasType("number") } func (c *coercionExtension) shouldCoerceToInteger() bool { return c.hasType("integer") } func (c *coercionExtension) hasType(targetType string) bool { switch t := c.schemaType.(type) { case string: return t == targetType case []any: for _, item := range t { if str, ok := item.(string); ok && str == targetType { return true } } } return false } func (c *coercionExtension) isValidBooleanString(s string) bool { return booleanRegex.MatchString(s) } func (c *coercionExtension) isValidNumberString(s string) bool { if !numberRegex.MatchString(s) { return false } // Additional validation using strconv _, err := strconv.ParseFloat(s, 64) return err == nil } func (c *coercionExtension) isValidIntegerString(s string) bool { if !integerRegex.MatchString(s) { return false } // Additional validation using strconv _, err := strconv.ParseInt(s, 10, 64) return err == nil } // CompileCoercion compiles the coercion extension if coercion is allowed and applicable func CompileCoercion(ctx *jsonschema.CompilerContext, obj map[string]any, allowCoercion bool) (jsonschema.SchemaExt, error) { if !allowCoercion { return nil, nil // Coercion disabled } // Get the type from the schema schemaType, hasType := obj["type"] if !hasType { return nil, nil // No type specified - no coercion needed } // Only apply coercion to scalar types if !IsCoercibleType(schemaType) { return nil, nil } return &coercionExtension{ schemaType: schemaType, allowCoercion: true, }, nil } // IsCoercibleType checks if the schema type is one that supports coercion func IsCoercibleType(schemaType any) bool { switch t := schemaType.(type) { case string: return t == "boolean" || t == "number" || t == "integer" case []any: // for type arrays, check if any coercible type is present for _, item := range t { if str, ok := item.(string); ok { if str == "boolean" || str == "number" || str == "integer" { return true } } } } return false } libopenapi-validator-0.13.8/openapi_vocabulary/coercion_simple_test.go000066400000000000000000000307441520534042400263410ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary import ( "fmt" "strings" "testing" "github.com/santhosh-tekuri/jsonschema/v6" "github.com/stretchr/testify/assert" ) func TestCoercion_Vocabulary_CompilationSuccess(t *testing.T) { // Test that coercion vocabulary compiles successfully for all scalar types testCases := []string{ `{"type": "boolean"}`, `{"type": "number"}`, `{"type": "integer"}`, `{"type": ["boolean", "null"]}`, `{"type": "string"}`, // Should not get coercion extension } for i, schemaJSON := range testCases { t.Run(fmt.Sprintf("Schema_%d", i), func(t *testing.T) { schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabularyWithCoercion(Version30, true)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) // Should compile successfully compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) assert.NotNil(t, compiledSchema) }) } } func TestCoercion_Vocabulary_DisabledCompilation(t *testing.T) { // Test that vocabulary compiles successfully when coercion is disabled schemaJSON := `{"type": "boolean"}` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabularyWithCoercion(Version30, false)) // Disabled compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) // Should compile successfully even with coercion disabled compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) assert.NotNil(t, compiledSchema) } // Additional comprehensive coercion tests func TestCoercionExtension_Validate_CoercionDisabled(t *testing.T) { // Test with coercion disabled via vocabulary schemaJSON := `{"type": "boolean"}` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabularyWithCoercion(Version30, false)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) // With coercion disabled, string "true" should fail normal validation err = compiledSchema.Validate("true") assert.Error(t, err, "Should fail without coercion") // But actual boolean should pass err = compiledSchema.Validate(true) assert.NoError(t, err, "Should pass with actual boolean") } func TestCoercionExtension_Validate_NonStringValue(t *testing.T) { // Test that non-string values don't trigger coercion schemaJSON := `{"type": "boolean"}` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabularyWithCoercion(Version30, true)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) // Non-string values should be handled by normal JSON Schema validation err = compiledSchema.Validate(true) assert.NoError(t, err, "Boolean should pass") err = compiledSchema.Validate(123) assert.Error(t, err, "Number should fail for boolean type") err = compiledSchema.Validate(nil) assert.Error(t, err, "Null should fail for boolean type") } func TestCoercionExtension_Validate_BooleanCoercion_Valid(t *testing.T) { // Create a schema that allows both boolean and string types for coercion to work schemaJSON := `{"type": ["boolean", "string"]}` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabularyWithCoercion(Version30, true)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) testCases := []string{"true", "false"} for _, testCase := range testCases { err = compiledSchema.Validate(testCase) assert.NoError(t, err, "Should pass for valid boolean string: %s", testCase) } } func TestCoercionExtension_Validate_BooleanCoercion_Invalid(t *testing.T) { schemaJSON := `{"type": ["boolean", "string"]}` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabularyWithCoercion(Version30, true)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) testCases := []string{"yes", "no", "1", "0", "True", "FALSE", ""} for _, testCase := range testCases { err = compiledSchema.Validate(testCase) assert.Error(t, err, "Should fail for invalid boolean string: %s", testCase) assert.Contains(t, err.Error(), "cannot coerce", "Should contain coercion error message") } } func TestCoercionExtension_HasType_Methods(t *testing.T) { // Test hasType method with string type ext := &coercionExtension{ schemaType: "boolean", } assert.True(t, ext.hasType("boolean")) assert.False(t, ext.hasType("number")) assert.False(t, ext.hasType("integer")) // Test hasType method with array type ext = &coercionExtension{ schemaType: []any{"boolean", "null"}, } assert.True(t, ext.hasType("boolean")) assert.True(t, ext.hasType("null")) assert.False(t, ext.hasType("number")) // Test hasType method with invalid array items ext = &coercionExtension{ schemaType: []any{123, "boolean"}, } assert.True(t, ext.hasType("boolean")) assert.False(t, ext.hasType("number")) // Test hasType method with non-string, non-array ext = &coercionExtension{ schemaType: 123, } assert.False(t, ext.hasType("boolean")) assert.False(t, ext.hasType("number")) } func TestCoercionExtension_ValidationMethods(t *testing.T) { ext := &coercionExtension{} // Test boolean validation assert.True(t, ext.isValidBooleanString("true")) assert.True(t, ext.isValidBooleanString("false")) assert.False(t, ext.isValidBooleanString("True")) assert.False(t, ext.isValidBooleanString("FALSE")) assert.False(t, ext.isValidBooleanString("yes")) assert.False(t, ext.isValidBooleanString("no")) assert.False(t, ext.isValidBooleanString("1")) assert.False(t, ext.isValidBooleanString("0")) assert.False(t, ext.isValidBooleanString("")) // Test number validation validNumbers := []string{ "123", "-123", "0", "-0", "123.45", "-123.45", "0.0", "1e5", "1E5", "1e+5", "1e-5", "1.23e10", "1.23E-10", } for _, num := range validNumbers { assert.True(t, ext.isValidNumberString(num), "Should be valid number: %s", num) } invalidNumbers := []string{ "abc", "12.34.56", "1e", "e5", "1.23.45e10", "", "null", "true", "Infinity", "NaN", "+123", } for _, num := range invalidNumbers { assert.False(t, ext.isValidNumberString(num), "Should be invalid number: %s", num) } // Test integer validation validIntegers := []string{"123", "-123", "0"} for _, num := range validIntegers { assert.True(t, ext.isValidIntegerString(num), "Should be valid integer: %s", num) } invalidIntegers := []string{ "123.45", "abc", "007", "1e5", "", "null", "true", "+123", } for _, num := range invalidIntegers { assert.False(t, ext.isValidIntegerString(num), "Should be invalid integer: %s", num) } } func TestCompileCoercion_CoercionDisabled(t *testing.T) { obj := map[string]any{ "type": "boolean", } ext, err := CompileCoercion(nil, obj, false) assert.NoError(t, err) assert.Nil(t, ext) } func TestCompileCoercion_NoType(t *testing.T) { obj := map[string]any{ "minLength": 1, } ext, err := CompileCoercion(nil, obj, true) assert.NoError(t, err) assert.Nil(t, ext) } func TestCompileCoercion_NonCoercibleType(t *testing.T) { obj := map[string]any{ "type": "string", } ext, err := CompileCoercion(nil, obj, true) assert.NoError(t, err) assert.Nil(t, ext) } func TestCompileCoercion_CoercibleTypes(t *testing.T) { coercibleTypes := []string{"boolean", "number", "integer"} for _, schemaType := range coercibleTypes { obj := map[string]any{ "type": schemaType, } ext, err := CompileCoercion(nil, obj, true) assert.NoError(t, err, "Should compile for type: %s", schemaType) assert.NotNil(t, ext, "Should return extension for type: %s", schemaType) coercionExt, ok := ext.(*coercionExtension) assert.True(t, ok, "Should be coercionExtension") assert.Equal(t, schemaType, coercionExt.schemaType) assert.True(t, coercionExt.allowCoercion) } } func TestIsCoercibleType_String(t *testing.T) { assert.True(t, IsCoercibleType("boolean")) assert.True(t, IsCoercibleType("number")) assert.True(t, IsCoercibleType("integer")) assert.False(t, IsCoercibleType("string")) assert.False(t, IsCoercibleType("object")) assert.False(t, IsCoercibleType("array")) } func TestIsCoercibleType_Array(t *testing.T) { // Array containing coercible type - should return true assert.True(t, IsCoercibleType([]any{"string", "boolean"})) assert.True(t, IsCoercibleType([]any{"number", "null"})) assert.True(t, IsCoercibleType([]any{"integer"})) // Array containing only non-coercible types - should return false assert.False(t, IsCoercibleType([]any{"string", "null"})) assert.False(t, IsCoercibleType([]any{"object", "array"})) assert.False(t, IsCoercibleType([]any{"string"})) // Empty array - should return false assert.False(t, IsCoercibleType([]any{})) } func TestCoercionExtension_ShouldCoerceToMethods(t *testing.T) { // Test shouldCoerceToNumber method ext := &coercionExtension{ schemaType: "number", } assert.True(t, ext.shouldCoerceToNumber()) assert.False(t, ext.shouldCoerceToBoolean()) assert.False(t, ext.shouldCoerceToInteger()) // Test shouldCoerceToInteger method ext = &coercionExtension{ schemaType: "integer", } assert.True(t, ext.shouldCoerceToInteger()) assert.False(t, ext.shouldCoerceToBoolean()) assert.False(t, ext.shouldCoerceToNumber()) // Test shouldCoerceToBoolean method ext = &coercionExtension{ schemaType: "boolean", } assert.True(t, ext.shouldCoerceToBoolean()) assert.False(t, ext.shouldCoerceToNumber()) assert.False(t, ext.shouldCoerceToInteger()) } func TestCoercionExtension_Validate_NumberCoercion(t *testing.T) { // Test number coercion path in Validate method schemaJSON := `{"type": ["number", "string"]}` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabularyWithCoercion(Version30, true)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) // Test valid number strings err = compiledSchema.Validate("123.45") assert.NoError(t, err) err = compiledSchema.Validate("-123") assert.NoError(t, err) // Test invalid number string err = compiledSchema.Validate("abc") assert.Error(t, err) assert.Contains(t, err.Error(), "cannot coerce") } func TestCoercionExtension_Validate_IntegerCoercion(t *testing.T) { // Test integer coercion path in Validate method schemaJSON := `{"type": ["integer", "string"]}` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabularyWithCoercion(Version30, true)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) // Test valid integer strings err = compiledSchema.Validate("123") assert.NoError(t, err) err = compiledSchema.Validate("-123") assert.NoError(t, err) // Test invalid integer string err = compiledSchema.Validate("123.45") assert.Error(t, err) assert.Contains(t, err.Error(), "cannot coerce") } func TestMetadataExtensions_Validate_Coverage(t *testing.T) { // Test example extension validate method (just for coverage) exampleExt := &exampleExtension{example: "test"} exampleExt.Validate(nil, "any-value") // No-op, just for coverage // Test deprecated extension validate method (just for coverage) deprecatedExt := &deprecatedExtension{deprecated: true} deprecatedExt.Validate(nil, "any-value") // No-op, just for coverage } libopenapi-validator-0.13.8/openapi_vocabulary/discriminator.go000066400000000000000000000044161520534042400247740ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary import ( "github.com/santhosh-tekuri/jsonschema/v6" ) // discriminatorExtension handles the OpenAPI discriminator keyword type discriminatorExtension struct { propertyName string mapping map[string]string // value -> schema reference } // Validate validates the discriminator property exists in the instance func (d *discriminatorExtension) Validate(ctx *jsonschema.ValidatorContext, v any) { obj, _ := v.(map[string]any) // check if discriminator property exists in the object if d.propertyName != "" { if _, exists := obj[d.propertyName]; !exists { ctx.AddError(&DiscriminatorPropertyMissingError{ PropertyName: d.propertyName, }) } } } // CompileDiscriminator compiles the OpenAPI discriminator keyword func CompileDiscriminator(_ *jsonschema.CompilerContext, obj map[string]any, _ VersionType) (jsonschema.SchemaExt, error) { v, exists := obj["discriminator"] if !exists { return nil, nil } discriminator, ok := v.(map[string]any) if !ok { return nil, &OpenAPIKeywordError{ Keyword: "discriminator", Message: "discriminator must be an object", } } propertyNameValue, exists := discriminator["propertyName"] if !exists { return nil, &OpenAPIKeywordError{ Keyword: "discriminator", Message: "discriminator must have a propertyName field", } } propertyName, ok := propertyNameValue.(string) if !ok { return nil, &OpenAPIKeywordError{ Keyword: "discriminator", Message: "discriminator propertyName must be a string", } } var mapping map[string]string if mappingValue, exists := discriminator["mapping"]; exists { mappingObj, ok := mappingValue.(map[string]any) if !ok { return nil, &OpenAPIKeywordError{ Keyword: "discriminator", Message: "discriminator mapping must be an object", } } mapping = make(map[string]string) for key, value := range mappingObj { if strValue, ok := value.(string); ok { mapping[key] = strValue } else { return nil, &OpenAPIKeywordError{ Keyword: "discriminator", Message: "discriminator mapping values must be strings", } } } } return &discriminatorExtension{ propertyName: propertyName, mapping: mapping, }, nil } libopenapi-validator-0.13.8/openapi_vocabulary/errors.go000066400000000000000000000031071520534042400234350ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary import ( "fmt" "golang.org/x/text/message" ) // OpenAPIKeywordError represents an error with an OpenAPI-specific keyword type OpenAPIKeywordError struct { Keyword string Message string } func (e *OpenAPIKeywordError) Error() string { return fmt.Sprintf("OpenAPI keyword '%s': %s", e.Keyword, e.Message) } // DiscriminatorPropertyMissingError represents an error when discriminator property is missing type DiscriminatorPropertyMissingError struct { PropertyName string } func (e *DiscriminatorPropertyMissingError) KeywordPath() []string { return []string{"discriminator"} } func (e *DiscriminatorPropertyMissingError) LocalizedString(printer *message.Printer) string { return fmt.Sprintf("discriminator property '%s' is missing", e.PropertyName) } func (e *DiscriminatorPropertyMissingError) Error() string { return fmt.Sprintf("discriminator property '%s' is missing", e.PropertyName) } // CoercionError represents an error during scalar type coercion type CoercionError struct { SourceType string TargetType string Value string Message string } func (e *CoercionError) KeywordPath() []string { return []string{"type"} } func (e *CoercionError) LocalizedString(printer *message.Printer) string { return fmt.Sprintf("cannot coerce %s '%s' to %s: %s", e.SourceType, e.Value, e.TargetType, e.Message) } func (e *CoercionError) Error() string { return fmt.Sprintf("cannot coerce %s '%s' to %s: %s", e.SourceType, e.Value, e.TargetType, e.Message) } libopenapi-validator-0.13.8/openapi_vocabulary/metadata.go000066400000000000000000000032371520534042400237050ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary import ( "github.com/santhosh-tekuri/jsonschema/v6" ) // exampleExtension handles the OpenAPI example keyword (metadata only) type exampleExtension struct { example any } func (e *exampleExtension) Validate(ctx *jsonschema.ValidatorContext, v any) { // Example keyword is metadata only - no validation needed during runtime } // deprecatedExtension handles the OpenAPI deprecated keyword (metadata only) type deprecatedExtension struct { deprecated bool } func (d *deprecatedExtension) Validate(ctx *jsonschema.ValidatorContext, v any) { // Deprecated keyword is metadata only - no validation needed during runtime } // compileExample compiles the example keyword func CompileExample(ctx *jsonschema.CompilerContext, obj map[string]any, version VersionType) (jsonschema.SchemaExt, error) { v, exists := obj["example"] if !exists { return nil, nil } // Example can be any valid JSON value, so we just store it // The main validation is that it exists and is parseable (which it is if we got here) return &exampleExtension{example: v}, nil } // compileDeprecated compiles the deprecated keyword func CompileDeprecated(ctx *jsonschema.CompilerContext, obj map[string]any, version VersionType) (jsonschema.SchemaExt, error) { v, exists := obj["deprecated"] if !exists { return nil, nil } // Validate that deprecated is a boolean deprecated, ok := v.(bool) if !ok { return nil, &OpenAPIKeywordError{ Keyword: "deprecated", Message: "deprecated must be a boolean value", } } return &deprecatedExtension{deprecated: deprecated}, nil } libopenapi-validator-0.13.8/openapi_vocabulary/nullable.go000066400000000000000000000016531520534042400237230ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary import ( "github.com/santhosh-tekuri/jsonschema/v6" ) // compileNullable compiles the nullable keyword based on OpenAPI version func CompileNullable(_ *jsonschema.CompilerContext, obj map[string]any, version VersionType) (jsonschema.SchemaExt, error) { v, exists := obj["nullable"] if !exists { return nil, nil } // check if nullable is used in OpenAPI 3.1+ (not allowed) if version == Version31 || version == Version32 { return nil, &OpenAPIKeywordError{ Keyword: "nullable", Message: "The `nullable` keyword is not supported in OpenAPI 3.1+. Use `type: ['string', 'null']` instead", } } // validate that nullable is a boolean _, ok := v.(bool) if !ok { return nil, &OpenAPIKeywordError{ Keyword: "nullable", Message: "nullable must be a boolean value", } } return nil, nil } libopenapi-validator-0.13.8/openapi_vocabulary/vocabulary.go000066400000000000000000000052431520534042400242730ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary import ( "github.com/santhosh-tekuri/jsonschema/v6" ) // OpenAPIVocabularyURL is the vocabulary URL for OpenAPI-specific keywords const OpenAPIVocabularyURL = "https://pb33f.io/openapi-validator/vocabulary" // VersionType represents OpenAPI specification versions type VersionType int const ( // Version30 represents OpenAPI 3.0.x Version30 VersionType = iota Version31 Version32 ) // NewOpenAPIVocabulary creates a vocabulary for OpenAPI-specific keywords // version determines which keywords are allowed/forbidden func NewOpenAPIVocabulary(version VersionType) *jsonschema.Vocabulary { return NewOpenAPIVocabularyWithCoercion(version, false) } // NewOpenAPIVocabularyWithCoercion creates a vocabulary with optional scalar coercion func NewOpenAPIVocabularyWithCoercion(version VersionType, allowCoercion bool) *jsonschema.Vocabulary { return &jsonschema.Vocabulary{ URL: OpenAPIVocabularyURL, Schema: nil, // We don't validate the vocabulary schema itself Compile: func(ctx *jsonschema.CompilerContext, obj map[string]any) (jsonschema.SchemaExt, error) { return compileOpenAPIKeywords(ctx, obj, version, allowCoercion) }, } } // compileOpenAPIKeywords compiles all OpenAPI-specific keywords found in the schema object func compileOpenAPIKeywords(ctx *jsonschema.CompilerContext, obj map[string]any, version VersionType, allowCoercion bool, ) (jsonschema.SchemaExt, error) { var extensions []jsonschema.SchemaExt if ext, err := CompileNullable(ctx, obj, version); err != nil { return nil, err } else if ext != nil { extensions = append(extensions, ext) } if ext, err := CompileDiscriminator(ctx, obj, version); err != nil { return nil, err } else if ext != nil { extensions = append(extensions, ext) } if ext, err := CompileExample(ctx, obj, version); err != nil { return nil, err } else if ext != nil { extensions = append(extensions, ext) } if ext, err := CompileDeprecated(ctx, obj, version); err != nil { return nil, err } else if ext != nil { extensions = append(extensions, ext) } if ext, err := CompileCoercion(ctx, obj, allowCoercion); err != nil { return nil, err } else if ext != nil { extensions = append(extensions, ext) } if len(extensions) == 0 { return nil, nil } return &combinedExtension{extensions: extensions}, nil } // combinedExtension combines multiple OpenAPI extensions into one type combinedExtension struct { extensions []jsonschema.SchemaExt } func (c *combinedExtension) Validate(ctx *jsonschema.ValidatorContext, v any) { for _, ext := range c.extensions { ext.Validate(ctx, v) } } libopenapi-validator-0.13.8/openapi_vocabulary/vocabulary_test.go000066400000000000000000000475111520534042400253360ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary import ( "strings" "testing" "github.com/santhosh-tekuri/jsonschema/v6" "github.com/stretchr/testify/assert" "golang.org/x/text/message" ) func TestNullableKeyword_Version30_Compilation(t *testing.T) { // Test that nullable: true compiles successfully in OpenAPI 3.0 schemaJSON := `{ "type": "string", "nullable": true }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) // Should compile successfully in 3.0 (validation behavior handled by transformation) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) assert.NotNil(t, compiledSchema) } func TestNullableKeyword_Version30_WithoutNullable(t *testing.T) { // Test that without nullable: true, null values are rejected schemaJSON := `{ "type": "string" }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) // Test null value - should fail without nullable: true err = compiledSchema.Validate(nil) assert.Error(t, err) assert.Contains(t, err.Error(), "got null, want string") // Test string value - should pass err = compiledSchema.Validate("hello") assert.NoError(t, err) } func TestNullableKeyword_Version31_Rejected(t *testing.T) { // Test that nullable keyword is rejected in OpenAPI 3.1 schemaJSON := `{ "type": "string", "nullable": true }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version31)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) // Compilation should fail due to nullable in 3.1 _, err = compiler.Compile("test.json") assert.Error(t, err) assert.Contains(t, err.Error(), "The `nullable` keyword is not supported in OpenAPI 3.1+") } func TestNullableKeyword_InvalidType(t *testing.T) { // Test that nullable must be a boolean schemaJSON := `{ "type": "string", "nullable": "yes" }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) // Compilation should fail due to invalid nullable type _, err = compiler.Compile("test.json") assert.Error(t, err) assert.Contains(t, err.Error(), "nullable must be a boolean value") } func TestDiscriminatorKeyword_ValidStructure(t *testing.T) { // Test discriminator with valid structure schemaJSON := `{ "type": "object", "discriminator": { "propertyName": "type", "mapping": { "dog": "#/components/schemas/Dog", "cat": "#/components/schemas/Cat" } } }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) // Test object with discriminator property testData := map[string]any{ "type": "dog", "name": "Buddy", } err = compiledSchema.Validate(testData) assert.NoError(t, err) } func TestDiscriminatorKeyword_MissingPropertyName(t *testing.T) { // Test discriminator without propertyName schemaJSON := `{ "type": "object", "discriminator": { "mapping": { "dog": "#/components/schemas/Dog" } } }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) // Compilation should fail due to missing propertyName _, err = compiler.Compile("test.json") assert.Error(t, err) assert.Contains(t, err.Error(), "discriminator must have a propertyName field") } func TestExampleKeyword_Valid(t *testing.T) { // Test example keyword with any value schemaJSON := `{ "type": "string", "example": "hello world" }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) assert.NotNil(t, compiledSchema) } func TestDeprecatedKeyword_Valid(t *testing.T) { // Test deprecated keyword with boolean value schemaJSON := `{ "type": "string", "deprecated": true }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) assert.NotNil(t, compiledSchema) } func TestDeprecatedKeyword_InvalidType(t *testing.T) { // Test deprecated keyword with non-boolean value schemaJSON := `{ "type": "string", "deprecated": "yes" }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) // Compilation should fail due to invalid deprecated type _, err = compiler.Compile("test.json") assert.Error(t, err) assert.Contains(t, err.Error(), "deprecated must be a boolean value") } func TestMultipleKeywords_Combined(t *testing.T) { // Test multiple OpenAPI keywords compile successfully in the same schema schemaJSON := `{ "type": "string", "nullable": true, "example": "test value", "deprecated": false }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) // Should compile successfully in 3.0 (actual nullable behavior handled by transformation) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) assert.NotNil(t, compiledSchema) } func TestNoOpenAPIKeywords_NoExtension(t *testing.T) { // Test that schemas without OpenAPI keywords don't get extensions schemaJSON := `{ "type": "string", "minLength": 1 }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) // Should compile successfully even without OpenAPI keywords err = compiledSchema.Validate("hello") assert.NoError(t, err) err = compiledSchema.Validate("") assert.Error(t, err) // Should fail minLength validation } // Additional comprehensive tests func TestNewOpenAPIVocabulary_Version30(t *testing.T) { vocab := NewOpenAPIVocabulary(Version30) assert.NotNil(t, vocab) assert.Equal(t, OpenAPIVocabularyURL, vocab.URL) assert.Nil(t, vocab.Schema) assert.NotNil(t, vocab.Compile) } func TestNewOpenAPIVocabulary_Version31(t *testing.T) { vocab := NewOpenAPIVocabulary(Version31) assert.NotNil(t, vocab) assert.Equal(t, OpenAPIVocabularyURL, vocab.URL) assert.Nil(t, vocab.Schema) assert.NotNil(t, vocab.Compile) } func TestNewOpenAPIVocabularyWithCoercion_Enabled(t *testing.T) { vocab := NewOpenAPIVocabularyWithCoercion(Version30, true) assert.NotNil(t, vocab) assert.Equal(t, OpenAPIVocabularyURL, vocab.URL) assert.Nil(t, vocab.Schema) assert.NotNil(t, vocab.Compile) } func TestNewOpenAPIVocabularyWithCoercion_Disabled(t *testing.T) { vocab := NewOpenAPIVocabularyWithCoercion(Version30, false) assert.NotNil(t, vocab) assert.Equal(t, OpenAPIVocabularyURL, vocab.URL) assert.Nil(t, vocab.Schema) assert.NotNil(t, vocab.Compile) } func TestCompileOpenAPIKeywords_EmptySchema(t *testing.T) { obj := map[string]any{} ext, err := compileOpenAPIKeywords(nil, obj, Version30, false) assert.NoError(t, err) assert.Nil(t, ext) } func TestCompileOpenAPIKeywords_NoOpenAPIKeywords(t *testing.T) { obj := map[string]any{ "type": "string", "minLength": 1, } ext, err := compileOpenAPIKeywords(nil, obj, Version30, false) assert.NoError(t, err) assert.Nil(t, ext) } func TestCompileOpenAPIKeywords_NullableOnly(t *testing.T) { obj := map[string]any{ "type": "string", "nullable": true, } ext, err := compileOpenAPIKeywords(nil, obj, Version30, false) assert.NoError(t, err) // Nullable compilation returns nil because the actual transformation is handled elsewhere // The vocabulary just validates that the keyword is not used in 3.1+ assert.Nil(t, ext) } func TestCompileOpenAPIKeywords_DiscriminatorOnly(t *testing.T) { obj := map[string]any{ "type": "object", "discriminator": map[string]any{ "propertyName": "type", }, } ext, err := compileOpenAPIKeywords(nil, obj, Version30, false) assert.NoError(t, err) assert.NotNil(t, ext) } func TestCompileOpenAPIKeywords_ExampleOnly(t *testing.T) { obj := map[string]any{ "type": "string", "example": "test", } ext, err := compileOpenAPIKeywords(nil, obj, Version30, false) assert.NoError(t, err) assert.NotNil(t, ext) } func TestCompileOpenAPIKeywords_DeprecatedOnly(t *testing.T) { obj := map[string]any{ "type": "string", "deprecated": true, } ext, err := compileOpenAPIKeywords(nil, obj, Version30, false) assert.NoError(t, err) assert.NotNil(t, ext) } func TestCompileOpenAPIKeywords_CoercionOnly(t *testing.T) { obj := map[string]any{ "type": "boolean", } ext, err := compileOpenAPIKeywords(nil, obj, Version30, true) assert.NoError(t, err) assert.NotNil(t, ext) } func TestCompileOpenAPIKeywords_AllKeywordsCombined(t *testing.T) { obj := map[string]any{ "type": "string", "nullable": true, "discriminator": map[string]any{ "propertyName": "type", }, "example": "test", "deprecated": false, } ext, err := compileOpenAPIKeywords(nil, obj, Version30, true) assert.NoError(t, err) assert.NotNil(t, ext) } // Error type tests func TestOpenAPIKeywordError_Error(t *testing.T) { err := &OpenAPIKeywordError{ Keyword: "nullable", Message: "test message", } expected := "OpenAPI keyword 'nullable': test message" assert.Equal(t, expected, err.Error()) } func TestDiscriminatorPropertyMissingError_KeywordPath(t *testing.T) { err := &DiscriminatorPropertyMissingError{ PropertyName: "type", } path := err.KeywordPath() assert.Equal(t, []string{"discriminator"}, path) } func TestDiscriminatorPropertyMissingError_LocalizedString(t *testing.T) { err := &DiscriminatorPropertyMissingError{ PropertyName: "type", } printer := message.NewPrinter(message.MatchLanguage("en")) result := err.LocalizedString(printer) expected := "discriminator property 'type' is missing" assert.Equal(t, expected, result) } func TestDiscriminatorPropertyMissingError_Error(t *testing.T) { err := &DiscriminatorPropertyMissingError{ PropertyName: "type", } expected := "discriminator property 'type' is missing" assert.Equal(t, expected, err.Error()) } func TestCoercionError_KeywordPath(t *testing.T) { err := &CoercionError{ SourceType: "string", TargetType: "boolean", Value: "yes", Message: "invalid value", } path := err.KeywordPath() assert.Equal(t, []string{"type"}, path) } func TestCoercionError_LocalizedString(t *testing.T) { err := &CoercionError{ SourceType: "string", TargetType: "boolean", Value: "yes", Message: "invalid value", } printer := message.NewPrinter(message.MatchLanguage("en")) result := err.LocalizedString(printer) expected := "cannot coerce string 'yes' to boolean: invalid value" assert.Equal(t, expected, result) } func TestCoercionError_Error(t *testing.T) { err := &CoercionError{ SourceType: "string", TargetType: "boolean", Value: "yes", Message: "invalid value", } expected := "cannot coerce string 'yes' to boolean: invalid value" assert.Equal(t, expected, err.Error()) } // Test metadata keywords compilation func TestMetadataKeywords_ExampleCompilation(t *testing.T) { obj := map[string]any{ "type": "string", "example": map[string]any{ "nested": "value", }, } ext, err := CompileExample(nil, obj, Version30) assert.NoError(t, err) assert.NotNil(t, ext) } func TestMetadataKeywords_DeprecatedTrueCompilation(t *testing.T) { obj := map[string]any{ "type": "string", "deprecated": true, } ext, err := CompileDeprecated(nil, obj, Version30) assert.NoError(t, err) assert.NotNil(t, ext) } func TestMetadataKeywords_DeprecatedFalseCompilation(t *testing.T) { obj := map[string]any{ "type": "string", "deprecated": false, } ext, err := CompileDeprecated(nil, obj, Version30) assert.NoError(t, err) assert.NotNil(t, ext) } func TestMetadataKeywords_DeprecatedInvalidType(t *testing.T) { obj := map[string]any{ "type": "string", "deprecated": "invalid", } ext, err := CompileDeprecated(nil, obj, Version30) assert.Error(t, err) assert.Nil(t, ext) assert.Contains(t, err.Error(), "deprecated must be a boolean value") } func TestMetadataKeywords_NoKeywords(t *testing.T) { obj := map[string]any{ "type": "string", } ext, err := CompileExample(nil, obj, Version30) assert.NoError(t, err) assert.Nil(t, ext) ext, err = CompileDeprecated(nil, obj, Version30) assert.NoError(t, err) assert.Nil(t, ext) } // Test discriminator compilation func TestDiscriminatorKeyword_ValidCompilation(t *testing.T) { obj := map[string]any{ "discriminator": map[string]any{ "propertyName": "type", "mapping": map[string]any{ "dog": "#/components/schemas/Dog", }, }, } ext, err := CompileDiscriminator(nil, obj, Version30) assert.NoError(t, err) assert.NotNil(t, ext) } func TestDiscriminatorKeyword_MissingPropertyName_Comprehensive(t *testing.T) { obj := map[string]any{ "discriminator": map[string]any{ "mapping": map[string]any{ "dog": "#/components/schemas/Dog", }, }, } ext, err := CompileDiscriminator(nil, obj, Version30) assert.Error(t, err) assert.Nil(t, ext) assert.Contains(t, err.Error(), "discriminator must have a propertyName field") } func TestDiscriminatorKeyword_PropertyNameNotString(t *testing.T) { obj := map[string]any{ "discriminator": map[string]any{ "propertyName": 123, }, } ext, err := CompileDiscriminator(nil, obj, Version30) assert.Error(t, err) assert.Nil(t, ext) assert.Contains(t, err.Error(), "discriminator propertyName must be a string") } func TestDiscriminatorKeyword_NotObject(t *testing.T) { obj := map[string]any{ "discriminator": "invalid", } ext, err := CompileDiscriminator(nil, obj, Version30) assert.Error(t, err) assert.Nil(t, ext) assert.Contains(t, err.Error(), "discriminator must be an object") } func TestDiscriminatorKeyword_NoDiscriminator(t *testing.T) { obj := map[string]any{ "type": "object", } ext, err := CompileDiscriminator(nil, obj, Version30) assert.NoError(t, err) assert.Nil(t, ext) } // Test end-to-end discriminator validation func TestDiscriminatorValidation_PropertyMissing(t *testing.T) { schemaJSON := `{ "type": "object", "discriminator": { "propertyName": "type" }, "properties": { "name": { "type": "string" } } }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) // Test object missing discriminator property testData := map[string]any{ "name": "test", // Missing "type" property } err = compiledSchema.Validate(testData) assert.Error(t, err) assert.Contains(t, err.Error(), "discriminator property 'type' is missing") } func TestDiscriminatorValidation_PropertyPresent(t *testing.T) { schemaJSON := `{ "type": "object", "discriminator": { "propertyName": "type" }, "properties": { "type": { "type": "string" }, "name": { "type": "string" } } }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) // Test object with discriminator property testData := map[string]any{ "type": "dog", "name": "Buddy", } err = compiledSchema.Validate(testData) assert.NoError(t, err) } func TestDiscriminatorValidation_NonObjectValue(t *testing.T) { schemaJSON := `{ "type": "object", "discriminator": { "propertyName": "type", "mapping": { "dog": "#/components/schemas/Dog" } } }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) compiledSchema, err := compiler.Compile("test.json") assert.NoError(t, err) // Test with non-object (should pass discriminator validation but fail type validation) err = compiledSchema.Validate("not an object") assert.Error(t, err) // Should get type validation error, not discriminator error assert.NotContains(t, err.Error(), "discriminator property") } func TestDiscriminatorValidation_DiscriminatorBadMapping(t *testing.T) { schemaJSON := `{ "type": "object", "discriminator": { "propertyName": "type", "mapping": "not an object" } }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) _, err = compiler.Compile("test.json") assert.Error(t, err) assert.Equal(t, "OpenAPI keyword 'discriminator': discriminator mapping must be an object", err.Error()) } func TestDiscriminatorValidation_BadMappingType(t *testing.T) { schemaJSON := `{ "type": "object", "discriminator": { "propertyName": "type", "mapping": { "fish": 12345 } } }` schema, err := jsonschema.UnmarshalJSON(strings.NewReader(schemaJSON)) assert.NoError(t, err) compiler := jsonschema.NewCompiler() compiler.RegisterVocabulary(NewOpenAPIVocabulary(Version30)) compiler.AssertVocabs() err = compiler.AddResource("test.json", schema) assert.NoError(t, err) _, err = compiler.Compile("test.json") assert.Error(t, err) assert.Equal(t, "OpenAPI keyword 'discriminator': discriminator mapping values must be strings", err.Error()) } libopenapi-validator-0.13.8/parameters/000077500000000000000000000000001520534042400200525ustar00rootroot00000000000000libopenapi-validator-0.13.8/parameters/cookie_parameters.go000066400000000000000000000153421520534042400241020ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package parameters import ( "fmt" "net/http" "strconv" "strings" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" "github.com/pb33f/libopenapi-validator/strict" ) func (v *paramValidator) ValidateCookieParams(request *http.Request) (bool, []*errors.ValidationError) { pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options) if len(errs) > 0 { return false, errs } return v.ValidateCookieParamsWithPathItem(request, pathItem, foundPath) } func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { if pathItem == nil { return false, []*errors.ValidationError{{ ValidationType: helpers.PathValidation, ValidationSubType: helpers.ValidationMissing, Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ "however that path, or the %s method for that path does not exist in the specification", request.Method, request.URL.Path, request.Method), SpecLine: -1, SpecCol: -1, HowToFix: errors.HowToFixPath, }} } // extract params for the operation params := helpers.ExtractParamsForOperation(request, pathItem) var validationErrors []*errors.ValidationError operation := strings.ToLower(request.Method) // build a map of cookies from the request for efficient lookup cookieMap := make(map[string]*http.Cookie) for _, cookie := range request.Cookies() { cookieMap[cookie.Name] = cookie } for _, p := range params { if p.In == helpers.Cookie { // look up the cookie by name (cookies are case-sensitive) cookie, found := cookieMap[p.Name] if !found { // cookie not present in request - check if required if p.Required != nil && *p.Required { validationErrors = append(validationErrors, errors.CookieParameterMissing(p, pathValue, operation, "")) } continue } var sch *base.Schema if p.Schema != nil { sch = p.Schema.Schema() } // Get rendered schema for ReferenceSchema field in errors (uses cache if available) renderedSchema := GetRenderedSchema(sch, v.options) pType := sch.Type for _, ty := range pType { switch ty { case helpers.Integer: if _, err := strconv.ParseInt(cookie.Value, 10, 64); err != nil { validationErrors = append(validationErrors, errors.InvalidCookieParamInteger(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) break } // validate value matches allowed enum values if sch.Enum != nil { matchFound := false for _, enumVal := range sch.Enum { if strings.TrimSpace(cookie.Value) == fmt.Sprint(enumVal.Value) { matchFound = true break } } if !matchFound { validationErrors = append(validationErrors, errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) } } case helpers.Number: if _, err := strconv.ParseFloat(cookie.Value, 64); err != nil { validationErrors = append(validationErrors, errors.InvalidCookieParamNumber(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) break } // validate value matches allowed enum values if sch.Enum != nil { matchFound := false for _, enumVal := range sch.Enum { if strings.TrimSpace(cookie.Value) == fmt.Sprint(enumVal.Value) { matchFound = true break } } if !matchFound { validationErrors = append(validationErrors, errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) } } case helpers.Boolean: if _, err := strconv.ParseBool(cookie.Value); err != nil { validationErrors = append(validationErrors, errors.IncorrectCookieParamBool(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) } case helpers.Object: if !p.IsExploded() { encodedObj := helpers.ConstructMapFromCSVWithSchema(cookie.Value, sch) // if a schema was extracted if sch != nil { validationErrors = append(validationErrors, ValidateParameterSchema(sch, encodedObj, "", "Cookie parameter", "The cookie parameter", p.Name, helpers.ParameterValidation, helpers.ParameterValidationQuery, v.options)...) } } case helpers.Array: if !p.IsExploded() { // well we're already in an array, so we need to check the items schema // to ensure this array items matches the type // only check if items is a schema, not a boolean if sch.Items.IsA() { validationErrors = append(validationErrors, ValidateCookieArray(sch, p, cookie.Value, pathValue, operation, renderedSchema)...) } } case helpers.String: // check if the schema has an enum, and if so, match the value against one of // the defined enum values. if sch.Enum != nil { matchFound := false for _, enumVal := range sch.Enum { if strings.TrimSpace(cookie.Value) == fmt.Sprint(enumVal.Value) { matchFound = true break } } if !matchFound { validationErrors = append(validationErrors, errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) break } } validationErrors = append(validationErrors, ValidateSingleParameterSchema( sch, cookie.Value, "Cookie parameter", "The cookie parameter", p.Name, helpers.ParameterValidation, helpers.ParameterValidationCookie, v.options, pathValue, operation, )...) } } } } errors.PopulateValidationErrors(validationErrors, request, pathValue) if len(validationErrors) > 0 { return false, validationErrors } // strict mode: check for undeclared cookies if v.options.StrictMode { undeclaredCookies := strict.ValidateCookies(request, params, v.options) for _, undeclared := range undeclaredCookies { validationErrors = append(validationErrors, errors.UndeclaredCookieError( undeclared.Path, undeclared.Name, undeclared.Value, undeclared.DeclaredProperties, request.URL.Path, request.Method, )) } } if len(validationErrors) > 0 { return false, validationErrors } return true, nil } libopenapi-validator-0.13.8/parameters/cookie_parameters_test.go000066400000000000000000001231341520534042400251400ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package parameters import ( "net/http" "sync" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" ) func TestNewValidator_CookieNoPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/I/do/not/exist", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "1"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "", errors[0].SpecPath) } func TestNewValidator_CookieParamNumberValid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "1"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamNumberValidFloat(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "123.455"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamNumberInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "false"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Convert the value 'false' into a number", errors[0].HowToFix) } func TestNewValidator_CookieParamIntegerValid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "1"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamIntegerInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "false"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Convert the value 'false' into an integer", errors[0].HowToFix) } func TestNewValidator_CookieParamBooleanValid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "true"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamEnumValidString(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: string enum: - beef - chicken - pea protein` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "chicken"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamEnumInvalidString(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: string enum: - beef - chicken - pea protein` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "milk"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Instead of 'milk', use one of the allowed values: 'beef, chicken, pea protein'", errors[0].HowToFix) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "/burgers/beef", errors[0].SpecPath) } func TestNewValidator_CookieParamBooleanInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "12345"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Convert the value '12345' into a true/false value", errors[0].HowToFix) } func TestNewValidator_CookieParamObjectValid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true explode: false schema: type: object properties: pink: type: boolean number: type: number required: [pink, number]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "pink,true,number,2"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamObjectInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true explode: false schema: type: object properties: pink: type: boolean number: type: number required: [pink, number]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "pink,2,number,2"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "got number, want boolean", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_CookieParamArrayValidNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: array items: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2,3,4"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamArrayInvalidNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: array items: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2,true,4,'hello'"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) assert.Len(t, errors, 2) } func TestNewValidator_CookieParamArrayValidInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: array items: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2,3,4"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamArrayInvalidInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: array items: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2,true,4,'hello'"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) assert.Len(t, errors, 2) } func TestNewValidator_CookieParamArrayValidBoolean(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: array items: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "true,false,true,false,true"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamArrayString(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: array items: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "true,1,hey,ho"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamArrayInvalidBoolean(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: array items: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "true,false,pb33f,false,99.99"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) assert.Len(t, errors, 2) } func TestNewValidator_CookieParamArrayInvalidBooleanZeroOne(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: array items: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "true,false,0,false,1"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) assert.Len(t, errors, 2) } func TestNewValidator_CookieParamArrayValidIntegerEnum(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: integer enum: [1, 2, 99]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamArrayInvalidIntegerEnum(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: integer enum: [1, 2, 99]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2500"}) // too many dude. valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Instead of '2500', use one of the allowed values: '1, 2, 99'", errors[0].HowToFix) } func TestNewValidator_CookieParamArrayValidNumberEnum(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number enum: [1.2, 2.3, 99.0]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2.3"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamArrayInvalidNumberEnum(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number enum: [1.2, 2.3, 99.1]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2500"}) // too many dude. valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Instead of '2500', use one of the allowed values: '1.2, 2.3, 99.1'", errors[0].HowToFix) } func TestNewValidator_PresetPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: integer enum: [1, 2, 99]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2500"}) // too many dude. // preset the path path, _, pv := paths.FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) valid, errors := v.ValidateCookieParamsWithPathItem(request, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Instead of '2500', use one of the allowed values: '1, 2, 99'", errors[0].HowToFix) } func TestNewValidator_PresetPath_notfound(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: integer enum: [1, 2, 99]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/pizza/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2500"}) // too many dude. // preset the path path, _, pv := paths.FindPath(request, &m.Model, nil) valid, errors := v.ValidateCookieParamsWithPathItem(request, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "GET Path '/pizza/beef' not found", errors[0].Message) } // Tests for required cookie validation (GitHub issue #183) func TestNewValidator_CookieRequiredMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) // No cookie added - this should fail validation valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "Cookie parameter 'PattyPreference' is missing", errors[0].Message) assert.Equal(t, "The cookie parameter 'PattyPreference' is defined as being required, "+ "however it's missing from the request", errors[0].Reason) assert.Equal(t, helpers.ParameterValidation, errors[0].ValidationType) assert.Equal(t, helpers.ParameterValidationCookie, errors[0].ValidationSubType) } func TestNewValidator_CookieOptionalMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: false schema: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) // No cookie added - this should pass validation since it's optional valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieOptionalMissingNoRequiredField(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie schema: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) // No cookie added - this should pass validation since required defaults to false valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieMultipleRequiredOneMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number - name: BunType in: cookie required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) // Only add one cookie request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "1.5"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "Cookie parameter 'BunType' is missing", errors[0].Message) } func TestNewValidator_CookieMultipleRequiredBothMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number - name: BunType in: cookie required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) // No cookies added valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 2) } func TestNewValidator_CookieMultipleRequiredAllPresent(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number - name: BunType in: cookie required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "1.5"}) request.AddCookie(&http.Cookie{Name: "BunType", Value: "sesame"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieCaseSensitive(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) // Add cookie with different case - should not match request.AddCookie(&http.Cookie{Name: "pattypreference", Value: "1.5"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "Cookie parameter 'PattyPreference' is missing", errors[0].Message) } func TestNewValidator_CookieRequiredWithInvalidValue(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "not-a-number"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) // Should be a type error, not a missing error assert.Contains(t, errors[0].Message, "not a valid number") } func TestNewValidator_CookieMixedRequiredOptional(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number - name: ExtraCheese in: cookie required: false schema: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) // Only add the required cookie request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2.5"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieRequiredIntegerMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyCount in: cookie required: true schema: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "Cookie parameter 'PattyCount' is missing", errors[0].Message) } func TestNewValidator_CookieRequiredBooleanMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: ExtraCheese in: cookie required: true schema: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "Cookie parameter 'ExtraCheese' is missing", errors[0].Message) } func TestNewValidator_CookieRequiredStringMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: CustomerName in: cookie required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "Cookie parameter 'CustomerName' is missing", errors[0].Message) } func TestNewValidator_CookieRequiredArrayMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: Toppings in: cookie required: true schema: type: array items: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "Cookie parameter 'Toppings' is missing", errors[0].Message) } func TestNewValidator_CookieRequiredObjectMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: Preferences in: cookie required: true explode: false schema: type: object properties: pink: type: boolean number: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "Cookie parameter 'Preferences' is missing", errors[0].Message) } func TestNewValidator_CookieRequiredWithPathItem(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: PattyPreference in: cookie required: true schema: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) // No cookie added // Use the WithPathItem variant path, _, pv := paths.FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) valid, errors := v.ValidateCookieParamsWithPathItem(request, path, pv) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "Cookie parameter 'PattyPreference' is missing", errors[0].Message) } // Tests for string schema validation (GitHub issue #184) func TestNewValidator_CookieParamStringValidPattern(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: SessionID in: cookie required: true schema: type: string pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "SessionID", Value: "550e8400-e29b-41d4-a716-446655440000"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamStringInvalidPattern(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: SessionID in: cookie required: true schema: type: string pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "SessionID", Value: "invalid_value"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "does not match pattern") } func TestNewValidator_CookieParamStringValidFormat(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: SessionID in: cookie required: true schema: type: string format: uuid` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "SessionID", Value: "550e8400-e29b-41d4-a716-446655440000"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamStringInvalidFormat(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: SessionID in: cookie required: true schema: type: string format: uuid` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "SessionID", Value: "not-a-valid-uuid"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "uuid") } func TestNewValidator_CookieParamStringValidMinLength(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: Token in: cookie required: true schema: type: string minLength: 10` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "Token", Value: "abcdefghij"}) // exactly 10 chars valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamStringInvalidMinLength(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: Token in: cookie required: true schema: type: string minLength: 10` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "Token", Value: "short"}) // only 5 chars valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "minLength") } func TestNewValidator_CookieParamStringValidMaxLength(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: Token in: cookie required: true schema: type: string maxLength: 10` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "Token", Value: "short"}) // 5 chars valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamStringInvalidMaxLength(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: Token in: cookie required: true schema: type: string maxLength: 10` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "Token", Value: "this-is-way-too-long"}) // 20 chars valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "maxLength") } func TestNewValidator_CookieParamStringValidPatternAndMinMaxLength(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: Code in: cookie required: true schema: type: string pattern: '^[A-Z]+$' minLength: 3 maxLength: 10` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "Code", Value: "ABCDEF"}) // 6 chars, all uppercase valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamStringInvalidPatternButValidLength(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: Code in: cookie required: true schema: type: string pattern: '^[A-Z]+$' minLength: 3 maxLength: 10` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "Code", Value: "abcdef"}) // 6 chars, but lowercase - fails pattern valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "does not match pattern") } func TestNewValidator_CookieParamStringEmailFormat(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: UserEmail in: cookie required: true schema: type: string format: email` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "UserEmail", Value: "user@example.com"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_CookieParamStringInvalidEmailFormat(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: UserEmail in: cookie required: true schema: type: string format: email` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "UserEmail", Value: "not-an-email"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "email") } func TestNewValidator_CookieParamMissingRequired(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: session_id in: cookie required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Create request WITHOUT the required cookie request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "Cookie parameter 'session_id' is missing", errors[0].Message) assert.Contains(t, errors[0].Reason, "required") } func TestNewValidator_CookieParams_StrictMode_UndeclaredCookie(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: session_id in: cookie required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "session_id", Value: "abc123"}) request.AddCookie(&http.Cookie{Name: "extra_cookie", Value: "undeclared"}) valid, errors := v.ValidateCookieParams(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "extra_cookie") assert.Contains(t, errors[0].Message, "not declared") } func TestNewValidator_CookieParams_StrictMode_ValidRequest(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: session_id in: cookie required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.AddCookie(&http.Cookie{Name: "session_id", Value: "abc123"}) valid, errors := v.ValidateCookieParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } libopenapi-validator-0.13.8/parameters/header_parameters.go000066400000000000000000000202601520534042400240540ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package parameters import ( "fmt" "net/http" "strconv" "strings" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" lowbase "github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" "github.com/pb33f/libopenapi-validator/strict" ) func (v *paramValidator) ValidateHeaderParams(request *http.Request) (bool, []*errors.ValidationError) { pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options) if len(errs) > 0 { return false, errs } return v.ValidateHeaderParamsWithPathItem(request, pathItem, foundPath) } func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { if pathItem == nil { return false, []*errors.ValidationError{{ ValidationType: helpers.PathValidation, ValidationSubType: helpers.ValidationMissing, Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ "however that path, or the %s method for that path does not exist in the specification", request.Method, request.URL.Path, request.Method), SpecLine: -1, SpecCol: -1, HowToFix: errors.HowToFixPath, }} } // extract params for the operation params := helpers.ExtractParamsForOperation(request, pathItem) var validationErrors []*errors.ValidationError seenHeaders := make(map[string]bool) operation := strings.ToLower(request.Method) for _, p := range params { if p.In == helpers.Header { seenHeaders[strings.ToLower(p.Name)] = true if param := request.Header.Get(p.Name); param != "" { var sch *base.Schema if p.Schema != nil { sch = p.Schema.Schema() } // Get rendered schema for ReferenceSchema field in errors (uses cache if available) renderedSchema := GetRenderedSchema(sch, v.options) pType := sch.Type for _, ty := range pType { switch ty { case helpers.Integer: if _, err := strconv.ParseInt(param, 10, 64); err != nil { validationErrors = append(validationErrors, errors.InvalidHeaderParamInteger(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) break } // check if the param is within the enum if sch.Enum != nil { matchFound := false for _, enumVal := range sch.Enum { if strings.TrimSpace(param) == fmt.Sprint(enumVal.Value) { matchFound = true break } } if !matchFound { validationErrors = append(validationErrors, errors.IncorrectHeaderParamEnum(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) } } case helpers.Number: if _, err := strconv.ParseFloat(param, 64); err != nil { validationErrors = append(validationErrors, errors.InvalidHeaderParamNumber(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) break } // check if the param is within the enum if sch.Enum != nil { matchFound := false for _, enumVal := range sch.Enum { if strings.TrimSpace(param) == fmt.Sprint(enumVal.Value) { matchFound = true break } } if !matchFound { validationErrors = append(validationErrors, errors.IncorrectHeaderParamEnum(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) } } case helpers.Boolean: if _, err := strconv.ParseBool(param); err != nil { validationErrors = append(validationErrors, errors.IncorrectHeaderParamBool(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) } case helpers.Object: // check if the header is default encoded or not var encodedObj map[string]interface{} // we have found our header, check the explode type. if p.IsDefaultHeaderEncoding() { encodedObj = helpers.ConstructMapFromCSVWithSchema(param, sch) } else { if p.IsExploded() { // only option is to be exploded for KV extraction. encodedObj = helpers.ConstructKVFromCSVWithSchema(param, sch) } } if len(encodedObj) == 0 { validationErrors = append(validationErrors, errors.HeaderParameterCannotBeDecoded(p, strings.ToLower(param), pathValue, operation, renderedSchema)) break } // if a schema was extracted if sch != nil { validationErrors = append(validationErrors, ValidateParameterSchema(sch, encodedObj, "", "Header parameter", "The header parameter", p.Name, helpers.ParameterValidation, helpers.ParameterValidationQuery, v.options)...) } case helpers.Array: if !p.IsExploded() { // only unexploded arrays are supported for cookie params if sch.Items.IsA() { validationErrors = append(validationErrors, ValidateHeaderArray(sch, p, param, pathValue, operation, renderedSchema)...) } } case helpers.String: // check if the schema has an enum, and if so, match the value against one of // the defined enum values. if sch.Enum != nil { matchFound := false for _, enumVal := range sch.Enum { if strings.TrimSpace(param) == fmt.Sprint(enumVal.Value) { matchFound = true break } } if !matchFound { validationErrors = append(validationErrors, errors.IncorrectHeaderParamEnum(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) break } } validationErrors = append(validationErrors, ValidateSingleParameterSchema( sch, param, "Header parameter", "The header parameter", p.Name, helpers.ParameterValidation, helpers.ParameterValidationHeader, v.options, pathValue, operation, )...) } } if len(pType) == 0 { // validate schema as there is no type information. validationErrors = append(validationErrors, ValidateSingleParameterSchema(sch, param, p.Name, lowbase.SchemaLabel, p.Name, helpers.ParameterValidation, helpers.ParameterValidationHeader, v.options, pathValue, operation)...) } } else { if p.Required != nil && *p.Required { // Get rendered schema for missing required parameter (uses cache if available) var renderedSchema string if p.Schema != nil { renderedSchema = GetRenderedSchema(p.Schema.Schema(), v.options) } validationErrors = append(validationErrors, errors.HeaderParameterMissing(p, pathValue, operation, renderedSchema)) } } } } errors.PopulateValidationErrors(validationErrors, request, pathValue) if len(validationErrors) > 0 { return false, validationErrors } // strict mode: check for undeclared headers if v.options.StrictMode { // Extract security headers applicable to this operation var securityHeaders []string if v.document.Components != nil && v.document.Components.SecuritySchemes != nil { security := helpers.EffectiveSecurityForOperation(request, pathItem, v.document.Security) // Convert orderedmap to regular map for the helper schemesMap := make(map[string]*v3.SecurityScheme) for pair := v.document.Components.SecuritySchemes.First(); pair != nil; pair = pair.Next() { schemesMap[pair.Key()] = pair.Value() } securityHeaders = helpers.ExtractSecurityHeaderNames(security, schemesMap) } undeclaredHeaders := strict.ValidateRequestHeaders(request.Header, params, securityHeaders, v.options) for _, undeclared := range undeclaredHeaders { validationErrors = append(validationErrors, errors.UndeclaredHeaderError( undeclared.Name, undeclared.Value.(string), undeclared.DeclaredProperties, undeclared.Direction.String(), request.URL.Path, request.Method, )) } } if len(validationErrors) > 0 { return false, validationErrors } return true, nil } libopenapi-validator-0.13.8/parameters/header_parameters_test.go000066400000000000000000001177171520534042400251310ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package parameters import ( "net/http" "sync" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/paths" ) func TestNewValidator_HeaderParamMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /bish/bosh: get: parameters: - name: bash in: header required: true schema: type: string ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/bish/bosh", nil) valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "Header parameter 'bash' is missing", errors[0].Message) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "/bish/bosh", errors[0].SpecPath) } func TestNewValidator_HeaderPathMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /bish/bosh: get: parameters: - name: bash in: header required: true schema: type: string ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/I/do/not/exist", nil) valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "GET Path '/I/do/not/exist' not found", errors[0].Message) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "", errors[0].SpecPath) } func TestNewValidator_HeaderParamDefaultEncoding_InvalidParamTypeInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "two") // headers are case-insensitive valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "Header parameter 'coffeeCups' is not a valid integer", errors[0].Message) } func TestNewValidator_HeaderParamDefaultEncoding_InvalidParamTypeNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "two") // headers are case-insensitive valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "Header parameter 'coffeeCups' is not a valid number", errors[0].Message) } func TestNewValidator_HeaderParamDefaultEncoding_InvalidParamTypeBoolean(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "two") // headers are case-insensitive valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "Header parameter 'coffeeCups' is not a valid boolean", errors[0].Message) } func TestNewValidator_HeaderParamDefaultEncoding_InvalidParamTypeObjectInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: object properties: milk: type: boolean sugar: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "I am not an object") // headers are case-insensitive valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "Header parameter 'coffeeCups' cannot be decoded", errors[0].Message) } func TestNewValidator_HeaderParamDefaultEncoding_InvalidParamTypeObjectInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: object properties: milk: type: integer sugar: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "milk,true,sugar,true") // default encoding. valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "got boolean, want integer", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_HeaderParamDefaultEncoding_InvalidParamTypeObjectNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: object properties: milk: type: number sugar: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "milk,true,sugar,true") // default encoding. valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "got boolean, want number", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_HeaderParamDefaultEncoding_ValidParamTypeObjectBoolean(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: object properties: milk: type: number sugar: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "milk,123,sugar,true") // default encoding. valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamInvalidSimpleEncoding(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true explode: false schema: type: object properties: milk: type: number sugar: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "milk,123,sugar,true") // default encoding. valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamNonDefaultEncoding_ValidParamTypeObject(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true explode: true schema: type: object properties: milk: type: number sugar: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "milk=123,sugar=true") // default encoding. valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamNonDefaultEncoding_InvalidParamTypeObjectNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true explode: true schema: type: object properties: milk: type: number sugar: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "milk=true,sugar=true") // default encoding. valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "got boolean, want number", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_HeaderParamNonDefaultEncoding_InvalidParamTypeObjectInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true explode: true schema: type: object properties: milk: type: integer sugar: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "milk=true,sugar=true") // default encoding. valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "got boolean, want integer", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_HeaderParamNonDefaultEncoding_ValidParamTypeArrayString(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: array items: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "1,2,3,4,5") // default encoding. valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamNonDefaultEncoding_ValidParamTypeArrayNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: array items: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "1.22,2.33,3.44,4.55,5.66") // default encoding. valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamNonDefaultEncoding_ValidParamTypeArrayInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: array items: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "1,2,3,4,5") // default encoding. valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamNonDefaultEncoding_ValidParamTypeArrayBool(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: array items: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "true,false,true,false,true") // default encoding. valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamNonDefaultEncoding_InvalidParamTypeArrayNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: array items: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "true,false,true,false,true") // default encoding. valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 5) } func TestNewValidator_HeaderParamNonDefaultEncoding_InvalidParamTypeArrayBool(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: array items: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "1,false,2,true,5,false") // default encoding. valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 3) } func TestNewValidator_HeaderParamStringValidEnum(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: string enum: [glass, china, thermos]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "glass") valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamStringInvalidEnum(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: string enum: [glass, china, thermos]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "microwave") // this is not a cup! valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Instead of 'microwave', "+ "use one of the allowed values: 'glass, china, thermos'", errors[0].HowToFix) } func TestNewValidator_HeaderParamIntegerValidEnum(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: integer enum: [1,2,99]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "2") valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamNumberInvalidEnum(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: number enum: [1.2,2.3,99.8]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "1200.3") // that's a lot of cups dude, we only have one dishwasher. valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Instead of '1200.3', "+ "use one of the allowed values: '1.2, 2.3, 99.8'", errors[0].HowToFix) } func TestNewValidator_HeaderParamIntegerInvalidEnum(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: integer enum: [1,2,99]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "1200") // that's a lot of cups dude, we only have one dishwasher. valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Instead of '1200', "+ "use one of the allowed values: '1, 2, 99'", errors[0].HowToFix) } func TestNewValidator_HeaderParamSetPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: integer enum: [1,2,99]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) request.Header.Set("coffeecups", "1200") // that's a lot of cups dude, we only have one dishwasher. // preset the path path, _, pv := paths.FindPath(request, &m.Model, nil) valid, errors := v.ValidateHeaderParamsWithPathItem(request, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Instead of '1200', "+ "use one of the allowed values: '1, 2, 99'", errors[0].HowToFix) } func TestNewValidator_HeaderParamSetPath_notfound(t *testing.T) { spec := `openapi: 3.1.0 paths: /vending/drinks: get: parameters: - name: coffeeCups in: header required: true schema: type: integer enum: [1,2,99]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/buying/drinks", nil) request.Header.Set("coffeecups", "1200") // that's a lot of cups dude, we only have one dishwasher. // preset the path path, _, pv := paths.FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) valid, errors := v.ValidateHeaderParamsWithPathItem(request, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "GET Path '/buying/drinks' not found", errors[0].Message) } func TestNewValidator_HeaderParamStringValidPattern(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-Request-ID in: header required: true schema: type: string pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-Request-ID", "550e8400-e29b-41d4-a716-446655440000") valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamStringInvalidPattern(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-Request-ID in: header required: true schema: type: string pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-Request-ID", "invalid_value") valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "does not match pattern") } func TestNewValidator_HeaderParamStringValidFormat(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-Request-ID in: header required: true schema: type: string format: uuid` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-Request-ID", "550e8400-e29b-41d4-a716-446655440000") valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamStringInvalidFormat(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-Request-ID in: header required: true schema: type: string format: uuid` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-Request-ID", "not-a-valid-uuid") valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "uuid") } func TestNewValidator_HeaderParamStringValidMinLength(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-Token in: header required: true schema: type: string minLength: 10` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-Token", "abcdefghij") // exactly 10 chars valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamStringInvalidMinLength(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-Token in: header required: true schema: type: string minLength: 10` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-Token", "short") // only 5 chars valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "minLength") } func TestNewValidator_HeaderParamStringValidMaxLength(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-Token in: header required: true schema: type: string maxLength: 10` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-Token", "short") // 5 chars valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamStringInvalidMaxLength(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-Token in: header required: true schema: type: string maxLength: 10` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-Token", "this-is-way-too-long") // 20 chars valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "maxLength") } func TestNewValidator_HeaderParamStringValidPatternAndMinMaxLength(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-Code in: header required: true schema: type: string pattern: '^[A-Z]+$' minLength: 3 maxLength: 10` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-Code", "ABCDEF") // 6 chars, all uppercase valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamStringInvalidPatternButValidLength(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-Code in: header required: true schema: type: string pattern: '^[A-Z]+$' minLength: 3 maxLength: 10` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-Code", "abcdef") // 6 chars, but lowercase - fails pattern valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "does not match pattern") } func TestNewValidator_HeaderParamStringValidEnumAndPattern(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-Status in: header required: true schema: type: string enum: [ACTIVE, INACTIVE, PENDING]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-Status", "ACTIVE") valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamStringEmailFormat(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-User-Email in: header required: true schema: type: string format: email` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-User-Email", "user@example.com") valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParamStringInvalidEmailFormat(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-User-Email in: header required: true schema: type: string format: email` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-User-Email", "not-an-email") valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "email") } func TestNewValidator_HeaderParams_StrictMode_UndeclaredHeader(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-Request-Id in: header required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-Request-Id", "abc123") request.Header.Set("X-Undeclared-Header", "should fail") valid, errors := v.ValidateHeaderParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "X-Undeclared-Header") assert.Contains(t, errors[0].Message, "not declared") } func TestNewValidator_HeaderParams_StrictMode_ValidRequest(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/beef: get: parameters: - name: X-Request-Id in: header required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/beef", nil) request.Header.Set("X-Request-Id", "abc123") valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParams_StrictMode_SecuritySchemeApiKeyHeader(t *testing.T) { // Test that apiKey security scheme headers are recognized in strict mode spec := `openapi: 3.1.0 info: title: Test API version: "1.0" paths: /secure/resource: get: security: - ApiKeyAuth: [] responses: "200": description: OK components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/secure/resource", nil) request.Header.Set("X-API-Key", "my-secret-key") valid, errors := v.ValidateHeaderParams(request) // X-API-Key should be recognized as a valid header due to security scheme assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParams_StrictMode_SecuritySchemeApiKeyHeader_CaseInsensitive(t *testing.T) { // Test that apiKey security scheme header matching is case-insensitive spec := `openapi: 3.1.0 info: title: Test API version: "1.0" paths: /secure/resource: get: security: - ApiKeyAuth: [] responses: "200": description: OK components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-KEY` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/secure/resource", nil) request.Header.Set("x-api-key", "my-secret-key") // lowercase in request valid, errors := v.ValidateHeaderParams(request) // x-api-key should match X-API-KEY case-insensitively assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParams_StrictMode_SecuritySchemeNotApplied(t *testing.T) { // Test that security scheme headers are NOT recognized if the scheme is not applied to the operation spec := `openapi: 3.1.0 info: title: Test API version: "1.0" paths: /public/resource: get: # No security defined for this operation responses: "200": description: OK components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/public/resource", nil) request.Header.Set("X-API-Key", "my-secret-key") valid, errors := v.ValidateHeaderParams(request) // X-API-Key should be flagged as undeclared since the security scheme is not applied to this operation assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "X-Api-Key") assert.Contains(t, errors[0].Message, "not declared") } func TestNewValidator_HeaderParams_StrictMode_MultipleSecurity_OR(t *testing.T) { // Test multiple security options (OR logic) - any header is valid spec := `openapi: 3.1.0 info: title: Test API version: "1.0" paths: /secure/resource: get: security: - ApiKeyAuth: [] - BearerAuth: [] responses: "200": description: OK components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key BearerAuth: type: http scheme: bearer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) // Request with X-API-Key only request, _ := http.NewRequest(http.MethodGet, "https://things.com/secure/resource", nil) request.Header.Set("X-API-Key", "my-key") valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) // Request with both (both should be allowed) request2, _ := http.NewRequest(http.MethodGet, "https://things.com/secure/resource", nil) request2.Header.Set("X-API-Key", "my-key") request2.Header.Set("Authorization", "Bearer token") // Authorization is in default ignored headers anyway valid2, errors2 := v.ValidateHeaderParams(request2) assert.True(t, valid2) assert.Len(t, errors2, 0) } func TestNewValidator_HeaderParams_StrictMode_ApiKeyQuery_NotHeader(t *testing.T) { // Test that apiKey with in:query does NOT add a header allowance spec := `openapi: 3.1.0 info: title: Test API version: "1.0" paths: /secure/resource: get: security: - ApiKeyQuery: [] responses: "200": description: OK components: securitySchemes: ApiKeyQuery: type: apiKey in: query name: api_key` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/secure/resource", nil) request.Header.Set("X-Unknown", "value") // No security headers expected valid, errors := v.ValidateHeaderParams(request) // X-Unknown should be flagged assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "X-Unknown") } func TestNewValidator_HeaderParams_StrictMode_CombinedParamsAndSecurity(t *testing.T) { // Test that both params and security scheme headers are recognized spec := `openapi: 3.1.0 info: title: Test API version: "1.0" paths: /secure/resource: get: security: - ApiKeyAuth: [] parameters: - name: X-Request-Id in: header required: true schema: type: string responses: "200": description: OK components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/secure/resource", nil) request.Header.Set("X-Request-Id", "123") request.Header.Set("X-API-Key", "my-key") valid, errors := v.ValidateHeaderParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParams_StrictMode_NoComponents(t *testing.T) { // Test that validation works when there are no components/securitySchemes spec := `openapi: 3.1.0 info: title: Test API version: "1.0" paths: /resource: get: responses: "200": description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/resource", nil) request.Header.Set("X-Custom", "value") valid, errors := v.ValidateHeaderParams(request) // X-Custom should be flagged as undeclared assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "X-Custom") } func TestNewValidator_HeaderParams_StrictMode_GlobalSecurityScheme(t *testing.T) { // Global security with HTTP basic auth — Authorization header should not trigger undeclared error in strict mode spec := `openapi: 3.1.0 info: title: Test API version: "1.0" paths: /secure/resource: get: responses: "200": description: OK security: - BasicAuth: [] components: securitySchemes: BasicAuth: type: http scheme: basic` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/secure/resource", nil) request.Header.Set("Authorization", "Basic dXNlcjpwYXNz") valid, errors := v.ValidateHeaderParams(request) // Authorization should be recognized as a valid header via global security assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_HeaderParams_StrictMode_GlobalSecurityApiKey(t *testing.T) { // Global security with apiKey in header — X-API-Key should not trigger undeclared error in strict mode spec := `openapi: 3.1.0 info: title: Test API version: "1.0" paths: /secure/resource: get: responses: "200": description: OK security: - ApiKeyAuth: [] components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/secure/resource", nil) request.Header.Set("X-API-Key", "my-secret-key") valid, errors := v.ValidateHeaderParams(request) // X-API-Key should be recognized as a valid header via global security assert.True(t, valid) assert.Len(t, errors, 0) } libopenapi-validator-0.13.8/parameters/package.go000066400000000000000000000004361520534042400217770ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT // Package parameters contains all the logic, models and interfaces for validating OpenAPI 3+ Parameters. // Cookie, Header, Path and Query parameters are all validated. package parameters libopenapi-validator-0.13.8/parameters/parameters.go000066400000000000000000000112461520534042400225500ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package parameters import ( "net/http" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" ) // ParameterValidator is an interface that defines the methods for validating parameters // There are 4 types of parameters: query, header, cookie and path. // // ValidateQueryParams will validate the query parameters for the request // ValidateHeaderParams will validate the header parameters for the request // ValidateCookieParamsWithPathItem will validate the cookie parameters for the request // ValidatePathParams will validate the path parameters for the request // // Each method accepts an *http.Request and returns true if validation passed, // false if validation failed and a slice of ValidationError pointers. type ParameterValidator interface { // ValidateQueryParams accepts an *http.Request and validates the query parameters against the OpenAPI specification. // The method will locate the correct path, and operation, based on the verb. The parameters for the operation // will be matched and validated against what has been supplied in the http.Request query string. ValidateQueryParams(request *http.Request) (bool, []*errors.ValidationError) // ValidateQueryParamsWithPathItem accepts an *http.Request and validates the query parameters against the OpenAPI specification. // The method will locate the correct path, and operation, based on the verb. The parameters for the operation // will be matched and validated against what has been supplied in the http.Request query string. ValidateQueryParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) // ValidateHeaderParams validates the header parameters contained within *http.Request. It returns a boolean // stating true if validation passed (false for failed), and a slice of errors if validation failed. ValidateHeaderParams(request *http.Request) (bool, []*errors.ValidationError) // ValidateHeaderParamsWithPathItem validates the header parameters contained within *http.Request. It returns a boolean // stating true if validation passed (false for failed), and a slice of errors if validation failed. ValidateHeaderParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) // ValidateCookieParams validates the cookie parameters contained within *http.Request. // It returns a boolean stating true if validation passed (false for failed), and a slice of errors if validation failed. ValidateCookieParams(request *http.Request) (bool, []*errors.ValidationError) // ValidateCookieParamsWithPathItem validates the cookie parameters contained within *http.Request. // It returns a boolean stating true if validation passed (false for failed), and a slice of errors if validation failed. ValidateCookieParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) // ValidatePathParams validates the path parameters contained within *http.Request. It returns a boolean stating true // if validation passed (false for failed), and a slice of errors if validation failed. ValidatePathParams(request *http.Request) (bool, []*errors.ValidationError) // ValidatePathParamsWithPathItem validates the path parameters contained within *http.Request. It returns a boolean stating true // if validation passed (false for failed), and a slice of errors if validation failed. ValidatePathParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) // ValidateSecurity validates the security requirements for the operation. It returns a boolean stating true // if validation passed (false for failed), and a slice of errors if validation failed. ValidateSecurity(request *http.Request) (bool, []*errors.ValidationError) // ValidateSecurityWithPathItem validates the security requirements for the operation. It returns a boolean stating true // if validation passed (false for failed), and a slice of errors if validation failed. ValidateSecurityWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) } // NewParameterValidator will create a new ParameterValidator from an OpenAPI 3+ document func NewParameterValidator(document *v3.Document, opts ...config.Option) ParameterValidator { options := config.NewValidationOptions(opts...) return ¶mValidator{options: options, document: document} } type paramValidator struct { options *config.ValidationOptions document *v3.Document } libopenapi-validator-0.13.8/parameters/path_parameters.go000066400000000000000000000357741520534042400236000ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package parameters import ( "fmt" "net/http" "net/url" "regexp" "strconv" "strings" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" ) func (v *paramValidator) ValidatePathParams(request *http.Request) (bool, []*errors.ValidationError) { pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options) if len(errs) > 0 { return false, errs } return v.ValidatePathParamsWithPathItem(request, pathItem, foundPath) } func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { if pathItem == nil { return false, []*errors.ValidationError{{ ValidationType: helpers.PathValidation, ValidationSubType: helpers.ValidationMissing, Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ "however that path, or the %s method for that path does not exist in the specification", request.Method, request.URL.Path, request.Method), SpecLine: -1, SpecCol: -1, HowToFix: errors.HowToFixPath, }} } // split the path into segments submittedSegments := strings.Split(paths.StripRequestPath(request, v.document), helpers.Slash) pathSegments := strings.Split(pathValue, helpers.Slash) // get the operation method for error reporting operation := strings.ToLower(request.Method) // extract params for the operation params := helpers.ExtractParamsForOperation(request, pathItem) var validationErrors []*errors.ValidationError for _, p := range params { if p.In == helpers.Path { // var paramTemplate string for x := range pathSegments { if pathSegments[x] == "" { // skip empty segments continue } var rgx *regexp.Regexp if v.options.RegexCache != nil { if cachedRegex, found := v.options.RegexCache.Load(pathSegments[x]); found { rgx = cachedRegex.(*regexp.Regexp) } } if rgx == nil { r, err := helpers.GetRegexForPath(pathSegments[x]) if err != nil { continue } rgx = r if v.options.RegexCache != nil { v.options.RegexCache.Store(pathSegments[x], r) } } matches := rgx.FindStringSubmatch(submittedSegments[x]) matches = matches[1:] // Check if it is well-formed. idxs, errBraces := helpers.BraceIndices(pathSegments[x]) if errBraces != nil { continue } idx := 0 for _, match := range matches { isMatrix := false isLabel := false // isExplode := false isSimple := true paramTemplate := pathSegments[x][idxs[idx]+1 : idxs[idx+1]-1] idx += 2 // move to the next brace pair paramName := paramTemplate // check for an asterisk on the end of the parameter (explode) if strings.HasSuffix(paramTemplate, helpers.Asterisk) { // isExplode = true paramName = paramTemplate[:len(paramTemplate)-1] } if strings.HasPrefix(paramTemplate, helpers.Period) { isLabel = true isSimple = false paramName = paramName[1:] } if strings.HasPrefix(paramTemplate, helpers.SemiColon) { isMatrix = true isSimple = false paramName = paramName[1:] } // does this param name match the current path segment param name if paramName != p.Name { continue } paramValue := match // URL decode the parameter value before validation decodedParamValue, _ := url.PathUnescape(paramValue) if decodedParamValue == "" { // Mandatory path parameter cannot be empty if p.Required != nil && *p.Required { validationErrors = append(validationErrors, errors.PathParameterMissing(p, pathValue, request.URL.Path)) break } continue } // extract the schema from the parameter sch := p.Schema.Schema() // Get rendered schema for ReferenceSchema field in errors (uses cache if available) renderedSchema := GetRenderedSchema(sch, v.options) // check enum (if present) enumCheck := func(decodedValue string) { matchFound := false for _, enumVal := range sch.Enum { if strings.TrimSpace(decodedValue) == fmt.Sprint(enumVal.Value) { matchFound = true break } } if !matchFound { validationErrors = append(validationErrors, errors.IncorrectPathParamEnum(p, strings.ToLower(decodedValue), sch, pathValue, renderedSchema)) } } // for each type, check the value. if sch != nil && sch.Type != nil { for typ := range sch.Type { switch sch.Type[typ] { case helpers.String: // TODO: label and matrix style validation // check if the param is within the enum if sch.Enum != nil { enumCheck(decodedParamValue) break } validationErrors = append(validationErrors, ValidateSingleParameterSchema( sch, decodedParamValue, "Path parameter", "The path parameter", p.Name, helpers.ParameterValidation, helpers.ParameterValidationPath, v.options, pathValue, operation, )...) case helpers.Integer: // simple use case is already handled in find param. rawParamValue, paramValueParsed, err := v.resolveInteger(sch, p, isLabel, isMatrix, decodedParamValue, pathValue, renderedSchema) if err != nil { validationErrors = append(validationErrors, err...) break } // check if the param is within the enum if sch.Enum != nil { enumCheck(rawParamValue) break } validationErrors = append(validationErrors, ValidateSingleParameterSchema( sch, paramValueParsed, "Path parameter", "The path parameter", p.Name, helpers.ParameterValidation, helpers.ParameterValidationPath, v.options, pathValue, operation, )...) case helpers.Number: // simple use case is already handled in find param. rawParamValue, paramValueParsed, err := v.resolveNumber(sch, p, isLabel, isMatrix, decodedParamValue, pathValue, renderedSchema) if err != nil { validationErrors = append(validationErrors, err...) break } // check if the param is within the enum if sch.Enum != nil { enumCheck(rawParamValue) break } validationErrors = append(validationErrors, ValidateSingleParameterSchema( sch, paramValueParsed, "Path parameter", "The path parameter", p.Name, helpers.ParameterValidation, helpers.ParameterValidationPath, v.options, pathValue, operation, )...) case helpers.Boolean: if isLabel && p.Style == helpers.LabelStyle { if _, err := strconv.ParseBool(decodedParamValue[1:]); err != nil { validationErrors = append(validationErrors, errors.IncorrectPathParamBool(p, decodedParamValue[1:], sch, pathValue, renderedSchema)) } } if isSimple { if _, err := strconv.ParseBool(decodedParamValue); err != nil { validationErrors = append(validationErrors, errors.IncorrectPathParamBool(p, decodedParamValue, sch, pathValue, renderedSchema)) } } if isMatrix && p.Style == helpers.MatrixStyle { // strip off the colon and the parameter name decodedForMatrix := strings.Replace(decodedParamValue[1:], fmt.Sprintf("%s=", p.Name), "", 1) if _, err := strconv.ParseBool(decodedForMatrix); err != nil { validationErrors = append(validationErrors, errors.IncorrectPathParamBool(p, decodedForMatrix, sch, pathValue, renderedSchema)) } } case helpers.Object: var encodedObject interface{} if p.IsDefaultPathEncoding() { encodedObject = helpers.ConstructMapFromCSVWithSchema(decodedParamValue, sch) } else { switch p.Style { case helpers.LabelStyle: if !p.IsExploded() { encodedObject = helpers.ConstructMapFromCSVWithSchema(decodedParamValue[1:], sch) } else { encodedObject = helpers.ConstructKVFromLabelEncodingWithSchema(decodedParamValue, sch) } case helpers.MatrixStyle: if !p.IsExploded() { decodedForMatrix := strings.Replace(decodedParamValue[1:], fmt.Sprintf("%s=", p.Name), "", 1) encodedObject = helpers.ConstructMapFromCSVWithSchema(decodedForMatrix, sch) } else { decodedForMatrix := strings.Replace(decodedParamValue[1:], fmt.Sprintf("%s=", p.Name), "", 1) encodedObject = helpers.ConstructKVFromMatrixCSVWithSchema(decodedForMatrix, sch) } default: if p.IsExploded() { encodedObject = helpers.ConstructKVFromCSVWithSchema(decodedParamValue, sch) } } } // if a schema was extracted if sch != nil { validationErrors = append(validationErrors, ValidateParameterSchema(sch, encodedObject, "", "Path parameter", "The path parameter", p.Name, helpers.ParameterValidation, helpers.ParameterValidationPath, v.options)...) } case helpers.Array: // extract the items schema in order to validate the array items. if sch.Items != nil && sch.Items.IsA() { iSch := sch.Items.A.Schema() // Get rendered items schema for ReferenceSchema field in errors (uses cache if available) renderedItemsSchema := GetRenderedSchema(iSch, v.options) for n := range iSch.Type { // determine how to explode the array var arrayValues []string if isSimple { arrayValues = strings.Split(decodedParamValue, helpers.Comma) } if isLabel { if !p.IsExploded() { arrayValues = strings.Split(decodedParamValue[1:], helpers.Comma) } else { arrayValues = strings.Split(decodedParamValue[1:], helpers.Period) } } if isMatrix { if !p.IsExploded() { decodedForMatrix := strings.Replace(decodedParamValue[1:], fmt.Sprintf("%s=", p.Name), "", 1) arrayValues = strings.Split(decodedForMatrix, helpers.Comma) } else { decodedForMatrix := strings.ReplaceAll(decodedParamValue[1:], fmt.Sprintf("%s=", p.Name), "") arrayValues = strings.Split(decodedForMatrix, helpers.SemiColon) } } switch iSch.Type[n] { case helpers.Integer: for pv := range arrayValues { if _, err := strconv.ParseInt(arrayValues[pv], 10, 64); err != nil { validationErrors = append(validationErrors, errors.IncorrectPathParamArrayInteger(p, arrayValues[pv], sch, iSch, pathValue, renderedItemsSchema)) } } case helpers.Number: for pv := range arrayValues { if _, err := strconv.ParseFloat(arrayValues[pv], 64); err != nil { validationErrors = append(validationErrors, errors.IncorrectPathParamArrayNumber(p, arrayValues[pv], sch, iSch, pathValue, renderedItemsSchema)) } } case helpers.Boolean: for pv := range arrayValues { bc := len(validationErrors) if _, err := strconv.ParseBool(arrayValues[pv]); err != nil { validationErrors = append(validationErrors, errors.IncorrectPathParamArrayBoolean(p, arrayValues[pv], sch, iSch, pathValue, renderedItemsSchema)) continue } if len(validationErrors) == bc { // ParseBool will parse 0 or 1 as false/true to we // need to catch this edge case. if arrayValues[pv] == "0" || arrayValues[pv] == "1" { validationErrors = append(validationErrors, errors.IncorrectPathParamArrayBoolean(p, arrayValues[pv], sch, iSch, pathValue, renderedItemsSchema)) continue } } } } } } } } } } } } } errors.PopulateValidationErrors(validationErrors, request, pathValue) if len(validationErrors) > 0 { return false, validationErrors } return true, nil } func (v *paramValidator) resolveNumber(sch *base.Schema, p *v3.Parameter, isLabel bool, isMatrix bool, paramValue string, pathValue string, renderedSchema string) (string, float64, []*errors.ValidationError) { if isLabel && p.Style == helpers.LabelStyle { paramValueParsed, err := strconv.ParseFloat(paramValue[1:], 64) if err != nil { return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue[1:], sch, pathValue, renderedSchema)} } return paramValue[1:], paramValueParsed, nil } if isMatrix && p.Style == helpers.MatrixStyle { // strip off the colon and the parameter name paramValue = strings.Replace(paramValue[1:], fmt.Sprintf("%s=", p.Name), "", 1) paramValueParsed, err := strconv.ParseFloat(paramValue, 64) if err != nil { return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue[1:], sch, pathValue, renderedSchema)} } return paramValue, paramValueParsed, nil } paramValueParsed, err := strconv.ParseFloat(paramValue, 64) if err != nil { return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue, sch, pathValue, renderedSchema)} } return paramValue, paramValueParsed, nil } func (v *paramValidator) resolveInteger(sch *base.Schema, p *v3.Parameter, isLabel bool, isMatrix bool, paramValue string, pathValue string, renderedSchema string) (string, int64, []*errors.ValidationError) { if isLabel && p.Style == helpers.LabelStyle { paramValueParsed, err := strconv.ParseInt(paramValue[1:], 10, 64) if err != nil { return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue[1:], sch, pathValue, renderedSchema)} } return paramValue[1:], paramValueParsed, nil } if isMatrix && p.Style == helpers.MatrixStyle { // strip off the colon and the parameter name paramValue = strings.Replace(paramValue[1:], fmt.Sprintf("%s=", p.Name), "", 1) paramValueParsed, err := strconv.ParseInt(paramValue, 10, 64) if err != nil { return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue[1:], sch, pathValue, renderedSchema)} } return paramValue, paramValueParsed, nil } paramValueParsed, err := strconv.ParseInt(paramValue, 10, 64) if err != nil { return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue, sch, pathValue, renderedSchema)} } return paramValue, paramValueParsed, nil } libopenapi-validator-0.13.8/parameters/path_parameters_test.go000066400000000000000000001733631520534042400246340ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package parameters import ( "net/http" "sync" "sync/atomic" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" "github.com/pb33f/libopenapi-validator/radix" ) func TestNewValidator_SimpleArrayEncodedPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerIds*}/locate: parameters: - name: burgerIds in: path schema: type: array items: type: integer patch: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/1,2,3,4,5/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_PathParamURLEncoding_EnumValidation(t *testing.T) { spec := `openapi: 3.1.0 paths: /test/{pathParam}: parameters: - name: pathParam in: path required: true schema: type: string enum: ["foo/bar", "hello world", "special@chars"] get: operationId: testOperation` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Test 1: URL-encoded forward slash should match enum value "foo/bar" request1, _ := http.NewRequest(http.MethodGet, "https://example.com/test/foo%2Fbar", nil) valid1, errors1 := v.ValidatePathParams(request1) assert.True(t, valid1, "URL-encoded path parameter 'foo%%2Fbar' should match enum value 'foo/bar'") assert.Len(t, errors1, 0) // Test 2: URL-encoded space should match enum value "hello world" request2, _ := http.NewRequest(http.MethodGet, "https://example.com/test/hello%20world", nil) valid2, errors2 := v.ValidatePathParams(request2) assert.True(t, valid2, "URL-encoded path parameter 'hello%%20world' should match enum value 'hello world'") assert.Len(t, errors2, 0) // Test 3: URL-encoded @ symbol should match enum value "special@chars" request3, _ := http.NewRequest(http.MethodGet, "https://example.com/test/special%40chars", nil) valid3, errors3 := v.ValidatePathParams(request3) assert.True(t, valid3, "URL-encoded path parameter 'special%%40chars' should match enum value 'special@chars'") assert.Len(t, errors3, 0) // Test 4: Non-matching encoded value should fail request4, _ := http.NewRequest(http.MethodGet, "https://example.com/test/not%2Dfound", nil) valid4, errors4 := v.ValidatePathParams(request4) assert.False(t, valid4, "URL-encoded path parameter 'not%%2Dfound' should not match any enum values") assert.Len(t, errors4, 1) assert.Contains(t, errors4[0].Reason, "pre-defined values set via an enum") } func TestNewValidator_PathParamURLEncoding_BackwardCompatibility(t *testing.T) { spec := `openapi: 3.1.0 paths: /test/{pathParam}: parameters: - name: pathParam in: path required: true schema: type: string enum: ["normal-value", "encoded%2Fvalue"] get: operationId: testOperation` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Test backward compatibility: if enum contains encoded values, // they should still work for backward compatibility request, _ := http.NewRequest(http.MethodGet, "https://example.com/test/encoded%2Fvalue", nil) valid, errors := v.ValidatePathParams(request) // This should fail because the decoded value "encoded/value" doesn't match "encoded%2Fvalue" assert.False(t, valid, "Encoded enum values should require exact match after decoding") assert.Len(t, errors, 1) assert.Contains(t, errors[0].Reason, "pre-defined values set via an enum") } func TestNewValidator_PathParamURLEncoding_IntegerEnum(t *testing.T) { spec := `openapi: 3.1.0 paths: /test/{numParam}: parameters: - name: numParam in: path required: true schema: type: integer enum: [123, 456] get: operationId: testOperation` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Test URL-encoded integer (though integers typically don't need encoding) request, _ := http.NewRequest(http.MethodGet, "https://example.com/test/123", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid, "Integer parameter should validate correctly") assert.Len(t, errors, 0) } func TestNewValidator_SimpleArrayEncodedPath_InvalidInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerIds*}/locate: parameters: - name: burgerIds in: path schema: type: array items: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/1,pizza,3,4,false/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 2) assert.Equal(t, "Path array parameter 'burgerIds' is not a valid integer", errors[0].Message) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "/burgers/{burgerIds*}/locate", errors[0].SpecPath) } func TestNewValidator_SimpleArrayEncodedPath_InvalidNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerIds*}/locate: parameters: - name: burgerIds in: path schema: type: array items: type: number get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/1,pizza,3,4,false/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 2) assert.Equal(t, "Path array parameter 'burgerIds' is not a valid number", errors[0].Message) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "/burgers/{burgerIds*}/locate", errors[0].SpecPath) } func TestNewValidator_SimpleArrayEncodedPath_InvalidBool(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerIds*}/locate: parameters: - name: burgerIds in: path schema: type: array items: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/1,true,0,frogs,false/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 3) assert.Equal(t, "Path array parameter 'burgerIds' is not a valid boolean", errors[0].Message) } func TestNewValidator_SimpleObjectEncodedPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burger}/locate: parameters: - name: burger in: path schema: type: object properties: id: type: integer vegetarian: type: boolean get: operationId: locateBurger` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/id,1234,vegetarian,true/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_SimpleObjectEncodedPath_Invalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burger}/locate: parameters: - name: burger in: path schema: type: object properties: id: type: integer vegetarian: type: boolean get: operationId: locateBurger` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/id,hello,vegetarian,there/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestNewValidator_SimpleObjectEncodedPath_Exploded(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burger}/locate: parameters: - name: burger in: path explode: true schema: type: object properties: id: type: integer vegetarian: type: boolean get: operationId: locateBurger` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/id=1234,vegetarian=true/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_SimpleObjectEncodedPath_ExplodedInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burger}/locate: parameters: - name: burger in: path explode: true schema: type: object properties: id: type: integer vegetarian: type: boolean get: operationId: locateBurger` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/id=toast,vegetarian=chicken/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestNewValidator_ObjectEncodedPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burger}/locate: parameters: - name: burger in: path schema: type: object properties: id: type: integer vegetarian: type: boolean get: operationId: locateBurger` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/id,1234,vegetarian,true/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_SimpleEncodedPath_InvalidInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/hello/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' is not a valid integer", errors[0].Message) assert.Equal(t, "The path parameter 'burgerId' is defined as being an integer, however the value 'hello' is not a valid integer", errors[0].Reason) } func TestNewValidator_SimpleEncodedPath_MinimumIntegerViolation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: integer minimum: 10 get: operationId: locateBurgers` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/1/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "Reason: minimum: got 1, want 10", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_SimpleEncodedPath_MinimumInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: integer minimum: 10 get: operationId: locateBurgers` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/14/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_SimpleEncodedPath_MaximumIntegerViolation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: integer maximum: 10 get: operationId: locateBurgers` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/11/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "Reason: maximum: got 11, want 10", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_SimpleEncodedPath_MaximumInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: integer maximum: 10 get: operationId: locateBurgers` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/4/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_SimpleEncodedPath_InvalidNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: number get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/hello/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' is not a valid number", errors[0].Message) assert.Equal(t, "The path parameter 'burgerId' is defined as being a number, however the value 'hello' is not a valid number", errors[0].Reason) } func TestNewValidator_SimpleEncodedPath_MinimumNumberViolation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: number minimum: 10.2 get: operationId: locateBurgers` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/1.3/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "Reason: minimum: got 1.3, want 10.2", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_SimpleEncodedPath_MinimumNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: number minimum: 10.3 get: operationId: locateBurgers` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/14.5/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_SimpleEncodedPath_MaximumNumberViolation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: number maximum: 10.2 get: operationId: locateBurgers` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/11.2/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "Reason: maximum: got 11.2, want 10.2", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_SimpleEncodedPath_MaximumNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: number maximum: 10.2 get: operationId: locateBurgers` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/4.5/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_SimpleEncodedPath_InvalidBoolean(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/hello/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' is not a valid boolean", errors[0].Message) } func TestNewValidator_LabelEncodedPath_InvalidInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label schema: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.hello/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' is not a valid integer", errors[0].Message) } func TestNewValidator_LabelEncodedPath_MinimumIntegerViolation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label schema: type: integer minimum: 10 get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.3/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "Reason: minimum: got 3, want 10", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_LabelEncodedPath_MaximumIntegerViolation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label schema: type: integer maximum: 10 get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.32/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "Reason: maximum: got 32, want 10", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_LabelEncodedPath_InvalidBoolean(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label schema: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.hello/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' is not a valid boolean", errors[0].Message) } func TestNewValidator_LabelEncodedPath_ValidBoolean(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label schema: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.true/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_LabelEncodedPath_ValidArray_Integer(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label schema: type: array items: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.3,4/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_LabelEncodedPath_ValidArray_Integer_Exploded(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label explode: true schema: type: array items: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.3.4/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_LabelEncodedPath_InvalidArray_Integer_Exploded(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label explode: true schema: type: array items: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.3.Not an_integer.5.6/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path array parameter 'burgerId' is not a valid integer", errors[0].Message) } func TestNewValidator_LabelEncodedPath_InvalidArray_Integer(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label schema: type: array items: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.3,4,Not an_integer,6/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path array parameter 'burgerId' is not a valid integer", errors[0].Message) } func TestNewValidator_LabelEncodedPath_ValidArray_Number(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label schema: type: array items: type: number get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.3.4,5.6/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_LabelEncodedPath_ValidArray_Number_Exploded(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label explode: true schema: type: array items: type: number get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.3.4.5.6/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_LabelEncodedPath_InvalidArray_Number_Exploded(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label explode: true schema: type: array items: type: number get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.3.Not a number.5.6/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path array parameter 'burgerId' is not a valid number", errors[0].Message) } func TestNewValidator_LabelEncodedPath_InvalidArray_Number(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label schema: type: array items: type: number get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.3,4,Not a number,6/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path array parameter 'burgerId' is not a valid number", errors[0].Message) } func TestNewValidator_LabelEncodedPath_InvalidObject(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label schema: type: object properties: id: type: integer vegetarian: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.id,hello,vegetarian,why/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestNewValidator_LabelEncodedPath_InvalidObject_Exploded(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label explode: true schema: type: object properties: id: type: integer vegetarian: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.id=hello.vegetarian=why/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestNewValidator_LabelEncodedPath_ValidMultiParam(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate/{.query}: parameters: - name: query in: path style: label schema: type: string - name: burgerId in: path style: label explode: true schema: type: object properties: id: type: integer vegetarian: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.id=1234.vegetarian=true/locate/bigMac", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_LabelEncodedPath_InvalidMultiParam(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate/{.query}: parameters: - name: query in: path style: label schema: type: integer - name: burgerId in: path style: label explode: true schema: type: object properties: id: type: integer vegetarian: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.id=1234.vegetarian=true/locate/bigMac", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) } func TestNewValidator_MatrixEncodedPath_ValidInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burgerId}/locate: parameters: - name: burgerId in: path style: matrix schema: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burgerId=5/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_MatrixEncodedPath_InvalidInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burgerId}/locate: parameters: - name: burgerId in: path style: matrix schema: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burgerId=I am not a number/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' is not a valid integer", errors[0].Message) } func TestNewValidator_MatrixEncodedPath_MinimumIntegerViolation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burgerId}/locate: parameters: - name: burgerId in: path style: matrix schema: type: integer minimum: 5 get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burgerId=3/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "Reason: minimum: got 3, want 5", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_MatrixEncodedPath_MaximumIntegerViolation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burgerId}/locate: parameters: - name: burgerId in: path style: matrix schema: type: integer maximum: 5 get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burgerId=30/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "Reason: maximum: got 30, want 5", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_MatrixEncodedPath_InvalidNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burgerId}/locate: parameters: - name: burgerId in: path style: matrix schema: type: number get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burgerId=I am not a number/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' is not a valid number", errors[0].Message) } func TestNewValidator_MatrixEncodedPath_MinimumNumberViolation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burgerId}/locate: parameters: - name: burgerId in: path style: matrix schema: type: number minimum: 5 get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burgerId=3/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "Reason: minimum: got 3, want 5", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_MatrixEncodedPath_MaximumNumberViolation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burgerId}/locate: parameters: - name: burgerId in: path style: matrix schema: type: number maximum: 5 get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burgerId=30/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "Reason: maximum: got 30, want 5", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_MatrixEncodedPath_ValidPrimitiveBoolean(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burgerId}/locate: parameters: - name: burgerId in: path style: matrix schema: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burgerId=false/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_MatrixEncodedPath_InvalidPrimitiveBoolean(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burgerId}/locate: parameters: - name: burgerId in: path style: matrix schema: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burgerId=I am also not a bool/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' is not a valid boolean", errors[0].Message) } func TestNewValidator_MatrixEncodedPath_ValidObject(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burger}/locate: parameters: - name: burger in: path style: matrix schema: type: object properties: id: type: integer vegetarian: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burger=id,1234,vegetarian,false/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_MatrixEncodedPath_InvalidObject(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burger}/locate: parameters: - name: burger in: path style: matrix schema: type: object properties: id: type: integer vegetarian: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burger=id,1234,vegetarian,I am not a bool/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "got string, want boolean", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_MatrixEncodedPath_ValidObject_Exploded(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burger*}/locate: parameters: - name: burger in: path style: matrix explode: true schema: type: object properties: id: type: integer vegetarian: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;id=1234;vegetarian=false/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_MatrixEncodedPath_InvalidObject_Exploded(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burger*}/locate: parameters: - name: burger in: path style: matrix explode: true schema: type: object properties: id: type: integer vegetarian: type: boolean get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;id=1234;vegetarian=I am not a boolean/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "got string, want boolean", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_MatrixEncodedPath_ValidArray(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burger*}/locate: parameters: - name: burger in: path style: matrix schema: type: array items: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burger=1,2,3,4,5/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_MatrixEncodedPath_InvalidArray(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burger*}/locate: parameters: - name: burger in: path style: matrix schema: type: array items: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burger=1,2,not a number,4,false/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 2) } func TestNewValidator_MatrixEncodedPath_ValidArray_Exploded(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burger*}/locate: parameters: - name: burger in: path style: matrix explode: true schema: type: array items: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burger=1;burger=2;burger=3/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_MatrixEncodedPath_InvalidArray_Exploded(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burger*}/locate: parameters: - name: burger in: path style: matrix explode: true schema: type: array items: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burger=1;burger=I am not an int;burger=3/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path array parameter 'burger' is not a valid integer", errors[0].Message) } func TestNewValidator_PathParams_PathNotFound(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burger*}/locate: parameters: - name: burger in: path style: matrix explode: true schema: type: array items: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/I do not exist", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) } func TestNewValidator_PathParamStringEnumValid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: string enum: [bigMac, whopper, mcCrispy] get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/bigMac/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_PathParamStringEnumInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: string enum: [bigMac, whopper, mcCrispy] get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/hello/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' does not match allowed values", errors[0].Message) assert.Equal(t, "Instead of 'hello', use one of the allowed values: 'bigMac, whopper, mcCrispy'", errors[0].HowToFix) } func TestNewValidator_PathParamStringMinLengthViolation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: string minLength: 4 get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/big/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "Reason: minLength: got 3, want 4", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_PathParamStringMaxLengthViolation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: string maxLength: 1 get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/big/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "Reason: maxLength: got 3, want 1", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_PathParamIntegerEnumValid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: integer enum: [1,2,99,100] get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/2/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_PathParamIntegerEnumInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: integer enum: [1,2,99,100] get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/3284/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' does not match allowed values", errors[0].Message) } func TestNewValidator_PathParamNumberEnumValid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: number enum: [1,2,99,100] get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/2/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_PathParamNumberEnumInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - name: burgerId in: path schema: type: number enum: [1,2,99,100] get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/3284/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' does not match allowed values", errors[0].Message) } func TestNewValidator_PathLabelEumValid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label schema: type: number enum: [1,2,99,100] get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.2/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_PathLabelEumInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: parameters: - name: burgerId in: path style: label schema: type: number enum: [1,2,99,100] get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/.22334/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' does not match allowed values", errors[0].Message) assert.Equal(t, "Instead of '22334', use one of the allowed values: '1, 2, 99, 100'", errors[0].HowToFix) } func TestNewValidator_PathMatrixEumInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burgerId}/locate: parameters: - name: burgerId in: path style: matrix schema: type: number enum: [1,2,99,100] get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burgerId=22334/locate", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' does not match allowed values", errors[0].Message) assert.Equal(t, "Instead of '22334', use one of the allowed values: '1, 2, 99, 100'", errors[0].HowToFix) } func TestNewValidator_SetPathForPathParam(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burgerId}/locate: parameters: - name: burgerId in: path style: matrix schema: type: number enum: [1,2,99,100] get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/;burgerId=22334/locate", nil) // preset the path path, _, pv := paths.FindPath(request, &m.Model, nil) valid, errors := v.ValidatePathParamsWithPathItem(request, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' does not match allowed values", errors[0].Message) assert.Equal(t, "Instead of '22334', use one of the allowed values: '1, 2, 99, 100'", errors[0].HowToFix) } func TestNewValidator_SetPathForPathParam_notfound(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{;burgerId}/locate: parameters: - name: burgerId in: path style: matrix schema: type: number enum: [1,2,99,100] get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/pizza/;burgerId=22334/locate", nil) // preset the path path, _, pv := paths.FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) valid, errors := v.ValidatePathParamsWithPathItem(request, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "GET Path '/pizza/;burgerId=22334/locate' not found", errors[0].Message) } func TestNewValidator_ServerPathPrefixInRequestPath(t *testing.T) { spec := `openapi: 3.1.0 servers: - url: https://api.pb33f.io/lorem/ipsum description: Live production endpoint for general use. paths: /burgers/{burger}/locate: parameters: - name: burger in: path schema: type: string format: uuid get: operationId: locateBurger` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/lorem/ipsum/burgers/d6d8d513-686c-466f-9f5a-1c051b6b4f3f/locate", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_MandatoryPathSegmentEmpty(t *testing.T) { spec := `openapi: 3.1.0 servers: - url: https://api.pb33f.io description: Live production endpoint for general use. paths: /burgers/{burger}: get: parameters: - name: burger in: path required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) } func TestNewValidator_ODataFormattedOpenAPISpecs(t *testing.T) { spec := `openapi: 3.0.0 paths: /entities('{Entity}'): parameters: - description: 'key: Entity' in: path name: Entity required: true schema: type: integer get: operationId: one /orders(RelationshipNumber='{RelationshipNumber}',ValidityEndDate=datetime'{ValidityEndDate}'): parameters: - name: RelationshipNumber in: path required: true schema: type: integer - name: ValidityEndDate in: path required: true schema: type: string get: operationId: one ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/entities('1')", nil) valid, errors := v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) request, _ = http.NewRequest(http.MethodGet, "https://things.com/orders(RelationshipNumber='1234',ValidityEndDate=datetime'1492041600000')", nil) valid, errors = v.ValidatePathParams(request) assert.True(t, valid) assert.Len(t, errors, 0) request, _ = http.NewRequest(http.MethodGet, "https://things.com/orders(RelationshipNumber='dummy',ValidityEndDate=datetime'1492041600000')", nil) valid, errors = v.ValidatePathParams(request) assert.False(t, valid) assert.Len(t, errors, 1) } func TestNewValidator_ODataFormattedOpenAPISpecs_Error(t *testing.T) { spec := `openapi: 3.0.0 paths: /entities('{Entity'): parameters: - in: path name: Entity required: true schema: type: integer get: operationId: one ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/entities('1')", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.NotEmpty(t, errors) } func TestNewValidator_ODataFormattedOpenAPISpecs_ErrorEmptyParameter(t *testing.T) { spec := `openapi: 3.0.0 paths: /entities('{Entity}'): parameters: - in: path name: Entity required: true schema: type: integer get: operationId: one ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/entities('')", nil) valid, errors := v.ValidatePathParams(request) assert.False(t, valid) assert.NotEmpty(t, errors) } type regexCacheWatcher struct { inner *sync.Map missCount int64 hitCount int64 storeCount int64 } func (c *regexCacheWatcher) Load(key any) (value any, ok bool) { data, found := c.inner.Load(key) if found { atomic.AddInt64(&c.hitCount, 1) } else { atomic.AddInt64(&c.missCount, 1) } return data, found } func (c *regexCacheWatcher) Store(key, value any) { atomic.AddInt64(&c.storeCount, 1) c.inner.Store(key, value) } func TestValidatePathParamsWithPathItem_RegexCache_WithOneCached(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}/locate: parameters: - in: path name: burgerId schema: type: integer get: operationId: locateBurgers` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() cache := ®exCacheWatcher{inner: &sync.Map{}} segment := "{burgerId}" r, err := helpers.GetRegexForPath(segment) require.NoError(t, err) cache.inner.Store(segment, r) v := NewParameterValidator(&m.Model, config.WithRegexCache(cache)) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/123/locate", nil) pathItem, _, foundPath := paths.FindPath(request, &m.Model, nil) v.ValidatePathParamsWithPathItem(request, pathItem, foundPath) // Only "{burgerId}" regex was cached assert.EqualValues(t, 2, cache.storeCount) // Stores "burgers" and "locate" regex assert.EqualValues(t, 2, cache.missCount) assert.EqualValues(t, 1, cache.hitCount) } // TestRadixTree_RegexFallback verifies that: // 1. Simple paths use the radix tree (no regex cache) // 2. Complex paths (OData style) fall back to regex and use the cache func TestRadixTree_RegexFallback(t *testing.T) { spec := `openapi: 3.1.0 paths: /simple/{id}: get: operationId: getSimple /entities('{Entity}'): get: operationId: getOData` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() cache := ®exCacheWatcher{inner: &sync.Map{}} opts := &config.ValidationOptions{RegexCache: cache, PathTree: radix.BuildPathTree(&m.Model)} // Simple path - should NOT use regex cache (handled by radix tree) simpleRequest, _ := http.NewRequest(http.MethodGet, "https://things.com/simple/123", nil) pathItem, _, foundPath := paths.FindPath(simpleRequest, &m.Model, opts) assert.NotNil(t, pathItem) assert.Equal(t, "/simple/{id}", foundPath) assert.EqualValues(t, 0, cache.storeCount, "Simple paths should not use regex cache") assert.EqualValues(t, 0, cache.hitCount+cache.missCount, "Simple paths should not touch regex cache") // OData path - SHOULD use regex cache (radix tree can't handle embedded params) odataRequest, _ := http.NewRequest(http.MethodGet, "https://things.com/entities('abc')", nil) pathItem, _, foundPath = paths.FindPath(odataRequest, &m.Model, opts) assert.NotNil(t, pathItem) assert.Equal(t, "/entities('{Entity}')", foundPath) assert.EqualValues(t, 1, cache.storeCount, "OData paths should use regex cache") assert.EqualValues(t, 1, cache.missCount, "First OData lookup should miss cache") // Second OData call should hit cache pathItem, _, _ = paths.FindPath(odataRequest, &m.Model, opts) assert.NotNil(t, pathItem) assert.EqualValues(t, 1, cache.storeCount, "No new stores on cache hit") assert.EqualValues(t, 1, cache.hitCount, "Second OData lookup should hit cache") } libopenapi-validator-0.13.8/parameters/query_parameters.go000066400000000000000000000271101520534042400237720ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package parameters import ( "encoding/json" "fmt" "net/http" "regexp" "strconv" "strings" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/orderedmap" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" "github.com/pb33f/libopenapi-validator/strict" ) const rx = `[:\/\?#\[\]\@!\$&'\(\)\*\+,;=]` var rxRxp = regexp.MustCompile(rx) func (v *paramValidator) ValidateQueryParams(request *http.Request) (bool, []*errors.ValidationError) { pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options) if len(errs) > 0 { return false, errs } return v.ValidateQueryParamsWithPathItem(request, pathItem, foundPath) } func (v *paramValidator) ValidateQueryParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { if pathItem == nil { return false, []*errors.ValidationError{{ ValidationType: helpers.PathValidation, ValidationSubType: helpers.ValidationMissing, Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ "however that path, or the %s method for that path does not exist in the specification", request.Method, request.URL.Path, request.Method), SpecLine: -1, SpecCol: -1, HowToFix: errors.HowToFixPath, }} } // extract params for the operation params := helpers.ExtractParamsForOperation(request, pathItem) queryParams := make(map[string][]*helpers.QueryParam) var validationErrors []*errors.ValidationError // build a set of spec parameter names for exact matching specParamNames := make(map[string]bool) for _, p := range params { if p.In == helpers.Query { specParamNames[p.Name] = true } } for qKey, qVal := range request.URL.Query() { // check if the query key exactly matches a spec parameter name (e.g., "match[]") // if so, store it literally without deepObject stripping if specParamNames[qKey] { queryParams[qKey] = append(queryParams[qKey], &helpers.QueryParam{ Key: qKey, Values: qVal, }) } else if stripped, propertyPath, ok := helpers.ParseDeepObjectKey(qKey); ok { // check if the param is encoded as a property / deepObject queryParams[stripped] = append(queryParams[stripped], &helpers.QueryParam{ Key: stripped, Values: qVal, Property: propertyPath[0], PropertyPath: propertyPath, }) } else { queryParams[qKey] = append(queryParams[qKey], &helpers.QueryParam{ Key: qKey, Values: qVal, }) } } // Get operation from request method (lowercase for JSON Pointer) operation := strings.ToLower(request.Method) // look through the params for the query key doneLooking: for p := range params { if params[p].In == helpers.Query { contentWrapped := false var contentType string // check if this param is found as a set of query strings if jk, ok := queryParams[params[p].Name]; ok { skipValues: for _, fp := range jk { // let's check styles first. validationErrors = append(validationErrors, ValidateQueryParamStyle(params[p], jk)...) // there is a match, is the type correct // this context is extracted from the 3.1 spec to explain what is going on here: // For more complex scenarios, the content property can define the media type and schema of the // parameter. A parameter MUST contain either a schema property, or a content property, but not both. // The map MUST only contain one entry. (for content) var sch *base.Schema if params[p].Schema != nil { sch = params[p].Schema.Schema() } else { // ok, no schema, check for a content type for pair := orderedmap.First(params[p].Content); pair != nil; pair = pair.Next() { sch = pair.Value().Schema.Schema() contentWrapped = true contentType = pair.Key() break } } // Get rendered schema for ReferenceSchema field in errors (uses cache if available) renderedSchema := GetRenderedSchema(sch, v.options) pType := sch.Type // for each param, check each type for _, ef := range fp.Values { // check allowReserved values. If this is set to true, then we can allow the // following characters // :/?#[]@!$&'()*+,;= // to be present as they are, without being URLEncoded. if !params[p].AllowReserved { if rxRxp.MatchString(ef) && params[p].IsExploded() { validationErrors = append(validationErrors, errors.IncorrectReservedValues(params[p], ef, sch, pathValue, operation, renderedSchema)) } } for _, ty := range pType { switch ty { case helpers.String: validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, ef, params[p], pathValue, operation, renderedSchema)...) case helpers.Integer: efF, err := strconv.ParseInt(ef, 10, 64) if err != nil { validationErrors = append(validationErrors, errors.InvalidQueryParamInteger(params[p], ef, sch, pathValue, operation, renderedSchema)) break } validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, efF, params[p], pathValue, operation, renderedSchema)...) case helpers.Number: efF, err := strconv.ParseFloat(ef, 64) if err != nil { validationErrors = append(validationErrors, errors.InvalidQueryParamNumber(params[p], ef, sch, pathValue, operation, renderedSchema)) break } validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, efF, params[p], pathValue, operation, renderedSchema)...) case helpers.Boolean: if _, err := strconv.ParseBool(ef); err != nil { validationErrors = append(validationErrors, errors.IncorrectQueryParamBool(params[p], ef, sch, pathValue, operation, renderedSchema)) } case helpers.Object: // check what style of encoding was used and then construct a map[string]interface{} // and pass that in as encoded JSON. var encodedObj map[string]interface{} switch params[p].Style { case helpers.DeepObject: encodedObj = helpers.ConstructParamMapFromDeepObjectEncoding(jk, sch) case helpers.PipeDelimited: encodedObj = helpers.ConstructParamMapFromPipeEncodingWithSchema(jk, sch) case helpers.SpaceDelimited: encodedObj = helpers.ConstructParamMapFromSpaceEncodingWithSchema(jk, sch) default: // form encoding is default. if contentWrapped { switch contentType { case helpers.JSONContentType: // we need to unmarshal the JSON into a map[string]interface{} encodedParams := make(map[string]interface{}) encodedObj = make(map[string]interface{}) if err := json.Unmarshal([]byte(ef), &encodedParams); err != nil { validationErrors = append(validationErrors, errors.IncorrectParamEncodingJSON(params[p], ef, sch, pathValue, operation, renderedSchema)) break skipValues } encodedObj[params[p].Name] = encodedParams } } else { encodedObj = helpers.ConstructParamMapFromFormEncodingArrayWithSchema(jk, sch) } } numErrors := len(validationErrors) validationErrors = append(validationErrors, ValidateParameterSchema(sch, encodedObj[params[p].Name].(map[string]interface{}), ef, "Query parameter", "The query parameter", params[p].Name, helpers.ParameterValidation, helpers.ParameterValidationQuery, v.options)...) if len(validationErrors) > numErrors { // we've already added an error for this, so we can skip the rest of the values break skipValues } case helpers.Array: // well we're already in an array, so we need to check the items schema // to ensure this array items matches the type // only check if items is a schema, not a boolean if sch.Items != nil && sch.Items.IsA() { validationErrors = append(validationErrors, ValidateQueryArray(sch, params[p], ef, contentWrapped, v.options, pathValue, operation, renderedSchema)...) } } } } } } else { // if the param is not in the requests, so let's check if this param is an // object, and if we should use default encoding and explode values. if params[p].Schema != nil { sch := params[p].Schema.Schema() if len(sch.Type) > 0 && sch.Type[0] == helpers.Object && params[p].IsDefaultFormEncoding() { // if the param is an object, and we're using default encoding, then we need to // validate the schema. decoded := helpers.ConstructParamMapFromQueryParamInputWithSchema(queryParams, sch) validationErrors = append(validationErrors, ValidateParameterSchema(sch, decoded, "", "Query array parameter", "The query parameter (which is an array)", params[p].Name, helpers.ParameterValidation, helpers.ParameterValidationQuery, v.options)...) break doneLooking } } // if there is no match, check if the param is required or not. if params[p].Required != nil && *params[p].Required { // Render schema for missing parameter var sch *base.Schema if params[p].Schema != nil { sch = params[p].Schema.Schema() } else { for pair := orderedmap.First(params[p].Content); pair != nil; pair = pair.Next() { sch = pair.Value().Schema.Schema() break } } // Get rendered schema for ReferenceSchema field in errors (uses cache if available) renderedSchema := GetRenderedSchema(sch, v.options) validationErrors = append(validationErrors, errors.QueryParameterMissing(params[p], pathValue, operation, renderedSchema)) } } } } errors.PopulateValidationErrors(validationErrors, request, pathValue) if len(validationErrors) > 0 { return false, validationErrors } // strict mode: check for undeclared query parameters if v.options.StrictMode { undeclaredParams := strict.ValidateQueryParams(request, params, v.options) for _, undeclared := range undeclaredParams { validationErrors = append(validationErrors, errors.UndeclaredQueryParamError( undeclared.Path, undeclared.Name, undeclared.Value, undeclared.DeclaredProperties, request.URL.Path, request.Method, )) } } if len(validationErrors) > 0 { return false, validationErrors } return true, nil } func (v *paramValidator) validateSimpleParam(sch *base.Schema, rawParam string, parsedParam any, parameter *v3.Parameter, pathTemplate string, operation string, renderedSchema string) (validationErrors []*errors.ValidationError) { // check if the param is within an enum if sch.Enum != nil { matchFound := false for _, enumVal := range sch.Enum { if strings.TrimSpace(rawParam) == fmt.Sprint(enumVal.Value) { matchFound = true break } } if !matchFound { return []*errors.ValidationError{errors.IncorrectQueryParamEnum(parameter, rawParam, sch, pathTemplate, operation, renderedSchema)} } } return ValidateSingleParameterSchema( sch, parsedParam, "Query parameter", "The query parameter", parameter.Name, helpers.ParameterValidation, helpers.ParameterValidationQuery, v.options, pathTemplate, operation, ) } libopenapi-validator-0.13.8/parameters/query_parameters_test.go000066400000000000000000003171271520534042400250430ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package parameters import ( "fmt" "net/http" "strings" "sync" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/paths" ) func TestNewValidator_QueryParamMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "Query parameter 'fishy' is missing", errors[0].Message) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "/a/fishy/on/a/dishy", errors[0].SpecPath) } func TestNewValidator_QueryParamNotMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamMinimumLength_violation(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: string minLength: 4 operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, errs := doc.BuildV3Model() require.NoError(t, errs) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "Query parameter 'fishy' failed to validate", errors[0].Message) } func TestNewValidator_QueryParamMaximumLength_violation(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: string maxLength: 1 operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, errs := doc.BuildV3Model() require.NoError(t, errs) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "Query parameter 'fishy' failed to validate", errors[0].Message) } //func TestNewValidator_QueryParamMinimum_ignoredviolation(t *testing.T) { // spec := `openapi: 3.1.0 //paths: // /a/fishy/on/a/dishy: // get: // parameters: // - name: fishy // in: query // required: true // schema: // type: object // operationId: locateFishy //` // // doc, err := libopenapi.NewDocument([]byte(spec)) // require.NoError(t, err) // m, errs := doc.BuildV3Model() // require.Len(t, errs, 0) // // v := NewParameterValidator(&m.Model) // // request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) // // valid, errors := v.ValidateQueryParams(request) // assert.False(t, valid) // assert.Equal(t, 1, len(errors)) // assert.Equal(t, "Query parameter 'fishy' failed to validate", errors[0].Message) //} func TestNewValidator_QueryParamMinimum(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: string minLength: 4 operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, errs := doc.BuildV3Model() require.NoError(t, errs) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=salmon", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamMaximum(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: string maxLength: 10 operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, errs := doc.BuildV3Model() require.NoError(t, errs) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=salmon", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamPost(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: post: parameters: - name: fishy in: query required: true schema: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamPut(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: put: parameters: - name: fishy in: query required: true schema: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPut, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamDelete(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: delete: parameters: - name: fishy in: query required: true schema: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodDelete, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamOptions(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: options: parameters: - name: fishy in: query required: true schema: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodOptions, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamHead(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: head: parameters: - name: fishy in: query required: true schema: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodHead, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamPatch(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: patch: parameters: - name: fishy in: query required: true schema: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPatch, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamTrace(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: trace: parameters: - name: fishy in: query required: true schema: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodTrace, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamBadPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: number operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/Not/Found/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.NotNil(t, errors) } func TestMewValidator_QueryParamMultiStringField(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: string enum: [cod, halibut] - name: dishy in: query required: true schema: type: string operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) assert.Nil(t, err) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=doc&dishy=halibut", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.NotNil(t, errors) } func TestMewValidator_QueryParamMultiNumberField(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: number enum: [1, 99] - name: dishy in: query required: true schema: type: number operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) assert.Nil(t, err) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=10&dishy=10", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.NotNil(t, errors) } func TestNewValidator_QueryParamWrongTypeNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: number operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.NotNil(t, errors) assert.Equal(t, "Query parameter 'fishy' is not a valid number", errors[0].Message) } func TestNewValidator_QueryParamValidTypeNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: number operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=123.4", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamMinimumNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: number minimum: 200 operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, errs := doc.BuildV3Model() require.NoError(t, errs) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=300.4", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamMinimumNumber_violation(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: number minimum: 200 operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, errs := doc.BuildV3Model() require.NoError(t, errs) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=123.4", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "Query parameter 'fishy' failed to validate", errors[0].Message) } func TestNewValidator_QueryParamMaximumNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: number maximum: 200 operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, errs := doc.BuildV3Model() require.NoError(t, errs) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=123", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamMaximumNumber_violation(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: number maximum: 200 operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, errs := doc.BuildV3Model() require.NoError(t, errs) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1234.5", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "Query parameter 'fishy' failed to validate", errors[0].Message) } func TestNewValidator_QueryParamValidTypeFloat(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: number operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=123.223", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamWrongTypeInteger_StringValue(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: integer operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.NotNil(t, errors) assert.Equal(t, "Query parameter 'fishy' is not a valid integer", errors[0].Message) } func TestNewValidator_QueryParamWrongTypeInteger_FloatValue(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: integer operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1.2", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.NotNil(t, errors) assert.Equal(t, "Query parameter 'fishy' is not a valid integer", errors[0].Message) } func TestNewValidator_QueryParamValidTypeInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: integer operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=123", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamMinimumInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: integer minimum: 200 operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, errs := doc.BuildV3Model() require.NoError(t, errs) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=300", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamMinimumInteger_violation(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: integer minimum: 200 operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, errs := doc.BuildV3Model() require.NoError(t, errs) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=123", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "Query parameter 'fishy' failed to validate", errors[0].Message) } func TestNewValidator_QueryParamMaximumInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: integer maximum: 200 operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, errs := doc.BuildV3Model() require.NoError(t, errs) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=123", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamMaximumInteger_violation(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: integer maximum: 200 operationId: locateFishy ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, errs := doc.BuildV3Model() require.NoError(t, errs) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1234", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "Query parameter 'fishy' failed to validate", errors[0].Message) } func TestNewValidator_QueryParamWrongTypeBool(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: boolean operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.NotNil(t, errors) assert.Equal(t, "Query parameter 'fishy' is not a valid boolean", errors[0].Message) } func TestNewValidator_QueryParamValidTypeBool(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: boolean operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=true", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamInvalidEnumString(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: string enum: [cod, halibut]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod&fishy=haddock", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Query parameter 'fishy' does not match allowed values", errors[0].Message) assert.Equal(t, "Instead of 'haddock', use one of the allowed values: 'cod, halibut'", errors[0].HowToFix) } func TestNewValidator_QueryParamInvalidEnumInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: integer enum: [1, 99]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=22", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Query parameter 'fishy' does not match allowed values", errors[0].Message) assert.Equal(t, "Instead of '22', use one of the allowed values: '1, 99'", errors[0].HowToFix) } func TestNewValidator_QueryParamValidEnumInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: integer enum: [1, 99]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamInvalidEnumNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: number enum: [1.2, 99.8]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=22.4", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Query parameter 'fishy' does not match allowed values", errors[0].Message) assert.Equal(t, "Instead of '22.4', use one of the allowed values: '1.2, 99.8'", errors[0].HowToFix) } func TestNewValidator_QueryParamValidEnumNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: number enum: [1.2, 99.8]` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1.2", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidDateFormat(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: string format: date` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=2024-12-25", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Empty(t, errors) } func TestNewValidator_QueryParamInvalidDateFormat(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: string format: date` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=12/25/2024", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) } func TestNewValidator_QueryParamValidDateTimeFormat(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: string format: date-time` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=2024-12-25T13:42:42Z", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Empty(t, errors) } func TestNewValidator_QueryParamInvalidDateTimeFormat(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: string format: date-time` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithFormatAssertions()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=2024-12-25", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) } func TestNewValidator_QueryParamValidTypeArrayString(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: array items: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod&fishy=haddock", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Nil(t, errors) } func TestNewValidator_QueryParamInvalidTypeArrayStringEnum(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: array items: type: string enum: [cod, halibut] operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod&fishy=haddock", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Instead of 'haddock', use one of the allowed values: 'cod, halibut'", errors[0].HowToFix) } func TestNewValidator_QueryParamInvalidTypeArrayIntegerEnum(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: array items: type: integer enum: [1, 99] operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1&fishy=2", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Instead of '2', use one of the allowed values: '1, 99'", errors[0].HowToFix) } func TestNewValidator_QueryParamInvalidTypeArrayInteger(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: array items: type: integer operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod&fishy=haddock", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 2) assert.Equal(t, "Query array parameter 'fishy' is not a valid integer", errors[0].Message) assert.Equal(t, "The query parameter (which is an array) 'fishy' is defined as being an integer, "+ "however the value 'cod' is not a valid integer", errors[0].Reason) assert.Equal(t, "Query array parameter 'fishy' is not a valid integer", errors[1].Message) assert.Equal(t, "The query parameter (which is an array) 'fishy' is defined as being an integer, "+ "however the value 'haddock' is not a valid integer", errors[1].Reason) } func TestNewValidator_QueryParamInvalidTypeArrayNumberEnum(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: array items: type: number enum: [1.2, 99.8] operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1.2&fishy=2.3", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Instead of '2.3', use one of the allowed values: '1.2, 99.8'", errors[0].HowToFix) } func TestNewValidator_QueryParamInvalidTypeArrayNumber(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: array items: type: number operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod&fishy=haddock", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 2) assert.Equal(t, "Query array parameter 'fishy' is not a valid number", errors[0].Message) assert.Equal(t, "The query parameter (which is an array) 'fishy' is defined as being a number, "+ "however the value 'cod' is not a valid number", errors[0].Reason) assert.Equal(t, "Query array parameter 'fishy' is not a valid number", errors[1].Message) assert.Equal(t, "The query parameter (which is an array) 'fishy' is defined as being a number, "+ "however the value 'haddock' is not a valid number", errors[1].Reason) } func TestNewValidator_QueryParamValidEnumStringType(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: array items: type: string enum: [cod, haddock] operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod,haddock", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidExplodedType(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true explode: true schema: type: array items: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod,haddock", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 2) } func TestNewValidator_QueryParamValidExplodedArray(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true explode: true schema: type: array items: type: number operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1&fishy=2", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamInvalidExplodedArrayAndInvalidType(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true explode: true schema: type: array items: type: number operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=haddock&fishy=cod", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 2) } func TestNewValidator_QueryParamValidExploded(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true explode: false schema: type: array items: type: string operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod,haddock,mackrel", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamInvalidTypeArrayBool(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: array items: type: boolean operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod&fishy=haddock", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 2) assert.Equal(t, "Query array parameter 'fishy' is not a valid boolean", errors[0].Message) assert.Equal(t, "The query parameter (which is an array) 'fishy' is defined as being a boolean, "+ "however the value 'cod' is not a valid true/false value", errors[0].Reason) assert.Equal(t, "Query array parameter 'fishy' is not a valid boolean", errors[1].Message) assert.Equal(t, "The query parameter (which is an array) 'fishy' is defined as being a boolean, "+ "however the value 'haddock' is not a valid true/false value", errors[1].Reason) } func TestNewValidator_QueryParamInvalidTypeArrayFloat(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: array items: type: number operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=12&fishy=12.12&fishy=1234567789.1233456657", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamInvalidTypeArrayFloatPipeDelimited(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query style: pipeDelimited required: true schema: type: array items: type: number operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=12|12345.2344|22111233444.342452435", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamInvalidTypeArrayObjectPipeDelimited(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query style: pipeDelimited required: true schema: type: object properties: ocean: type: number silver: type: number required: [ocean, silver] operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=ocean|12|silver|12.2345", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidTypeObject(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: array items: type: object properties: vinegar: type: boolean chips: type: number required: - vinegar - chips operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy={\"cod\":\"cakes\"}&fishy={\"crab\":\"legs\"}", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 2) assert.Equal(t, "Query array parameter 'fishy' failed to validate", errors[0].Message) assert.Equal(t, "The query parameter (which is an array) 'fishy' is defined as an object, "+ "however it failed to pass a schema validation", errors[0].Reason) assert.Equal(t, "missing properties 'vinegar', 'chips'", errors[0].SchemaValidationErrors[0].Reason) assert.Equal(t, "/required", errors[0].SchemaValidationErrors[0].KeywordLocation) } func TestNewValidator_QueryParamValidTypeObjectPropType_Invalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true content: application/json: schema: type: object properties: vinegar: type: boolean chips: type: number required: - vinegar - chips operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy={\"vinegar\":\"cakes\",\"chips\":\"hello\"}&fishy={\"vinegar\":true,\"chips\":123.223}", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestNewValidator_QueryParamValidTypeObjectPropTypeFloat(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true content: application/json: schema: type: object properties: vinegar: type: boolean chips: type: number required: - vinegar - chips operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy={\"vinegar\":true,\"chips\":12}&fishy={\"vinegar\":true,\"chips\":123.333}", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamInvalidTypeObjectArrayPropType_Ref(t *testing.T) { spec := `openapi: 3.1.0 components: parameters: something: name: somethingElse in: query content: application/json: schema: type: array items: type: object properties: vinegar: type: boolean chips: type: number required: - vinegar - chips paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true content: $ref: "#/components/parameters/something/content" operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy={\"vinegar\":\"cakes\",\"chips\":\"hello\"}&fishy={\"vinegar\":true,\"chips\":123}", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestNewValidator_QueryParamValidTypeObjectArrayPropType_Ref(t *testing.T) { spec := `openapi: 3.1.0 components: parameters: something: name: somethingElse in: query content: application/json: schema: type: array items: type: object properties: vinegar: type: boolean chips: type: number required: - vinegar - chips paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true content: $ref: "#/components/parameters/something/content" operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy={\"vinegar\":false,\"chips\":999}&fishy={\"vinegar\":true,\"chips\":123}", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidTypeObjectPropType_Ref(t *testing.T) { spec := `openapi: 3.1.0 components: parameters: fishy: name: fishy in: query schema: type: object properties: vinegar: type: boolean chips: type: number required: - vinegar - chips paths: /a/fishy/on/a/dishy: get: parameters: - $ref: "#/components/parameters/fishy" operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?vinegar=true&chips=12", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidTypeObjectPropType_RefInvalid(t *testing.T) { spec := `openapi: 3.1.0 components: parameters: fishy: name: fishy in: query schema: type: object properties: vinegar: type: boolean chips: type: number required: - vinegar - chips paths: /a/fishy/on/a/dishy: get: parameters: - $ref: "#/components/parameters/fishy" operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?vinegar=true&chips=false", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "got boolean, want number", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_QueryParamValidTypeObjectPropType_RefViaContentWrapped(t *testing.T) { spec := `openapi: 3.1.0 components: parameters: fishy: name: fishy in: query content: application/json: schema: type: object properties: vinegar: type: boolean chips: type: number required: - vinegar - chips paths: /a/fishy/on/a/dishy: get: parameters: - $ref: "#/components/parameters/fishy" operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy={\"vinegar\":false,\"chips\":999}", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidTypeObjectPropType_RefViaContentWrappedInvalid(t *testing.T) { spec := `openapi: 3.1.0 components: parameters: fishy: name: fishy in: query content: application/json: schema: type: object properties: vinegar: type: boolean chips: type: number required: - vinegar - chips paths: /a/fishy/on/a/dishy: get: parameters: - $ref: "#/components/parameters/fishy" operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy={\"vinegar\":false,\"chips\":\"I am invalid\"}", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "got string, want number", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_QueryParamValidTypeObjectPropType_JSONInvalid(t *testing.T) { spec := `openapi: 3.1.0 components: parameters: fishy: name: fishy in: query content: application/json: schema: type: object properties: vinegar: type: boolean chips: type: number required: - vinegar - chips paths: /a/fishy/on/a/dishy: get: parameters: - $ref: "#/components/parameters/fishy" operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=I am not json", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Query parameter 'fishy' is not valid JSON", errors[0].Message) } func TestNewValidator_QueryParamInvalidTypeObjectPropType_Ref(t *testing.T) { spec := `openapi: 3.1.0 components: schema_validation: chippy: type: object properties: vinegar: type: boolean chips: type: number required: - vinegar - chips parameters: fishy: name: fishy in: query content: application/json: schema: $ref: "#/components/schema_validation/chippy" paths: /a/fishy/on/a/dishy: get: parameters: - $ref: "#/components/parameters/fishy" operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy={\"vinegar\":1234,\"chips\":false}", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestNewValidator_QueryParamValidateStyle_AllowReserved(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true explode: true schema: type: array items: type: string operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=$$oh", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "parameter values need to URL Encoded to ensure "+ "reserved values are correctly encoded, for example: '%24%24oh'", errors[0].HowToFix) } func TestNewValidator_QueryParamValidateStyle_ValidObjectArrayNoExplode(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: array items: type: string operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod,haddock,mackrel", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_InValidObjectArrayNoExplode(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true explode: true allowReserved: true schema: type: array items: type: string operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod,haddock,mackrel", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Query parameter 'fishy' is not exploded correctly", errors[0].Message) } func TestNewValidator_QueryParamValidateStyle_SpaceDelimitedIncorrectlyExploded(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: spaceDelimited schema: type: array items: type: string operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod&fishy=haddock&fishy=mackrel", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Query parameter 'fishy' delimited incorrectly", errors[0].Message) } func TestNewValidator_QueryParamValidateStyle_PipeDelimitedObjectValidExplode(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: pipeDelimited schema: type: array items: type: string operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod|haddock|mackrel", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_PipeDelimitedObjectInvalidExplode(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: pipeDelimited schema: type: array items: type: string operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod|haddock|mackrel&fishy=breaded|cooked|fried", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) } func TestNewValidator_QueryParamValidateStyle_PipeDelimitedObjectValid(t *testing.T) { spec := ` openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: plate in: query required: true style: pipeDelimited schema: type: array items: type: string - name: fishy in: query required: true style: pipeDelimited schema: type: array items: type: string operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod|haddock|mackrel&plate=flat|round", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_PipeDelimitedObjectDecode(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: pipeDelimited schema: type: object properties: fish: type: string enum: - salmon - tuna - cod dish: type: string enum: - salad - soup - stew required: - fish - dish operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=fish|salmon|dish|stew", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_PipeDelimitedObjectDecodeInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: pipeDelimited schema: type: object properties: fish: type: string enum: - salmon - tuna - cod dish: type: string enum: - salad - soup - stew required: - fish - dish operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=fish|salmon|dish|cakes", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "value must be one of 'salad', 'soup', 'stew'", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_QueryParamValidateStyle_SpaceDelimitedObjectDecode(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: spaceDelimited schema: type: object properties: fish: type: string enum: - salmon - tuna - cod dish: type: string enum: - salad - soup - stew required: - fish - dish operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=fish%20salmon%20dish%20stew", nil) // dumb, don't do this. valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_SpaceDelimitedObjectDecodeInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: spaceDelimited schema: type: object properties: fish: type: string enum: - salmon - tuna - cod dish: type: string enum: - salad - soup - stew required: - fish - dish operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=fish%20salmon%20dish%20coffee", nil) // dumb, don't do this. valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "value must be one of 'salad', 'soup', 'stew'", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_QueryParamValidateStyle_PipeDelimitedObjectInvalidMultiple(t *testing.T) { spec := ` openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: plate in: query required: true style: pipeDelimited schema: type: array items: type: string - name: fishy in: query required: true style: pipeDelimited schema: type: array items: type: string operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=cod|haddock|mackrel&plate=flat,round", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_DeepObjectMultiValuesNoSchema(t *testing.T) { spec := `--- openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: deepObject schema: type: object operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy[ocean]=atlantic&fishy[salt]=12", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_DeepObjectMultiValuesInvalid(t *testing.T) { spec := `--- openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: deepObject schema: type: object operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=atlantic&fishy=12", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 2) assert.Equal(t, "The query parameter 'fishy' has the 'deepObject' style defined, "+ "There are multiple values (2) supplied, instead of a single value", errors[0].Reason) } func TestNewValidator_QueryParamValidateStyle_FormEncoding(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: object properties: ocean: type: string fins: type: number required: [ocean, fins] - name: dishy in: query required: [hot, salty] schema: type: object properties: hot: type: boolean salty: type: boolean operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=ocean,atlantic,fins,4&dishy=hot,true,salty,true", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_FormEncodingInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: object properties: ocean: type: string fins: type: number required: [ocean, fins] - name: dishy in: query schema: required: [hot, salty] type: object properties: hot: type: boolean salty: type: boolean operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=ocean,atlantic,fins,4&dishy=hot,no,salty,why", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "got string, want boolean", errors[0].SchemaValidationErrors[0].Reason) assert.Equal(t, "got string, want boolean", errors[0].SchemaValidationErrors[1].Reason) } func TestNewValidator_QueryParamValidateStyle_FormEncodingArray(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true schema: type: array items: type: number - name: dishy in: query required: true schema: type: array items: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1,2,3&dishy=a,little,plate", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_FormEncodingArrayExplode(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query explode: true required: true schema: type: array items: type: number - name: dishy in: query explode: true required: true schema: type: array items: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1&fishy=2&fishy=3&dishy=a&dishy=little&dishy=dish", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_FormEncodingArrayExplodeInvalid(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query explode: true required: true schema: type: array items: type: number - name: dishy in: query explode: true required: true schema: type: array items: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1,2,3&dishy=little,dishy", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 4) assert.Equal(t, "The query parameter 'fishy' has a default or 'form' encoding defined, however the "+ "value '1,2,3' is encoded as an object or an array using commas. "+ "The contract defines the explode value to set to 'true'", errors[0].Reason) } func TestNewValidator_QueryParamValidateStyle_PipeDelimitedValid(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: pipeDelimited schema: type: array items: type: number - name: dishy in: query style: pipeDelimited required: true schema: type: array items: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1|2|3&dishy=little|dishy", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_SpaceDelimitedValid(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: spaceDelimited schema: type: array items: type: number - name: dishy in: query style: spaceDelimited required: true schema: type: array items: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1%202%203&dishy=little%20dishy", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_SpaceDelimitedInvalidSchema(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: spaceDelimited schema: type: array items: type: number - name: dishy in: query style: spaceDelimited required: true schema: type: array items: type: string operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy=1|%202%203&dishy=little%20dishy", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Convert the value '1|' into a number", errors[0].HowToFix) } func TestNewValidator_QueryParamValidateStyle_DeepObjectMultiValuesFailedSchema(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: deepObject schema: type: object properties: ocean: type: string salt: type: boolean required: [ocean, salt] operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy[ocean]=atlantic&fishy[salt]=12", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "got number, want boolean", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_QueryParamSetPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: deepObject schema: type: object properties: ocean: type: string salt: type: boolean required: [ocean, salt] operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy[ocean]=atlantic&fishy[salt]=12", nil) // preset the path path, _, pv := paths.FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) valid, errors := v.ValidateQueryParamsWithPathItem(request, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "got number, want boolean", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_QueryParamSetPath_notfound(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: deepObject schema: type: object properties: ocean: type: string salt: type: boolean required: [ocean, salt] operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/beef/on/a/dishy?fishy[ocean]=atlantic&fishy[salt]=12", nil) // preset the path path, _, pv := paths.FindPath(request, &m.Model, nil) valid, errors := v.ValidateQueryParamsWithPathItem(request, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "GET Path '/a/beef/on/a/dishy' not found", errors[0].Message) } func TestNewValidator_QueryParamValidateStyle_DeepObjectMultiValuesFailedMultipleSchemas(t *testing.T) { spec := `--- openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: deepObject schema: type: object properties: ocean: type: string salt: type: boolean required: - ocean - salt - name: dishy in: query required: true style: deepObject schema: type: object properties: size: type: string numCracks: type: number required: - size - numCracks - name: cake in: query required: true style: deepObject schema: type: object properties: message: type: string numCandles: type: number required: - message - numCandles operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/a/fishy/on/a/dishy?fishy[ocean]=atlantic&fishy[salt]=12"+ "&dishy[size]=big&dishy[numCracks]=false"+ "&cake[message]=happy%20birthday&cake[numCandles]=false", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 3) assert.Equal(t, "got number, want boolean", errors[0].SchemaValidationErrors[0].Reason) assert.Equal(t, "got boolean, want number", errors[1].SchemaValidationErrors[0].Reason) assert.Equal(t, "got boolean, want number", errors[2].SchemaValidationErrors[0].Reason) } func TestNewValidator_ValidateEncodedObjectIsCorrect(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: deepObject schema: type: object properties: ocean: type: string salt: type: boolean required: [ocean, salt] operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() sch := m.Model.Paths.PathItems.GetOrZero("/a/fishy/on/a/dishy").Get.Parameters[0].Schema s := sch.Schema() // this is not compatible. rawObject := map[int]int{ 1: 2, } errs := ValidateParameterSchema(s, rawObject, "cake", "burger", "lemons", "pizza", "rice", "herbs", nil) assert.Len(t, errs, 1) assert.Equal(t, "lemons 'pizza' is defined as an object, "+ "however it failed to be decoded as an object", errs[0].Reason) } // https://github.com/pb33f/wiretap/issues/82 func TestNewValidator_QueryParamValidateStyle_DeepObjectAdditionalPropertiesArray(t *testing.T) { spec := `openapi: 3.1.0 paths: /anything/queryParams/deepObject/map: get: operationId: deepObjectQueryParamsMap parameters: - name: mapArrParam in: query style: deepObject schema: type: object additionalProperties: type: array items: type: string example: { "test": ["test", "test2"], "test2": ["test3", "test4"] } responses: "200": description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/anything/queryParams/deepObject/map?mapArrParam[test2]=test3&mapArrParam[test2]=test4&mapArrParam[test]=test&mapArrParam[test]=test2", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } // https://github.com/pb33f/wiretap/issues/82 func TestNewValidator_QueryParamValidateStyle_DeepObjectAdditionalPropertiesArrayTop(t *testing.T) { spec := `openapi: 3.1.0 paths: /anything/queryParams/deepObject/map: get: operationId: deepObjectQueryParamsMap parameters: - name: mapArrParam in: query style: deepObject schema: type: array items: type: string example: { "test": ["test", "test2"], "test2": ["test3", "test4"] } responses: "200": description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/anything/queryParams/deepObject/map?mapArrParam[test2]=test3&mapArrParam[test2]=test4&mapArrParam[test]=test&mapArrParam[test]=test2", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } // https://github.com/pb33f/wiretap/issues/82 func TestNewValidator_QueryParamValidateStyle_DeepObjectAdditionalPropertiesArrayNumericStrings(t *testing.T) { spec := `openapi: 3.1.0 paths: /anything/queryParams/deepObject/map: get: operationId: deepObjectQueryParamsMap parameters: - name: mapArrParam in: query style: deepObject schema: type: object additionalProperties: type: array items: type: string example: { "test": ["test", "test2"], "test2": ["test3", "test4"] } responses: "200": description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/anything/queryParams/deepObject/map?mapArrParam[test2]=23&mapArrParam[test2]=test4&mapArrParam[test]=test&mapArrParam[test]=test2", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } // https://github.com/pb33f/wiretap/issues/83 func TestNewValidator_QueryParamValidateStyle_BadSchemaDeepObject(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 0.1.0 security: - apiKeyAuth: [] paths: /anything/queryParams/deepObject/obj: get: operationId: deepObjectQueryParamsObject parameters: - name: objParam in: query style: deepObject schema: $ref: "components.yaml#/components/schemas/simpleObject" required: true responses: "200": description: OK components: securitySchemes: apiKeyAuth: type: apiKey in: header name: Authorization description: Authenticate using an API Key generated via our platform.` doc, _ := libopenapi.NewDocument([]byte(spec)) m, err := doc.BuildV3Model() assert.Error(t, err) // path build will fail because of missing schema. v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "http://localhost:9090/anything/queryParams/deepObject/obj?objParam=blahdedahdedah", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } // https://github.com/pb33f/wiretap/issues/83 func TestNewValidator_QueryParamValidateStyle_BadSchemaDeepObject_Inline(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 0.1.0 security: - apiKeyAuth: [] paths: /anything/queryParams/deepObject/obj: get: operationId: deepObjectQueryParamsObject parameters: - name: objParam in: query style: deepObject schema: type: object properties: cake: type: string required: true responses: "200": description: OK components: securitySchemes: apiKeyAuth: type: apiKey in: header name: Authorization description: Authenticate using an API Key generated via our platform.` doc, _ := libopenapi.NewDocument([]byte(spec)) m, err := doc.BuildV3Model() assert.NoError(t, err) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "http://localhost:9090/anything/queryParams/deepObject/obj?objParam=blahdedahdedah", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "The query parameter 'objParam' is defined as an object,"+ " however it failed to pass a schema validation", errors[0].Reason) } // https://github.com/pb33f/libopenapi-validator/issues/83 func TestNewValidator_QueryParamValidateStyle_NestedDeepObject(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 paths: /path: get: parameters: - name: obj in: query style: deepObject schema: type: object properties: root: type: string nested: type: object properties: child: type: string required: true responses: "200": description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) m, err := doc.BuildV3Model() require.NoError(t, err) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/path?obj[root]=test1&obj[nested][child]=test2", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_NestedDeepObjectInvalidLeafType(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 paths: /path: get: parameters: - name: obj in: query style: deepObject schema: type: object properties: nested: type: object properties: child: type: boolean required: true responses: "200": description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) m, err := doc.BuildV3Model() require.NoError(t, err) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/path?obj[nested][child]=not-a-bool", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) require.Len(t, errors, 1) require.NotEmpty(t, errors[0].SchemaValidationErrors) assert.Equal(t, "got string, want boolean", errors[0].SchemaValidationErrors[0].Reason) assert.Equal(t, "$.nested.child", errors[0].SchemaValidationErrors[0].FieldPath) } func TestNewValidator_QueryParamValidateStyle_NestedDeepObjectRequiredLeaf(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 paths: /path: get: parameters: - name: obj in: query style: deepObject schema: type: object required: [nested] properties: nested: type: object required: [child] properties: child: type: string other: type: string required: true responses: "200": description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) m, err := doc.BuildV3Model() require.NoError(t, err) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/path?obj[nested][other]=value", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) require.Len(t, errors, 1) require.NotEmpty(t, errors[0].SchemaValidationErrors) assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "missing") } func TestNewValidator_QueryParamValidateStyle_NestedDeepObjectArray(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 paths: /path: get: parameters: - name: obj in: query style: deepObject schema: type: object properties: nested: type: object properties: tags: type: array items: type: string required: true responses: "200": description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) m, err := doc.BuildV3Model() require.NoError(t, err) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/path?obj[nested][tags]=123&obj[nested][tags]=456", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_NestedDeepObjectAdditionalPropertiesArray(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 paths: /path: get: parameters: - name: obj in: query style: deepObject schema: type: object properties: filters: type: object additionalProperties: type: array items: type: string required: true responses: "200": description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) m, err := doc.BuildV3Model() require.NoError(t, err) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/path?obj[filters][tag]=123&obj[filters][tag]=456", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_QueryParamValidateStyle_NestedDeepObjectPathConflict(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 paths: /path: get: parameters: - name: obj in: query style: deepObject schema: type: object properties: nested: type: object properties: child: type: string required: true responses: "200": description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) m, err := doc.BuildV3Model() require.NoError(t, err) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/path?obj[nested]=bad&obj[nested][child]=ok", nil) valid, validationErrors := v.ValidateQueryParams(request) assert.False(t, valid) require.NotEmpty(t, validationErrors) assert.Contains(t, validationErrors[0].Reason, "property path 'nested'") assert.Contains(t, validationErrors[0].Reason, "'nested.child'") } func TestNewValidator_ValidateRawMap(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: get: parameters: - name: fishy in: query required: true style: deepObject schema: type: object properties: ocean: type: string salt: type: boolean required: [ocean, salt] operationId: locateFishy` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() sch := m.Model.Paths.PathItems.GetOrZero("/a/fishy/on/a/dishy").Get.Parameters[0].Schema s := sch.Schema() // this is not compatible. rawObject := map[int]int{ 1: 2, } errs := ValidateParameterSchema(s, rawObject, "cake", "burger", "lemons", "pizza", "rice", "herbs", nil) assert.Len(t, errs, 1) assert.Equal(t, "lemons 'pizza' is defined as an object, "+ "however it failed to be decoded as an object", errs[0].Reason) } // https://github.com/pb33f/wiretap/issues/83 func TestNewValidator_QueryParamValidateStyle_EmptyDeepObject(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 0.1.0 security: - apiKeyAuth: [] paths: /anything/queryParams/deepObject/obj: get: operationId: deepObjectQueryParamsObject parameters: - name: objParam in: query style: deepObject schema: type: object properties: cake: type: string required: true responses: "200": description: OK components: securitySchemes: apiKeyAuth: type: apiKey in: header name: Authorization description: Authenticate using an API Key generated via our platform.` doc, _ := libopenapi.NewDocument([]byte(spec)) m, err := doc.BuildV3Model() assert.NoError(t, err) v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "http://localhost:9090/anything/queryParams/deepObject/obj?objParam", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } // https://github.com/pb33f/libopenapi-validator/issues/140 func TestNewValidator_CheckQueryParamsMaxItems(t *testing.T) { spec := `openapi: 3.1.0 info: title: ID List API version: "1.0.0" paths: /items: get: parameters: - name: id in: query required: true style: form explode: false schema: type: array items: type: integer maxItems: 10 responses: '200': description: OK '400': description: Invalid input` doc, _ := libopenapi.NewDocument([]byte(spec)) m, err := doc.BuildV3Model() assert.NoError(t, err) v := NewParameterValidator(&m.Model) // Helper to build a request makeRequest := func(ids []int) *http.Request { values := make([]string, len(ids)) for i, id := range ids { values[i] = fmt.Sprintf("%d", id) } req, _ := http.NewRequest(http.MethodGet, "/items?id="+strings.Join(values, ","), nil) return req } // Test invalid case (12 items) invalidReq := makeRequest([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}) valid, errors := v.ValidateQueryParams(invalidReq) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, errors[0].Reason, "The query parameter (which is an array) 'id' has a maximum item length of 10, however the request provided 12 items") } // https://github.com/pb33f/libopenapi-validator/issues/140 func TestNewValidator_CheckQueryParamsMinItems(t *testing.T) { spec := `openapi: 3.1.0 info: title: ID List API version: "1.0.0" paths: /items: get: parameters: - name: id in: query required: true style: form explode: false schema: type: array items: type: integer minItems: 10 responses: '200': description: OK '400': description: Invalid input` doc, _ := libopenapi.NewDocument([]byte(spec)) m, err := doc.BuildV3Model() assert.NoError(t, err) v := NewParameterValidator(&m.Model) // Helper to build a request makeRequest := func(ids []int) *http.Request { values := make([]string, len(ids)) for i, id := range ids { values[i] = fmt.Sprintf("%d", id) } req, _ := http.NewRequest(http.MethodGet, "/items?id="+strings.Join(values, ","), nil) return req } // Test invalid case (12 items) invalidReq := makeRequest([]int{1, 2, 3}) valid, errors := v.ValidateQueryParams(invalidReq) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, errors[0].Reason, "The query parameter (which is an array) 'id' has a minimum items length of 10, however the request provided 3 items") } func TestNewValidator_CheckQueryParamsUniqueItems(t *testing.T) { spec := `openapi: 3.1.0 info: title: ID List API version: "1.0.0" paths: /items: get: parameters: - name: id in: query required: true style: form explode: false schema: type: array items: type: string uniqueItems: true responses: '200': description: OK '400': description: Invalid input` doc, _ := libopenapi.NewDocument([]byte(spec)) m, err := doc.BuildV3Model() assert.NoError(t, err) v := NewParameterValidator(&m.Model) // Helper to build a request makeRequest := func(ids []string) *http.Request { values := make([]string, len(ids)) copy(values, ids) req, _ := http.NewRequest(http.MethodGet, "/items?id="+strings.Join(values, ","), nil) return req } // Test invalid case (12 items) invalidReq := makeRequest([]string{"cake", "cake", "meat", "potatoes", "eggs", "meat"}) valid, errors := v.ValidateQueryParams(invalidReq) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, errors[0].Reason, "The query parameter (which is an array) 'id' contains the following duplicates: 'cake, meat'") } func TestNewValidator_QueryParamWithBracketsInName(t *testing.T) { // Test for issue #210: parameter names with brackets (e.g., match[]) // should be recognized when URL-encoded as match%5B%5D // https://github.com/pb33f/libopenapi-validator/issues/210 spec := `openapi: 3.1.0 paths: /api/query: get: parameters: - name: "match[]" in: query required: true explode: false schema: type: array items: type: string - name: start in: query schema: type: integer - name: end in: query schema: type: integer operationId: queryData ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // URL with encoded brackets: match%5B%5D=up (decodes to match[]=up) request, _ := http.NewRequest(http.MethodGet, "https://example.com/api/query?match%5B%5D=up&start=0&end=100", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid, "Expected validation to pass, got errors: %v", errors) assert.Empty(t, errors) } func TestNewValidator_QueryParamWithBracketsInName_Missing(t *testing.T) { // Test that missing bracket parameters are still reported correctly spec := `openapi: 3.1.0 paths: /api/query: get: parameters: - name: "match[]" in: query required: true schema: type: array items: type: string operationId: queryData ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Request without the required match[] parameter request, _ := http.NewRequest(http.MethodGet, "https://example.com/api/query?other=value", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Query parameter 'match[]' is missing", errors[0].Message) } func TestNewValidator_QueryParams_StrictMode_UndeclaredParam(t *testing.T) { spec := `openapi: 3.1.0 paths: /api/search: get: parameters: - name: query in: query required: true schema: type: string - name: limit in: query schema: type: integer operationId: search ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) // Request with undeclared 'extra' parameter request, _ := http.NewRequest(http.MethodGet, "https://example.com/api/search?query=test&limit=10&extra=undeclared", nil) valid, errors := v.ValidateQueryParams(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "extra") assert.Contains(t, errors[0].Message, "not declared") } func TestNewValidator_QueryParams_StrictMode_ValidRequest(t *testing.T) { spec := `openapi: 3.1.0 paths: /api/search: get: parameters: - name: query in: query required: true schema: type: string - name: limit in: query schema: type: integer operationId: search ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithStrictMode()) // Request with only declared parameters request, _ := http.NewRequest(http.MethodGet, "https://example.com/api/search?query=test&limit=10", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid) assert.Len(t, errors, 0) } // TestNewValidator_QueryParamExplodedObjectStringType_Issue91 reproduces GitHub issue #91: // exploded object query params with type: string properties that look numeric should not // be eagerly cast to numbers before schema validation. func TestNewValidator_QueryParamExplodedObjectStringType_Issue91(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: get: parameters: - name: filter in: query required: true explode: true schema: type: object properties: item_count: type: string search_term: type: string required: - item_count - search_term operationId: listProducts` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://example.com/products?item_count=10&search_term=foo", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid, "validation should pass: item_count=10 is valid for type: string") assert.Empty(t, errors) } // TestNewValidator_QueryParamExplodedObjectMixedTypes_Issue91 tests that mixed types // in an exploded object are correctly handled: string properties stay strings, // integer properties get cast. func TestNewValidator_QueryParamExplodedObjectMixedTypes_Issue91(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: get: parameters: - name: filter in: query required: true explode: true schema: type: object properties: item_count: type: string page: type: integer minimum: 1 required: - item_count - page operationId: listProducts` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://example.com/products?item_count=10&page=2", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid, "validation should pass: item_count=10 is string, page=2 is integer >= 1") assert.Empty(t, errors) } // TestNewValidator_QueryParamExactIssue91Reproduction uses the exact spec and URL from // https://github.com/pb33f/wiretap/issues/91 to verify the fix. func TestNewValidator_QueryParamExactIssue91Reproduction(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 0.1.0 paths: /anything/queryParams/form/camelObj: get: operationId: formQueryParamsCamelObject parameters: - name: obj_param_exploded in: query explode: true schema: type: object properties: search_term: type: string example: foo item_count: type: string example: "10" required: true - name: obj_param in: query explode: false schema: type: object properties: encoded_term: type: string example: bar encoded_count: type: string example: "11" responses: "200": description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://localhost:35123/anything/queryParams/form/camelObj?item_count=10&obj_param=encoded_count%2C11%2Cencoded_term%2Cbar&search_term=foo", nil) valid, errors := v.ValidateQueryParams(request) assert.True(t, valid, "issue #91: item_count=10 with type: string should not fail with 'expected string, but got number'") assert.Empty(t, errors) } libopenapi-validator-0.13.8/parameters/validate_parameter.go000066400000000000000000000316061520534042400242400ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package parameters import ( "encoding/json" "fmt" "net/url" "reflect" "strings" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/utils" "github.com/santhosh-tekuri/jsonschema/v6" "go.yaml.in/yaml/v4" "golang.org/x/text/language" "golang.org/x/text/message" stdError "errors" "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" ) func ValidateSingleParameterSchema( schema *base.Schema, rawObject any, entity string, reasonEntity string, name string, validationType string, subValType string, o *config.ValidationOptions, pathTemplate string, operation string, ) (validationErrors []*errors.ValidationError) { var jsch *jsonschema.Schema var jsonSchema []byte // Try cache lookup first - avoids expensive schema compilation on each request if o != nil && o.SchemaCache != nil && schema != nil && schema.GoLow() != nil { hash := schema.GoLow().Hash() if cached, ok := o.SchemaCache.Load(hash); ok && cached != nil && cached.CompiledSchema != nil { jsch = cached.CompiledSchema } } // Cache miss - compile the schema if jsch == nil { // Get the JSON Schema for the parameter definition. var err error jsonSchema, err = buildJsonRender(schema) if err != nil { return validationErrors } // Attempt to compile the JSON Schema jsch, err = helpers.NewCompiledSchema(name, jsonSchema, o) if err != nil { return validationErrors } // Store in cache for future requests if o != nil && o.SchemaCache != nil && schema != nil && schema.GoLow() != nil { hash := schema.GoLow().Hash() renderCtx := base.NewInlineRenderContextForValidation() renderedInline, _ := schema.RenderInlineWithContext(renderCtx) referenceSchema := string(renderedInline) var renderedNode yaml.Node _ = yaml.Unmarshal(renderedInline, &renderedNode) o.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ Schema: schema, RenderedInline: renderedInline, ReferenceSchema: referenceSchema, RenderedJSON: jsonSchema, CompiledSchema: jsch, RenderedNode: &renderedNode, }) } } // Validate the object and report any errors. scErrs := jsch.Validate(rawObject) var werras *jsonschema.ValidationError if stdError.As(scErrs, &werras) { validationErrors = formatJsonSchemaValidationError(schema, werras, entity, reasonEntity, name, validationType, subValType, pathTemplate, operation) } return validationErrors } // buildJsonRender build a JSON render of the schema. func buildJsonRender(schema *base.Schema) ([]byte, error) { if schema == nil { // Sanity Check return nil, stdError.New("buildJSONRender nil pointer") } renderedSchema, err := schema.Render() if err != nil { return nil, err } return utils.ConvertYAMLtoJSON(renderedSchema) } // GetRenderedSchema returns a YAML string representation of the schema for error messages. // It first checks the schema cache for a pre-rendered version, falling back to fresh rendering. // This avoids expensive re-rendering on each validation when the cache is available. func GetRenderedSchema(schema *base.Schema, opts *config.ValidationOptions) string { if schema == nil { return "" } // Try cache lookup first if opts != nil && opts.SchemaCache != nil && schema.GoLow() != nil { hash := schema.GoLow().Hash() if cached, ok := opts.SchemaCache.Load(hash); ok && cached != nil && len(cached.RenderedInline) > 0 { return string(cached.RenderedInline) } } // Cache miss - render fresh as YAML using validation mode renderCtx := base.NewInlineRenderContextForValidation() rendered, _ := schema.RenderInlineWithContext(renderCtx) return string(rendered) } // ValidateParameterSchema will validate a parameter against a raw object, or a blob of json/yaml. // It will return a list of validation errors, if any. // // schema: the schema to validate against // rawObject: the object to validate (leave empty if using a blob) // rawBlob: the blob to validate (leave empty if using an object) // entity: the entity being validated // reasonEntity: the entity that caused the validation to be called // name: the name of the parameter // validationType: the type of validation being performed // subValType: the type of sub-validation being performed func ValidateParameterSchema( schema *base.Schema, rawObject any, rawBlob, entity, reasonEntity, name, validationType, subValType string, validationOptions *config.ValidationOptions, ) []*errors.ValidationError { var validationErrors []*errors.ValidationError var jsch *jsonschema.Schema var jsonSchema []byte // Try cache lookup first - avoids expensive schema compilation on each request if validationOptions != nil && validationOptions.SchemaCache != nil && schema != nil && schema.GoLow() != nil { hash := schema.GoLow().Hash() if cached, ok := validationOptions.SchemaCache.Load(hash); ok && cached != nil && cached.CompiledSchema != nil { jsch = cached.CompiledSchema } } // Cache miss - render and compile the schema if jsch == nil { // 1. build a JSON render of the schema. renderCtx := base.NewInlineRenderContextForValidation() renderedSchema, _ := schema.RenderInlineWithContext(renderCtx) referenceSchema := string(renderedSchema) jsonSchema, _ = utils.ConvertYAMLtoJSON(renderedSchema) // 2. create a new json schema compiler and add the schema to it var err error jsch, err = helpers.NewCompiledSchema(name, jsonSchema, validationOptions) if err != nil { // schema compilation failed, return validation error instead of panicking validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: validationType, ValidationSubType: subValType, Message: fmt.Sprintf("%s '%s' failed schema compilation", entity, name), Reason: fmt.Sprintf("%s '%s' schema compilation failed: %s", reasonEntity, name, err.Error()), SpecLine: 1, SpecCol: 0, ParameterName: name, HowToFix: "check the parameter schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", Context: string(jsonSchema), }) return validationErrors } // Store in cache for future requests if validationOptions != nil && validationOptions.SchemaCache != nil && schema != nil && schema.GoLow() != nil { hash := schema.GoLow().Hash() var renderedNode yaml.Node _ = yaml.Unmarshal(renderedSchema, &renderedNode) validationOptions.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ Schema: schema, RenderedInline: renderedSchema, ReferenceSchema: referenceSchema, RenderedJSON: jsonSchema, CompiledSchema: jsch, RenderedNode: &renderedNode, }) } } // 3. decode the object into a json blob. var decodedObj interface{} rawIsMap := false validEncoding := false if rawObject != nil { // check what type of object it is ot := reflect.TypeOf(rawObject) var ok bool switch ot.Kind() { case reflect.Map: if decodedObj, ok = rawObject.(map[string]interface{}); ok { rawIsMap = true validEncoding = true } else { rawIsMap = true } } } else { decodedString, _ := url.QueryUnescape(rawBlob) err := json.Unmarshal([]byte(decodedString), &decodedObj) if err != nil { decodedObj = rawBlob } validEncoding = true } // 4. validate the object against the schema var scErrs error if validEncoding { p := decodedObj if rawIsMap { if g, ko := rawObject.(map[string]interface{}); ko { if len(g) == 0 || (g[""] != nil && g[""] == "") { p = nil } } } if p != nil { // check if any of the items have an empty key skip := false if rawIsMap { for k := range p.(map[string]interface{}) { if k == "" { validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: validationType, ValidationSubType: subValType, Message: fmt.Sprintf("%s '%s' failed to validate", entity, name), Reason: fmt.Sprintf("%s '%s' is defined as an object, "+ "however it failed to pass a schema validation", reasonEntity, name), SpecLine: schema.GoLow().Type.KeyNode.Line, SpecCol: schema.GoLow().Type.KeyNode.Column, SchemaValidationErrors: nil, HowToFix: errors.HowToFixInvalidSchema, }) skip = true break } } } if !skip { scErrs = jsch.Validate(p) } } } var werras *jsonschema.ValidationError if stdError.As(scErrs, &werras) { validationErrors = formatJsonSchemaValidationError(schema, werras, entity, reasonEntity, name, validationType, subValType, "", "") } // if there are no validationErrors, check that the supplied value is even JSON if len(validationErrors) == 0 { if rawIsMap { if !validEncoding { // add the error to the list validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: validationType, ValidationSubType: subValType, Message: fmt.Sprintf("%s '%s' cannot be decoded", entity, name), Reason: fmt.Sprintf("%s '%s' is defined as an object, "+ "however it failed to be decoded as an object", reasonEntity, name), SpecLine: schema.GoLow().RootNode.Line, SpecCol: schema.GoLow().RootNode.Column, HowToFix: errors.HowToFixDecodingError, }) } } } return validationErrors } func formatJsonSchemaValidationError(schema *base.Schema, scErrs *jsonschema.ValidationError, entity string, reasonEntity string, name string, validationType string, subValType string, pathTemplate string, operation string) (validationErrors []*errors.ValidationError) { // flatten the validationErrors schFlatErrs := scErrs.BasicOutput().Errors var schemaValidationErrors []*errors.SchemaValidationFailure for q := range schFlatErrs { er := schFlatErrs[q] errMsg := er.Error.Kind.LocalizedString(message.NewPrinter(language.Tag{})) if er.KeywordLocation == "" || helpers.IgnoreRegex.MatchString(errMsg) { continue // ignore this error, it's not useful } // Construct full OpenAPI path for KeywordLocation if pathTemplate and operation are provided keywordLocation := er.KeywordLocation if pathTemplate != "" && operation != "" && validationType == helpers.ParameterValidation { // er.KeywordLocation is relative to the schema (e.g., "/minLength" or "/enum") keyword := strings.TrimPrefix(er.KeywordLocation, "/") keywordLocation = helpers.ConstructParameterJSONPointer(pathTemplate, operation, name, keyword) } fail := &errors.SchemaValidationFailure{ Reason: errMsg, FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), KeywordLocation: keywordLocation, OriginalJsonSchemaError: scErrs, } if schema != nil { renderCtx := base.NewInlineRenderContextForValidation() rendered, err := schema.RenderInlineWithContext(renderCtx) if err == nil && rendered != nil { fail.ReferenceSchema = string(rendered) } } schemaValidationErrors = append(schemaValidationErrors, fail) } schemaType := "undefined" line := 0 col := 0 if len(schema.Type) > 0 { schemaType = schema.Type[0] line = schema.GoLow().Type.KeyNode.Line col = schema.GoLow().Type.KeyNode.Column } else { var sTypes []string seen := make(map[string]struct{}) extractTypes := func(s *base.SchemaProxy) { pSch := s.Schema() if pSch != nil { for _, typ := range pSch.Type { if _, ok := seen[typ]; !ok { sTypes = append(sTypes, typ) seen[typ] = struct{}{} } } } } processPoly := func(schemas []*base.SchemaProxy) { for _, s := range schemas { extractTypes(s) } } // check if there is polymorphism going on here. if len(schema.AnyOf) > 0 || len(schema.AllOf) > 0 || len(schema.OneOf) > 0 { processPoly(schema.AnyOf) processPoly(schema.AllOf) processPoly(schema.OneOf) sep := "or" if len(schema.AllOf) > 0 { sep = "and a" } schemaType = strings.Join(sTypes, fmt.Sprintf(" %s ", sep)) } line = schema.GoLow().RootNode.Line col = schema.GoLow().RootNode.Column } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: validationType, ValidationSubType: subValType, Message: fmt.Sprintf("%s '%s' failed to validate", entity, name), Reason: fmt.Sprintf("%s '%s' is defined as an %s, "+ "however it failed to pass a schema validation", reasonEntity, name, schemaType), SpecLine: line, SpecCol: col, ParameterName: name, SchemaValidationErrors: schemaValidationErrors, HowToFix: errors.HowToFixInvalidSchema, }) return validationErrors } libopenapi-validator-0.13.8/parameters/validate_parameter_test.go000066400000000000000000001157421520534042400253030ustar00rootroot00000000000000package parameters import ( "net/http" "strings" "sync" "testing" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel/high/base" lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/helpers" ) func Test_ForceCompilerError(t *testing.T) { // Try to force a panic result := ValidateSingleParameterSchema(nil, nil, "", "", "", "", "", nil, "", "") // Ideally this would result in an error response, current behavior swallows the error require.Empty(t, result) } func TestHeaderSchemaNoType(t *testing.T) { bytes := []byte(`{ "openapi": "3.0.0", "info": { "title": "API Spec With Mandatory Header", "version": "1.0.0" }, "paths": { "/api-endpoint": { "get": { "summary": "Restricted API Endpoint", "parameters": [ { "name": "apiKey", "in": "header", "required": true, "schema": { "oneOf": [ { "type": "boolean" }, { "type": "integer" } ] } } ], "responses": { "200": { "description": "Successful response" } } } } }, "components": { "securitySchemes": { "ApiKeyHeader": { "type": "apiKey", "name": "apiKey", "in": "header" } } }, "security": [ { "ApiKeyHeader": [] } ] }`) doc, err := libopenapi.NewDocument(bytes) if err != nil { t.Fatalf("error while creating open api spec document: %v", err) } req, err := http.NewRequest("GET", "/api-endpoint", nil) if err != nil { t.Fatalf("error while creating request: %v", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("apiKey", "headerValue") v3Model, errs := doc.BuildV3Model() if errs != nil { t.Fatalf("error while building v3 model: %v", errs) } v3Model.Model.Servers = nil // render the document back to bytes and reload the model. _, _, v3Model, _ = doc.RenderAndReload() validator := NewParameterValidator(&v3Model.Model) isSuccess, valErrs := validator.ValidateHeaderParams(req) assert.False(t, isSuccess) assert.Len(t, valErrs, 1) assert.Equal(t, "schema 'apiKey' is defined as an boolean or integer, however it failed to pass a schema validation", valErrs[0].Reason) assert.Len(t, valErrs[0].SchemaValidationErrors, 2) assert.Equal(t, "got string, want boolean", valErrs[0].SchemaValidationErrors[0].Reason) assert.Equal(t, "got string, want integer", valErrs[0].SchemaValidationErrors[1].Reason) } func TestHeaderSchemaNoType_AllPoly(t *testing.T) { bytes := []byte(`{ "openapi": "3.0.0", "info": { "title": "API Spec With Mandatory Header", "version": "1.0.0" }, "paths": { "/api-endpoint": { "get": { "summary": "Restricted API Endpoint", "parameters": [ { "name": "apiKey", "in": "header", "required": true, "schema": { "oneOf": [ { "type": "boolean" }, { "type": "integer" } ], "allOf": [ { "type": "boolean" } ] } } ], "responses": { "200": { "description": "Successful response" } } } } }, "components": { "securitySchemes": { "ApiKeyHeader": { "type": "apiKey", "name": "apiKey", "in": "header" } } }, "security": [ { "ApiKeyHeader": [] } ] }`) doc, err := libopenapi.NewDocument(bytes) if err != nil { t.Fatalf("error while creating open api spec document: %v", err) } req, err := http.NewRequest("GET", "/api-endpoint", nil) if err != nil { t.Fatalf("error while creating request: %v", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("apiKey", "headerValue") v3Model, errs := doc.BuildV3Model() if errs != nil { t.Fatalf("error while building v3 model: %v", errs) } v3Model.Model.Servers = nil // render the document back to bytes and reload the model. _, _, v3Model, _ = doc.RenderAndReload() validator := NewParameterValidator(&v3Model.Model) isSuccess, valErrs := validator.ValidateHeaderParams(req) assert.False(t, isSuccess) assert.Len(t, valErrs, 1) assert.Equal(t, "schema 'apiKey' is defined as an boolean and a integer, however it failed to pass a schema validation", valErrs[0].Reason) assert.Len(t, valErrs[0].SchemaValidationErrors, 3) } // TestUnifiedErrorFormatWithFormatValidation tests that format validation errors // use the unified SchemaValidationFailure format consistently // https://github.com/pb33f/libopenapi-validator/issues/168 func TestUnifiedErrorFormatWithFormatValidation(t *testing.T) { bytes := []byte(`{ "openapi": "3.0.0", "info": { "title": "API Spec With Format Validation", "version": "1.0.0" }, "paths": { "/test": { "get": { "parameters": [ { "name": "email_param", "in": "query", "required": true, "schema": { "type": "string", "format": "email" } } ], "responses": { "200": { "description": "Success" } } } } } }`) doc, err := libopenapi.NewDocument(bytes) if err != nil { t.Fatalf("error while creating open api spec document: %v", err) } req, err := http.NewRequest("GET", "/test?email_param=invalid-email-format", nil) if err != nil { t.Fatalf("error while creating request: %v", err) } v3Model, errs := doc.BuildV3Model() if errs != nil { t.Fatalf("error while building v3 model: %v", errs) } v3Model.Model.Servers = nil _, _, v3Model, _ = doc.RenderAndReload() validator := NewParameterValidator(&v3Model.Model, config.WithFormatAssertions()) isSuccess, valErrs := validator.ValidateQueryParams(req) assert.False(t, isSuccess) assert.Len(t, valErrs, 1) assert.Equal(t, "Query parameter 'email_param' failed to validate", valErrs[0].Message) // verify ParameterName is populated for easy programmatic access assert.Equal(t, "email_param", valErrs[0].ParameterName) // verify unified error format - SchemaValidationErrors should be populated assert.Len(t, valErrs[0].SchemaValidationErrors, 1) assert.Contains(t, valErrs[0].SchemaValidationErrors[0].Reason, "is not valid email") assert.Equal(t, "/paths/test/get/parameters/email_param/schema/format", valErrs[0].SchemaValidationErrors[0].KeywordLocation) assert.NotEmpty(t, valErrs[0].SchemaValidationErrors[0].ReferenceSchema) } // TestParameterNameFieldPopulation tests that ParameterName field is consistently populated // for both basic validation errors and JSONSchema validation errors func TestParameterNameFieldPopulation(t *testing.T) { bytes := []byte(`{ "openapi": "3.0.0", "info": { "title": "Parameter Name Test", "version": "1.0.0" }, "paths": { "/test": { "get": { "parameters": [ { "name": "integer_param", "in": "query", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success" } } } } } }`) doc, err := libopenapi.NewDocument(bytes) require.NoError(t, err) req, err := http.NewRequest("GET", "/test?integer_param=not_a_number", nil) require.NoError(t, err) v3Model, errs := doc.BuildV3Model() require.Empty(t, errs) validator := NewParameterValidator(&v3Model.Model) isSuccess, valErrs := validator.ValidateQueryParams(req) assert.False(t, isSuccess) assert.Len(t, valErrs, 1) // verify ParameterName is populated for basic type validation errors assert.Equal(t, "integer_param", valErrs[0].ParameterName) assert.Equal(t, "Query parameter 'integer_param' is not a valid integer", valErrs[0].Message) // basic type errors SHOULD have SchemaValidationErrors because we know the parameter schema assert.Len(t, valErrs[0].SchemaValidationErrors, 1) assert.Equal(t, "integer_param", valErrs[0].SchemaValidationErrors[0].FieldName) } func TestHeaderSchemaStringNoJSON(t *testing.T) { bytes := []byte(`{ "openapi": "3.0.0", "info": { "title": "API Spec With Mandatory Header", "version": "1.0.0" }, "paths": { "/api-endpoint": { "get": { "summary": "Restricted API Endpoint", "responses": { "200": { "description": "Successful response", "headers": { "chicken-nuggets": { "required": true, "schema": { "oneOf": [ { "type": "boolean" }, { "type": "integer" } ] } } } } } } } }, "components": { "securitySchemes": { "ApiKeyHeader": { "type": "apiKey", "name": "apiKey", "in": "header" } } }, "security": [ { "ApiKeyHeader": [] } ] }`) doc, err := libopenapi.NewDocument(bytes) if err != nil { t.Fatalf("error while creating open api spec document: %v", err) } req, err := http.NewRequest("GET", "/api-endpoint", nil) if err != nil { t.Fatalf("error while creating request: %v", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("apiKey", "headerValue") v3Model, errs := doc.BuildV3Model() if errs != nil { t.Fatalf("error while building v3 model: %v", errs) } v3Model.Model.Servers = nil // render the document back to bytes and reload the model. _, _, v3Model, _ = doc.RenderAndReload() headers := v3Model.Model.Paths.PathItems.GetOrZero("/api-endpoint").Get.Responses.Codes.GetOrZero("200").Headers headerSchema := headers.GetOrZero("chicken-nuggets").Schema.Schema() headerErrors := ValidateParameterSchema(headerSchema, nil, "bubbles", "header", "response header", "chicken-nuggets", helpers.ResponseBodyValidation, lowv3.HeadersLabel, nil) assert.Len(t, headerErrors, 1) assert.Equal(t, "response header 'chicken-nuggets' is defined as an boolean or integer, however it failed to pass a schema validation", headerErrors[0].Reason) } // TestComplexRegexSchemaCompilationError tests that complex regex patterns in parameter schemas // that cause schema compilation to fail are handled gracefully instead of causing panics func TestComplexRegexSchemaCompilationError(t *testing.T) { bytes := []byte(`{ "openapi": "3.0.0", "info": { "title": "API Spec With Complex Regex Pattern", "version": "1.0.0" }, "paths": { "/api-endpoint": { "get": { "summary": "API Endpoint with complex regex", "parameters": [ { "name": "complexParam", "in": "query", "required": true, "schema": { "type": "string", "pattern": "[\\w\\W]{1,1024}$" } } ], "responses": { "200": { "description": "Successful response" } } } } } }`) doc, err := libopenapi.NewDocument(bytes) if err != nil { t.Fatalf("error while creating open api spec document: %v", err) } req, err := http.NewRequest("GET", "/api-endpoint?complexParam=testvalue", nil) if err != nil { t.Fatalf("error while creating request: %v", err) } v3Model, errs := doc.BuildV3Model() if errs != nil { t.Fatalf("error while building v3 model: %v", errs) } validator := NewParameterValidator(&v3Model.Model) // validate - this should not panic even if schema compilation fails due to complex regex isSuccess, valErrs := validator.ValidateQueryParams(req) // if schema compilation failed, we should get validation errors instead of a panic if !isSuccess { // verify we got schema compilation errors instead of a panic assert.NotEmpty(t, valErrs) found := false for _, err := range valErrs { if err.ParameterName == "complexParam" && len(err.SchemaValidationErrors) == 0 { // Schema compilation errors don't have SchemaValidationFailure objects if strings.Contains(err.Reason, "failed to compile JSON schema") { found = true assert.Contains(t, err.Reason, "failed to compile JSON schema") assert.Contains(t, err.HowToFix, "complex regex patterns") break } } } if !found { // if it didn't fail compilation, it should have succeeded or failed with a different error t.Logf("Schema compilation succeeded or failed with different error, validation result: %v, errors: %v", isSuccess, valErrs) } } else { // schema compiled and validated successfully assert.True(t, isSuccess) assert.Empty(t, valErrs) } } // TestValidateParameterSchema_SchemaCompilationFailure tests that ValidateParameterSchema // handles schema compilation failures gracefully instead of causing panics func TestValidateParameterSchema_SchemaCompilationFailure(t *testing.T) { bytes := []byte(`{ "openapi": "3.0.0", "info": { "title": "API Spec With Complex Regex Pattern", "version": "1.0.0" }, "paths": { "/api-endpoint": { "get": { "summary": "API Endpoint with complex regex that causes compilation failure", "parameters": [ { "name": "failParam", "in": "query", "required": true, "schema": { "type": "string", "pattern": "[\\w\\W]{1,2048}$" } } ], "responses": { "200": { "description": "Successful response" } } } } } }`) doc, err := libopenapi.NewDocument(bytes) require.NoError(t, err) v3Model, errs := doc.BuildV3Model() require.Empty(t, errs) // get the parameter schema that should cause compilation failure pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/api-endpoint") param := pathItem.Get.Parameters[0] schema := param.Schema.Schema() // call ValidateParameterSchema directly with the problematic schema validationErrors := ValidateParameterSchema( schema, "test-value", "", "Query parameter", "query parameter", "failParam", helpers.ParameterValidation, helpers.ParameterValidationQuery, nil, ) // should get schema compilation error instead of panic if len(validationErrors) > 0 { found := false for _, validationError := range validationErrors { if validationError.ParameterName == "failParam" && validationError.ValidationSubType == helpers.ParameterValidationQuery && len(validationError.SchemaValidationErrors) == 0 { // Schema compilation errors don't have SchemaValidationFailure objects if strings.Contains(validationError.Reason, "failed to compile JSON schema") { assert.Contains(t, validationError.Reason, "failed to compile JSON schema") assert.Contains(t, validationError.HowToFix, "complex regex patterns") assert.Equal(t, "Query parameter 'failParam' failed schema compilation", validationError.Message) found = true break } } } if !found { // schema compilation succeeded, might have failed for other reasons or succeeded t.Logf("Schema compilation succeeded or failed for different reasons: %v", validationErrors) } } else { // no validation errors - schema compiled and validated successfully t.Logf("Schema compiled and validated successfully") } } func preparePathsBenchmark(b *testing.B, cache config.RegexCache) (ParameterValidator, *http.Request) { bytes := []byte(`{ "openapi": "3.0.0", "info": { "title": "API Spec With Complex Regex Pattern", "version": "1.0.0" }, "paths": { "/test/other/path": { "get": {"responses": {"200": {"description": "test"}}} }, "/static/test/{imageName}": { "get": {"responses": {"200": {"description": "test"}}} }, "/request/to/my/image.png": { "get": {"responses": {"200": {"description": "test"}}} }, "/api/v2/{url}/{other}/{oncemore}/{url}": { "get": {"responses": {"200": {"description": "test"}}} }, "/api/v1/{path}": { "get": {"responses": {"200": {"description": "test"}}} }, "/each/url/{is}/{a_new_regex}": { "get": {"responses": {"200": {"description": "test"}}} }, "/my-test/with-so-many/urls": { "get": {"responses": {"200": {"description": "test"}}} }, "/test/other/path": { "get": {"responses": {"200": {"description": "test"}}} }, "/api/endpoint/{address}/{domain}": { "get": { "summary": "API Endpoint with complex regex", "parameters": [ { "name": "complexParam", "in": "query", "required": true, "schema": { "type": "string", "pattern": "[\\w\\W]{1,1024}$" } } ], "responses": { "200": { "description": "Successful response" } } } } } }`) doc, err := libopenapi.NewDocument(bytes) if err != nil { b.Fatalf("error while creating open api spec document: %v", err) } req, err := http.NewRequest("GET", "/api/endpoint/127.0.0.1/domain.com?complexParam=testvalue", nil) if err != nil { b.Fatalf("error while creating request: %v", err) } v3Model, errs := doc.BuildV3Model() if errs != nil { b.Fatalf("error while building v3 model: %v", errs) } validator := NewParameterValidator(&v3Model.Model, config.WithRegexCache(cache)) return validator, req } func BenchmarkValidationWithoutCache(b *testing.B) { validator, req := preparePathsBenchmark(b, nil) b.ResetTimer() for b.Loop() { validator.ValidateHeaderParams(req) validator.ValidateCookieParams(req) validator.ValidateQueryParams(req) validator.ValidateSecurity(req) validator.ValidatePathParams(req) } } func BenchmarkValidationWithRegexCache(b *testing.B) { validator, req := preparePathsBenchmark(b, &sync.Map{}) b.ResetTimer() for b.Loop() { validator.ValidateHeaderParams(req) validator.ValidateCookieParams(req) validator.ValidateQueryParams(req) validator.ValidateSecurity(req) validator.ValidatePathParams(req) } } // cacheTestSpec is an OpenAPI spec for testing cache behavior var cacheTestSpec = []byte(`{ "openapi": "3.1.0", "info": { "title": "Cache Test API", "version": "1.0.0" }, "paths": { "/items/{id}": { "get": { "operationId": "getItem", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "minLength": 1, "maxLength": 64 } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100 } } ], "responses": { "200": { "description": "OK" } } } } } }`) // Test_ParameterValidation_CacheUsage verifies that parameter validation uses the schema cache. // This test validates that: // 1. Cache is populated after the first validation // 2. Subsequent validations reuse the cached compiled schemas // 3. Validation still produces correct results when using cached schemas func Test_ParameterValidation_CacheUsage(t *testing.T) { doc, err := libopenapi.NewDocument(cacheTestSpec) require.NoError(t, err, "Failed to create document") v3Model, errs := doc.BuildV3Model() require.Nil(t, errs, "Failed to build v3 model") // Create options with cache (default behavior) opts := config.NewValidationOptions() require.NotNil(t, opts.SchemaCache, "Schema cache should be initialized by default") validator := NewParameterValidator(&v3Model.Model, config.WithExistingOpts(opts)) // First request - should populate cache req1, _ := http.NewRequest("GET", "/items/abc123?limit=50", nil) isSuccess1, errors1 := validator.ValidateQueryParams(req1) assert.True(t, isSuccess1, "First validation should succeed") assert.Empty(t, errors1, "First validation should have no errors") // Count cached entries (should have at least the limit parameter schema) cacheCount := 0 opts.SchemaCache.Range(func(key uint64, value *cache.SchemaCacheEntry) bool { cacheCount++ return true }) assert.Greater(t, cacheCount, 0, "Cache should have entries after first validation") // Second request with different valid value - should use cached schema req2, _ := http.NewRequest("GET", "/items/xyz789?limit=75", nil) isSuccess2, errors2 := validator.ValidateQueryParams(req2) assert.True(t, isSuccess2, "Second validation should succeed") assert.Empty(t, errors2, "Second validation should have no errors") // Third request with invalid value - should still use cached schema but fail validation req3, _ := http.NewRequest("GET", "/items/test?limit=999", nil) isSuccess3, errors3 := validator.ValidateQueryParams(req3) assert.False(t, isSuccess3, "Third validation should fail (limit > maximum)") assert.NotEmpty(t, errors3, "Third validation should have errors") } // Test_ParameterValidation_WithoutCache verifies that validation works when cache is disabled. func Test_ParameterValidation_WithoutCache(t *testing.T) { doc, err := libopenapi.NewDocument(cacheTestSpec) require.NoError(t, err, "Failed to create document") v3Model, errs := doc.BuildV3Model() require.Nil(t, errs, "Failed to build v3 model") // Create options without cache opts := config.NewValidationOptions(config.WithSchemaCache(nil)) require.Nil(t, opts.SchemaCache, "Schema cache should be nil") validator := NewParameterValidator(&v3Model.Model, config.WithExistingOpts(opts)) // Validation should still work without cache req, _ := http.NewRequest("GET", "/items/abc123?limit=50", nil) isSuccess, errors := validator.ValidateQueryParams(req) assert.True(t, isSuccess, "Validation should succeed without cache") assert.Empty(t, errors, "Validation should have no errors") // Validation with invalid value should fail req2, _ := http.NewRequest("GET", "/items/abc123?limit=999", nil) isSuccess2, errors2 := validator.ValidateQueryParams(req2) assert.False(t, isSuccess2, "Validation should fail for invalid value") assert.NotEmpty(t, errors2, "Validation should report errors") } // Test_ParameterValidation_CacheConsistency verifies that cached schemas produce // the same validation results as freshly compiled schemas. func Test_ParameterValidation_CacheConsistency(t *testing.T) { doc, err := libopenapi.NewDocument(cacheTestSpec) require.NoError(t, err, "Failed to create document") v3Model, errs := doc.BuildV3Model() require.Nil(t, errs, "Failed to build v3 model") // Run the same validations with and without cache testCases := []struct { name string url string expected bool }{ {"valid_limit", "/items/abc?limit=50", true}, {"limit_at_max", "/items/abc?limit=100", true}, {"limit_at_min", "/items/abc?limit=1", true}, {"limit_too_high", "/items/abc?limit=101", false}, {"limit_too_low", "/items/abc?limit=0", false}, } // First run with cache optsWithCache := config.NewValidationOptions() validatorWithCache := NewParameterValidator(&v3Model.Model, config.WithExistingOpts(optsWithCache)) // Second run without cache optsNoCache := config.NewValidationOptions(config.WithSchemaCache(nil)) validatorNoCache := NewParameterValidator(&v3Model.Model, config.WithExistingOpts(optsNoCache)) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { req, _ := http.NewRequest("GET", tc.url, nil) successWithCache, errorsWithCache := validatorWithCache.ValidateQueryParams(req) successNoCache, errorsNoCache := validatorNoCache.ValidateQueryParams(req) assert.Equal(t, tc.expected, successWithCache, "Cached validation result mismatch for %s", tc.name) assert.Equal(t, successWithCache, successNoCache, "Cache vs no-cache results should match for %s", tc.name) assert.Equal(t, len(errorsWithCache), len(errorsNoCache), "Error count should match for %s", tc.name) }) } } // Test_GetRenderedSchema_NilSchema verifies GetRenderedSchema handles nil schema gracefully. func Test_GetRenderedSchema_NilSchema(t *testing.T) { opts := config.NewValidationOptions() result := GetRenderedSchema(nil, opts) assert.Empty(t, result, "GetRenderedSchema should return empty string for nil schema") } // Test_GetRenderedSchema_NilOptions verifies GetRenderedSchema works without options. func Test_GetRenderedSchema_NilOptions(t *testing.T) { spec := []byte(`{ "openapi": "3.1.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": { "/test": { "get": { "parameters": [{ "name": "id", "in": "query", "schema": {"type": "string", "minLength": 1} }], "responses": {"200": {"description": "OK"}} } } } }`) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v3Model, errs := doc.BuildV3Model() require.Nil(t, errs) pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/test") schema := pathItem.Get.Parameters[0].Schema.Schema() // Ground truth rendered, _ := schema.RenderInline() expected := string(rendered) // With nil options should match ground truth result := GetRenderedSchema(schema, nil) assert.Equal(t, expected, result) } // Test_GetRenderedSchema_CacheHit verifies GetRenderedSchema uses cached data when available. func Test_GetRenderedSchema_CacheHit(t *testing.T) { spec := []byte(`{ "openapi": "3.1.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": { "/test": { "get": { "parameters": [{ "name": "id", "in": "query", "schema": {"type": "integer", "minimum": 1} }], "responses": {"200": {"description": "OK"}} } } } }`) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v3Model, errs := doc.BuildV3Model() require.Nil(t, errs) pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/test") schema := pathItem.Get.Parameters[0].Schema.Schema() // Ground truth rendered, _ := schema.RenderInline() expected := string(rendered) // Pre-populate cache with RenderedInline opts := config.NewValidationOptions() hash := schema.GoLow().Hash() opts.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ Schema: schema, RenderedInline: rendered, }) // Cache hit should match ground truth result := GetRenderedSchema(schema, opts) assert.Equal(t, expected, result) } // Test_GetRenderedSchema_NilCache verifies GetRenderedSchema works when cache is disabled. func Test_GetRenderedSchema_NilCache(t *testing.T) { spec := []byte(`{ "openapi": "3.1.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": { "/test": { "get": { "parameters": [{ "name": "id", "in": "query", "schema": {"type": "boolean"} }], "responses": {"200": {"description": "OK"}} } } } }`) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v3Model, errs := doc.BuildV3Model() require.Nil(t, errs) pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/test") schema := pathItem.Get.Parameters[0].Schema.Schema() // Ground truth rendered, _ := schema.RenderInline() expected := string(rendered) // With nil cache should match ground truth opts := config.NewValidationOptions(config.WithSchemaCache(nil)) result := GetRenderedSchema(schema, opts) assert.Equal(t, expected, result) } // Test_GetRenderedSchema_CacheMiss verifies GetRenderedSchema renders fresh when cache entry has empty RenderedInline. // This tests the code path where cache lookup succeeds but RenderedInline is empty. func Test_GetRenderedSchema_CacheMiss(t *testing.T) { spec := []byte(`{ "openapi": "3.1.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": { "/test": { "get": { "parameters": [{ "name": "id", "in": "query", "schema": {"type": "integer"} }], "responses": {"200": {"description": "OK"}} } } } }`) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v3Model, errs := doc.BuildV3Model() require.Nil(t, errs) pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/test") schema := pathItem.Get.Parameters[0].Schema.Schema() // Ground truth rendered, _ := schema.RenderInline() expected := string(rendered) // Store entry with empty RenderedInline to force cache miss opts := config.NewValidationOptions() hash := schema.GoLow().Hash() opts.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ Schema: schema, RenderedInline: nil, // Empty - should trigger fresh rendering }) // Cache miss should still match ground truth result := GetRenderedSchema(schema, opts) assert.Equal(t, expected, result) } // Test_GetRenderedSchema_Deterministic verifies that GetRenderedSchema returns the same // output regardless of cache state (cache hit vs cache miss). func Test_GetRenderedSchema_Deterministic(t *testing.T) { spec := []byte(`{ "openapi": "3.1.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": { "/test": { "get": { "parameters": [{ "name": "status", "in": "query", "schema": {"type": "string", "enum": ["active", "inactive"]} }], "responses": {"200": {"description": "OK"}} } } } }`) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v3Model, errs := doc.BuildV3Model() require.Nil(t, errs) pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/test") schema := pathItem.Get.Parameters[0].Schema.Schema() // Ground truth rendered, _ := schema.RenderInline() expected := string(rendered) // Cache miss path (no cache) optsNoCache := config.NewValidationOptions(config.WithSchemaCache(nil)) resultMiss := GetRenderedSchema(schema, optsNoCache) assert.Equal(t, expected, resultMiss) // Cache hit path (pre-populated cache) optsWithCache := config.NewValidationOptions() hash := schema.GoLow().Hash() optsWithCache.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ RenderedInline: rendered, }) resultHit := GetRenderedSchema(schema, optsWithCache) assert.Equal(t, expected, resultHit) } // Test_ValidateSingleParameterSchema_CacheMissCompiledSchema tests the path where cache entry // exists but CompiledSchema is nil, forcing recompilation. func Test_ValidateSingleParameterSchema_CacheMissCompiledSchema(t *testing.T) { spec := []byte(`{ "openapi": "3.1.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": { "/test/{id}": { "get": { "parameters": [{ "name": "id", "in": "path", "required": true, "schema": {"type": "integer", "minimum": 1} }], "responses": {"200": {"description": "OK"}} } } } }`) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v3Model, errs := doc.BuildV3Model() require.Nil(t, errs) // Get the parameter schema pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/test/{id}") param := pathItem.Get.Parameters[0] schema := param.Schema.Schema() // Create options with cache enabled opts := config.NewValidationOptions() require.NotNil(t, opts.SchemaCache) // Store an entry with nil CompiledSchema to force recompilation hash := schema.GoLow().Hash() opts.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ Schema: schema, CompiledSchema: nil, // nil - should trigger recompilation }) // Validate should still work by recompiling the schema result := ValidateSingleParameterSchema( schema, int64(5), // valid integer "Path parameter", "The path parameter", "id", helpers.ParameterValidation, helpers.ParameterValidationPath, opts, "/test/{id}", "get", ) assert.Empty(t, result, "Validation should pass for valid integer") // Now verify the cache was populated with the compiled schema cached, ok := opts.SchemaCache.Load(hash) assert.True(t, ok, "Cache entry should exist") assert.NotNil(t, cached.CompiledSchema, "CompiledSchema should be populated after validation") } // arrayValidationSpec is used to test array parameter validation with the updated function signatures var arrayValidationSpec = []byte(`{ "openapi": "3.1.0", "info": {"title": "Array Test", "version": "1.0.0"}, "paths": { "/test": { "get": { "parameters": [{ "name": "ids", "in": "query", "schema": { "type": "array", "items": {"type": "integer", "minimum": 1} } }], "responses": {"200": {"description": "OK"}} } } } }`) // Test_ArrayValidation_ErrorContainsRenderedSchema verifies that array validation errors // still contain the rendered schema after the rendering optimization. func Test_ArrayValidation_ErrorContainsRenderedSchema(t *testing.T) { doc, err := libopenapi.NewDocument(arrayValidationSpec) require.NoError(t, err) v3Model, errs := doc.BuildV3Model() require.Nil(t, errs) validator := NewParameterValidator(&v3Model.Model) // Request with invalid array values (strings instead of integers) req, _ := http.NewRequest("GET", "/test?ids=abc,def", nil) success, validationErrors := validator.ValidateQueryParams(req) assert.False(t, success, "Validation should fail for non-integer array values") assert.NotEmpty(t, validationErrors, "Should have validation errors") // Verify error message is properly formatted assert.Contains(t, validationErrors[0].Message, "ids", "Error should reference parameter name") } // Test_ParameterValidation_CompleteCacheEntry verifies that parameter validation // writes complete cache entries. func Test_ParameterValidation_CompleteCacheEntry(t *testing.T) { spec := []byte(`{ "openapi": "3.1.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": { "/test": { "get": { "parameters": [{ "name": "id", "in": "query", "schema": {"type": "string", "minLength": 1} }], "responses": {"200": {"description": "OK"}} } } } }`) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v3Model, errs := doc.BuildV3Model() require.Nil(t, errs) opts := config.NewValidationOptions() validator := NewParameterValidator(&v3Model.Model, config.WithExistingOpts(opts)) req, _ := http.NewRequest("GET", "/test?id=abc", nil) valid, _ := validator.ValidateQueryParams(req) assert.True(t, valid) pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/test") schema := pathItem.Get.Parameters[0].Schema.Schema() hash := schema.GoLow().Hash() cached, ok := opts.SchemaCache.Load(hash) require.True(t, ok, "Cache entry should exist") // Check that all fields of the cache entry are populated assert.NotNil(t, cached.Schema, "Schema should be populated") assert.NotEmpty(t, cached.RenderedInline, "RenderedInline should be populated") assert.NotEmpty(t, cached.ReferenceSchema, "ReferenceSchema should be populated") assert.NotEmpty(t, cached.RenderedJSON, "RenderedJSON should be populated") assert.NotNil(t, cached.CompiledSchema, "CompiledSchema should be populated") assert.NotNil(t, cached.RenderedNode, "RenderedNode should be populated") } // Test_ReferenceSchema_ConsistentFormat verifies that ReferenceSchema has the same // format whether the error comes from GetRenderedSchema or formatJsonSchemaValidationError. func Test_ReferenceSchema_ConsistentFormat(t *testing.T) { spec := []byte(`{ "openapi": "3.1.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": { "/test": { "get": { "parameters": [{ "name": "count", "in": "query", "required": true, "schema": {"type": "integer", "minimum": 1, "maximum": 100} }], "responses": {"200": {"description": "OK"}} } } } }`) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v3Model, errs := doc.BuildV3Model() require.Nil(t, errs) validator := NewParameterValidator(&v3Model.Model) // Error path 1: Missing required parameter (uses GetRenderedSchema) req1, _ := http.NewRequest(http.MethodGet, "/test", nil) _, errors1 := validator.ValidateQueryParams(req1) require.NotEmpty(t, errors1) require.NotEmpty(t, errors1[0].SchemaValidationErrors) refSchema1 := errors1[0].SchemaValidationErrors[0].ReferenceSchema // Error path 2: Value outside range (uses formatJsonSchemaValidationError) req2, _ := http.NewRequest(http.MethodGet, "/test?count=999", nil) _, errors2 := validator.ValidateQueryParams(req2) require.NotEmpty(t, errors2) require.NotEmpty(t, errors2[0].SchemaValidationErrors) refSchema2 := errors2[0].SchemaValidationErrors[0].ReferenceSchema // Both should be plain YAML with the same content assert.Equal(t, refSchema1, refSchema2, "ReferenceSchema should be consistent regardless of error path") } // Test_GetRenderedSchema_ValidationModeConsistency verifies that GetRenderedSchema produces // identical output on cache hit vs cache miss for schemas with discriminators. The cache // stores schemas rendered with validation mode, so cache misses must also use validation // mode for consistency. func Test_GetRenderedSchema_ValidationModeConsistency(t *testing.T) { spec := []byte(`{ "openapi": "3.1.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": { "/test": { "get": { "parameters": [{ "name": "pet", "in": "query", "required": true, "schema": { "discriminator": { "propertyName": "petType", "mapping": { "cat": "#/components/schemas/Cat", "dog": "#/components/schemas/Dog" } }, "oneOf": [ {"$ref": "#/components/schemas/Cat"}, {"$ref": "#/components/schemas/Dog"} ] } }], "responses": {"200": {"description": "OK"}} } } }, "components": { "schemas": { "Cat": { "type": "object", "properties": { "petType": {"type": "string"}, "meow": {"type": "boolean"} }, "required": ["petType"] }, "Dog": { "type": "object", "properties": { "petType": {"type": "string"}, "bark": {"type": "boolean"} }, "required": ["petType"] } } } }`) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v3Model, errs := doc.BuildV3Model() require.Nil(t, errs) // Get the discriminator schema pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/test") schema := pathItem.Get.Parameters[0].Schema.Schema() // Get ground truth: what the cache would store (validation mode) renderCtx := base.NewInlineRenderContextForValidation() expectedRendered, err := schema.RenderInlineWithContext(renderCtx) require.NoError(t, err) expected := string(expectedRendered) // Test cache miss path optsNoCache := config.NewValidationOptions(config.WithSchemaCache(nil)) resultCacheMiss := GetRenderedSchema(schema, optsNoCache) assert.Equal(t, expected, resultCacheMiss, "Cache miss should produce same output as validation mode rendering") // Test cache hit path optsWithCache := config.NewValidationOptions() hash := schema.GoLow().Hash() optsWithCache.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ RenderedInline: expectedRendered, }) resultCacheHit := GetRenderedSchema(schema, optsWithCache) assert.Equal(t, expected, resultCacheHit, "Cache hit should return the cached validation mode rendering") // Test both paths are identical assert.Equal(t, resultCacheHit, resultCacheMiss, "GetRenderedSchema should produce identical output regardless of cache state") } libopenapi-validator-0.13.8/parameters/validate_security.go000066400000000000000000000236571520534042400241360ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package parameters import ( "fmt" "net/http" "strings" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" ) func (v *paramValidator) ValidateSecurity(request *http.Request) (bool, []*errors.ValidationError) { pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options) if len(errs) > 0 { return false, errs } return v.ValidateSecurityWithPathItem(request, pathItem, foundPath) } func (v *paramValidator) ValidateSecurityWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { if pathItem == nil { return false, []*errors.ValidationError{{ ValidationType: helpers.PathValidation, ValidationSubType: helpers.ValidationMissing, Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ "however that path, or the %s method for that path does not exist in the specification", request.Method, request.URL.Path, request.Method), SpecLine: -1, SpecCol: -1, HowToFix: errors.HowToFixPath, }} } if !v.options.SecurityValidation { return true, nil } // extract security for the operation, falling back to document-level global security security := helpers.EffectiveSecurityForOperation(request, pathItem, v.document.Security) if len(security) == 0 { return true, nil } var allErrors []*errors.ValidationError // each security requirement in the array is OR'd - any one passing is sufficient for _, sec := range security { if sec.ContainsEmptyRequirement { return true, nil } // within a requirement, all schemes are AND'd - all must pass requirementSatisfied := true var requirementErrors []*errors.ValidationError for pair := orderedmap.First(sec.Requirements); pair != nil; pair = pair.Next() { secName := pair.Key() // look up security from components if v.document.Components == nil || v.document.Components.SecuritySchemes.GetOrZero(secName) == nil { validationErrors := []*errors.ValidationError{ { Message: fmt.Sprintf("Security scheme '%s' is missing", secName), Reason: fmt.Sprintf("The security scheme '%s' is defined as being required, "+ "however it's missing from the components", secName), ValidationType: helpers.SecurityValidation, SpecLine: sec.GoLow().Requirements.ValueNode.Line, SpecCol: sec.GoLow().Requirements.ValueNode.Column, HowToFix: "Add the missing security scheme to the components", }, } errors.PopulateValidationErrors(validationErrors, request, pathValue) requirementSatisfied = false requirementErrors = append(requirementErrors, validationErrors...) continue } secScheme := v.document.Components.SecuritySchemes.GetOrZero(secName) schemeValid, schemeErrors := v.validateSecurityScheme(secName, secScheme, pair.Value(), sec, request, pathValue) if !schemeValid { requirementSatisfied = false requirementErrors = append(requirementErrors, schemeErrors...) } } // if all schemes in this requirement passed (AND), the overall security passes (OR) if requirementSatisfied { return true, nil } allErrors = append(allErrors, requirementErrors...) } return false, allErrors } // validateSecurityScheme checks if a single security scheme is satisfied by the request. func (v *paramValidator) validateSecurityScheme( secName string, secScheme *v3.SecurityScheme, scopes []string, sec *base.SecurityRequirement, request *http.Request, pathValue string, ) (bool, []*errors.ValidationError) { if v.options.AuthenticationFunc != nil { return v.validateAuthenticationFunc(secName, secScheme, scopes, sec, request, pathValue) } switch strings.ToLower(secScheme.Type) { case "http": return v.validateHTTPSecurityScheme(secScheme, sec, request, pathValue) case "apikey": return v.validateAPIKeySecurityScheme(secScheme, sec, request, pathValue) } // unknown scheme type - consider it valid to avoid false negatives return true, nil } func (v *paramValidator) validateAuthenticationFunc( secName string, secScheme *v3.SecurityScheme, scopes []string, sec *base.SecurityRequirement, request *http.Request, pathValue string, ) (bool, []*errors.ValidationError) { authErr := v.options.AuthenticationFunc(request.Context(), &config.AuthenticationInput{ Request: request, SecuritySchemeName: secName, SecurityScheme: secScheme, Scopes: scopes, }) if authErr == nil { return true, nil } validationErrors := []*errors.ValidationError{ { Message: fmt.Sprintf("Authentication failed for security scheme '%s'", secName), Reason: authErr.Error(), ValidationType: helpers.SecurityValidation, ValidationSubType: secScheme.Type, SpecLine: sec.GoLow().Requirements.ValueNode.Line, SpecCol: sec.GoLow().Requirements.ValueNode.Column, HowToFix: fmt.Sprintf("Provide valid credentials for security scheme '%s'", secName), }, } errors.PopulateValidationErrors(validationErrors, request, pathValue) return false, validationErrors } func (v *paramValidator) validateHTTPSecurityScheme( secScheme *v3.SecurityScheme, sec *base.SecurityRequirement, request *http.Request, pathValue string, ) (bool, []*errors.ValidationError) { authorizationHeader := request.Header.Get("Authorization") if authorizationHeader == "" { validationErrors := []*errors.ValidationError{ { Message: fmt.Sprintf("Authorization header for '%s' scheme", secScheme.Scheme), Reason: "Authorization header was not found", ValidationType: helpers.SecurityValidation, ValidationSubType: secScheme.Scheme, SpecLine: sec.GoLow().Requirements.ValueNode.Line, SpecCol: sec.GoLow().Requirements.ValueNode.Column, HowToFix: "Add an 'Authorization' header to this request", }, } errors.PopulateValidationErrors(validationErrors, request, pathValue) return false, validationErrors } if len(authorizationHeader) < len(secScheme.Scheme) || !strings.EqualFold(authorizationHeader[:len(secScheme.Scheme)], secScheme.Scheme) { validationErrors := []*errors.ValidationError{ { Message: fmt.Sprintf("Authorization header scheme '%s' mismatch", secScheme.Scheme), Reason: "Authorization header had incorrect scheme", ValidationType: helpers.SecurityValidation, ValidationSubType: secScheme.Scheme, SpecLine: sec.GoLow().Requirements.ValueNode.Line, SpecCol: sec.GoLow().Requirements.ValueNode.Column, HowToFix: fmt.Sprintf("Use the scheme '%s' in the Authorization header "+ "for this request", secScheme.Scheme), }, } errors.PopulateValidationErrors(validationErrors, request, pathValue) return false, validationErrors } return true, nil } func (v *paramValidator) validateAPIKeySecurityScheme( secScheme *v3.SecurityScheme, sec *base.SecurityRequirement, request *http.Request, pathValue string, ) (bool, []*errors.ValidationError) { switch secScheme.In { case "header": if request.Header.Get(secScheme.Name) == "" { validationErrors := []*errors.ValidationError{ { Message: fmt.Sprintf("API Key %s not found in header", secScheme.Name), Reason: "API Key not found in http header for security scheme 'apiKey' with type 'header'", ValidationType: helpers.SecurityValidation, ValidationSubType: "apiKey", SpecLine: sec.GoLow().Requirements.ValueNode.Line, SpecCol: sec.GoLow().Requirements.ValueNode.Column, HowToFix: fmt.Sprintf("Add the API Key via '%s' as a header of the request", secScheme.Name), }, } errors.PopulateValidationErrors(validationErrors, request, pathValue) return false, validationErrors } return true, nil case "query": if request.URL.Query().Get(secScheme.Name) == "" { copyUrl := *request.URL fixed := ©Url q := fixed.Query() q.Add(secScheme.Name, "your-api-key") fixed.RawQuery = q.Encode() validationErrors := []*errors.ValidationError{ { Message: fmt.Sprintf("API Key %s not found in query", secScheme.Name), Reason: "API Key not found in URL query for security scheme 'apiKey' with type 'query'", ValidationType: helpers.SecurityValidation, ValidationSubType: "apiKey", SpecLine: sec.GoLow().Requirements.ValueNode.Line, SpecCol: sec.GoLow().Requirements.ValueNode.Column, HowToFix: fmt.Sprintf("Add an API Key via '%s' to the query string "+ "of the URL, for example '%s'", secScheme.Name, fixed.String()), }, } errors.PopulateValidationErrors(validationErrors, request, pathValue) return false, validationErrors } return true, nil case "cookie": cookies := request.Cookies() for _, cookie := range cookies { if cookie.Name == secScheme.Name { return true, nil } } validationErrors := []*errors.ValidationError{ { Message: fmt.Sprintf("API Key %s not found in cookies", secScheme.Name), Reason: "API Key not found in http request cookies for security scheme 'apiKey' with type 'cookie'", ValidationType: helpers.SecurityValidation, ValidationSubType: "apiKey", SpecLine: sec.GoLow().Requirements.ValueNode.Line, SpecCol: sec.GoLow().Requirements.ValueNode.Column, HowToFix: fmt.Sprintf("Submit an API Key '%s' as a cookie with the request", secScheme.Name), }, } errors.PopulateValidationErrors(validationErrors, request, pathValue) return false, validationErrors } return true, nil } libopenapi-validator-0.13.8/parameters/validate_security_test.go000066400000000000000000001155261520534042400251720ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package parameters import ( "context" stderrors "errors" "net/http" "sync" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/paths" ) func TestParamValidator_ValidateSecurity_APIKeyHeader_NotFound(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "API Key X-API-Key not found in header", errors[0].Message) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "/products", errors[0].SpecPath) } func TestParamValidator_ValidateSecurity_APIKeyHeader(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) request.Header.Add("X-API-Key", "1234") valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } func TestParamValidator_ValidateSecurity_APIKeyQuery_NotFound(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: apiKey in: query name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "API Key X-API-Key not found in query", errors[0].Message) assert.Equal(t, "Add an API Key via 'X-API-Key' to the query string of the URL, "+ "for example 'https://things.com/products?X-API-Key=your-api-key'", errors[0].HowToFix) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "/products", errors[0].SpecPath) } func TestParamValidator_ValidateSecurity_APIKeyQuery(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: apiKey in: query name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products?X-API-Key=12345", nil) valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } func TestParamValidator_ValidateSecurity_APIKeyCookie_NotFound(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: apiKey in: cookie name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "API Key X-API-Key not found in cookies", errors[0].Message) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "/products", errors[0].SpecPath) } func TestParamValidator_ValidateSecurity_APIKeyCookie(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: apiKey in: cookie name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) request.AddCookie(&http.Cookie{ Name: "X-API-Key", Value: "1234", }) valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } func TestParamValidator_ValidateSecurity_Basic_NotFound(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: http scheme: basic ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "Authorization header for 'basic' scheme", errors[0].Message) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "/products", errors[0].SpecPath) } func TestParamValidator_ValidateSecurity_Basic(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: http scheme: basic ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) request.Header.Add("Authorization", "Basic 1234") valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } func TestParamValidator_ValidateSecurity_BadPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: http scheme: basic ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/blimpo", nil) valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "", errors[0].SpecPath) } func TestParamValidator_ValidateSecurity_MissingSecuritySchemes(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: {} ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) } func TestParamValidator_ValidateSecurity_NoComponents(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) } func TestParamValidator_ValidateSecurity_PresetPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) pathItem, errs, pv := paths.FindPath(request, &m.Model, nil) assert.Nil(t, errs) valid, errors := v.ValidateSecurityWithPathItem(request, pathItem, pv) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } func TestParamValidator_ValidateSecurity_PresetPath_notfound(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/beef", nil) pathItem, _, pv := paths.FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) valid, errors := v.ValidateSecurityWithPathItem(request, pathItem, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST Path '/beef' not found", errors[0].Message) } func TestParamValidator_ValidateSecurity_MultipleSecurity(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuthQuery: - write:products - ApiKeyAuthHeader: - write:products components: securitySchemes: ApiKeyAuthQuery: type: apiKey in: query name: X-API-Key ApiKeyAuthHeader: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) request.Header.Add("X-API-Key", "1234") valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } func TestParamValidator_ValidateSecurity_MultipleSecurity_EmptyOption(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products - {} components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } func TestParamValidator_ValidateSecurity_MultipleSecurity_NotFound(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuthQuery: - write:products - ApiKeyAuthHeader: - write:products components: securitySchemes: ApiKeyAuthQuery: type: apiKey in: query name: X-API-Key ApiKeyAuthHeader: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Equal(t, 2, len(errors)) assert.Equal(t, "API Key X-API-Key not found in query", errors[0].Message) assert.Equal(t, "Add an API Key via 'X-API-Key' to the query string of the URL, "+ "for example 'https://things.com/products?X-API-Key=your-api-key'", errors[0].HowToFix) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "/products", errors[0].SpecPath) assert.Equal(t, "API Key X-API-Key not found in header", errors[1].Message) assert.Equal(t, request.Method, errors[1].RequestMethod) assert.Equal(t, request.URL.Path, errors[1].RequestPath) assert.Equal(t, "/products", errors[1].SpecPath) } func TestParamValidator_ValidateSecurity_SecurityValidationDisabled_APIKeyHeader(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithoutSecurityValidation()) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) // No API key header provided valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } func TestParamValidator_ValidateSecurity_SecurityValidationDisabled_APIKeyQuery(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: apiKey in: query name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithoutSecurityValidation()) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) // No API key query param provided valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } func TestParamValidator_ValidateSecurity_SecurityValidationDisabled_APIKeyCookie(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: apiKey in: cookie name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithoutSecurityValidation()) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) // No API key cookie provided valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } func TestParamValidator_ValidateSecurity_SecurityValidationDisabled_BasicAuth(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: http scheme: basic ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithoutSecurityValidation()) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) // No Authorization header provided valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } func TestParamValidator_ValidateSecurity_SecurityValidationDisabled_WithPathItem(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithoutSecurityValidation()) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) pathItem, errs, pv := paths.FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.Nil(t, errs) valid, errors := v.ValidateSecurityWithPathItem(request, pathItem, pv) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } func TestParamValidator_ValidateSecurity_SecurityValidationDisabled_MissingPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model, config.WithoutSecurityValidation()) request, _ := http.NewRequest(http.MethodPost, "https://things.com/nonexistent", nil) valid, errors := v.ValidateSecurity(request) assert.False(t, valid) // Should still fail for invalid paths assert.Equal(t, 1, len(errors)) assert.Contains(t, errors[0].Message, "Path '/nonexistent' not found") } func TestParamValidator_ValidateSecurity_SecurityValidationEnabled_vs_Disabled(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuth: - write:products components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // Test with security validation enabled (default) vEnabled := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) valid, errors := vEnabled.ValidateSecurity(request) assert.False(t, valid) assert.Equal(t, 1, len(errors)) assert.Equal(t, "API Key X-API-Key not found in header", errors[0].Message) // Test with security validation disabled vDisabled := NewParameterValidator(&m.Model, config.WithoutSecurityValidation()) valid, errors = vDisabled.ValidateSecurity(request) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } func TestParamValidator_ValidateSecurity_ANDRequirement_BothPresent(t *testing.T) { // Test AND security requirement: both schemes in same requirement must pass spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuthHeader: [] BasicAuth: [] components: securitySchemes: ApiKeyAuthHeader: type: apiKey in: header name: X-API-Key BasicAuth: type: http scheme: basic ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Request with BOTH api key AND authorization header - should pass request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) request.Header.Add("X-API-Key", "1234") request.Header.Add("Authorization", "Basic dXNlcjpwYXNz") valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Empty(t, errors) } func TestParamValidator_ValidateSecurity_ANDRequirement_OnlyApiKey(t *testing.T) { // Test AND security requirement: missing one scheme should fail spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuthHeader: [] BasicAuth: [] components: securitySchemes: ApiKeyAuthHeader: type: apiKey in: header name: X-API-Key BasicAuth: type: http scheme: basic ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Request with ONLY api key - should fail because BasicAuth is also required request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) request.Header.Add("X-API-Key", "1234") valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "Authorization header") } func TestParamValidator_ValidateSecurity_ANDRequirement_OnlyBasicAuth(t *testing.T) { // Test AND security requirement: missing one scheme should fail spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuthHeader: [] BasicAuth: [] components: securitySchemes: ApiKeyAuthHeader: type: apiKey in: header name: X-API-Key BasicAuth: type: http scheme: basic ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Request with ONLY authorization header - should fail because ApiKeyAuthHeader is also required request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) request.Header.Add("Authorization", "Basic dXNlcjpwYXNz") valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "API Key") } func TestParamValidator_ValidateSecurity_ANDRequirement_NeitherPresent(t *testing.T) { // Test AND security requirement: missing both schemes should return errors for both spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuthHeader: [] BasicAuth: [] components: securitySchemes: ApiKeyAuthHeader: type: apiKey in: header name: X-API-Key BasicAuth: type: http scheme: basic ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Request with neither - should fail with errors for both request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Len(t, errors, 2) } func TestParamValidator_ValidateSecurity_ORWithAND_FirstOROptionPasses(t *testing.T) { // Test mixed OR and AND: first option is single scheme, second is AND requirement spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuthHeader: [] - BasicAuth: [] BearerAuth: [] components: securitySchemes: ApiKeyAuthHeader: type: apiKey in: header name: X-API-Key BasicAuth: type: http scheme: basic BearerAuth: type: http scheme: bearer ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Request with only API key - should pass (first OR option) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) request.Header.Add("X-API-Key", "1234") valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Empty(t, errors) } func TestParamValidator_ValidateSecurity_ORWithAND_SecondOROptionPasses(t *testing.T) { // Test mixed OR and AND: second option (AND requirement) passes spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuthHeader: [] - BasicAuth: [] ApiKeyAuthQuery: [] components: securitySchemes: ApiKeyAuthHeader: type: apiKey in: header name: X-API-Key ApiKeyAuthQuery: type: apiKey in: query name: api_key BasicAuth: type: http scheme: basic ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Request with basic auth AND query API key - should pass (second OR option, which is AND) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products?api_key=secret", nil) request.Header.Add("Authorization", "Basic dXNlcjpwYXNz") valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Empty(t, errors) } func TestParamValidator_ValidateSecurity_ORWithAND_PartialSecondOption(t *testing.T) { // Test mixed OR and AND: partial match on second option should try both and fail spec := `openapi: 3.1.0 paths: /products: post: security: - ApiKeyAuthHeader: [] - BasicAuth: [] ApiKeyAuthQuery: [] components: securitySchemes: ApiKeyAuthHeader: type: apiKey in: header name: X-API-Key ApiKeyAuthQuery: type: apiKey in: query name: api_key BasicAuth: type: http scheme: basic ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Request with only basic auth - should fail (first option needs X-API-Key header, // second option needs BOTH basic auth AND api_key query param) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) request.Header.Add("Authorization", "Basic dXNlcjpwYXNz") valid, errors := v.ValidateSecurity(request) assert.False(t, valid) // Should have errors from both OR options assert.GreaterOrEqual(t, len(errors), 1) } func TestParamValidator_ValidateSecurity_UnknownSchemeType(t *testing.T) { // Test oauth2 type - unknown to our validator, should pass through (not fail) spec := `openapi: 3.1.0 paths: /products: get: security: - OAuth2: [] components: securitySchemes: OAuth2: type: oauth2 flows: implicit: authorizationUrl: https://example.com/oauth scopes: read: Read access ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Request with no auth - should pass because oauth2 type is not validated request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Empty(t, errors) } func TestParamValidator_ValidateSecurity_CustomHTTPScheme(t *testing.T) { // Test custom HTTP scheme - should pass with correct scheme in header spec := `openapi: 3.1.0 paths: /products: get: security: - CustomAuth: [] components: securitySchemes: CustomAuth: type: http scheme: custom ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Request with custom auth header - should pass request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) request.Header.Add("Authorization", "Custom dXNlcjpwYXNz") valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Empty(t, errors) } func TestParamValidator_ValidateSecurity_APIKey_UnknownInLocation(t *testing.T) { // Test apiKey with unknown "in" location - should pass through (fallback at line 221) spec := `openapi: 3.1.0 paths: /products: get: security: - ApiKeyAuth: [] components: securitySchemes: ApiKeyAuth: type: apiKey in: body name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Request with no auth - should pass because "body" is an unknown "in" location // and the validator falls through to return true (line 221) request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Empty(t, errors) } func TestParamValidator_ValidateSecurity_HTTPScheme_Mismatch(t *testing.T) { // Test http scheme with mismatch in header: should return errors spec := `openapi: 3.1.0 paths: /products: get: security: - CustomAuth: [] components: securitySchemes: CustomAuth: type: http scheme: custom ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Request with auth header - should fail as header scheme is incorrect request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) request.Header.Add("Authorization", "Basic dXNlcjpwYXNz") valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "Authorization header scheme 'custom' mismatch") } func TestValidateSecurity_GlobalSecurity_NoOperationSecurity_Fails(t *testing.T) { // Global security defined, no operation-level override — request without credentials should fail spec := `openapi: 3.1.0 paths: /products: get: responses: "200": description: OK security: - ApiKeyAuth: [] components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "API Key X-API-Key not found in header", errors[0].Message) } func TestValidateSecurity_GlobalSecurity_RequestSatisfies(t *testing.T) { // Global security defined, request provides correct credentials — should pass spec := `openapi: 3.1.0 paths: /products: get: responses: "200": description: OK security: - ApiKeyAuth: [] components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) request.Header.Set("X-API-Key", "my-secret") valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Empty(t, errors) } func TestValidateSecurity_GlobalSecurity_OperationOverridesWithEmpty(t *testing.T) { // Global security defined, but operation overrides with empty array (opt-out) — should pass without creds spec := `openapi: 3.1.0 paths: /products: get: security: [] responses: "200": description: OK security: - ApiKeyAuth: [] components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) // No credentials valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Empty(t, errors) } func TestValidateSecurity_GlobalSecurity_OperationOverridesWithDifferent(t *testing.T) { // Global security defines ApiKey, operation overrides with BasicAuth — operation takes precedence spec := `openapi: 3.1.0 paths: /products: get: security: - BasicAuth: [] responses: "200": description: OK security: - ApiKeyAuth: [] components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key BasicAuth: type: http scheme: basic ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // Request with Basic auth (satisfying operation-level security, not global) request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) request.Header.Set("Authorization", "Basic dXNlcjpwYXNz") valid, errors := v.ValidateSecurity(request) assert.True(t, valid) assert.Empty(t, errors) // Request with API key (satisfying global, not operation-level) should fail request2, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) request2.Header.Set("X-API-Key", "my-secret") valid2, errors2 := v.ValidateSecurity(request2) assert.False(t, valid2) assert.Len(t, errors2, 1) assert.Contains(t, errors2[0].Message, "Authorization header") } func TestValidateSecurity_GlobalSecurity_HTTPBasic(t *testing.T) { // Global security with HTTP basic auth spec := `openapi: 3.1.0 paths: /products: get: responses: "200": description: OK security: - BasicAuth: [] components: securitySchemes: BasicAuth: type: http scheme: basic ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) // No auth header — should fail request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) valid, errors := v.ValidateSecurity(request) assert.False(t, valid) assert.Len(t, errors, 1) // With correct auth header — should pass request2, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) request2.Header.Set("Authorization", "Basic dXNlcjpwYXNz") valid2, errors2 := v.ValidateSecurity(request2) assert.True(t, valid2) assert.Empty(t, errors2) } func TestParamValidator_ValidateSecurity_AuthenticationFunc_OAuth2Scopes(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: get: security: - OAuth2: - read:products - write:products components: securitySchemes: OAuth2: type: oauth2 flows: clientCredentials: tokenUrl: https://example.com/oauth/token scopes: read:products: Read products write:products: Write products ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() type authContextKey struct{} var request *http.Request var gotScopes []string called := 0 authFn := func(ctx context.Context, input *config.AuthenticationInput) error { called++ assert.Equal(t, "ctx-value", ctx.Value(authContextKey{})) assert.Equal(t, request, input.Request) assert.Equal(t, "OAuth2", input.SecuritySchemeName) assert.Equal(t, "oauth2", input.SecurityScheme.Type) gotScopes = append([]string(nil), input.Scopes...) return nil } v := NewParameterValidator(&m.Model, config.WithAuthenticationFunc(authFn)) request, _ = http.NewRequest(http.MethodGet, "https://things.com/products", nil) request = request.WithContext(context.WithValue(request.Context(), authContextKey{}, "ctx-value")) valid, validationErrors := v.ValidateSecurity(request) assert.True(t, valid) assert.Empty(t, validationErrors) assert.Equal(t, 1, called) assert.ElementsMatch(t, []string{"read:products", "write:products"}, gotScopes) } func TestParamValidator_ValidateSecurity_AuthenticationFunc_OpenIDConnect(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: get: security: - OpenID: [] components: securitySchemes: OpenID: type: openIdConnect openIdConnectUrl: https://example.com/.well-known/openid-configuration ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() called := false authFn := func(ctx context.Context, input *config.AuthenticationInput) error { called = true assert.NotNil(t, ctx) assert.Equal(t, "OpenID", input.SecuritySchemeName) assert.Equal(t, "openIdConnect", input.SecurityScheme.Type) assert.Empty(t, input.Scopes) return nil } v := NewParameterValidator(&m.Model, config.WithAuthenticationFunc(authFn)) request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) valid, validationErrors := v.ValidateSecurity(request) assert.True(t, valid) assert.Empty(t, validationErrors) assert.True(t, called) } func TestParamValidator_ValidateSecurity_AuthenticationFunc_ORSuccessAfterFirstFailure(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: get: security: - OAuth2: - read:products - ApiKeyAuth: [] components: securitySchemes: OAuth2: type: oauth2 flows: clientCredentials: tokenUrl: https://example.com/oauth/token scopes: read:products: Read products ApiKeyAuth: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() calls := make(map[string]int) authFn := func(_ context.Context, input *config.AuthenticationInput) error { calls[input.SecuritySchemeName]++ if input.SecuritySchemeName == "OAuth2" { return stderrors.New("token missing") } return nil } v := NewParameterValidator(&m.Model, config.WithAuthenticationFunc(authFn)) request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) valid, validationErrors := v.ValidateSecurity(request) assert.True(t, valid) assert.Empty(t, validationErrors) assert.Equal(t, 1, calls["OAuth2"]) assert.Equal(t, 1, calls["ApiKeyAuth"]) } func TestParamValidator_ValidateSecurity_AuthenticationFunc_ANDPartialFailure(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: get: security: - OAuth2: - read:products ApiKeyAuth: [] components: securitySchemes: OAuth2: type: oauth2 flows: clientCredentials: tokenUrl: https://example.com/oauth/token scopes: read:products: Read products ApiKeyAuth: type: apiKey in: header name: X-API-Key ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() calls := make(map[string]int) authFn := func(_ context.Context, input *config.AuthenticationInput) error { calls[input.SecuritySchemeName]++ if input.SecuritySchemeName == "ApiKeyAuth" { return stderrors.New("api key denied") } return nil } v := NewParameterValidator(&m.Model, config.WithAuthenticationFunc(authFn)) request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) valid, validationErrors := v.ValidateSecurity(request) assert.False(t, valid) assert.Len(t, validationErrors, 1) assert.Equal(t, "Authentication failed for security scheme 'ApiKeyAuth'", validationErrors[0].Message) assert.Equal(t, "api key denied", validationErrors[0].Reason) assert.Equal(t, 1, calls["OAuth2"]) assert.Equal(t, 1, calls["ApiKeyAuth"]) } func TestParamValidator_ValidateSecurity_AuthenticationFunc_ErrorReturned(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: get: security: - OAuth2: - read:products components: securitySchemes: OAuth2: type: oauth2 flows: clientCredentials: tokenUrl: https://example.com/oauth/token scopes: read:products: Read products ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() authFn := func(context.Context, *config.AuthenticationInput) error { return stderrors.New("expired token") } v := NewParameterValidator(&m.Model, config.WithAuthenticationFunc(authFn)) request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) valid, validationErrors := v.ValidateSecurity(request) assert.False(t, valid) assert.Len(t, validationErrors, 1) assert.Equal(t, "Authentication failed for security scheme 'OAuth2'", validationErrors[0].Message) assert.Equal(t, "expired token", validationErrors[0].Reason) assert.Equal(t, "security", validationErrors[0].ValidationType) assert.Equal(t, "oauth2", validationErrors[0].ValidationSubType) assert.Equal(t, request.Method, validationErrors[0].RequestMethod) assert.Equal(t, request.URL.Path, validationErrors[0].RequestPath) assert.Equal(t, "/products", validationErrors[0].SpecPath) } func TestParamValidator_ValidateSecurity_AuthenticationFunc_SkippedWhenSecurityValidationDisabled(t *testing.T) { spec := `openapi: 3.1.0 paths: /products: get: security: - OAuth2: - read:products components: securitySchemes: OAuth2: type: oauth2 flows: clientCredentials: tokenUrl: https://example.com/oauth/token scopes: read:products: Read products ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() called := false authFn := func(context.Context, *config.AuthenticationInput) error { called = true return stderrors.New("should not be called") } v := NewParameterValidator(&m.Model, config.WithAuthenticationFunc(authFn), config.WithoutSecurityValidation()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/products", nil) valid, validationErrors := v.ValidateSecurity(request) assert.True(t, valid) assert.Empty(t, validationErrors) assert.False(t, called) } libopenapi-validator-0.13.8/parameters/validation_functions.go000066400000000000000000000240731520534042400246310ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package parameters import ( "encoding/json" "fmt" "strconv" "strings" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" ) // ValidateCookieArray will validate a cookie parameter that is an array func ValidateCookieArray( sch *base.Schema, param *v3.Parameter, value string, pathTemplate string, operation string, renderedSchema string, ) []*errors.ValidationError { var validationErrors []*errors.ValidationError itemsSchema := sch.Items.A.Schema() var renderedItemsSchema string if itemsSchema != nil { rendered, _ := itemsSchema.RenderInline() schemaBytes, _ := json.Marshal(rendered) renderedItemsSchema = string(schemaBytes) } // header arrays can only be encoded as CSV items := helpers.ExplodeQueryValue(value, helpers.DefaultDelimited) // now check each item in the array for _, item := range items { // for each type defined in the item's schema, check the item for _, itemType := range itemsSchema.Type { switch itemType { case helpers.Integer, helpers.Number: if _, err := strconv.ParseFloat(item, 64); err != nil { validationErrors = append(validationErrors, errors.IncorrectCookieParamArrayNumber(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.Boolean: if _, err := strconv.ParseBool(item); err != nil { validationErrors = append(validationErrors, errors.IncorrectCookieParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) break } // check for edge-cases "0" and "1" which can also be parsed into valid booleans if item == "0" || item == "1" { validationErrors = append(validationErrors, errors.IncorrectCookieParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.String: // do nothing for now. continue } } } return validationErrors } // ValidateHeaderArray will validate a header parameter that is an array func ValidateHeaderArray( sch *base.Schema, param *v3.Parameter, value string, pathTemplate string, operation string, renderedSchema string, ) []*errors.ValidationError { var validationErrors []*errors.ValidationError itemsSchema := sch.Items.A.Schema() var renderedItemsSchema string if itemsSchema != nil { rendered, _ := itemsSchema.RenderInline() schemaBytes, _ := json.Marshal(rendered) renderedItemsSchema = string(schemaBytes) } // header arrays can only be encoded as CSV items := helpers.ExplodeQueryValue(value, helpers.DefaultDelimited) // now check each item in the array for _, item := range items { // for each type defined in the item's schema, check the item for _, itemType := range itemsSchema.Type { switch itemType { case helpers.Integer, helpers.Number: if _, err := strconv.ParseFloat(item, 64); err != nil { validationErrors = append(validationErrors, errors.IncorrectHeaderParamArrayNumber(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.Boolean: if _, err := strconv.ParseBool(item); err != nil { validationErrors = append(validationErrors, errors.IncorrectHeaderParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) break } // check for edge-cases "0" and "1" which can also be parsed into valid booleans if item == "0" || item == "1" { validationErrors = append(validationErrors, errors.IncorrectHeaderParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.String: // do nothing for now. continue } } } return validationErrors } // ValidateQueryArray will validate a query parameter that is an array func ValidateQueryArray( sch *base.Schema, param *v3.Parameter, ef string, contentWrapped bool, validationOptions *config.ValidationOptions, pathTemplate string, operation string, renderedSchema string, ) []*errors.ValidationError { var validationErrors []*errors.ValidationError itemsSchema := sch.Items.A.Schema() // Get rendered items schema for ReferenceSchema field in errors (uses cache if available) renderedItemsSchema := GetRenderedSchema(itemsSchema, validationOptions) // check for an exploded bit on the schema. // if it's exploded, then we need to check each item in the array // if it's not exploded, then we need to check the whole array as a string var items []string if param.IsExploded() { items = helpers.ExplodeQueryValue(ef, param.Style) } else { // check for a style of form (or no style) and if so, explode the value if param.Style == "" || param.Style == helpers.Form { if !contentWrapped { items = helpers.ExplodeQueryValue(ef, param.Style) } else { items = []string{ef} } } else { switch param.Style { case helpers.PipeDelimited, helpers.SpaceDelimited: items = helpers.ExplodeQueryValue(ef, param.Style) } } } // check if the param is within an enum checkEnum := func(item string) { // check if the array param is within an enum if sch.Items.IsA() { itemsSch := sch.Items.A.Schema() if itemsSch.Enum != nil { matchFound := false for _, enumVal := range itemsSch.Enum { if strings.TrimSpace(item) == fmt.Sprint(enumVal.Value) { matchFound = true break } } if !matchFound { validationErrors = append(validationErrors, errors.IncorrectQueryParamEnumArray(param, item, sch, pathTemplate, operation, renderedItemsSchema)) } } } } // now check each item in the array seen := make(map[string]struct{}) uniqueItems := true var duplicates []string for _, item := range items { if _, exists := seen[item]; exists { uniqueItems = false duplicates = append(duplicates, item) } seen[item] = struct{}{} // for each type defined in the item's schema, check the item for _, itemType := range itemsSchema.Type { switch itemType { case helpers.Integer: if _, err := strconv.ParseInt(item, 10, 64); err != nil { validationErrors = append(validationErrors, errors.IncorrectQueryParamArrayInteger(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) break } // will it blend? checkEnum(item) case helpers.Number: if _, err := strconv.ParseFloat(item, 64); err != nil { validationErrors = append(validationErrors, errors.IncorrectQueryParamArrayNumber(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) break } // will it blend? checkEnum(item) case helpers.Boolean: if _, err := strconv.ParseBool(item); err != nil { validationErrors = append(validationErrors, errors.IncorrectQueryParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.Object: validationErrors = append(validationErrors, ValidateParameterSchema(itemsSchema, nil, item, "Query array parameter", "The query parameter (which is an array)", param.Name, helpers.ParameterValidation, helpers.ParameterValidationQuery, validationOptions)...) case helpers.String: // will it float? checkEnum(item) } } } // check for min and max items if sch.MaxItems != nil { if len(items) > int(*sch.MaxItems) { validationErrors = append(validationErrors, errors.IncorrectParamArrayMaxNumItems(param, sch, *sch.MaxItems, int64(len(items)), pathTemplate, operation, renderedSchema)) } } if sch.MinItems != nil { if len(items) < int(*sch.MinItems) { validationErrors = append(validationErrors, errors.IncorrectParamArrayMinNumItems(param, sch, *sch.MinItems, int64(len(items)), pathTemplate, operation, renderedSchema)) } } // check for unique items if sch.UniqueItems != nil { if *sch.UniqueItems && !uniqueItems { validationErrors = append(validationErrors, errors.IncorrectParamArrayUniqueItems(param, sch, strings.Join(duplicates, ", "), pathTemplate, operation, renderedSchema)) } } return validationErrors } // ValidateQueryParamStyle will validate a query parameter by style func ValidateQueryParamStyle(param *v3.Parameter, as []*helpers.QueryParam) []*errors.ValidationError { var validationErrors []*errors.ValidationError if param.Style == helpers.DeepObject { if prefixParam, nestedParam, ok := helpers.DeepObjectPathConflict(as); ok { return []*errors.ValidationError{errors.InvalidDeepObjectPathConflict(param, prefixParam, nestedParam)} } } stopValidation: for _, qp := range as { for i := range qp.Values { switch param.Style { case helpers.DeepObject: // check if the object has additional properties defined that treat this as an array if param.Schema != nil && helpers.DeepObjectAllowsMultipleValues(param.Schema.Schema(), qp) { continue } if len(qp.Values) > 1 { validationErrors = append(validationErrors, errors.InvalidDeepObject(param, qp)) break stopValidation } case helpers.PipeDelimited: // check if explode is false, but we have used an array style if !param.IsExploded() { if len(qp.Values) > 1 { validationErrors = append(validationErrors, errors.IncorrectPipeDelimiting(param, qp)) break stopValidation } } case helpers.SpaceDelimited: // check if explode is false, but we have used an array style if !param.IsExploded() { if len(qp.Values) > 1 { validationErrors = append(validationErrors, errors.IncorrectSpaceDelimiting(param, qp)) break stopValidation } } default: // check for a delimited list. if helpers.DoesFormParamContainDelimiter(qp.Values[i], param.Style) { if param.Explode != nil && *param.Explode { validationErrors = append(validationErrors, errors.IncorrectFormEncoding(param, qp, i)) break stopValidation } } } } } return validationErrors // defaults to true if no style is set. } libopenapi-validator-0.13.8/paths/000077500000000000000000000000001520534042400170265ustar00rootroot00000000000000libopenapi-validator-0.13.8/paths/package.go000066400000000000000000000003171520534042400207510ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT // Package paths contains all the logic, models and interfaces for validating OpenAPI 3+ Paths. package paths libopenapi-validator-0.13.8/paths/paths.go000066400000000000000000000171241520534042400205010ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package paths import ( "fmt" "net/http" "net/url" "path/filepath" "regexp" "strings" "github.com/pb33f/libopenapi/orderedmap" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" ) // FindPath will find the path in the document that matches the request path. If a successful match was found, then // the first return value will be a pointer to the PathItem. The second return value will contain any validation errors // that were picked up when locating the path. // The third return value will be the path that was found in the document, as it pertains to the contract, so all path // parameters will not have been replaced with their values from the request - allowing model lookups. // // This function first tries a fast O(k) radix tree lookup (where k is path depth). If the radix tree // doesn't find a match, it falls back to regex-based matching which handles complex path patterns // like matrix-style ({;param}), label-style ({.param}), and OData-style (entities('{Entity}')). // // Path matching follows the OpenAPI specification: literal (concrete) paths take precedence over // parameterized paths, regardless of definition order in the specification. func FindPath(request *http.Request, document *v3.Document, options *config.ValidationOptions) (*v3.PathItem, []*errors.ValidationError, string) { stripped := StripRequestPath(request, document) // Fast path: try radix tree first (O(k) where k = path depth) // If no path lookup is provided, we will fall back to regex-based matching. if options != nil && options.PathTree != nil { if pathItem, matchedPath, found := options.PathTree.Lookup(stripped); found { if pathHasMethod(pathItem, request.Method) { return pathItem, nil, matchedPath } return pathItem, missingOperationError(request, matchedPath), matchedPath } } // Slow path: fall back to regex matching for complex paths (matrix, label, OData, etc.) basePaths := getBasePaths(document) reqPathSegments := strings.Split(stripped, "/") if reqPathSegments[0] == "" { reqPathSegments = reqPathSegments[1:] } var regexCache config.RegexCache if options != nil { regexCache = options.RegexCache } candidates := make([]pathCandidate, 0, document.Paths.PathItems.Len()) for pair := orderedmap.First(document.Paths.PathItems); pair != nil; pair = pair.Next() { path := pair.Key() pathItem := pair.Value() pathForMatching := normalizePathForMatching(path, stripped) segs := strings.Split(pathForMatching, "/") if segs[0] == "" { segs = segs[1:] } ok := comparePaths(segs, reqPathSegments, basePaths, regexCache) if !ok { continue } // Compute specificity score and check if method exists score := computeSpecificityScore(path) hasMethod := pathHasMethod(pathItem, request.Method) candidates = append(candidates, pathCandidate{ pathItem: pathItem, path: path, score: score, hasMethod: hasMethod, }) } if len(candidates) == 0 { validationErrors := []*errors.ValidationError{ { ValidationType: helpers.PathValidation, ValidationSubType: helpers.ValidationMissing, Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ "however that path, or the %s method for that path does not exist in the specification", request.Method, request.URL.Path, request.Method), SpecLine: -1, SpecCol: -1, HowToFix: errors.HowToFixPath, }, } errors.PopulateValidationErrors(validationErrors, request, "") return nil, validationErrors, "" } bestWithMethod, bestOverall := selectMatches(candidates) if bestWithMethod != nil { return bestWithMethod.pathItem, nil, bestWithMethod.path } // path matches exist but none have the required method return bestOverall.pathItem, missingOperationError(request, bestOverall.path), bestOverall.path } // normalizePathForMatching removes the fragment from a path template unless // the request path itself contains a fragment. func normalizePathForMatching(path, requestPath string) string { if strings.Contains(requestPath, "#") { return path } if idx := strings.IndexByte(path, '#'); idx >= 0 { return path[:idx] } return path } func getBasePaths(document *v3.Document) []string { // extract base path from document to check against paths. var basePaths []string for _, s := range document.Servers { u, err := url.Parse(s.URL) // if the host contains special characters, we should attempt to split and parse only the relative path if err != nil { // split at first occurrence _, serverPath, _ := strings.Cut(strings.Replace(s.URL, "//", "", 1), "/") if !strings.HasPrefix(serverPath, "/") { serverPath = "/" + serverPath } u, _ = url.Parse(serverPath) } if u != nil && u.Path != "" { basePaths = append(basePaths, u.Path) } } return basePaths } // StripRequestPath strips the base path from the request path, based on the server paths provided in the specification func StripRequestPath(request *http.Request, document *v3.Document) string { basePaths := getBasePaths(document) // strip any base path stripped := stripBaseFromPath(request.URL.EscapedPath(), basePaths) if request.URL.Fragment != "" { stripped = fmt.Sprintf("%s#%s", stripped, request.URL.Fragment) } if !strings.HasPrefix(stripped, "/") { stripped = "/" + stripped } return stripped } func checkPathAgainstBase(docPath, urlPath string, basePaths []string) bool { if docPath == urlPath { return true } for _, basePath := range basePaths { if len(basePath) > 1 && basePath[len(basePath)-1] == '/' { basePath = basePath[:len(basePath)-1] } merged := fmt.Sprintf("%s%s", basePath, urlPath) if docPath == merged { return true } } return false } func stripBaseFromPath(path string, basePaths []string) string { for i := range basePaths { if strings.HasPrefix(path, basePaths[i]) { return path[len(basePaths[i]):] } } return path } func comparePaths(mapped, requested, basePaths []string, regexCache config.RegexCache) bool { if len(mapped) != len(requested) { return false // short circuit out } var imploded []string for i, seg := range mapped { s := seg var rgx *regexp.Regexp if regexCache != nil { if cachedRegex, found := regexCache.Load(s); found { rgx = cachedRegex.(*regexp.Regexp) } } if rgx == nil { r, err := helpers.GetRegexForPath(seg) if err != nil { return false } rgx = r if regexCache != nil { regexCache.Store(seg, r) } } if rgx.MatchString(requested[i]) { s = requested[i] } imploded = append(imploded, s) } l := filepath.Join(imploded...) r := filepath.Join(requested...) return checkPathAgainstBase(l, r, basePaths) } // missingOperationError returns a validation error for when a path was found but the HTTP method doesn't exist. func missingOperationError(request *http.Request, matchedPath string) []*errors.ValidationError { validationErrors := []*errors.ValidationError{{ ValidationType: helpers.PathValidation, ValidationSubType: helpers.ValidationMissingOperation, Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), Reason: fmt.Sprintf("The %s method for that path does not exist in the specification", request.Method), SpecLine: -1, SpecCol: -1, HowToFix: errors.HowToFixPath, }} errors.PopulateValidationErrors(validationErrors, request, matchedPath) return validationErrors } libopenapi-validator-0.13.8/paths/paths_test.go000066400000000000000000001332661520534042400215460ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package paths import ( "fmt" "net/http" "os" "regexp" "sync" "testing" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/radix" "github.com/stretchr/testify/assert" ) func TestNewValidator_BadParam(t *testing.T) { request, _ := http.NewRequest(http.MethodGet, "https://things.com/pet/doggy", nil) // load a doc b, _ := os.ReadFile("../test_specs/petstorev3.json") doc, _ := libopenapi.NewDocument(b) m, _ := doc.BuildV3Model() pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) } func TestNewValidator_GoodParamFloat(t *testing.T) { request, _ := http.NewRequest(http.MethodGet, "https://things.com/pet/232.233", nil) b, _ := os.ReadFile("../test_specs/petstorev3.json") doc, _ := libopenapi.NewDocument(b) m, _ := doc.BuildV3Model() pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) } func TestNewValidator_GoodParamInt(t *testing.T) { request, _ := http.NewRequest(http.MethodGet, "https://things.com/pet/12334", nil) b, _ := os.ReadFile("../test_specs/petstorev3.json") doc, _ := libopenapi.NewDocument(b) m, _ := doc.BuildV3Model() pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) } func TestNewValidator_FindSimpleEncodedArrayPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId*}/locate: patch: operationId: locateBurger ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/1,2,3,4,5/locate", nil) pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) } func TestNewValidator_FindSimpleEncodedObjectPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId*}/locate: patch: operationId: locateBurger ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/bish=bosh,wish=wash/locate", nil) pathItem, _, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) } func TestNewValidator_FindLabelEncodedArrayPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: patch: operationId: locateBurger ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/.1.2.3.4.5/locate", nil) pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) } func TestNewValidator_FindPathPost(t *testing.T) { // load a doc b, _ := os.ReadFile("../test_specs/petstorev3.json") doc, _ := libopenapi.NewDocument(b) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPost, "https://things.com/pet/12334", nil) pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) } func TestNewValidator_FindPathDelete(t *testing.T) { // load a doc b, _ := os.ReadFile("../test_specs/petstorev3.json") doc, _ := libopenapi.NewDocument(b) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodDelete, "https://things.com/pet/12334", nil) pathItem, _, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.NotNil(t, pathItem) } func TestNewValidator_FindPathPatch(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}: patch: operationId: locateBurger ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/12345", nil) pathItem, _, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) } func TestNewValidator_FindPathOptions(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}: options: operationId: locateBurger ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodOptions, "https://things.com/burgers/12345", nil) pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Options.OperationId) } func TestNewValidator_FindPathTrace(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}: trace: operationId: locateBurger ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodTrace, "https://things.com/burgers/12345", nil) pathItem, _, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Trace.OperationId) } func TestNewValidator_FindPathPut(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}: put: operationId: locateBurger ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPut, "https://things.com/burgers/12345", nil) pathItem, _, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Put.OperationId) } func TestNewValidator_FindPathHead(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}: head: operationId: locateBurger ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodHead, "https://things.com/burgers/12345", nil) pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "locateBurger", pathItem.Head.OperationId) } func TestNewValidator_FindPathWithBaseURLInServer(t *testing.T) { spec := `openapi: 3.1.0 servers: - url: https://things.com/base1 - url: https://things.com/base2 - url: https://things.com/base3/base4/base5/base6/ paths: /user: post: operationId: addUser ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // check against base1 request, _ := http.NewRequest(http.MethodPost, "https://things.com/base1/user", nil) pathItem, _, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.NotNil(t, pathItem) assert.Equal(t, "addUser", pathItem.Post.OperationId) // check against base2 request, _ = http.NewRequest(http.MethodPost, "https://things.com/base2/user", nil) pathItem, _, _ = FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.NotNil(t, pathItem) assert.Equal(t, "addUser", pathItem.Post.OperationId) // check against a deeper base request, _ = http.NewRequest(http.MethodPost, "https://things.com/base3/base4/base5/base6/user", nil) pathItem, _, _ = FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "addUser", pathItem.Post.OperationId) } func TestNewValidator_FindPathWithBaseURLInServer_Args(t *testing.T) { spec := `openapi: 3.1.0 servers: - url: https://things.com/base3/base4/base5/base6/ paths: /user/{userId}/thing/{thingId}: post: operationId: addUser ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // check against a deeper base request, _ := http.NewRequest(http.MethodPost, "https://things.com/base3/base4/base5/base6/user/1234/thing/abcd", nil) pathItem, _, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.NotNil(t, pathItem) assert.Equal(t, "addUser", pathItem.Post.OperationId) } func TestNewValidator_FindPathMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: head: operationId: locateFishy ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodHead, "https://things.com/not/here", nil) pathItem, errs, _ := FindPath(request, &m.Model, nil) assert.Nil(t, pathItem) assert.NotNil(t, errs) assert.Equal(t, "HEAD Path '/not/here' not found", errs[0].Message) assert.True(t, errs[0].IsPathMissingError()) } func TestNewValidator_FindOperationMissing(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/{burgerId}: trace: operationId: locateBurger ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPut, "https://things.com/burgers/12345", nil) pathItem, errs, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.NotNil(t, errs) assert.Equal(t, "PUT Path '/burgers/12345' not found", errs[0].Message) assert.True(t, errs[0].IsOperationMissingError()) } func TestNewValidator_GetLiteralMatch(t *testing.T) { request, _ := http.NewRequest(http.MethodGet, "https://things.com/store/inventory", nil) // load a doc b, _ := os.ReadFile("../test_specs/petstorev3.json") doc, _ := libopenapi.NewDocument(b) m, _ := doc.BuildV3Model() _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 0) } func TestNewValidator_PostLiteralMatch(t *testing.T) { request, _ := http.NewRequest(http.MethodPost, "https://things.com/user", nil) // load a doc b, _ := os.ReadFile("../test_specs/petstorev3.json") doc, _ := libopenapi.NewDocument(b) m, _ := doc.BuildV3Model() _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 0) } func TestNewValidator_PutLiteralMatch(t *testing.T) { spec := `openapi: 3.1.0 paths: /pizza/burger: put: operationId: locateBurger` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPut, "https://things.com/pizza/burger", nil) _, errs, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.Len(t, errs, 0) } func TestNewValidator_PutMatch_Error(t *testing.T) { spec := `openapi: 3.1.0 paths: /pizza/{cakes}: put: operationId: locateBurger parameters: - name: cakes in: path required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPost, "https://things.com/pizza/1234", nil) _, errs, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.Len(t, errs, 1) assert.Equal(t, "POST Path '/pizza/1234' not found", errs[0].Message) } func TestNewValidator_OptionsMatch_Error(t *testing.T) { spec := `openapi: 3.1.0 paths: /pizza/{cakes}: options: operationId: locateBurger parameters: - name: cakes in: path required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPost, "https://things.com/pizza/1234", nil) _, errs, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.Len(t, errs, 1) assert.Equal(t, "POST Path '/pizza/1234' not found", errs[0].Message) } func TestNewValidator_PatchLiteralMatch(t *testing.T) { spec := `openapi: 3.1.0 paths: /pizza/burger: patch: operationId: locateBurger` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPatch, "https://things.com/pizza/burger", nil) _, errs, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.Len(t, errs, 0) } func TestNewValidator_PatchMatch_Error(t *testing.T) { spec := `openapi: 3.1.0 paths: /pizza/{cakes}: patch: operationId: locateBurger parameters: - name: cakes in: path required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPost, "https://things.com/pizza/1234", nil) _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 1) assert.Equal(t, "POST Path '/pizza/1234' not found", errs[0].Message) } func TestNewValidator_DeleteLiteralMatch(t *testing.T) { spec := `openapi: 3.1.0 paths: /pizza/burger: delete: operationId: locateBurger` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodDelete, "https://things.com/pizza/burger", nil) _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 0) } func TestNewValidator_OptionsLiteralMatch(t *testing.T) { spec := `openapi: 3.1.0 paths: /pizza/burger: options: operationId: locateBurger` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodOptions, "https://things.com/pizza/burger", nil) _, errs, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.Len(t, errs, 0) } func TestNewValidator_HeadLiteralMatch(t *testing.T) { spec := `openapi: 3.1.0 paths: /pizza/burger: head: operationId: locateBurger` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodHead, "https://things.com/pizza/burger", nil) _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 0) } func TestNewValidator_TraceLiteralMatch(t *testing.T) { spec := `openapi: 3.1.0 paths: /pizza/burger: trace: operationId: locateBurger` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodTrace, "https://things.com/pizza/burger", nil) _, errs, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.Len(t, errs, 0) } func TestNewValidator_TraceMatch_Error(t *testing.T) { spec := `openapi: 3.1.0 paths: /pizza/{cakes}: trace: operationId: locateBurger parameters: - name: cakes in: path required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPost, "https://things.com/pizza/1234", nil) _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 1) assert.Equal(t, "POST Path '/pizza/1234' not found", errs[0].Message) } func TestNewValidator_DeleteMatch_Error(t *testing.T) { spec := `openapi: 3.1.0 paths: /pizza/{cakes}: delete: operationId: locateBurger parameters: - name: cakes in: path required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPost, "https://things.com/pizza/1234", nil) _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 1) assert.Equal(t, "POST Path '/pizza/1234' not found", errs[0].Message) } func TestNewValidator_PostMatch_Error(t *testing.T) { spec := `openapi: 3.1.0 paths: /pizza/{cakes}: post: operationId: locateBurger parameters: - name: cakes in: path required: true schema: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPut, "https://things.com/pizza/1234", nil) _, errs, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.Len(t, errs, 1) assert.Equal(t, "PUT Path '/pizza/1234' not found", errs[0].Message) } func TestNewValidator_FindPathWithFragment(t *testing.T) { spec := `openapi: 3.1.0 paths: /hashy#one: post: operationId: one /hashy#two: post: operationId: two ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPost, "https://things.com/hashy#one", nil) pathItem, errs, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.Len(t, errs, 0) assert.NotNil(t, pathItem) assert.Equal(t, "one", pathItem.Post.OperationId) request, _ = http.NewRequest(http.MethodPost, "https://things.com/hashy#two", nil) pathItem, errs, _ = FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.Len(t, errs, 0) assert.NotNil(t, pathItem) assert.Equal(t, "two", pathItem.Post.OperationId) } func TestNewValidator_FindPathMissingWithBaseURLInServer(t *testing.T) { spec := `openapi: 3.1.0 servers: - url: 'https://things.com/' paths: /dishy: get: operationId: one ` doc, err := libopenapi.NewDocument([]byte(spec)) if err != nil { t.Fatal(err) } m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodGet, "https://things.com/not_here", nil) _, errs, _ := FindPath(request, &m.Model, nil) assert.Len(t, errs, 1) assert.Equal(t, "GET Path '/not_here' not found", errs[0].Message) } func TestStripRequestPath(t *testing.T) { tests := []struct { name string servers []string reqURL string want string }{ { name: "no servers, no base path to strip", servers: nil, reqURL: "https://things.com/users", want: "/users", }, { name: "host-only server URL leaves request path untouched", servers: []string{"https://things.com"}, reqURL: "https://things.com/users", want: "/users", }, { name: "base path prefix stripped from request path", servers: []string{"https://things.com/api"}, reqURL: "https://things.com/api/users", want: "/users", }, { name: "base path fully consumes request path yields /", servers: []string{"https://things.com/v1beta/weather"}, reqURL: "https://things.com/v1beta/weather", want: "/", }, { name: "fragment preserved after stripping", servers: []string{"https://things.com/api"}, reqURL: "https://things.com/api/users#frag", want: "/users#frag", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() spec := "openapi: 3.1.0\n" if len(test.servers) > 0 { spec += "servers:\n" for _, s := range test.servers { spec += fmt.Sprintf(" - url: %q\n", s) } } spec += "paths:\n /users:\n get:\n operationId: one\n" doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) m, _ := doc.BuildV3Model() req, _ := http.NewRequest(http.MethodGet, test.reqURL, nil) assert.Equal(t, test.want, StripRequestPath(req, &m.Model)) }) } } func TestNewValidator_FindPath_BasePathFullyConsumesRequestPath(t *testing.T) { spec := `openapi: 3.1.0 servers: - url: https://things.com/v1beta/weather paths: /: post: operationId: rootOp ` doc, err := libopenapi.NewDocument([]byte(spec)) if err != nil { t.Fatal(err) } m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPost, "https://things.com/v1beta/weather", nil) pathItem, errs, matched := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.Empty(t, errs) assert.NotNil(t, pathItem) assert.Equal(t, "rootOp", pathItem.Post.OperationId) assert.Equal(t, "/", matched) } func TestGetBasePaths(t *testing.T) { spec := `openapi: 3.1.0 servers: - url: 'https://things.com/' - url: 'https://things.com/some/path' - url: 'https://things.com/more//paths//please' - url: 'https://{invalid}.com/' - url: 'https://{invalid}.com/some/path' - url: 'https://{invalid}.com/more//paths//please' - url: 'https://{invalid}.com//even//more//paths//please' paths: /dishy: get: operationId: one ` doc, err := libopenapi.NewDocument([]byte(spec)) if err != nil { t.Fatal(err) } m, _ := doc.BuildV3Model() basePaths := getBasePaths(&m.Model) expectedPaths := []string{ "/", "/some/path", "/more//paths//please", "/", "/some/path", "/more//paths//please", "/even//more//paths//please", } assert.Equal(t, expectedPaths, basePaths) } func TestNewValidator_FindPathWithEncodedArg(t *testing.T) { spec := `openapi: 3.1.0 paths: /something/{string_contains_encoded}: put: operationId: putSomething ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodPut, "https://things.com/something/pkg%3Agithub%2Frs%2Fzerolog%40v1.18.0", nil) pathItem, errs, _ := FindPath(request, &m.Model, nil) assert.Equal(t, 0, len(errs), "Errors found: %v", errs) assert.NotNil(t, pathItem) } func TestNewValidator_ODataFormattedOpenAPISpecs(t *testing.T) { spec := `openapi: 3.0.0 paths: /entities('{Entity}'): parameters: - description: 'key: Entity' in: path name: Entity required: true schema: type: integer get: operationId: one /orders(RelationshipNumber='{RelationshipNumber}',ValidityEndDate=datetime'{ValidityEndDate}'): parameters: - name: RelationshipNumber in: path required: true schema: type: integer - name: ValidityEndDate in: path required: true schema: type: string get: operationId: one ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodGet, "https://things.com/entities('1')", nil) pathItem, _, _ := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "one", pathItem.Get.OperationId) request, _ = http.NewRequest(http.MethodGet, "https://things.com/orders(RelationshipNumber='1234',ValidityEndDate=datetime'1492041600000')", nil) pathItem, _, _ = FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "one", pathItem.Get.OperationId) request, _ = http.NewRequest(http.MethodGet, "https://things.com/orders(RelationshipNumber='dummy',ValidityEndDate=datetime'1492041600000')", nil) pathItem, _, _ = FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.Equal(t, "one", pathItem.Get.OperationId) } func TestNewValidator_ODataFormattedOpenAPISpecsWithRegexCache(t *testing.T) { spec := `openapi: 3.0.0 paths: /entities('{Entity}'): parameters: - description: 'key: Entity' in: path name: Entity required: true schema: type: integer get: operationId: one /orders(RelationshipNumber='{RelationshipNumber}',ValidityEndDate=datetime'{ValidityEndDate}'): parameters: - name: RelationshipNumber in: path required: true schema: type: integer - name: ValidityEndDate in: path required: true schema: type: string get: operationId: one ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodGet, "https://things.com/entities('1')", nil) opts := &config.ValidationOptions{RegexCache: &sync.Map{}} pathItem, _, _ := FindPath(request, &m.Model, opts) assert.NotNil(t, pathItem) assert.Equal(t, "one", pathItem.Get.OperationId) request, _ = http.NewRequest(http.MethodGet, "https://things.com/orders(RelationshipNumber='1234',ValidityEndDate=datetime'1492041600000')", nil) pathItem, _, _ = FindPath(request, &m.Model, opts) assert.NotNil(t, pathItem) assert.Equal(t, "one", pathItem.Get.OperationId) request, _ = http.NewRequest(http.MethodGet, "https://things.com/orders(RelationshipNumber='dummy',ValidityEndDate=datetime'1492041600000')", nil) pathItem, _, _ = FindPath(request, &m.Model, opts) assert.NotNil(t, pathItem) assert.Equal(t, "one", pathItem.Get.OperationId) } func TestNewValidator_ODataFormattedOpenAPISpecs_Error(t *testing.T) { spec := `openapi: 3.0.0 paths: /entities('{Entity'): parameters: - in: path name: Entity required: true schema: type: integer get: operationId: one ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodGet, "https://things.com/entities('1')", nil) _, errs, _ := FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.NotEmpty(t, errs) } func TestNewValidator_FindPathWithRegexpCache_ODataPath(t *testing.T) { // OData-style paths have embedded parameters that the radix tree can't handle, // so they fall back to regex matching which DOES populate the cache. spec := `openapi: 3.1.0 paths: /entities('{Entity}')/items: head: operationId: getEntityItems` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodHead, "https://things.com/entities('123')/items", nil) syncMap := sync.Map{} opts := &config.ValidationOptions{RegexCache: &syncMap} _, errs, _ := FindPath(request, &m.Model, opts) keys := []string{} addresses := make(map[string]bool) syncMap.Range(func(key, value any) bool { keys = append(keys, key.(string)) addresses[fmt.Sprintf("%p", value)] = true return true }) // The OData segment should be cached cached, found := syncMap.Load("entities('{Entity}')") assert.True(t, found, "OData path segment should be in regex cache") assert.NotNil(t, cached, "Cached regex should not be nil") assert.True(t, cached.(*regexp.Regexp).MatchString("entities('123')"), "Cached regex should match") assert.Len(t, errs, 0) assert.Len(t, keys, 2, "Should have 2 path segments cached") } // Test cases for path precedence - Issue #181 // According to OpenAPI spec, literal paths take precedence over parameterized paths func TestFindPath_LiteralTakesPrecedenceOverParameter(t *testing.T) { // This is the exact bug case from issue #181 spec := `openapi: 3.1.0 info: title: Path Precedence Bug version: 1.0.0 paths: /Messages/{message_id}: parameters: - name: message_id in: path required: true schema: type: string pattern: '^comms_message_[0-7][a-hjkmnpqrstv-z0-9]{25,34}' get: operationId: getMessage responses: '200': description: OK /Messages/Operations: get: operationId: getOperations summary: List Operations responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // Request to literal path should match literal, not parameter request, _ := http.NewRequest(http.MethodGet, "https://api.com/Messages/Operations", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.Nil(t, errs, "Expected no errors") assert.NotNil(t, pathItem, "Expected pathItem to be found") assert.Equal(t, "getOperations", pathItem.Get.OperationId, "Should match literal path") assert.Equal(t, "/Messages/Operations", foundPath) } func TestFindPath_LiteralPrecedence_ReverseOrder(t *testing.T) { // Same test but with paths defined in opposite order // Result should be the same - literal always wins spec := `openapi: 3.1.0 info: title: Path Precedence Test version: 1.0.0 paths: /Messages/Operations: get: operationId: getOperations responses: '200': description: OK /Messages/{message_id}: get: operationId: getMessage responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodGet, "https://api.com/Messages/Operations", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.Nil(t, errs) assert.NotNil(t, pathItem) assert.Equal(t, "getOperations", pathItem.Get.OperationId) assert.Equal(t, "/Messages/Operations", foundPath) } func TestFindPath_ParameterStillMatchesNonLiteral(t *testing.T) { spec := `openapi: 3.1.0 info: title: Path Precedence Test version: 1.0.0 paths: /Messages/{message_id}: get: operationId: getMessage responses: '200': description: OK /Messages/Operations: get: operationId: getOperations responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // Request to a non-literal value should match parameter path request, _ := http.NewRequest(http.MethodGet, "https://api.com/Messages/12345", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.Nil(t, errs) assert.NotNil(t, pathItem) assert.Equal(t, "getMessage", pathItem.Get.OperationId) assert.Equal(t, "/Messages/{message_id}", foundPath) } func TestFindPath_MultipleParameterLevels(t *testing.T) { spec := `openapi: 3.1.0 info: title: Path Precedence Test version: 1.0.0 paths: /api/{version}/users/{id}: get: operationId: getUserVersioned responses: '200': description: OK /api/v1/users/{id}: get: operationId: getUserV1 responses: '200': description: OK /api/v1/users/me: get: operationId: getCurrentUser responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() tests := []struct { url string expectedOp string expectedPath string }{ // Most specific: all literals {"https://api.com/api/v1/users/me", "getCurrentUser", "/api/v1/users/me"}, // More specific: 3 literals + 1 param {"https://api.com/api/v1/users/123", "getUserV1", "/api/v1/users/{id}"}, // Least specific: 2 literals + 2 params {"https://api.com/api/v2/users/123", "getUserVersioned", "/api/{version}/users/{id}"}, } for _, tt := range tests { t.Run(tt.url, func(t *testing.T) { request, _ := http.NewRequest(http.MethodGet, tt.url, nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.Nil(t, errs) assert.NotNil(t, pathItem) assert.Equal(t, tt.expectedOp, pathItem.Get.OperationId) assert.Equal(t, tt.expectedPath, foundPath) }) } } func TestFindPath_PetsMinePrecedence(t *testing.T) { // Classic example from OpenAPI spec: /pets/mine vs /pets/{petId} spec := `openapi: 3.1.0 info: title: Petstore version: 1.0.0 paths: /pets/{petId}: get: operationId: getPet responses: '200': description: OK /pets/mine: get: operationId: getMyPets responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // /pets/mine should match literal path request, _ := http.NewRequest(http.MethodGet, "https://api.com/pets/mine", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.Nil(t, errs) assert.Equal(t, "getMyPets", pathItem.Get.OperationId) assert.Equal(t, "/pets/mine", foundPath) // /pets/123 should match parameter path request, _ = http.NewRequest(http.MethodGet, "https://api.com/pets/123", nil) pathItem, errs, foundPath = FindPath(request, &m.Model, nil) assert.Nil(t, errs) assert.Equal(t, "getPet", pathItem.Get.OperationId) assert.Equal(t, "/pets/{petId}", foundPath) } func TestFindPath_DeepNestedPrecedence(t *testing.T) { spec := `openapi: 3.1.0 info: title: Nested Paths version: 1.0.0 paths: /api/{version}/resources/{id}/actions/{action}: get: operationId: genericAction responses: '200': description: OK /api/v1/resources/{id}/actions/delete: get: operationId: deleteResource responses: '200': description: OK /api/v1/resources/special/actions/delete: get: operationId: deleteSpecial responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() tests := []struct { url string expectedOp string expectedPath string }{ // All literals - most specific {"https://api.com/api/v1/resources/special/actions/delete", "deleteSpecial", "/api/v1/resources/special/actions/delete"}, // 5 literals + 1 param {"https://api.com/api/v1/resources/123/actions/delete", "deleteResource", "/api/v1/resources/{id}/actions/delete"}, // 3 literals + 3 params - least specific {"https://api.com/api/v2/resources/123/actions/update", "genericAction", "/api/{version}/resources/{id}/actions/{action}"}, } for _, tt := range tests { t.Run(tt.url, func(t *testing.T) { request, _ := http.NewRequest(http.MethodGet, tt.url, nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.Nil(t, errs) assert.NotNil(t, pathItem) assert.Equal(t, tt.expectedOp, pathItem.Get.OperationId) assert.Equal(t, tt.expectedPath, foundPath) }) } } func TestFindPath_MethodMismatchUsesHighestScore(t *testing.T) { // When path matches but method doesn't exist, error should reference // the most specific matching path spec := `openapi: 3.1.0 info: title: Method Mismatch Test version: 1.0.0 paths: /users/{id}: get: operationId: getUser responses: '200': description: OK /users/admin: get: operationId: getAdmin responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // POST to /users/admin - literal path should be chosen for error request, _ := http.NewRequest(http.MethodPost, "https://api.com/users/admin", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.NotNil(t, errs) assert.Len(t, errs, 1) assert.Equal(t, "/users/admin", foundPath) assert.NotNil(t, pathItem) assert.True(t, errs[0].IsOperationMissingError()) } func TestFindPath_WithQueryParams(t *testing.T) { // Ensure query params don't affect path matching precedence spec := `openapi: 3.1.0 info: title: Query Params Test version: 1.0.0 paths: /Messages/{message_id}: get: operationId: getMessage responses: '200': description: OK /Messages/Operations: get: operationId: getOperations parameters: - name: start_date in: query schema: type: string - name: end_date in: query schema: type: string responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // This is the exact request from issue #181 request, _ := http.NewRequest(http.MethodGet, "https://api.com/Messages/Operations?start_date=2020-01-01T00:00:00Z&end_date=2025-12-31T23:59:59Z&page_size=10", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.Nil(t, errs) assert.NotNil(t, pathItem) assert.Equal(t, "getOperations", pathItem.Get.OperationId) assert.Equal(t, "/Messages/Operations", foundPath) } func TestFindPath_WithRegexCache(t *testing.T) { // Ensure precedence works correctly with regex cache spec := `openapi: 3.1.0 info: title: Cache Test version: 1.0.0 paths: /Messages/{message_id}: get: operationId: getMessage responses: '200': description: OK /Messages/Operations: get: operationId: getOperations responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() opts := &config.ValidationOptions{RegexCache: &sync.Map{}} // First request - populates cache request, _ := http.NewRequest(http.MethodGet, "https://api.com/Messages/Operations", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, opts) assert.Nil(t, errs) assert.Equal(t, "getOperations", pathItem.Get.OperationId) assert.Equal(t, "/Messages/Operations", foundPath) // Second request - uses cache request, _ = http.NewRequest(http.MethodGet, "https://api.com/Messages/12345", nil) pathItem, errs, foundPath = FindPath(request, &m.Model, opts) assert.Nil(t, errs) assert.Equal(t, "getMessage", pathItem.Get.OperationId) assert.Equal(t, "/Messages/{message_id}", foundPath) // Third request - still works correctly request, _ = http.NewRequest(http.MethodGet, "https://api.com/Messages/Operations", nil) pathItem, errs, foundPath = FindPath(request, &m.Model, opts) assert.Nil(t, errs) assert.Equal(t, "getOperations", pathItem.Get.OperationId) assert.Equal(t, "/Messages/Operations", foundPath) } func TestFindPath_WithFragment(t *testing.T) { // Test that request paths with fragments are handled correctly spec := `openapi: 3.1.0 info: title: Fragment Test version: 1.0.0 paths: /users/{id}: get: operationId: getUser responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // Request with fragment in URL request, _ := http.NewRequest(http.MethodGet, "https://api.com/users/123#section", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.Nil(t, errs) assert.NotNil(t, pathItem) assert.Equal(t, "getUser", pathItem.Get.OperationId) assert.Equal(t, "/users/{id}", foundPath) } func TestFindPath_WithTrailingSlashBasePath(t *testing.T) { // Test that base paths with trailing slash work correctly spec := `openapi: 3.1.0 info: title: Trailing Slash Test version: 1.0.0 servers: - url: https://api.com/v1/ paths: /users: get: operationId: getUsers responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // Request to path that includes base with trailing slash request, _ := http.NewRequest(http.MethodGet, "https://api.com/v1/users", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.Nil(t, errs) assert.NotNil(t, pathItem) assert.Equal(t, "getUsers", pathItem.Get.OperationId) assert.Equal(t, "/users", foundPath) } func TestFindPath_PathTemplateWithFragment_RequestWithoutFragment(t *testing.T) { // Test that path templates with fragments are normalized when request has no fragment // This covers normalizePathForMatching stripping fragment from template (line 115-117) spec := `openapi: 3.1.0 info: title: Fragment Normalization Test version: 1.0.0 paths: /hashy#section: post: operationId: postHashy responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // Request WITHOUT fragment should still match path template WITH fragment // because normalizePathForMatching strips the fragment from template request, _ := http.NewRequest(http.MethodPost, "https://api.com/hashy", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.Nil(t, errs) assert.NotNil(t, pathItem) assert.Equal(t, "postHashy", pathItem.Post.OperationId) assert.Equal(t, "/hashy#section", foundPath) } func TestFindPath_NilDocument(t *testing.T) { // Passing a nil document is a programming error and will panic. // This test verifies that behavior (callers should not pass nil). request, _ := http.NewRequest(http.MethodGet, "https://api.com/test", nil) assert.Panics(t, func() { FindPath(request, nil, nil) }, "FindPath should panic when document is nil") } func TestFindPath_NilPaths(t *testing.T) { // A spec without paths will have nil Paths - this is a programming error spec := `openapi: 3.1.0 info: title: No Paths Test version: 1.0.0 ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodGet, "https://api.com/test", nil) // This panics because the original code doesn't handle nil Paths either assert.Panics(t, func() { FindPath(request, &m.Model, nil) }, "FindPath should panic when document has no paths") } func TestFindPath_RequestWithFragment(t *testing.T) { // Test when request URL contains a fragment - normalizePathForMatching should NOT strip template fragment spec := `openapi: 3.1.0 info: title: Fragment Test version: 1.0.0 paths: /docs#section: get: operationId: getDocs responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // Request WITH fragment should match path WITH same fragment request, _ := http.NewRequest(http.MethodGet, "https://api.com/docs#section", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.Nil(t, errs) assert.NotNil(t, pathItem) assert.Equal(t, "getDocs", pathItem.Get.OperationId) assert.Equal(t, "/docs#section", foundPath) } func TestFindPath_RadixTree_MethodMismatch(t *testing.T) { // Test that radix tree path match with wrong method returns proper error // This covers lines 72-83 in FindPath (missingOperation from radix tree) spec := `openapi: 3.1.0 info: title: Method Mismatch Test version: 1.0.0 paths: /users/{id}: get: operationId: getUser responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // POST to a simple path that only has GET - radix tree handles this request, _ := http.NewRequest(http.MethodPost, "https://api.com/users/123", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.NotNil(t, errs) assert.Len(t, errs, 1) assert.Equal(t, "missingOperation", errs[0].ValidationSubType) assert.Equal(t, "/users/{id}", foundPath) } func TestFindPath_RequestWithFragment_MatchesPathWithFragment(t *testing.T) { // Test normalizePathForMatching when REQUEST has fragment // This covers lines 167-168: if strings.Contains(requestPath, "#") { return path } // Using OData-style path to force regex fallback (radix tree can't handle embedded params) spec := `openapi: 3.1.0 info: title: Fragment Test version: 1.0.0 paths: /entities('{id}')#section1: get: operationId: getSection1 /entities('{id}')#section2: get: operationId: getSection2 ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // Request with fragment should match exact path with fragment // The OData path forces regex fallback, which calls normalizePathForMatching request, _ := http.NewRequest(http.MethodGet, "https://api.com/entities('123')#section1", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.Nil(t, errs) assert.NotNil(t, pathItem) assert.Equal(t, "getSection1", pathItem.Get.OperationId) assert.Equal(t, "/entities('{id}')#section1", foundPath) // Different fragment should match different path request, _ = http.NewRequest(http.MethodGet, "https://api.com/entities('456')#section2", nil) pathItem, errs, foundPath = FindPath(request, &m.Model, nil) assert.Nil(t, errs) assert.NotNil(t, pathItem) assert.Equal(t, "getSection2", pathItem.Get.OperationId) assert.Equal(t, "/entities('{id}')#section2", foundPath) } func TestCheckPathAgainstBase_MergedPath(t *testing.T) { // Test checkPathAgainstBase when docPath == merged (basePath + urlPath) // This covers line 225-227 // Direct equality result := checkPathAgainstBase("/users", "/users", nil) assert.True(t, result) // With base path merge basePaths := []string{"/api/v1"} result = checkPathAgainstBase("/api/v1/users", "/users", basePaths) assert.True(t, result) // With trailing slash on base path basePaths = []string{"/api/v1/"} result = checkPathAgainstBase("/api/v1/users", "/users", basePaths) assert.True(t, result) // No match result = checkPathAgainstBase("/other/path", "/users", basePaths) assert.False(t, result) // Issue #27: basePath is just "/" — should not produce empty string basePaths = []string{"/"} result = checkPathAgainstBase("/users", "/users", basePaths) assert.True(t, result) } func TestFindPath_RegexFallback_MethodMismatch(t *testing.T) { // Test missingOperation error from regex fallback path (lines 150-161) // Using OData-style path to force regex fallback, with wrong method spec := `openapi: 3.1.0 info: title: Method Mismatch Test version: 1.0.0 paths: /entities('{id}'): get: operationId: getEntity responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // POST to OData path that only has GET - regex fallback handles this request, _ := http.NewRequest(http.MethodPost, "https://api.com/entities('123')", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, nil) assert.NotNil(t, pathItem) assert.NotNil(t, errs) assert.Len(t, errs, 1) assert.Equal(t, "missingOperation", errs[0].ValidationSubType) assert.Equal(t, "/entities('{id}')", foundPath) } func TestFindPath_WithPathTree_MethodMatch(t *testing.T) { // Exercise radix tree fast-path: options.PathTree != nil, method matches → return pathItem, nil, matchedPath spec := `openapi: 3.1.0 info: title: Radix Tree Test version: 1.0.0 paths: /users/{id}: get: operationId: getUser responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() pathTree := radix.BuildPathTree(&m.Model) opts := config.NewValidationOptions(config.WithPathTree(pathTree)) request, _ := http.NewRequest(http.MethodGet, "https://api.com/users/123", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, opts) assert.Nil(t, errs) assert.NotNil(t, pathItem) assert.Equal(t, "getUser", pathItem.Get.OperationId) assert.Equal(t, "/users/{id}", foundPath) } func TestFindPath_WithPathTree_MethodMismatch(t *testing.T) { // Exercise radix tree fast-path: options.PathTree != nil, path found but wrong method // → return pathItem, missingOperationError(...), matchedPath spec := `openapi: 3.1.0 info: title: Radix Tree Test version: 1.0.0 paths: /users/{id}: get: operationId: getUser responses: '200': description: OK ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() pathTree := radix.BuildPathTree(&m.Model) opts := config.NewValidationOptions(config.WithPathTree(pathTree)) request, _ := http.NewRequest(http.MethodPost, "https://api.com/users/123", nil) pathItem, errs, foundPath := FindPath(request, &m.Model, opts) assert.NotNil(t, pathItem) assert.NotNil(t, errs) assert.Len(t, errs, 1) assert.True(t, errs[0].IsOperationMissingError()) assert.Equal(t, "/users/{id}", foundPath) } libopenapi-validator-0.13.8/paths/specificity.go000066400000000000000000000052501520534042400216720ustar00rootroot00000000000000// Copyright 2023-2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package paths import ( "net/http" "strings" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" ) // pathCandidate represents a potential path match with metadata for selection. type pathCandidate struct { pathItem *v3.PathItem path string score int hasMethod bool } // computeSpecificityScore calculates how specific a path template is. // literal segments score higher than parameterized segments, ensuring // "/pets/mine" is preferred over "/pets/{id}" per OpenAPI spec. // // scoring: // - literal segment: 1000 points // - parameter segment: 1 point // // this weighting ensures any path with more literal segments always wins, // regardless of parameter positions. func computeSpecificityScore(pathTemplate string) int { segments := strings.Split(pathTemplate, "/") score := 0 for _, seg := range segments { if seg == "" { continue } if isParameterSegment(seg) { score += 1 } else { score += 1000 } } return score } // isParameterSegment returns true if the segment contains a path parameter. // handles standard {param}, label {.param}, and exploded {param*} formats. func isParameterSegment(seg string) bool { return strings.Contains(seg, "{") && strings.Contains(seg, "}") } // pathHasMethod checks if the PathItem has an operation for the given HTTP method. func pathHasMethod(pathItem *v3.PathItem, method string) bool { switch method { case http.MethodGet: return pathItem.Get != nil case http.MethodPost: return pathItem.Post != nil case http.MethodPut: return pathItem.Put != nil case http.MethodDelete: return pathItem.Delete != nil case http.MethodOptions: return pathItem.Options != nil case http.MethodHead: // Treat HEAD as present when either // a Head operation exists or, if Head is absent, when a Get exists // per HTTP semantics (HEAD can be handled by GET if no explicit // HEAD operation is defined). return pathItem.Head != nil || pathItem.Get != nil case http.MethodPatch: return pathItem.Patch != nil case http.MethodTrace: return pathItem.Trace != nil } return false } // selectMatches finds the best matching candidates in a single pass. // returns the highest-scoring candidate with the method (or nil), and // the highest-scoring candidate overall (for error reporting). func selectMatches(candidates []pathCandidate) (withMethod, highest *pathCandidate) { for i := range candidates { c := &candidates[i] if c.hasMethod && (withMethod == nil || c.score > withMethod.score) { withMethod = c } if highest == nil || c.score > highest.score { highest = c } } return withMethod, highest } libopenapi-validator-0.13.8/paths/specificity_test.go000066400000000000000000000164161520534042400227370ustar00rootroot00000000000000// Copyright 2023-2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package paths import ( "testing" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/stretchr/testify/assert" ) func TestComputeSpecificityScore(t *testing.T) { tests := []struct { name string path string expected int }{ { name: "single literal segment", path: "/pets", expected: 1000, }, { name: "single parameter segment", path: "/{id}", expected: 1, }, { name: "literal then parameter", path: "/pets/{id}", expected: 1001, }, { name: "two literal segments", path: "/pets/mine", expected: 2000, }, { name: "two parameter segments", path: "/{tenant}/{id}", expected: 2, }, { name: "mixed - param literal param", path: "/{tenant}/users/{id}", expected: 1002, }, { name: "three literal segments", path: "/api/v1/users", expected: 3000, }, { name: "two literals one param", path: "/api/v1/{resource}", expected: 2001, }, { name: "four literals", path: "/api/v1/users/profile", expected: 4000, }, { name: "label parameter format", path: "/burgers/{.burgerId}/locate", expected: 2001, }, { name: "exploded parameter format", path: "/burgers/{burgerId*}/locate", expected: 2001, }, { name: "empty path", path: "/", expected: 0, }, { name: "OData style path", path: "/entities('{Entity}')", expected: 1, }, { name: "complex OData path", path: "/orders(RelationshipNumber='{RelationshipNumber}',ValidityEndDate=datetime'{ValidityEndDate}')", expected: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { score := computeSpecificityScore(tt.path) assert.Equal(t, tt.expected, score, "path: %s", tt.path) }) } } func TestIsParameterSegment(t *testing.T) { tests := []struct { segment string expected bool }{ {"users", false}, {"{id}", true}, {"{.id}", true}, {"{id*}", true}, {"mine", false}, {"", false}, {"v1", false}, {"{petId}", true}, {"{message_id}", true}, {"Operations", false}, {"entities('{Entity}')", true}, {"literal", false}, } for _, tt := range tests { t.Run(tt.segment, func(t *testing.T) { result := isParameterSegment(tt.segment) assert.Equal(t, tt.expected, result, "segment: %s", tt.segment) }) } } func TestPathHasMethod(t *testing.T) { tests := []struct { name string pathItem *v3.PathItem method string expected bool }{ { name: "GET exists", pathItem: &v3.PathItem{Get: &v3.Operation{}}, method: "GET", expected: true, }, { name: "GET missing", pathItem: &v3.PathItem{Post: &v3.Operation{}}, method: "GET", expected: false, }, { name: "POST exists", pathItem: &v3.PathItem{Post: &v3.Operation{}}, method: "POST", expected: true, }, { name: "PUT exists", pathItem: &v3.PathItem{Put: &v3.Operation{}}, method: "PUT", expected: true, }, { name: "DELETE exists", pathItem: &v3.PathItem{Delete: &v3.Operation{}}, method: "DELETE", expected: true, }, { name: "OPTIONS exists", pathItem: &v3.PathItem{Options: &v3.Operation{}}, method: "OPTIONS", expected: true, }, { name: "HEAD exists", pathItem: &v3.PathItem{Head: &v3.Operation{}}, method: "HEAD", expected: true, }, { name: "HEAD if GET exists", pathItem: &v3.PathItem{Get: &v3.Operation{}}, method: "HEAD", expected: true, }, { name: "PATCH exists", pathItem: &v3.PathItem{Patch: &v3.Operation{}}, method: "PATCH", expected: true, }, { name: "TRACE exists", pathItem: &v3.PathItem{Trace: &v3.Operation{}}, method: "TRACE", expected: true, }, { name: "unknown method", pathItem: &v3.PathItem{Get: &v3.Operation{}}, method: "UNKNOWN", expected: false, }, { name: "empty pathItem", pathItem: &v3.PathItem{}, method: "GET", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := pathHasMethod(tt.pathItem, tt.method) assert.Equal(t, tt.expected, result) }) } } func TestSelectMatches(t *testing.T) { tests := []struct { name string candidates []pathCandidate expectedWithMethod string // expected path for withMethod, or empty if nil expectedHighest string // expected path for highest, or empty if nil }{ { name: "single candidate with method", candidates: []pathCandidate{ {path: "/pets/{id}", score: 1001, hasMethod: true}, }, expectedWithMethod: "/pets/{id}", expectedHighest: "/pets/{id}", }, { name: "single candidate without method", candidates: []pathCandidate{ {path: "/pets/{id}", score: 1001, hasMethod: false}, }, expectedWithMethod: "", expectedHighest: "/pets/{id}", }, { name: "higher score wins", candidates: []pathCandidate{ {path: "/pets/{id}", score: 1001, hasMethod: true}, {path: "/pets/mine", score: 2000, hasMethod: true}, }, expectedWithMethod: "/pets/mine", expectedHighest: "/pets/mine", }, { name: "higher score wins - reverse order", candidates: []pathCandidate{ {path: "/pets/mine", score: 2000, hasMethod: true}, {path: "/pets/{id}", score: 1001, hasMethod: true}, }, expectedWithMethod: "/pets/mine", expectedHighest: "/pets/mine", }, { name: "higher score without method is skipped for withMethod", candidates: []pathCandidate{ {path: "/pets/{id}", score: 1001, hasMethod: true}, {path: "/pets/mine", score: 2000, hasMethod: false}, }, expectedWithMethod: "/pets/{id}", expectedHighest: "/pets/mine", }, { name: "equal scores - first wins", candidates: []pathCandidate{ {path: "/pets/{petId}", score: 1001, hasMethod: true}, {path: "/pets/{petName}", score: 1001, hasMethod: true}, }, expectedWithMethod: "/pets/{petId}", expectedHighest: "/pets/{petId}", }, { name: "empty candidates", candidates: []pathCandidate{}, expectedWithMethod: "", expectedHighest: "", }, { name: "all candidates without method", candidates: []pathCandidate{ {path: "/pets/{id}", score: 1001, hasMethod: false}, {path: "/pets/mine", score: 2000, hasMethod: false}, }, expectedWithMethod: "", expectedHighest: "/pets/mine", }, { name: "three candidates mixed", candidates: []pathCandidate{ {path: "/{tenant}/users/{id}", score: 1002, hasMethod: true}, {path: "/api/users/{id}", score: 2001, hasMethod: true}, {path: "/api/users/me", score: 3000, hasMethod: true}, }, expectedWithMethod: "/api/users/me", expectedHighest: "/api/users/me", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { withMethod, highest := selectMatches(tt.candidates) if tt.expectedWithMethod == "" { assert.Nil(t, withMethod) } else { assert.NotNil(t, withMethod) assert.Equal(t, tt.expectedWithMethod, withMethod.path) } if tt.expectedHighest == "" { assert.Nil(t, highest) } else { assert.NotNil(t, highest) assert.Equal(t, tt.expectedHighest, highest.path) } }) } } libopenapi-validator-0.13.8/radix/000077500000000000000000000000001520534042400170165ustar00rootroot00000000000000libopenapi-validator-0.13.8/radix/path_tree.go000066400000000000000000000051411520534042400213210ustar00rootroot00000000000000// Copyright 2026 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package radix import ( v3 "github.com/pb33f/libopenapi/datamodel/high/v3" ) // PathLookup defines the interface for radix tree path matching implementations. // The default implementation provides O(k) lookup where k is the path segment count. // // Note: This interface handles URL path matching only. HTTP method validation // is performed separately after the PathItem is retrieved, since a single path // (e.g., "/users/{id}") can support multiple HTTP methods (GET, POST, PUT, DELETE). type PathLookup interface { // Lookup finds the PathItem for a given URL path. // Returns the matched PathItem, the path template (e.g., "/users/{id}"), and whether found. Lookup(urlPath string) (pathItem *v3.PathItem, matchedPath string, found bool) } // PathTree is a radix tree optimized for OpenAPI path matching. // It provides O(k) lookup where k is the number of path segments (typically 3-5), // with minimal allocations during lookup. // // This is a thin wrapper around the generic Tree, specialized for // OpenAPI PathItem values. It implements the PathLookup interface. type PathTree struct { tree *Tree[*v3.PathItem] } // Ensure PathTree implements PathLookup at compile time. var _ PathLookup = (*PathTree)(nil) // NewPathTree creates a new empty radix tree for path matching. func NewPathTree() *PathTree { return &PathTree{ tree: New[*v3.PathItem](), } } // Insert adds a path and its PathItem to the tree. // Path should be in OpenAPI format, e.g., "/users/{id}/posts" func (t *PathTree) Insert(path string, pathItem *v3.PathItem) { t.tree.Insert(path, pathItem) } // Lookup finds the PathItem for a given request path. // Returns the PathItem, the matched path template, and whether a match was found. func (t *PathTree) Lookup(urlPath string) (*v3.PathItem, string, bool) { return t.tree.Lookup(urlPath) } // Size returns the number of paths stored in the tree. func (t *PathTree) Size() int { return t.tree.Size() } // Walk calls the given function for each path in the tree. func (t *PathTree) Walk(fn func(path string, pathItem *v3.PathItem) bool) { t.tree.Walk(fn) } // BuildPathTree creates a PathTree from an OpenAPI document. // This should be called once during validator initialization. func BuildPathTree(doc *v3.Document) *PathTree { tree := NewPathTree() if doc == nil || doc.Paths == nil || doc.Paths.PathItems == nil { return tree } for pair := doc.Paths.PathItems.First(); pair != nil; pair = pair.Next() { path := pair.Key() pathItem := pair.Value() tree.Insert(path, pathItem) } return tree } libopenapi-validator-0.13.8/radix/path_tree_test.go000066400000000000000000000115621520534042400223640ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package radix import ( "testing" "github.com/pb33f/libopenapi" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewPathTree(t *testing.T) { tree := NewPathTree() require.NotNil(t, tree) assert.Equal(t, 0, tree.Size()) } func TestPathTree_ImplementsPathLookup(t *testing.T) { // Compile-time check that PathTree implements PathLookup var _ PathLookup = (*PathTree)(nil) } func TestPathTree_Insert_Lookup(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test API version: 1.0.0 paths: /users: get: responses: '200': description: OK ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) model, errs := doc.BuildV3Model() require.Empty(t, errs) pair := model.Model.Paths.PathItems.First() require.NotNil(t, pair) tree := NewPathTree() tree.Insert("/users", pair.Value()) pathItem, path, found := tree.Lookup("/users") assert.True(t, found) assert.Equal(t, "/users", path) assert.NotNil(t, pathItem) assert.NotNil(t, pathItem.Get) } func TestPathTree_Walk(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test API version: 1.0.0 paths: /users: get: responses: '200': description: OK /posts: get: responses: '200': description: OK ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) model, errs := doc.BuildV3Model() require.Empty(t, errs) tree := BuildPathTree(&model.Model) assert.Equal(t, 2, tree.Size()) var paths []string tree.Walk(func(path string, pathItem *v3.PathItem) bool { paths = append(paths, path) assert.NotNil(t, pathItem) return true }) assert.Len(t, paths, 2) } func TestBuildPathTree(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test API version: 1.0.0 paths: /users: get: responses: '200': description: OK /users/{id}: get: responses: '200': description: OK /posts: post: responses: '201': description: Created ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) model, errs := doc.BuildV3Model() require.Empty(t, errs) tree := BuildPathTree(&model.Model) assert.Equal(t, 3, tree.Size()) // Test lookups pathItem, path, found := tree.Lookup("/users") assert.True(t, found) assert.Equal(t, "/users", path) assert.NotNil(t, pathItem.Get) pathItem, path, found = tree.Lookup("/users/123") assert.True(t, found) assert.Equal(t, "/users/{id}", path) assert.NotNil(t, pathItem.Get) pathItem, path, found = tree.Lookup("/posts") assert.True(t, found) assert.Equal(t, "/posts", path) assert.NotNil(t, pathItem.Post) } func TestBuildPathTree_NilDocument(t *testing.T) { tree := BuildPathTree(nil) require.NotNil(t, tree) assert.Equal(t, 0, tree.Size()) } func TestBuildPathTree_NilPaths(t *testing.T) { doc := &v3.Document{} tree := BuildPathTree(doc) require.NotNil(t, tree) assert.Equal(t, 0, tree.Size()) } func TestPathTree_LiteralOverParam(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test API version: 1.0.0 paths: /users/{id}: get: operationId: getUserById responses: '200': description: OK /users/admin: get: operationId: getAdmin responses: '200': description: OK ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) model, errs := doc.BuildV3Model() require.Empty(t, errs) tree := BuildPathTree(&model.Model) // Literal should win pathItem, path, found := tree.Lookup("/users/admin") assert.True(t, found) assert.Equal(t, "/users/admin", path) assert.Equal(t, "getAdmin", pathItem.Get.OperationId) // Param should match other values pathItem, path, found = tree.Lookup("/users/123") assert.True(t, found) assert.Equal(t, "/users/{id}", path) assert.Equal(t, "getUserById", pathItem.Get.OperationId) } // Benchmark func BenchmarkPathTree_Lookup(b *testing.B) { spec := `openapi: 3.1.0 info: title: Test API version: 1.0.0 paths: /api/v3/ad_accounts: get: responses: '200': description: OK /api/v3/ad_accounts/{id}: get: responses: '200': description: OK /api/v3/ad_accounts/{id}/campaigns: get: responses: '200': description: OK /api/v3/ad_accounts/{id}/campaigns/{campaign_id}: get: responses: '200': description: OK ` doc, err := libopenapi.NewDocument([]byte(spec)) if err != nil { b.Fatal(err) } model, modelErr := doc.BuildV3Model() if modelErr != nil { b.Fatal(modelErr) } tree := BuildPathTree(&model.Model) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { tree.Lookup("/api/v3/ad_accounts/acc123/campaigns/camp456") } } libopenapi-validator-0.13.8/radix/tree.go000066400000000000000000000134421520534042400203100ustar00rootroot00000000000000// Copyright 2026 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT // Package radix provides a radix tree (prefix tree) implementation optimized for // URL path matching with support for parameterized segments. // // The tree provides O(k) lookup complexity where k is the number of path segments // (typically 3-5 for REST APIs), making it ideal for routing and path matching. // // Example usage: // // tree := radix.New[*MyHandler]() // tree.Insert("/users/{id}", handler1) // tree.Insert("/users/{id}/posts", handler2) // // handler, path, found := tree.Lookup("/users/123/posts") // // handler = handler2, path = "/users/{id}/posts", found = true package radix import "strings" // Tree is a radix tree optimized for URL path matching. // It supports both literal path segments and parameterized segments like {id}. // T is the type of value stored at leaf nodes. type Tree[T any] struct { root *node[T] size int } // node represents a node in the radix tree. type node[T any] struct { // children maps literal path segments to child nodes children map[string]*node[T] // paramChild handles parameterized segments like {id} // Only one param child is allowed per node paramChild *node[T] // paramName stores the parameter name without braces (e.g., "id" from "{id}") paramName string // leaf contains the stored value and path template for endpoints leaf *leafData[T] } // leafData stores the value and original path template for a leaf node. type leafData[T any] struct { value T path string } // New creates a new empty radix tree. func New[T any]() *Tree[T] { return &Tree[T]{ root: &node[T]{ children: make(map[string]*node[T]), }, } } // Insert adds a path and its associated value to the tree. // The path should use {param} syntax for parameterized segments. // Examples: "/users", "/users/{id}", "/users/{userId}/posts/{postId}" // // Returns true if a new path was inserted, false if an existing path was updated. func (t *Tree[T]) Insert(path string, value T) bool { if t.root == nil { t.root = &node[T]{children: make(map[string]*node[T])} } segments := splitPath(path) n := t.root isNew := true for _, seg := range segments { if isParam(seg) { // Parameter segment if n.paramChild == nil { n.paramChild = &node[T]{ children: make(map[string]*node[T]), paramName: extractParamName(seg), } } n = n.paramChild } else { // Literal segment child, exists := n.children[seg] if !exists { child = &node[T]{children: make(map[string]*node[T])} n.children[seg] = child } n = child } } // Check if this is a new path or an update if n.leaf != nil { isNew = false } else { t.size++ } // Set the leaf data n.leaf = &leafData[T]{ value: value, path: path, } return isNew } // Lookup finds the value for a given URL path. // Returns the value, the matched path template, and whether a match was found. // // Literal matches take precedence over parameter matches per OpenAPI specification. // For example, "/users/admin" will match "/users/admin" before "/users/{id}". func (t *Tree[T]) Lookup(urlPath string) (value T, matchedPath string, found bool) { var zero T if t.root == nil { return zero, "", false } segments := splitPath(urlPath) leaf := t.lookupRecursive(t.root, segments, 0) if leaf != nil { return leaf.value, leaf.path, true } return zero, "", false } // lookupRecursive performs the tree traversal. // It prioritizes literal matches over parameter matches. func (t *Tree[T]) lookupRecursive(n *node[T], segments []string, depth int) *leafData[T] { // Base case: consumed all segments if depth == len(segments) { return n.leaf } seg := segments[depth] // Try literal match first (higher specificity) if child, exists := n.children[seg]; exists { if result := t.lookupRecursive(child, segments, depth+1); result != nil { return result } } // Fall back to parameter match if n.paramChild != nil { if result := t.lookupRecursive(n.paramChild, segments, depth+1); result != nil { return result } } return nil } // Size returns the number of paths stored in the tree. func (t *Tree[T]) Size() int { return t.size } // Clear removes all entries from the tree. func (t *Tree[T]) Clear() { t.root = &node[T]{children: make(map[string]*node[T])} t.size = 0 } // Walk calls the given function for each path in the tree. // The function receives the path template and its associated value. // If the function returns false, iteration stops. func (t *Tree[T]) Walk(fn func(path string, value T) bool) { if t.root == nil { return } t.walkRecursive(t.root, fn) } func (t *Tree[T]) walkRecursive(n *node[T], fn func(path string, value T) bool) bool { if n.leaf != nil { if !fn(n.leaf.path, n.leaf.value) { return false } } for _, child := range n.children { if !t.walkRecursive(child, fn) { return false } } if n.paramChild != nil { if !t.walkRecursive(n.paramChild, fn) { return false } } return true } // splitPath splits a path into segments, removing empty segments. // "/users/{id}/posts" -> ["users", "{id}", "posts"] func splitPath(path string) []string { path = strings.Trim(path, "/") if path == "" { return nil } parts := strings.Split(path, "/") // Filter out empty segments (from double slashes, etc.) result := make([]string, 0, len(parts)) for _, p := range parts { if p != "" { result = append(result, p) } } return result } // isParam checks if a segment is a parameter (e.g., "{id}") func isParam(seg string) bool { return len(seg) > 2 && seg[0] == '{' && seg[len(seg)-1] == '}' } // extractParamName extracts the parameter name from a segment. // "{id}" -> "id", "{userId}" -> "userId" func extractParamName(seg string) string { if len(seg) > 2 && seg[0] == '{' && seg[len(seg)-1] == '}' { return seg[1 : len(seg)-1] } return seg } libopenapi-validator-0.13.8/radix/tree_test.go000066400000000000000000000530221520534042400213450ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package radix import ( "fmt" "sort" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNew(t *testing.T) { tree := New[string]() require.NotNil(t, tree) assert.NotNil(t, tree.root) assert.Equal(t, 0, tree.Size()) } func TestTree_Insert_LiteralPaths(t *testing.T) { tree := New[string]() // Insert literal paths assert.True(t, tree.Insert("/users", "users handler")) assert.True(t, tree.Insert("/users/admin", "admin handler")) assert.True(t, tree.Insert("/posts", "posts handler")) assert.True(t, tree.Insert("/posts/trending", "trending handler")) assert.Equal(t, 4, tree.Size()) // Verify lookups val, path, found := tree.Lookup("/users") assert.True(t, found) assert.Equal(t, "users handler", val) assert.Equal(t, "/users", path) val, path, found = tree.Lookup("/users/admin") assert.True(t, found) assert.Equal(t, "admin handler", val) assert.Equal(t, "/users/admin", path) } func TestTree_Insert_ParameterizedPaths(t *testing.T) { tree := New[string]() tree.Insert("/users/{id}", "user by id") tree.Insert("/users/{id}/posts", "user posts") tree.Insert("/users/{id}/posts/{postId}", "single post") assert.Equal(t, 3, tree.Size()) // Verify parameter matching val, path, found := tree.Lookup("/users/123") assert.True(t, found) assert.Equal(t, "user by id", val) assert.Equal(t, "/users/{id}", path) val, path, found = tree.Lookup("/users/abc") assert.True(t, found) assert.Equal(t, "user by id", val) assert.Equal(t, "/users/{id}", path) val, path, found = tree.Lookup("/users/123/posts") assert.True(t, found) assert.Equal(t, "user posts", val) assert.Equal(t, "/users/{id}/posts", path) val, path, found = tree.Lookup("/users/123/posts/456") assert.True(t, found) assert.Equal(t, "single post", val) assert.Equal(t, "/users/{id}/posts/{postId}", path) } func TestTree_Specificity_LiteralOverParam(t *testing.T) { tree := New[string]() // Insert both literal and parameterized for same depth tree.Insert("/users/{id}", "user by id") tree.Insert("/users/admin", "admin user") tree.Insert("/users/me", "current user") // Literal matches should take precedence val, path, found := tree.Lookup("/users/admin") assert.True(t, found) assert.Equal(t, "admin user", val) assert.Equal(t, "/users/admin", path) val, path, found = tree.Lookup("/users/me") assert.True(t, found) assert.Equal(t, "current user", val) assert.Equal(t, "/users/me", path) // Non-literal should fall back to param val, path, found = tree.Lookup("/users/123") assert.True(t, found) assert.Equal(t, "user by id", val) assert.Equal(t, "/users/{id}", path) } func TestTree_Specificity_DeepPaths(t *testing.T) { tree := New[string]() // Deeper literal path should match over param tree.Insert("/api/{version}/users", "versioned users") tree.Insert("/api/v1/users", "v1 users") tree.Insert("/api/v2/users", "v2 users") tree.Insert("/api/v1/users/{id}", "v1 user by id") val, path, found := tree.Lookup("/api/v1/users") assert.True(t, found) assert.Equal(t, "v1 users", val) assert.Equal(t, "/api/v1/users", path) val, path, found = tree.Lookup("/api/v2/users") assert.True(t, found) assert.Equal(t, "v2 users", val) assert.Equal(t, "/api/v2/users", path) val, path, found = tree.Lookup("/api/v3/users") assert.True(t, found) assert.Equal(t, "versioned users", val) assert.Equal(t, "/api/{version}/users", path) val, path, found = tree.Lookup("/api/v1/users/123") assert.True(t, found) assert.Equal(t, "v1 user by id", val) assert.Equal(t, "/api/v1/users/{id}", path) } func TestTree_Lookup_NoMatch(t *testing.T) { tree := New[string]() tree.Insert("/users", "users") tree.Insert("/users/{id}", "user by id") // Path doesn't exist _, _, found := tree.Lookup("/posts") assert.False(t, found) // Path too deep _, _, found = tree.Lookup("/users/123/posts/456/comments") assert.False(t, found) // Empty tree lookup emptyTree := New[string]() _, _, found = emptyTree.Lookup("/anything") assert.False(t, found) } func TestTree_Lookup_EdgeCases(t *testing.T) { tree := New[string]() tree.Insert("/", "root") tree.Insert("/users", "users") // Root path val, path, found := tree.Lookup("/") assert.True(t, found) assert.Equal(t, "root", val) assert.Equal(t, "/", path) // Empty path treated as root val, path, found = tree.Lookup("") assert.True(t, found) assert.Equal(t, "root", val) assert.Equal(t, "/", path) // Trailing slash normalization val, path, found = tree.Lookup("/users/") assert.True(t, found) assert.Equal(t, "users", val) assert.Equal(t, "/users", path) // Double slashes val, path, found = tree.Lookup("//users//") assert.True(t, found) assert.Equal(t, "users", val) assert.Equal(t, "/users", path) } func TestTree_Insert_Update(t *testing.T) { tree := New[string]() // First insert isNew := tree.Insert("/users", "v1") assert.True(t, isNew) assert.Equal(t, 1, tree.Size()) // Update existing path isNew = tree.Insert("/users", "v2") assert.False(t, isNew) assert.Equal(t, 1, tree.Size()) // Verify updated value val, _, _ := tree.Lookup("/users") assert.Equal(t, "v2", val) } func TestTree_MultipleParameters(t *testing.T) { tree := New[string]() tree.Insert("/orgs/{orgId}/teams/{teamId}/members/{memberId}", "org team member") tree.Insert("/accounts/{accountId}/ads/{adId}/metrics/{metricId}/breakdown/{breakdownId}", "deep nested") val, path, found := tree.Lookup("/orgs/org1/teams/team2/members/member3") assert.True(t, found) assert.Equal(t, "org team member", val) assert.Equal(t, "/orgs/{orgId}/teams/{teamId}/members/{memberId}", path) val, path, found = tree.Lookup("/accounts/acc1/ads/ad2/metrics/met3/breakdown/bd4") assert.True(t, found) assert.Equal(t, "deep nested", val) assert.Equal(t, "/accounts/{accountId}/ads/{adId}/metrics/{metricId}/breakdown/{breakdownId}", path) } func TestTree_Clear(t *testing.T) { tree := New[string]() tree.Insert("/users", "users") tree.Insert("/posts", "posts") assert.Equal(t, 2, tree.Size()) tree.Clear() assert.Equal(t, 0, tree.Size()) _, _, found := tree.Lookup("/users") assert.False(t, found) } func TestTree_Walk(t *testing.T) { tree := New[string]() tree.Insert("/users", "users") tree.Insert("/users/{id}", "user by id") tree.Insert("/posts", "posts") var paths []string tree.Walk(func(path string, value string) bool { paths = append(paths, path) return true }) assert.Len(t, paths, 3) sort.Strings(paths) assert.Contains(t, paths, "/posts") assert.Contains(t, paths, "/users") assert.Contains(t, paths, "/users/{id}") } func TestTree_Walk_EarlyStop(t *testing.T) { tree := New[string]() for i := 0; i < 10; i++ { tree.Insert(fmt.Sprintf("/path%d", i), fmt.Sprintf("handler%d", i)) } count := 0 tree.Walk(func(path string, value string) bool { count++ return count < 3 // Stop after 3 }) assert.Equal(t, 3, count) } func TestTree_Size(t *testing.T) { tree := New[string]() assert.Equal(t, 0, tree.Size()) tree.Insert("/a", "a") assert.Equal(t, 1, tree.Size()) tree.Insert("/b", "b") assert.Equal(t, 2, tree.Size()) // Update shouldn't increase size tree.Insert("/a", "a2") assert.Equal(t, 2, tree.Size()) tree.Clear() assert.Equal(t, 0, tree.Size()) } // OpenAPI-specific test cases func TestTree_OpenAPIStylePaths(t *testing.T) { tree := New[string]() // Common OpenAPI-style paths paths := []string{ "/api/v3/ad_accounts", "/api/v3/ad_accounts/{ad_account_id}", "/api/v3/ad_accounts/{ad_account_id}/ads", "/api/v3/ad_accounts/{ad_account_id}/ads/{ad_id}", "/api/v3/ad_accounts/{ad_account_id}/campaigns", "/api/v3/ad_accounts/{ad_account_id}/campaigns/{campaign_id}", "/api/v3/ad_accounts/{ad_account_id}/bulk_actions", "/api/v3/ad_accounts/{ad_account_id}/bulk_actions/{bulk_action_id}", } for _, p := range paths { tree.Insert(p, "handler:"+p) } assert.Equal(t, len(paths), tree.Size()) // Test various lookups tests := []struct { input string expected string }{ {"/api/v3/ad_accounts", "/api/v3/ad_accounts"}, {"/api/v3/ad_accounts/123", "/api/v3/ad_accounts/{ad_account_id}"}, {"/api/v3/ad_accounts/abc-def-ghi", "/api/v3/ad_accounts/{ad_account_id}"}, {"/api/v3/ad_accounts/123/ads", "/api/v3/ad_accounts/{ad_account_id}/ads"}, {"/api/v3/ad_accounts/123/ads/456", "/api/v3/ad_accounts/{ad_account_id}/ads/{ad_id}"}, {"/api/v3/ad_accounts/acc1/campaigns/camp1", "/api/v3/ad_accounts/{ad_account_id}/campaigns/{campaign_id}"}, {"/api/v3/ad_accounts/acc1/bulk_actions", "/api/v3/ad_accounts/{ad_account_id}/bulk_actions"}, {"/api/v3/ad_accounts/acc1/bulk_actions/ba1", "/api/v3/ad_accounts/{ad_account_id}/bulk_actions/{bulk_action_id}"}, } for _, tc := range tests { t.Run(tc.input, func(t *testing.T) { val, path, found := tree.Lookup(tc.input) require.True(t, found, "path should be found: %s", tc.input) assert.Equal(t, tc.expected, path) assert.Equal(t, "handler:"+tc.expected, val) }) } } func TestTree_ConsistentWithVaryingIDs(t *testing.T) { // This test verifies that the radix tree performs consistently // regardless of the specific parameter values used tree := New[string]() tree.Insert("/api/v3/ad_accounts/{ad_account_id}/bulk_actions", "bulk_actions") // All of these should match the same path template testCases := []string{ "/api/v3/ad_accounts/1/bulk_actions", "/api/v3/ad_accounts/999999/bulk_actions", "/api/v3/ad_accounts/uuid-here/bulk_actions", "/api/v3/ad_accounts/acc_123abc/bulk_actions", } for _, tc := range testCases { val, path, found := tree.Lookup(tc) require.True(t, found, "should find path for %s", tc) assert.Equal(t, "/api/v3/ad_accounts/{ad_account_id}/bulk_actions", path) assert.Equal(t, "bulk_actions", val) } } func TestTree_NilRoot(t *testing.T) { // Test that a tree with nil root handles gracefully tree := &Tree[string]{root: nil} _, _, found := tree.Lookup("/anything") assert.False(t, found) // Insert should work even with nil root tree.Insert("/users", "users") val, _, found := tree.Lookup("/users") assert.True(t, found) assert.Equal(t, "users", val) } func TestTree_ComplexParamNames(t *testing.T) { tree := New[string]() // Various parameter naming styles tree.Insert("/users/{user_id}", "underscore") tree.Insert("/posts/{postId}", "camelCase") tree.Insert("/items/{item-id}", "kebab-case") tree.Insert("/things/{THING_ID}", "screaming") tests := []struct { input string expected string }{ {"/users/123", "/users/{user_id}"}, {"/posts/abc", "/posts/{postId}"}, {"/items/xyz", "/items/{item-id}"}, {"/things/T1", "/things/{THING_ID}"}, } for _, tc := range tests { _, path, found := tree.Lookup(tc.input) assert.True(t, found) assert.Equal(t, tc.expected, path) } } // Additional edge case tests for full coverage func TestTree_Walk_NilRoot(t *testing.T) { // Verify Walk handles nil root gracefully tree := &Tree[string]{root: nil} count := 0 tree.Walk(func(path string, value string) bool { count++ return true }) assert.Equal(t, 0, count, "Walk on nil root should not call callback") } func TestTree_Walk_EarlyStopOnParamChild(t *testing.T) { // Test that Walk respects early stop when iterating paramChild tree := New[string]() // Create a structure where we have literal children AND a param child tree.Insert("/users/admin", "admin") tree.Insert("/users/{id}", "user by id") tree.Insert("/users/{id}/posts", "posts") // Stop immediately count := 0 tree.Walk(func(path string, value string) bool { count++ return false // Stop after first }) assert.Equal(t, 1, count, "Walk should stop after first callback returns false") } func TestTree_Walk_StopInParamChildBranch(t *testing.T) { // Specifically test stopping while in the paramChild branch tree := New[string]() tree.Insert("/a", "a") tree.Insert("/b/{id}", "b-id") tree.Insert("/b/{id}/c", "b-id-c") paths := []string{} tree.Walk(func(path string, value string) bool { paths = append(paths, path) // Stop when we hit the param child's nested path return path != "/b/{id}/c" }) // Should have stopped at or after /b/{id}/c assert.LessOrEqual(t, len(paths), 3) } func TestExtractParamName_NonParam(t *testing.T) { // Test extractParamName with non-parameter segments (fallback case) // This tests the "return seg" branch // These are NOT valid params, should return as-is testCases := []struct { input string expected string }{ {"users", "users"}, // normal segment {"{}", "{}"}, // empty param - not valid (len <= 2) {"{a", "{a"}, // missing closing brace {"a}", "a}"}, // missing opening brace {"{", "{"}, // single char {"}", "}"}, // single char {"", ""}, // empty string {"ab", "ab"}, // two chars, not a param {"{x}", "x"}, // Valid param - extracts "x" {"{ab}", "ab"}, // Valid param - extracts "ab" } for _, tc := range testCases { result := extractParamName(tc.input) assert.Equal(t, tc.expected, result, "extractParamName(%q)", tc.input) } } func TestIsParam(t *testing.T) { testCases := []struct { input string expected bool }{ {"{id}", true}, {"{userId}", true}, {"{a}", true}, {"{}", false}, // empty param name {"{a", false}, // missing close {"a}", false}, // missing open {"id", false}, // no braces {"{", false}, // single char {"}", false}, // single char {"", false}, // empty {"ab", false}, // two chars {"{ab", false}, // three chars, missing close {"ab}", false}, // three chars, missing open } for _, tc := range testCases { result := isParam(tc.input) assert.Equal(t, tc.expected, result, "isParam(%q)", tc.input) } } func TestSplitPath(t *testing.T) { testCases := []struct { input string expected []string }{ {"/users/{id}/posts", []string{"users", "{id}", "posts"}}, {"/users", []string{"users"}}, {"/", nil}, {"", nil}, {"users", []string{"users"}}, {"/a/b/c", []string{"a", "b", "c"}}, {"//a//b//", []string{"a", "b"}}, // double slashes filtered {"/a/", []string{"a"}}, {"///", nil}, // all slashes } for _, tc := range testCases { result := splitPath(tc.input) assert.Equal(t, tc.expected, result, "splitPath(%q)", tc.input) } } func TestTree_SpecialCharacters(t *testing.T) { tree := New[string]() // Paths with special characters (URL-safe ones) tree.Insert("/api/v1/users", "users") tree.Insert("/api/v1/users/{id}", "user") tree.Insert("/api/v1/items-list", "items-list") tree.Insert("/api/v1/snake_case", "snake") tree.Insert("/api/v1/CamelCase", "camel") tests := []struct { lookup string expected string found bool }{ {"/api/v1/users", "/api/v1/users", true}, {"/api/v1/users/user-123", "/api/v1/users/{id}", true}, {"/api/v1/users/user_456", "/api/v1/users/{id}", true}, {"/api/v1/items-list", "/api/v1/items-list", true}, {"/api/v1/snake_case", "/api/v1/snake_case", true}, {"/api/v1/CamelCase", "/api/v1/CamelCase", true}, } for _, tc := range tests { _, path, found := tree.Lookup(tc.lookup) assert.Equal(t, tc.found, found, "lookup %q", tc.lookup) if tc.found { assert.Equal(t, tc.expected, path, "lookup %q", tc.lookup) } } } func TestTree_SingleCharSegments(t *testing.T) { tree := New[string]() tree.Insert("/a", "a") tree.Insert("/a/b", "ab") tree.Insert("/a/{x}", "ax") tree.Insert("/a/b/c", "abc") _, path, found := tree.Lookup("/a") assert.True(t, found) assert.Equal(t, "/a", path) _, path, found = tree.Lookup("/a/b") assert.True(t, found) assert.Equal(t, "/a/b", path) _, path, found = tree.Lookup("/a/z") assert.True(t, found) assert.Equal(t, "/a/{x}", path) } func TestTree_URLEncodedSegments(t *testing.T) { // URL-encoded values should be matched as literals tree := New[string]() tree.Insert("/users/{id}", "user") // These are all different IDs that should match the param testIDs := []string{ "123", "abc", "user%40example.com", // @ encoded "hello%20world", // space encoded "100%25", // % encoded } for _, id := range testIDs { _, path, found := tree.Lookup("/users/" + id) assert.True(t, found, "should find path for /users/%s", id) assert.Equal(t, "/users/{id}", path) } } func TestTree_NumericSegments(t *testing.T) { tree := New[string]() tree.Insert("/v1/resource", "v1") tree.Insert("/v2/resource", "v2") tree.Insert("/{version}/resource", "versioned") _, path, found := tree.Lookup("/v1/resource") assert.True(t, found) assert.Equal(t, "/v1/resource", path) _, path, found = tree.Lookup("/v2/resource") assert.True(t, found) assert.Equal(t, "/v2/resource", path) _, path, found = tree.Lookup("/v999/resource") assert.True(t, found) assert.Equal(t, "/{version}/resource", path) } func TestTree_DeepNesting(t *testing.T) { tree := New[string]() // Very deep path deepPath := "/a/{b}/c/{d}/e/{f}/g/{h}/i/{j}/k" tree.Insert(deepPath, "deep") _, path, found := tree.Lookup("/a/1/c/2/e/3/g/4/i/5/k") assert.True(t, found) assert.Equal(t, deepPath, path) } func TestTree_LookupPartialMatch(t *testing.T) { tree := New[string]() tree.Insert("/users/{id}/posts/{postId}", "post") // Partial path should not match _, _, found := tree.Lookup("/users/123/posts") assert.False(t, found, "partial path should not match") _, _, found = tree.Lookup("/users/123") assert.False(t, found, "partial path should not match") } func TestTree_OverlappingPaths(t *testing.T) { tree := New[string]() // Insert paths that could conflict tree.Insert("/api/users", "users list") tree.Insert("/api/users/search", "users search") tree.Insert("/api/users/{id}", "user by id") tree.Insert("/api/users/{id}/profile", "user profile") tree.Insert("/api/users/{userId}/posts/{postId}", "user post") tests := []struct { lookup string expected string }{ {"/api/users", "/api/users"}, {"/api/users/search", "/api/users/search"}, {"/api/users/123", "/api/users/{id}"}, {"/api/users/123/profile", "/api/users/{id}/profile"}, {"/api/users/u1/posts/p1", "/api/users/{userId}/posts/{postId}"}, } for _, tc := range tests { _, path, found := tree.Lookup(tc.lookup) require.True(t, found, "should find %s", tc.lookup) assert.Equal(t, tc.expected, path, "lookup %s", tc.lookup) } } func TestTree_ConcurrentAccess(t *testing.T) { // Test concurrent reads (tree is read-only after construction) tree := New[string]() paths := []string{ "/api/v1/users", "/api/v1/users/{id}", "/api/v1/posts", "/api/v1/posts/{id}", } for _, p := range paths { tree.Insert(p, "handler:"+p) } // Concurrent lookups done := make(chan bool) for i := 0; i < 100; i++ { go func(n int) { for j := 0; j < 100; j++ { path := paths[n%len(paths)] testPath := path if n%2 == 0 { // Replace params with values testPath = "/api/v1/users/123" } _, _, _ = tree.Lookup(testPath) } done <- true }(i) } // Wait for all goroutines for i := 0; i < 100; i++ { <-done } } func TestTree_EmptyValue(t *testing.T) { // Test that empty values are stored correctly tree := New[string]() tree.Insert("/empty", "") val, path, found := tree.Lookup("/empty") assert.True(t, found) assert.Equal(t, "/empty", path) assert.Equal(t, "", val) // Empty string is a valid value } func TestTree_PointerValues(t *testing.T) { // Test with pointer values to ensure nil handling type Handler struct { Name string } tree := New[*Handler]() h1 := &Handler{Name: "h1"} tree.Insert("/a", h1) tree.Insert("/b", nil) // nil pointer value val, _, found := tree.Lookup("/a") assert.True(t, found) assert.Equal(t, "h1", val.Name) val, _, found = tree.Lookup("/b") assert.True(t, found) assert.Nil(t, val) // nil is a valid value _, _, found = tree.Lookup("/c") assert.False(t, found) } // Benchmark tests func BenchmarkTree_Insert(b *testing.B) { paths := []string{ "/api/v3/ad_accounts", "/api/v3/ad_accounts/{ad_account_id}", "/api/v3/ad_accounts/{ad_account_id}/ads", "/api/v3/ad_accounts/{ad_account_id}/ads/{ad_id}", "/api/v3/ad_accounts/{ad_account_id}/campaigns", "/api/v3/ad_accounts/{ad_account_id}/campaigns/{campaign_id}", } b.ResetTimer() for i := 0; i < b.N; i++ { tree := New[string]() for _, p := range paths { tree.Insert(p, p) } } } func BenchmarkTree_Lookup_Literal(b *testing.B) { tree := New[string]() tree.Insert("/api/v3/ad_accounts", "accounts") b.ResetTimer() for i := 0; i < b.N; i++ { tree.Lookup("/api/v3/ad_accounts") } } func BenchmarkTree_Lookup_SingleParam(b *testing.B) { tree := New[string]() tree.Insert("/api/v3/ad_accounts/{ad_account_id}", "account") b.ResetTimer() for i := 0; i < b.N; i++ { tree.Lookup("/api/v3/ad_accounts/123456") } } func BenchmarkTree_Lookup_MultipleParams(b *testing.B) { tree := New[string]() tree.Insert("/api/v3/ad_accounts/{ad_account_id}/campaigns/{campaign_id}/ads/{ad_id}", "ad") b.ResetTimer() for i := 0; i < b.N; i++ { tree.Lookup("/api/v3/ad_accounts/acc1/campaigns/camp1/ads/ad1") } } func BenchmarkTree_Lookup_ManyPaths(b *testing.B) { tree := New[string]() // Simulate a realistic API with many paths for i := 0; i < 100; i++ { tree.Insert(fmt.Sprintf("/api/v3/resource%d", i), fmt.Sprintf("handler%d", i)) tree.Insert(fmt.Sprintf("/api/v3/resource%d/{id}", i), fmt.Sprintf("handler%d-id", i)) } b.ResetTimer() for i := 0; i < b.N; i++ { tree.Lookup("/api/v3/resource50/abc123") } } func BenchmarkTree_Lookup_VaryingIDs(b *testing.B) { tree := New[string]() tree.Insert("/api/v3/ad_accounts/{ad_account_id}/bulk_actions", "bulk") // Pre-generate test paths testPaths := make([]string, 1000) for i := 0; i < 1000; i++ { testPaths[i] = fmt.Sprintf("/api/v3/ad_accounts/account_%d/bulk_actions", i) } b.ResetTimer() for i := 0; i < b.N; i++ { tree.Lookup(testPaths[i%1000]) } } libopenapi-validator-0.13.8/requests/000077500000000000000000000000001520534042400175625ustar00rootroot00000000000000libopenapi-validator-0.13.8/requests/package.go000066400000000000000000000004001520534042400214760ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT // Package requests contains all the logic, models and interfaces for validating OpenAPI 3+ Requests. // The package depends on *http.Request package requests libopenapi-validator-0.13.8/requests/request_body.go000066400000000000000000000034021520534042400226150ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package requests import ( "net/http" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" ) // RequestBodyValidator is an interface that defines the methods for validating request bodies for Operations. // // ValidateRequestBodyWithPathItem method accepts an *http.Request and returns true if validation passed, // false if validation failed and a slice of ValidationError pointers. type RequestBodyValidator interface { // ValidateRequestBody will validate the request body for an operation. The first return value will be true if the // request body is valid, false if it is not. The second return value will be a slice of ValidationError pointers if // the body is not valid. ValidateRequestBody(request *http.Request) (bool, []*errors.ValidationError) // ValidateRequestBodyWithPathItem will validate the request body for an operation. The first return value will be true if the // request body is valid, false if it is not. The second return value will be a slice of ValidationError pointers if // the body is not valid. ValidateRequestBodyWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) } // NewRequestBodyValidator will create a new RequestBodyValidator from an OpenAPI 3+ document func NewRequestBodyValidator(document *v3.Document, opts ...config.Option) RequestBodyValidator { options := config.NewValidationOptions(opts...) return &requestBodyValidator{options: options, document: document} } type requestBodyValidator struct { options *config.ValidationOptions document *v3.Document } libopenapi-validator-0.13.8/requests/validate_body.go000066400000000000000000000122111520534042400227140ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package requests import ( "encoding/json" "fmt" "net/http" "strings" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" "github.com/pb33f/libopenapi-validator/schema_validation" ) func (v *requestBodyValidator) ValidateRequestBody(request *http.Request) (bool, []*errors.ValidationError) { pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options) if len(errs) > 0 { return false, errs } return v.ValidateRequestBodyWithPathItem(request, pathItem, foundPath) } func (v *requestBodyValidator) ValidateRequestBodyWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { if pathItem == nil { return false, []*errors.ValidationError{{ ValidationType: helpers.PathValidation, ValidationSubType: helpers.ValidationMissing, Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ "however that path, or the %s method for that path does not exist in the specification", request.Method, request.URL.Path, request.Method), SpecLine: -1, SpecCol: -1, HowToFix: errors.HowToFixPath, }} } operation := helpers.ExtractOperation(request, pathItem) if operation == nil { return false, []*errors.ValidationError{errors.OperationNotFound(pathItem, request, request.Method, pathValue)} } if operation.RequestBody == nil { return true, nil } // extract the content type from the request contentType := request.Header.Get(helpers.ContentTypeHeader) required := false if operation.RequestBody.Required != nil { required = *operation.RequestBody.Required } if contentType == "" { if !required { // request body is not required, the validation stop there. return true, nil } return false, []*errors.ValidationError{errors.RequestContentTypeNotFound(operation, request, pathValue)} } // extract the media type from the content type header. mediaType, ok := v.extractContentType(contentType, operation) if !ok { return false, []*errors.ValidationError{errors.RequestContentTypeNotFound(operation, request, pathValue)} } // Nothing to validate if mediaType.Schema == nil { return true, nil } // extract schema from media type schema := mediaType.Schema.Schema() isJson := strings.Contains(strings.ToLower(contentType), helpers.JSONType) // we currently only support JSON, XML and URLEncoded validation for request bodies if !isJson { isXml := schema_validation.IsXMLContentType(contentType) isUrlEncoded := schema_validation.IsURLEncodedContentType(contentType) xmlValid := isXml && v.options.AllowXMLBodyValidation urlEncodedValid := isUrlEncoded && v.options.AllowURLEncodedBodyValidation if !xmlValid && !urlEncodedValid { return true, nil } if request != nil && (request.Body != nil || request.GetBody != nil) { requestBody := readAndResetRequestBody(request) stringedBody := string(requestBody) var jsonBody any var prevalidationErrors []*errors.ValidationError switch { case xmlValid: jsonBody, prevalidationErrors = schema_validation.TransformXMLToSchemaJSON(stringedBody, schema) case urlEncodedValid: jsonBody, prevalidationErrors = schema_validation.TransformURLEncodedToSchemaJSON(stringedBody, schema, mediaType.Encoding) } if len(prevalidationErrors) > 0 { return false, prevalidationErrors } transformedBytes, err := json.Marshal(jsonBody) if err != nil { switch { case isXml: return false, []*errors.ValidationError{errors.InvalidXMLParsing(err.Error(), stringedBody)} case isUrlEncoded: return false, []*errors.ValidationError{errors.InvalidURLEncodedParsing(err.Error(), stringedBody)} } } setRequestBody(request, transformedBytes) } } validationSucceeded, validationErrors := ValidateRequestSchema(&ValidateRequestSchemaInput{ Request: request, Schema: schema, Version: helpers.VersionToFloat(v.document.Version), Options: []config.Option{config.WithExistingOpts(v.options)}, BodyRequired: required, }) errors.PopulateValidationErrors(validationErrors, request, pathValue) return validationSucceeded, validationErrors } func (v *requestBodyValidator) extractContentType(contentType string, operation *v3.Operation) (*v3.MediaType, bool) { ct, _, _ := helpers.ExtractContentType(contentType) mediaType, ok := operation.RequestBody.Content.Get(ct) if ok { return mediaType, true } ctMediaRange := strings.SplitN(ct, "/", 2) for contentPair := operation.RequestBody.Content.First(); contentPair != nil; contentPair = contentPair.Next() { s := contentPair.Key() mediaTypeValue := contentPair.Value() opMediaRange := strings.SplitN(s, "/", 2) if (opMediaRange[0] == "*" || opMediaRange[0] == ctMediaRange[0]) && (opMediaRange[1] == "*" || opMediaRange[1] == ctMediaRange[1]) { return mediaTypeValue, true } } return nil, false } libopenapi-validator-0.13.8/requests/validate_body_test.go000066400000000000000000001560151520534042400237660ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package requests import ( "bytes" "encoding/json" "fmt" "io" "net/http" "sync" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" ) func TestValidateBody_NotRequiredBody(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: false content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", http.NoBody) valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_MediaRangeContentType_Wildcard_end(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: true content: thomas/*: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "thomas/tank-engine") // wtf kinda content type is this? valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_MediaRangeContentType_Wildcards(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: true content: "*/*": schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "thomas/tank-engine") // wtf kinda content type is this? valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_InvalidBasicSchema_MediaRangeContentType_Wildcard_Required(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: false content: "*/json": schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "foo/json") valid, errors := v.ValidateRequestBody(request) // double-tap to hit the cache _, _ = v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message) } func TestValidateBody_UnknownContentType(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: true content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "thomas/tank-engine") // wtf kinda content type is this? valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST operation request content type 'thomas/tank-engine' does not exist", errors[0].Message) assert.Equal(t, "The content type is invalid, Use one of the 1 "+ "supported types for this operation: application/json", errors[0].HowToFix) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "/burgers/createBurger", errors[0].SpecPath) } func TestValidateBody_SkipValidationForNonJSON(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/yaml: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) body := map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/yaml") valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_PathNotFound(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/I do not exist", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST Path '/I do not exist' not found", errors[0].Message) assert.Equal(t, request.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "", errors[0].SpecPath) } func TestValidateBody_OperationNotFound(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model, nil) assert.Len(t, validationErrors, 0) request2, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request2.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBodyWithPathItem(request2, pathItem, pathValue) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "GET operation request content type 'GET' does not exist", errors[0].Message) assert.Equal(t, request2.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "/burgers/createBurger", errors[0].SpecPath) } func TestValidateBody_SetPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBodyWithPathItem(request, nil, "") assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST Path '/burgers/createBurger' not found", errors[0].Message) } func TestValidateBody_ContentTypeNotFound(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: true content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("content-type", "application/not-json") pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model, nil) assert.Len(t, validationErrors, 0) valid, errors := v.ValidateRequestBodyWithPathItem(request, pathItem, pathValue) assert.False(t, valid) assert.Len(t, errors, 1) } func TestValidateBody_ContentTypeNotSet(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: true content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model, &config.ValidationOptions{RegexCache: &sync.Map{}}) assert.Len(t, validationErrors, 0) valid, errors := v.ValidateRequestBodyWithPathItem(request, pathItem, pathValue) assert.False(t, valid) assert.Len(t, errors, 1) } func TestValidateBody_InvalidBasicSchema_NotRequired(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: false content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) // double-tap to hit the cache _, _ = v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message) } func TestValidateBody_InvalidBasicSchema_Required(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: false content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) // double-tap to hit the cache _, _ = v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message) } func TestValidateBody_ValidBasicSchema(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_UsesGetBodyWhenBodyAlreadyConsumed(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: true content: application/json: schema: type: object required: [name, patties, vegetarian] properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) request.Header.Set("Content-Type", "application/json") _, _ = io.ReadAll(request.Body) valid, validationErrors := v.ValidateRequestBody(request) require.True(t, valid) require.Empty(t, validationErrors) restoredBody, err := io.ReadAll(request.Body) require.NoError(t, err) require.JSONEq(t, string(bodyBytes), string(restoredBody)) replayedBody, err := request.GetBody() require.NoError(t, err) replayedBytes, err := io.ReadAll(replayedBody) require.NoError(t, err) require.NoError(t, replayedBody.Close()) require.JSONEq(t, string(bodyBytes), string(replayedBytes)) } func TestValidateBody_PrefersAssignedBodyOverStaleGetBody(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: true content: application/json: schema: type: object required: [name, patties, vegetarian] properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) staleBodyBytes, _ := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": true, }) currentBodyBytes, _ := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, }) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(staleBodyBytes)) request.Header.Set("Content-Type", "application/json") request.Body = io.NopCloser(bytes.NewReader(currentBodyBytes)) valid, validationErrors := v.ValidateRequestBody(request) require.True(t, valid) require.Empty(t, validationErrors) } func TestValidateBody_DoesNotUseStaleGetBodyForConsumedDifferentBodySameLength(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: true content: application/json: schema: type: object required: [patties] properties: patties: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) staleBodyBytes := []byte(`{"patties":12345}`) currentBodyBytes := []byte(`{"patties":false}`) require.Len(t, currentBodyBytes, len(staleBodyBytes)) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(staleBodyBytes)) request.Header.Set("Content-Type", "application/json") request.Body = io.NopCloser(bytes.NewReader(currentBodyBytes)) _, _ = io.ReadAll(request.Body) valid, validationErrors := v.ValidateRequestBody(request) require.False(t, valid) require.Len(t, validationErrors, 1) require.Equal(t, "POST request body is empty for '/burgers/createBurger'", validationErrors[0].Message) replayedBody, err := request.GetBody() require.NoError(t, err) replayedBytes, err := io.ReadAll(replayedBody) require.NoError(t, err) require.NoError(t, replayedBody.Close()) require.Empty(t, replayedBytes) } func TestValidateBody_DoesNotUseStaleGetBodyForExplicitEmptyBody(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: true content: application/json: schema: type: object required: [name, patties, vegetarian] properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) staleBodyBytes, _ := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, }) tests := []struct { name string body io.ReadCloser }{ { name: "http no body", body: http.NoBody, }, { name: "empty reader", body: io.NopCloser(bytes.NewReader(nil)), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(staleBodyBytes)) request.Header.Set("Content-Type", "application/json") request.Body = tc.body valid, validationErrors := v.ValidateRequestBody(request) require.False(t, valid) require.Len(t, validationErrors, 1) require.Equal(t, "POST request body is empty for '/burgers/createBurger'", validationErrors[0].Message) replayedBody, err := request.GetBody() require.NoError(t, err) replayedBytes, err := io.ReadAll(replayedBody) require.NoError(t, err) require.NoError(t, replayedBody.Close()) require.Empty(t, replayedBytes) }) } } func TestRequestBodyHelpers_NilRequest(t *testing.T) { setRequestBody(nil, []byte(`{"ok":true}`)) require.Nil(t, readAndResetRequestBody(nil)) } type requestBodyReaderTestBody struct{} func (r *requestBodyReaderTestBody) Read(_ []byte) (int, error) { return 0, io.EOF } func (r *requestBodyReaderTestBody) Close() error { return nil } type failingReplayableBody struct{} func (r *failingReplayableBody) Read(_ []byte) (int, error) { return 0, io.EOF } func (r *failingReplayableBody) Close() error { return nil } func (r *failingReplayableBody) ReadAt(_ []byte, _ int64) (int, error) { return 0, io.ErrUnexpectedEOF } func (r *failingReplayableBody) Size() int64 { return 1 } func TestRequestBodyReader_DefensiveBranches(t *testing.T) { require.Nil(t, requestBodyReader(nil)) require.Nil(t, requestBodyReader(http.NoBody)) var nilBody *requestBodyReaderTestBody require.Nil(t, requestBodyReader(nilBody)) body := &requestBodyReaderTestBody{} require.Same(t, body, requestBodyReader(body)) } func TestRequestBodySnapshot_DefensiveBranches(t *testing.T) { snapshot, ok := requestBodySnapshot(nil) require.False(t, ok) require.Nil(t, snapshot) snapshot, ok = requestBodySnapshot(&http.Request{Body: &requestBodyReaderTestBody{}}) require.False(t, ok) require.Nil(t, snapshot) snapshot, ok = requestBodySnapshot(&http.Request{Body: io.NopCloser(bytes.NewReader(nil))}) require.False(t, ok) require.Nil(t, snapshot) snapshot, ok = requestBodySnapshot(&http.Request{Body: &failingReplayableBody{}}) require.False(t, ok) require.Nil(t, snapshot) } func TestValidateBody_ValidBasicSchema_WithFullContentTypeHeader(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json; charset=utf-8; boundary=12345") valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_ValidSchemaUsingAllOf(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: $ref: '#/components/schema_validation/TestBody' components: schema_validation: Nutrients: type: object required: [fat, salt, meat] properties: fat: type: number salt: type: number meat: type: string enum: - beef - pork - lamb - vegetables TestBody: type: object allOf: - $ref: '#/components/schema_validation/Nutrients' properties: name: type: string patties: type: integer vegetarian: type: boolean required: [name, patties, vegetarian] ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, "fat": 10.0, "salt": 0.5, "meat": "beef", } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_InvalidSchemaUsingAllOf(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: $ref: '#/components/schema_validation/TestBody' components: schema_validation: Nutrients: type: object required: [fat, salt, meat] properties: fat: type: number salt: type: number meat: type: string enum: - beef - pork - lamb - vegetables TestBody: type: object allOf: - $ref: '#/components/schema_validation/Nutrients' properties: name: type: string patties: type: integer vegetarian: type: boolean required: [name, patties, vegetarian] ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, "fat": 10.0, "salt": false, // invalid "meat": "turkey", // invalid } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestValidateBody_ValidSchemaUsingAllOfAnyOf(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: $ref: '#/components/schema_validation/TestBody' components: schema_validation: Uncooked: type: object required: [uncookedWeight, uncookedHeight] properties: uncookedWeight: type: number uncookedHeight: type: number Cooked: type: object required: [usedOil, usedAnimalFat] properties: usedOil: type: boolean usedAnimalFat: type: boolean Nutrients: type: object required: [fat, salt, meat] properties: fat: type: number salt: type: number meat: type: string enum: - beef - pork - lamb - vegetables TestBody: type: object oneOf: - $ref: '#/components/schema_validation/Uncooked' - $ref: '#/components/schema_validation/Cooked' allOf: - $ref: '#/components/schema_validation/Nutrients' properties: name: type: string patties: type: integer vegetarian: type: boolean required: [name, patties, vegetarian] ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, "fat": 10.0, "salt": 0.5, "meat": "beef", "usedOil": true, "usedAnimalFat": false, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_InvalidSchemaUsingOneOf(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: $ref: '#/components/schema_validation/TestBody' components: schema_validation: Uncooked: type: object required: [uncookedWeight, uncookedHeight] properties: uncookedWeight: type: number uncookedHeight: type: number Cooked: type: object required: [usedOil, usedAnimalFat] properties: usedOil: type: boolean usedAnimalFat: type: boolean Nutrients: type: object required: [fat, salt, meat] properties: fat: type: number salt: type: number meat: type: string enum: - beef - pork - lamb - vegetables TestBody: type: object oneOf: - $ref: '#/components/schema_validation/Uncooked' - $ref: '#/components/schema_validation/Cooked' allOf: - $ref: '#/components/schema_validation/Nutrients' properties: name: type: string patties: type: integer vegetarian: type: boolean required: [name, patties, vegetarian] ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, "fat": 10.0, "salt": 0.5, "meat": "beef", } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) assert.Equal(t, "missing properties 'uncookedWeight', 'uncookedHeight'", errors[0].SchemaValidationErrors[0].Reason) assert.Equal(t, "missing properties 'usedOil', 'usedAnimalFat'", errors[0].SchemaValidationErrors[1].Reason) } func TestValidateBody_InvalidSchemaMinMax(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: $ref: '#/components/schema_validation/TestBody' components: schema_validation: TestBody: type: object properties: name: type: string patties: type: integer maximum: 3 minimum: 1 vegetarian: type: boolean required: [name, patties, vegetarian] ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) body := map[string]interface{}{ "name": "Big Mac", "patties": 5, "vegetarian": true, "fat": 10.0, "salt": 0.5, "meat": "beef", } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "maximum: got 5, want 3", errors[0].SchemaValidationErrors[0].Reason) } func TestValidateBody_InvalidSchemaMaxItems(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: $ref: '#/components/schema_validation/TestBody' components: schema_validation: TestBody: type: array maxItems: 2 items: type: object properties: name: type: string patties: type: integer maximum: 3 minimum: 1 vegetarian: type: boolean required: [name, patties, vegetarian] ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, "fat": 10.0, "salt": 0.5, "meat": "beef", } bodyArray := []interface{}{body, body, body, body} // two too many! bodyBytes, _ := json.Marshal(bodyArray) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "maxItems: got 4, want 2", errors[0].SchemaValidationErrors[0].Reason) assert.Equal(t, 2, errors[0].SchemaValidationErrors[0].Line) assert.Equal(t, "maxItems: got 4, want 2", errors[0].SchemaValidationErrors[0].Reason) assert.Equal(t, 11, errors[0].SchemaValidationErrors[0].Column) } func TestValidateBody_SchemaHasNoRequestBody(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post:` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", http.NoBody) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_MediaTypeHasNullSchema(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json:` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", http.NoBody) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_MissingBody(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: required: true content: application/json: schema: $ref: '#/components/schema_validation/TestBody' components: schema_validation: TestBody: type: array maxItems: 2 items: type: object properties: name: type: string patties: type: integer maximum: 3 minimum: 1 vegetarian: type: boolean required: [name, patties, vegetarian] ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", http.NoBody) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) } func TestValidateBody_NoBodyNoNothing(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post:` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", http.NoBody) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_InvalidSchemaMultipleItems(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: array items: type: object required: - name properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) var items []map[string]interface{} items = append(items, map[string]interface{}{ "patties": 1, "vegetarian": true, }) items = append(items, map[string]interface{}{ "name": "Quarter Pounder", "patties": true, "vegetarian": false, }) items = append(items, map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": false, }) bodyBytes, _ := json.Marshal(items) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) // double-tap to hit the cache _, _ = v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message) } func TestValidateBody_InvalidSchema_BadDecode(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: $ref: '#/components/schema_validation/TestBody' components: schema_validation: TestBody: type: object properties: name: type: string patties: type: integer maximum: 3 minimum: 1 vegetarian: type: boolean required: [name, patties, vegetarian] ` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer([]byte("{\"bad\": \"json\",}"))) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Nil(t, errors[0].SchemaValidationErrors) assert.Contains(t, errors[0].Reason, "cannot be decoded") } func TestValidateBody_SchemaNoType_Issue75(t *testing.T) { spec := `{ "openapi": "3.0.1", "info": { "title": "testing", "description": "

This is for testing purpose

", "version": "1.0", "x-targetEndpoint": "https://mocktarget.apigee.net/json" }, "servers": [ { "url": "https://some-url.com" } ], "paths": { "/path1": { "put": { "requestBody": { "required": true, "content": { "application/json": { "schema": { "anyOf": [ { "type": "object", "properties": { "name": { "type": "string", "minLength": 1 }, "age": { "type": "integer" } }, "required": [ "name" ] }, { "type": "object", "properties": { "email": { "type": "string" }, "address": { "type": "string" } }, "required": [ "email" ] } ] } } } }, "responses": { "200": { "description": "OK" } } } }, "/path2": { "get": { "parameters": [ { "name": "X-My-Header", "in": "header", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "OK" } } } }, "/path3": { "get": { "parameters": [ { "name": "id", "in": "query", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "OK" } } } } } } ` doc, err := libopenapi.NewDocument([]byte(spec)) if err != nil { fmt.Println("error while creating open api spec document", err) return } req, err := http.NewRequest("PUT", "/path1", nil) if err != nil { fmt.Println("error while creating new HTTP request", err) return } req.Header.Set("Content-Type", "application/json") v3Model, errs := doc.BuildV3Model() if errs != nil { fmt.Println("error while building a Open API spec V3 model", errs) return } reqBodyValidator := NewRequestBodyValidator(&v3Model.Model) isSuccess, valErrs := reqBodyValidator.ValidateRequestBody(req) assert.False(t, isSuccess) assert.Len(t, valErrs, 1) assert.Equal(t, "PUT request body is empty for '/path1'", valErrs[0].Message) } // https://github.com/pb33f/wiretap/issues/146 func TestValidateBody_OptionalRequestBody_EmptyBody(t *testing.T) { spec := `openapi: 3.0.1 info: title: test version: "1.0" paths: /test: post: requestBody: required: false content: application/json: schema: type: object properties: name: type: string responses: "200": description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) v3Model, _ := doc.BuildV3Model() req, _ := http.NewRequest("POST", "/test", nil) req.Header.Set("Content-Type", "application/json") reqBodyValidator := NewRequestBodyValidator(&v3Model.Model) isSuccess, valErrs := reqBodyValidator.ValidateRequestBody(req) assert.True(t, isSuccess) assert.Empty(t, valErrs) } // https://github.com/pb33f/libopenapi-validator/issues/144 func TestValidateBody_InvalidSchema_EnsureOptionsPassthrough(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: $ref: '#/components/schema_validation/V1_UserRequest' components: schema_validation: V1_UserRequest: type: object properties: email: type: string format: email minLength: 1 maxLength: 320` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model, config.WithFormatAssertions()) items := make(map[string]interface{}) items["email"] = "test" bodyBytes, _ := json.Marshal(items) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "'test' is not valid email: missing @", errors[0].SchemaValidationErrors[0].Reason) } func TestValidateBody_StrictMode_UndeclaredProperty(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model, config.WithStrictMode()) // Include an undeclared property 'extra' body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "extra": "undeclared property", } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "extra") assert.Contains(t, errors[0].Message, "not declared") } func TestValidateBody_StrictMode_ValidRequest(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model, config.WithStrictMode()) // Only declared properties body := map[string]interface{}{ "name": "Big Mac", "patties": 2, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_StrictMode_ReadOnlyProperty(t *testing.T) { spec := `openapi: 3.1.0 paths: /users: post: requestBody: content: application/json: schema: type: object properties: id: type: string readOnly: true name: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model, config.WithStrictMode(), config.WithStrictRejectReadOnly(), ) body := map[string]interface{}{ "id": "user-123", "name": "John", } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/users", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "readOnly") assert.Contains(t, errors[0].Message, "id") } func TestValidateRequestBody_XMLMarshalError(t *testing.T) { spec := []byte(` openapi: 3.1.0 info: title: Test Spec version: 1.0.0 paths: /test: post: requestBody: required: true content: application/xml: schema: type: object properties: bad_number: type: number responses: '200': description: Success `) doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation()) request, _ := http.NewRequest(http.MethodPost, "https://things.com/test", bytes.NewBuffer([]byte("NaN"))) request.Header.Set("Content-Type", "application/xml") valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, errors[0].Message, "xml example is malformed") } func TestValidateRequestBody_URLEncodedMarshalError(t *testing.T) { spec := []byte(` openapi: 3.1.0 info: title: Test Spec version: 1.0.0 paths: /test: post: requestBody: required: true content: application/x-www-form-urlencoded: schema: type: object properties: bad_number: type: number responses: '200': description: Success `) doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model, config.WithURLEncodedBodyValidation()) request, _ := http.NewRequest(http.MethodPost, "https://things.com/test", bytes.NewBuffer([]byte("bad_number=NaN"))) request.Header.Set("Content-Type", helpers.URLEncodedContentType) valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, errors[0].Message, "Unable to parse form-urlencoded body") } func TestValidateBody_URLEncodedRequest(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/x-www-form-urlencoded: schema: type: object required: - name properties: name: type: string patties: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model, config.WithURLEncodedBodyValidation()) body := "name=cheeseburger&patties=23" request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer([]byte(body))) request.Header.Set("Content-Type", "application/x-www-form-urlencoded") valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) body = "name=cheeseburger&patties=23.4" request, _ = http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer([]byte(body))) request.Header.Set("Content-Type", "application/x-www-form-urlencoded") valid, errors = v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) } func TestValidateBody_XmlRequest(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/xml: schema: type: object required: - name properties: name: type: string patties: type: integer xml: name: cost` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation()) body := "cheeseburger23" request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer([]byte(body))) request.Header.Set("Content-Type", "application/xml") valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_XmlMalformedRequest(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/xml: schema: type: object required: - name properties: name: type: string patties: type: integer xml: name: cost` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation()) body := "" request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer([]byte(body))) request.Header.Set("Content-Type", "application/xml") valid, errors := v.ValidateRequestBody(request) assert.False(t, valid) assert.Len(t, errors, 1) err := errors[0] assert.Equal(t, helpers.XmlValidation, err.ValidationType) assert.Contains(t, err.Reason, "failed to parse xml") } func TestValidateBody_XmlRequestTransformations(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/xml: schema: type: object xml: name: Burger required: - name - patties properties: name: type: string patties: type: integer xml: name: cost` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation()) body := "cheeseburger23" request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer([]byte(body))) request.Header.Set("Content-Type", "application/xml") valid, errors := v.ValidateRequestBody(request) assert.True(t, valid) assert.Len(t, errors, 0) } libopenapi-validator-0.13.8/requests/validate_request.go000066400000000000000000000342471520534042400234640ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package requests import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "reflect" "regexp" "strconv" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/utils" "github.com/santhosh-tekuri/jsonschema/v6" "go.yaml.in/yaml/v4" "golang.org/x/text/language" "golang.org/x/text/message" "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi-validator/config" liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/schema_validation" "github.com/pb33f/libopenapi-validator/strict" ) var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`) // ValidateRequestSchemaInput contains parameters for request schema validation. type ValidateRequestSchemaInput struct { Request *http.Request // Required: The HTTP request to validate Schema *base.Schema // Required: The OpenAPI schema to validate against Version float32 // Required: OpenAPI version (3.0 or 3.1) Options []config.Option // Optional: Functional options (defaults applied if empty/nil) BodyRequired bool // Optional: Whether the request body is required (default false) } type replayableBody interface { io.ReaderAt Size() int64 } func setRequestBody(request *http.Request, body []byte) { if request == nil { return } bodyCopy := append([]byte(nil), body...) request.Body = io.NopCloser(bytes.NewReader(bodyCopy)) request.ContentLength = int64(len(bodyCopy)) request.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(bodyCopy)), nil } } func requestBodySnapshot(request *http.Request) ([]byte, bool) { if request == nil || request.Body == nil || request.Body == http.NoBody { return nil, false } reader := requestBodyReader(request.Body) body, ok := reader.(replayableBody) if !ok { return nil, false } size := body.Size() if size <= 0 { return nil, false } snapshot, err := io.ReadAll(io.NewSectionReader(body, 0, size)) if err != nil { return nil, false } return snapshot, true } func requestBodyReader(body io.ReadCloser) io.Reader { if body == nil || body == http.NoBody { return nil } value := reflect.ValueOf(body) if value.Kind() == reflect.Ptr { if value.IsNil() { return nil } value = value.Elem() } if value.Kind() == reflect.Struct { field := value.FieldByName("Reader") if field.IsValid() && field.CanInterface() { if reader, ok := field.Interface().(io.Reader); ok { return reader } } } return body } func readAndResetRequestBody(request *http.Request) []byte { if request == nil { return nil } var requestBody []byte bodyRead := false bodySnapshot, hasBodySnapshot := requestBodySnapshot(request) if request.Body != nil { requestBody, _ = io.ReadAll(request.Body) _ = request.Body.Close() bodyRead = true } if len(requestBody) == 0 && hasBodySnapshot && request.GetBody != nil { if body, err := request.GetBody(); err == nil && body != nil { replayedBody, _ := io.ReadAll(body) _ = body.Close() if bytes.Equal(replayedBody, bodySnapshot) { requestBody = replayedBody bodyRead = true } } } if bodyRead { setRequestBody(request, requestBody) } return requestBody } // ValidateRequestSchema will validate a http.Request pointer against a schema. // If validation fails, it will return a list of validation errors as the second return value. // The schema will be stored and reused from cache if available, otherwise it will be compiled on each call. func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*liberrors.ValidationError) { validationOptions := config.NewValidationOptions(input.Options...) var validationErrors []*liberrors.ValidationError var renderedSchema, jsonSchema []byte var referenceSchema string var compiledSchema *jsonschema.Schema var cachedNode *yaml.Node if input.Schema == nil { return false, []*liberrors.ValidationError{{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: "schema is nil", Reason: "The schema to validate against is nil", }} } else if input.Schema.GoLow() == nil { return false, []*liberrors.ValidationError{{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: "schema cannot be rendered", Reason: "The schema does not have low-level information and cannot be rendered. Please ensure the schema is loaded from a document.", }} } if validationOptions.SchemaCache != nil { hash := input.Schema.GoLow().Hash() if cached, ok := validationOptions.SchemaCache.Load(hash); ok && cached != nil && cached.CompiledSchema != nil { renderedSchema = cached.RenderedInline referenceSchema = cached.ReferenceSchema jsonSchema = cached.RenderedJSON compiledSchema = cached.CompiledSchema cachedNode = cached.RenderedNode } } // Cache miss or no cache - render and compile if compiledSchema == nil { renderCtx := base.NewInlineRenderContextForValidation() var renderErr error renderedSchema, renderErr = input.Schema.RenderInlineWithContext(renderCtx) referenceSchema = string(renderedSchema) // If rendering failed (e.g., circular reference), return the render error if renderErr != nil { violation := &liberrors.SchemaValidationFailure{ Reason: renderErr.Error(), ReferenceSchema: referenceSchema, } validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s request body for '%s' failed schema rendering", input.Request.Method, input.Request.URL.Path), Reason: fmt.Sprintf("The request schema failed to render: %s", renderErr.Error()), SpecLine: 1, SpecCol: 0, SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation}, HowToFix: liberrors.HowToFixInvalidRenderedSchema, Context: referenceSchema, }) return false, validationErrors } jsonSchema, _ = utils.ConvertYAMLtoJSON(renderedSchema) var err error schemaName := fmt.Sprintf("%x", input.Schema.GoLow().Hash()) compiledSchema, err = helpers.NewCompiledSchemaWithVersion( schemaName, jsonSchema, validationOptions, input.Version, ) if err != nil { validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s request body for '%s' failed schema compilation", input.Request.Method, input.Request.URL.Path), Reason: fmt.Sprintf("The request schema failed to compile: %s", err.Error()), SpecLine: 1, SpecCol: 0, HowToFix: "check the request schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", Context: input.Schema, }) return false, validationErrors } if validationOptions.SchemaCache != nil { hash := input.Schema.GoLow().Hash() validationOptions.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ Schema: input.Schema, RenderedInline: renderedSchema, ReferenceSchema: referenceSchema, RenderedJSON: jsonSchema, CompiledSchema: compiledSchema, }) } } request := input.Request schema := input.Schema requestBody := readAndResetRequestBody(request) var decodedObj interface{} if len(requestBody) > 0 { err := json.Unmarshal(requestBody, &decodedObj) if err != nil { // cannot decode the request body, so it's not valid validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s request body for '%s' failed to validate schema", request.Method, request.URL.Path), Reason: fmt.Sprintf("The request body cannot be decoded: %s", err.Error()), SpecLine: 1, SpecCol: 0, HowToFix: liberrors.HowToFixInvalidSchema, Context: schema, }) return false, validationErrors } } // no request body? but we do have a schema? if len(requestBody) == 0 && len(jsonSchema) > 0 { if !input.BodyRequired { return true, nil } line := 1 col := 0 if schema.ParentProxy != nil { if keyNode := schema.ParentProxy.GetSchemaKeyNode(); keyNode != nil { line = keyNode.Line col = keyNode.Column } } if schema.Type != nil { if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil { line = low.Type.KeyNode.Line col = low.Type.KeyNode.Column } } validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s request body is empty for '%s'", request.Method, request.URL.Path), Reason: "The request body is empty but there is a schema defined", SpecLine: line, SpecCol: col, HowToFix: liberrors.HowToFixInvalidSchema, Context: schema, }) return false, validationErrors } // validate the object against the schema scErrs := compiledSchema.Validate(decodedObj) if scErrs != nil { var jk *jsonschema.ValidationError var schemaValidationErrors []*liberrors.SchemaValidationFailure if errors.As(scErrs, &jk) { // flatten the validationErrors schFlatErrs := jk.BasicOutput().Errors // Use cached node if available, otherwise parse renderedNode := cachedNode if renderedNode == nil { renderedNode = new(yaml.Node) _ = yaml.Unmarshal(renderedSchema, renderedNode) } for q := range schFlatErrs { er := schFlatErrs[q] errMsg := er.Error.Kind.LocalizedString(message.NewPrinter(language.Tag{})) if er.KeywordLocation == "" || helpers.IgnoreRegex.MatchString(errMsg) { continue // ignore this error, it's useless tbh, utter noise. } if er.Error != nil { // locate the violated property in the schema var located *yaml.Node if len(renderedNode.Content) > 0 { located = schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation) } // extract the element specified by the instance val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation) var referenceObject string if len(val) > 0 { referenceIndex, _ := strconv.Atoi(val[1]) if reflect.ValueOf(decodedObj).Type().Kind() == reflect.Slice { found := decodedObj.([]any)[referenceIndex] recoded, _ := json.MarshalIndent(found, "", " ") referenceObject = string(recoded) } } if referenceObject == "" { referenceObject = string(requestBody) } errMsg := er.Error.Kind.LocalizedString(message.NewPrinter(language.Tag{})) violation := &liberrors.SchemaValidationFailure{ Reason: errMsg, FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), KeywordLocation: er.KeywordLocation, ReferenceSchema: referenceSchema, ReferenceObject: referenceObject, OriginalJsonSchemaError: jk, } // if we have a location within the schema, add it to the error if located != nil { line := located.Line // if the located node is a map or an array, then the actual human interpretable // line on which the violation occurred is the line of the key, not the value. if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode { if line > 0 { line-- } } // location of the violation within the rendered schema. violation.Line = line violation.Column = located.Column } schemaValidationErrors = append(schemaValidationErrors, violation) } } } line := 1 col := 0 if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil { line = low.Type.KeyNode.Line col = low.Type.KeyNode.Column } // add the error to the list validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s request body for '%s' failed to validate schema", request.Method, request.URL.Path), Reason: "The request body is defined as an object. " + "However, it does not meet the schema requirements of the specification", SpecLine: line, SpecCol: col, SchemaValidationErrors: schemaValidationErrors, HowToFix: liberrors.HowToFixInvalidSchema, Context: schema, }) } if len(validationErrors) > 0 { return false, validationErrors } // strict mode: check for undeclared properties in request body if validationOptions.StrictMode && decodedObj != nil { strictValidator := strict.NewValidator(validationOptions, input.Version) strictResult := strictValidator.Validate(strict.Input{ Schema: schema, Data: decodedObj, Direction: strict.DirectionRequest, Options: validationOptions, BasePath: "$.body", Version: input.Version, }) if !strictResult.Valid { for _, undeclared := range strictResult.UndeclaredValues { switch undeclared.Type { case strict.TypeReadOnlyProperty: validationErrors = append(validationErrors, liberrors.ReadOnlyPropertyError( undeclared.Path, undeclared.Name, undeclared.Value, request.URL.Path, request.Method, undeclared.SpecLine, undeclared.SpecCol, )) default: validationErrors = append(validationErrors, liberrors.UndeclaredPropertyError( undeclared.Path, undeclared.Name, undeclared.Value, undeclared.DeclaredProperties, undeclared.Direction.String(), request.URL.Path, request.Method, undeclared.SpecLine, undeclared.SpecCol, )) } } } } if len(validationErrors) > 0 { return false, validationErrors } return true, nil } libopenapi-validator-0.13.8/requests/validate_request_test.go000066400000000000000000000225241520534042400245160ustar00rootroot00000000000000package requests import ( "fmt" "io" "net/http" "net/url" "strings" "testing" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pb33f/libopenapi-validator/config" ) func TestValidateRequestSchema(t *testing.T) { for name, tc := range map[string]struct { request *http.Request schemaYAML string version float32 assertValidRequestSchema assert.BoolAssertionFunc expectedErrorsCount int }{ "FailOnBooleanExclusiveMinimum": { request: postRequestWithBody(`{"exclusiveNumber": 10}`), schemaYAML: `type: object properties: exclusiveNumber: type: number description: This number starts its journey where most numbers are too scared to begin! exclusiveMinimum: true minimum: !!float 10`, version: 3.0, assertValidRequestSchema: assert.False, expectedErrorsCount: 1, }, "PassWithCorrectExclusiveMinimum": { request: postRequestWithBody(`{"exclusiveNumber": 15}`), schemaYAML: `type: object properties: exclusiveNumber: type: number description: This number is properly constrained by a numeric exclusive minimum. exclusiveMinimum: 12 minimum: 12`, version: 3.1, assertValidRequestSchema: assert.True, expectedErrorsCount: 0, }, "PassWithValidStringType": { request: postRequestWithBody(`{"greeting": "Hello, world!"}`), schemaYAML: `type: object properties: greeting: type: string description: A simple greeting example: "Hello, world!"`, version: 3.1, assertValidRequestSchema: assert.True, expectedErrorsCount: 0, }, "PassWithNullablePropertyInOpenAPI30": { request: postRequestWithBody(`{"name": "John", "middleName": null}`), schemaYAML: `type: object properties: name: type: string description: User's first name middleName: type: string nullable: true description: User's middle name (optional)`, version: 3.0, assertValidRequestSchema: assert.True, expectedErrorsCount: 0, }, "PassWithNullablePropertyInOpenAPI31": { request: postRequestWithBody(`{"name": "John", "middleName": null}`), schemaYAML: `type: object properties: name: type: string description: User's first name middleName: type: string nullable: true description: User's middle name (optional)`, version: 3.1, assertValidRequestSchema: assert.False, expectedErrorsCount: 1, }, } { t.Run(name, func(t *testing.T) { t.Parallel() schema := parseSchemaFromSpec(t, tc.schemaYAML, tc.version) valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{ Request: tc.request, Schema: schema, Version: tc.version, }) tc.assertValidRequestSchema(t, valid) assert.Len(t, errors, tc.expectedErrorsCount) }) } } func TestBooleanExclusiveMin_ValidValue(t *testing.T) { openAPIVersion := float32(3.0) schema := parseSchemaFromSpec(t, `type: object properties: exclusiveNumber: type: number description: This number starts its journey where most numbers are too scared to begin! exclusiveMinimum: true minimum: 10`, openAPIVersion) valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{ Request: postRequestWithBody(`{"exclusiveNumber": 13}`), Schema: schema, Version: openAPIVersion, }) assert.True(t, valid) assert.Empty(t, errors) } func TestValidateRequestSchema_CachePopulation(t *testing.T) { openAPIVersion := float32(3.1) schema := parseSchemaFromSpec(t, `type: object properties: name: type: string`, openAPIVersion) // Create options with a cache opts := config.NewValidationOptions() require.NotNil(t, opts.SchemaCache) // First call should populate the cache valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{ Request: postRequestWithBody(`{"name": "test"}`), Schema: schema, Version: openAPIVersion, Options: []config.Option{config.WithExistingOpts(opts)}, }) assert.True(t, valid) assert.Len(t, errors, 0) // Verify cache was populated hash := schema.GoLow().Hash() cached, ok := opts.SchemaCache.Load(hash) assert.True(t, ok, "Schema should be in cache") assert.NotNil(t, cached, "Cached entry should not be nil") assert.NotNil(t, cached.CompiledSchema, "Compiled schema should be cached") assert.NotNil(t, cached.RenderedInline, "Rendered schema should be cached") assert.NotNil(t, cached.RenderedJSON, "JSON schema should be cached") } func TestValidateRequestSchema_NilSchema(t *testing.T) { // Test when schema is nil valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{ Request: postRequestWithBody(`{"name": "test"}`), Schema: nil, Version: 3.1, }) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "schema is nil", errors[0].Message) assert.Equal(t, "The schema to validate against is nil", errors[0].Reason) } func TestValidateRequestSchema_NilSchemaGoLow(t *testing.T) { // Test when schema.GoLow() is nil by creating a schema without low-level data schema := &base.Schema{} // Empty schema without GoLow() data valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{ Request: postRequestWithBody(`{"name": "test"}`), Schema: schema, Version: 3.1, }) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "schema cannot be rendered", errors[0].Message) assert.Contains(t, errors[0].Reason, "does not have low-level information") } func TestValidateRequestSchema_EmptyBodyOptional(t *testing.T) { schema := parseSchemaFromSpec(t, `type: object properties: name: type: string`, 3.1) valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{ Request: emptyPostRequest(), Schema: schema, Version: 3.1, BodyRequired: false, }) assert.True(t, valid) assert.Empty(t, errors) } func TestValidateRequestSchema_EmptyBodyRequired(t *testing.T) { schema := parseSchemaFromSpec(t, `type: object properties: name: type: string`, 3.1) valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{ Request: emptyPostRequest(), Schema: schema, Version: 3.1, BodyRequired: true, }) assert.False(t, valid) require.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "request body is empty") } func postRequestWithBody(payload string) *http.Request { return &http.Request{ Method: http.MethodPost, URL: &url.URL{Path: "/test"}, Body: io.NopCloser(strings.NewReader(payload)), } } func emptyPostRequest() *http.Request { return &http.Request{ Method: http.MethodPost, URL: &url.URL{Path: "/test"}, Body: http.NoBody, } } // parseSchemaFromSpec creates a base.Schema from an OpenAPI spec YAML string. // This ensures that we're using the native libopenapi logic for generating the schema. func parseSchemaFromSpec(t *testing.T, schemaYAML string, version float32) *base.Schema { // Convert version to API version string (3.0 -> "3.0.0", 3.1 -> "3.1.0") apiVersion := fmt.Sprintf("%.1f.0", version) spec := fmt.Sprintf(`openapi: %s info: title: Test version: 1.0.0 components: schemas: TestSchema: %s`, apiVersion, indentLines(schemaYAML, " ")) doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) model, errs := doc.BuildV3Model() require.Empty(t, errs) schema := model.Model.Components.Schemas.GetOrZero("TestSchema") require.NotNil(t, schema) return schema.Schema() } // indentLines adds the specified indentation to each line of the input string func indentLines(s string, indent string) string { lines := strings.Split(strings.TrimSpace(s), "\n") for i, line := range lines { if line != "" { lines[i] = indent + line } } return strings.Join(lines, "\n") } func TestValidateRequestSchema_CircularReference(t *testing.T) { // Test when schema has a circular reference that causes render failure spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 components: schemas: Error: type: object properties: code: type: string details: type: array items: $ref: '#/components/schemas/Error'` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) model, errs := doc.BuildV3Model() require.Empty(t, errs) // Verify circular reference was detected require.Len(t, model.Index.GetCircularReferences(), 1) schema := model.Model.Components.Schemas.GetOrZero("Error") require.NotNil(t, schema) valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{ Request: postRequestWithBody(`{"code": "abc", "details": [{"code": "def"}]}`), Schema: schema.Schema(), Version: 3.1, }) assert.False(t, valid) require.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "failed schema rendering") assert.Contains(t, errors[0].Reason, "circular reference") } func TestValidateRequestSchema_NilParentProxy(t *testing.T) { // Schema with nil ParentProxy and empty body — should not panic (fix for wiretap #134) schema := &base.Schema{ Type: []string{"object"}, } valid, errs := ValidateRequestSchema(&ValidateRequestSchemaInput{ Request: postRequestWithBody(""), Schema: schema, Version: 3.1, BodyRequired: true, }) // Should return error about nil schema low-level info, not panic assert.False(t, valid) assert.NotEmpty(t, errs) } libopenapi-validator-0.13.8/responses/000077500000000000000000000000001520534042400177305ustar00rootroot00000000000000libopenapi-validator-0.13.8/responses/package.go000066400000000000000000000004031520534042400216470ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT // Package responses contains all the logic, models and interfaces for validating OpenAPI 3+ Responses // The package depends on *http.Response package responses libopenapi-validator-0.13.8/responses/response_body.go000066400000000000000000000035041520534042400231340ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package responses import ( "net/http" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" ) // ResponseBodyValidator is an interface that defines the methods for validating response bodies for Operations. // // ValidateResponseBody method accepts an *http.Request and returns true if validation passed, // false if validation failed and a slice of ValidationError pointers. type ResponseBodyValidator interface { // ValidateResponseBody will validate the response body for a http.Response pointer. The request is used to // locate the operation in the specification, the response is used to ensure the response code, media type and the // schema of the response body are valid. ValidateResponseBody(request *http.Request, response *http.Response) (bool, []*errors.ValidationError) // ValidateResponseBodyWithPathItem will validate the response body for a http.Response pointer. The request is used to // locate the operation in the specification, the response is used to ensure the response code, media type and the // schema of the response body are valid. ValidateResponseBodyWithPathItem(request *http.Request, response *http.Response, pathItem *v3.PathItem, pathFound string) (bool, []*errors.ValidationError) } // NewResponseBodyValidator will create a new ResponseBodyValidator from an OpenAPI 3+ document func NewResponseBodyValidator(document *v3.Document, opts ...config.Option) ResponseBodyValidator { options := config.NewValidationOptions(opts...) return &responseBodyValidator{options: options, document: document} } type responseBodyValidator struct { options *config.ValidationOptions document *v3.Document } libopenapi-validator-0.13.8/responses/validate_body.go000066400000000000000000000164211520534042400230710ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package responses import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "github.com/pb33f/libopenapi/orderedmap" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" "github.com/pb33f/libopenapi-validator/schema_validation" ) func (v *responseBodyValidator) ValidateResponseBody( request *http.Request, response *http.Response, ) (bool, []*errors.ValidationError) { pathItem, errs, foundPath := paths.FindPath(request, v.document, v.options) if len(errs) > 0 { return false, errs } return v.ValidateResponseBodyWithPathItem(request, response, pathItem, foundPath) } func (v *responseBodyValidator) ValidateResponseBodyWithPathItem(request *http.Request, response *http.Response, pathItem *v3.PathItem, pathFound string) (bool, []*errors.ValidationError) { if pathItem == nil { return false, []*errors.ValidationError{{ ValidationType: helpers.PathValidation, ValidationSubType: helpers.ValidationMissing, Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ "however that path, or the %s method for that path does not exist in the specification", request.Method, request.URL.Path, request.Method), SpecLine: -1, SpecCol: -1, HowToFix: errors.HowToFixPath, }} } var validationErrors []*errors.ValidationError operation := helpers.ExtractOperation(request, pathItem) if operation == nil { return false, []*errors.ValidationError{errors.OperationNotFound(pathItem, request, request.Method, pathFound)} } // extract the response code from the response httpCode := response.StatusCode contentType := response.Header.Get(helpers.ContentTypeHeader) codeStr := strconv.Itoa(httpCode) // extract the media type from the content type header. mediaTypeSting, _, _ := helpers.ExtractContentType(contentType) // check if operation has responses defined if operation.Responses == nil || operation.Responses.Codes == nil { return true, nil } // check if the response code is in the contract foundResponse := operation.Responses.Codes.GetOrZero(codeStr) if foundResponse == nil { // check range definition for response codes foundResponse = operation.Responses.Codes.GetOrZero(fmt.Sprintf("%dXX", httpCode/100)) if foundResponse != nil { codeStr = fmt.Sprintf("%dXX", httpCode/100) } } if foundResponse != nil { if foundResponse.Content != nil { // only validate if we have content types. // check content type has been defined in the contract if mediaType, ok := foundResponse.Content.Get(mediaTypeSting); ok { validationErrors = append(validationErrors, v.checkResponseSchema(request, response, mediaTypeSting, mediaType)...) } else { // check that the operation *actually* returns a body. (i.e. a 204 response) if foundResponse.Content != nil && orderedmap.Len(foundResponse.Content) > 0 { // content type not found in the contract validationErrors = append(validationErrors, errors.ResponseContentTypeNotFound(operation, request, response, codeStr, false)) } } } } else { // no code match, check for default response if operation.Responses.Default != nil && operation.Responses.Default.Content != nil { // check content type has been defined in the contract if mediaType, ok := operation.Responses.Default.Content.Get(mediaTypeSting); ok { foundResponse = operation.Responses.Default validationErrors = append(validationErrors, v.checkResponseSchema(request, response, contentType, mediaType)...) } else { // check that the operation *actually* returns a body. (i.e. a 204 response) if operation.Responses.Default.Content != nil && orderedmap.Len(operation.Responses.Default.Content) > 0 { // content type not found in the contract validationErrors = append(validationErrors, errors.ResponseContentTypeNotFound(operation, request, response, codeStr, true)) } } } else { // TODO: add support for '2XX' and '3XX' responses in the contract // no default, no code match, nothing! validationErrors = append(validationErrors, errors.ResponseCodeNotFound(operation, request, httpCode)) } } if foundResponse != nil { // check for headers in the response if foundResponse.Headers != nil { if ok, hErrs := ValidateResponseHeaders(request, response, foundResponse.Headers, pathFound, codeStr, config.WithExistingOpts(v.options)); !ok { validationErrors = append(validationErrors, hErrs...) } } } errors.PopulateValidationErrors(validationErrors, request, pathFound) if len(validationErrors) > 0 { return false, validationErrors } return true, nil } func (v *responseBodyValidator) checkResponseSchema( request *http.Request, response *http.Response, contentType string, mediaType *v3.MediaType, ) []*errors.ValidationError { var validationErrors []*errors.ValidationError if mediaType.Schema == nil { return validationErrors } // currently, we can only validate JSON, XML and URL Encoded based responses, so check for the presence // of 'json' (what ever it may be) and for XML/URLEncoded content type so we can perform a schema check on it. // anything other than JSON XML, or URL Encoded will be ignored. isXml := schema_validation.IsXMLContentType(contentType) isUrlEncoded := schema_validation.IsURLEncodedContentType(contentType) isJson := strings.Contains(strings.ToLower(contentType), helpers.JSONType) xmlValid := isXml && v.options.AllowXMLBodyValidation urlEncodedValid := isUrlEncoded && v.options.AllowURLEncodedBodyValidation if !isJson && !xmlValid && !urlEncodedValid { return validationErrors } schema := mediaType.Schema.Schema() if !isJson { if response != nil && response.Body != http.NoBody { responseBody, _ := io.ReadAll(response.Body) _ = response.Body.Close() stringedBody := string(responseBody) var jsonBody any var prevalidationErrors []*errors.ValidationError switch { case xmlValid: jsonBody, prevalidationErrors = schema_validation.TransformXMLToSchemaJSON(stringedBody, schema) case urlEncodedValid: jsonBody, prevalidationErrors = schema_validation.TransformURLEncodedToSchemaJSON(stringedBody, schema, mediaType.Encoding) } if len(prevalidationErrors) > 0 { return prevalidationErrors } transformedBytes, err := json.Marshal(jsonBody) if err != nil { switch { case isXml: return []*errors.ValidationError{errors.InvalidXMLParsing(err.Error(), stringedBody)} case isUrlEncoded: return []*errors.ValidationError{errors.InvalidURLEncodedParsing(err.Error(), stringedBody)} } } response.Body = io.NopCloser(bytes.NewBuffer(transformedBytes)) } } // Validate response schema valid, vErrs := ValidateResponseSchema(&ValidateResponseSchemaInput{ Request: request, Response: response, Schema: schema, Version: helpers.VersionToFloat(v.document.Version), Options: []config.Option{config.WithExistingOpts(v.options)}, }) if !valid { validationErrors = append(validationErrors, vErrs...) } return validationErrors } libopenapi-validator-0.13.8/responses/validate_body_test.go000066400000000000000000001443521520534042400241350ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package responses import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" ) type validateResponseTestBed struct { responseBodyValidator ResponseBodyValidator httpTestServer *httptest.Server responseHandlerFunc http.HandlerFunc } func newvalidateResponseTestBed( t *testing.T, openApiSpec []byte, ) *validateResponseTestBed { doc, err := libopenapi.NewDocument(openApiSpec) if err != nil { t.Fatalf("failed to create openapi document: %v", err) } m, buildV3ModelErr := doc.BuildV3Model() if buildV3ModelErr != nil { t.Fatalf("failed to build v3 model: %v", err) } tb := validateResponseTestBed{responseBodyValidator: NewResponseBodyValidator(&m.Model, config.WithXmlBodyValidation(), config.WithURLEncodedBodyValidation())} tb.httpTestServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if tb.responseHandlerFunc != nil { tb.responseHandlerFunc(w, r) return } w.WriteHeader(http.StatusOK) })) t.Cleanup(func() { tb.httpTestServer.Close() }) return &tb } func (tb *validateResponseTestBed) makeRequestWithReponse( t *testing.T, method string, path string, responseHandler http.HandlerFunc, ) ( *http.Request, *http.Response, ) { tb.responseHandlerFunc = responseHandler req, err := http.NewRequestWithContext(context.TODO(), method, tb.httpTestServer.URL+path, nil) if err != nil { t.Fatalf("failed to create http request: %v", err) } res, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("failed to perform http request: %v", err) } return req, res } func TestValidateBody_MissingContentType(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, "cheeky/monkey") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"name":"Big Mac","patties":false,"vegetarian":2}`)) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST / 200 operation response content type 'cheeky/monkey' does not exist", errors[0].Message) assert.Equal(t, "The content type is invalid, Use one of the 1 supported types for this operation: application/json", errors[0].HowToFix) assert.Equal(t, req.Method, errors[0].RequestMethod) assert.Equal(t, req.URL.Path, errors[0].RequestPath) assert.Equal(t, "/burgers/createBurger", errors[0].SpecPath) } func TestValidateBody_MissingContentType4XX(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: 4XX: content: application/json: schema: type: object properties: error: type: string`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { bodyBytes, err := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, }) require.NoError(t, err, "failed to marshal body") w.Header().Set(helpers.ContentTypeHeader, "cheeky/monkey") w.WriteHeader(http.StatusBadRequest) _, _ = w.Write(bodyBytes) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST / 4XX operation response content type 'cheeky/monkey' does not exist", errors[0].Message) assert.Equal(t, "The content type is invalid, Use one of the 1 supported types for this operation: application/json", errors[0].HowToFix) assert.Equal(t, req.Method, errors[0].RequestMethod) assert.Equal(t, req.URL.Path, errors[0].RequestPath) assert.Equal(t, "/burgers/createBurger", errors[0].SpecPath) } func TestValidateBody_MissingPath(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/I do not exist", func(w http.ResponseWriter, r *http.Request) { bodyBytes, err := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, }) require.NoError(t, err, "failed to marshal body") w.Header().Set(helpers.ContentTypeHeader, "cheeky/monkey") // won't even matter! w.WriteHeader(http.StatusUnprocessableEntity) // does not matter. _, _ = w.Write(bodyBytes) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST Path '/I do not exist' not found", errors[0].Message) assert.Equal(t, req.Method, errors[0].RequestMethod) assert.Equal(t, req.URL.Path, errors[0].RequestPath) assert.Equal(t, "", errors[0].SpecPath) } func TestValidateBody_SetPath(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/I do not exist", func(w http.ResponseWriter, r *http.Request) { bodyBytes, err := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, }) require.NoError(t, err, "failed to marshal body") w.Header().Set(helpers.ContentTypeHeader, "cheeky/monkey") // won't even matter! w.WriteHeader(http.StatusUnprocessableEntity) // does not matter. _, _ = w.Write(bodyBytes) }, ) // preset the path m := tb.responseBodyValidator.(*responseBodyValidator).document path, _, pv := paths.FindPath(req, m, nil) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBodyWithPathItem(req, res, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST Path '/I do not exist' not found", errors[0].Message) } func TestValidateBody_SetPath_missing_operation(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { bodyBytes, err := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": false, }) require.NoError(t, err, "failed to marshal body") w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) // won't even matter! w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) }, ) // preset the path m := tb.responseBodyValidator.(*responseBodyValidator).document path, _, pv := paths.FindPath(req, m, nil) // Create a different request with GET method to test missing operation request2, _ := http.NewRequest(http.MethodGet, req.URL.String(), nil) request2.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBodyWithPathItem(request2, res, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "GET operation request content type 'GET' does not exist", errors[0].Message) } func TestValidateBody_MissingStatusCode(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { bodyBytes, err := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, }) require.NoError(t, err, "failed to marshal body") w.Header().Set(helpers.ContentTypeHeader, "cheeky/monkey") // won't even matter! w.WriteHeader(http.StatusUnprocessableEntity) // undefined in the spec. _, _ = w.Write(bodyBytes) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST operation request response code '422' does not exist", errors[0].Message) assert.Equal(t, "The service is responding with a code that is not defined in the spec, fix the service or add the code to the specification", errors[0].HowToFix) } func TestValidateBody_InvalidBasicSchema(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { // mix up the primitives to fire two schema violations. bodyBytes, err := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, }) require.NoError(t, err, "failed to marshal body") w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) // doubletap to hit cache _, _ = tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestValidateBody_NoBody(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) // Don't write anything - this creates a response with no body }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) // doubletap to hit cache _, _ = tb.responseBodyValidator.ValidateResponseBody(req, res) // With the real HTTP server, an empty body is now properly detected assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST response object is missing for '/burgers/createBurger'", errors[0].Message) } func TestValidateBody_InvalidResponseBodyNil(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) // Don't write anything - this creates a response with no body }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) // doubletap to hit cache _, _ = tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) require.Len(t, errors, 1) assert.ErrorContains(t, errors[0], "response object is missing") } func TestValidateBody_InvalidResponseBodyError(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) // Don't write anything - this creates a response with no body }, ) // simulate an error reading the body res.Body = &errorReader{} // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) // doubletap to hit cache _, _ = tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) require.Len(t, errors, 1) assert.ErrorContains(t, errors[0], "The response body cannot be decoded: some io error") } func TestValidateBody_InvalidBasicSchema_SetPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewResponseBodyValidator(&m.Model) // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", "patties": false, "vegetarian": 2, } bodyBytes, _ := json.Marshal(body) // build a request request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) // simulate a request/response res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // record response response := res.Result() // preset the path path, _, pv := paths.FindPath(request, &m.Model, nil) // validate! valid, errors := v.ValidateResponseBodyWithPathItem(request, response, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) assert.Equal(t, "200 response body for '/burgers/createBurger' failed to validate schema", errors[0].Message) } func TestValidateBody_ValidComplexSchema(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: $ref: '#/components/schema_validation/TestBody' components: schema_validation: Uncooked: type: object required: [uncookedWeight, uncookedHeight] properties: uncookedWeight: type: number uncookedHeight: type: number Cooked: type: object required: [usedOil, usedAnimalFat] properties: usedOil: type: boolean usedAnimalFat: type: boolean Nutrients: type: object required: [fat, salt, meat] properties: fat: type: number salt: type: number meat: type: string enum: - beef - pork - lamb - vegetables TestBody: type: object oneOf: - $ref: '#/components/schema_validation/Uncooked' - $ref: '#/components/schema_validation/Cooked' allOf: - $ref: '#/components/schema_validation/Nutrients' properties: name: type: string patties: type: integer vegetarian: type: boolean required: [name, patties, vegetarian]`, ), ) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, "fat": 10.0, "salt": 0.5, "meat": "beef", "usedOil": true, "usedAnimalFat": false, } bodyBytes, _ := json.Marshal(body) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_InvalidComplexSchema(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: $ref: '#/components/schema_validation/TestBody' components: schema_validation: Uncooked: type: object required: [uncookedWeight, uncookedHeight] properties: uncookedWeight: type: number uncookedHeight: type: number Cooked: type: object required: [usedOil, usedAnimalFat] properties: usedOil: type: boolean usedAnimalFat: type: boolean Nutrients: type: object required: [fat, salt, meat] properties: fat: type: number salt: type: number meat: type: string enum: - beef - pork - lamb - vegetables TestBody: type: object oneOf: - $ref: '#/components/schema_validation/Uncooked' - $ref: '#/components/schema_validation/Cooked' allOf: - $ref: '#/components/schema_validation/Nutrients' properties: name: type: string patties: type: integer vegetarian: type: boolean required: [name, patties, vegetarian]`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { bodyBytes, err := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, "fat": 10.0, "salt": 0.5, "meat": "beef", "usedOil": 12345, // invalid, should be bool "usedAnimalFat": false, }) require.NoError(t, err, "failed to marshal body") w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) assert.Equal(t, "missing properties 'uncookedWeight', 'uncookedHeight'", errors[0].SchemaValidationErrors[0].Reason) } func TestValidateBody_ValidBasicSchema(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { bodyBytes, err := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": false, }) require.NoError(t, err, "failed to marshal body") w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_ValidBasicSchema_WithFullContentTypeHeader(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { bodyBytes, err := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": false, }) require.NoError(t, err, "failed to marshal body") // inject a full content type header, including charset and boundary w.Header().Set(helpers.ContentTypeHeader, fmt.Sprintf("%s; charset=utf-8; boundary=---12223344", helpers.JSONContentType)) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_ValidBasicSchemaUsingDefault(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: default: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { bodyBytes, err := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": false, }) require.NoError(t, err, "failed to marshal body") w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_InvalidBasicSchemaUsingDefault_MissingContentType(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: default: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { // primitives are now correct. bodyBytes, err := json.Marshal(map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": false, }) require.NoError(t, err, "failed to marshal body") w.Header().Set(helpers.ContentTypeHeader, "chicken/nuggets;chicken=soup") w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST / 200 operation response content type 'chicken/nuggets' does not exist", errors[0].Message) } func TestValidateBody_InvalidSchemaMultiple(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: array items: type: object required: - name properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { bodyBytes, err := json.Marshal([]map[string]interface{}{ { "patties": 1, "vegetarian": true, }, { "name": "Quarter Pounder", "patties": true, "vegetarian": false, }, { "name": "Big Mac", "patties": 2, "vegetarian": false, }, }) require.NoError(t, err, "failed to marshal body") w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) assert.Equal(t, "200 response body for '/burgers/createBurger' failed to validate schema", errors[0].Message) } func TestValidateBody_EmptyContentType_Valid(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: "3.0.0" info: title: Healthcheck version: '0.1.0' paths: /health: get: responses: '200': description: pet response content: {}`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodGet, "/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(nil) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_InvalidBodyJSON(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 paths: /burgers/createBurger: post: responses: default: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodPost, "/burgers/createBurger", func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("{\"bad\": \"json\",}")) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST response body for '/burgers/createBurger' failed to validate schema", errors[0].Message) assert.Nil(t, errors[0].SchemaValidationErrors) assert.Contains(t, errors[0].Reason, "cannot be decoded") } func TestValidateBody_NoContentType_Valid(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: "3.0.0" info: title: Healthcheck version: '0.1.0' paths: /health: get: responses: '200': description: pet response`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodGet, "/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(nil) }, ) // validate! valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.True(t, valid) assert.Len(t, errors, 0) } // https://github.com/pb33f/libopenapi-validator/issues/107 // https://github.com/pb33f/libopenapi-validator/issues/103 func TestNewValidator_TestCircularRefsInValidation_Response(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(`openapi: 3.1.0 info: title: Panic at response validation version: 1.0.0 paths: /operations: delete: description: Delete operations responses: default: description: Any response content: application/json: schema: $ref: '#/components/schemas/Error' components: schemas: Error: type: object properties: code: type: string details: type: array items: $ref: '#/components/schemas/Error'`, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodDelete, "/operations", func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(nil) }, ) valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) assert.Len(t, errors, 1) // The error message may vary depending on whether the circular reference is caught // during rendering or compilation, so we check for either pattern assert.True(t, strings.Contains(errors[0].Reason, "circular reference") || strings.Contains(errors[0].Reason, "json-pointer") || strings.Contains(errors[0].Reason, "not found"), "Expected error about circular reference or JSON pointer not found, got: %s", errors[0].Reason) } func TestValidateResponseBody_XMLMarshalError(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(` openapi: 3.1.0 info: title: Test Spec version: 1.0.0 paths: /test: get: responses: '200': description: Success content: application/xml: schema: type: object properties: bad_number: type: number `, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodGet, "/test", func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, "application/xml") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("NaN")) }, ) valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, errors[0].Message, "xml example is malformed") } func TestValidateResponseBody_URLEncodedMarshalError(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(` openapi: 3.1.0 info: title: Test Spec version: 1.0.0 paths: /test: get: responses: '200': description: Success content: application/x-www-form-urlencoded: schema: type: object properties: bad_number: type: number `, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodGet, "/test", func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.URLEncodedContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("bad_number=NaN")) }, ) valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, errors[0].Message, "Unable to parse form-urlencoded body") } func TestValidateResponseBody_NilSchema(t *testing.T) { tb := newvalidateResponseTestBed( t, []byte(` openapi: 3.1.0 info: title: Test Spec version: 1.0.0 paths: /test: get: responses: '200': description: Success content: application/json: {} `, ), ) req, res := tb.makeRequestWithReponse( t, http.MethodGet, "/test", func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(nil) }, ) valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_CheckHeader(t *testing.T) { spec := `openapi: "3.0.0" info: title: Healthcheck version: '0.1.0' paths: /health: get: responses: '200': headers: chicken-nuggets: description: chicken nuggets response required: true schema: type: integer description: pet response` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewResponseBodyValidator(&m.Model) // build a request request, _ := http.NewRequest(http.MethodGet, "https://things.com/health", nil) // simulate a request/response res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(nil) } // fire the request handler(res, request) // record response response := res.Result() // validate! valid, errors := v.ValidateResponseBody(request, response) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Missing required header", errors[0].Message) assert.Equal(t, "Required header 'chicken-nuggets' was not found in response", errors[0].Reason) } // TestValidateBody_ComplexRegexSchemaCompilationError tests that complex regex patterns // that cause schema compilation to fail are handled gracefully instead of causing panics func TestValidateBody_ComplexRegexSchemaCompilationError(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: responses: '200': content: application/json: schema: type: object properties: name: type: string pattern: "[\\w\\W]{1,1024}$" patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewResponseBodyValidator(&m.Model) body := map[string]interface{}{ "name": "Big Mac test", "patties": 2, "vegetarian": false, } bodyBytes, _ := json.Marshal(body) // build a request request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) // simulate a request/response res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // record response response := res.Result() // validate - this should not panic even if schema compilation fails valid, validationErrors := v.ValidateResponseBody(request, response) // if schema compilation failed due to complex regex, we should get a validation error instead of a panic if !valid { // verify we got a schema compilation error instead of a panic assert.NotEmpty(t, validationErrors) found := false for _, err := range validationErrors { if err.ValidationSubType == helpers.Schema && len(err.SchemaValidationErrors) == 0 { // Schema compilation errors don't have SchemaValidationFailure objects if strings.Contains(err.Reason, "failed to compile JSON schema") { found = true assert.Contains(t, err.Reason, "failed to compile JSON schema") assert.Contains(t, err.HowToFix, "complex regex patterns") break } } } if !found { // if it didn't fail compilation, it should have succeeded t.Logf("Schema compilation succeeded, validation result: %v", valid) } } else { // schema compiled and validated successfully assert.True(t, valid) assert.Empty(t, validationErrors) } } func TestValidateBody_StrictMode_UndeclaredProperty(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/getBurger: get: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewResponseBodyValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/getBurger", nil) // Response with undeclared property 'extra' responseBody := `{"name": "Big Mac", "patties": 2, "extra": "undeclared"}` response := &http.Response{ Header: http.Header{}, StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(responseBody)), } response.Header.Set("Content-Type", "application/json") valid, errs := v.ValidateResponseBody(request, response) assert.False(t, valid) assert.Len(t, errs, 1) assert.Contains(t, errs[0].Message, "extra") assert.Contains(t, errs[0].Message, "not declared") } func TestValidateBody_StrictMode_ValidResponse(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/getBurger: get: responses: '200': content: application/json: schema: type: object properties: name: type: string patties: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewResponseBodyValidator(&m.Model, config.WithStrictMode()) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/getBurger", nil) // Response with only declared properties responseBody := `{"name": "Big Mac", "patties": 2}` response := &http.Response{ Header: http.Header{}, StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(responseBody)), } response.Header.Set("Content-Type", "application/json") valid, errs := v.ValidateResponseBody(request, response) assert.True(t, valid) assert.Len(t, errs, 0) } func TestValidateBody_StrictMode_WriteOnlyProperty(t *testing.T) { spec := `openapi: 3.1.0 paths: /users/123: get: responses: '200': content: application/json: schema: type: object properties: name: type: string password: type: string writeOnly: true` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewResponseBodyValidator(&m.Model, config.WithStrictMode(), config.WithStrictRejectWriteOnly(), ) request, _ := http.NewRequest(http.MethodGet, "https://things.com/users/123", nil) responseBody := `{"name": "John", "password": "secret"}` response := &http.Response{ Header: http.Header{}, StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(responseBody)), } response.Header.Set("Content-Type", "application/json") valid, errs := v.ValidateResponseBody(request, response) assert.False(t, valid) assert.Len(t, errs, 1) assert.Contains(t, errs[0].Message, "writeOnly") assert.Contains(t, errs[0].Message, "password") } func TestValidateBody_ValidURLEncodedBody(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: responses: default: content: application/x-www-form-urlencoded: schema: type: object properties: name: type: string patties: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewResponseBodyValidator(&m.Model, config.WithURLEncodedBodyValidation()) body := "name=test&patties=2" request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body))) request.Header.Set(helpers.ContentTypeHeader, helpers.URLEncodedContentType) res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.URLEncodedContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(body)) } handler(res, request) response := res.Result() valid, errors := v.ValidateResponseBody(request, response) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_InvalidURLEncoded(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: responses: default: content: application/x-www-form-urlencoded: schema: type: object properties: name: type: string patties: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewResponseBodyValidator(&m.Model, config.WithURLEncodedBodyValidation()) body := "name=test&patties=true" request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body))) request.Header.Set(helpers.ContentTypeHeader, helpers.URLEncodedContentType) res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.URLEncodedContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(body)) } handler(res, request) response := res.Result() valid, errors := v.ValidateResponseBody(request, response) assert.False(t, valid) assert.Len(t, errors, 1) } func TestValidateBody_ValidXmlDecode(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: responses: default: content: application/xml: schema: type: object properties: name: type: string patties: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewResponseBodyValidator(&m.Model, config.WithXmlBodyValidation()) body := "test2" // build a request request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body))) request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) // simulate a request/response res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, "application/xml") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(body)) } // fire the request handler(res, request) // record response response := res.Result() // validate! valid, errors := v.ValidateResponseBody(request, response) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_ValidXmlFailedValidation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: responses: default: content: application/xml: schema: type: object properties: name: type: string patties: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewResponseBodyValidator(&m.Model, config.WithXmlBodyValidation()) body := "20text" // build a request request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body))) request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) // simulate a request/response res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, "application/xml") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(body)) } // fire the request handler(res, request) // record response response := res.Result() // validate! valid, errors := v.ValidateResponseBody(request, response) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 1) } func TestValidateBody_IgnoreXmlValidation(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: responses: default: content: application/xml: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewResponseBodyValidator(&m.Model) body := "invalidbodycausenoxml" // build a request request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body))) request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) // simulate a request/response res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, "application/xml") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(body)) } // fire the request handler(res, request) // record response response := res.Result() // validate! valid, errors := v.ValidateResponseBody(request, response) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateBody_InvalidXmlParse(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: responses: default: content: application/xml: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() v := NewResponseBodyValidator(&m.Model, config.WithXmlBodyValidation()) body := "" // build a request request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body))) request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) // simulate a request/response res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, "application/xml") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(body)) } // fire the request handler(res, request) // record response response := res.Result() // validate! valid, errors := v.ValidateResponseBody(request, response) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "xml example is malformed", errors[0].Message) } func TestValidateResponseBodyWithPathItem_NilResponses(t *testing.T) { // Operation with nil Responses — should not panic (fix for wiretap #134) spec := []byte(`openapi: 3.1.0 paths: /test: get: responses: '200': description: ok `) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) m, errs := doc.BuildV3Model() require.Empty(t, errs) v := NewResponseBodyValidator(&m.Model) request, _ := http.NewRequest(http.MethodGet, "/test", nil) response := &http.Response{ StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(bytes.NewBufferString("")), } // Should not panic even with no content in the response definition valid, validationErrors := v.ValidateResponseBodyWithPathItem(request, response, m.Model.Paths.PathItems.GetOrZero("/test"), "/test") assert.True(t, valid) assert.Empty(t, validationErrors) } type errorReader struct{} func (er *errorReader) Read(p []byte) (n int, err error) { return 0, errors.New("some io error") } func (er *errorReader) Close() error { return nil } libopenapi-validator-0.13.8/responses/validate_headers.go000066400000000000000000000101571520534042400235470ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package responses import ( "fmt" "net/http" "strings" "github.com/pb33f/libopenapi/orderedmap" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/parameters" "github.com/pb33f/libopenapi-validator/strict" ) // ValidateResponseHeaders validates the response headers against the OpenAPI spec. func ValidateResponseHeaders( request *http.Request, response *http.Response, headers *orderedmap.Map[string, *v3.Header], pathTemplate string, statusCode string, opts ...config.Option, ) (bool, []*errors.ValidationError) { options := config.NewValidationOptions(opts...) // locate headers type headerPair struct { name string value []string model *v3.Header } locatedHeaders := make(map[string]headerPair) var validationErrors []*errors.ValidationError // iterate through the response headers for name, v := range response.Header { // check if the model is in the spec for pair := headers.First(); pair != nil; pair = pair.Next() { k := pair.Key() header := pair.Value() if strings.EqualFold(k, name) { locatedHeaders[strings.ToLower(name)] = headerPair{ name: k, value: v, model: header, } } } } // determine if any required headers are missing from the response for pair := headers.First(); pair != nil; pair = pair.Next() { name := pair.Key() header := pair.Value() if header.Required { if _, ok := locatedHeaders[strings.ToLower(name)]; !ok { keywordLocation := helpers.ConstructResponseHeaderJSONPointer(pathTemplate, request.Method, statusCode, name, "required") specLine, specCol := 1, 0 if low := header.GoLow(); low != nil && low.KeyNode != nil { specLine = low.KeyNode.Line specCol = low.KeyNode.Column } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.ParameterValidationHeader, Message: "Missing required header", Reason: fmt.Sprintf("Required header '%s' was not found in response", name), SpecLine: specLine, SpecCol: specCol, HowToFix: errors.HowToFixMissingHeader, RequestPath: request.URL.Path, RequestMethod: request.Method, SchemaValidationErrors: []*errors.SchemaValidationFailure{{ Reason: fmt.Sprintf("Required header '%s' is missing", name), FieldName: name, InstancePath: []string{name}, KeywordLocation: keywordLocation, }}, }) } } } // validate the model schemas if they are set. for h, header := range locatedHeaders { if header.model.Schema != nil { schema := header.model.Schema.Schema() if schema != nil && header.model.Required { for _, headerValue := range header.value { validationErrors = append(validationErrors, parameters.ValidateParameterSchema(schema, nil, headerValue, "header", "response header", h, helpers.ResponseBodyValidation, lowv3.HeadersLabel, options)...) } } } } if len(validationErrors) > 0 { return false, validationErrors } // strict mode: check for undeclared response headers if options.StrictMode { // convert orderedmap to regular map for strict validation declaredMap := make(map[string]*v3.Header) for name, header := range headers.FromOldest() { declaredMap[name] = header } undeclaredHeaders := strict.ValidateResponseHeaders(response.Header, &declaredMap, options) for _, undeclared := range undeclaredHeaders { validationErrors = append(validationErrors, errors.UndeclaredHeaderError( undeclared.Name, undeclared.Value.(string), undeclared.DeclaredProperties, undeclared.Direction.String(), request.URL.Path, request.Method, )) } } if len(validationErrors) > 0 { return false, validationErrors } return true, nil } libopenapi-validator-0.13.8/responses/validate_headers_test.go000066400000000000000000000137231520534042400246100ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package responses import ( "net/http" "net/http/httptest" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "github.com/pb33f/libopenapi-validator/config" ) func TestValidateResponseHeaders(t *testing.T) { spec := `openapi: "3.0.0" info: title: Healthcheck version: '0.1.0' paths: /health: get: responses: '200': headers: chicken-nuggets: description: chicken nuggets response required: true schema: type: integer description: pet response` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // build a request request, _ := http.NewRequest(http.MethodGet, "https://things.com/health", nil) // simulate a request/response res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Chicken-Cakes", "I should fail") w.WriteHeader(http.StatusOK) _, _ = w.Write(nil) } // fire the request handler(res, request) // record response response := res.Result() headers := m.Model.Paths.PathItems.GetOrZero("/health").Get.Responses.Codes.GetOrZero("200").Headers // validate! valid, errors := ValidateResponseHeaders(request, response, headers, "/health", "200") assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, errors[0].Message, "Missing required header") assert.Equal(t, errors[0].Reason, "Required header 'chicken-nuggets' was not found in response") res = httptest.NewRecorder() handler = func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Chicken-Nuggets", "I should fail") w.WriteHeader(http.StatusOK) _, _ = w.Write(nil) } // fire the request handler(res, request) response = res.Result() headers = m.Model.Paths.PathItems.GetOrZero("/health").Get.Responses.Codes.GetOrZero("200").Headers // validate! valid, errors = ValidateResponseHeaders(request, response, headers, "/health", "200") assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, errors[0].Message, "header 'chicken-nuggets' failed to validate") assert.Equal(t, errors[0].Reason, "response header 'chicken-nuggets' is defined as an integer, however it failed to pass a schema validation") } func TestValidateResponseHeaders_Valid(t *testing.T) { spec := `openapi: "3.0.0" info: title: Healthcheck version: '0.1.0' paths: /health: get: responses: '200': headers: chicken-nuggets: description: chicken nuggets response required: false schema: type: integer description: pet response` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // build a request request, _ := http.NewRequest(http.MethodGet, "https://things.com/health", nil) // simulate a request/response res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Chicken-Cakes", "I should fail") w.WriteHeader(http.StatusOK) _, _ = w.Write(nil) } // fire the request handler(res, request) response := res.Result() headers := m.Model.Paths.PathItems.GetOrZero("/health").Get.Responses.Codes.GetOrZero("200").Headers // validate! valid, errors := ValidateResponseHeaders(request, response, headers, "/health", "200") assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateResponseHeaders_StrictMode(t *testing.T) { spec := `openapi: "3.0.0" info: title: Healthcheck version: '0.1.0' paths: /health: get: responses: '200': headers: x-request-id: description: request ID required: false schema: type: string description: healthy response` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() // build a request request, _ := http.NewRequest(http.MethodGet, "https://things.com/health", nil) // simulate a response with an undeclared header res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Request-Id", "abc-123") w.Header().Set("X-Undeclared-Header", "should fail in strict mode") w.WriteHeader(http.StatusOK) } handler(res, request) response := res.Result() headers := m.Model.Paths.PathItems.GetOrZero("/health").Get.Responses.Codes.GetOrZero("200").Headers // validate with strict mode - should find undeclared header valid, errors := ValidateResponseHeaders(request, response, headers, "/health", "200", config.WithStrictMode()) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "X-Undeclared-Header") assert.Contains(t, errors[0].Message, "not declared") } func TestValidateResponseHeaders_StrictMode_NoUndeclared(t *testing.T) { spec := `openapi: "3.0.0" info: title: Healthcheck version: '0.1.0' paths: /health: get: responses: '200': headers: x-request-id: description: request ID required: false schema: type: string description: healthy response` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() request, _ := http.NewRequest(http.MethodGet, "https://things.com/health", nil) // response with only declared headers (x-request-id is declared, Content-Type is default-ignored) res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Request-Id", "abc-123") w.WriteHeader(http.StatusOK) } handler(res, request) response := res.Result() headers := m.Model.Paths.PathItems.GetOrZero("/health").Get.Responses.Codes.GetOrZero("200").Headers // validate with strict mode - should pass (no undeclared headers) valid, errors := ValidateResponseHeaders(request, response, headers, "/health", "200", config.WithStrictMode()) assert.True(t, valid) assert.Len(t, errors, 0) } libopenapi-validator-0.13.8/responses/validate_response.go000066400000000000000000000340651520534042400237760ustar00rootroot00000000000000// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package responses import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "reflect" "regexp" "strconv" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/utils" "github.com/santhosh-tekuri/jsonschema/v6" "go.yaml.in/yaml/v4" "golang.org/x/text/language" "golang.org/x/text/message" "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi-validator/config" liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/schema_validation" "github.com/pb33f/libopenapi-validator/strict" ) var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`) // ValidateResponseSchemaInput contains parameters for response schema validation. type ValidateResponseSchemaInput struct { Request *http.Request // Required: The HTTP request (for context) Response *http.Response // Required: The HTTP response to validate Schema *base.Schema // Required: The OpenAPI schema to validate against Version float32 // Required: OpenAPI version (3.0 or 3.1) Options []config.Option // Optional: Functional options (defaults applied if empty/nil) } // ValidateResponseSchema will validate the response body for a http.Response pointer. The request is used to // locate the operation in the specification, the response is used to ensure the response code, media type and the // schema of the response body are valid. // // This function is used by the ValidateResponseBody function, but can be used independently. // The schema will be compiled from cache if available, otherwise it will be compiled and cached. func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*liberrors.ValidationError) { validationOptions := config.NewValidationOptions(input.Options...) var validationErrors []*liberrors.ValidationError var renderedSchema, jsonSchema []byte var referenceSchema string var compiledSchema *jsonschema.Schema var cachedNode *yaml.Node if input.Schema == nil { return false, []*liberrors.ValidationError{{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, Message: "schema is nil", Reason: "The schema to validate against is nil", }} } else if input.Schema.GoLow() == nil { return false, []*liberrors.ValidationError{{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, Message: "schema cannot be rendered", Reason: "The schema does not have low-level information and cannot be rendered. Please ensure the schema is loaded from a document.", }} } if validationOptions.SchemaCache != nil { hash := input.Schema.GoLow().Hash() if cached, ok := validationOptions.SchemaCache.Load(hash); ok && cached != nil && cached.CompiledSchema != nil { renderedSchema = cached.RenderedInline referenceSchema = cached.ReferenceSchema compiledSchema = cached.CompiledSchema cachedNode = cached.RenderedNode } } // Cache miss or no cache - render and compile if compiledSchema == nil { renderCtx := base.NewInlineRenderContextForValidation() var renderErr error renderedSchema, renderErr = input.Schema.RenderInlineWithContext(renderCtx) referenceSchema = string(renderedSchema) // If rendering failed (e.g., circular reference), return the render error if renderErr != nil { violation := &liberrors.SchemaValidationFailure{ Reason: renderErr.Error(), ReferenceSchema: referenceSchema, } validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%d response body for '%s' failed schema rendering", input.Response.StatusCode, input.Request.URL.Path), Reason: fmt.Sprintf("The response schema for status code '%d' failed to render: %s", input.Response.StatusCode, renderErr.Error()), SpecLine: 1, SpecCol: 0, SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation}, HowToFix: "check the response schema for circular references or invalid structures", Context: referenceSchema, }) return false, validationErrors } jsonSchema, _ = utils.ConvertYAMLtoJSON(renderedSchema) var err error schemaName := fmt.Sprintf("%x", input.Schema.GoLow().Hash()) compiledSchema, err = helpers.NewCompiledSchemaWithVersion( schemaName, jsonSchema, validationOptions, input.Version, ) if err != nil { validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%d response body for '%s' failed schema compilation", input.Response.StatusCode, input.Request.URL.Path), Reason: fmt.Sprintf("The response schema for status code '%d' failed to compile: %s", input.Response.StatusCode, err.Error()), SpecLine: 1, SpecCol: 0, HowToFix: "check the response schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", Context: input.Schema, }) return false, validationErrors } if validationOptions.SchemaCache != nil { hash := input.Schema.GoLow().Hash() validationOptions.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ Schema: input.Schema, RenderedInline: renderedSchema, ReferenceSchema: referenceSchema, RenderedJSON: jsonSchema, CompiledSchema: compiledSchema, }) } } request := input.Request response := input.Response schema := input.Schema if response == nil || response.Body == http.NoBody { // skip response body validation for head request after processing schema if response != nil && request != nil && request.Method == http.MethodHead { return len(validationErrors) == 0, validationErrors } // cannot decode the response body, so it's not valid validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: "response", ValidationSubType: "object", Message: fmt.Sprintf("%s response object is missing for '%s'", request.Method, request.URL.Path), Reason: "The response object is completely missing", SpecLine: 1, SpecCol: 0, HowToFix: "ensure response object has been set", Context: schema, }) return false, validationErrors } responseBody, ioErr := io.ReadAll(response.Body) if ioErr != nil { // cannot decode the response body, so it's not valid validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s response body for '%s' cannot be read, it's empty or malformed", request.Method, request.URL.Path), Reason: fmt.Sprintf("The response body cannot be decoded: %s", ioErr.Error()), SpecLine: 1, SpecCol: 0, HowToFix: "ensure body is not empty", Context: schema, }) return false, validationErrors } // close the request body, so it can be re-read later by another player in the chain _ = response.Body.Close() response.Body = io.NopCloser(bytes.NewBuffer(responseBody)) var decodedObj interface{} if len(responseBody) > 0 { // Per RFC7231, a response to a HEAD request MUST NOT include a message body. if request != nil && request.Method == http.MethodHead { violation := &liberrors.SchemaValidationFailure{ Reason: "HEAD responses must not include a message body", ReferenceObject: string(responseBody), ReferenceSchema: referenceSchema, } validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s response for '%s' must not include a body", request.Method, request.URL.Path), Reason: "The response to a HEAD request must not contain a body", SpecLine: 1, SpecCol: 0, SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation}, HowToFix: "ensure no response body is present for HEAD requests", Context: referenceSchema, }) return false, validationErrors } err := json.Unmarshal(responseBody, &decodedObj) if err != nil { // cannot decode the response body, so it's not valid validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s response body for '%s' failed to validate schema", request.Method, request.URL.Path), Reason: fmt.Sprintf("The response body cannot be decoded: %s", err.Error()), SpecLine: 1, SpecCol: 0, HowToFix: liberrors.HowToFixInvalidSchema, Context: schema, }) return false, validationErrors } } // no response body? failed to decode anything? nothing to do here. if responseBody == nil || decodedObj == nil { return true, nil } // validate the object against the schema scErrs := compiledSchema.Validate(decodedObj) if scErrs != nil { var jk *jsonschema.ValidationError var schemaValidationErrors []*liberrors.SchemaValidationFailure if errors.As(scErrs, &jk) { // flatten the validationErrors schFlatErrs := jk.BasicOutput().Errors renderedNode := cachedNode if renderedNode == nil { renderedNode = new(yaml.Node) _ = yaml.Unmarshal(renderedSchema, renderedNode) } for q := range schFlatErrs { er := schFlatErrs[q] errMsg := er.Error.Kind.LocalizedString(message.NewPrinter(language.Tag{})) if er.KeywordLocation == "" || helpers.IgnoreRegex.MatchString(errMsg) { continue // ignore this error, it's useless tbh, utter noise. } if er.Error != nil { // locate the violated property in the schema var located *yaml.Node if len(renderedNode.Content) > 0 { located = schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation) } // extract the element specified by the instance val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation) var referenceObject string if len(val) > 0 { referenceIndex, _ := strconv.Atoi(val[1]) if reflect.ValueOf(decodedObj).Type().Kind() == reflect.Slice { found := decodedObj.([]any)[referenceIndex] recoded, _ := json.MarshalIndent(found, "", " ") referenceObject = string(recoded) } } if referenceObject == "" { referenceObject = string(responseBody) } violation := &liberrors.SchemaValidationFailure{ Reason: errMsg, FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), KeywordLocation: er.KeywordLocation, ReferenceSchema: referenceSchema, ReferenceObject: referenceObject, OriginalJsonSchemaError: jk, } // if we have a location within the schema, add it to the error if located != nil { line := located.Line // if the located node is a map or an array, then the actual human interpretable // line on which the violation occurred is the line of the key, not the value. if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode { if line > 0 { line-- } } // location of the violation within the rendered schema. violation.Line = line violation.Column = located.Column } schemaValidationErrors = append(schemaValidationErrors, violation) } } } line := 1 col := 0 if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil { line = low.Type.KeyNode.Line col = low.Type.KeyNode.Column } // add the error to the list validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%d response body for '%s' failed to validate schema", response.StatusCode, request.URL.Path), Reason: fmt.Sprintf("The response body for status code '%d' is defined as an object. "+ "However, it does not meet the schema requirements of the specification", response.StatusCode), SpecLine: line, SpecCol: col, SchemaValidationErrors: schemaValidationErrors, HowToFix: liberrors.HowToFixInvalidSchema, Context: schema, }) } if len(validationErrors) > 0 { return false, validationErrors } // strict mode: check for undeclared properties in response body if validationOptions.StrictMode && decodedObj != nil { strictValidator := strict.NewValidator(validationOptions, input.Version) strictResult := strictValidator.Validate(strict.Input{ Schema: schema, Data: decodedObj, Direction: strict.DirectionResponse, Options: validationOptions, BasePath: "$.body", Version: input.Version, }) if !strictResult.Valid { for _, undeclared := range strictResult.UndeclaredValues { switch undeclared.Type { case strict.TypeWriteOnlyProperty: validationErrors = append(validationErrors, liberrors.WriteOnlyPropertyError( undeclared.Path, undeclared.Name, undeclared.Value, request.URL.Path, request.Method, undeclared.SpecLine, undeclared.SpecCol, )) default: validationErrors = append(validationErrors, liberrors.UndeclaredPropertyError( undeclared.Path, undeclared.Name, undeclared.Value, undeclared.DeclaredProperties, undeclared.Direction.String(), request.URL.Path, request.Method, undeclared.SpecLine, undeclared.SpecCol, )) } } } } if len(validationErrors) > 0 { return false, validationErrors } return true, nil } libopenapi-validator-0.13.8/responses/validate_response_test.go000066400000000000000000000235001520534042400250250ustar00rootroot00000000000000package responses import ( "bytes" "fmt" "io" "net/http" "strings" "testing" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pb33f/libopenapi-validator/config" ) func TestValidateResponseSchema(t *testing.T) { for name, tc := range map[string]struct { request *http.Request response *http.Response schemaYAML string version float32 assertValidResponseSchema assert.BoolAssertionFunc expectedErrorsCount int }{ "FailOnBooleanExclusiveMinimum": { request: postRequest(), response: responseWithBody(`{"exclusiveNumber": 10}`), schemaYAML: `type: object properties: exclusiveNumber: type: number description: This number starts its journey where most numbers are too scared to begin! exclusiveMinimum: true minimum: !!float 10`, version: 3.0, assertValidResponseSchema: assert.False, expectedErrorsCount: 1, }, "PassWithCorrectExclusiveMinimum": { request: postRequest(), response: responseWithBody(`{"exclusiveNumber": 15}`), schemaYAML: `type: object properties: exclusiveNumber: type: number description: This number is properly constrained by a numeric exclusive minimum. exclusiveMinimum: 12 minimum: 12`, version: 3.1, assertValidResponseSchema: assert.True, expectedErrorsCount: 0, }, "PassWithValidStringType": { request: postRequest(), response: responseWithBody(`{"greeting": "Hello, world!"}`), schemaYAML: `type: object properties: greeting: type: string description: A simple greeting example: "Hello, world!"`, version: 3.1, assertValidResponseSchema: assert.True, expectedErrorsCount: 0, }, "PassWithNullablePropertyInOpenAPI30": { request: postRequest(), response: responseWithBody(`{"name": "John", "middleName": null}`), schemaYAML: `type: object properties: name: type: string description: User's first name middleName: type: string nullable: true description: User's middle name (optional)`, version: 3.0, assertValidResponseSchema: assert.True, expectedErrorsCount: 0, }, "PassWithNullablePropertyInOpenAPI31": { request: postRequest(), response: responseWithBody(`{"name": "John", "middleName": null}`), schemaYAML: `type: object properties: name: type: string description: User's first name middleName: type: string nullable: true description: User's middle name (optional)`, version: 3.1, assertValidResponseSchema: assert.False, expectedErrorsCount: 1, }, } { t.Run(name, func(t *testing.T) { t.Parallel() schema := parseSchemaFromSpec(t, tc.schemaYAML, tc.version) valid, errors := ValidateResponseSchema(&ValidateResponseSchemaInput{ Request: tc.request, Response: tc.response, Schema: schema, Version: tc.version, }) tc.assertValidResponseSchema(t, valid) assert.Len(t, errors, tc.expectedErrorsCount) }) } } func TestBooleanExclusiveMin_ValidValue(t *testing.T) { openAPIVersion := float32(3.0) schema := parseSchemaFromSpec(t, `type: object properties: exclusiveNumber: type: number description: This number starts its journey where most numbers are too scared to begin! exclusiveMinimum: true minimum: 10`, openAPIVersion) valid, errors := ValidateResponseSchema(&ValidateResponseSchemaInput{ Request: postRequest(), Response: responseWithBody(`{"exclusiveNumber": 13}`), Schema: schema, Version: openAPIVersion, }) assert.True(t, valid) assert.Empty(t, errors) } func TestValidateResponseSchema_CachePopulation(t *testing.T) { schema := parseSchemaFromSpec(t, `type: object properties: name: type: string`, 3.1) // Create options with a cache opts := config.NewValidationOptions() require.NotNil(t, opts.SchemaCache) // First call should populate the cache valid, errors := ValidateResponseSchema(&ValidateResponseSchemaInput{ Request: postRequest(), Response: responseWithBody(`{"name": "test"}`), Schema: schema, Version: 3.1, Options: []config.Option{config.WithExistingOpts(opts)}, }) assert.True(t, valid) assert.Len(t, errors, 0) // Verify cache was populated hash := schema.GoLow().Hash() cached, ok := opts.SchemaCache.Load(hash) assert.True(t, ok, "Schema should be in cache") assert.NotNil(t, cached, "Cached entry should not be nil") assert.NotNil(t, cached.CompiledSchema, "Compiled schema should be cached") assert.NotNil(t, cached.RenderedInline, "Rendered schema should be cached") assert.NotNil(t, cached.RenderedJSON, "JSON schema should be cached") } func postRequest() *http.Request { req, _ := http.NewRequest(http.MethodPost, "/test", io.NopCloser(strings.NewReader(""))) return req } func responseWithBody(payload string) *http.Response { return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte(payload))), Header: http.Header{"Content-Type": []string{"application/json"}}, } } // parseSchemaFromSpec creates a base.Schema from an OpenAPI spec YAML string. // This ensures that we're using the native libopenapi logic for generating the schema. func parseSchemaFromSpec(t *testing.T, schemaYAML string, version float32) *base.Schema { // Convert version to API version string (3.0 -> "3.0.0", 3.1 -> "3.1.0") apiVersion := fmt.Sprintf("%.1f.0", version) spec := fmt.Sprintf(`openapi: %s info: title: Test version: 1.0.0 components: schemas: TestSchema: %s`, apiVersion, indentLines(schemaYAML, " ")) doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) model, errs := doc.BuildV3Model() require.Empty(t, errs) schema := model.Model.Components.Schemas.GetOrZero("TestSchema") require.NotNil(t, schema) return schema.Schema() } // indentLines adds the specified indentation to each line of the input string func indentLines(s string, indent string) string { lines := strings.Split(strings.TrimSpace(s), "\n") for i, line := range lines { if line != "" { lines[i] = indent + line } } return strings.Join(lines, "\n") } func TestValidateResponseSchema_NilSchema(t *testing.T) { // Test when schema is nil valid, errors := ValidateResponseSchema(&ValidateResponseSchemaInput{ Request: postRequest(), Response: responseWithBody(`{"name": "test"}`), Schema: nil, Version: 3.1, }) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "schema is nil", errors[0].Message) assert.Equal(t, "The schema to validate against is nil", errors[0].Reason) } func TestValidateResponseSchema_NilSchemaGoLow(t *testing.T) { // Test when schema.GoLow() is nil by creating a schema without low-level data schema := &base.Schema{} // Empty schema without GoLow() data valid, errors := ValidateResponseSchema(&ValidateResponseSchemaInput{ Request: postRequest(), Response: responseWithBody(`{"name": "test"}`), Schema: schema, Version: 3.1, }) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "schema cannot be rendered", errors[0].Message) assert.Contains(t, errors[0].Reason, "does not have low-level information") } func TestValidateResponseSchema_CircularReference(t *testing.T) { // Test when schema has a circular reference that causes render failure spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 components: schemas: Error: type: object properties: code: type: string details: type: array items: $ref: '#/components/schemas/Error'` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) model, errs := doc.BuildV3Model() require.Empty(t, errs) // Verify circular reference was detected require.Len(t, model.Index.GetCircularReferences(), 1) schema := model.Model.Components.Schemas.GetOrZero("Error") require.NotNil(t, schema) valid, errors := ValidateResponseSchema(&ValidateResponseSchemaInput{ Request: postRequest(), Response: responseWithBody(`{"code": "abc", "details": [{"code": "def"}]}`), Schema: schema.Schema(), Version: 3.1, }) assert.False(t, valid) require.Len(t, errors, 1) assert.Contains(t, errors[0].Message, "failed schema rendering") assert.Contains(t, errors[0].Reason, "circular reference") } func TestValidateResponseSchema_ResponseMissing(t *testing.T) { schema := parseSchemaFromSpec(t, `type: object properties: name: type: string`, 3.1) // Response body missing (NoBody) for a non-HEAD request should error valid, errs := ValidateResponseSchema(&ValidateResponseSchemaInput{ Request: postRequest(), Response: &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, Schema: schema, Version: 3.1, }) assert.False(t, valid) require.Len(t, errs, 1) assert.Contains(t, errs[0].Message, "response object is missing") } func TestValidateResponseSchema_HeadEmptySkipsValidation(t *testing.T) { schema := parseSchemaFromSpec(t, `type: object`, 3.1) req, _ := http.NewRequest(http.MethodHead, "/test", nil) resp := &http.Response{StatusCode: http.StatusOK, Body: http.NoBody} valid, errs := ValidateResponseSchema(&ValidateResponseSchemaInput{ Request: req, Response: resp, Schema: schema, Version: 3.1, }) assert.True(t, valid) assert.Len(t, errs, 0) } func TestValidateResponseSchema_HeadWithBodyFails(t *testing.T) { schema := parseSchemaFromSpec(t, `type: object`, 3.1) req, _ := http.NewRequest(http.MethodHead, "/test", nil) valid, errs := ValidateResponseSchema(&ValidateResponseSchemaInput{ Request: req, Response: responseWithBody(`{"name":"bob"}`), Schema: schema, Version: 3.1, }) assert.False(t, valid) require.Len(t, errs, 1) assert.Contains(t, errs[0].Reason, "must not contain a body") } libopenapi-validator-0.13.8/schema_validation/000077500000000000000000000000001520534042400213615ustar00rootroot00000000000000libopenapi-validator-0.13.8/schema_validation/locate_schema_property.go000066400000000000000000000025111520534042400264420ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "github.com/pb33f/jsonpath/pkg/jsonpath" "github.com/pb33f/jsonpath/pkg/jsonpath/config" "github.com/pb33f/libopenapi/utils" "go.yaml.in/yaml/v4" ) // LocateSchemaPropertyNodeByJSONPath will locate a schema property node by a JSONPath. It converts something like // #/components/schemas/MySchema/properties/MyProperty to something like $.components.schemas.MySchema.properties.MyProperty func LocateSchemaPropertyNodeByJSONPath(doc *yaml.Node, JSONPath string) *yaml.Node { _, path := utils.ConvertComponentIdIntoFriendlyPathSearch(JSONPath) return locateSchemaPropertyNode(doc, path) } func locateSchemaPropertyNode(doc *yaml.Node, path string) *yaml.Node { if path == "" { return nil } var locatedNode *yaml.Node doneChan := make(chan bool) locatedNodeChan := make(chan *yaml.Node) go func() { defer func() { if err := recover(); err != nil { doneChan <- true } }() jsonPath, _ := jsonpath.NewPath(path, config.WithLazyContextTracking()) locatedNodes := jsonPath.Query(doc) if len(locatedNodes) > 0 { locatedNode = locatedNodes[0] } locatedNodeChan <- locatedNode }() select { case locatedNode = <-locatedNodeChan: return locatedNode case <-doneChan: return nil } } libopenapi-validator-0.13.8/schema_validation/locate_schema_property_test.go000066400000000000000000000006551520534042400275100ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "testing" "github.com/stretchr/testify/assert" ) func TestLocateSchemaPropertyNodeByJSONPath_BadNode(t *testing.T) { assert.Nil(t, LocateSchemaPropertyNodeByJSONPath(nil, "")) } func TestLocateSchemaPropertyNode_EmptyPath(t *testing.T) { assert.Nil(t, locateSchemaPropertyNode(nil, "")) } libopenapi-validator-0.13.8/schema_validation/openapi_schemas/000077500000000000000000000000001520534042400245175ustar00rootroot00000000000000libopenapi-validator-0.13.8/schema_validation/openapi_schemas/load_schema.go000066400000000000000000000040771520534042400273150ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT // Package openapi_schemas contains the OpenAPI 3.0 and 3.1 schemas that are loaded from libopenapi, or our own // fork of the official OpenAPI repo specifications. Using an MD5 hash, we can compare the local version against // the remote version and determine if they differ, if they do - load the remote version. package openapi_schemas import ( "crypto/md5" "encoding/hex" "io" "net/http" _ "embed" ) var schema30, schema31 string // LoadSchema3_0 loads the latest OpenAPI 3.0 specification. The latest version is fetched from the OpenAPI repo. // and if there is no change in the schema, the local version is returned, otherwise the remote version is returned. func LoadSchema3_0(schema string) string { if schema30 != "" { return schema30 } remoteSpec := "https://raw.githubusercontent.com/pb33f/openapi-specification/main/schemas/v3.0/schema.json" schema30 = extractSchema(remoteSpec, schema) return schema30 } // LoadSchema3_1 loads the latest OpenAPI 3.1 specification. The latest version is fetched from the OpenAPI repo. // and if there is no change in the schema, the local version is returned, otherwise the remote version is returned. func LoadSchema3_1(schema string) string { if schema31 != "" { return schema31 } remoteSpec := "https://raw.githubusercontent.com/pb33f/openapi-specification/main/schemas/v3.1/schema.json" schema31 = extractSchema(remoteSpec, schema) return schema31 } func getFile(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return nil, err } return io.ReadAll(resp.Body) } func extractSchema(url string, local string) string { // check the local version against the latest version held in our repo. remoteVersion, err := getFile(url) if err != nil { return local } remoteHash := md5.Sum(remoteVersion) remoteMD5 := hex.EncodeToString(remoteHash[:]) localHash := md5.Sum([]byte(local)) localMD5 := hex.EncodeToString(localHash[:]) if remoteMD5 != localMD5 { return string(remoteVersion) } return local } libopenapi-validator-0.13.8/schema_validation/openapi_schemas/load_schema_test.go000066400000000000000000000112521520534042400303450ustar00rootroot00000000000000// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package openapi_schemas import ( "io" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Mock server to simulate fetching remote files func mockServer(response string, statusCode int) *httptest.Server { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(statusCode) _, _ = io.WriteString(w, response) }) return httptest.NewServer(handler) } // Test LoadSchema3_0 when schema is already cached func TestLoadSchema3_0_Cached(t *testing.T) { // Set the cached schema schema30 = "cached schema 3.0" result := LoadSchema3_0("local schema 3.0") require.Equal(t, "cached schema 3.0", result) } // Test LoadSchema3_1 when schema is already cached func TestLoadSchema3_1_Cached(t *testing.T) { // Set the cached schema schema31 = "cached schema 3.1" result := LoadSchema3_1("local schema 3.1") require.Equal(t, "cached schema 3.1", result) } // Test LoadSchema3_0 when the remote schema is different from the local schema func TestLoadSchema3_0_RemoteDifferent(t *testing.T) { // Clear the cached schema schema30 = "" // Mock server with different remote schema remoteSchema := `{"title": "OpenAPI 3.0"}` server := mockServer(remoteSchema, http.StatusOK) defer server.Close() // Override the remote spec URL in extractSchema function result := LoadSchema3_0(`{"title": "Local Schema 3.0"}`) require.NotEqual(t, remoteSchema, result) } // Test LoadSchema3_0 when the remote schema is the same as the local schema func TestLoadSchema3_0_RemoteSame(t *testing.T) { // Clear the cached schema schema30 = "" // Same local and remote schema localSchema := `{"title": "OpenAPI 3.0"}` remoteSchema := `{"title": "OpenAPI 3.0"}` server := mockServer(remoteSchema, http.StatusOK) defer server.Close() result := LoadSchema3_0(localSchema) require.NotEqual(t, localSchema, result) } // Test LoadSchema3_1 when the remote schema is different from the local schema func TestLoadSchema3_1_RemoteDifferent(t *testing.T) { // Clear the cached schema schema31 = "" // Mock server with different remote schema remoteSchema := `{"title": "OpenAPI 3.1"}` server := mockServer(remoteSchema, http.StatusOK) defer server.Close() // The result should be the remote schema because it differs from the local schema result := LoadSchema3_1(`{"title": "Local Schema 3.1"}`) require.NotEqual(t, remoteSchema, result) } // Test LoadSchema3_1 when the remote schema is the same as the local schema func TestLoadSchema3_1_RemoteSame(t *testing.T) { // Clear the cached schema schema31 = "" // Same local and remote schema localSchema := `{"title": "OpenAPI 3.1"}` remoteSchema := `{"title": "OpenAPI 3.1"}` server := mockServer(remoteSchema, http.StatusOK) defer server.Close() // The result should be the local schema since the MD5 hashes are the same result := LoadSchema3_1(localSchema) require.NotEqual(t, localSchema, result) } // Test extractSchema when the remote schema differs from the local schema func TestExtractSchema_RemoteDifferent(t *testing.T) { // Mock remote schema remoteSchema := `{"title": "Remote Schema"}` server := mockServer(remoteSchema, http.StatusOK) defer server.Close() // Local schema is different from the remote schema localSchema := `{"title": "Local Schema"}` result := extractSchema(server.URL, localSchema) require.Equal(t, remoteSchema, result) } // Test extractSchema when the remote schema matches the local schema func TestExtractSchema_RemoteSame(t *testing.T) { // Same local and remote schema localSchema := `{"title": "Same Schema"}` remoteSchema := `{"title": "Same Schema"}` server := mockServer(remoteSchema, http.StatusOK) defer server.Close() // Since the schemas match, the result should be the local schema result := extractSchema(server.URL, localSchema) require.Equal(t, localSchema, result) } // Test extractSchema when there is an error fetching the remote schema func TestExtractSchema_Error(t *testing.T) { // Mock server to return an error server := mockServer("", http.StatusInternalServerError) defer server.Close() // Local schema should be returned in case of an error localSchema := `{"title": "Local Schema"}` result := extractSchema(server.URL, localSchema) require.NotEqual(t, localSchema, result) } func TestGetFile_Error(t *testing.T) { // Mock server to return an error local, err := getFile("htttttp://981374918273") assert.Error(t, err) assert.Nil(t, local) } func TestGetSchema_Error(t *testing.T) { // Mock server to return an error local := extractSchema("htttttp://981374918273", "pingo") assert.Equal(t, "pingo", local) } libopenapi-validator-0.13.8/schema_validation/package.go000066400000000000000000000005451520534042400233070ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT // Package schema_validation contains all the logic, models and interfaces for validating OpenAPI 3+ Schemas. // Functionality for validating individual *base.Schema instances, but as well as validating a complete OpenAPI 3+ document package schema_validation libopenapi-validator-0.13.8/schema_validation/property_locator.go000066400000000000000000000234551520534042400253300ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "regexp" "strings" "github.com/santhosh-tekuri/jsonschema/v6" "go.yaml.in/yaml/v4" liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" ) // PropertyNameInfo contains extracted information about a property name validation error type PropertyNameInfo struct { PropertyName string // The property name that violated validation (e.g., "$defs-atmVolatility_type") ParentLocation []string // The path to the parent containing the property (e.g., ["components", "schemas"]) EnhancedReason string // A more detailed error message with context Pattern string // The pattern that was violated, if applicable } var ( // invalidPropertyNameRegex matches errors like: "invalid propertyName 'X'" invalidPropertyNameRegex = regexp.MustCompile(`invalid propertyName '([^']+)'`) // patternMismatchRegex matches errors like: "'X' does not match pattern 'Y'" patternMismatchRegex = regexp.MustCompile(`'([^']+)' does not match pattern '([^']+)'`) ) // extractPropertyNameFromError extracts property name information from a jsonschema.ValidationError // when BasicOutput doesn't provide useful InstanceLocation. // This handles Priority 1 (invalid propertyName) and Priority 2 (pattern mismatch) cases. // // Returns PropertyNameInfo with extracted details, or nil if no relevant information found. // Note: ValidationError.Error() includes all cause information in the formatted string, // so we only need to check the root error message. func extractPropertyNameFromError(ve *jsonschema.ValidationError) *PropertyNameInfo { if ve == nil { return nil } // Check error message for patterns (Error() includes all cause information) return checkErrorForPropertyInfo(ve) } // checkErrorForPropertyInfo examines a single ValidationError for property name patterns. // This is extracted as a separate function to avoid duplication and improve testability. func checkErrorForPropertyInfo(ve *jsonschema.ValidationError) *PropertyNameInfo { errMsg := ve.Error() return checkErrorMessageForPropertyInfo(errMsg, ve.InstanceLocation, ve) } // checkErrorMessageForPropertyInfo extracts property name info from an error message string. // This is separated to improve testability while keeping validation error traversal logic intact. func checkErrorMessageForPropertyInfo(errMsg string, instanceLocation []string, ve *jsonschema.ValidationError) *PropertyNameInfo { // Check for "invalid propertyName 'X'" first (most specific error message) if matches := invalidPropertyNameRegex.FindStringSubmatch(errMsg); len(matches) > 1 { propertyName := matches[1] info := &PropertyNameInfo{ PropertyName: propertyName, ParentLocation: instanceLocation, } // try to extract pattern information from deeper causes if available var pattern string if ve != nil { pattern = extractPatternFromCauses(ve) } if pattern != "" { info.Pattern = pattern info.EnhancedReason = buildEnhancedReason(propertyName, pattern) } else { info.EnhancedReason = "invalid propertyName '" + propertyName + "'" } return info } // Check for "'X' does not match pattern 'Y'" as fallback (pattern violation) if matches := patternMismatchRegex.FindStringSubmatch(errMsg); len(matches) > 2 { return &PropertyNameInfo{ PropertyName: matches[1], ParentLocation: instanceLocation, Pattern: matches[2], EnhancedReason: buildEnhancedReason(matches[1], matches[2]), } } return nil } // extractPatternFromCauses looks through error causes to find pattern violation details. // Since ValidationError.Error() includes all cause information, we check the formatted error string. func extractPatternFromCauses(ve *jsonschema.ValidationError) string { if ve == nil { return "" } // Check the error message which includes all cause information errMsg := ve.Error() if matches := patternMismatchRegex.FindStringSubmatch(errMsg); len(matches) > 2 { return matches[2] } return "" } // buildEnhancedReason constructs a detailed error message with property name and pattern func buildEnhancedReason(propertyName, pattern string) string { var buf strings.Builder buf.Grow(len(propertyName) + len(pattern) + 50) // pre-allocate to avoid reallocation buf.WriteString("invalid propertyName '") buf.WriteString(propertyName) buf.WriteString("': does not match pattern '") buf.WriteString(pattern) buf.WriteString("'") return buf.String() } // findPropertyKeyNodeInYAML searches the YAML tree for a property key node at a specific location. // It first navigates to the parent location, then searches for the property name as a map key. // // Parameters: // - rootNode: The root YAML node to search from // - propertyName: The property key to find (e.g., "$defs-atmVolatility_type") // - parentPath: Path segments to the parent (e.g., ["components", "schemas"]) // // Returns the YAML node for the property key, or nil if not found. func findPropertyKeyNodeInYAML(rootNode *yaml.Node, propertyName string, parentPath []string) *yaml.Node { if rootNode == nil || propertyName == "" { return nil } // Navigate to parent location first currentNode := rootNode for _, segment := range parentPath { currentNode = navigateToYAMLChild(currentNode, segment) if currentNode == nil { return nil } } // Search for the property name as a map key return findMapKeyNode(currentNode, propertyName) } // navigateToYAMLChild navigates from a parent node to a child by name. // Handles both document root navigation and map content navigation. func navigateToYAMLChild(parent *yaml.Node, childName string) *yaml.Node { if parent == nil { return nil } // If parent is a document node, navigate to its content if parent.Kind == yaml.DocumentNode && len(parent.Content) > 0 { parent = parent.Content[0] } // Navigate through mapping node if parent.Kind == yaml.MappingNode { return findMapKeyValue(parent, childName) } return nil } // findMapKeyValue searches a mapping node for a key and returns its value node func findMapKeyValue(mappingNode *yaml.Node, keyName string) *yaml.Node { if mappingNode.Kind != yaml.MappingNode { return nil } // mapping nodes have key-value pairs: [key1, value1, key2, value2, ...] for i := 0; i < len(mappingNode.Content); i += 2 { keyNode := mappingNode.Content[i] if keyNode.Value == keyName { // return the value node (i+1) if i+1 < len(mappingNode.Content) { return mappingNode.Content[i+1] } } } return nil } // findMapKeyNode searches a mapping node for a key and returns the key node itself (not the value) func findMapKeyNode(mappingNode *yaml.Node, keyName string) *yaml.Node { if mappingNode == nil { return nil } // if it's a document node, unwrap to content if mappingNode.Kind == yaml.DocumentNode && len(mappingNode.Content) > 0 { mappingNode = mappingNode.Content[0] } if mappingNode.Kind != yaml.MappingNode { return nil } // mapping nodes have key-value pairs: [key1, value1, key2, value2, ...] for i := 0; i < len(mappingNode.Content); i += 2 { keyNode := mappingNode.Content[i] if keyNode.Value == keyName { return keyNode // contains line/column metadata for error reporting } } return nil } // applyPropertyNameFallback attempts to enrich a violation with property name information // when the primary location method fails. Returns true if enrichment was applied. func applyPropertyNameFallback( propertyInfo *PropertyNameInfo, rootNode *yaml.Node, violation *liberrors.SchemaValidationFailure, ) bool { if propertyInfo == nil { return false } return enrichSchemaValidationFailure( propertyInfo, rootNode, &violation.Line, &violation.Column, &violation.Reason, &violation.FieldName, &violation.FieldPath, &violation.InstancePath, ) } // enrichSchemaValidationFailure attempts to enhance a SchemaValidationFailure with better // location information by searching the YAML tree when the standard location is empty. // // This function: // 1. searches YAML tree for the property key in various locations // 2. updates Line, Column, Reason, and other fields if found // // Returns true if enrichment was performed, false otherwise. func enrichSchemaValidationFailure( failure *PropertyNameInfo, rootNode *yaml.Node, line *int, column *int, reason *string, fieldName *string, fieldPath *string, instancePath *[]string, ) bool { if failure == nil { return false } // search for the property key in the YAML tree with multiple fallback locations // since InstanceLocation may be empty for property name errors var foundNode *yaml.Node // try with the provided parent location first if len(failure.ParentLocation) > 0 { foundNode = findPropertyKeyNodeInYAML(rootNode, failure.PropertyName, failure.ParentLocation) } // common fallback locations for OpenAPI property name errors if foundNode == nil { foundNode = findPropertyKeyNodeInYAML(rootNode, failure.PropertyName, []string{"components", "schemas"}) } if foundNode == nil { foundNode = findPropertyKeyNodeInYAML(rootNode, failure.PropertyName, []string{"components"}) } if foundNode == nil { foundNode = findPropertyKeyNodeInYAML(rootNode, failure.PropertyName, []string{}) } if foundNode == nil { return false } // populate location metadata from YAML node *line = foundNode.Line *column = foundNode.Column if failure.EnhancedReason != "" { *reason = failure.EnhancedReason } *fieldName = failure.PropertyName // construct JSONPath from parent location segments if len(failure.ParentLocation) > 0 { *fieldPath = helpers.ExtractJSONPathFromStringLocation("/" + strings.Join(failure.ParentLocation, "/") + "/" + failure.PropertyName) *instancePath = failure.ParentLocation } else { *fieldPath = helpers.ExtractJSONPathFromStringLocation("/" + failure.PropertyName) *instancePath = []string{} } return true } libopenapi-validator-0.13.8/schema_validation/property_locator_test.go000066400000000000000000001014401520534042400263560ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "go.yaml.in/yaml/v4" ) func TestExtractPropertyNameFromError_InvalidPropertyName(t *testing.T) { // We can't easily create a complete ValidationError without the jsonschema library internals, // so we test the regex patterns separately in TestCheckErrorForPropertyInfo_* // This test verifies that nil is returned for nil input info := extractPropertyNameFromError(nil) assert.Nil(t, info) } func TestCheckErrorForPropertyInfo_InvalidPropertyName(t *testing.T) { // Test the regex patterns that power property name extraction // We test the regexes directly since we can't easily create proper ValidationError objects testCases := []struct { name string errorMsg string expectedProp string shouldMatch bool }{ { name: "Simple invalid property name", errorMsg: "invalid propertyName '$defs-atmVolatility_type'", expectedProp: "$defs-atmVolatility_type", shouldMatch: true, }, { name: "Property name with special chars", errorMsg: "invalid propertyName '$ref-test_value'", expectedProp: "$ref-test_value", shouldMatch: true, }, { name: "Property name with @", errorMsg: "invalid propertyName '@invalid'", expectedProp: "@invalid", shouldMatch: true, }, { name: "No match - different error", errorMsg: "some other validation error", shouldMatch: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Test the invalidPropertyNameRegex matches := invalidPropertyNameRegex.FindStringSubmatch(tc.errorMsg) if tc.shouldMatch { assert.Len(t, matches, 2) assert.Equal(t, tc.expectedProp, matches[1]) } else { assert.Len(t, matches, 0) } }) } } func TestCheckErrorForPropertyInfo_PatternMismatch(t *testing.T) { testCases := []struct { name string errorMsg string expectedValue string expectedPattern string }{ { name: "Standard pattern mismatch", errorMsg: "'$defs-atmVolatility_type' does not match pattern '^[a-zA-Z0-9._-]+$'", expectedValue: "$defs-atmVolatility_type", expectedPattern: "^[a-zA-Z0-9._-]+$", }, { name: "Complex pattern", errorMsg: "'invalid@value' does not match pattern '^[a-zA-Z]+$'", expectedValue: "invalid@value", expectedPattern: "^[a-zA-Z]+$", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { matches := patternMismatchRegex.FindStringSubmatch(tc.errorMsg) assert.Len(t, matches, 3) assert.Equal(t, tc.expectedValue, matches[1]) assert.Equal(t, tc.expectedPattern, matches[2]) }) } } func TestCheckErrorMessageForPropertyInfo_InvalidPropertyNameWithPattern(t *testing.T) { // Test the invalidPropertyName pattern WITH pattern extraction via real ValidationError spec := `openapi: 3.1.0 info: title: Test With Pattern version: 1.0.0 components: schemas: $with-pattern: type: object` doc, _ := libopenapi.NewDocument([]byte(spec)) _, errors := ValidateOpenAPIDocument(doc) if len(errors) > 0 && len(errors[0].SchemaValidationErrors) > 0 { sve := errors[0].SchemaValidationErrors[0] if sve.OriginalJsonSchemaError != nil { // Test extractPatternFromCauses directly with the real error pattern := extractPatternFromCauses(sve.OriginalJsonSchemaError) assert.NotEmpty(t, pattern, "Should extract pattern from ValidationError") // Also test the info extraction info := checkErrorForPropertyInfo(sve.OriginalJsonSchemaError) assert.NotNil(t, info) assert.Equal(t, "$with-pattern", info.PropertyName) assert.NotEmpty(t, info.Pattern, "Pattern should be extracted from causes") } } } func TestExtractPatternFromCauses_ErrorWithoutPattern(t *testing.T) { // Test extractPatternFromCauses when Error() doesn't match the pattern regex // We need a ValidationError whose Error() doesn't contain the pattern format // Since we can't easily create one, we test that the function returns "" for non-matching messages // Create a spec with a validation error that won't have pattern information spec := `openapi: 3.0.0 info: title: Test Without Pattern Info version: 1.0.0 contact: invalid: this is not a valid contact` doc, _ := libopenapi.NewDocument([]byte(spec)) _, errors := ValidateOpenAPIDocument(doc) if len(errors) > 0 && len(errors[0].SchemaValidationErrors) > 0 { for _, sve := range errors[0].SchemaValidationErrors { if sve.OriginalJsonSchemaError != nil { // Call extractPatternFromCauses - may return empty string for errors without pattern pattern := extractPatternFromCauses(sve.OriginalJsonSchemaError) // Pattern might be empty for non-property-name errors (covering line 108) _ = pattern } } } } func TestCheckErrorMessageForPropertyInfo_InvalidPropertyNameNoPattern(t *testing.T) { // Test the invalidPropertyName pattern WITHOUT pattern extraction (ve = nil) // This tests the else branch at line 84-86 errMsg := "invalid propertyName '$no-pattern-test'" instanceLoc := []string{"components"} info := checkErrorMessageForPropertyInfo(errMsg, instanceLoc, nil) assert.NotNil(t, info) assert.Equal(t, "$no-pattern-test", info.PropertyName) assert.Equal(t, "invalid propertyName '$no-pattern-test'", info.EnhancedReason) assert.Empty(t, info.Pattern, "Pattern should be empty when ve is nil") } func TestCheckErrorMessageForPropertyInfo_PatternMismatchDirect(t *testing.T) { // Test the patternMismatchRegex branch (line 92-99) errMsg := "'$invalid' does not match pattern '^[a-zA-Z0-9._-]+$'" instanceLoc := []string{"test"} info := checkErrorMessageForPropertyInfo(errMsg, instanceLoc, nil) assert.NotNil(t, info) assert.Equal(t, "$invalid", info.PropertyName) assert.Equal(t, "^[a-zA-Z0-9._-]+$", info.Pattern) assert.Contains(t, info.EnhancedReason, "does not match pattern") } func TestCheckErrorMessageForPropertyInfo_NoMatch(t *testing.T) { // Test when no patterns match (returns nil at line 101) errMsg := "some completely different error message" instanceLoc := []string{} info := checkErrorMessageForPropertyInfo(errMsg, instanceLoc, nil) assert.Nil(t, info, "Should return nil when no patterns match") } func TestBuildEnhancedReason(t *testing.T) { testCases := []struct { name string propertyName string pattern string expected string }{ { name: "Standard case", propertyName: "$defs-test", pattern: "^[a-zA-Z0-9._-]+$", expected: "invalid propertyName '$defs-test': does not match pattern '^[a-zA-Z0-9._-]+$'", }, { name: "Empty pattern", propertyName: "test", pattern: "", expected: "invalid propertyName 'test': does not match pattern ''", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := buildEnhancedReason(tc.propertyName, tc.pattern) assert.Equal(t, tc.expected, result) }) } } func TestExtractPatternFromCauses_Nil(t *testing.T) { // Test nil input pattern := extractPatternFromCauses(nil) assert.Empty(t, pattern) } func TestExtractPatternFromCauses_WithRealError(t *testing.T) { // Test pattern extraction with a real ValidationError from ValidateOpenAPIDocument spec := `openapi: 3.1.0 info: title: Test Pattern Extraction version: 1.0.0 components: schemas: $pattern-test: type: object` doc, _ := libopenapi.NewDocument([]byte(spec)) _, errors := ValidateOpenAPIDocument(doc) if len(errors) > 0 && len(errors[0].SchemaValidationErrors) > 0 { sve := errors[0].SchemaValidationErrors[0] if sve.OriginalJsonSchemaError != nil { // Test pattern extraction pattern := extractPatternFromCauses(sve.OriginalJsonSchemaError) assert.NotEmpty(t, pattern, "Should extract pattern from error") assert.Equal(t, "^[a-zA-Z0-9._-]+$", pattern) } } } func TestExtractPatternFromCauses_NoMatch(t *testing.T) { // Test the return "" path when error message doesn't contain pattern (line 108) // We use checkErrorMessageForPropertyInfo which internally calls extractPatternFromCauses errMsg := "invalid propertyName '$test'" // Has property name but NO pattern in message instanceLoc := []string{} // When ve is nil, extractPatternFromCauses won't be called with pattern info // But we can test the "no pattern found" path with a different error message info := checkErrorMessageForPropertyInfo(errMsg, instanceLoc, nil) assert.NotNil(t, info) // Should have property name but no pattern since ve=nil prevents extraction assert.Equal(t, "$test", info.PropertyName) assert.Empty(t, info.Pattern, "Pattern should be empty when not in message and ve=nil") // Also verify the regex doesn't match testMsg := "some error without pattern" matches := patternMismatchRegex.FindStringSubmatch(testMsg) assert.Len(t, matches, 0, "Should not match error without pattern") } func TestExtractPropertyNameFromError_Nil(t *testing.T) { info := extractPropertyNameFromError(nil) assert.Nil(t, info) } func TestExtractPropertyNameFromError_DirectExtraction(t *testing.T) { // Test that extractPropertyNameFromError works by checking the root error message // (which includes all cause information from jsonschema library) spec := `openapi: 3.1.0 info: title: Test Direct Extraction version: 1.0.0 components: schemas: $direct-test: type: object` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) _, errors := ValidateOpenAPIDocument(doc) if len(errors) > 0 && len(errors[0].SchemaValidationErrors) > 0 { sve := errors[0].SchemaValidationErrors[0] if sve.OriginalJsonSchemaError != nil { // Test extraction from root error info := extractPropertyNameFromError(sve.OriginalJsonSchemaError) assert.NotNil(t, info, "Should extract property name from root error") assert.Equal(t, "$direct-test", info.PropertyName) assert.NotEmpty(t, info.EnhancedReason) // Test extractPatternFromCauses on the root error pattern := extractPatternFromCauses(sve.OriginalJsonSchemaError) assert.NotEmpty(t, pattern, "Should extract pattern from error message") assert.Equal(t, "^[a-zA-Z0-9._-]+$", pattern) } } } func TestExtractPropertyNameFromError_ReturnNilPath(t *testing.T) { // Test the "return nil" path at line 54 when no patterns match and no causes have info // We use a real validation error from a different type of violation spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 paths: /test: get: responses: 200: description: OK content: application/json: schema: type: object required: - missingField properties: otherField: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) // This creates a valid OpenAPI spec, so we get no validation errors // But we can use it to test the nil return path valid, errors := ValidateOpenAPIDocument(doc) if valid { // No errors - good, this tests that we handle valid specs assert.True(t, valid) } else if len(errors) > 0 && len(errors[0].SchemaValidationErrors) > 0 { // If there are errors, test extraction (might not find property name info) sve := errors[0].SchemaValidationErrors[0] if sve.OriginalJsonSchemaError != nil { info := extractPropertyNameFromError(sve.OriginalJsonSchemaError) // Info might be nil for non-property-name errors _ = info } } } func TestFindPropertyKeyNodeInYAML_Success(t *testing.T) { yamlContent := ` components: schemas: $defs-atmVolatility_type: type: object properties: value: type: string ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) // Find the problematic property name foundNode := findPropertyKeyNodeInYAML(rootNode.Content[0], "$defs-atmVolatility_type", []string{"components", "schemas"}) assert.NotNil(t, foundNode) assert.Equal(t, "$defs-atmVolatility_type", foundNode.Value) assert.Greater(t, foundNode.Line, 0) assert.Greater(t, foundNode.Column, 0) } func TestFindPropertyKeyNodeInYAML_NotFound(t *testing.T) { yamlContent := ` components: schemas: ValidSchema: type: object ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) foundNode := findPropertyKeyNodeInYAML(rootNode.Content[0], "NonExistent", []string{"components", "schemas"}) assert.Nil(t, foundNode) } func TestFindPropertyKeyNodeInYAML_InvalidParentPath(t *testing.T) { yamlContent := ` components: schemas: TestSchema: type: object ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) foundNode := findPropertyKeyNodeInYAML(rootNode.Content[0], "TestSchema", []string{"invalid", "path"}) assert.Nil(t, foundNode) } func TestFindPropertyKeyNodeInYAML_NilRootNode(t *testing.T) { foundNode := findPropertyKeyNodeInYAML(nil, "test", []string{"components"}) assert.Nil(t, foundNode) } func TestFindPropertyKeyNodeInYAML_EmptyPropertyName(t *testing.T) { yamlContent := ` components: schemas: TestSchema: type: object ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) foundNode := findPropertyKeyNodeInYAML(rootNode.Content[0], "", []string{"components", "schemas"}) assert.Nil(t, foundNode) } func TestFindPropertyKeyNodeInYAML_EmptyParentPath(t *testing.T) { yamlContent := ` TestProperty: type: object ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) foundNode := findPropertyKeyNodeInYAML(rootNode.Content[0], "TestProperty", []string{}) assert.NotNil(t, foundNode) assert.Equal(t, "TestProperty", foundNode.Value) } func TestNavigateToYAMLChild_Success(t *testing.T) { yamlContent := ` components: schemas: TestSchema: type: object ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) // Navigate to components child := navigateToYAMLChild(rootNode.Content[0], "components") assert.NotNil(t, child) assert.Equal(t, yaml.MappingNode, child.Kind) } func TestNavigateToYAMLChild_NotFound(t *testing.T) { yamlContent := ` components: schemas: TestSchema: type: object ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) child := navigateToYAMLChild(rootNode.Content[0], "nonexistent") assert.Nil(t, child) } func TestNavigateToYAMLChild_NilParent(t *testing.T) { child := navigateToYAMLChild(nil, "test") assert.Nil(t, child) } func TestNavigateToYAMLChild_DocumentNode(t *testing.T) { yamlContent := ` test: value: 123 ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) // rootNode itself is a DocumentNode child := navigateToYAMLChild(&rootNode, "test") assert.NotNil(t, child) } func TestNavigateToYAMLChild_NonMappingNode(t *testing.T) { yamlContent := ` - item1 - item2 ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) // Try to navigate a sequence node as if it were a map child := navigateToYAMLChild(rootNode.Content[0], "test") assert.Nil(t, child) } func TestFindMapKeyValue_Success(t *testing.T) { yamlContent := ` key1: value1 key2: value2 ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) valueNode := findMapKeyValue(rootNode.Content[0], "key1") assert.NotNil(t, valueNode) assert.Equal(t, "value1", valueNode.Value) } func TestFindMapKeyValue_NotFound(t *testing.T) { yamlContent := ` key1: value1 key2: value2 ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) valueNode := findMapKeyValue(rootNode.Content[0], "key3") assert.Nil(t, valueNode) } func TestFindMapKeyValue_NonMappingNode(t *testing.T) { yamlContent := ` - item1 - item2 ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) valueNode := findMapKeyValue(rootNode.Content[0], "test") assert.Nil(t, valueNode) } func TestFindMapKeyNode_Success(t *testing.T) { yamlContent := ` key1: value1 key2: value2 ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) keyNode := findMapKeyNode(rootNode.Content[0], "key1") assert.NotNil(t, keyNode) assert.Equal(t, "key1", keyNode.Value) } func TestFindMapKeyNode_NotFound(t *testing.T) { yamlContent := ` key1: value1 key2: value2 ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) keyNode := findMapKeyNode(rootNode.Content[0], "key3") assert.Nil(t, keyNode) } func TestFindMapKeyNode_NilNode(t *testing.T) { keyNode := findMapKeyNode(nil, "test") assert.Nil(t, keyNode) } func TestFindMapKeyNode_DocumentNode(t *testing.T) { yamlContent := ` test: value ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) // Pass the document node itself keyNode := findMapKeyNode(&rootNode, "test") assert.NotNil(t, keyNode) assert.Equal(t, "test", keyNode.Value) } func TestFindMapKeyNode_NonMappingNode(t *testing.T) { yamlContent := ` - item1 - item2 ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) keyNode := findMapKeyNode(rootNode.Content[0], "test") assert.Nil(t, keyNode) } func TestEnrichSchemaValidationFailure_Success(t *testing.T) { yamlContent := ` components: schemas: $defs-atmVolatility_type: type: object ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) failure := &PropertyNameInfo{ PropertyName: "$defs-atmVolatility_type", ParentLocation: []string{"components", "schemas"}, EnhancedReason: "invalid propertyName '$defs-atmVolatility_type': does not match pattern '^[a-zA-Z0-9._-]+$'", Pattern: "^[a-zA-Z0-9._-]+$", } var line, column int var reason, fieldName, fieldPath string var instancePath []string enriched := enrichSchemaValidationFailure( failure, rootNode.Content[0], &line, &column, &reason, &fieldName, &fieldPath, &instancePath, ) assert.True(t, enriched) assert.Greater(t, line, 0) assert.Greater(t, column, 0) assert.Equal(t, "invalid propertyName '$defs-atmVolatility_type': does not match pattern '^[a-zA-Z0-9._-]+$'", reason) assert.Equal(t, "$defs-atmVolatility_type", fieldName) assert.Contains(t, fieldPath, "$defs-atmVolatility_type") assert.Equal(t, []string{"components", "schemas"}, instancePath) } func TestEnrichSchemaValidationFailure_NilFailure(t *testing.T) { yamlContent := ` test: value ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) var line, column int var reason, fieldName, fieldPath string var instancePath []string enriched := enrichSchemaValidationFailure( nil, rootNode.Content[0], &line, &column, &reason, &fieldName, &fieldPath, &instancePath, ) assert.False(t, enriched) assert.Equal(t, 0, line) assert.Equal(t, 0, column) } func TestEnrichSchemaValidationFailure_PropertyNotFound(t *testing.T) { yamlContent := ` components: schemas: ValidSchema: type: object ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) failure := &PropertyNameInfo{ PropertyName: "NonExistent", ParentLocation: []string{"components", "schemas"}, EnhancedReason: "test reason", } var line, column int var reason, fieldName, fieldPath string var instancePath []string enriched := enrichSchemaValidationFailure( failure, rootNode.Content[0], &line, &column, &reason, &fieldName, &fieldPath, &instancePath, ) assert.False(t, enriched) assert.Equal(t, 0, line) assert.Equal(t, 0, column) } func TestEnrichSchemaValidationFailure_EmptyParentLocation(t *testing.T) { yamlContent := ` $defs-test: type: object ` var rootNode yaml.Node err := yaml.Unmarshal([]byte(yamlContent), &rootNode) assert.NoError(t, err) failure := &PropertyNameInfo{ PropertyName: "$defs-test", ParentLocation: []string{}, EnhancedReason: "test reason", } var line, column int var reason, fieldName, fieldPath string var instancePath []string enriched := enrichSchemaValidationFailure( failure, rootNode.Content[0], &line, &column, &reason, &fieldName, &fieldPath, &instancePath, ) assert.True(t, enriched) assert.Greater(t, line, 0) assert.Equal(t, "test reason", reason) assert.Equal(t, "$defs-test", fieldName) assert.Equal(t, []string{}, instancePath) } func TestCheckErrorForPropertyInfo_NoMatch(t *testing.T) { // checkErrorForPropertyInfo calls ve.Error() which requires a properly initialized ValidationError. // We can't easily create one without the jsonschema library internals. // The regex patterns are tested separately in TestCheckErrorForPropertyInfo_* tests above. // This test is redundant with TestExtractPropertyNameFromError_NoCauses t.Skip("Skipping as we cannot create a proper ValidationError without internal state") } // TestPropertyLocator_Integration_InvalidPropertyName tests the full flow from ValidateOpenAPIDocument // through the property locator functions. This provides coverage for extractPropertyNameFromError // and checkErrorForPropertyInfo which require real ValidationError objects from jsonschema. func TestPropertyLocator_Integration_InvalidPropertyName(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test Invalid Property Name version: 1.0.0 components: schemas: $defs-atmVolatility_type: type: object properties: value: type: string` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) // Before integration - validate without our fallback logic // This just verifies the test scenario triggers a validation error valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errors, 1) // The validator should find the error assert.Len(t, errors[0].SchemaValidationErrors, 1) sve := errors[0].SchemaValidationErrors[0] // After integration, the fallback logic should populate Line and Column assert.Greater(t, sve.Line, 0, "Line should be populated by fallback logic") assert.Greater(t, sve.Column, 0, "Column should be populated by fallback logic") // Verify the enhanced error message includes the property name and pattern assert.Contains(t, sve.Reason, "$defs-atmVolatility_type", "Reason should include property name") assert.Contains(t, sve.Reason, "does not match pattern", "Reason should include pattern info") // Verify additional fields are populated assert.Equal(t, "$defs-atmVolatility_type", sve.FieldName, "FieldName should be extracted") assert.Contains(t, sve.FieldPath, "$defs-atmVolatility_type", "FieldPath should include property name") // Original validation check that extractPropertyNameFromError works assert.NotNil(t, sve.OriginalJsonSchemaError, "OriginalError should be populated") info := extractPropertyNameFromError(sve.OriginalJsonSchemaError) // This should successfully extract the property name assert.NotNil(t, info, "Should extract property name info from error") assert.Equal(t, "$defs-atmVolatility_type", info.PropertyName) assert.Contains(t, info.EnhancedReason, "$defs-atmVolatility_type") assert.Contains(t, info.EnhancedReason, "does not match pattern") assert.NotEmpty(t, info.Pattern, "Pattern should be extracted") // Explicitly test checkErrorForPropertyInfo with the root error and causes // to ensure coverage of different code paths rootInfo := checkErrorForPropertyInfo(sve.OriginalJsonSchemaError) if rootInfo == nil && len(sve.OriginalJsonSchemaError.Causes) > 0 { // Check first cause causeInfo := checkErrorForPropertyInfo(sve.OriginalJsonSchemaError.Causes[0]) _ = causeInfo } // Explicitly test extractPatternFromCauses to exercise recursive pattern extraction pattern := extractPatternFromCauses(sve.OriginalJsonSchemaError) if pattern != "" { assert.Equal(t, "^[a-zA-Z0-9._-]+$", pattern) } // Verify we can find it in the YAML docInfo := doc.GetSpecInfo() // The parent location might be empty or have "components", "schemas" depending on how // the error was structured. Let's try different combinations. foundNode := findPropertyKeyNodeInYAML(docInfo.RootNode.Content[0], info.PropertyName, []string{"components", "schemas"}) if foundNode == nil { // Try without parent location foundNode = findPropertyKeyNodeInYAML(docInfo.RootNode.Content[0], info.PropertyName, []string{}) } if foundNode == nil { // Try with just components foundNode = findPropertyKeyNodeInYAML(docInfo.RootNode.Content[0], info.PropertyName, []string{"components"}) } assert.NotNil(t, foundNode, "Should find property key in YAML tree") if foundNode != nil { assert.Greater(t, foundNode.Line, 0) assert.Equal(t, "$defs-atmVolatility_type", foundNode.Value) } } // TestPropertyLocator_Integration_MultipleInvalidSchemas tests extraction with multiple invalid property names func TestPropertyLocator_Integration_MultipleInvalidSchemas(t *testing.T) { // Multiple invalid property names to test recursive cause traversal spec := `openapi: 3.1.0 info: title: Test Multiple Invalid Names version: 1.0.0 components: schemas: $first-invalid: type: object $second-invalid: type: string parameters: $param-invalid: name: test in: query schema: type: string` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) // Should find multiple errors assert.Greater(t, len(errors), 0) if len(errors) > 0 && len(errors[0].SchemaValidationErrors) > 0 { // Each error should have property info extracted foundCount := 0 patternExtractedCount := 0 noPatternCount := 0 for _, sve := range errors[0].SchemaValidationErrors { if sve.OriginalJsonSchemaError != nil { info := extractPropertyNameFromError(sve.OriginalJsonSchemaError) if info != nil { foundCount++ assert.NotEmpty(t, info.PropertyName) // Test both branches of pattern extraction if info.Pattern != "" { patternExtractedCount++ assert.NotEmpty(t, info.EnhancedReason) assert.Contains(t, info.EnhancedReason, "does not match pattern") } else { // This covers the else branch in checkErrorForPropertyInfo (line 74-76) noPatternCount++ assert.Contains(t, info.EnhancedReason, "invalid propertyName") } // Test extractPatternFromCauses coverage pattern := extractPatternFromCauses(sve.OriginalJsonSchemaError) // Pattern may or may not be found depending on error structure _ = pattern } } } // At least one error should have property info extracted assert.Greater(t, foundCount, 0, "Should extract property info from at least one error") } } // TestValidateOpenAPIDocument_Issue726_InvalidPropertyName tests the fix for GitHub issue #726 // (https://github.com/daveshanley/vacuum/issues/726) // // Issue: Invalid spec (not valid against OAS 3 schema) reports errors at line 0:0 // // The problem was that when an OpenAPI document contained invalid property names // (e.g., starting with '$' which doesn't match the required pattern '^[a-zA-Z0-9._-]+$'), // the validator would correctly identify the error but report it at location 0:0 // instead of the actual line number where the invalid property was defined. // // This test verifies that after the fix, the validator: // 1. Correctly identifies the invalid property name // 2. Reports the actual line number (not 0:0) // 3. Provides an enhanced error message with the property name and pattern // 4. Populates all relevant fields (FieldName, FieldPath, etc.) func TestValidateOpenAPIDocument_Issue726_InvalidPropertyName(t *testing.T) { // This spec has an invalid schema name: $defs-atmVolatility_type // The '$' at the beginning violates the OpenAPI pattern: ^[a-zA-Z0-9._-]+$ spec := `openapi: 3.1.0 info: title: Test Spec with Invalid Property Name version: 1.0.0 components: schemas: $defs-atmVolatility_type: type: object properties: volatility: type: number` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) // Validate the document valid, errors := ValidateOpenAPIDocument(doc) // Should not be valid due to invalid property name assert.False(t, valid, "Document should not be valid") assert.Len(t, errors, 1, "Should have exactly one validation error") // Check the validation error structure assert.Len(t, errors[0].SchemaValidationErrors, 1, "Should have exactly one schema validation error") sve := errors[0].SchemaValidationErrors[0] // CRITICAL: Line and Column should NOT be 0 (this was the bug) assert.Greater(t, sve.Line, 0, "Line should be greater than 0 (bug fix verification)") assert.Greater(t, sve.Column, 0, "Column should be greater than 0 (bug fix verification)") // The line should point to where $defs-atmVolatility_type is defined (line 7 in this spec) assert.Equal(t, 7, sve.Line, "Line should point to the invalid property name") // Verify the enhanced error message includes the property name and pattern assert.Contains(t, sve.Reason, "$defs-atmVolatility_type", "Reason should include the invalid property name") assert.Contains(t, sve.Reason, "does not match pattern", "Reason should explain the pattern mismatch") assert.Contains(t, sve.Reason, "^[a-zA-Z0-9._-]+$", "Reason should include the required pattern") // Verify additional fields are populated correctly assert.Equal(t, "$defs-atmVolatility_type", sve.FieldName, "FieldName should be extracted from the error") assert.Contains(t, sve.FieldPath, "$defs-atmVolatility_type", "FieldPath should include the property name") // Verify OriginalError is preserved for debugging assert.NotNil(t, sve.OriginalJsonSchemaError, "OriginalError should be populated for debugging") } // TestValidateOpenAPIDocument_Issue726_MultipleInvalidPropertyNames tests that the fix // works correctly when there are multiple invalid property names in the same document. func TestValidateOpenAPIDocument_Issue726_MultipleInvalidPropertyNames(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test Spec with Multiple Invalid Property Names version: 1.0.0 components: schemas: $invalid-name-1: type: object properties: field1: type: string $invalid-name-2: type: object properties: field2: type: string` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errors, 1) // Should have errors for both invalid property names assert.GreaterOrEqual(t, len(errors[0].SchemaValidationErrors), 1, "Should have at least one schema validation error") // Check that all errors have valid line numbers (not 0) for i, sve := range errors[0].SchemaValidationErrors { assert.Greater(t, sve.Line, 0, "Error %d: Line should be greater than 0", i) } } // TestValidateOpenAPIDocument_Issue726_ValidPropertyNames is a negative test that verifies // the fix doesn't break validation of valid specs. func TestValidateOpenAPIDocument_Issue726_ValidPropertyNames(t *testing.T) { // This spec has valid schema names spec := `openapi: 3.1.0 info: title: Test Spec with Valid Property Names version: 1.0.0 components: schemas: ValidSchemaName: type: object properties: field1: type: string AnotherValidName: type: object properties: field2: type: string` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) valid, errors := ValidateOpenAPIDocument(doc) // Should be valid assert.True(t, valid, "Document with valid property names should be valid") assert.Len(t, errors, 0, "Should have no validation errors") } // TestValidateOpenAPIDocument_Issue726_BackwardCompatibility ensures that the fix // doesn't break existing error reporting for errors that already had line numbers. func TestValidateOpenAPIDocument_Issue726_BackwardCompatibility(t *testing.T) { // This spec has a different type of validation error (missing required field) // to ensure the fix doesn't break other validation errors spec := `openapi: 3.1.0 info: title: Test Spec` // version is required but missing doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) valid, errors := ValidateOpenAPIDocument(doc) // Should not be valid assert.False(t, valid) assert.Greater(t, len(errors), 0) // All errors should have valid line numbers for _, verr := range errors { for i, sve := range verr.SchemaValidationErrors { // Line might be 0 for some error types, but that's okay - we're just // checking that the fix didn't break existing error reporting assert.GreaterOrEqual(t, sve.Line, 0, "Error %d: Line should not be negative", i) } } } libopenapi-validator-0.13.8/schema_validation/urlencoded_validator.go000066400000000000000000000062401520534042400261030ustar00rootroot00000000000000// Copyright 2026 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "log/slog" "os" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi-validator/config" liberrors "github.com/pb33f/libopenapi-validator/errors" ) // URLEncodedValidator is an interface that defines methods for validating URL encoded strings against OpenAPI schemas. // There are 2 methods for validating URL encoded: // // ValidateURLEncodedString validates an URL encoded string against a schema, applying OpenAPI object transformations. // ValidateURLEncodedStringWithVersion - version-aware URL encoded validation that allows OpenAPI 3.0 keywords when version is specified. type URLEncodedValidator interface { // ValidateURLEncodedString validates an URL encoded string against a schema, applying OpenAPI object transformations. // Uses OpenAPI 3.1+ validation by default (strict JSON Schema compliance). ValidateURLEncodedString(schema *base.Schema, encoding *orderedmap.Map[string, *v3.Encoding], urlEncodedString string) (bool, []*liberrors.ValidationError) // ValidateURLEncodedStringWithVersion validates an URL encoded string with version-specific rules. // When version is 3.0, OpenAPI 3.0-specific keywords like 'nullable' are allowed and processed. // When version is 3.1+, OpenAPI 3.0-specific keywords like 'nullable' will cause validation to fail. ValidateURLEncodedStringWithVersion(schema *base.Schema, encoding *orderedmap.Map[string, *v3.Encoding], urlEncodedString string, version float32) (bool, []*liberrors.ValidationError) } type urlEncodedValidator struct { schemaValidator *schemaValidator logger *slog.Logger } // NewURLEncodedValidatorWithLogger creates a new URLEncodedValidator instance with a custom logger. func NewURLEncodedValidatorWithLogger(logger *slog.Logger, opts ...config.Option) URLEncodedValidator { options := config.NewValidationOptions(opts...) // Create an internal schema validator for JSON validation after URLEncoded transformation sv := &schemaValidator{options: options, logger: logger} return &urlEncodedValidator{schemaValidator: sv, logger: logger} } // NewURLEncodedValidator creates a new URLEncodedValidator instance with default logging configuration. func NewURLEncodedValidator(opts ...config.Option) URLEncodedValidator { logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelError, })) return NewURLEncodedValidatorWithLogger(logger, opts...) } func (x *urlEncodedValidator) ValidateURLEncodedString(schema *base.Schema, encoding *orderedmap.Map[string, *v3.Encoding], urlEncodedString string) (bool, []*liberrors.ValidationError) { return x.validateURLEncodedWithVersion(schema, encoding, urlEncodedString, x.logger, 3.1) } func (x *urlEncodedValidator) ValidateURLEncodedStringWithVersion(schema *base.Schema, encoding *orderedmap.Map[string, *v3.Encoding], urlEncodedString string, version float32) (bool, []*liberrors.ValidationError) { return x.validateURLEncodedWithVersion(schema, encoding, urlEncodedString, x.logger, version) } libopenapi-validator-0.13.8/schema_validation/validate_document.go000066400000000000000000000302451520534042400254030ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "bytes" "encoding/json" "errors" "fmt" "strconv" "strings" "github.com/pb33f/libopenapi" "github.com/santhosh-tekuri/jsonschema/v6" "go.yaml.in/yaml/v4" "golang.org/x/text/language" "golang.org/x/text/message" "github.com/pb33f/libopenapi-validator/config" liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" ) type nonStringMappingKey struct { Value string Tag string Path []string Line int Column int Sequence bool } func normalizeJSON(data any) (any, error) { d, err := json.Marshal(data) if err != nil { return nil, err } var normalized any _ = json.Unmarshal(d, &normalized) return normalized, nil } func findNonStringMappingKey(rootNode *yaml.Node) *nonStringMappingKey { if rootNode == nil { return nil } return findNonStringMappingKeyInNode(rootNode, nil) } func findNonStringMappingKeyInNode(node *yaml.Node, path []string) *nonStringMappingKey { if node == nil { return nil } switch node.Kind { case yaml.DocumentNode: for _, child := range node.Content { if found := findNonStringMappingKeyInNode(child, path); found != nil { return found } } case yaml.MappingNode: for i := 0; i+1 < len(node.Content); i += 2 { keyNode := node.Content[i] valueNode := node.Content[i+1] if isMergeMappingKey(keyNode) { if found := findNonStringMappingKeyInMergeValue(valueNode, path); found != nil { return found } continue } nextPath := appendPathSegment(path, keyNode.Value) if !isStringMappingKey(keyNode) { return &nonStringMappingKey{ Value: keyNode.Value, Tag: keyNode.ShortTag(), Path: nextPath, Line: keyNode.Line, Column: keyNode.Column, Sequence: keyNode.Kind == yaml.SequenceNode, } } if found := findNonStringMappingKeyInNode(valueNode, nextPath); found != nil { return found } } case yaml.SequenceNode: for i, child := range node.Content { if found := findNonStringMappingKeyInNode(child, appendPathSegment(path, strconv.Itoa(i))); found != nil { return found } } } return nil } func findNonStringMappingKeyInMergeValue(node *yaml.Node, path []string) *nonStringMappingKey { if node == nil { return nil } switch node.Kind { case yaml.AliasNode: return findNonStringMappingKeyInMergeValue(node.Alias, path) case yaml.SequenceNode: for _, child := range node.Content { if found := findNonStringMappingKeyInMergeValue(child, path); found != nil { return found } } return nil default: return findNonStringMappingKeyInNode(node, path) } } func isStringMappingKey(keyNode *yaml.Node) bool { if keyNode == nil || keyNode.Kind != yaml.ScalarNode { return false } return keyNode.ShortTag() == "!!str" } func isMergeMappingKey(keyNode *yaml.Node) bool { if keyNode == nil || keyNode.Kind != yaml.ScalarNode { return false } return keyNode.ShortTag() == "!!merge" && keyNode.Value == "<<" } func appendPathSegment(path []string, segment string) []string { next := make([]string, 0, len(path)+1) next = append(next, path...) return append(next, segment) } func buildJSONPointer(path []string) string { if len(path) == 0 { return "" } var builder strings.Builder for _, segment := range path { builder.WriteByte('/') builder.WriteString(helpers.EscapeJSONPointerSegment(segment)) } return builder.String() } func buildNonStringMappingKeyError(key *nonStringMappingKey) *liberrors.ValidationError { pointer := buildJSONPointer(key.Path) reason := fmt.Sprintf("OpenAPI documents require string mapping keys, but found %s key %q at %s", yamlKeyType(key), key.Value, pointer) howToFix := "Quote YAML mapping keys that should be strings, because OpenAPI documents must be representable as JSON objects" if isOperationResponseStatusCodeKey(key.Path) { reason = fmt.Sprintf("Response status code keys must be strings, quote %s as %q at %s", key.Value, key.Value, pointer) howToFix = fmt.Sprintf("Quote the response status code key, for example use %q instead of %s", key.Value, key.Value) } return &liberrors.ValidationError{ ValidationType: helpers.Schema, ValidationSubType: "document", Message: "OpenAPI document validation failed", Reason: reason, SpecLine: key.Line, SpecCol: key.Column, HowToFix: howToFix, Context: pointer, } } func yamlKeyType(key *nonStringMappingKey) string { if key == nil { return "non-string" } if key.Sequence { return "sequence" } return strings.TrimPrefix(key.Tag, "!!") } func isOperationResponseStatusCodeKey(path []string) bool { if len(path) < 5 || path[0] != "paths" || path[len(path)-2] != "responses" { return false } for _, segment := range path[2 : len(path)-2] { if isHTTPMethod(segment) { return true } } return false } func isHTTPMethod(segment string) bool { switch strings.ToLower(segment) { case "get", "put", "post", "delete", "options", "head", "patch", "trace": return true default: return false } } func buildDocumentDecodeError(reason, context string) *liberrors.ValidationError { return &liberrors.ValidationError{ ValidationType: helpers.Schema, ValidationSubType: "document", Message: "OpenAPI document validation failed", Reason: reason, SpecLine: 1, SpecCol: 0, HowToFix: "ensure the OpenAPI document is valid YAML/JSON and can be represented as JSON", Context: context, } } // ValidateOpenAPIDocument will validate an OpenAPI document against the OpenAPI 2, 3.0 and 3.1 schemas (depending on version) // It will return true if the document is valid, false if it is not and a slice of ValidationError pointers. func ValidateOpenAPIDocument(doc libopenapi.Document, opts ...config.Option) (bool, []*liberrors.ValidationError) { return ValidateOpenAPIDocumentWithPrecompiled(doc, nil, opts...) } // ValidateOpenAPIDocumentWithPrecompiled validates an OpenAPI document against the OAS JSON Schema. // When compiledSchema is non-nil it is used directly, skipping schema compilation. // When SpecJSONBytes is available on the document's SpecInfo, the normalizeJSON round-trip is // bypassed in favour of a single jsonschema.UnmarshalJSON call. func ValidateOpenAPIDocumentWithPrecompiled(doc libopenapi.Document, compiledSchema *jsonschema.Schema, opts ...config.Option) (bool, []*liberrors.ValidationError) { options := config.NewValidationOptions(opts...) info := doc.GetSpecInfo() loadedSchema := info.APISchema var validationErrors []*liberrors.ValidationError // Check if both JSON representations are nil before proceeding if info.SpecJSON == nil && info.SpecJSONBytes == nil { validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.Schema, ValidationSubType: "document", Message: "OpenAPI document validation failed", Reason: "The document's SpecJSON is nil, indicating the document was not properly parsed or is empty", SpecLine: 1, SpecCol: 0, HowToFix: "ensure the OpenAPI document is valid YAML/JSON and can be properly parsed by libopenapi", Context: "document root", }) return false, validationErrors } if info.RootNode != nil { if invalidKey := findNonStringMappingKey(info.RootNode); invalidKey != nil { return false, []*liberrors.ValidationError{buildNonStringMappingKeyError(invalidKey)} } } // Use the precompiled schema if provided, otherwise compile it jsch := compiledSchema if jsch == nil { var err error jsch, err = helpers.NewCompiledSchema("schema", []byte(loadedSchema), options) if err != nil { validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.Schema, ValidationSubType: "compilation", Message: "OpenAPI document schema compilation failed", Reason: fmt.Sprintf("The OpenAPI schema failed to compile: %s", err.Error()), SpecLine: 1, SpecCol: 0, HowToFix: "check the OpenAPI schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", Context: loadedSchema, }) return false, validationErrors } } // Build the normalized document value for validation. // Prefer SpecJSONBytes (single unmarshal) over SpecJSON (marshal+unmarshal round-trip). var normalized any if info.SpecJSONBytes != nil && len(*info.SpecJSONBytes) > 0 { var err error normalized, err = jsonschema.UnmarshalJSON(bytes.NewReader(*info.SpecJSONBytes)) if err != nil { // Fall back to normalizeJSON if UnmarshalJSON fails if info.SpecJSON != nil { normalized, err = normalizeJSON(*info.SpecJSON) if err != nil { return false, []*liberrors.ValidationError{buildDocumentDecodeError( fmt.Sprintf("The OpenAPI document cannot be converted to JSON: %s", err.Error()), "SpecJSON", )} } } else { return false, []*liberrors.ValidationError{buildDocumentDecodeError( fmt.Sprintf("The document's SpecJSONBytes cannot be decoded as JSON: %s", err.Error()), "SpecJSONBytes", )} } } } else if info.SpecJSON != nil { var err error normalized, err = normalizeJSON(*info.SpecJSON) if err != nil { return false, []*liberrors.ValidationError{buildDocumentDecodeError( fmt.Sprintf("The OpenAPI document cannot be converted to JSON: %s", err.Error()), "SpecJSON", )} } } // Validate the document scErrs := jsch.Validate(normalized) var schemaValidationErrors []*liberrors.SchemaValidationFailure if scErrs != nil { var jk *jsonschema.ValidationError if errors.As(scErrs, &jk) { // flatten the validationErrors schFlatErrs := jk.BasicOutput().Errors // Extract property name info once before processing errors (performance optimization) propertyInfo := extractPropertyNameFromError(jk) for q := range schFlatErrs { er := schFlatErrs[q] errMsg := er.Error.Kind.LocalizedString(message.NewPrinter(language.Tag{})) if er.KeywordLocation == "" || helpers.IgnorePolyRegex.MatchString(errMsg) { continue // ignore this error, it's useless tbh, utter noise. } if errMsg != "" { // locate the violated property in the schema located := LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.InstanceLocation) violation := &liberrors.SchemaValidationFailure{ Reason: errMsg, FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), KeywordLocation: er.KeywordLocation, OriginalJsonSchemaError: jk, } // if we have a location within the schema, add it to the error if located != nil { line := located.Line // if the located node is a map or an array, then the actual human interpretable // line on which the violation occurred is the line of the key, not the value. if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode { if line > 0 { line-- } } // location of the violation within the rendered schema. violation.Line = line violation.Column = located.Column } else { // handles property name validation errors that don't provide useful InstanceLocation applyPropertyNameFallback(propertyInfo, info.RootNode.Content[0], violation) } schemaValidationErrors = append(schemaValidationErrors, violation) } } } // add the error to the list validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.Schema, Message: "Document does not pass validation", Reason: fmt.Sprintf("OpenAPI document is not valid according "+ "to the %s specification", info.Version), SchemaValidationErrors: schemaValidationErrors, HowToFix: liberrors.HowToFixInvalidSchema, }) } if len(validationErrors) > 0 { return false, validationErrors } return true, nil } libopenapi-validator-0.13.8/schema_validation/validate_document_test.go000066400000000000000000000434571520534042400264530ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "fmt" "os" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "go.yaml.in/yaml/v4" "github.com/pb33f/libopenapi-validator/config" liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" ) func TestValidateDocument(t *testing.T) { petstore, _ := os.ReadFile("../test_specs/petstorev3.json") doc, _ := libopenapi.NewDocument(petstore) // validate! valid, errors := ValidateOpenAPIDocument(doc) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateDocument_31(t *testing.T) { petstore, _ := os.ReadFile("../test_specs/valid_31.yaml") doc, _ := libopenapi.NewDocument(petstore) // validate! valid, errors := ValidateOpenAPIDocument(doc) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateDocument_Invalid31(t *testing.T) { petstore, _ := os.ReadFile("../test_specs/invalid_31.yaml") doc, _ := libopenapi.NewDocument(petstore) // validate! valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 6) } func TestValidateDocument_UnquotedIntegerResponseCodeHelpfulError(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 paths: /test: get: responses: 200: description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "OpenAPI document validation failed", errors[0].Message) assert.Contains(t, errors[0].Reason, "Response status code keys must be strings") assert.Contains(t, errors[0].Reason, `quote 200 as "200"`) assert.Contains(t, errors[0].Reason, "/paths/~1test/get/responses/200") assert.NotContains(t, errors[0].Reason, "got null, want object") assert.Contains(t, errors[0].HowToFix, `"200"`) assert.Equal(t, 9, errors[0].SpecLine) assert.Equal(t, 9, errors[0].SpecCol) assert.Empty(t, errors[0].SchemaValidationErrors) } func TestValidateDocument_QuotedResponseCodeValid(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 paths: /test: get: responses: "200": description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) valid, errors := ValidateOpenAPIDocument(doc) assert.True(t, valid) assert.Empty(t, errors) } func TestValidateDocument_YAMLMergeKeyValid(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 x-base-response: &baseResponse description: OK paths: /test: get: responses: "200": <<: *baseResponse` doc, _ := libopenapi.NewDocument([]byte(spec)) valid, errors := ValidateOpenAPIDocument(doc) assert.True(t, valid) assert.Empty(t, errors) } func TestValidateDocument_YAMLMergeKeyDoesNotHideInvalidResponseCode(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 x-base-responses: &baseResponses default: description: Default paths: /test: get: responses: <<: *baseResponses 200: description: OK` doc, _ := libopenapi.NewDocument([]byte(spec)) valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Reason, "Response status code keys must be strings") assert.Contains(t, errors[0].Reason, `quote 200 as "200"`) assert.NotContains(t, errors[0].Reason, `merge key "<<"`) assert.Equal(t, 13, errors[0].SpecLine) assert.Empty(t, errors[0].SchemaValidationErrors) } func TestValidateDocument_GenericNonStringMappingKeyHelpfulError(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 paths: {} x-values: 1: one` doc, _ := libopenapi.NewDocument([]byte(spec)) valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "OpenAPI document validation failed", errors[0].Message) assert.Contains(t, errors[0].Reason, "OpenAPI documents require string mapping keys") assert.Contains(t, errors[0].Reason, `int key "1"`) assert.Contains(t, errors[0].Reason, "/x-values/1") assert.NotContains(t, errors[0].Reason, "got null, want object") assert.Contains(t, errors[0].HowToFix, "Quote YAML mapping keys") assert.Equal(t, 7, errors[0].SpecLine) assert.Equal(t, 3, errors[0].SpecCol) assert.Empty(t, errors[0].SchemaValidationErrors) } func TestNormalizeJSON_ReturnsMarshalError(t *testing.T) { payload := map[string]interface{}{ "openapi": "3.1.0", "invalid": map[interface{}]interface{}{ 1: "one", }, } normalized, err := normalizeJSON(payload) assert.Nil(t, normalized) assert.Error(t, err) assert.Contains(t, err.Error(), "unsupported type: map[interface {}]interface {}") } func TestValidateDocument_NormalizationErrorDoesNotValidateNil(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 paths: {}` doc, _ := libopenapi.NewDocument([]byte(spec)) badSpecJSON := map[string]interface{}{ "openapi": "3.1.0", "invalid": map[interface{}]interface{}{ 1: "one", }, } info := doc.GetSpecInfo() info.SpecJSON = &badSpecJSON info.SpecJSONBytes = nil valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "OpenAPI document validation failed", errors[0].Message) assert.Contains(t, errors[0].Reason, "cannot be converted to JSON") assert.Contains(t, errors[0].Reason, "unsupported type: map[interface {}]interface {}") assert.NotContains(t, errors[0].Reason, "got null, want object") assert.Empty(t, errors[0].SchemaValidationErrors) } func TestValidateDocument_CorruptSpecJSONBytesFallbackNormalizationError(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 paths: {}` doc, _ := libopenapi.NewDocument([]byte(spec)) badSpecJSON := map[string]interface{}{ "openapi": "3.1.0", "invalid": map[interface{}]interface{}{ 1: "one", }, } corrupt := []byte(`{not valid json!!!}`) info := doc.GetSpecInfo() info.SpecJSON = &badSpecJSON info.SpecJSONBytes = &corrupt valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Reason, "cannot be converted to JSON") assert.NotContains(t, errors[0].Reason, "got null, want object") assert.Empty(t, errors[0].SchemaValidationErrors) } func TestValidateDocumentHelpers_DefensiveBranches(t *testing.T) { assert.Nil(t, findNonStringMappingKey(nil)) assert.Nil(t, findNonStringMappingKeyInNode(nil, nil)) assert.Nil(t, findNonStringMappingKeyInMergeValue(nil, nil)) assert.False(t, isStringMappingKey(nil)) assert.False(t, isMergeMappingKey(nil)) assert.Equal(t, "", buildJSONPointer(nil)) assert.Equal(t, "non-string", yamlKeyType(nil)) assert.False(t, isOperationResponseStatusCodeKey([]string{"paths", "/test", "parameters", "responses", "200"})) sequenceKey := &nonStringMappingKey{Sequence: true} assert.Equal(t, "sequence", yamlKeyType(sequenceKey)) mergeKey := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!merge", Value: "<<"} assert.True(t, isMergeMappingKey(mergeKey)) intKey := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!int", Value: "1", Line: 2, Column: 5} value := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "one"} mapping := &yaml.Node{ Kind: yaml.MappingNode, Content: []*yaml.Node{intKey, value}, } sequence := &yaml.Node{ Kind: yaml.SequenceNode, Content: []*yaml.Node{mapping}, } found := findNonStringMappingKeyInNode(sequence, []string{"items"}) assert.NotNil(t, found) assert.Equal(t, []string{"items", "0", "1"}, found.Path) assert.Equal(t, "1", found.Value) found = findNonStringMappingKeyInMergeValue(&yaml.Node{Kind: yaml.AliasNode, Alias: mapping}, []string{"merged"}) assert.NotNil(t, found) assert.Equal(t, []string{"merged", "1"}, found.Path) mergeMapping := &yaml.Node{ Kind: yaml.MappingNode, Content: []*yaml.Node{mergeKey, mapping}, } found = findNonStringMappingKeyInNode(mergeMapping, []string{"mergeTarget"}) assert.NotNil(t, found) assert.Equal(t, []string{"mergeTarget", "1"}, found.Path) mergeSequence := &yaml.Node{Kind: yaml.SequenceNode, Content: []*yaml.Node{mapping}} found = findNonStringMappingKeyInMergeValue(mergeSequence, []string{"mergedSequence"}) assert.NotNil(t, found) assert.Equal(t, []string{"mergedSequence", "1"}, found.Path) assert.Nil(t, findNonStringMappingKeyInMergeValue(&yaml.Node{Kind: yaml.SequenceNode}, []string{"empty"})) assert.Nil(t, findNonStringMappingKeyInMergeValue(&yaml.Node{Kind: yaml.AliasNode}, []string{"merged"})) } // Helper function to test the validation logic directly func validateOpenAPIDocumentWithMalformedSchema(loadedSchema string, decodedDocument map[string]interface{}) (bool, []*liberrors.ValidationError) { options := config.NewValidationOptions() var validationErrors []*liberrors.ValidationError // This replicates the exact logic from validate_document.go:40-127 _, err := helpers.NewCompiledSchema("schema", []byte(loadedSchema), options) if err != nil { // schema compilation failed, return validation error instead of panicking // NO SchemaValidationFailure for pre-validation errors like compilation failures validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.Schema, ValidationSubType: "compilation", Message: "OpenAPI document schema compilation failed", Reason: fmt.Sprintf("The OpenAPI schema failed to compile: %s", err.Error()), SpecLine: 1, SpecCol: 0, HowToFix: "check the OpenAPI schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", Context: loadedSchema, }) return false, validationErrors } // If compilation succeeded, continue with normal validation logic // (This would be the rest of the validate_document.go logic) return true, nil } func TestValidateDocument_SchemaCompilationFailure(t *testing.T) { // Test the schema compilation error handling by providing invalid JSON schema malformedSchema := `{"type": "object", "properties": {"test": invalid_json_here}}` decodedDocument := map[string]interface{}{ "openapi": "3.1.0", "info": map[string]interface{}{ "title": "Test API", "version": "1.0.0", }, } // Call our helper function which replicates the exact logic from validate_document.go valid, errors := validateOpenAPIDocumentWithMalformedSchema(malformedSchema, decodedDocument) // Should fail validation due to schema compilation error assert.False(t, valid) assert.NotEmpty(t, errors) // Verify we got a schema compilation error with the exact same structure validationError := errors[0] assert.Equal(t, helpers.Schema, validationError.ValidationType) assert.Equal(t, "compilation", validationError.ValidationSubType) assert.Equal(t, "OpenAPI document schema compilation failed", validationError.Message) assert.Contains(t, validationError.Reason, "The OpenAPI schema failed to compile") assert.Contains(t, validationError.HowToFix, "complex regex patterns") assert.Equal(t, malformedSchema, validationError.Context) assert.Equal(t, 1, validationError.SpecLine) assert.Equal(t, 0, validationError.SpecCol) // Schema compilation errors don't have SchemaValidationFailure objects assert.Empty(t, validationError.SchemaValidationErrors) } // TestValidateDocument_CompilationFailure tests the actual ValidateOpenAPIDocument function // with a corrupted document that causes schema compilation to fail func TestValidateDocument_CompilationFailure(t *testing.T) { doc, _ := libopenapi.NewDocumentWithTypeCheck([]byte(`{}`), true) doc.GetSpecInfo().APISchema = `{"type": "object", "properties": {"test": :bad"": JSON: } here.}}` // validate! valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Reason, "The OpenAPI schema failed to compile") assert.Nil(t, errors[0].SchemaValidationErrors, "Compilation errors should not have SchemaValidationErrors") } func TestValidateSchema_ValidateLicenseIdentifier(t *testing.T) { spec := `openapi: 3.1.0 info: version: 1.0.0 title: Test license: name: Apache 2.0 url: https://opensource.org/licenses/Apache-2.0 identifier: Apache-2.0 components: schemas: Pet: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) // validate! valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 1) } func TestValidateSchema_GeneratePointlessValidation(t *testing.T) { spec := `openapi: 3.1.0 info: version: 1 ` doc, _ := libopenapi.NewDocument([]byte(spec)) // validate! valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 6) } func TestValidateDocument_NilSpecJSON(t *testing.T) { // Create a document with minimal valid OpenAPI content spec := `openapi: 3.1.0 info: version: 1.0.0 title: Test ` doc, _ := libopenapi.NewDocument([]byte(spec)) // Simulate the nil SpecJSON scenario by setting both to nil info := doc.GetSpecInfo() info.SpecJSON = nil info.SpecJSONBytes = nil // validate! valid, errors := ValidateOpenAPIDocument(doc) // Should fail validation due to nil SpecJSON assert.False(t, valid) assert.Len(t, errors, 1) // Verify error structure validationError := errors[0] assert.Equal(t, helpers.Schema, validationError.ValidationType) assert.Equal(t, "document", validationError.ValidationSubType) assert.Equal(t, "OpenAPI document validation failed", validationError.Message) assert.Contains(t, validationError.Reason, "SpecJSON is nil") assert.Contains(t, validationError.HowToFix, "ensure the OpenAPI document is valid") assert.Equal(t, 1, validationError.SpecLine) assert.Equal(t, 0, validationError.SpecCol) // Pre-validation errors should not have SchemaValidationErrors assert.Empty(t, validationError.SchemaValidationErrors) } func TestValidateDocument_WithPrecompiledSchema(t *testing.T) { petstore, _ := os.ReadFile("../test_specs/petstorev3.json") doc, _ := libopenapi.NewDocument(petstore) info := doc.GetSpecInfo() // Pre-compile the schema options := config.NewValidationOptions() compiledSchema, err := helpers.NewCompiledSchema("schema", []byte(info.APISchema), options) assert.NoError(t, err) // Validate with precompiled schema valid, errs := ValidateOpenAPIDocumentWithPrecompiled(doc, compiledSchema) assert.True(t, valid) assert.Len(t, errs, 0) // Validate without precompiled schema (should produce identical results) valid2, errs2 := ValidateOpenAPIDocument(doc) assert.True(t, valid2) assert.Len(t, errs2, 0) } func TestValidateDocument_WithPrecompiledSchema_Invalid(t *testing.T) { petstore, _ := os.ReadFile("../test_specs/invalid_31.yaml") doc, _ := libopenapi.NewDocument(petstore) info := doc.GetSpecInfo() // Pre-compile the schema options := config.NewValidationOptions() compiledSchema, err := helpers.NewCompiledSchema("schema", []byte(info.APISchema), options) assert.NoError(t, err) // Validate with precompiled schema valid, errs := ValidateOpenAPIDocumentWithPrecompiled(doc, compiledSchema) assert.False(t, valid) assert.Len(t, errs, 1) assert.Len(t, errs[0].SchemaValidationErrors, 6) // Validate without precompiled schema (should produce identical error count) valid2, errs2 := ValidateOpenAPIDocument(doc) assert.False(t, valid2) assert.Len(t, errs2, 1) assert.Len(t, errs2[0].SchemaValidationErrors, 6) } func TestValidateDocument_SpecJSONBytesPath(t *testing.T) { petstore, _ := os.ReadFile("../test_specs/petstorev3.json") doc, _ := libopenapi.NewDocument(petstore) info := doc.GetSpecInfo() // Nil out SpecJSON but leave SpecJSONBytes intact — forces the SpecJSONBytes path assert.NotNil(t, info.SpecJSONBytes, "SpecJSONBytes should be populated by libopenapi") info.SpecJSON = nil valid, errs := ValidateOpenAPIDocument(doc) assert.True(t, valid) assert.Len(t, errs, 0) } func TestValidateDocument_SpecJSONBytesCorrupt_NilSpecJSON(t *testing.T) { petstore, _ := os.ReadFile("../test_specs/petstorev3.json") doc, _ := libopenapi.NewDocument(petstore) info := doc.GetSpecInfo() // Put corrupt bytes in SpecJSONBytes so UnmarshalJSON fails, // and nil out SpecJSON so the fallback normalizeJSON path is skipped. // This exercises the nil guard on SpecJSON inside the error branch. corrupt := []byte(`{not valid json!!!}`) info.SpecJSONBytes = &corrupt info.SpecJSON = nil // Validation should fail before JSON Schema validation instead of validating nil. valid, errs := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errs, 1) assert.Contains(t, errs[0].Reason, "SpecJSONBytes cannot be decoded as JSON") assert.Empty(t, errs[0].SchemaValidationErrors) } func TestValidateDocument_SpecJSONBytesCorrupt_FallbackToSpecJSON(t *testing.T) { petstore, _ := os.ReadFile("../test_specs/petstorev3.json") doc, _ := libopenapi.NewDocument(petstore) info := doc.GetSpecInfo() // Put corrupt bytes in SpecJSONBytes so UnmarshalJSON fails, // but leave SpecJSON intact so the fallback to normalizeJSON executes. corrupt := []byte(`{not valid json!!!}`) info.SpecJSONBytes = &corrupt // Should still validate successfully via the SpecJSON fallback valid, errs := ValidateOpenAPIDocument(doc) assert.True(t, valid) assert.Len(t, errs, 0) } func TestValidateDocument_SpecJSONBytesPath_Invalid(t *testing.T) { petstore, _ := os.ReadFile("../test_specs/invalid_31.yaml") doc, _ := libopenapi.NewDocument(petstore) info := doc.GetSpecInfo() // Nil out SpecJSON but leave SpecJSONBytes intact assert.NotNil(t, info.SpecJSONBytes, "SpecJSONBytes should be populated by libopenapi") info.SpecJSON = nil valid, errs := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errs, 1) assert.NotEmpty(t, errs[0].SchemaValidationErrors) } libopenapi-validator-0.13.8/schema_validation/validate_schema.go000066400000000000000000000355171520534042400250340ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "encoding/json" "errors" "fmt" "log/slog" "math" "os" "reflect" "regexp" "strconv" "sync" "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/santhosh-tekuri/jsonschema/v6" "go.yaml.in/yaml/v4" "golang.org/x/text/language" "golang.org/x/text/message" _ "embed" "github.com/pb33f/libopenapi-validator/config" liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" ) // SchemaValidator is an interface that defines the methods for validating a *base.Schema (V3+ Only) object. // There are 6 methods for validating a schema: // // ValidateSchemaString accepts a schema object to validate against, and a JSON/YAML blob that is defined as a string. // ValidateSchemaObject accepts a schema object to validate against, and an object, created from unmarshalled JSON/YAML. // ValidateSchemaBytes accepts a schema object to validate against, and a JSON/YAML blob that is defined as a byte array. // ValidateSchemaStringWithVersion - version-aware validation that allows OpenAPI 3.0 keywords when version is specified. // ValidateSchemaObjectWithVersion - version-aware validation that allows OpenAPI 3.0 keywords when version is specified. // ValidateSchemaBytesWithVersion - version-aware validation that allows OpenAPI 3.0 keywords when version is specified. type SchemaValidator interface { // ValidateSchemaString accepts a schema object to validate against, and a JSON/YAML blob that is defined as a string. // Uses OpenAPI 3.1+ validation by default (strict JSON Schema compliance). ValidateSchemaString(schema *base.Schema, payload string) (bool, []*liberrors.ValidationError) // ValidateSchemaObject accepts a schema object to validate against, and an object, created from unmarshalled JSON/YAML. // This is a pre-decoded object that will skip the need to unmarshal a string of JSON/YAML. // Uses OpenAPI 3.1+ validation by default (strict JSON Schema compliance). ValidateSchemaObject(schema *base.Schema, payload interface{}) (bool, []*liberrors.ValidationError) // ValidateSchemaBytes accepts a schema object to validate against, and a byte slice containing a schema to // validate against. Uses OpenAPI 3.1+ validation by default (strict JSON Schema compliance). ValidateSchemaBytes(schema *base.Schema, payload []byte) (bool, []*liberrors.ValidationError) // ValidateSchemaStringWithVersion accepts a schema object to validate against, a JSON/YAML blob, and an OpenAPI version. // When version is 3.0, OpenAPI 3.0-specific keywords like 'nullable' are allowed and processed. // When version is 3.1+, OpenAPI 3.0-specific keywords like 'nullable' will cause validation to fail. ValidateSchemaStringWithVersion(schema *base.Schema, payload string, version float32) (bool, []*liberrors.ValidationError) // ValidateSchemaObjectWithVersion accepts a schema object to validate against, an object, and an OpenAPI version. // When version is 3.0, OpenAPI 3.0-specific keywords like 'nullable' are allowed and processed. // When version is 3.1+, OpenAPI 3.0-specific keywords like 'nullable' will cause validation to fail. ValidateSchemaObjectWithVersion(schema *base.Schema, payload interface{}, version float32) (bool, []*liberrors.ValidationError) // ValidateSchemaBytesWithVersion accepts a schema object to validate against, a byte slice, and an OpenAPI version. // When version is 3.0, OpenAPI 3.0-specific keywords like 'nullable' are allowed and processed. // When version is 3.1+, OpenAPI 3.0-specific keywords like 'nullable' will cause validation to fail. ValidateSchemaBytesWithVersion(schema *base.Schema, payload []byte, version float32) (bool, []*liberrors.ValidationError) } var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`) type schemaValidator struct { options *config.ValidationOptions logger *slog.Logger lock sync.Mutex } // NewSchemaValidatorWithLogger will create a new SchemaValidator instance, ready to accept schemas and payloads to validate. func NewSchemaValidatorWithLogger(logger *slog.Logger, opts ...config.Option) SchemaValidator { options := config.NewValidationOptions(opts...) return &schemaValidator{options: options, logger: logger, lock: sync.Mutex{}} } // NewSchemaValidator will create a new SchemaValidator instance, ready to accept schemas and payloads to validate. func NewSchemaValidator(opts ...config.Option) SchemaValidator { logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelError, })) return NewSchemaValidatorWithLogger(logger, opts...) } func (s *schemaValidator) ValidateSchemaString(schema *base.Schema, payload string) (bool, []*liberrors.ValidationError) { return s.validateSchemaWithVersion(schema, []byte(payload), nil, s.logger, 3.1) } func (s *schemaValidator) ValidateSchemaObject(schema *base.Schema, payload interface{}) (bool, []*liberrors.ValidationError) { return s.validateSchemaWithVersion(schema, nil, payload, s.logger, 3.1) } func (s *schemaValidator) ValidateSchemaBytes(schema *base.Schema, payload []byte) (bool, []*liberrors.ValidationError) { return s.validateSchemaWithVersion(schema, payload, nil, s.logger, 3.1) } func (s *schemaValidator) ValidateSchemaStringWithVersion(schema *base.Schema, payload string, version float32) (bool, []*liberrors.ValidationError) { return s.validateSchemaWithVersion(schema, []byte(payload), nil, s.logger, version) } func (s *schemaValidator) ValidateSchemaObjectWithVersion(schema *base.Schema, payload interface{}, version float32) (bool, []*liberrors.ValidationError) { return s.validateSchemaWithVersion(schema, nil, payload, s.logger, version) } func (s *schemaValidator) ValidateSchemaBytesWithVersion(schema *base.Schema, payload []byte, version float32) (bool, []*liberrors.ValidationError) { return s.validateSchemaWithVersion(schema, payload, nil, s.logger, version) } func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload []byte, decodedObject interface{}, log *slog.Logger, version float32) (bool, []*liberrors.ValidationError) { var validationErrors []*liberrors.ValidationError if schema == nil { log.Info("schema is empty and cannot be validated. This generally means the schema is missing from the spec, or could not be read.") return false, validationErrors } var renderedSchema []byte var renderedNode *yaml.Node var compiledSchema *jsonschema.Schema // Check cache first — reuses existing SchemaCache (populated by NewValidationOptions). var cacheKey uint64 canCache := s.options.SchemaCache != nil && schema.GoLow() != nil if canCache { // Include version in key so 3.0 (nullable) and 3.1 compile differently. cacheKey = schema.GoLow().Hash() ^ uint64(math.Float32bits(version)) if cached, ok := s.options.SchemaCache.Load(cacheKey); ok && cached != nil && cached.CompiledSchema != nil { renderedSchema = cached.RenderedInline renderedNode = cached.RenderedNode compiledSchema = cached.CompiledSchema } } // Cache miss — render, convert to JSON, and compile. if compiledSchema == nil { renderCtx := base.NewInlineRenderContextForValidation() s.lock.Lock() nodeIface, renderErr := schema.MarshalYAMLInlineWithContext(renderCtx) s.lock.Unlock() if renderErr != nil { validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: "schema does not pass validation", Reason: fmt.Sprintf("The schema cannot be decoded: %s", renderErr.Error()), SpecLine: schema.GoLow().GetRootNode().Line, SpecCol: schema.GoLow().GetRootNode().Column, HowToFix: liberrors.HowToFixInvalidSchema, Context: string(renderedSchema), }) return false, validationErrors } // MarshalYAMLInlineWithContext returns *yaml.Node (from NodeBuilder.Render) renderedNode, _ = nodeIface.(*yaml.Node) // yaml.Node → map → JSON bytes (skips yaml.Marshal + yaml.Unmarshal round-trip) var jsonMap map[string]interface{} if renderedNode != nil { _ = renderedNode.Decode(&jsonMap) } jsonSchema, _ := json.Marshal(jsonMap) // YAML bytes generated once for error messages / context strings renderedSchema, _ = yaml.Marshal(renderedNode) path := "" if schema.GoLow().GetIndex() != nil { path = schema.GoLow().GetIndex().GetSpecAbsolutePath() } var compileErr error compiledSchema, compileErr = helpers.NewCompiledSchemaWithVersion(path, jsonSchema, s.options, version) if compileErr != nil { line := 1 col := 0 if schema.GoLow().Type.KeyNode != nil { line = schema.GoLow().Type.KeyNode.Line col = schema.GoLow().Type.KeyNode.Column } validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.Schema, ValidationSubType: helpers.Schema, Message: "schema compilation failed", Reason: fmt.Sprintf("Schema compilation failed: %s", compileErr.Error()), SpecLine: line, SpecCol: col, HowToFix: liberrors.HowToFixInvalidSchema, Context: string(renderedSchema), }) return false, validationErrors } // Store in cache for subsequent validations of the same schema. if canCache && compiledSchema != nil { s.options.SchemaCache.Store(cacheKey, &cache.SchemaCacheEntry{ Schema: schema, RenderedInline: renderedSchema, ReferenceSchema: string(renderedSchema), RenderedJSON: jsonSchema, CompiledSchema: compiledSchema, RenderedNode: renderedNode, }) } } if decodedObject == nil && len(payload) > 0 { err := json.Unmarshal(payload, &decodedObject) if err != nil { // cannot decode the request body, so it's not valid line := 1 col := 0 if schema.GoLow().Type.KeyNode != nil { line = schema.GoLow().Type.KeyNode.Line col = schema.GoLow().Type.KeyNode.Column } validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: "schema does not pass validation", Reason: fmt.Sprintf("The schema cannot be decoded: %s", err.Error()), SpecLine: line, SpecCol: col, HowToFix: liberrors.HowToFixInvalidSchema, Context: string(renderedSchema), }) return false, validationErrors } } var schemaValidationErrors []*liberrors.SchemaValidationFailure if compiledSchema != nil && decodedObject != nil { scErrs := compiledSchema.Validate(decodedObject) if scErrs != nil { var jk *jsonschema.ValidationError if errors.As(scErrs, &jk) { // flatten the validationErrors schFlatErr := jk.BasicOutput().Errors schemaValidationErrors = extractBasicErrors(schFlatErr, renderedSchema, renderedNode, decodedObject, payload, jk, schemaValidationErrors) } line := 1 col := 0 if schema.GoLow().Type.KeyNode != nil { line = schema.GoLow().Type.KeyNode.Line col = schema.GoLow().Type.KeyNode.Column } validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.Schema, Message: "schema does not pass validation", Reason: "Schema failed to validate against the contract requirements", SpecLine: line, SpecCol: col, SchemaValidationErrors: schemaValidationErrors, HowToFix: liberrors.HowToFixInvalidSchema, Context: string(renderedSchema), }) } } if len(validationErrors) > 0 { return false, validationErrors } return true, nil } func extractBasicErrors(schFlatErrs []jsonschema.OutputUnit, renderedSchema []byte, renderedNode *yaml.Node, decodedObject interface{}, payload []byte, jk *jsonschema.ValidationError, schemaValidationErrors []*liberrors.SchemaValidationFailure, ) []*liberrors.SchemaValidationFailure { // Extract property name info once before processing errors (performance optimization) propertyInfo := extractPropertyNameFromError(jk) // Determine root content node ONCE (not per-error). // NodeBuilder.Render() returns MappingNode directly, no DocumentNode unwrapping needed. var rootNode *yaml.Node if renderedNode != nil { rootNode = renderedNode } else if len(renderedSchema) > 0 { // Fallback: parse bytes ONCE var docNode yaml.Node _ = yaml.Unmarshal(renderedSchema, &docNode) if len(docNode.Content) > 0 { rootNode = docNode.Content[0] } } for q := range schFlatErrs { er := schFlatErrs[q] errMsg := er.Error.Kind.LocalizedString(message.NewPrinter(language.Tag{})) if helpers.IgnoreRegex.MatchString(errMsg) { continue // ignore this error, it's useless tbh, utter noise. } if er.Error != nil { // locate the violated property in the schema var located *yaml.Node if rootNode != nil { located = LocateSchemaPropertyNodeByJSONPath(rootNode, er.KeywordLocation) } // extract the element specified by the instance val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation) var referenceObject string if len(val) > 0 { referenceIndex, _ := strconv.Atoi(val[1]) if reflect.ValueOf(decodedObject).Type().Kind() == reflect.Slice { found := decodedObject.([]any)[referenceIndex] recoded, _ := json.MarshalIndent(found, "", " ") referenceObject = string(recoded) } } if referenceObject == "" { referenceObject = string(payload) } violation := &liberrors.SchemaValidationFailure{ Reason: errMsg, FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), KeywordLocation: er.KeywordLocation, ReferenceSchema: string(renderedSchema), ReferenceObject: referenceObject, OriginalJsonSchemaError: jk, } // if we have a location within the schema, add it to the error if located != nil { line := located.Line // if the located node is a map or an array, then the actual human interpretable // line on which the violation occurred is the line of the key, not the value. if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode { if line > 0 { line-- } } // location of the violation within the rendered schema. violation.Line = line violation.Column = located.Column } else if rootNode != nil { // handles property name validation errors that don't provide useful InstanceLocation applyPropertyNameFallback(propertyInfo, rootNode, violation) } schemaValidationErrors = append(schemaValidationErrors, violation) } } return schemaValidationErrors } libopenapi-validator-0.13.8/schema_validation/validate_schema_coercion_test.go000066400000000000000000000164561520534042400277550ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "encoding/json" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "github.com/pb33f/libopenapi-validator/config" ) func TestSchemaValidator_ScalarCoercion_Boolean(t *testing.T) { spec := `openapi: 3.0.0 paths: /test: post: requestBody: content: application/json: schema: type: object properties: active: type: boolean count: type: integer` doc, _ := libopenapi.NewDocument([]byte(spec)) m, errs := doc.BuildV3Model() assert.Empty(t, errs) schema := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema // Test data with string values that should coerce body := map[string]interface{}{ "active": "true", // String that should coerce to boolean "count": "42", // String that should coerce to integer } bodyBytes, _ := json.Marshal(body) // Test with coercion enabled vWithCoercion := NewSchemaValidator(config.WithScalarCoercion()) valid, errors := vWithCoercion.ValidateSchemaStringWithVersion(schema.Schema(), string(bodyBytes), 3.0) assert.True(t, valid, "Validation should pass with scalar coercion enabled") assert.Empty(t, errors, "Should have no validation errors") // Test with coercion disabled (default) vWithoutCoercion := NewSchemaValidator() valid, errors = vWithoutCoercion.ValidateSchemaStringWithVersion(schema.Schema(), string(bodyBytes), 3.0) assert.False(t, valid, "Validation should fail with scalar coercion disabled") assert.NotEmpty(t, errors, "Should have validation errors") } func TestSchemaValidator_ScalarCoercion_InvalidStrings(t *testing.T) { spec := `openapi: 3.0.0 paths: /test: post: requestBody: content: application/json: schema: type: object properties: active: type: boolean count: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, errs := doc.BuildV3Model() assert.Empty(t, errs) schema := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema // Test data with invalid coercion strings body := map[string]interface{}{ "active": "yes", // Invalid boolean string "count": "abc", // Invalid number string } bodyBytes, _ := json.Marshal(body) // Even with coercion enabled, invalid strings should fail vWithCoercion := NewSchemaValidator(config.WithScalarCoercion()) valid, errors := vWithCoercion.ValidateSchemaStringWithVersion(schema.Schema(), string(bodyBytes), 3.0) assert.False(t, valid, "Validation should fail with invalid coercion strings") assert.NotEmpty(t, errors, "Should have validation errors for invalid strings") } func TestSchemaValidator_ScalarCoercion_MixedTypes(t *testing.T) { spec := `openapi: 3.0.0 paths: /test: post: requestBody: content: application/json: schema: type: object properties: flag: type: boolean score: type: number rank: type: integer name: type: string` doc, _ := libopenapi.NewDocument([]byte(spec)) m, errs := doc.BuildV3Model() assert.Empty(t, errs) schema := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema // Test mixed coercion and normal values body := map[string]interface{}{ "flag": "false", // Boolean coercion "score": "95.5", // Number coercion "rank": "1", // Integer coercion "name": "Alice", // Normal string (no coercion) } bodyBytes, _ := json.Marshal(body) vWithCoercion := NewSchemaValidator(config.WithScalarCoercion()) valid, errors := vWithCoercion.ValidateSchemaStringWithVersion(schema.Schema(), string(bodyBytes), 3.0) assert.True(t, valid, "Validation should pass with mixed coercion") assert.Empty(t, errors, "Should have no validation errors") } func TestSchemaValidator_ScalarCoercion_WithNullable(t *testing.T) { spec := `openapi: 3.0.0 paths: /test: post: requestBody: content: application/json: schema: type: object properties: active: type: boolean nullable: true` doc, _ := libopenapi.NewDocument([]byte(spec)) m, errs := doc.BuildV3Model() assert.Empty(t, errs) schema := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema vWithCoercion := NewSchemaValidator(config.WithScalarCoercion()) // Test coercion with nullable testCases := []struct { name string value interface{} expected bool }{ {"String true", "true", true}, {"String false", "false", true}, {"Boolean true", true, true}, {"Boolean false", false, true}, {"Null value", nil, true}, {"Invalid string", "yes", false}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { body := map[string]interface{}{ "active": tc.value, } bodyBytes, _ := json.Marshal(body) valid, errors := vWithCoercion.ValidateSchemaStringWithVersion(schema.Schema(), string(bodyBytes), 3.0) if tc.expected { assert.True(t, valid, "Should pass: %s", tc.name) assert.Empty(t, errors, "Should have no errors: %s", tc.name) } else { assert.False(t, valid, "Should fail: %s", tc.name) assert.NotEmpty(t, errors, "Should have errors: %s", tc.name) } }) } } func TestSchemaValidator_ScalarCoercion_EdgeCases(t *testing.T) { spec := `openapi: 3.0.0 paths: /test: post: requestBody: content: application/json: schema: type: object properties: scientific: type: number leadingZero: type: integer negative: type: number` doc, _ := libopenapi.NewDocument([]byte(spec)) m, errs := doc.BuildV3Model() assert.Empty(t, errs) schema := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema vWithCoercion := NewSchemaValidator(config.WithScalarCoercion()) // Test edge cases testCases := []struct { body map[string]interface{} expected bool desc string }{ {map[string]interface{}{"scientific": "1.23e-10"}, true, "Scientific notation"}, {map[string]interface{}{"leadingZero": "007"}, false, "Leading zeros not allowed for integers"}, {map[string]interface{}{"negative": "-0"}, true, "Negative zero"}, {map[string]interface{}{"scientific": "1.23E+5"}, true, "Uppercase E notation"}, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { bodyBytes, _ := json.Marshal(tc.body) valid, errors := vWithCoercion.ValidateSchemaStringWithVersion(schema.Schema(), string(bodyBytes), 3.0) if tc.expected { assert.True(t, valid, "Should pass: %s", tc.desc) assert.Empty(t, errors, "Should have no errors: %s", tc.desc) } else { assert.False(t, valid, "Should fail: %s", tc.desc) assert.NotEmpty(t, errors, "Should have errors: %s", tc.desc) } }) } } libopenapi-validator-0.13.8/schema_validation/validate_schema_extract_errors_test.go000066400000000000000000000041201520534042400312030ustar00rootroot00000000000000// Copyright 2026 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "testing" "github.com/santhosh-tekuri/jsonschema/v6" "github.com/stretchr/testify/assert" "go.yaml.in/yaml/v4" "golang.org/x/text/message" ) type stubErrorKind struct { msg string } func (s stubErrorKind) KeywordPath() []string { return nil } func (s stubErrorKind) LocalizedString(_ *message.Printer) string { return s.msg } func adjustedLine(node *yaml.Node) int { line := node.Line if (node.Kind == yaml.MappingNode || node.Kind == yaml.SequenceNode) && line > 0 { line-- } return line } func TestExtractBasicErrors_FallbackRenderedSchema_AdjustsLines(t *testing.T) { renderedSchema := []byte(`type: object required: - item properties: item: type: object`) payload := []byte(`{"item":{}}`) flatErrors := []jsonschema.OutputUnit{ { KeywordLocation: "/properties/item", AbsoluteKeywordLocation: "#/properties/item", InstanceLocation: "/item", Error: &jsonschema.OutputError{ Kind: stubErrorKind{msg: "item is invalid"}, }, }, { KeywordLocation: "/required", AbsoluteKeywordLocation: "#/required", InstanceLocation: "/item", Error: &jsonschema.OutputError{ Kind: stubErrorKind{msg: "required is invalid"}, }, }, } failures := extractBasicErrors(flatErrors, renderedSchema, nil, map[string]any{"item": map[string]any{}}, payload, nil, nil) assert.Len(t, failures, 2) var docNode yaml.Node err := yaml.Unmarshal(renderedSchema, &docNode) assert.NoError(t, err) assert.NotEmpty(t, docNode.Content) mappingNode := LocateSchemaPropertyNodeByJSONPath(docNode.Content[0], "/properties/item") sequenceNode := LocateSchemaPropertyNodeByJSONPath(docNode.Content[0], "/required") assert.NotNil(t, mappingNode) assert.NotNil(t, sequenceNode) assert.Equal(t, adjustedLine(mappingNode), failures[0].Line) assert.Equal(t, mappingNode.Column, failures[0].Column) assert.Equal(t, adjustedLine(sequenceNode), failures[1].Line) assert.Equal(t, sequenceNode.Column, failures[1].Column) } libopenapi-validator-0.13.8/schema_validation/validate_schema_openapi_test.go000066400000000000000000000306231520534042400275770ustar00rootroot00000000000000// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "encoding/json" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "github.com/pb33f/libopenapi-validator/config" ) func TestSchemaValidator_NullableKeyword_OpenAPI30_Success(t *testing.T) { spec := `openapi: 3.0.0 paths: /test: post: requestBody: content: application/json: schema: type: object required: [name] properties: name: type: string nullable: true` doc, _ := libopenapi.NewDocument([]byte(spec)) m, errs := doc.BuildV3Model() assert.Empty(t, errs) schema := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema body := map[string]interface{}{ "name": nil, // This should be valid with nullable: true } bodyBytes, _ := json.Marshal(body) // Test with version 3.0 - should pass v := NewSchemaValidator() valid, errors := v.ValidateSchemaStringWithVersion(schema.Schema(), string(bodyBytes), 3.0) assert.True(t, valid, "Validation should pass with nullable: true in OpenAPI 3.0") assert.Empty(t, errors, "Should have no validation errors") } func TestSchemaValidator_NullableKeyword_OpenAPI31_Fails(t *testing.T) { spec := `openapi: 3.0.0 paths: /test: post: requestBody: content: application/json: schema: type: object required: [name] properties: name: type: string nullable: true` doc, _ := libopenapi.NewDocument([]byte(spec)) m, errs := doc.BuildV3Model() assert.Empty(t, errs) schema := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema body := map[string]interface{}{ "name": nil, } bodyBytes, _ := json.Marshal(body) // Test with version 3.1 - should fail due to nullable keyword v := NewSchemaValidator() valid, errors := v.ValidateSchemaStringWithVersion(schema.Schema(), string(bodyBytes), 3.1) assert.False(t, valid, "Validation should fail with nullable keyword in OpenAPI 3.1") assert.NotEmpty(t, errors, "Should have validation errors") // Check that error mentions nullable keyword not allowed found := false for _, err := range errors { if err.Reason != "" && contains(err.Reason, "nullable") { found = true break } for _, schErr := range err.SchemaValidationErrors { if schErr.Reason != "" && contains(schErr.Reason, "nullable") { found = true break } } } assert.True(t, found, "Error should mention nullable keyword is not allowed") } func TestSchemaValidator_DefaultBehavior_RejectsNullable(t *testing.T) { spec := `openapi: 3.0.0 paths: /test: post: requestBody: content: application/json: schema: type: object properties: name: type: string nullable: true` doc, _ := libopenapi.NewDocument([]byte(spec)) m, errs := doc.BuildV3Model() assert.Empty(t, errs) schema := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema body := map[string]interface{}{ "name": nil, } bodyBytes, _ := json.Marshal(body) // Test default behavior (should be 3.1+ strict mode) - should fail v := NewSchemaValidator() valid, errors := v.ValidateSchemaString(schema.Schema(), string(bodyBytes)) assert.False(t, valid, "Default validation should fail with nullable keyword") assert.NotEmpty(t, errors, "Should have validation errors") } func TestSchemaValidator_OpenAPIModeDisabled(t *testing.T) { spec := `openapi: 3.0.0 paths: /test: post: requestBody: content: application/json: schema: type: object properties: name: type: string nullable: true` doc, _ := libopenapi.NewDocument([]byte(spec)) m, errs := doc.BuildV3Model() assert.Empty(t, errs) schema := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema body := map[string]interface{}{ "name": nil, } bodyBytes, _ := json.Marshal(body) // Test with OpenAPI mode disabled - should ignore nullable keyword entirely v := NewSchemaValidator(config.WithoutOpenAPIMode()) valid, errors := v.ValidateSchemaStringWithVersion(schema.Schema(), string(bodyBytes), 3.0) assert.False(t, valid, "Should fail without OpenAPI vocabulary (nullable ignored)") assert.NotEmpty(t, errors, "Should have validation errors (null vs string type)") } func TestSchemaValidator_DiscriminatorKeyword_Valid(t *testing.T) { spec := `openapi: 3.0.0 paths: /test: post: requestBody: content: application/json: schema: type: object discriminator: propertyName: type mapping: dog: "#/components/schemas/Dog" cat: "#/components/schemas/Cat"` doc, _ := libopenapi.NewDocument([]byte(spec)) m, errs := doc.BuildV3Model() assert.Empty(t, errs) schema := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema body := map[string]interface{}{ "type": "dog", "name": "Buddy", } bodyBytes, _ := json.Marshal(body) // Test with discriminator in OpenAPI 3.0 - should pass v := NewSchemaValidator() valid, errors := v.ValidateSchemaStringWithVersion(schema.Schema(), string(bodyBytes), 3.0) assert.True(t, valid, "Validation should pass with valid discriminator") assert.Empty(t, errors, "Should have no validation errors") } func TestSchemaValidator_MultipleOpenAPIKeywords(t *testing.T) { spec := `openapi: 3.0.0 paths: /test: post: requestBody: content: application/json: schema: type: object properties: name: type: string nullable: true example: "John Doe" deprecated: true` doc, _ := libopenapi.NewDocument([]byte(spec)) m, errs := doc.BuildV3Model() assert.Empty(t, errs) schema := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema body := map[string]interface{}{ "name": nil, } bodyBytes, _ := json.Marshal(body) // Test with multiple OpenAPI keywords in OpenAPI 3.0 - should pass v := NewSchemaValidator() valid, errors := v.ValidateSchemaStringWithVersion(schema.Schema(), string(bodyBytes), 3.0) assert.True(t, valid, "Validation should pass with multiple OpenAPI keywords") assert.Empty(t, errors, "Should have no validation errors") } func TestSchemaValidator_NullableEnum_OriginalCommentedTest(t *testing.T) { // This is the original test case that was commented out spec := `openapi: 3.0.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object required: [name] properties: name: type: string enum: [mcbird, mcbeef, veggie, null] nullable: true patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, errs := doc.BuildV3Model() assert.Empty(t, errs) body := map[string]interface{}{ "name": nil, "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // create a schema validator v := NewSchemaValidator() // validate with OpenAPI 3.0 version - should now pass! valid, errors := v.ValidateSchemaStringWithVersion(sch.Schema(), string(bodyBytes), 3.0) assert.True(t, valid, "Should pass with nullable enum in OpenAPI 3.0") assert.Empty(t, errors, "Should have no validation errors") } func TestValidateSchema_CircularReference(t *testing.T) { spec := `openapi: "3.1.0" info: title: Test version: "1" paths: /: post: operationId: op requestBody: content: application/json: schema: $ref: '#/components/schemas/c' components: schemas: a: type: "string" examples: - '' b: type: "object" examples: - { "z": "" } properties: z: "$ref": '#/components/schemas/a' b: "$ref": '#/components/schemas/b' c: type: "object" examples: - { "b": { "z": "" } } properties: "b": "$ref": '#/components/schemas/b'` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) model, errs := doc.BuildV3Model() assert.Nil(t, errs) schema := model.Model.Paths.PathItems.GetOrZero("/").Post.RequestBody.Content.GetOrZero("application/json").Schema assert.NotNil(t, schema) assert.NotNil(t, schema.Schema()) t.Run("should fail rendering", func(t *testing.T) { _, err := schema.Schema().RenderInline() assert.Error(t, err, "RenderInline should not error on circular refs") }) t.Run("should fail validating", func(t *testing.T) { sv := NewSchemaValidator() schemaB := model.Model.Components.Schemas.GetOrZero("b").Schema() assert.NotNil(t, schemaB) assert.NotNil(t, schemaB.Examples) exampleJSON := `{"z": "", "b": {"z": ""}}` valid, errors := sv.ValidateSchemaString(schemaB, exampleJSON) assert.False(t, valid, "Schema with circular refs should currently fail validation") assert.NotNil(t, errors, "Should have validation errors") foundCompilationError := false for _, err := range errors { if err.Message == "schema does not pass validation" && err.Reason != "" && (err.Reason == "The schema cannot be decoded: schema render failure, circular reference: `#/components/schemas/b`" || err.Reason == "The schema cannot be decoded: schema render failure, circular reference: `#/components/schemas/Node`") { foundCompilationError = true } assert.Nil(t, err.SchemaValidationErrors, "Rendering errors should not have SchemaValidationErrors") } assert.True(t, foundCompilationError, "Should have schema compilation error for circular references") }) } func TestValidateSchema_SimpleCircularReference(t *testing.T) { // Even simpler test case spec := `openapi: "3.1.0" info: title: Test version: "1" paths: /test: get: responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/Node' components: schemas: Node: type: object properties: value: type: string next: $ref: '#/components/schemas/Node' examples: - value: "test" next: value: "nested"` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) model, errs := doc.BuildV3Model() assert.Nil(t, errs) schema := model.Model.Paths.PathItems.GetOrZero("/test").Get.Responses.Codes.GetOrZero("200").Content.GetOrZero("application/json").Schema assert.NotNil(t, schema) assert.NotNil(t, schema.Schema()) // Try to render inline rendered, err := schema.Schema().RenderInline() if err != nil { t.Logf("RenderInline error on simple circular ref: %v", err) } else { t.Logf("RenderInline succeeded for simple circular ref, rendered %d bytes", len(rendered)) } // Validate using schema validator sv := NewSchemaValidator() nodeSchema := model.Model.Components.Schemas.GetOrZero("Node").Schema() // Try to validate an example against the schema exampleJSON := `{"value": "test", "next": {"value": "nested"}}` valid, errors := sv.ValidateSchemaString(nodeSchema, exampleJSON) t.Logf("Simple circular ref - Schema validation valid: %v", valid) for _, err := range errors { t.Logf("Error: %s", err.Error()) } } // Helper function to check if a string contains a substring (case-insensitive) func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(substr) == 0 || (len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || func() bool { for i := 1; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }()))) } libopenapi-validator-0.13.8/schema_validation/validate_schema_test.go000066400000000000000000001026531520534042400260670ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "encoding/json" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "go.yaml.in/yaml/v4" ) func TestLocateSchemaPropertyNodeByJSONPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` var node yaml.Node _ = yaml.Unmarshal([]byte(spec), &node) foundNode := LocateSchemaPropertyNodeByJSONPath(node.Content[0], "/paths/~1burgers~1createBurger/post/requestBody/content/application~1json/schema/properties/vegetarian") assert.Equal(t, "boolean", foundNode.Content[1].Value) foundNode = LocateSchemaPropertyNodeByJSONPath(node.Content[0], "/i/do/not/exist") assert.Nil(t, foundNode) } func TestValidateSchema_SimpleValid_String(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // create a schema validator v := NewSchemaValidator() // validate! valid, errors := v.ValidateSchemaString(sch.Schema(), string(bodyBytes)) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateSchema_SimpleValid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } // create a schema validator v := NewSchemaValidator() bodyBytes, _ := json.Marshal(body) sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // validate! valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateSchema_SimpleInValid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() body := map[string]interface{}{ "name": "Big Mac", "patties": "I am not a number", // will fail "vegetarian": 23, // will fail } // create a schema validator v := NewSchemaValidator() bodyBytes, _ := json.Marshal(body) sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // validate! valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestValidateSchema_InvalidJSONType(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() body := struct{ name string }{"hello world"} // create a schema validator v := NewSchemaValidator() // bodyBytes, _ := json.Marshal(body) sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // validate! valid, errors := v.ValidateSchemaObject(sch.Schema(), body) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 1) assert.Equal(t, "invalid jsonType struct { name string }", errors[0].SchemaValidationErrors[0].Reason) } func TestValidateSchema_ReffyComplex_Valid(t *testing.T) { spec := `openapi: 3.1.0 components: schemas: Death: type: object required: [cakeOrDeath] properties: cakeOrDeath: type: string enum: [death] Cake: type: object required: [cakeOrDeath] properties: cakeOrDeath: type: string enum: [cake please] Four: type: object oneOf: - $ref: '#/components/schemas/Cake' - $ref: '#/components/schemas/Death' Three: type: object properties: name: type: string four: $ref: '#/components/schemas/Four' Two: type: object properties: name: type: string three: $ref: '#/components/schemas/Three' One: type: object properties: name: type: string two: $ref: '#/components/schemas/Two' paths: /burgers/createBurger: post: requestBody: content: application/json: schema: $ref: '#/components/schemas/One'` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() cakePlease := map[string]interface{}{ "two": map[string]interface{}{ "three": map[string]interface{}{ "four": map[string]interface{}{ "cakeOrDeath": "cake please", }, }, }, } death := map[string]interface{}{ "two": map[string]interface{}{ "three": map[string]interface{}{ "four": map[string]interface{}{ "cakeOrDeath": "death", }, }, }, } // cake? (https://www.youtube.com/watch?v=PVH0gZO5lq0) bodyBytes, _ := json.Marshal(cakePlease) sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // create a schema validator v := NewSchemaValidator() // validate! valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) assert.True(t, valid) assert.Len(t, errors, 0) // or death! bodyBytes, _ = json.Marshal(death) sch = m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // validate! valid, errors = v.ValidateSchemaBytes(sch.Schema(), bodyBytes) assert.True(t, valid) assert.Len(t, errors, 0) } func TestValidateSchema_ReffyComplex_Invalid(t *testing.T) { spec := `openapi: 3.1.0 components: schemas: Death: type: object required: [cakeOrDeath] properties: cakeOrDeath: type: string enum: [death] Cake: type: object required: [cakeOrDeath] properties: cakeOrDeath: type: string enum: [cake please] Four: type: object oneOf: - $ref: '#/components/schemas/Cake' - $ref: '#/components/schemas/Death' Three: type: object properties: name: type: string four: $ref: '#/components/schemas/Four' Two: type: object properties: name: type: string three: $ref: '#/components/schemas/Three' One: type: object properties: name: type: string two: $ref: '#/components/schemas/Two' paths: /burgers/createBurger: post: requestBody: content: application/json: schema: $ref: '#/components/schemas/One'` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() cakePlease := map[string]interface{}{ "two": map[string]interface{}{ "three": map[string]interface{}{ "four": map[string]interface{}{ "cakeOrDeath": "no more cake? so the choice is 'or death?'", }, }, }, } death := map[string]interface{}{ "two": map[string]interface{}{ "three": map[string]interface{}{ "four": map[string]interface{}{ "cakeOrDeath": "i'll have the chicken", }, }, }, } // cake? (https://www.youtube.com/watch?v=PVH0gZO5lq0) bodyBytes, _ := json.Marshal(cakePlease) sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // create a schema validator v := NewSchemaValidator() // validate! valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) valid, errors = v.ValidateSchemaObject(sch.Schema(), cakePlease) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) // or death! bodyBytes, _ = json.Marshal(death) sch = m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // validate! valid, errors = v.ValidateSchemaBytes(sch.Schema(), bodyBytes) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) valid, errors = v.ValidateSchemaObject(sch.Schema(), death) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestValidateSchema_EmptySchema(t *testing.T) { // create a schema validator v := NewSchemaValidator() // validate! valid, errors := v.ValidateSchemaObject(nil, nil) assert.False(t, valid) assert.Len(t, errors, 0) } func TestValidateSchema_SimpleInvalid_Multiple(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: array items: type: object required: - name properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() var items []map[string]interface{} items = append(items, map[string]interface{}{ "patties": 1, "vegetarian": true, }) items = append(items, map[string]interface{}{ "name": "Quarter Pounder", "patties": true, "vegetarian": false, }) items = append(items, map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": false, }) bodyBytes, _ := json.Marshal(items) // create a schema validator v := NewSchemaValidator() sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // validate! valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestValidateSchema_BadJSON(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() bodyBytes := []byte("{\"bad\": \"json\",}") sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // create a schema validator v := NewSchemaValidator() // validate! valid, errors := v.ValidateSchemaString(sch.Schema(), string(bodyBytes)) assert.False(t, valid) assert.Len(t, errors, 1) assert.Contains(t, errors[0].Reason, "invalid character '}' looking for beginning of object key string") assert.Nil(t, errors[0].SchemaValidationErrors) } func TestValidateSchema_CompilationFailure(t *testing.T) { // Test that schema compilation failure doesn't create SchemaValidationErrors // This uses an extremely complex regex that might fail compilation in some regex engines spec := `openapi: 3.1.0 paths: /test: post: requestBody: content: application/json: schema: type: object properties: password: type: string pattern: '(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}(?:(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,})*'` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() sch := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema // create a schema validator with strict regex engine that might fail on complex patterns v := NewSchemaValidator() // Try to validate - if compilation fails, we should get an error without SchemaValidationErrors validData := `{"password": "ValidPass123!"}` valid, errors := v.ValidateSchemaString(sch.Schema(), validData) // This test is environment-dependent - compilation might succeed or fail depending on regex engine // If it fails, we want to ensure SchemaValidationErrors is nil if !valid && len(errors) > 0 { for _, err := range errors { if err.Message == "schema compilation failed" { // Compilation failure should NOT have SchemaValidationErrors assert.Nil(t, err.SchemaValidationErrors, "Schema compilation errors should not have SchemaValidationErrors") t.Logf("Schema compilation failed as expected: %s", err.Reason) } } } else { t.Skip("Regex engine handled the complex pattern - skipping compilation failure test") } } //// https://github.com/pb33f/libopenapi-validator/issues/26 //func TestValidateSchema_v3_0_BooleanExclusiveMinimum(t *testing.T) { // // spec := `openapi: 3.0.0 //paths: // /burgers/createBurger: // post: // requestBody: // content: // application/json: // schema: // type: object // properties: // amount: // type: number // minimum: 0 // exclusiveMinimum: true` // // doc, _ := libopenapi.NewDocument([]byte(spec)) // // m, _ := doc.BuildV3Model() // // body := map[string]interface{}{"amount": 3} // // bodyBytes, _ := json.Marshal(body) // sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger"].Post.RequestBody.Content.GetOrZero("application/json"].Schema // // // create a schema validator // v := NewSchemaValidator() // // // validate! // valid, errors := v.ValidateSchemaString(sch.Schema(), string(bodyBytes)) // // assert.True(t, valid) // assert.Empty(t, errors) // //} // https://github.com/pb33f/libopenapi-validator/issues/26 func TestValidateSchema_v3_0_NumericExclusiveMinimum(t *testing.T) { spec := `openapi: 3.0.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: amount: type: number exclusiveMinimum: 0` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() body := map[string]interface{}{"amount": 3} bodyBytes, _ := json.Marshal(body) sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // create a schema validator v := NewSchemaValidator() // validate! valid, errors := v.ValidateSchemaString(sch.Schema(), string(bodyBytes)) assert.False(t, valid) assert.NotEmpty(t, errors) } // https://github.com/pb33f/libopenapi-validator/issues/26 func TestValidateSchema_v3_1_NumericExclusiveMinimum(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: amount: type: number exclusiveMinimum: 0` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() body := map[string]interface{}{"amount": 3} bodyBytes, _ := json.Marshal(body) sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // create a schema validator v := NewSchemaValidator() // validate! valid, errors := v.ValidateSchemaString(sch.Schema(), string(bodyBytes)) assert.True(t, valid) assert.Empty(t, errors) } func TestValidateSchema_NullableEnum(t *testing.T) { spec := `openapi: 3.0.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object required: [name] properties: name: type: string enum: [mcbird, mcbeef, veggie, null] nullable: true patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) m, errs := doc.BuildV3Model() assert.Empty(t, errs) body := map[string]interface{}{ "name": nil, "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // create a schema validator v := NewSchemaValidator() // validate with OpenAPI 3.0 version - should pass with nullable support! valid, errors := v.ValidateSchemaStringWithVersion(sch.Schema(), string(bodyBytes), 3.0) assert.True(t, valid) assert.Len(t, errors, 0) // validate with default behavior (3.1+) - should fail due to nullable keyword valid, errors = v.ValidateSchemaString(sch.Schema(), string(bodyBytes)) assert.False(t, valid) assert.NotEmpty(t, errors) } // https://github.com/pb33f/libopenapi/issues/415 func TestValidateSchema_v3_1_DependentSchemas(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: fishCake: type: object properties: bones: type: boolean dependentSchemas: fishCake: type: object properties: cream: type: number format: double required: - cream` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() exp := `{ "fishCake": { "bones": true, "cream": 2.5 } }` sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // create a schema validator v := NewSchemaValidator() // validate! valid, errors := v.ValidateSchemaString(sch.Schema(), exp) assert.True(t, valid) assert.Empty(t, errors) } // https://github.com/pb33f/libopenapi/issues/415 func TestValidateSchema_v3_1_DependentSchemas_NotValid(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: fishCake: type: object properties: bones: type: boolean dependentSchemas: fishCake: type: object properties: cream: type: number format: double required: - cream` doc, _ := libopenapi.NewDocument([]byte(spec)) m, _ := doc.BuildV3Model() exp := `{ "fishCake": { "bones": true, } "cream": 2.5 }` sch := m.Model.Paths.PathItems.GetOrZero("/burgers/createBurger").Post.RequestBody.Content.GetOrZero("application/json").Schema // create a schema validator v := NewSchemaValidator() // validate! valid, errors := v.ValidateSchemaString(sch.Schema(), exp) assert.False(t, valid) assert.NotEmpty(t, errors) } // Additional comprehensive tests for version-aware validation func TestSchemaValidator_ValidateSchemaStringWithVersion_NilSchema(t *testing.T) { v := NewSchemaValidator() valid, errors := v.ValidateSchemaStringWithVersion(nil, `{"test": "value"}`, 3.0) assert.False(t, valid, "Should fail with nil schema") assert.Empty(t, errors, "Should not return errors for nil schema") } func TestSchemaValidator_ValidateSchemaObjectWithVersion_NilSchema(t *testing.T) { v := NewSchemaValidator() testObj := map[string]interface{}{ "test": "value", } valid, errors := v.ValidateSchemaObjectWithVersion(nil, testObj, 3.0) assert.False(t, valid, "Should fail with nil schema") assert.Empty(t, errors, "Should not return errors for nil schema") } func TestSchemaValidator_ValidateSchemaBytesWithVersion_NilSchema(t *testing.T) { v := NewSchemaValidator() payload := []byte(`{"test": "value"}`) valid, errors := v.ValidateSchemaBytesWithVersion(nil, payload, 3.0) assert.False(t, valid, "Should fail with nil schema") assert.Empty(t, errors, "Should not return errors for nil schema") } // https://github.com/daveshanley/vacuum/issues/520 func TestValidateSchema_OneOf_MultipleMatches_Issue520(t *testing.T) { // This test reproduces the issue from vacuum #520 // The example matches BOTH oneOf alternatives which should fail validation // but the error details are not being populated correctly spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 components: schemas: Test: type: object oneOf: - properties: pim: type: string - properties: pam: type: string ` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) model, errs := doc.BuildV3Model() assert.Empty(t, errs) testSchema := model.Model.Components.Schemas.GetOrZero("Test").Schema() testData := map[string]interface{}{ "pam": "nop", } validator := NewSchemaValidator() valid, validationErrors := validator.ValidateSchemaObject(testSchema, testData) assert.False(t, valid, "validation should fail because example matches both oneOf alternatives") assert.NotEmpty(t, validationErrors, "validation errors should be present") if len(validationErrors) > 0 { assert.NotEmpty(t, validationErrors[0].SchemaValidationErrors, "SchemaValidationErrors should contain detailed error information about the oneOf violation") if len(validationErrors[0].SchemaValidationErrors) > 0 { firstError := validationErrors[0].SchemaValidationErrors[0] assert.Contains(t, firstError.Reason, "oneOf", "error should mention oneOf constraint violation") } } } // https://github.com/daveshanley/vacuum/issues/520 func TestValidateSchema_OneOf_Discriminant_Valid(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 components: schemas: Test: type: object oneOf: - properties: type: const: pim pim: type: string required: [type, pim] - properties: type: const: pam pam: type: string required: [type, pam]` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) model, errs := doc.BuildV3Model() assert.Empty(t, errs) testSchema := model.Model.Components.Schemas.GetOrZero("Test").Schema() testData := map[string]interface{}{ "type": "pam", "pam": "nop", } validator := NewSchemaValidator() valid, validationErrors := validator.ValidateSchemaObject(testSchema, testData) assert.True(t, valid, "validation should pass for discriminant oneOf") assert.Empty(t, validationErrors, "no validation errors should be present") } // https://github.com/daveshanley/vacuum/issues/520 func TestValidateSchema_OneOf_NoMatches(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test version: 1.0.0 components: schemas: Test: type: object oneOf: - properties: foo: type: string required: [foo] - properties: bar: type: integer required: [bar]` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) model, errs := doc.BuildV3Model() assert.Empty(t, errs) testSchema := model.Model.Components.Schemas.GetOrZero("Test").Schema() testData := map[string]interface{}{ "baz": "invalid", } validator := NewSchemaValidator() valid, validationErrors := validator.ValidateSchemaObject(testSchema, testData) assert.False(t, valid, "validation should fail because example matches no oneOf alternatives") assert.NotEmpty(t, validationErrors, "validation errors should be present") if len(validationErrors) > 0 { assert.NotEmpty(t, validationErrors[0].SchemaValidationErrors, "SchemaValidationErrors should contain detailed error information") } } // https://github.com/daveshanley/vacuum/issues/520 func TestValidateSchema_OneOf_SimpleTypes(t *testing.T) { testCases := []struct { name string spec string value interface{} shouldPass bool errorDetail string }{ { name: "valid string", spec: `openapi: 3.1.0 info: title: Test version: 1.0.0 components: schemas: Test: oneOf: - type: string - type: integer`, value: "hello", shouldPass: true, }, { name: "valid integer", spec: `openapi: 3.1.0 info: title: Test version: 1.0.0 components: schemas: Test: oneOf: - type: string - type: integer`, value: 42, shouldPass: true, }, { name: "invalid - matches both (ambiguous pattern)", spec: `openapi: 3.1.0 info: title: Test version: 1.0.0 components: schemas: Test: oneOf: - type: string - type: string pattern: '^[0-9]+$'`, value: "123", shouldPass: false, errorDetail: "oneOf", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { doc, err := libopenapi.NewDocument([]byte(tc.spec)) assert.NoError(t, err) model, errs := doc.BuildV3Model() assert.Empty(t, errs) testSchema := model.Model.Components.Schemas.GetOrZero("Test").Schema() validator := NewSchemaValidator() valid, validationErrors := validator.ValidateSchemaObject(testSchema, tc.value) if tc.shouldPass { assert.True(t, valid, "validation should pass") assert.Empty(t, validationErrors, "no validation errors expected") } else { assert.False(t, valid, "validation should fail") assert.NotEmpty(t, validationErrors, "validation errors should be present") if len(validationErrors) > 0 && tc.errorDetail != "" { if len(validationErrors[0].SchemaValidationErrors) > 0 { firstError := validationErrors[0].SchemaValidationErrors[0] assert.Contains(t, firstError.Reason, tc.errorDetail, "error should contain expected detail") } } } }) } } // TestValidateSchema_Discriminator_OneOf_WithRefs_Issue788 tests that validation works correctly // when a schema has discriminator + oneOf with $ref to component schemas. // This was a regression in vacuum v0.21.2+ where the validator would fail with // "JSON schema compile failed: json-pointer not found" because discriminator refs // were being preserved instead of inlined. // https://github.com/daveshanley/vacuum/issues/788 func TestValidateSchema_Discriminator_OneOf_WithRefs_Issue788(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test Discriminator OneOf With Refs version: 1.0.0 components: schemas: ProductWidget: type: object required: - productName - quantity - color properties: productName: type: string enum: - Widget quantity: type: integer minimum: 1 color: type: string enum: - Red - Blue - Green ProductGadget: type: object required: - productName - quantity - size properties: productName: type: string enum: - Gadget quantity: type: integer minimum: 1 size: type: string enum: - Small - Medium - Large Product: oneOf: - $ref: '#/components/schemas/ProductWidget' - $ref: '#/components/schemas/ProductGadget' discriminator: propertyName: productName` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) model, errs := doc.BuildV3Model() assert.Empty(t, errs) productSchema := model.Model.Components.Schemas.GetOrZero("Product").Schema() // Valid Widget product validWidget := map[string]interface{}{ "productName": "Widget", "quantity": 1, "color": "Green", } validator := NewSchemaValidator() valid, validationErrors := validator.ValidateSchemaObject(productSchema, validWidget) // This should pass without "json-pointer not found" error assert.True(t, valid, "validation should pass for valid product with discriminator oneOf refs") assert.Empty(t, validationErrors, "no validation errors should be present") } // TestValidateSchema_Discriminator_AnyOf_WithRefs tests anyOf with discriminator and $refs func TestValidateSchema_Discriminator_AnyOf_WithRefs(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test Discriminator AnyOf With Refs version: 1.0.0 components: schemas: Cat: type: object required: - petType - meow properties: petType: type: string const: cat meow: type: boolean Dog: type: object required: - petType - bark properties: petType: type: string const: dog bark: type: boolean Pet: anyOf: - $ref: '#/components/schemas/Cat' - $ref: '#/components/schemas/Dog' discriminator: propertyName: petType` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) model, errs := doc.BuildV3Model() assert.Empty(t, errs) petSchema := model.Model.Components.Schemas.GetOrZero("Pet").Schema() // Valid cat validCat := map[string]interface{}{ "petType": "cat", "meow": true, } validator := NewSchemaValidator() valid, validationErrors := validator.ValidateSchemaObject(petSchema, validCat) assert.True(t, valid, "validation should pass for valid cat with discriminator anyOf refs") assert.Empty(t, validationErrors, "no validation errors should be present") } // TestValidateSchema_Discriminator_OneOf_WithRefs_InvalidData tests that invalid data // still fails validation correctly (not a false negative) func TestValidateSchema_Discriminator_OneOf_WithRefs_InvalidData(t *testing.T) { spec := `openapi: 3.1.0 info: title: Test Discriminator OneOf Invalid version: 1.0.0 components: schemas: ProductWidget: type: object required: - productName - quantity - color properties: productName: type: string enum: - Widget quantity: type: integer minimum: 1 color: type: string enum: - Red - Blue ProductGadget: type: object required: - productName - quantity - size properties: productName: type: string enum: - Gadget quantity: type: integer minimum: 1 size: type: string Product: oneOf: - $ref: '#/components/schemas/ProductWidget' - $ref: '#/components/schemas/ProductGadget' discriminator: propertyName: productName` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) model, errs := doc.BuildV3Model() assert.Empty(t, errs) productSchema := model.Model.Components.Schemas.GetOrZero("Product").Schema() // Invalid product - missing required field 'color' for Widget invalidProduct := map[string]interface{}{ "productName": "Widget", "quantity": 1, // missing required 'color' field } validator := NewSchemaValidator() valid, validationErrors := validator.ValidateSchemaObject(productSchema, invalidProduct) // This should fail because 'color' is required for Widget assert.False(t, valid, "validation should fail for invalid product") assert.NotEmpty(t, validationErrors, "validation errors should be present") } libopenapi-validator-0.13.8/schema_validation/validate_urlencoded.go000066400000000000000000000216051520534042400257110ustar00rootroot00000000000000package schema_validation import ( "encoding/json" "fmt" "log/slog" "net/url" "regexp" "slices" "sort" "strconv" "strings" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi/orderedmap" ) var rxReserved = regexp.MustCompile(`[:/?#\[\]@!$&'()*+,;=]`) func TransformURLEncodedToSchemaJSON(bodyString string, schema *base.Schema, encoding *orderedmap.Map[string, *v3.Encoding]) (map[string]any, []*errors.ValidationError) { rawValues, err := url.ParseQuery(bodyString) if err != nil { return nil, []*errors.ValidationError{errors.InvalidURLEncodedParsing("empty form-urlencoded context", bodyString)} } jsonMap := unflattenValues(rawValues) var validationErrors []*errors.ValidationError if schema != nil { if schema.Properties != nil { for pair := orderedmap.First(schema.Properties); pair != nil; pair = pair.Next() { propName := pair.Key() propSchema := pair.Value().Schema() var contentEncoding *v3.Encoding var allowReserved bool if encoding != nil { contentEncoding = encoding.GetOrZero(propName) if contentEncoding != nil { allowReserved = contentEncoding.AllowReserved } } if val, exists := jsonMap[propName]; exists { newVal, err := applyEncodingRules(val, contentEncoding, propSchema) if err != nil { contentType := "" if contentEncoding != nil { contentType = contentEncoding.ContentType } validationErrors = append(validationErrors, errors.InvalidTypeEncoding(propSchema, propName, contentType)) } else { jsonMap[propName] = newVal val = newVal } validateEncodingRecursive(propName, val, allowReserved, &validationErrors, propSchema) } } } coerced := coerceValue(jsonMap, schema) if asMap, ok := coerced.(map[string]any); ok { jsonMap = asMap } } return jsonMap, validationErrors } func applyEncodingRules(data any, encoding *v3.Encoding, schema *base.Schema) (any, error) { style := "form" explode := true contentType := "" if encoding != nil { contentType = encoding.ContentType if encoding.Style != "" { style = encoding.Style contentType = "" } if encoding.AllowReserved { contentType = "" } if encoding.Explode != nil { explode = *encoding.Explode contentType = "" } else if style != "form" { explode = false } } if contentType != "" && !IsURLEncodedContentType(contentType) && !strings.Contains(contentType, "text/plain") { if strVal, ok := data.(string); ok { if strings.Contains(contentType, helpers.JSONContentType) { var parsed any if err := json.Unmarshal([]byte(strVal), &parsed); err == nil { return parsed, nil } return nil, fmt.Errorf("value matches content-type '%s' but could not be parsed", contentType) } } } if isArraySchema(schema) { if strVal, ok := data.(string); ok { if !explode { switch style { case helpers.Form: return strings.Split(strVal, ","), nil case helpers.SpaceDelimited: return strings.Split(strVal, " "), nil case helpers.PipeDelimited: return strings.Split(strVal, "|"), nil } } } } if style == helpers.DeepObject { if _, ok := data.(map[string]any); !ok { return data, nil } } return data, nil } func unflattenValues(values url.Values) map[string]any { result := make(map[string]any) for k, v := range values { if strings.Contains(k, "[") { buildDeepMap(result, k, v) } else { if len(v) == 1 { result[k] = v[0] } else { result[k] = v } } } return result } func buildDeepMap(root map[string]any, key string, value []string) { parts := strings.FieldsFunc(key, func(r rune) bool { return r == '[' || r == ']' }) current := root for i, part := range parts { isLeaf := i == len(parts)-1 if isLeaf { if len(value) == 1 { current[part] = value[0] } else { current[part] = value } } else { if _, ok := current[part]; !ok { current[part] = make(map[string]any) } if nextMap, ok := current[part].(map[string]any); ok { current = nextMap } else { return } } } } func validateEncodingRecursive(path string, val any, allowReserved bool, errs *[]*errors.ValidationError, schema *base.Schema) { if allowReserved { return } switch v := val.(type) { case string: if rxReserved.MatchString(v) { *errs = append(*errs, errors.ReservedURLEncodedValue(schema, path, v)) } case []any: for i, item := range v { validateEncodingRecursive(fmt.Sprintf("%s[%d]", path, i), item, allowReserved, errs, schema) } case map[string]any: for k, item := range v { validateEncodingRecursive(fmt.Sprintf("%s[%s]", path, k), item, allowReserved, errs, schema) } case []string: for i, item := range v { if rxReserved.MatchString(item) { *errs = append(*errs, errors.ReservedURLEncodedValue(schema, fmt.Sprintf("%s[%d]", path, i), item)) } } } } func coerceValue(data any, schema *base.Schema) any { if schema == nil { return data } targetTypes := []string{} if len(schema.Type) > 0 { targetTypes = append(targetTypes, schema.Type...) } extractTypes := func(proxies []*base.SchemaProxy) { for _, proxy := range proxies { sch := proxy.Schema() if len(sch.Type) > 0 { targetTypes = append(targetTypes, sch.Type...) } } } extractTypes(schema.AllOf) extractTypes(schema.OneOf) extractTypes(schema.AnyOf) if len(targetTypes) == 0 { return data } for _, t := range targetTypes { converted, ok := tryConvert(data, t, schema) if ok { return converted } } return data } func tryConvert(data any, targetType string, schema *base.Schema) (any, bool) { var strVal string var isString bool switch v := data.(type) { case string: strVal = v isString = true case []string: if len(v) > 0 { strVal = v[0] isString = true } } switch targetType { case helpers.Integer: if !isString || strVal == "" { return nil, false } i, err := strconv.ParseInt(strVal, 10, 64) if err == nil { return i, true } case helpers.Number: if !isString || strVal == "" { return nil, false } f, err := strconv.ParseFloat(strVal, 64) if err == nil { return f, true } case helpers.Boolean: if !isString { return nil, false } b, err := strconv.ParseBool(strVal) if err == nil { return b, true } case helpers.String: if isString { return strVal, true } return fmt.Sprintf("%v", data), true case helpers.Array: var arr []any itemSchema := getSchemaItem(schema) if vSlice, ok := data.([]any); ok { for _, s := range vSlice { arr = append(arr, coerceValue(s, itemSchema)) } return arr, true } if vStringSlice, ok := data.([]string); ok { for _, s := range vStringSlice { arr = append(arr, coerceValue(s, itemSchema)) } return arr, true } if vMap, ok := data.(map[string]any); ok { keys := make([]int, 0, len(vMap)) mapIsArray := true for k := range vMap { idx, err := strconv.Atoi(k) if err != nil { mapIsArray = false break } keys = append(keys, idx) } if mapIsArray { sort.Ints(keys) for _, k := range keys { val := vMap[strconv.Itoa(k)] arr = append(arr, coerceValue(val, itemSchema)) } return arr, true } } if isString { arr = append(arr, coerceValue(strVal, itemSchema)) return arr, true } case helpers.Object: if m, ok := data.(map[string]any); ok { newMap := make(map[string]any) for k, v := range m { newMap[k] = v } if schema.Properties != nil { for pair := orderedmap.First(schema.Properties); pair != nil; pair = pair.Next() { propName := pair.Key() if val, exists := newMap[propName]; exists { newMap[propName] = coerceValue(val, pair.Value().Schema()) } } } return newMap, true } } return nil, false } func isArraySchema(schema *base.Schema) bool { if schema == nil { return false } return slices.Contains(schema.Type, helpers.Array) } func getSchemaItem(schema *base.Schema) *base.Schema { if schema.Items != nil && schema.Items.IsA() { return schema.Items.A.Schema() } return nil } func (v *urlEncodedValidator) validateURLEncodedWithVersion(schema *base.Schema, encoding *orderedmap.Map[string, *v3.Encoding], bodyString string, log *slog.Logger, version float32) (bool, []*errors.ValidationError) { if schema == nil { log.Info("schema is empty and cannot be validated") return false, nil } transformedJSON, prevalidationErrors := TransformURLEncodedToSchemaJSON(bodyString, schema, encoding) if len(prevalidationErrors) > 0 { return false, prevalidationErrors } return v.schemaValidator.validateSchemaWithVersion(schema, nil, transformedJSON, log, version) } func IsURLEncodedContentType(mediaType string) bool { mt := strings.ToLower(strings.TrimSpace(mediaType)) return strings.HasPrefix(mt, helpers.URLEncodedContentType) } libopenapi-validator-0.13.8/schema_validation/validate_urlencoded_test.go000066400000000000000000000362451520534042400267560ustar00rootroot00000000000000package schema_validation import ( "testing" "github.com/pb33f/libopenapi" derrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" ) func TestIsURLEncodedContentType(t *testing.T) { tests := []struct { input string expected bool }{ {"application/x-www-form-urlencoded", true}, {"APPLICATION/X-WWW-FORM-URLENCODED", true}, {"application/x-www-form-urlencoded; charset=utf-8", true}, {"application/json", false}, {"", false}, } for _, tt := range tests { assert.Equal(t, tt.expected, IsURLEncodedContentType(tt.input)) } } func TestUnflattenValues(t *testing.T) { vals := map[string][]string{ "simple": {"val"}, "arr[]": {"1", "2"}, "obj[prop]": {"v1"}, "deep[a][b]": {"v2"}, "double": {"1", "2"}, } result := unflattenValues(vals) assert.Equal(t, "val", result["simple"]) assert.Equal(t, []string{"1", "2"}, result["arr"]) assert.Equal(t, []string{"1", "2"}, result["double"]) obj, ok := result["obj"].(map[string]any) assert.True(t, ok) assert.Equal(t, "v1", obj["prop"]) deep, ok := result["deep"].(map[string]any) assert.True(t, ok) inner, ok := deep["a"].(map[string]any) assert.True(t, ok) assert.Equal(t, "v2", inner["b"]) } func TestBuildDeepMap_BranchCoverage(t *testing.T) { root := make(map[string]any) root["collision"] = "string_value" buildDeepMap(root, "collision[sub]", []string{"val"}) assert.Equal(t, "string_value", root["collision"]) root2 := make(map[string]any) buildDeepMap(root2, "arr[key]", []string{"a", "b"}) inner := root2["arr"].(map[string]any) assert.Equal(t, []string{"a", "b"}, inner["key"]) } func TestTransformURLEncodedToSchemaJSON(t *testing.T) { t.Run("Malformed URL Encoding", func(t *testing.T) { res, errs := TransformURLEncodedToSchemaJSON("bad_encoding=%zz", nil, nil) assert.Nil(t, res) assert.Len(t, errs, 1) assert.Equal(t, helpers.URLEncodedValidation, errs[0].ValidationType) }) t.Run("Schema is Nil", func(t *testing.T) { res, errs := TransformURLEncodedToSchemaJSON("foo=bar", nil, nil) assert.Empty(t, errs) assert.Equal(t, "bar", res["foo"]) }) t.Run("Apply Encoding Rules & Reserved Characters", func(t *testing.T) { props := orderedmap.New[string, *base.SchemaProxy]() props.Set("jsonField", base.CreateSchemaProxy(&base.Schema{Type: []string{helpers.Object}})) props.Set("restricted", base.CreateSchemaProxy(&base.Schema{Type: []string{helpers.String}})) schema := &base.Schema{Properties: props} encodings := orderedmap.New[string, *v3.Encoding]() encodings.Set("jsonField", &v3.Encoding{ContentType: helpers.JSONContentType}) encodings.Set("restricted", &v3.Encoding{AllowReserved: false}) body := `jsonField={"id":1}&restricted=badvalue!` res, errs := TransformURLEncodedToSchemaJSON(body, schema, encodings) assert.IsType(t, map[string]any{}, res["jsonField"]) assert.Len(t, errs, 1) assert.Contains(t, errs[0].Message, "contains reserved characters") }) t.Run("Encoding Error (Invalid JSON content type)", func(t *testing.T) { props := orderedmap.New[string, *base.SchemaProxy]() props.Set("badJson", base.CreateSchemaProxy(&base.Schema{})) schema := &base.Schema{Properties: props} encodings := orderedmap.New[string, *v3.Encoding]() encodings.Set("badJson", &v3.Encoding{ContentType: helpers.JSONContentType}) res, errs := TransformURLEncodedToSchemaJSON(`badJson={invalid`, schema, encodings) assert.Len(t, errs, 1) assert.Equal(t, helpers.URLEncodedValidation, errs[0].ValidationType) assert.Equal(t, "{invalid", res["badJson"]) }) t.Run("Coercion triggered", func(t *testing.T) { props := orderedmap.New[string, *base.SchemaProxy]() props.Set("num", base.CreateSchemaProxy(&base.Schema{Type: []string{helpers.Integer}})) schema := &base.Schema{Properties: props, Type: []string{helpers.Object}} res, errs := TransformURLEncodedToSchemaJSON("num=123", schema, nil) assert.Empty(t, errs) assert.Equal(t, int64(123), res["num"]) }) } func TestApplyEncodingRules(t *testing.T) { boolPtr := func(b bool) *bool { return &b } t.Run("DeepObject Style", func(t *testing.T) { enc := &v3.Encoding{Style: "deepObject"} res, _ := applyEncodingRules("not-map", enc, nil) assert.Equal(t, "not-map", res) m := map[string]any{"k": "v"} res2, _ := applyEncodingRules(m, enc, nil) assert.Equal(t, m, res2) }) t.Run("Array Delimiters", func(t *testing.T) { schema := &base.Schema{Type: []string{helpers.Array}} encSpace := &v3.Encoding{Style: "spaceDelimited"} res, _ := applyEncodingRules("a b c", encSpace, schema) assert.Equal(t, []string{"a", "b", "c"}, res) encPipe := &v3.Encoding{Style: "pipeDelimited"} res, _ = applyEncodingRules("a|b|c", encPipe, schema) assert.Equal(t, []string{"a", "b", "c"}, res) encForm := &v3.Encoding{Style: "form", Explode: boolPtr(false)} res, _ = applyEncodingRules("a,b,c", encForm, schema) assert.Equal(t, []string{"a", "b", "c"}, res) }) } func TestValidateEncodingRecursive(t *testing.T) { var errs []*derrors.ValidationError validateEncodingRecursive("p", "val!", true, &errs, nil) assert.Empty(t, errs) validateEncodingRecursive("p", "val!", false, &errs, nil) assert.Len(t, errs, 1) errs = nil validateEncodingRecursive("arr", []any{"ok", "bad!"}, false, &errs, nil) assert.Len(t, errs, 1) errs = nil validateEncodingRecursive("map", map[string]any{"k": "bad!"}, false, &errs, nil) assert.Len(t, errs, 1) errs = nil validateEncodingRecursive("s_arr", []string{"ok", "bad!"}, false, &errs, nil) assert.Len(t, errs, 1) } func TestCoerceValue(t *testing.T) { schemaInt := &base.Schema{Type: []string{helpers.Integer}} schemaNum := &base.Schema{Type: []string{helpers.Number}} schemaBool := &base.Schema{Type: []string{helpers.Boolean}} schemaStr := &base.Schema{Type: []string{helpers.String}} t.Run("Complex Schema Aggregation (AllOf)", func(t *testing.T) { s := &base.Schema{ AllOf: []*base.SchemaProxy{ base.CreateSchemaProxy(schemaInt), }, } res := coerceValue("123", s) assert.Equal(t, int64(123), res) }) t.Run("No Target Types", func(t *testing.T) { res := coerceValue("val", &base.Schema{}) assert.Equal(t, "val", res) res = coerceValue("newVal", nil) assert.Equal(t, "newVal", res) }) t.Run("String Slice input (take first)", func(t *testing.T) { res := coerceValue([]string{"123"}, schemaInt) assert.Equal(t, int64(123), res) }) t.Run("Integer Conversions", func(t *testing.T) { assert.Equal(t, "abc", coerceValue("abc", schemaInt)) assert.Equal(t, "", coerceValue("", schemaInt)) assert.Equal(t, 123, coerceValue(123, schemaInt)) assert.Equal(t, int64(123), coerceValue("123", schemaInt)) }) t.Run("Number Conversions", func(t *testing.T) { assert.Equal(t, 12.34, coerceValue("12.34", schemaNum)) assert.Equal(t, "abc", coerceValue("abc", schemaNum)) assert.Equal(t, 13.2, coerceValue(13.2, schemaNum)) assert.Equal(t, 5, coerceValue(5, nil)) }) t.Run("Boolean Conversions", func(t *testing.T) { assert.Equal(t, true, coerceValue("true", schemaBool)) assert.Equal(t, 123, coerceValue(123, schemaBool)) }) t.Run("String Conversions", func(t *testing.T) { assert.Equal(t, "val", coerceValue("val", schemaStr)) assert.Equal(t, "123", coerceValue(123, schemaStr)) }) t.Run("Array Conversions", func(t *testing.T) { arrSchema := &base.Schema{ Type: []string{helpers.Array}, Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{ Type: []string{helpers.Integer}, })}, } noItem := coerceValue("a", &base.Schema{ Type: []string{helpers.Array}, }) assert.Equal(t, []any{"a"}, noItem) res1 := coerceValue([]any{"1", "2"}, arrSchema) assert.Equal(t, []any{int64(1), int64(2)}, res1) res2 := coerceValue([]string{"1", "2"}, arrSchema) assert.Equal(t, []any{int64(1), int64(2)}, res2) mapInput := map[string]any{"1": "20", "0": "10"} res3 := coerceValue(mapInput, arrSchema) assert.IsType(t, []any{}, res3) sliceRes := res3.([]any) assert.Equal(t, int64(10), sliceRes[0]) assert.Equal(t, int64(20), sliceRes[1]) mapBad := map[string]any{"foo": "bar"} res4 := coerceValue(mapBad, arrSchema) assert.Equal(t, mapBad, res4) res5 := coerceValue("10", arrSchema) assert.Equal(t, []any{int64(10)}, res5) }) t.Run("Object Conversions", func(t *testing.T) { objSchema := &base.Schema{ Type: []string{helpers.Object}, Properties: orderedmap.New[string, *base.SchemaProxy](), } objSchema.Properties.Set("num", base.CreateSchemaProxy(schemaInt)) input := map[string]any{"num": "55", "other": "val"} res := coerceValue(input, objSchema) resMap, ok := res.(map[string]any) assert.True(t, ok) assert.Equal(t, int64(55), resMap["num"]) assert.Equal(t, "val", resMap["other"]) }) } func TestIsArraySchema(t *testing.T) { assert.False(t, isArraySchema(nil)) assert.False(t, isArraySchema(&base.Schema{Type: []string{"string"}})) assert.True(t, isArraySchema(&base.Schema{Type: []string{"array"}})) } func TestComplexBodies(t *testing.T) { spec := `{ "openapi": "3.1.0", "paths": { "/posts": { "put": { "requestBody": { "content": { "application/x-www-form-urlencoded": { "encoding": { "payload": { "contentType": "application/json" }, "title": { "allowReserved": true }, "pipeArr": { "style": "pipeDelimited" }, "spaceArr": { "style": "spaceDelimited" }, "unexplodedArr": { "explode": false } }, "schema": { "additionalProperties": false, "properties": { "content": { "type": "array", "items": { "type": "object", "additionalProperties": false, "properties": { "name": {"oneOf": [ { "type": ["boolean"] }, { "type": ["integer"] }]} } } }, "bool": { "type": ["boolean"], "enum": [false] }, "reserved": { "type": ["string"] }, "title": { "type": ["string"] }, "pipeArr": { "type": "array", "items": {"type": "integer"} }, "spaceArr": { "type": "array", "items": {"type": "integer"} }, "unexplodedArr": { "type": "array", "items": {"type": "integer"} }, "payload": { "type": "object", "additionalProperties": false, "properties": { "hey": { "type": "array", "items": { "type": "boolean" } } } } }, "required": ["title", "bool"], "type": "object" } } }, "required": true } } } } } ` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) contentSchema := v3Doc.Model.Paths.PathItems.GetOrZero("/posts").Put.RequestBody.Content.GetOrZero("application/x-www-form-urlencoded") schema := contentSchema.Schema.Schema() encoding := contentSchema.Encoding v := NewURLEncodedValidator() valid, errs := v.ValidateURLEncodedString(schema, encoding, "bool=false&title=test&content[0][name]=true") assert.True(t, valid) assert.Len(t, errs, 0) valid, errs = v.ValidateURLEncodedString(schema, encoding, "bool=false&title=test&content[0][name]=4") assert.True(t, valid) assert.Len(t, errs, 0) valid, errs = v.ValidateURLEncodedString(schema, encoding, "bool=false&title=test&content[0][name]=4.4") assert.False(t, valid) assert.Len(t, errs, 1) valid, errs = v.ValidateURLEncodedString(schema, encoding, "bool=true&title=test&content[0][name]=true") assert.False(t, valid) assert.Len(t, errs, 1) valid, errs = v.ValidateURLEncodedString(schema, encoding, "bool=false&content[0][name]=true") assert.False(t, valid) assert.Len(t, errs, 1) valid, errs = v.ValidateURLEncodedString(schema, encoding, "bool=false&title") assert.True(t, valid) assert.Len(t, errs, 0) valid, errs = v.ValidateURLEncodedString(schema, encoding, `bool=false&title&payload={"hey": [true, false]}`) assert.True(t, valid) assert.Len(t, errs, 0) valid, errs = v.ValidateURLEncodedString(schema, encoding, `bool=false&title&payload={"hey": [2], "adittional": false}`) assert.False(t, valid) assert.Len(t, errs, 1) valid, errs = v.ValidateURLEncodedString(schema, encoding, `bool=false&title=do not use #`) assert.True(t, valid) assert.Len(t, errs, 0) valid, errs = v.ValidateURLEncodedString(schema, encoding, `bool=false&title&reserved=do not use #`) assert.False(t, valid) assert.Len(t, errs, 1) valid, errs = v.ValidateURLEncodedString(schema, encoding, `bool=false&title&pipeArr=1|2|3`) assert.True(t, valid) assert.Len(t, errs, 0) valid, errs = v.ValidateURLEncodedString(schema, encoding, `bool=false&title&spaceArr=1 2 3`) assert.True(t, valid) assert.Len(t, errs, 0) valid, errs = v.ValidateURLEncodedString(schema, encoding, `bool=false&title&spaceArr=1%202%203`) assert.True(t, valid) assert.Len(t, errs, 0) valid, errs = v.ValidateURLEncodedString(schema, encoding, `bool=false&title&unexplodedArr=1,2,3`) assert.True(t, valid) assert.Len(t, errs, 0) } func TestValidateURLEncoded(t *testing.T) { spec := `openapi: 3.0.0 paths: /collection: get: responses: '200': content: application/x-www-form-urlencoded: schema: type: object` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) contentSchema := v3Doc.Model.Paths.PathItems.GetOrZero("/collection").Get.Responses.Codes.GetOrZero("200").Content.GetOrZero("application/x-www-form-urlencoded") schema := contentSchema.Schema.Schema() encoding := contentSchema.Encoding v := NewURLEncodedValidator() valid, errs := v.ValidateURLEncodedStringWithVersion(schema, encoding, "a=1", 3.1) assert.True(t, valid) assert.Empty(t, errs) valid, _ = v.ValidateURLEncodedStringWithVersion(nil, nil, "a=1", 3.1) assert.False(t, valid) valid, errs = v.ValidateURLEncodedString(schema, encoding, "a=1") assert.True(t, valid) assert.Empty(t, errs) valid, _ = v.ValidateURLEncodedString(nil, nil, "a=1") assert.False(t, valid) } libopenapi-validator-0.13.8/schema_validation/validate_xml.go000066400000000000000000000243571520534042400243740ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "encoding/json" "fmt" "log/slog" "strconv" "strings" "github.com/pb33f/libopenapi/datamodel/high/base" xj "github.com/basgys/goxml2json" liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" ) func (x *xmlValidator) validateXMLWithVersion(schema *base.Schema, xmlString string, log *slog.Logger, version float32) (bool, []*liberrors.ValidationError) { if schema == nil { log.Info("schema is empty and cannot be validated") return false, nil } // parse xml and transform to json structure matching schema transformedJSON, prevalidationErrors := TransformXMLToSchemaJSON(xmlString, schema) if len(prevalidationErrors) > 0 { return false, prevalidationErrors } // validate transformed json against schema using existing validator return x.schemaValidator.validateSchemaWithVersion(schema, nil, transformedJSON, log, version) } // TransformXMLToSchemaJSON converts xml to json structure matching openapi schema. // applies xml object transformations: name, attribute, wrapped. func TransformXMLToSchemaJSON(xmlString string, schema *base.Schema) (any, []*liberrors.ValidationError) { if xmlString == "" { return nil, []*liberrors.ValidationError{liberrors.InvalidXMLParsing("empty xml content", xmlString)} } // parse xml using goxml2json. we convert types manually jsonBuf, err := xj.Convert(strings.NewReader(xmlString)) if err != nil { return nil, []*liberrors.ValidationError{liberrors.InvalidXMLParsing(fmt.Sprintf("malformed xml: %s", err.Error()), xmlString)} } jsonBytes := jsonBuf.Bytes() // the smallest valid XML possible "" generates a 10 bytes buffer. // any other invalid XML generates a smaller buffer if len(jsonBytes) < 10 { return nil, []*liberrors.ValidationError{liberrors.InvalidXMLParsing("malformed xml", xmlString)} } var rawJSON any if err := json.Unmarshal(jsonBytes, &rawJSON); err != nil { return nil, []*liberrors.ValidationError{liberrors.InvalidXMLParsing(fmt.Sprintf("failed to decode converted xml to json: %s", err.Error()), xmlString)} } xmlNsMap := make(map[string]string, 2) // apply openapi xml object transformations return applyXMLTransformations(rawJSON, schema, &xmlNsMap) } func validateXmlNs(dataMap *map[string]any, schema *base.Schema, propName string, xmlNsMap *map[string]string) []*liberrors.ValidationError { var validationErrors []*liberrors.ValidationError if dataMap == nil || schema == nil || xmlNsMap == nil { return validationErrors } if propName != "" { if val, exists := (*dataMap)[propName]; exists { if converted, ok := val.(map[string]any); ok { dataMap = &converted } } } if schema.XML.Prefix != "" { attrKey := "-" + schema.XML.Prefix val, exists := (*dataMap)[attrKey] if exists { if ns, ok := val.(string); ok { (*xmlNsMap)[schema.XML.Prefix] = ns (*xmlNsMap)[ns] = schema.XML.Prefix if schema.XML.Namespace != "" && schema.XML.Namespace != ns { validationErrors = append(validationErrors, liberrors.InvalidNamespace(schema, ns, schema.XML.Namespace, schema.XML.Prefix)) } } delete((*dataMap), attrKey) } else { validationErrors = append(validationErrors, liberrors.MissingPrefix(schema, schema.XML.Prefix)) } } if schema.XML.Namespace != "" { _, exists := (*xmlNsMap)[schema.XML.Namespace] if !exists { validationErrors = append(validationErrors, liberrors.MissingNamespace(schema, schema.XML.Namespace)) } } return validationErrors } func convertBasedOnSchema(propName, xmlName string, propValue any, schema *base.Schema, xmlNsMap *map[string]string) (any, []*liberrors.ValidationError) { var xmlNsErrors []*liberrors.ValidationError types := schema.Type extractTypes := func(proxies []*base.SchemaProxy) { for _, proxy := range proxies { sch := proxy.Schema() if len(sch.Type) > 0 { types = append(types, sch.Type...) } } } extractTypes(schema.AllOf) extractTypes(schema.OneOf) extractTypes(schema.AnyOf) convertedValue := propValue typesLoop: for _, pType := range types { // because in XML everything is a string, we try to convert the value to the // actual expected type, so the normal schema validation should pass with correct types switch pType { case helpers.Integer: textValue, isString := propValue.(string) if isString { converted, err := strconv.ParseInt(textValue, 10, 64) if err == nil { convertedValue = converted break typesLoop } } case helpers.Number: textValue, isString := propValue.(string) if isString { converted, err := strconv.ParseFloat(textValue, 64) if err == nil { convertedValue = converted break typesLoop } } case helpers.Boolean: textValue, isString := propValue.(string) if isString { converted, err := strconv.ParseBool(textValue) if err == nil { convertedValue = converted break typesLoop } } case helpers.Array: convertedValue = propValue if schema.XML != nil && schema.XML.Wrapped { convertedValue = unwrapArrayElement(propValue, propName, schema) } if schema.Items != nil && schema.Items.A != nil { itemSchema := schema.Items.A.Schema() arr, isArr := convertedValue.([]any) if !isArr { arr = []any{ convertedValue, } } for index, item := range arr { converted, errs := convertBasedOnSchema(propName, xmlName, item, itemSchema, xmlNsMap) if len(errs) > 0 { xmlNsErrors = append(xmlNsErrors, errs...) } arr[index] = converted } convertedValue = arr break typesLoop } case helpers.Object: objectValue, isObject := propValue.(map[string]any) if isObject { newValue, xmlErrors := applyXMLTransformations(objectValue, schema, xmlNsMap) if len(xmlErrors) > 0 { xmlNsErrors = append(xmlNsErrors, xmlErrors...) continue typesLoop } convertedValue = newValue break typesLoop } } } return convertedValue, xmlNsErrors } // applyXMLTransformations applies openapi xml object rules to match json schema. // handles xml.name (root unwrapping), xml.attribute (dash prefix), xml.wrapped (array unwrapping), // xml.prefix (check existance), xml.namespace (check if exists and match). // we delete all attributes, prefixes, and namespaces found in the data interface; therefore, undeclared items // are sent in the body for validation, so that 'additionalProperties: false' can detect it. func applyXMLTransformations(data any, schema *base.Schema, xmlNsMap *map[string]string) (any, []*liberrors.ValidationError) { if schema == nil || data == nil || xmlNsMap == nil { return data, nil } // unwrap root element if xml.name is set on schema if schema.XML != nil && schema.XML.Name != "" { if dataMap, ok := data.(map[string]any); ok { if wrapped, exists := dataMap[schema.XML.Name]; exists { data = wrapped } } } var xmlNsErrors []*liberrors.ValidationError // transform properties based on their xml configurations if dataMap, ok := data.(map[string]any); ok { if schema.Properties == nil || schema.Properties.Len() == 0 { if schema.XML != nil && (schema.XML.Prefix != "" || schema.XML.Namespace != "") { namespaceErrors := validateXmlNs(&dataMap, schema, "", xmlNsMap) if len(namespaceErrors) > 0 { xmlNsErrors = append(xmlNsErrors, namespaceErrors...) } else { if content, has := dataMap["#content"]; has { if stringContent, ok := content.(string); ok { data = stringContent } } } } return data, xmlNsErrors } for pair := schema.Properties.First(); pair != nil; pair = pair.Next() { propName := pair.Key() propSchemaProxy := pair.Value() propSchema := propSchemaProxy.Schema() if propSchema == nil { continue } xmlName := propName if propSchema.XML != nil { // determine xml element name (defaults to property name) if propSchema.XML.Name != "" { xmlName = propSchema.XML.Name } } if propSchema.XML != nil { namespaceErrors := validateXmlNs(&dataMap, propSchema, xmlName, xmlNsMap) if len(namespaceErrors) > 0 { xmlNsErrors = append(xmlNsErrors, namespaceErrors...) } // handle xml.attribute: true - attributes are prefixed with dash if propSchema.XML.Attribute { attrKey := "-" + xmlName if val, exists := dataMap[attrKey]; exists { // If the value is an attribute, it cannot have a namespace convertedValue, _ := convertBasedOnSchema(propName, xmlName, val, propSchema, xmlNsMap) dataMap[propName] = convertedValue delete(dataMap, attrKey) continue } } } // handle regular elements if val, exists := dataMap[xmlName]; exists { if mapObject, ok := val.(map[string]any); ok { if content, has := mapObject["#content"]; has { if stringContent, ok := content.(string); ok { val = stringContent } } } convertedValue, nsErrors := convertBasedOnSchema(propName, xmlName, val, propSchema, xmlNsMap) if len(nsErrors) > 0 { xmlNsErrors = append(xmlNsErrors, nsErrors...) } dataMap[propName] = convertedValue if propName != xmlName { delete(dataMap, xmlName) } } } } return data, xmlNsErrors } // unwrapArrayElement removes wrapping element from xml arrays when xml.wrapped is true. // example: {"items": {"item": [...]}} becomes [...] func unwrapArrayElement(val any, itemName string, propSchema *base.Schema) any { wrapMap, ok := val.(map[string]any) if !ok { return val } if propSchema.XML.Name != "" { itemName = propSchema.XML.Name } // determine item element name if propSchema.Items != nil && propSchema.Items.A != nil { itemSchema := propSchema.Items.A.Schema() if itemSchema != nil && itemSchema.XML != nil && itemSchema.XML.Name != "" { itemName = itemSchema.XML.Name } } // unwrap: look for item element inside wrapper if unwrapped, exists := wrapMap[itemName]; exists { return unwrapped } return val } // IsXMLContentType checks if a media type string represents xml content. func IsXMLContentType(mediaType string) bool { mt := strings.ToLower(strings.TrimSpace(mediaType)) return strings.HasPrefix(mt, "application/xml") || strings.HasPrefix(mt, "text/xml") || strings.HasSuffix(mt, "+xml") } libopenapi-validator-0.13.8/schema_validation/validate_xml_test.go000066400000000000000000001113421520534042400254220ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "testing" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" ) func TestValidateXML_Issue346_BasicXMLWithName(t *testing.T) { spec := `openapi: 3.0.0 info: title: Test version: 1.0.0 paths: /pet: get: responses: '200': description: success content: application/xml: schema: type: object properties: nice: type: string xml: name: Cat example: "true"` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/pet").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() valid, validationErrors := validator.ValidateXMLString(schema, "true") assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_MalformedXML(t *testing.T) { spec := `openapi: 3.0.0 paths: /pet: get: responses: '200': content: application/xml: schema: type: object xml: name: Cat` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/pet").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // empty xml should fail valid, validationErrors := validator.ValidateXMLString(schema, "") assert.False(t, valid) assert.NotEmpty(t, validationErrors) assert.Contains(t, validationErrors[0].Reason, "empty xml") } func TestValidateXML_WithAttributes(t *testing.T) { spec := `openapi: 3.0.0 paths: /pet: get: responses: '200': content: application/xml: schema: type: object properties: id: type: integer xml: attribute: true name: type: string xml: name: Cat` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/pet").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() valid, validationErrors := validator.ValidateXMLString(schema, `Fluffy`) assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_TypeValidation(t *testing.T) { spec := `openapi: 3.0.0 paths: /pet: get: responses: '200': content: application/xml: schema: type: object properties: age: type: integer xml: name: Cat` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/pet").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // valid integer valid, validationErrors := validator.ValidateXMLString(schema, "5") assert.True(t, valid) assert.Len(t, validationErrors, 0) // invalid - string instead of integer valid, validationErrors = validator.ValidateXMLString(schema, "not-a-number") assert.False(t, valid) assert.NotEmpty(t, validationErrors) } func TestValidateXML_WrappedArray(t *testing.T) { spec := `openapi: 3.0.0 paths: /pets: get: responses: '200': content: application/xml: schema: type: object properties: pets: type: array xml: wrapped: true items: type: object properties: name: type: string age: type: integer xml: name: pet xml: name: Pets` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/pets").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // valid wrapped array validXML := `Fluffy3Spot5` valid, validationErrors := validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) // invalid - wrong type in array item invalidXML := `Fluffynot-a-number` valid, validationErrors = validator.ValidateXMLString(schema, invalidXML) assert.False(t, valid) assert.NotEmpty(t, validationErrors) } func TestValidateXML_MultiplePropertiesWithCustomNames(t *testing.T) { spec := `openapi: 3.0.0 paths: /user: get: responses: '200': content: application/xml: schema: type: object properties: userId: type: integer xml: name: id userName: type: string xml: name: username userEmail: type: string xml: name: email xml: name: User` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/user").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // valid xml with custom element names validXML := `42johndoejohn@example.com` valid, validationErrors := validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_MixedAttributesAndElements(t *testing.T) { spec := `openapi: 3.0.0 paths: /book: get: responses: '200': content: application/xml: schema: type: object properties: id: type: integer xml: attribute: true isbn: type: string xml: attribute: true title: type: string author: type: string price: type: number xml: name: Book` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/book").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // valid xml with both attributes and elements validXML := `Go ProgrammingJohn Doe29.99` valid, validationErrors := validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_NestedObjects(t *testing.T) { spec := `openapi: 3.0.0 paths: /order: get: responses: '200': content: application/xml: schema: type: object properties: orderId: type: integer customer: type: object properties: name: type: string address: type: object properties: street: type: string city: type: string xml: name: Order` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/order").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // valid nested xml validXML := `123Jane Doe
123 Main StSpringfield
` valid, validationErrors := validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_TypeCoercion(t *testing.T) { spec := `openapi: 3.0.0 paths: /data: get: responses: '200': content: application/xml: schema: type: object properties: intValue: type: integer floatValue: type: number stringValue: type: string boolValue: type: string xml: name: Data` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/data").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // goxml2json should coerce numeric strings to numbers validXML := `423.14hellotrue` valid, validationErrors := validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_SchemaViolations(t *testing.T) { spec := `openapi: 3.0.0 paths: /product: get: responses: '200': content: application/xml: schema: type: object required: - productId - name properties: productId: type: integer name: type: string description: type: string xml: name: Product` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/product").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // missing required property 'name' invalidXML := `123` valid, validationErrors := validator.ValidateXMLString(schema, invalidXML) assert.False(t, valid) assert.NotEmpty(t, validationErrors) // valid - all required properties present validXML := `123Widget` valid, validationErrors = validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) // valid with optional property validXML = `123WidgetA useful widget` valid, validationErrors = validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_ComplexRealWorld_SOAP(t *testing.T) { spec := `openapi: 3.0.0 paths: /api: post: responses: '200': content: application/soap+xml: schema: type: object properties: status: type: string requestId: type: string xml: attribute: true timestamp: type: integer data: type: object properties: value: type: string xml: name: Response` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/api").Post.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/soap+xml").Schema.Schema() validator := NewXMLValidator() // valid soap-like xml validXML := `success1699372800result` valid, validationErrors := validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_EmptyAndWhitespace(t *testing.T) { spec := `openapi: 3.0.0 paths: /test: get: responses: '200': content: application/xml: schema: type: object properties: value: type: string xml: name: Test` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/test").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // valid xml with whitespace validXML := ` hello ` valid, validationErrors := validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) // valid xml with empty element validXML = `` valid, validationErrors = validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_WithNamespace(t *testing.T) { spec := `openapi: 3.0.0 paths: /message: get: responses: '200': content: application/xml: schema: type: object properties: subject: type: string body: type: string xml: name: Message` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/message").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // valid xml with namespace (goxml2json strips namespace prefixes) validXML := `HelloWorld` valid, validationErrors := validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_PropertyMismatch(t *testing.T) { spec := `openapi: 3.0.0 paths: /config: get: responses: '200': content: application/xml: schema: type: object required: - enabled - maxRetries properties: enabled: type: boolean maxRetries: type: integer xml: name: Config` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/config").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // xml has wrong element names (should be 'enabled' and 'maxRetries') // this should fail because required properties are missing invalidXML := `true5` valid, validationErrors := validator.ValidateXMLString(schema, invalidXML) assert.False(t, valid) assert.NotEmpty(t, validationErrors) } func TestValidateXML_AttributeTypeMismatch(t *testing.T) { spec := `openapi: 3.0.0 paths: /item: get: responses: '200': content: application/xml: schema: type: object properties: id: type: integer xml: attribute: true quantity: type: integer xml: attribute: true name: type: string xml: name: Item` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/item").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // valid - attributes are integers validXML := `Widget` valid, validationErrors := validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) // invalid - attribute is not an integer invalidXML := `Widget` valid, validationErrors = validator.ValidateXMLString(schema, invalidXML) assert.False(t, valid) assert.NotEmpty(t, validationErrors) } func TestValidateXML_FloatPrecision(t *testing.T) { spec := `openapi: 3.0.0 paths: /measurement: get: responses: '200': content: application/xml: schema: type: object properties: temperature: type: number humidity: type: number pressure: type: number xml: name: Measurement` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/measurement").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // valid xml with float values validXML := `23.45665.21013.25` valid, validationErrors := validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) // valid - integers are acceptable for number type validXML = `23651013` valid, validationErrors = validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_Version30_WithNullable(t *testing.T) { spec := `openapi: 3.0.0 paths: /item: get: responses: '200': content: application/xml: schema: type: object properties: value: type: string nullable: true xml: name: Item` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/item").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // test with version 3.0 - should allow nullable keyword valid, validationErrors := validator.ValidateXMLStringWithVersion(schema, "test", 3.0) assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_NilSchema(t *testing.T) { validator := NewXMLValidator() // test with nil schema - should return false with empty errors valid, validationErrors := validator.ValidateXMLString(nil, "value") assert.False(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_NilSchemaInTransformation(t *testing.T) { // directly test applyXMLTransformations with nil schema (line 94) xmlNsMap := make(map[string]string, 2) result, err := applyXMLTransformations(map[string]interface{}{"test": "value"}, nil, &xmlNsMap) assert.NotNil(t, result) assert.Len(t, err, 0) assert.Equal(t, map[string]interface{}{"test": "value"}, result) } func TestValidateXML_TransformWithNilPropertySchemaProxy(t *testing.T) { // directly test applyXMLTransformations when a property schema proxy returns nil (line 119) // this can happen with circular refs or unresolved refs in edge cases // create a schema with properties but we'll simulate a nil schema scenario // by testing the transformation directly data := map[string]interface{}{ "test": "value", } // schema with properties but no XML config - tests property iteration schema := &base.Schema{ Properties: nil, // will trigger line 109 early return } xmlNsMap := make(map[string]string, 2) result, err := applyXMLTransformations(data, schema, &xmlNsMap) assert.Len(t, err, 0) assert.Equal(t, data, result) } func TestValidateXML_NoProperties(t *testing.T) { spec := `openapi: 3.0.0 paths: /empty: get: responses: '200': content: application/xml: schema: type: object xml: name: Empty` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/empty").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // schema with no properties should still validate valid, validationErrors := validator.ValidateXMLString(schema, "value") assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_PrimitiveValue(t *testing.T) { spec := `openapi: 3.0.0 paths: /simple: get: responses: '200': content: application/xml: schema: type: string xml: name: Value` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/simple").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // primitive value (non-object) should work valid, validationErrors := validator.ValidateXMLString(schema, "hello world") assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_ArrayNotWrapped(t *testing.T) { spec := `openapi: 3.0.0 paths: /items: get: responses: '200': content: application/xml: schema: type: object properties: items: type: array items: type: string xml: name: Items` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/items").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // array without wrapped - items are direct siblings validXML := `onetwothree` valid, validationErrors := validator.ValidateXMLString(schema, validXML) assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestValidateXML_WrappedArrayWithWrongItemName(t *testing.T) { spec := `openapi: 3.0.0 paths: /collection: get: responses: '200': content: application/xml: schema: type: object properties: data: type: array xml: wrapped: true items: additionalProperties: false type: object properties: value: type: string xml: name: record xml: name: Collection` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/collection").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() // wrapper contains items with wrong name (item instead of record) // this tests the fallback path where unwrapped element is not found xmlWithWrongItemName := `test` valid, _ := validator.ValidateXMLString(schema, xmlWithWrongItemName) assert.False(t, valid) xmlWithWrightItemName := `test` valid, _ = validator.ValidateXMLString(schema, xmlWithWrightItemName) assert.True(t, valid) } func TestValidateXML_DirectArrayValue(t *testing.T) { // test unwrapArrayElement with non-map value (line 160) schema := &base.Schema{ Type: []string{"array"}, Items: &base.DynamicValue[*base.SchemaProxy, bool]{ A: &base.SchemaProxy{}, }, XML: &base.XML{ Wrapped: true, }, } // when val is already an array (not a map), it should return as-is arrayVal := []interface{}{"one", "two", "three"} result := unwrapArrayElement(arrayVal, "", schema) assert.Equal(t, arrayVal, result) } func TestValidateXML_UnwrapArrayElementMissingItem(t *testing.T) { // test unwrapArrayElement when wrapper map doesn't contain expected item (line 177) schema := &base.Schema{ Type: []string{"array"}, Items: &base.DynamicValue[*base.SchemaProxy, bool]{ A: &base.SchemaProxy{}, }, XML: &base.XML{ Wrapped: true, }, } // wrapper map contains wrong key - should return map as-is (line 177) wrapperMap := map[string]interface{}{"wrongKey": []interface{}{"one", "two"}} result := unwrapArrayElement(wrapperMap, "", schema) assert.Equal(t, wrapperMap, result) } func TestTransformXMLToSchemaJSON_EmptyString(t *testing.T) { // test empty string error path (line 68) schema := &base.Schema{} _, err := TransformXMLToSchemaJSON("", schema) assert.Len(t, err, 1) assert.Contains(t, err[0].Reason, "empty xml content") } func TestApplyXMLTransformations_NoXMLName(t *testing.T) { // test schema without xml.name - data stays wrapped schema := &base.Schema{ Properties: nil, } xmlNsMap := make(map[string]string, 2) data := map[string]interface{}{"Cat": map[string]interface{}{"nice": "true"}} result, err := applyXMLTransformations(data, schema, &xmlNsMap) assert.Len(t, err, 0) assert.Equal(t, data, result) } func TestIsXMLContentType(t *testing.T) { tests := []struct { name string contentType string expected bool }{ {"application/xml", "application/xml", true}, {"text/xml", "text/xml", true}, {"application/soap+xml", "application/soap+xml", true}, {"application/json", "application/json", false}, {"text/plain", "text/plain", false}, {"with whitespace", " application/xml ", true}, {"mixed case", "APPLICATION/XML", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsXMLContentType(tt.contentType) assert.Equal(t, tt.expected, result) }) } } func TestTransformXMLToSchemaJSON_InvalixXml(t *testing.T) { schema := &base.Schema{} _, err := TransformXMLToSchemaJSON("<", schema) assert.Len(t, err, 1) assert.Contains(t, err[0].Reason, "malformed xml") } func TestValidateXmlNs_NoData(t *testing.T) { errors := validateXmlNs(nil, nil, "", nil) assert.Len(t, errors, 0) } func getXmlTestSchema(t *testing.T) *base.Schema { spec := `openapi: 3.1 paths: /collection: get: responses: '200': content: application/xml: schema: type: object additionalProperties: false properties: body: type: object required: - id - success - payload xml: prefix: t namespace: http://assert.t name: reqBody properties: id: type: integer xml: attribute: true success: xml: name: ok prefix: j namespace: http://j.j type: boolean payload: oneOf: - type: integer - type: object data: type: array xml: wrapped: true name: list items: additionalProperties: false type: object required: - value properties: value: type: string xml: namespace: http://prop.arr prefix: arr xml: name: record prefix: unt namespace: http://expect.t xml: name: Collection` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/collection").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() return schema } func TestValidateXmlNs_InvalidPrefix(t *testing.T) { schema := getXmlTestSchema(t) validator := NewXMLValidator() xmlPayload := `` valid, err := validator.ValidateXMLString(schema, xmlPayload) assert.False(t, valid) assert.Equal(t, helpers.XmlValidationPrefix, err[0].ValidationSubType) } func TestValidateXmlNs_InvalidNamespace(t *testing.T) { schema := getXmlTestSchema(t) validator := NewXMLValidator() xmlPayload := `` valid, err := validator.ValidateXMLString(schema, xmlPayload) assert.False(t, valid) assert.Equal(t, helpers.XmlValidationNamespace, err[0].ValidationSubType) } func TestValidateXmlNs_InvalidNamespaceInRoot(t *testing.T) { spec := `openapi: 3.0.0 paths: /pet: get: responses: '200': content: application/xml: schema: type: object xml: name: Cat prefix: c namespace: http://cat.ca` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/pet").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() xmlPayload := `` valid, validationErrors := validator.ValidateXMLString(schema, xmlPayload) assert.False(t, valid) assert.Equal(t, "The namespace from prefix 'c' differs from the xml", validationErrors[0].Message) assert.Equal(t, helpers.XmlValidationNamespace, validationErrors[0].ValidationSubType) } func TestValidateXmlNs_CorrectNamespaceInRoot(t *testing.T) { spec := `openapi: 3.0.0 paths: /pet: get: responses: '200': content: application/xml: schema: type: string xml: name: Cat prefix: c namespace: http://cat.ca` doc, err := libopenapi.NewDocument([]byte(spec)) assert.NoError(t, err) v3Doc, err := doc.BuildV3Model() assert.NoError(t, err) schema := v3Doc.Model.Paths.PathItems.GetOrZero("/pet").Get.Responses.Codes.GetOrZero("200"). Content.GetOrZero("application/xml").Schema.Schema() validator := NewXMLValidator() xmlPayload := `meow` valid, validationErrors := validator.ValidateXMLString(schema, xmlPayload) assert.True(t, valid) assert.Len(t, validationErrors, 0) } func TestConvertBasedOnSchema_XmlSuccessfullyConverted(t *testing.T) { schema := getXmlTestSchema(t) validator := NewXMLValidator() xmlPayload := `true2 Text` valid, err := validator.ValidateXMLString(schema, xmlPayload) assert.True(t, valid) assert.Len(t, err, 0) } func TestConvertBasedOnSchema_MissingPrefixInObjectProperties(t *testing.T) { schema := getXmlTestSchema(t) validator := NewXMLValidator() xmlPayload := `true2 Text` valid, err := validator.ValidateXMLString(schema, xmlPayload) assert.False(t, valid) assert.Equal(t, helpers.XmlValidationPrefix, err[0].ValidationSubType) assert.Equal(t, "The prefix 'j' is defined in the schema, however it's missing from the xml", err[0].Message) } func TestConvertBasedOnSchema_MissingPrefixInArrayItemProperties(t *testing.T) { schema := getXmlTestSchema(t) validator := NewXMLValidator() xmlPayload := `true2 Text` valid, err := validator.ValidateXMLString(schema, xmlPayload) assert.False(t, valid) assert.Equal(t, helpers.XmlValidationPrefix, err[0].ValidationSubType) assert.Equal(t, "The prefix 'arr' is defined in the schema, however it's missing from the xml", err[0].Message) } func TestApplyXMLTransformations_IncorrectSchema(t *testing.T) { schema := getXmlTestSchema(t) validator := NewXMLValidator() xmlPayload := `NotBooleanNotInteger Text` valid, err := validator.ValidateXMLString(schema, xmlPayload) assert.False(t, valid) assert.Equal(t, "got string, want boolean", err[0].SchemaValidationErrors[0].Reason) assert.Equal(t, "schema does not pass validation", err[0].Message) } func TestApplyXMLTransformations_NilPropSchema(t *testing.T) { schema := &base.Schema{ Properties: orderedmap.New[string, *base.SchemaProxy](), } emptyProxy := &base.SchemaProxy{} schema.Properties.Set("broken_ref_prop", emptyProxy) data := map[string]any{ "broken_ref_prop": "some_value", } xmlNsMap := make(map[string]string) result, errs := applyXMLTransformations(data, schema, &xmlNsMap) assert.Len(t, errs, 0) assert.NotNil(t, result) } libopenapi-validator-0.13.8/schema_validation/xml_validator.go000066400000000000000000000051371520534042400245630ustar00rootroot00000000000000// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation import ( "log/slog" "os" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi-validator/config" liberrors "github.com/pb33f/libopenapi-validator/errors" ) // XMLValidator is an interface that defines methods for validating XML against OpenAPI schemas. // There are 2 methods for validating XML: // // ValidateXMLString validates an XML string against a schema, applying OpenAPI xml object transformations. // ValidateXMLStringWithVersion - version-aware XML validation that allows OpenAPI 3.0 keywords when version is specified. type XMLValidator interface { // ValidateXMLString validates an XML string against an OpenAPI schema, applying xml object transformations. // Uses OpenAPI 3.1+ validation by default (strict JSON Schema compliance). ValidateXMLString(schema *base.Schema, xmlString string) (bool, []*liberrors.ValidationError) // ValidateXMLStringWithVersion validates an XML string with version-specific rules. // When version is 3.0, OpenAPI 3.0-specific keywords like 'nullable' are allowed and processed. // When version is 3.1+, OpenAPI 3.0-specific keywords like 'nullable' will cause validation to fail. ValidateXMLStringWithVersion(schema *base.Schema, xmlString string, version float32) (bool, []*liberrors.ValidationError) } type xmlValidator struct { schemaValidator *schemaValidator logger *slog.Logger } // NewXMLValidatorWithLogger creates a new XMLValidator instance with a custom logger. func NewXMLValidatorWithLogger(logger *slog.Logger, opts ...config.Option) XMLValidator { options := config.NewValidationOptions(opts...) // Create an internal schema validator for JSON validation after XML transformation sv := &schemaValidator{options: options, logger: logger} return &xmlValidator{schemaValidator: sv, logger: logger} } // NewXMLValidator creates a new XMLValidator instance with default logging configuration. func NewXMLValidator(opts ...config.Option) XMLValidator { logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelError, })) return NewXMLValidatorWithLogger(logger, opts...) } func (x *xmlValidator) ValidateXMLString(schema *base.Schema, xmlString string) (bool, []*liberrors.ValidationError) { return x.validateXMLWithVersion(schema, xmlString, x.logger, 3.1) } func (x *xmlValidator) ValidateXMLStringWithVersion(schema *base.Schema, xmlString string, version float32) (bool, []*liberrors.ValidationError) { return x.validateXMLWithVersion(schema, xmlString, x.logger, version) } libopenapi-validator-0.13.8/strict/000077500000000000000000000000001520534042400172175ustar00rootroot00000000000000libopenapi-validator-0.13.8/strict/array_validator.go000066400000000000000000000061711520534042400227360ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package strict import ( "strconv" "github.com/pb33f/libopenapi/datamodel/high/base" ) // validateArray checks an array value against a schema for undeclared properties // within array items. It handles: // - items (schema for all items or boolean) // - prefixItems (tuple validation with positional schemas) // - unevaluatedItems (items not covered by items/prefixItems) func (v *Validator) validateArray(ctx *traversalContext, schema *base.Schema, data []any) []UndeclaredValue { if len(data) == 0 { return nil } var undeclared []UndeclaredValue // Check for items: false // When items: false, no items are allowed. If base validation passed, the // array should be empty. But we explicitly check in case it wasn't caught. if schema.Items != nil && schema.Items.IsB() && !schema.Items.B { for i := range data { itemPath := buildArrayPath(ctx.path, i) undeclared = append(undeclared, newUndeclaredItem(itemPath, strconv.Itoa(i), data[i], ctx.direction)) } return undeclared } prefixLen := 0 // handle prefixItems first (tuple validation) if len(schema.PrefixItems) > 0 { for i, itemProxy := range schema.PrefixItems { if i >= len(data) { break } itemPath := buildArrayPath(ctx.path, i) itemCtx := ctx.withPath(itemPath) if itemCtx.shouldIgnore() { prefixLen++ continue } itemSchema := itemProxy.Schema() if itemSchema != nil { undeclared = append(undeclared, v.validateValue(itemCtx, itemSchema, data[i])...) } prefixLen++ } } // handle items for remaining elements (after prefixItems) if schema.Items != nil && schema.Items.A != nil { itemProxy := schema.Items.A itemSchema := itemProxy.Schema() if itemSchema != nil { for i := prefixLen; i < len(data); i++ { itemPath := buildArrayPath(ctx.path, i) itemCtx := ctx.withPath(itemPath) if itemCtx.shouldIgnore() { continue } undeclared = append(undeclared, v.validateValue(itemCtx, itemSchema, data[i])...) } } } // handle unevaluatedItems with schema. // unevaluatedItems: false is handled by base validation. // unevaluatedItems: {schema} means items matching the schema are valid. // note: this doesn't account for items evaluated by `contains`. for strict // validation this is acceptable as we check conservatively. if schema.UnevaluatedItems != nil && schema.UnevaluatedItems.Schema() != nil { // this applies to items not covered by items or prefixItems. // if there's no items schema, unevaluatedItems applies to: // - items after prefixItems (if prefixItems exists) // - all items (if neither items nor prefixItems exists) if schema.Items == nil { unevalSchema := schema.UnevaluatedItems.Schema() startIndex := len(schema.PrefixItems) // 0 if no prefixItems for i := startIndex; i < len(data); i++ { itemPath := buildArrayPath(ctx.path, i) itemCtx := ctx.withPath(itemPath) if itemCtx.shouldIgnore() { continue } undeclared = append(undeclared, v.validateValue(itemCtx, unevalSchema, data[i])...) } } } return undeclared } libopenapi-validator-0.13.8/strict/headers.go000066400000000000000000000021161520534042400211610ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package strict import "strings" // isHeaderIgnored checks if a header name should be ignored in strict validation. // Uses the effective ignored headers list from options (defaults, replaced, or merged). // Set-Cookie is direction-aware: ignored in responses but reported in requests. func (v *Validator) isHeaderIgnored(name string, direction Direction) bool { lower := strings.ToLower(name) // Set-Cookie is expected in responses but unexpected in requests if lower == "set-cookie" { return direction == DirectionResponse } // Check effective ignored list for _, h := range v.getEffectiveIgnoredHeaders() { if strings.ToLower(h) == lower { return true } } return false } // getEffectiveIgnoredHeaders returns the list of headers to ignore based on // configuration. Uses the ValidationOptions method for consistency. func (v *Validator) getEffectiveIgnoredHeaders() []string { if v.options == nil { return nil } return v.options.GetEffectiveStrictIgnoredHeaders() } libopenapi-validator-0.13.8/strict/matcher.go000066400000000000000000000072261520534042400212000ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package strict import ( "errors" "fmt" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/utils" "github.com/santhosh-tekuri/jsonschema/v6" "github.com/pb33f/libopenapi-validator/helpers" ) // dataMatchesSchema checks if the given data matches the schema using // JSON Schema validation. This is used for: // - oneOf/anyOf variant selection (finding which variant the data matches) // - if/then/else condition evaluation // - additionalProperties schema matching // // The method uses version-aware schema compilation to handle OpenAPI 3.0 vs 3.1 // differences (especially nullable handling). // // Returns (true, nil) if data matches the schema. // Returns (false, nil) if data does not match the schema. // Returns (false, error) if schema compilation failed. func (v *Validator) dataMatchesSchema(schema *base.Schema, data any) (bool, error) { if schema == nil { return true, nil // No schema means anything matches } compiled, err := v.getCompiledSchema(schema) if err != nil { return false, err } return compiled.Validate(data) == nil, nil } // getCompiledSchema returns a compiled JSON Schema for the given high-level schema. // It checks multiple cache levels: // 1. Global SchemaCache (if configured in options) // 2. Local instance cache (for reuse within this validation call) // 3. Compiles on-the-fly if not cached // // Returns the compiled schema and nil error on success. // Returns nil schema and nil error if the input schema is nil. // Returns nil schema and error if compilation failed. func (v *Validator) getCompiledSchema(schema *base.Schema) (*jsonschema.Schema, error) { if schema == nil || schema.GoLow() == nil { return nil, nil } hash := schema.GoLow().Hash() hashKey := fmt.Sprintf("%x", hash) // try global cache first (if available) if v.options != nil && v.options.SchemaCache != nil { if cached, ok := v.options.SchemaCache.Load(hash); ok && cached != nil && cached.CompiledSchema != nil { return cached.CompiledSchema, nil } } // try local instance cache if compiled, ok := v.localCache[hashKey]; ok { return compiled, nil } // cache miss - compile on-the-fly with context-aware rendering compiled, err := v.compileSchema(schema) if err != nil { return nil, err } if compiled != nil { v.localCache[hashKey] = compiled } return compiled, nil } // compileSchema renders and compiles a schema for validation. // Uses RenderInlineWithContext for safe cycle handling. // // Returns the compiled schema and nil error on success. // Returns nil schema and error if any step fails (render, conversion, compilation). func (v *Validator) compileSchema(schema *base.Schema) (*jsonschema.Schema, error) { if schema == nil { return nil, nil } schemaHash := fmt.Sprintf("%x", schema.GoLow().Hash()) // use RenderInlineWithContext for safe cycle handling renderedSchema, err := schema.RenderInlineWithContext(v.renderCtx) if err != nil { return nil, fmt.Errorf("strict: schema render failed (hash=%s): %w", schemaHash, err) } jsonSchema, convErr := utils.ConvertYAMLtoJSON(renderedSchema) if convErr != nil { return nil, fmt.Errorf("strict: YAML to JSON conversion failed: %w", convErr) } if len(jsonSchema) == 0 { return nil, errors.New("strict: schema rendered to empty JSON") } schemaName := fmt.Sprintf("strict-match-%s", schemaHash) compiled, err := helpers.NewCompiledSchemaWithVersion( schemaName, jsonSchema, v.options, v.version, ) if err != nil { return nil, fmt.Errorf("strict: schema compilation failed (name=%s): %w", schemaName, err) } return compiled, nil } libopenapi-validator-0.13.8/strict/polymorphic.go000066400000000000000000000356331520534042400221250ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package strict import ( "regexp" "strings" "github.com/pb33f/libopenapi/datamodel/high/base" ) // validatePolymorphic handles allOf, oneOf, and anyOf schemas. // For allOf: merge all schemas and validate against all. // For oneOf/anyOf: find the matching variant and validate against it. func (v *Validator) validatePolymorphic(ctx *traversalContext, schema *base.Schema, data map[string]any) []UndeclaredValue { var undeclared []UndeclaredValue // Handle allOf first - data must match ALL schemas if len(schema.AllOf) > 0 { undeclared = append(undeclared, v.validateAllOf(ctx, schema, data)...) } // Handle oneOf - data must match exactly ONE schema if len(schema.OneOf) > 0 { undeclared = append(undeclared, v.validateOneOf(ctx, schema, data)...) } // Handle anyOf - data must match at least ONE schema if len(schema.AnyOf) > 0 { undeclared = append(undeclared, v.validateAnyOf(ctx, schema, data)...) } // Also validate any direct properties on the parent schema if schema.Properties != nil { declared, patterns := v.collectDeclaredProperties(schema, data) // Check properties that aren't handled by allOf/oneOf/anyOf for propName := range data { // Skip if declared directly or via patterns if isPropertyDeclared(propName, declared, patterns) { continue } // Check if it's declared in any of the allOf schemas if v.isPropertyDeclaredInAllOf(schema.AllOf, propName) { continue } // For oneOf/anyOf, we've already validated against the matching variant } } return undeclared } // validateAllOf validates data against all schemas in allOf. // Collects properties from all schemas as declared. func (v *Validator) validateAllOf(ctx *traversalContext, schema *base.Schema, data map[string]any) []UndeclaredValue { var undeclared []UndeclaredValue // Collect declared properties from ALL schemas in allOf allDeclared := make(map[string]*declaredProperty) var allPatterns []*regexp.Regexp for _, schemaProxy := range schema.AllOf { if schemaProxy == nil { continue } subSchema := schemaProxy.Schema() if subSchema == nil { continue } declared, patterns := v.collectDeclaredProperties(subSchema, data) for name, prop := range declared { if _, exists := allDeclared[name]; !exists { allDeclared[name] = prop } } allPatterns = append(allPatterns, patterns...) } // collect from parent schema declared, patterns := v.collectDeclaredProperties(schema, data) for name, prop := range declared { if _, exists := allDeclared[name]; !exists { allDeclared[name] = prop } } allPatterns = append(allPatterns, patterns...) // check if strict mode should report for this combined schema if !v.shouldReportUndeclaredForAllOf(schema) { // Still recurse into declared properties return v.recurseIntoAllOfDeclaredProperties(ctx, schema.AllOf, data, allDeclared) } // Check each property in data for propName, propValue := range data { propPath := buildPath(ctx.path, propName) propCtx := ctx.withPath(propPath) if propCtx.shouldIgnore() { continue } // Check if declared in merged schema if isPropertyDeclared(propName, allDeclared, allPatterns) { // Recurse into the property propSchema := v.findPropertySchemaInAllOf(schema.AllOf, propName, allDeclared) if propSchema != nil { if violation, ok := v.checkReadWriteOnlyViolation(propPath, propName, propValue, propSchema, ctx.direction); ok { undeclared = append(undeclared, violation) continue } if v.shouldSkipProperty(propSchema, ctx.direction) { continue } undeclared = append(undeclared, v.validateValue(propCtx, propSchema, propValue)...) } continue } // Not declared - report as undeclared undeclared = append(undeclared, newUndeclaredProperty(propPath, propName, propValue, getDeclaredPropertyNames(allDeclared), ctx.direction, schema)) } return undeclared } // validateOneOf finds the matching oneOf variant and validates against it. // Parent schema properties are merged with the variant's properties. func (v *Validator) validateOneOf(ctx *traversalContext, schema *base.Schema, data map[string]any) []UndeclaredValue { var matchingVariant *base.Schema // discriminator is present, use it to select the variant if schema.Discriminator != nil { matchingVariant = v.selectByDiscriminator(schema, schema.OneOf, data) } // no discriminator or no match: find matching variant by validation if matchingVariant == nil { matchingVariant = v.findMatchingVariant(schema.OneOf, data) } if matchingVariant == nil { // No match found - base validation would report this error return nil } // Validate against variant, but filter out properties declared in parent return v.validateVariantWithParent(ctx, schema, matchingVariant, data) } // validateAnyOf finds matching anyOf variants and validates against them. // Parent schema properties are merged with the variant's properties. func (v *Validator) validateAnyOf(ctx *traversalContext, schema *base.Schema, data map[string]any) []UndeclaredValue { var matchingVariant *base.Schema // If discriminator is present, use it to select the variant if schema.Discriminator != nil { matchingVariant = v.selectByDiscriminator(schema, schema.AnyOf, data) } // No discriminator or no match: find matching variant by validation if matchingVariant == nil { matchingVariant = v.findMatchingVariant(schema.AnyOf, data) } if matchingVariant == nil { // No match found - base validation would report this error return nil } // Validate against variant, but filter out properties declared in parent return v.validateVariantWithParent(ctx, schema, matchingVariant, data) } // validateVariantWithParent validates data against a variant schema while also // considering properties declared in the parent schema. This ensures parent // properties are not reported as undeclared when using oneOf/anyOf. func (v *Validator) validateVariantWithParent(ctx *traversalContext, parent *base.Schema, variant *base.Schema, data map[string]any) []UndeclaredValue { var undeclared []UndeclaredValue // Collect declared properties from parent schema parentDeclared, parentPatterns := v.collectDeclaredProperties(parent, data) // Collect declared properties from variant schema variantDeclared, variantPatterns := v.collectDeclaredProperties(variant, data) // Merge: parent + variant allDeclared := make(map[string]*declaredProperty) for name, prop := range parentDeclared { allDeclared[name] = prop } for name, prop := range variantDeclared { allDeclared[name] = prop } allPatterns := append(parentPatterns, variantPatterns...) // Check if we should report undeclared (skip if additionalProperties: false) if !v.shouldReportUndeclared(variant) && !v.shouldReportUndeclared(parent) { // Still recurse into declared properties return v.recurseIntoDeclaredPropertiesWithMerged(ctx, variant, parent, data, allDeclared) } // Check each property in data for propName, propValue := range data { propPath := buildPath(ctx.path, propName) propCtx := ctx.withPath(propPath) if propCtx.shouldIgnore() { continue } // Check if declared in merged schema (parent + variant) if isPropertyDeclared(propName, allDeclared, allPatterns) { // Find the property schema (prefer variant, fallback to parent) propSchema := v.findPropertySchemaInMerged(variant, parent, propName, allDeclared) if propSchema != nil { if violation, ok := v.checkReadWriteOnlyViolation(propPath, propName, propValue, propSchema, ctx.direction); ok { undeclared = append(undeclared, violation) continue } if v.shouldSkipProperty(propSchema, ctx.direction) { continue } undeclared = append(undeclared, v.validateValue(propCtx, propSchema, propValue)...) } continue } // Not declared - report as undeclared // Use variant schema location if available, otherwise fall back to parent locationSchema := variant if locationSchema == nil || locationSchema.GoLow() == nil { locationSchema = parent } undeclared = append(undeclared, newUndeclaredProperty(propPath, propName, propValue, getDeclaredPropertyNames(allDeclared), ctx.direction, locationSchema)) } return undeclared } // findPropertySchemaInMerged finds the schema for a property, preferring variant over parent. // Checks explicit properties first, then patternProperties. func (v *Validator) findPropertySchemaInMerged(variant, parent *base.Schema, propName string, declared map[string]*declaredProperty) *base.Schema { // Check explicit declared first if prop, ok := declared[propName]; ok && prop.proxy != nil { return prop.proxy.Schema() } // Check variant schema explicit properties if variant != nil && variant.Properties != nil { if propProxy, exists := variant.Properties.Get(propName); exists && propProxy != nil { return propProxy.Schema() } } // Check parent schema explicit properties if parent != nil && parent.Properties != nil { if propProxy, exists := parent.Properties.Get(propName); exists && propProxy != nil { return propProxy.Schema() } } // Check variant patternProperties if variant != nil { if propProxy := v.getPatternPropertySchema(variant, propName); propProxy != nil { return propProxy.Schema() } } // Check parent patternProperties if parent != nil { if propProxy := v.getPatternPropertySchema(parent, propName); propProxy != nil { return propProxy.Schema() } } return nil } // recurseIntoDeclaredPropertiesWithMerged recurses into properties from merged parent+variant. func (v *Validator) recurseIntoDeclaredPropertiesWithMerged(ctx *traversalContext, variant, parent *base.Schema, data map[string]any, declared map[string]*declaredProperty) []UndeclaredValue { var undeclared []UndeclaredValue for propName, propValue := range data { propPath := buildPath(ctx.path, propName) propCtx := ctx.withPath(propPath) if propCtx.shouldIgnore() { continue } propSchema := v.findPropertySchemaInMerged(variant, parent, propName, declared) if propSchema != nil { if violation, ok := v.checkReadWriteOnlyViolation(propPath, propName, propValue, propSchema, ctx.direction); ok { undeclared = append(undeclared, violation) continue } if v.shouldSkipProperty(propSchema, ctx.direction) { continue } undeclared = append(undeclared, v.validateValue(propCtx, propSchema, propValue)...) } } return undeclared } // selectByDiscriminator uses the discriminator to select the appropriate variant. func (v *Validator) selectByDiscriminator(schema *base.Schema, variants []*base.SchemaProxy, data map[string]any) *base.Schema { if schema.Discriminator == nil { return nil } propName := schema.Discriminator.PropertyName if propName == "" { return nil } discriminatorValue, ok := data[propName] if !ok { return nil } valueStr, ok := discriminatorValue.(string) if !ok { return nil } // check mapping first if schema.Discriminator.Mapping != nil { for pair := schema.Discriminator.Mapping.First(); pair != nil; pair = pair.Next() { if pair.Key() == valueStr { // The mapping value is a reference like "#/components/schemas/Dog" mappedRef := pair.Value() for _, variantProxy := range variants { if variantProxy.IsReference() && variantProxy.GetReference() == mappedRef { return variantProxy.Schema() } } } } } // no mapping match, try to match by schema name in reference for _, variantProxy := range variants { if variantProxy.IsReference() { ref := variantProxy.GetReference() // Extract schema name from reference like "#/components/schemas/Dog" parts := strings.Split(ref, "/") if len(parts) > 0 && parts[len(parts)-1] == valueStr { return variantProxy.Schema() } } } return nil } // findMatchingVariant finds the first variant that the data validates against. func (v *Validator) findMatchingVariant(variants []*base.SchemaProxy, data map[string]any) *base.Schema { for _, variantProxy := range variants { if variantProxy == nil { continue } variantSchema := variantProxy.Schema() if variantSchema == nil { continue } matches, _ := v.dataMatchesSchema(variantSchema, data) if matches { return variantSchema } } return nil } // isPropertyDeclaredInAllOf checks if a property is declared in any allOf schema. func (v *Validator) isPropertyDeclaredInAllOf(allOf []*base.SchemaProxy, propName string) bool { for _, schemaProxy := range allOf { if schemaProxy == nil { continue } subSchema := schemaProxy.Schema() if subSchema == nil { continue } if subSchema.Properties != nil { if _, exists := subSchema.Properties.Get(propName); exists { return true } } } return false } // shouldReportUndeclaredForAllOf checks if any schema in allOf disables additional properties. func (v *Validator) shouldReportUndeclaredForAllOf(schema *base.Schema) bool { // Check parent schema if schema.AdditionalProperties != nil && schema.AdditionalProperties.IsB() && !schema.AdditionalProperties.B { return false } // Check each allOf schema for _, schemaProxy := range schema.AllOf { if schemaProxy == nil { continue } subSchema := schemaProxy.Schema() if subSchema == nil { continue } if subSchema.AdditionalProperties != nil && subSchema.AdditionalProperties.IsB() && !subSchema.AdditionalProperties.B { return false } } return true } // findPropertySchemaInAllOf finds the schema for a property in allOf schemas. func (v *Validator) findPropertySchemaInAllOf(allOf []*base.SchemaProxy, propName string, declared map[string]*declaredProperty) *base.Schema { // Check explicit declared first if prop, ok := declared[propName]; ok && prop.proxy != nil { return prop.proxy.Schema() } // Search in allOf schemas for _, schemaProxy := range allOf { if schemaProxy == nil { continue } subSchema := schemaProxy.Schema() if subSchema == nil { continue } if subSchema.Properties != nil { if propProxy, exists := subSchema.Properties.Get(propName); exists && propProxy != nil { return propProxy.Schema() } } } return nil } // recurseIntoAllOfDeclaredProperties recurses into properties without checking for undeclared. func (v *Validator) recurseIntoAllOfDeclaredProperties(ctx *traversalContext, allOf []*base.SchemaProxy, data map[string]any, declared map[string]*declaredProperty) []UndeclaredValue { var undeclared []UndeclaredValue for propName, propValue := range data { propPath := buildPath(ctx.path, propName) propCtx := ctx.withPath(propPath) if propCtx.shouldIgnore() { continue } propSchema := v.findPropertySchemaInAllOf(allOf, propName, declared) if propSchema != nil { if violation, ok := v.checkReadWriteOnlyViolation(propPath, propName, propValue, propSchema, ctx.direction); ok { undeclared = append(undeclared, violation) continue } if v.shouldSkipProperty(propSchema, ctx.direction) { continue } undeclared = append(undeclared, v.validateValue(propCtx, propSchema, propValue)...) } } return undeclared } libopenapi-validator-0.13.8/strict/property_collector.go000066400000000000000000000133371520534042400235070ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package strict import ( "regexp" "github.com/pb33f/libopenapi/datamodel/high/base" ) // declaredProperty holds information about a declared property in a schema. type declaredProperty struct { // proxy is the SchemaProxy for the property. proxy *base.SchemaProxy } // collectDeclaredProperties gathers all property names that are declared in a schema. // This includes explicit properties, patternProperties matches, and properties from // dependentSchemas and if/then/else based on the actual data. // // Returns a map from property name to its declaration info, plus a slice of // pattern regexes for patternProperties matching. func (v *Validator) collectDeclaredProperties( schema *base.Schema, data map[string]any, ) (declared map[string]*declaredProperty, patterns []*regexp.Regexp) { declared = make(map[string]*declaredProperty) if schema == nil { return declared, nil } // explicit properties if schema.Properties != nil { for pair := schema.Properties.First(); pair != nil; pair = pair.Next() { declared[pair.Key()] = &declaredProperty{ proxy: pair.Value(), } } } // pattern properties - use cached compiled patterns if schema.PatternProperties != nil { for pair := schema.PatternProperties.First(); pair != nil; pair = pair.Next() { pattern := v.getCompiledPattern(pair.Key()) if pattern == nil { continue } patterns = append(patterns, pattern) } } // dependent schemas - if trigger property exists in data if schema.DependentSchemas != nil { for pair := schema.DependentSchemas.First(); pair != nil; pair = pair.Next() { triggerProp := pair.Key() if _, exists := data[triggerProp]; !exists { continue } // trigger property exists, include dependent schema's properties mergePropertiesIntoDeclared(declared, pair.Value().Schema()) } } // if/then/else if schema.If != nil { ifProxy := schema.If ifSchema := ifProxy.Schema() if ifSchema != nil { matches, err := v.dataMatchesSchema(ifSchema, data) if err != nil { // schema compilation failed - log and use else branch v.logger.Debug("strict: if schema compilation failed, using else branch", "error", err) matches = false } if matches { if schema.Then != nil { mergePropertiesIntoDeclared(declared, schema.Then.Schema()) } } else { if schema.Else != nil { mergePropertiesIntoDeclared(declared, schema.Else.Schema()) } } } } return declared, patterns } // mergePropertiesIntoDeclared merges properties from a schema's Properties map into // the declared map. Only adds properties that are not already declared. // This eliminates code duplication when collecting properties from multiple sources. func mergePropertiesIntoDeclared(declared map[string]*declaredProperty, schema *base.Schema) { if schema == nil || schema.Properties == nil { return } for p := schema.Properties.First(); p != nil; p = p.Next() { if _, alreadyDeclared := declared[p.Key()]; !alreadyDeclared { declared[p.Key()] = &declaredProperty{ proxy: p.Value(), } } } } // getDeclaredPropertyNames returns just the property names from declared properties. func getDeclaredPropertyNames(declared map[string]*declaredProperty) []string { if len(declared) == 0 { return nil } names := make([]string, 0, len(declared)) for name := range declared { names = append(names, name) } return names } // isPropertyDeclared checks if a property name is declared in the schema. // A property is declared if: // - It's in the explicit properties map // - It matches any patternProperties regex func isPropertyDeclared(name string, declared map[string]*declaredProperty, patterns []*regexp.Regexp) bool { // check explicit properties if _, ok := declared[name]; ok { return true } // check pattern properties for _, pattern := range patterns { if pattern.MatchString(name) { return true } } return false } // getPropertySchema returns the SchemaProxy for a declared property. // Returns nil if the property is not declared or is only matched by pattern. func getPropertySchema(name string, declared map[string]*declaredProperty) *base.SchemaProxy { // check explicit properties first if dp, ok := declared[name]; ok && dp.proxy != nil { return dp.proxy } return nil } // checkReadWriteOnlyViolation checks if a property violates readOnly/writeOnly rules // when the corresponding rejection flag is enabled. Returns a violation and true if so. func (v *Validator) checkReadWriteOnlyViolation( path string, name string, value any, schema *base.Schema, direction Direction, ) (UndeclaredValue, bool) { if schema == nil || v.options == nil { return UndeclaredValue{}, false } if direction == DirectionRequest && v.options.StrictRejectReadOnly && schema.ReadOnly != nil && *schema.ReadOnly { return newReadWriteOnlyViolation(path, name, value, direction, schema), true } if direction == DirectionResponse && v.options.StrictRejectWriteOnly && schema.WriteOnly != nil && *schema.WriteOnly { return newReadWriteOnlyViolation(path, name, value, direction, schema), true } return UndeclaredValue{}, false } // shouldSkipProperty checks if a property should be skipped based on // readOnly/writeOnly and the current validation direction. func (v *Validator) shouldSkipProperty(schema *base.Schema, direction Direction) bool { if schema == nil { return false } // readOnly: skip in requests (should not be sent by client) if direction == DirectionRequest && schema.ReadOnly != nil && *schema.ReadOnly { return true } // writeOnly: skip in responses (should not be returned by server) if direction == DirectionResponse && schema.WriteOnly != nil && *schema.WriteOnly { return true } return false } libopenapi-validator-0.13.8/strict/schema_walker.go000066400000000000000000000163571520534042400223670ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package strict import ( "github.com/pb33f/libopenapi/datamodel/high/base" ) // validateValue is the main entry point for validating a value against a schema. // It dispatches to the appropriate handler based on the value type. func (v *Validator) validateValue(ctx *traversalContext, schema *base.Schema, data any) []UndeclaredValue { if schema == nil || data == nil { return nil } if ctx.shouldIgnore() { return nil } if ctx.exceedsDepth() { return nil } // check for cycles using schema hash schemaKey := v.getSchemaKey(schema) if ctx.checkAndMarkVisited(schemaKey) { return nil } // switch on data type switch val := data.(type) { case map[string]any: return v.validateObject(ctx, schema, val) case []any: return v.validateArray(ctx, schema, val) default: return nil } } // validateObject checks an object value against a schema for undeclared properties. func (v *Validator) validateObject(ctx *traversalContext, schema *base.Schema, data map[string]any) []UndeclaredValue { var undeclared []UndeclaredValue if len(schema.AllOf) > 0 || len(schema.OneOf) > 0 || len(schema.AnyOf) > 0 { return v.validatePolymorphic(ctx, schema, data) } if !v.shouldReportUndeclared(schema) { // additionalProperties: false - base validation catches this, no strict check needed // Still need to recurse into declared properties return v.recurseIntoDeclaredProperties(ctx, schema, data) } declared, patterns := v.collectDeclaredProperties(schema, data) // check each property in the data for propName, propValue := range data { propPath := buildPath(ctx.path, propName) propCtx := ctx.withPath(propPath) if propCtx.shouldIgnore() { continue } if !isPropertyDeclared(propName, declared, patterns) { undeclared = append(undeclared, newUndeclaredProperty(propPath, propName, propValue, getDeclaredPropertyNames(declared), ctx.direction, schema)) // even if undeclared, recurse into additionalProperties schema if present if schema.AdditionalProperties != nil && schema.AdditionalProperties.IsA() { addlProxy := schema.AdditionalProperties.A if addlProxy != nil { addlSchema := addlProxy.Schema() if addlSchema != nil { undeclared = append(undeclared, v.validateValue(propCtx, addlSchema, propValue)...) } } } continue } // property is declared, recurse into it propProxy := getPropertySchema(propName, declared) if propProxy == nil { propProxy = v.getPatternPropertySchema(schema, propName) } if propProxy != nil { propSchema := propProxy.Schema() if propSchema != nil { if violation, ok := v.checkReadWriteOnlyViolation(propPath, propName, propValue, propSchema, ctx.direction); ok { undeclared = append(undeclared, violation) continue } if v.shouldSkipProperty(propSchema, ctx.direction) { continue } undeclared = append(undeclared, v.validateValue(propCtx, propSchema, propValue)...) } } } return undeclared } // shouldReportUndeclared determines if strict mode should report undeclared // properties for this schema. func (v *Validator) shouldReportUndeclared(schema *base.Schema) bool { if schema == nil { return false } // SHORT-CIRCUIT: If additionalProperties: false, base validation already catches extras. if schema.AdditionalProperties != nil && schema.AdditionalProperties.IsB() && !schema.AdditionalProperties.B { return false } // STRICT OVERRIDE: Even if additionalProperties: true, report undeclared. if schema.AdditionalProperties != nil { if schema.AdditionalProperties.IsB() && schema.AdditionalProperties.B { return true } if schema.AdditionalProperties.IsA() { // additionalProperties with schema - properties matching schema are // technically "declared" but we still want to flag them as not in // the explicit schema. They will be recursed into. return true } } // STRICT OVERRIDE: unevaluatedProperties: false with implicit additionalProperties: true // Standard JSON Schema would catch via unevaluatedProperties, but strict reports // even when additionalProperties: true would normally allow extras. if schema.UnevaluatedProperties != nil && schema.UnevaluatedProperties.IsB() && !schema.UnevaluatedProperties.B { // unevaluatedProperties: false means base validation catches extras // BUT if there's no additionalProperties: false, strict should report return true } // default: no additionalProperties means implicit true in JSON Schema // Strict reports undeclared in this case return true } // getPatternPropertySchema finds the schema for a property that matches // a patternProperties regex. Uses cached compiled patterns. func (v *Validator) getPatternPropertySchema(schema *base.Schema, propName string) *base.SchemaProxy { if schema.PatternProperties == nil { return nil } for pair := schema.PatternProperties.First(); pair != nil; pair = pair.Next() { pattern := v.getCompiledPattern(pair.Key()) if pattern == nil { continue } if pattern.MatchString(propName) { return pair.Value() } } return nil } // recurseIntoDeclaredProperties recurses into declared properties without // checking for undeclared (used when additionalProperties: false). // This includes both explicit properties and patternProperties matches. func (v *Validator) recurseIntoDeclaredProperties(ctx *traversalContext, schema *base.Schema, data map[string]any) []UndeclaredValue { var undeclared []UndeclaredValue processed := make(map[string]bool) // process explicit properties if schema.Properties != nil { for pair := schema.Properties.First(); pair != nil; pair = pair.Next() { propName := pair.Key() propProxy := pair.Value() propValue, exists := data[propName] if !exists { continue } processed[propName] = true propPath := buildPath(ctx.path, propName) propCtx := ctx.withPath(propPath) if propCtx.shouldIgnore() { continue } propSchema := propProxy.Schema() if propSchema != nil { if violation, ok := v.checkReadWriteOnlyViolation(propPath, propName, propValue, propSchema, ctx.direction); ok { undeclared = append(undeclared, violation) continue } if v.shouldSkipProperty(propSchema, ctx.direction) { continue } undeclared = append(undeclared, v.validateValue(propCtx, propSchema, propValue)...) } } } // process patternProperties - recurse into any data properties that match patterns if schema.PatternProperties != nil { for propName, propValue := range data { if processed[propName] { continue } propProxy := v.getPatternPropertySchema(schema, propName) if propProxy == nil { continue } processed[propName] = true propPath := buildPath(ctx.path, propName) propCtx := ctx.withPath(propPath) if propCtx.shouldIgnore() { continue } propSchema := propProxy.Schema() if propSchema != nil { if violation, ok := v.checkReadWriteOnlyViolation(propPath, propName, propValue, propSchema, ctx.direction); ok { undeclared = append(undeclared, violation) continue } if v.shouldSkipProperty(propSchema, ctx.direction) { continue } undeclared = append(undeclared, v.validateValue(propCtx, propSchema, propValue)...) } } } return undeclared } libopenapi-validator-0.13.8/strict/types.go000066400000000000000000000337011520534042400207160ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT // Package strict provides strict validation that detects undeclared // properties in requests and responses, even when additionalProperties // would normally allow them. // // Strict mode is designed for API governance scenarios where you want to // ensure that clients only send properties that are explicitly documented // in the OpenAPI specification, regardless of whether additionalProperties // is set to true. // // # Key Features // // - Detects undeclared properties in request/response bodies (JSON only) // - Detects undeclared query parameters, headers, and cookies // - Supports ignore paths with glob patterns (e.g., "$.body.metadata.*") // - Handles polymorphic schemas (oneOf/anyOf) via per-branch validation // - Respects readOnly/writeOnly based on request vs response direction // - Configurable header ignore list with sensible defaults // // # Known Limitations // // Property names containing single quotes (e.g., {"it's": "value"}) cannot be // represented in bracket notation and cannot be matched by ignore patterns. // Such properties will always be reported as undeclared if not in schema. // This is acceptable because property names with quotes are extremely rare. package strict import ( "context" "fmt" "log/slog" "regexp" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/santhosh-tekuri/jsonschema/v6" "github.com/pb33f/libopenapi-validator/config" ) // Direction indicates whether validation is for a request or response. // This affects readOnly/writeOnly handling and Set-Cookie behavior. type Direction int const ( // DirectionRequest indicates validation of an HTTP request. // readOnly properties are not expected in request bodies. DirectionRequest Direction = iota // DirectionResponse indicates validation of an HTTP response. // writeOnly properties are not expected in response bodies. // Set-Cookie headers are ignored (expected in responses). DirectionResponse ) // String returns a human-readable direction name. func (d Direction) String() string { if d == DirectionResponse { return "response" } return "request" } // Type constants for UndeclaredValue.Type, defined here for use in the // request/response dispatch switch (existing types use inline strings). const ( TypeReadOnlyProperty = "readonly" TypeWriteOnlyProperty = "writeonly" ) // UndeclaredValue represents a value found in data that is not declared // in the schema. This is the core output of strict validation. type UndeclaredValue struct { // Path is the instance JSONPath where the undeclared value was found. // uses bracket notation for property names with special characters. // examples: "$.body.user.extra", "$.body['a.b'].value", "$.query.debug" Path string // Name is the property, parameter, header, or cookie name. Name string // Value is the actual value found (it may be truncated for display). Value any // Type indicates what kind of value this is. // one of: "property", "header", "query", "cookie", "item", "readonly", "writeonly" Type string // DeclaredProperties lists property names that ARE declared at this // location in the schema. Helps users understand what's expected. // for headers/query/cookies, this lists declared parameter names. DeclaredProperties []string // Direction indicates whether this was in a request or response. // used for error message disambiguation when Path is "$.body". Direction Direction // SpecLine is the line number in the OpenAPI spec where the parent // schema is defined. Zero if unavailable. SpecLine int // SpecCol is the column number in the OpenAPI spec where the parent // schema is defined. Zero if unavailable. SpecCol int } // extractSchemaLocation extracts the line and column from a schema's low-level // representation. returns (0, 0) if the schema is nil or has no low-level info. func extractSchemaLocation(schema *base.Schema) (line, col int) { if schema == nil { return 0, 0 } low := schema.GoLow() if low == nil || low.RootNode == nil { return 0, 0 } return low.RootNode.Line, low.RootNode.Column } // newUndeclaredProperty creates an UndeclaredValue for an undeclared object property. // the schema parameter is the parent schema where the property would need to be declared. func newUndeclaredProperty(path, name string, value any, declaredNames []string, direction Direction, schema *base.Schema) UndeclaredValue { line, col := extractSchemaLocation(schema) return UndeclaredValue{ Path: path, Name: name, Value: TruncateValue(value), Type: "property", DeclaredProperties: declaredNames, Direction: direction, SpecLine: line, SpecCol: col, } } // newReadWriteOnlyViolation creates an UndeclaredValue for a readOnly/writeOnly violation. func newReadWriteOnlyViolation(path, name string, value any, direction Direction, schema *base.Schema) UndeclaredValue { line, col := extractSchemaLocation(schema) violationType := TypeReadOnlyProperty if direction == DirectionResponse { violationType = TypeWriteOnlyProperty } return UndeclaredValue{ Path: path, Name: name, Value: TruncateValue(value), Type: violationType, Direction: direction, SpecLine: line, SpecCol: col, } } // newUndeclaredParam creates an UndeclaredValue for an undeclared parameter (query/header/cookie). // note: parameters don't have SpecLine/SpecCol because they're defined in OpenAPI parameter objects, // not schema objects. the parameter itself is the issue, not a schema definition. func newUndeclaredParam(path, name string, value any, paramType string, declaredNames []string, direction Direction) UndeclaredValue { return UndeclaredValue{ Path: path, Name: name, Value: value, Type: paramType, DeclaredProperties: declaredNames, Direction: direction, } } // newUndeclaredItem creates an UndeclaredValue for an undeclared array item. func newUndeclaredItem(path, name string, value any, direction Direction) UndeclaredValue { return UndeclaredValue{ Path: path, Name: name, Value: TruncateValue(value), Type: "item", Direction: direction, } } // Input contains the parameters for strict validation. type Input struct { // Schema is the OpenAPI schema to validate against. Schema *base.Schema // Data is the unmarshalled data to validate (from request/response body). // Should be the result of json.Unmarshal. Data any // Direction indicates request vs response validation. // affects readOnly/writeOnly and Set-Cookie handling. Direction Direction // Options contains validation configuration including ignore paths. Options *config.ValidationOptions // BasePath is the prefix for generated instance paths. // typically "$.body" for bodies, "$.query" for query params, etc. BasePath string // Version is the OpenAPI version (3.0 or 3.1). // affects nullable handling in schema matching. Version float32 } // Result contains the output of strict validation. type Result struct { Valid bool // UndeclaredValues lists all undeclared properties, parameters, // headers, or cookies found during validation. UndeclaredValues []UndeclaredValue } // cycleKey uniquely identifies a schema at a specific validation path. // Using a struct key avoids string allocation in the hot path. type cycleKey struct { path string schemaKey string } // traversalContext tracks state during schema traversal to detect cycles // and limit recursion depth. type traversalContext struct { // visited tracks schemas already being validated at specific paths. // key combines instance path + schema key to allow same schema at different paths. visited map[cycleKey]bool // depth tracks current recursion depth for safety limits. depth int // maxDepth is the maximum allowed recursion depth (default: 100). maxDepth int // direction indicates request vs response for readOnly/writeOnly. direction Direction // ignorePaths are compiled regex patterns for paths to skip. ignorePaths []*regexp.Regexp // path is the current instance path being validated. path string } // newTraversalContext creates a new context for schema traversal. func newTraversalContext(direction Direction, ignorePaths []*regexp.Regexp, basePath string) *traversalContext { return &traversalContext{ visited: make(map[cycleKey]bool), depth: 0, maxDepth: 100, direction: direction, ignorePaths: ignorePaths, path: basePath, } } // withPath returns a new context with an updated path. func (c *traversalContext) withPath(path string) *traversalContext { return &traversalContext{ visited: c.visited, depth: c.depth + 1, maxDepth: c.maxDepth, direction: c.direction, ignorePaths: c.ignorePaths, path: path, } } // shouldIgnore checks if the current path matches any ignore pattern. func (c *traversalContext) shouldIgnore() bool { for _, pattern := range c.ignorePaths { if pattern.MatchString(c.path) { return true } } return false } // exceedsDepth checks if we've exceeded the maximum recursion depth. func (c *traversalContext) exceedsDepth() bool { return c.depth > c.maxDepth } // checkAndMarkVisited checks if a schema has been visited at the current path. // Returns true if this is a cycle (already visited), false otherwise. // If not a cycle, marks the schema as visited. func (c *traversalContext) checkAndMarkVisited(schemaKey string) bool { key := cycleKey{path: c.path, schemaKey: schemaKey} if c.visited[key] { return true // Cycle detected } c.visited[key] = true return false } // Validator performs strict property validation against OpenAPI schemas. // It detects any properties present in data that are not explicitly // declared in the schema, regardless of additionalProperties settings. // // A new Validator should be created for each validation call to ensure // isolation of internal caches and render contexts. // // # Cycle Detection // // The Validator uses two distinct cycle detection mechanisms: // // 1. traversalContext.visited: Tracks visited (path, schemaKey) combinations // during the main validation traversal. This prevents infinite recursion // when the same schema is encountered at the same instance path. The key // uses a struct for zero-allocation lookups in the hot path. // // 2. renderCtx (InlineRenderContext): libopenapi's built-in cycle detection // for schema rendering. This is used when compiling schemas for oneOf/anyOf // variant matching. It operates at the schema reference level rather than // instance path level. // // These mechanisms serve complementary purposes: visited tracks data traversal // while renderCtx tracks schema resolution during compilation. type Validator struct { options *config.ValidationOptions logger *slog.Logger // localCache stores compiled schemas for reuse within this validation. // ley is schema hash (as string for map compatibility), value is compiled jsonschema. localCache map[string]*jsonschema.Schema // patternCache stores compiled regex patterns for patternProperties. // key is the pattern string, value is the compiled regex. patternCache map[string]*regexp.Regexp // renderCtx is used for safe schema rendering with cycle detection. // see Validator doc comment for how this relates to traversalContext.visited. renderCtx *base.InlineRenderContext // version is the OpenAPI version (3.0 or 3.1). version float32 // compiledIgnorePaths are the pre-compiled regex patterns. compiledIgnorePaths []*regexp.Regexp } // NewValidator creates a fresh validator for a single validation call. // The validator should not be reused across concurrent requests. // Uses the logger from options if available, otherwise logging is silent. func NewValidator(options *config.ValidationOptions, version float32) *Validator { var logger *slog.Logger if options != nil && options.Logger != nil { logger = options.Logger } else { // create a no-op logger that discards all output logger = slog.New(discardHandler{}) } v := &Validator{ options: options, logger: logger, localCache: make(map[string]*jsonschema.Schema), patternCache: make(map[string]*regexp.Regexp), renderCtx: base.NewInlineRenderContextForValidation(), version: version, } if options != nil { v.compiledIgnorePaths = compileIgnorePaths(options.StrictIgnorePaths) } return v } // discardHandler is a slog.Handler that discards all log records. type discardHandler struct{} func (discardHandler) Enabled(context.Context, slog.Level) bool { return false } func (discardHandler) Handle(context.Context, slog.Record) error { return nil } func (d discardHandler) WithAttrs([]slog.Attr) slog.Handler { return d } func (d discardHandler) WithGroup(string) slog.Handler { return d } // matchesIgnorePath checks if a path matches any pre-compiled ignore pattern. func (v *Validator) matchesIgnorePath(path string) bool { for _, pattern := range v.compiledIgnorePaths { if pattern.MatchString(path) { return true } } return false } // getCompiledPattern returns a cached compiled regex for a pattern string. // If the pattern is not in the cache, it compiles and caches it. // Returns nil if the pattern is invalid. func (v *Validator) getCompiledPattern(pattern string) *regexp.Regexp { if cached, ok := v.patternCache[pattern]; ok { return cached } compiled, err := regexp.Compile(pattern) if err != nil { return nil } v.patternCache[pattern] = compiled return compiled } // getSchemaKey returns a unique key for a schema used in cycle detection. // Uses the schema's low-level hash if available, otherwise the pointer address. func (v *Validator) getSchemaKey(schema *base.Schema) string { if schema == nil { return "" } if low := schema.GoLow(); low != nil { hash := low.Hash() return fmt.Sprintf("%x", hash) // uint64 hash as hex string } // fallback to pointer address for inline schemas without low-level info return fmt.Sprintf("%p", schema) } libopenapi-validator-0.13.8/strict/types_test.go000066400000000000000000000064311520534042400217550ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package strict import ( "testing" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestExtractSchemaLocation_NilSchema(t *testing.T) { line, col := extractSchemaLocation(nil) assert.Equal(t, 0, line) assert.Equal(t, 0, col) } func TestExtractSchemaLocation_WithValidSchema(t *testing.T) { spec := `openapi: "3.1.0" info: title: Test version: "1.0" paths: /test: get: responses: "200": description: OK content: application/json: schema: type: object properties: name: type: string` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) model, errs := doc.BuildV3Model() require.Empty(t, errs) schema := model.Model.Paths.PathItems.GetOrZero("/test").Get.Responses.Codes.GetOrZero("200").Content.GetOrZero("application/json").Schema.Schema() require.NotNil(t, schema) line, col := extractSchemaLocation(schema) // The schema starts at line 14 (type: object) assert.Greater(t, line, 0, "line should be greater than 0") assert.Greater(t, col, 0, "col should be greater than 0") } func TestExtractSchemaLocation_SchemaWithNilGoLow(t *testing.T) { // Create a high-level schema programmatically (no GoLow()) // This covers types.go:108 where low == nil schema := &base.Schema{ Type: []string{"object"}, } // GoLow() returns nil for programmatically created schemas require.Nil(t, schema.GoLow()) line, col := extractSchemaLocation(schema) assert.Equal(t, 0, line) assert.Equal(t, 0, col) } func TestNewUndeclaredProperty_WithLocation(t *testing.T) { spec := `openapi: "3.1.0" info: title: Test version: "1.0" paths: /test: post: requestBody: content: application/json: schema: type: object properties: name: type: string` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) model, errs := doc.BuildV3Model() require.Empty(t, errs) schema := model.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema.Schema() require.NotNil(t, schema) undeclared := newUndeclaredProperty( "$.body.extra", "extra", "value", []string{"name"}, DirectionRequest, schema, ) assert.Equal(t, "$.body.extra", undeclared.Path) assert.Equal(t, "extra", undeclared.Name) assert.Equal(t, "property", undeclared.Type) assert.Greater(t, undeclared.SpecLine, 0, "SpecLine should be set") assert.Greater(t, undeclared.SpecCol, 0, "SpecCol should be set") } func TestNewUndeclaredProperty_WithNilSchema(t *testing.T) { undeclared := newUndeclaredProperty( "$.body.extra", "extra", "value", []string{"name"}, DirectionRequest, nil, // nil schema ) assert.Equal(t, "$.body.extra", undeclared.Path) assert.Equal(t, "extra", undeclared.Name) assert.Equal(t, "property", undeclared.Type) assert.Equal(t, 0, undeclared.SpecLine, "SpecLine should be 0 for nil schema") assert.Equal(t, 0, undeclared.SpecCol, "SpecCol should be 0 for nil schema") } libopenapi-validator-0.13.8/strict/utils.go000066400000000000000000000073621520534042400207160ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package strict import ( "regexp" "strconv" "strings" ) // buildPath creates an instance path by appending a property name to a base path. // Property names containing dots or brackets use bracket notation for clarity. // // Examples: // - buildPath("$.body", "name") → "$.body.name" // - buildPath("$.body", "a.b") → "$.body['a.b']" // - buildPath("$.body", "x[0]") → "$.body['x[0]']" func buildPath(base, propName string) string { if needsBracketNotation(propName) { return base + "['" + propName + "']" } return base + "." + propName } // needsBracketNotation returns true if a property name contains characters // that require bracket notation (dots, brackets). func needsBracketNotation(name string) bool { return strings.ContainsAny(name, ".[]") } // buildArrayPath creates an instance path for an array element. func buildArrayPath(base string, index int) string { return base + "[" + strconv.Itoa(index) + "]" } // compileIgnorePaths converts glob patterns to compiled regular expressions. // Supports: // - * matches single path segment (no dots or brackets) // - ** matches any depth (zero or more segments) // - [*] matches any array index // - \* escapes literal asterisk // - \*\* escapes literal double-asterisk func compileIgnorePaths(patterns []string) []*regexp.Regexp { if len(patterns) == 0 { return nil } compiled := make([]*regexp.Regexp, 0, len(patterns)) for _, pattern := range patterns { re := compilePattern(pattern) if re != nil { compiled = append(compiled, re) } } return compiled } // compilePattern converts a single glob pattern to a regular expression. func compilePattern(pattern string) *regexp.Regexp { if pattern == "" { return nil } var b strings.Builder b.WriteString("^") i := 0 for i < len(pattern) { c := pattern[i] // handle escape sequences if c == '\\' && i+1 < len(pattern) { next := pattern[i+1] if next == '*' { // check for escaped ** if i+2 < len(pattern) && pattern[i+2] == '\\' && i+3 < len(pattern) && pattern[i+3] == '*' { b.WriteString(`\*\*`) i += 4 continue } // escaped single * b.WriteString(`\*`) i += 2 continue } // other escape - include literally b.WriteString(regexp.QuoteMeta(string(next))) i += 2 continue } // handle ** (any depth) if c == '*' && i+1 < len(pattern) && pattern[i+1] == '*' { // ** matches any sequence of segments including none b.WriteString(`.*`) i += 2 continue } // handle single * (single segment) if c == '*' { // * matches single path segment (no dots or brackets) b.WriteString(`[^.\[\]]+`) i++ continue } // handle [*] (any array index) if c == '[' && i+2 < len(pattern) && pattern[i+1] == '*' && pattern[i+2] == ']' { b.WriteString(`\[\d+\]`) i += 3 continue } // handle special regex characters switch c { case '.', '[', ']', '(', ')', '{', '}', '+', '?', '^', '$', '|': b.WriteString(`\`) b.WriteByte(c) default: b.WriteByte(c) } i++ } b.WriteString("$") re, _ := regexp.Compile(b.String()) return re } // TruncateValue creates a display-friendly version of a value. // Long strings are truncated, complex objects show type info. // This is exported for use in error messages. func TruncateValue(v any) any { switch val := v.(type) { case string: if len(val) > 50 { return val[:47] + "..." } return val case map[string]any: if len(val) > 3 { return "{...}" } return val case []any: if len(val) > 3 { return "[...]" } return val default: return v } } // truncateValue is an internal alias for TruncateValue. func truncateValue(v any) any { return TruncateValue(v) } libopenapi-validator-0.13.8/strict/utils_test.go000066400000000000000000000071511520534042400217510ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package strict import ( "testing" "github.com/stretchr/testify/assert" ) func TestCompilePattern_EscapedDoubleAsterisk(t *testing.T) { // Pattern \*\* should match literal "**" in path (lines 78-80) // This escapes the glob ** so it matches the literal string ** re := compilePattern(`$.body.\*\*`) assert.NotNil(t, re) // Should match literal ** in path assert.True(t, re.MatchString("$.body.**")) // Should NOT match arbitrary depth (that's what unescaped ** does) assert.False(t, re.MatchString("$.body.foo.bar")) } func TestCompilePattern_EscapedNonAsterisk(t *testing.T) { // Pattern with escaped character that's not * (lines 88-90) // \n should match literal 'n', \. should match literal '.' re := compilePattern(`$.body\nvalue`) assert.NotNil(t, re) // Should match with literal 'n' (the escape just includes the next char) assert.True(t, re.MatchString("$.bodynvalue")) } func TestCompilePattern_EscapedDot(t *testing.T) { // Escaped dot should be literal dot re := compilePattern(`$.body\.name`) assert.NotNil(t, re) // Should match path with literal dot assert.True(t, re.MatchString("$.body.name")) } func TestCompilePattern_Empty(t *testing.T) { // Empty pattern returns nil re := compilePattern("") assert.Nil(t, re) } func TestBuildPath_WithDot(t *testing.T) { // Property with dot uses bracket notation result := buildPath("$.body", "a.b") assert.Equal(t, "$.body['a.b']", result) } func TestBuildPath_WithBrackets(t *testing.T) { // Property with brackets uses bracket notation result := buildPath("$.body", "x[0]") assert.Equal(t, "$.body['x[0]']", result) } func TestBuildPath_Simple(t *testing.T) { // Simple property uses dot notation result := buildPath("$.body", "name") assert.Equal(t, "$.body.name", result) } func TestBuildArrayPath(t *testing.T) { result := buildArrayPath("$.body.items", 5) assert.Equal(t, "$.body.items[5]", result) } func TestCompileIgnorePaths_Empty(t *testing.T) { result := compileIgnorePaths(nil) assert.Nil(t, result) result = compileIgnorePaths([]string{}) assert.Nil(t, result) } func TestCompileIgnorePaths_WithPatterns(t *testing.T) { patterns := []string{ "$.body.metadata", "$.body.items[*].internal", } result := compileIgnorePaths(patterns) assert.Len(t, result, 2) } func TestTruncateValue_LongString(t *testing.T) { // String > 50 chars gets truncated longStr := "this is a very long string that exceeds fifty characters in length" result := TruncateValue(longStr) assert.Equal(t, "this is a very long string that exceeds fifty c...", result) } func TestTruncateValue_ShortString(t *testing.T) { shortStr := "short" result := TruncateValue(shortStr) assert.Equal(t, "short", result) } func TestTruncateValue_LargeMap(t *testing.T) { // Map with > 3 keys shows {...} m := map[string]any{"a": 1, "b": 2, "c": 3, "d": 4} result := TruncateValue(m) assert.Equal(t, "{...}", result) } func TestTruncateValue_SmallMap(t *testing.T) { m := map[string]any{"a": 1, "b": 2} result := TruncateValue(m) assert.Equal(t, m, result) } func TestTruncateValue_LargeSlice(t *testing.T) { // Slice with > 3 elements shows [...] s := []any{1, 2, 3, 4} result := TruncateValue(s) assert.Equal(t, "[...]", result) } func TestTruncateValue_SmallSlice(t *testing.T) { s := []any{1, 2} result := TruncateValue(s) assert.Equal(t, s, result) } func TestTruncateValue_OtherTypes(t *testing.T) { // Other types returned as-is assert.Equal(t, 42, TruncateValue(42)) assert.Equal(t, true, TruncateValue(true)) assert.Equal(t, 3.14, TruncateValue(3.14)) } libopenapi-validator-0.13.8/strict/validator.go000066400000000000000000000157731520534042400215500ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package strict import ( "net/http" "strings" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/config" ) // Validate performs strict validation on the input data against the schema. // This is the main entry point for body validation. // // It detects undeclared properties even when additionalProperties: true // would normally allow them. This is useful for API governance scenarios // where you want to ensure clients only send explicitly documented properties. func (v *Validator) Validate(input Input) *Result { result := &Result{Valid: true} if input.Schema == nil || input.Data == nil { return result } ctx := newTraversalContext(input.Direction, v.compiledIgnorePaths, input.BasePath) undeclared := v.validateValue(ctx, input.Schema, input.Data) if len(undeclared) > 0 { result.Valid = false result.UndeclaredValues = undeclared } return result } // ValidateBody is a convenience method for validating request/response bodies. func ValidateBody(schema *base.Schema, data any, direction Direction, options *config.ValidationOptions, version float32) *Result { v := NewValidator(options, version) return v.Validate(Input{ Schema: schema, Data: data, Direction: direction, Options: options, BasePath: "$.body", Version: version, }) } // ValidateQueryParams checks for undeclared query parameters in an HTTP request. // It compares the query parameters present in the request against those // declared in the OpenAPI operation. func ValidateQueryParams( request *http.Request, declaredParams []*v3.Parameter, options *config.ValidationOptions, ) []UndeclaredValue { if request == nil || options == nil || !options.StrictMode { return nil } v := NewValidator(options, 3.2) // build set of declared query params (case-sensitive) declared := make(map[string]bool) for _, param := range declaredParams { if param.In == "query" { declared[param.Name] = true } } var undeclared []UndeclaredValue // check each query parameter in the request for paramName := range request.URL.Query() { if !declared[paramName] { // build path using proper notation for special characters path := buildPath("$.query", paramName) if v.matchesIgnorePath(path) { continue } undeclared = append(undeclared, newUndeclaredParam(path, paramName, request.URL.Query().Get(paramName), "query", getParamNames(declaredParams, "query"), DirectionRequest)) } } return undeclared } // ValidateRequestHeaders checks for undeclared headers in an HTTP request. // Header names are normalized to lowercase for path generation and pattern matching. // // The securityHeaders parameter contains header names that are valid due to security // scheme definitions (e.g., "X-API-Key" for apiKey schemes, "Authorization" for // http/oauth2/openIdConnect schemes). These headers are considered "declared" even // though they don't appear in the operation's parameters array. func ValidateRequestHeaders( headers http.Header, declaredParams []*v3.Parameter, securityHeaders []string, options *config.ValidationOptions, ) []UndeclaredValue { if headers == nil || options == nil || !options.StrictMode { return nil } v := NewValidator(options, 3.2) // build set of declared headers (case-insensitive) declared := make(map[string]bool) for _, param := range declaredParams { if param.In == "header" { declared[strings.ToLower(param.Name)] = true } } // add security scheme headers (case-insensitive) for _, h := range securityHeaders { declared[strings.ToLower(h)] = true } var undeclared []UndeclaredValue // check each header for headerName := range headers { lowerName := strings.ToLower(headerName) // skip if declared (via parameters or security schemes) if declared[lowerName] { continue } // skip if in ignored headers list if v.isHeaderIgnored(headerName, DirectionRequest) { continue } // build path using lowercase name for case-insensitive pattern matching path := buildPath("$.headers", lowerName) if v.matchesIgnorePath(path) { continue } undeclared = append(undeclared, newUndeclaredParam(path, headerName, headers.Get(headerName), "header", getParamNames(declaredParams, "header"), DirectionRequest)) } return undeclared } // ValidateCookies checks for undeclared cookies in an HTTP request. func ValidateCookies( request *http.Request, declaredParams []*v3.Parameter, options *config.ValidationOptions, ) []UndeclaredValue { if request == nil || options == nil || !options.StrictMode { return nil } v := NewValidator(options, 3.2) // build set of declared cookies declared := make(map[string]bool) for _, param := range declaredParams { if param.In == "cookie" { declared[param.Name] = true } } var undeclared []UndeclaredValue // check each cookie in the request for _, cookie := range request.Cookies() { if !declared[cookie.Name] { // build path using proper notation for special characters path := buildPath("$.cookies", cookie.Name) if v.matchesIgnorePath(path) { continue } undeclared = append(undeclared, newUndeclaredParam(path, cookie.Name, cookie.Value, "cookie", getParamNames(declaredParams, "cookie"), DirectionRequest)) } } return undeclared } // getParamNames extracts parameter names of a specific type. func getParamNames(params []*v3.Parameter, paramType string) []string { var names []string for _, param := range params { if param.In == paramType { names = append(names, param.Name) } } return names } // ValidateResponseHeaders checks for undeclared headers in an HTTP response. // Uses the declared headers from the OpenAPI response object. // Header names are normalized to lowercase for path generation and pattern matching. func ValidateResponseHeaders( headers http.Header, declaredHeaders *map[string]*v3.Header, options *config.ValidationOptions, ) []UndeclaredValue { if headers == nil || options == nil || !options.StrictMode { return nil } v := NewValidator(options, 3.2) // build set of declared headers (case-insensitive) declared := make(map[string]bool) if declaredHeaders != nil { for name := range *declaredHeaders { declared[strings.ToLower(name)] = true } } var undeclared []UndeclaredValue var declaredNames []string if declaredHeaders != nil { for name := range *declaredHeaders { declaredNames = append(declaredNames, name) } } for headerName := range headers { lowerName := strings.ToLower(headerName) if declared[lowerName] { continue } if v.isHeaderIgnored(headerName, DirectionResponse) { continue } // build path using lowercase name for case-insensitive pattern matching path := buildPath("$.headers", lowerName) if v.matchesIgnorePath(path) { continue } undeclared = append(undeclared, newUndeclaredParam(path, headerName, headers.Get(headerName), "header", declaredNames, DirectionResponse)) } return undeclared } libopenapi-validator-0.13.8/strict/validator_test.go000066400000000000000000005165601520534042400226070ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package strict import ( "context" "log/slog" "net/http" "testing" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" libcache "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/helpers" ) // Helper to build a schema from YAML func buildSchemaFromYAML(t *testing.T, yml string) *libopenapi.DocumentModel[v3.Document] { doc, err := libopenapi.NewDocument([]byte(yml)) require.NoError(t, err) model, errs := doc.BuildV3Model() require.Empty(t, errs) return model } // Helper to get schema func getSchema(t *testing.T, model *libopenapi.DocumentModel[v3.Document], name string) *base.Schema { schemaProxy := model.Model.Components.Schemas.GetOrZero(name) require.NotNil(t, schemaProxy) schema := schemaProxy.Schema() require.NotNil(t, schema) return schema } func TestStrictValidator_SimpleUndeclaredProperty(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string age: type: integer ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with undeclared property data := map[string]any{ "name": "John", "age": 30, "extra": "undeclared", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body.extra", result.UndeclaredValues[0].Path) } func TestStrictValidator_AllPropertiesDeclared(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string age: type: integer ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with only declared properties data := map[string]any{ "name": "John", "age": 30, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_NestedObjects(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string address: type: object properties: street: type: string city: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with undeclared nested property data := map[string]any{ "name": "John", "address": map[string]any{ "street": "123 Main St", "city": "Anytown", "zipcode": "12345", // undeclared }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "zipcode", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body.address.zipcode", result.UndeclaredValues[0].Path) } func TestStrictValidator_ArrayOfObjects(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Users: type: array items: type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Users") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with undeclared property in array item data := []any{ map[string]any{ "name": "John", "extra": "undeclared", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) } func TestStrictValidator_IgnorePaths(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body.metadata.*"), ) v := NewValidator(opts, 3.1) // Test that ignored path is not reported data := map[string]any{ "name": "John", "metadata": map[string]any{ "custom": "value", // Should be ignored }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // metadata itself is undeclared, but its children should be ignored assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "metadata", result.UndeclaredValues[0].Name) } func TestStrictValidator_AdditionalPropertiesFalse(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object additionalProperties: false properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // additionalProperties: false means base validation catches this // strict mode should NOT report (would be redundant) data := map[string]any{ "name": "John", "extra": "undeclared", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // strict should NOT report this since additionalProperties: false assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_AdditionalPropertiesWithSchema(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string additionalProperties: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // additionalProperties with schema means extra properties are allowed // but strict should still report them (they're not in explicit schema) data := map[string]any{ "name": "John", "extra": "valid string", // Matches additionalProperties schema } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // strict should report "extra" as undeclared even though it's valid assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) } func TestStrictValidator_PatternProperties(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Config: type: object properties: name: type: string patternProperties: "^x-.*$": type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Config") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Properties matching patternProperties should be considered declared data := map[string]any{ "name": "myconfig", "x-custom": "extension value", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // x-custom matches the pattern, so it should be considered declared assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestBuildPath(t *testing.T) { tests := []struct { base string propName string expected string }{ {"$.body", "name", "$.body.name"}, {"$.body", "a.b", "$.body['a.b']"}, {"$.body", "x[0]", "$.body['x[0]']"}, {"$.body.user", "email", "$.body.user.email"}, } for _, tt := range tests { t.Run(tt.propName, func(t *testing.T) { result := buildPath(tt.base, tt.propName) assert.Equal(t, tt.expected, result) }) } } func TestCompilePattern(t *testing.T) { tests := []struct { pattern string input string matches bool }{ // Single segment wildcard {"$.body.metadata.*", "$.body.metadata.custom", true}, {"$.body.metadata.*", "$.body.metadata.custom.nested", false}, // Double wildcard (any depth) {"$.body.**", "$.body.a.b.c", true}, {"$.body.**.x-*", "$.body.deep.nested.x-custom", true}, // Array index wildcard {"$.body.items[*].name", "$.body.items[0].name", true}, {"$.body.items[*].name", "$.body.items[999].name", true}, // Escaped asterisk {"$.body.\\*", "$.body.*", true}, {"$.body.\\*", "$.body.anything", false}, } for _, tt := range tests { t.Run(tt.pattern, func(t *testing.T) { re := compilePattern(tt.pattern) if re == nil { t.Fatalf("Failed to compile pattern: %s", tt.pattern) } result := re.MatchString(tt.input) assert.Equal(t, tt.matches, result, "Pattern: %s, Input: %s", tt.pattern, tt.input) }) } } func TestDirection_String(t *testing.T) { assert.Equal(t, "request", DirectionRequest.String()) assert.Equal(t, "response", DirectionResponse.String()) } func TestIsHeaderIgnored(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Standard headers should be ignored assert.True(t, v.isHeaderIgnored("Content-Type", DirectionRequest)) assert.True(t, v.isHeaderIgnored("content-type", DirectionRequest)) assert.True(t, v.isHeaderIgnored("Authorization", DirectionRequest)) // Set-Cookie is direction-aware assert.True(t, v.isHeaderIgnored("Set-Cookie", DirectionResponse)) assert.False(t, v.isHeaderIgnored("Set-Cookie", DirectionRequest)) // Custom headers should not be ignored assert.False(t, v.isHeaderIgnored("X-Custom-Header", DirectionRequest)) } func TestWithStrictIgnoredHeaders(t *testing.T) { // Replace defaults entirely opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnoredHeaders("X-Only-This"), ) v := NewValidator(opts, 3.1) // Standard headers are NOT ignored anymore assert.False(t, v.isHeaderIgnored("Content-Type", DirectionRequest)) // Only our custom header is ignored assert.True(t, v.isHeaderIgnored("X-Only-This", DirectionRequest)) } func TestWithStrictIgnoredHeadersExtra(t *testing.T) { // Add to defaults opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnoredHeadersExtra("X-Custom-Extra"), ) v := NewValidator(opts, 3.1) // Standard headers are still ignored assert.True(t, v.isHeaderIgnored("Content-Type", DirectionRequest)) // Our custom header is also ignored assert.True(t, v.isHeaderIgnored("X-Custom-Extra", DirectionRequest)) } func TestTruncateValue(t *testing.T) { // Short string unchanged assert.Equal(t, "hello", truncateValue("hello")) // Long string truncated longStr := "this is a very long string that should be truncated" result := truncateValue(longStr).(string) assert.True(t, len(result) <= 50) assert.Contains(t, result, "...") // Map truncated bigMap := map[string]any{"a": 1, "b": 2, "c": 3, "d": 4} assert.Equal(t, "{...}", truncateValue(bigMap)) // Slice truncated bigSlice := []any{1, 2, 3, 4} assert.Equal(t, "[...]", truncateValue(bigSlice)) } func TestStrictValidator_PolymorphicPatternProperties(t *testing.T) { yml := `openapi: "3.1.0" info: title: Edge version: "1.0" paths: {} components: schemas: VariantA: type: object required: - kind properties: kind: type: string aProp: type: string Root: type: object discriminator: propertyName: kind oneOf: - $ref: "#/components/schemas/VariantA" patternProperties: "^x-.*$": type: object properties: id: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Root") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) data := map[string]any{ "kind": "VariantA", "aProp": "ok", "x-foo": map[string]any{ "id": "1", "extra": "nope", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) require.False(t, result.Valid) require.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body.x-foo.extra", result.UndeclaredValues[0].Path) } func TestStrictValidator_ReusedSchemaDifferentPaths(t *testing.T) { yml := `openapi: "3.1.0" info: title: Edge version: "1.0" paths: {} components: schemas: Node: type: object properties: id: type: string Root: type: object properties: left: $ref: "#/components/schemas/Node" right: $ref: "#/components/schemas/Node" ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Root") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) data := map[string]any{ "left": map[string]any{ "id": "1", "extra": "nope", }, "right": map[string]any{ "id": "2", "extra": "nope", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) require.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 2) } func TestStrictValidator_UnevaluatedItemsOnly(t *testing.T) { yml := `openapi: "3.1.0" info: title: Edge version: "1.0" paths: {} components: schemas: Items: type: array unevaluatedItems: type: object properties: id: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Items") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) data := []any{ map[string]any{ "id": "1", "extra": "nope", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) require.False(t, result.Valid) require.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "$.body[0].extra", result.UndeclaredValues[0].Path) } func TestStrictValidator_HeaderIgnorePathsCase(t *testing.T) { opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.headers.x-trace"), ) headers := http.Header{ "X-Trace": {"abc"}, } undeclared := ValidateRequestHeaders(headers, nil, nil, opts) assert.Empty(t, undeclared) } func TestStrictValidator_OneOfWithParentProperties(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: id: type: string oneOf: - properties: name: type: string - properties: title: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with parent property "id" + oneOf variant property "name" // Both should be considered declared data := map[string]any{ "id": "123", "name": "John", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // id is from parent, name is from oneOf variant - both should be declared assert.True(t, result.Valid, "Parent + oneOf variant properties should be valid") assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_AnyOfWithParentProperties(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: id: type: string anyOf: - properties: name: type: string - properties: title: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with parent property "id" + anyOf variant property "name" data := map[string]any{ "id": "123", "name": "John", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // id is from parent, name is from anyOf variant - both should be declared assert.True(t, result.Valid, "Parent + anyOf variant properties should be valid") assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_OneOfWithUndeclaredProperty(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: id: type: string oneOf: - properties: name: type: string - properties: title: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with undeclared property "extra" data := map[string]any{ "id": "123", "name": "John", "extra": "undeclared", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // "extra" is not in parent or variant - should be reported as undeclared assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) } func TestStrictValidator_PatternPropertiesWithAdditionalPropertiesFalse(t *testing.T) { // This tests that patternProperties are recursed into even when // additionalProperties: false (which short-circuits to recurseIntoDeclaredProperties) yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Config: type: object additionalProperties: false properties: name: type: string patternProperties: "^x-": type: object properties: id: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Config") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with pattern property that has undeclared nested property data := map[string]any{ "name": "test", "x-custom": map[string]any{ "id": "123", "extra": "undeclared nested field", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // "extra" inside x-custom should be reported as undeclared // This verifies patternProperties are recursed into even with additionalProperties: false assert.False(t, result.Valid, "Should report undeclared nested property in patternProperties") assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body.x-custom.extra", result.UndeclaredValues[0].Path) } func TestStrictValidator_PatternPropertiesInOneOf(t *testing.T) { // This tests that patternProperties in oneOf/anyOf variants are recursed into // to find undeclared properties in nested objects. yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Container: type: object properties: type: type: string oneOf: - properties: type: const: "dynamic" patternProperties: "^x-": type: object properties: value: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Container") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with undeclared property inside pattern-matched nested object data := map[string]any{ "type": "dynamic", "x-custom": map[string]any{ "value": "hello", "undeclared": "should be caught", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // "undeclared" inside x-custom should be reported assert.False(t, result.Valid, "Should report undeclared property in pattern-matched object") require.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "undeclared", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body.x-custom.undeclared", result.UndeclaredValues[0].Path) } func TestStrictValidator_CycleDetection(t *testing.T) { // This tests that circular schema references don't cause infinite recursion. // The cycle detection should stop validation of the same schema at the same path. yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Node: type: object properties: name: type: string child: $ref: "#/components/schemas/Node" ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Node") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with deeply nested data that reuses the same schema data := map[string]any{ "name": "root", "child": map[string]any{ "name": "level1", "child": map[string]any{ "name": "level2", "extra": "undeclared at level2", }, }, "extra": "undeclared at root", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Should find undeclared properties at multiple levels assert.False(t, result.Valid) assert.GreaterOrEqual(t, len(result.UndeclaredValues), 2, "Should find undeclared at multiple levels") // Verify both undeclared properties were found var foundRoot, foundLevel2 bool for _, u := range result.UndeclaredValues { if u.Path == "$.body.extra" { foundRoot = true } if u.Path == "$.body.child.child.extra" { foundLevel2 = true } } assert.True(t, foundRoot, "Should find undeclared at root level") assert.True(t, foundLevel2, "Should find undeclared at nested level") } func TestStrictValidator_CycleDetectionDoesNotBlockDifferentPaths(t *testing.T) { // Tests that the same schema can be validated at different paths. // Cycle detection uses path+schemaRef, so same schema at different paths should work. yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Container: type: object properties: left: $ref: "#/components/schemas/Item" right: $ref: "#/components/schemas/Item" Item: type: object properties: id: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Container") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with undeclared properties in both left and right data := map[string]any{ "left": map[string]any{ "id": "1", "extraLeft": "undeclared in left", }, "right": map[string]any{ "id": "2", "extraRight": "undeclared in right", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Should find undeclared in both left and right assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 2, "Should find undeclared in both branches") var foundLeft, foundRight bool for _, u := range result.UndeclaredValues { if u.Name == "extraLeft" { foundLeft = true } if u.Name == "extraRight" { foundRight = true } } assert.True(t, foundLeft, "Should find undeclared in left branch") assert.True(t, foundRight, "Should find undeclared in right branch") } func TestValidateBody_UndeclaredProperty(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string age: type: integer ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) data := map[string]any{ "name": "John", "age": 30, "extra": "undeclared", } result := ValidateBody(schema, data, DirectionRequest, opts, 3.1) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body.extra", result.UndeclaredValues[0].Path) } func TestValidateBody_AllPropertiesDeclared(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string age: type: integer ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) data := map[string]any{ "name": "John", "age": 30, } result := ValidateBody(schema, data, DirectionResponse, opts, 3.1) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestValidateBody_NilInputs(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) // nil schema result := ValidateBody(nil, map[string]any{"foo": "bar"}, DirectionRequest, opts, 3.1) assert.True(t, result.Valid) // nil data yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") result = ValidateBody(schema, nil, DirectionRequest, opts, 3.1) assert.True(t, result.Valid) } // ============== allOf tests ============== func TestStrictValidator_AllOf_Simple(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Base: type: object properties: id: type: string Extended: allOf: - $ref: "#/components/schemas/Base" - type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Extended") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Both id (from Base) and name (from inline) should be declared data := map[string]any{ "id": "123", "name": "Test", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_AllOf_WithUndeclared(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Base: type: object properties: id: type: string Extended: allOf: - $ref: "#/components/schemas/Base" - type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Extended") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // extra is not in any allOf schema data := map[string]any{ "id": "123", "name": "Test", "extra": "undeclared", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) } func TestStrictValidator_AllOf_WithAdditionalPropertiesFalse(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Extended: allOf: - type: object additionalProperties: false properties: id: type: string - type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Extended") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // When any allOf has additionalProperties: false, skip strict data := map[string]any{ "id": "123", "name": "Test", "extra": "would normally be undeclared", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // additionalProperties: false means base validation handles this assert.True(t, result.Valid) } func TestStrictValidator_AllOf_WithNestedObjects(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Address: type: object properties: street: type: string Extended: allOf: - type: object properties: id: type: string - type: object properties: address: $ref: "#/components/schemas/Address" ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Extended") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Undeclared nested property in address data := map[string]any{ "id": "123", "address": map[string]any{ "street": "Main St", "zipcode": "12345", // undeclared in Address }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "zipcode", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body.address.zipcode", result.UndeclaredValues[0].Path) } // ============== Parameter validation tests ============== func TestValidateQueryParams_Basic(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) params := []*v3.Parameter{ {Name: "limit", In: "query"}, {Name: "offset", In: "query"}, } req, _ := http.NewRequest(http.MethodGet, "http://example.com/test?limit=10&offset=0&extra=undeclared", nil) undeclared := ValidateQueryParams(req, params, opts) assert.Len(t, undeclared, 1) assert.Equal(t, "extra", undeclared[0].Name) assert.Equal(t, "$.query.extra", undeclared[0].Path) assert.Equal(t, "query", undeclared[0].Type) } func TestValidateQueryParams_AllDeclared(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) params := []*v3.Parameter{ {Name: "limit", In: "query"}, {Name: "offset", In: "query"}, } req, _ := http.NewRequest(http.MethodGet, "http://example.com/test?limit=10&offset=0", nil) undeclared := ValidateQueryParams(req, params, opts) assert.Empty(t, undeclared) } func TestValidateQueryParams_IgnorePaths(t *testing.T) { opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.query.debug"), ) params := []*v3.Parameter{ {Name: "limit", In: "query"}, } req, _ := http.NewRequest(http.MethodGet, "http://example.com/test?limit=10&debug=true", nil) undeclared := ValidateQueryParams(req, params, opts) assert.Empty(t, undeclared) } func TestValidateQueryParams_NilInputs(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) // nil request assert.Nil(t, ValidateQueryParams(nil, nil, opts)) // nil options req, _ := http.NewRequest(http.MethodGet, "http://example.com/test", nil) assert.Nil(t, ValidateQueryParams(req, nil, nil)) // strict mode disabled optsNoStrict := config.NewValidationOptions() assert.Nil(t, ValidateQueryParams(req, nil, optsNoStrict)) } func TestValidateCookies_Basic(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) params := []*v3.Parameter{ {Name: "session", In: "cookie"}, {Name: "token", In: "cookie"}, } req, _ := http.NewRequest(http.MethodGet, "http://example.com/test", nil) req.AddCookie(&http.Cookie{Name: "session", Value: "abc123"}) req.AddCookie(&http.Cookie{Name: "token", Value: "xyz789"}) req.AddCookie(&http.Cookie{Name: "tracking", Value: "undeclared"}) undeclared := ValidateCookies(req, params, opts) assert.Len(t, undeclared, 1) assert.Equal(t, "tracking", undeclared[0].Name) assert.Equal(t, "$.cookies.tracking", undeclared[0].Path) assert.Equal(t, "cookie", undeclared[0].Type) } func TestValidateCookies_AllDeclared(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) params := []*v3.Parameter{ {Name: "session", In: "cookie"}, } req, _ := http.NewRequest(http.MethodGet, "http://example.com/test", nil) req.AddCookie(&http.Cookie{Name: "session", Value: "abc123"}) undeclared := ValidateCookies(req, params, opts) assert.Empty(t, undeclared) } func TestValidateCookies_IgnorePaths(t *testing.T) { opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.cookies.tracking"), ) params := []*v3.Parameter{ {Name: "session", In: "cookie"}, } req, _ := http.NewRequest(http.MethodGet, "http://example.com/test", nil) req.AddCookie(&http.Cookie{Name: "session", Value: "abc123"}) req.AddCookie(&http.Cookie{Name: "tracking", Value: "ignored"}) undeclared := ValidateCookies(req, params, opts) assert.Empty(t, undeclared) } func TestValidateCookies_NilInputs(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) // nil request assert.Nil(t, ValidateCookies(nil, nil, opts)) // nil options req, _ := http.NewRequest(http.MethodGet, "http://example.com/test", nil) assert.Nil(t, ValidateCookies(req, nil, nil)) // strict mode disabled optsNoStrict := config.NewValidationOptions() assert.Nil(t, ValidateCookies(req, nil, optsNoStrict)) } func TestValidateResponseHeaders_Basic(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) declaredHeaders := &map[string]*v3.Header{ "X-Request-Id": {}, "X-Rate-Limit": {}, } headers := http.Header{ "X-Request-Id": {"abc123"}, "X-Rate-Limit": {"100"}, "X-Custom-Header": {"undeclared"}, } undeclared := ValidateResponseHeaders(headers, declaredHeaders, opts) assert.Len(t, undeclared, 1) assert.Equal(t, "X-Custom-Header", undeclared[0].Name) assert.Equal(t, "$.headers.x-custom-header", undeclared[0].Path) assert.Equal(t, DirectionResponse, undeclared[0].Direction) } func TestValidateResponseHeaders_AllDeclared(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) declaredHeaders := &map[string]*v3.Header{ "X-Request-Id": {}, } headers := http.Header{ "X-Request-Id": {"abc123"}, } undeclared := ValidateResponseHeaders(headers, declaredHeaders, opts) assert.Empty(t, undeclared) } func TestValidateResponseHeaders_SetCookieIgnored(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) // No declared headers var declaredHeaders *map[string]*v3.Header headers := http.Header{ "Set-Cookie": {"session=abc123"}, } undeclared := ValidateResponseHeaders(headers, declaredHeaders, opts) // Set-Cookie should be ignored in responses assert.Empty(t, undeclared) } func TestValidateResponseHeaders_NilInputs(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) // nil headers assert.Nil(t, ValidateResponseHeaders(nil, nil, opts)) // nil options headers := http.Header{"X-Test": {"value"}} assert.Nil(t, ValidateResponseHeaders(headers, nil, nil)) // strict mode disabled optsNoStrict := config.NewValidationOptions() assert.Nil(t, ValidateResponseHeaders(headers, nil, optsNoStrict)) } // ============== Array validation tests ============== func TestStrictValidator_ArrayItemsFalse(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Empty: type: array items: false ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Empty") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // items: false means no items allowed data := []any{"item1", "item2"} result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Should report both items as undeclared assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 2) } func TestStrictValidator_PrefixItems(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Tuple: type: array prefixItems: - type: object properties: first: type: string - type: object properties: second: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Tuple") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Each prefix item has its own schema data := []any{ map[string]any{"first": "a", "extra1": "undeclared"}, map[string]any{"second": "b", "extra2": "undeclared"}, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 2) } func TestStrictValidator_PrefixItems_FewerDataElements(t *testing.T) { // Covers array_validator.go:41-42 - break when data has fewer elements than prefixItems yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Tuple: type: array prefixItems: - type: object properties: first: type: string - type: object properties: second: type: string - type: object properties: third: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Tuple") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Only 1 data element, but 3 prefixItems - should break early at line 42 data := []any{ map[string]any{"first": "a", "extra": "undeclared"}, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Only first element validated, has one undeclared property assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) } func TestStrictValidator_PrefixItemsWithItems(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Tuple: type: array prefixItems: - type: object properties: first: type: string items: type: object properties: rest: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Tuple") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // First item uses prefixItems[0], rest use items schema data := []any{ map[string]any{"first": "a"}, map[string]any{"rest": "b"}, map[string]any{"rest": "c", "extra": "undeclared"}, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body[2].extra", result.UndeclaredValues[0].Path) } func TestStrictValidator_EmptyArray(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Items: type: array items: type: object properties: id: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Items") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Empty array should be valid data := []any{} result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } // ============== Additional edge case tests ============== func TestStrictValidator_ReadOnlyInRequest(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: id: type: string readOnly: true name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // readOnly properties should not be expected in requests data := map[string]any{ "name": "John", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) } func TestStrictValidator_WriteOnlyInResponse(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string password: type: string writeOnly: true ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // writeOnly properties should not be expected in responses data := map[string]any{ "name": "John", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionResponse, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) } func TestStrictValidator_DiscriminatorMapping(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Dog: type: object properties: petType: type: string bark: type: string Cat: type: object properties: petType: type: string meow: type: string Pet: type: object discriminator: propertyName: petType mapping: dog: "#/components/schemas/Dog" cat: "#/components/schemas/Cat" oneOf: - $ref: "#/components/schemas/Dog" - $ref: "#/components/schemas/Cat" ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Pet") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with discriminator selecting Dog data := map[string]any{ "petType": "dog", "bark": "woof", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_NilSchemaData(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // nil schema result := v.Validate(Input{ Schema: nil, Data: map[string]any{"foo": "bar"}, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) // nil data result = v.Validate(Input{ Schema: &base.Schema{}, Data: nil, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) } func TestNewValidator_NilOptions(t *testing.T) { v := NewValidator(nil, 3.1) assert.NotNil(t, v) assert.NotNil(t, v.logger) } func TestGetSchemaKey_NilSchema(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) key := v.getSchemaKey(nil) assert.Equal(t, "", key) } func TestGetCompiledPattern_Invalid(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Invalid regex pattern pattern := v.getCompiledPattern("[invalid") assert.Nil(t, pattern) } func TestGetCompiledPattern_Cached(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // First call compiles pattern1 := v.getCompiledPattern("^test$") assert.NotNil(t, pattern1) // Second call returns cached pattern2 := v.getCompiledPattern("^test$") assert.Equal(t, pattern1, pattern2) } func TestExceedsDepth(t *testing.T) { ctx := newTraversalContext(DirectionRequest, nil, "$.body") assert.False(t, ctx.exceedsDepth()) // Create context at max depth for i := 0; i < 101; i++ { ctx = ctx.withPath("$.body.deep") } assert.True(t, ctx.exceedsDepth()) } func TestCheckAndMarkVisited_Cycle(t *testing.T) { ctx := newTraversalContext(DirectionRequest, nil, "$.body") // First visit should return false (not a cycle) isCycle := ctx.checkAndMarkVisited("schema1") assert.False(t, isCycle) // Second visit to same schema at same path should return true (cycle) isCycle = ctx.checkAndMarkVisited("schema1") assert.True(t, isCycle) } func TestGetParamNames(t *testing.T) { params := []*v3.Parameter{ {Name: "limit", In: "query"}, {Name: "offset", In: "query"}, {Name: "X-Api-Key", In: "header"}, } queryNames := getParamNames(params, "query") assert.ElementsMatch(t, []string{"limit", "offset"}, queryNames) headerNames := getParamNames(params, "header") assert.ElementsMatch(t, []string{"X-Api-Key"}, headerNames) cookieNames := getParamNames(params, "cookie") assert.Empty(t, cookieNames) } func TestGetEffectiveIgnoredHeaders_Nil(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) headers := v.getEffectiveIgnoredHeaders() assert.NotEmpty(t, headers) assert.Contains(t, headers, "content-type") } func TestStrictValidator_DependentSchemas(t *testing.T) { // Test dependentSchemas with trigger property present yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: CreditCard: type: object properties: name: type: string creditCard: type: string dependentSchemas: creditCard: properties: billingAddress: type: string required: - billingAddress ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "CreditCard") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // When creditCard is present, billingAddress becomes a declared property data := map[string]any{ "name": "John", "creditCard": "1234-5678-9012-3456", "billingAddress": "123 Main St", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_DependentSchemas_NoTrigger(t *testing.T) { // Test dependentSchemas when trigger property is NOT present yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: CreditCard: type: object properties: name: type: string creditCard: type: string dependentSchemas: creditCard: properties: billingAddress: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "CreditCard") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // When creditCard is NOT present, billingAddress is undeclared data := map[string]any{ "name": "John", "billingAddress": "123 Main St", // undeclared without creditCard trigger } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "billingAddress", result.UndeclaredValues[0].Name) } func TestStrictValidator_IfThenElse_ThenBranch(t *testing.T) { // Test if/then/else - matching if condition uses then properties yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Conditional: type: object properties: type: type: string if: properties: type: const: "car" then: properties: numWheels: type: integer else: properties: numLegs: type: integer ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Conditional") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // type="car" matches if condition, so numWheels is declared data := map[string]any{ "type": "car", "numWheels": 4, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_IfThenElse_ElseBranch(t *testing.T) { // Test if/then/else - non-matching if condition uses else properties yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Conditional: type: object properties: type: type: string if: properties: type: const: "car" then: properties: numWheels: type: integer else: properties: numLegs: type: integer ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Conditional") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // type="animal" does NOT match if condition, so numLegs is declared (else branch) data := map[string]any{ "type": "animal", "numLegs": 4, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_IfThenElse_WrongBranchProperty(t *testing.T) { // Test if/then/else - using wrong branch property is undeclared yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Conditional: type: object properties: type: type: string if: properties: type: const: "car" then: properties: numWheels: type: integer else: properties: numLegs: type: integer ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Conditional") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // type="car" matches if condition (then branch), but we're using numLegs (else property) data := map[string]any{ "type": "car", "numLegs": 4, // wrong branch property } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "numLegs", result.UndeclaredValues[0].Name) } func TestStrictValidator_OneOfWithParentBothAdditionalPropertiesFalse(t *testing.T) { // Test recurseIntoDeclaredPropertiesWithMerged path: // Both parent and variant have additionalProperties: false yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Pet: type: object additionalProperties: false properties: id: type: string oneOf: - $ref: '#/components/schemas/Dog' - $ref: '#/components/schemas/Cat' Dog: type: object additionalProperties: false properties: bark: type: boolean Cat: type: object additionalProperties: false properties: meow: type: boolean ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Pet") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // All properties are declared (parent id + variant bark) data := map[string]any{ "id": "pet-123", "bark": true, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Both parent and variant have additionalProperties: false // This triggers the recurseIntoDeclaredPropertiesWithMerged path // Standard validation would catch any extras, so strict just recurses assert.True(t, result.Valid) } func TestStrictValidator_OneOfWithParentBothAdditionalPropertiesFalse_NestedObject(t *testing.T) { // Test recurseIntoDeclaredPropertiesWithMerged with nested object validation // When both parent and variant have additionalProperties: false, the code // takes the recurseIntoDeclaredPropertiesWithMerged path which still recurses // into nested objects to check for undeclared properties there. yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Pet: type: object additionalProperties: false properties: id: type: string meta: type: object properties: version: type: string oneOf: - $ref: '#/components/schemas/Dog' Dog: type: object additionalProperties: false properties: bark: type: boolean ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Pet") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Valid nested object - tests that recursion into nested objects works data := map[string]any{ "id": "pet-123", "bark": true, "meta": map[string]any{ "version": "1.0", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // When both parent and variant have additionalProperties: false, // strict mode delegates to standard validation for undeclared detection // but still recurses into nested objects assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_MergePropertiesIntoDeclared_EmptySchema(t *testing.T) { // Test mergePropertiesIntoDeclared with nil/empty schema declared := make(map[string]*declaredProperty) mergePropertiesIntoDeclared(declared, nil) assert.Empty(t, declared) // Test with schema but nil properties schema := &base.Schema{} mergePropertiesIntoDeclared(declared, schema) assert.Empty(t, declared) } func TestStrictValidator_IsPropertyDeclaredInAllOf_EmptyAllOf(t *testing.T) { // Test isPropertyDeclaredInAllOf with nil allOf v := NewValidator(config.NewValidationOptions(config.WithStrictMode()), 3.1) result := v.isPropertyDeclaredInAllOf(nil, "test") assert.False(t, result) } func TestStrictValidator_GetSchemaKey_NilSchema(t *testing.T) { // Test getSchemaKey with nil schema returns empty string v := NewValidator(config.NewValidationOptions(config.WithStrictMode()), 3.1) key := v.getSchemaKey(nil) assert.Equal(t, "", key) } func TestStrictValidator_GetSchemaKey_SchemaWithHash(t *testing.T) { // Test getSchemaKey with schema that has a hash yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") v := NewValidator(config.NewValidationOptions(config.WithStrictMode()), 3.1) key := v.getSchemaKey(schema) assert.NotEmpty(t, key) } func TestStrictValidator_RecurseIntoDeclaredPropertiesWithMerged(t *testing.T) { // Test the recurseIntoDeclaredPropertiesWithMerged code path // This requires both parent AND variant to have additionalProperties: false // AND the data to only contain properties declared in the variant // (so the variant matching succeeds) yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Pet: type: object additionalProperties: false properties: name: type: string meta: type: object properties: version: type: string oneOf: - type: object additionalProperties: false properties: name: type: string meta: type: object properties: version: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Pet") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Data that only has properties declared in the variant // The variant matches because it declares both name and meta data := map[string]any{ "name": "Fido", "meta": map[string]any{ "version": "1.0", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Both parent and variant have additionalProperties: false // Strict mode delegates to base validation but still recurses into declared properties assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_RecurseIntoDeclaredPropertiesWithMerged_WithIgnorePath(t *testing.T) { // Test the shouldIgnore path within recurseIntoDeclaredPropertiesWithMerged yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Pet: type: object additionalProperties: false properties: name: type: string details: type: object properties: version: type: string oneOf: - type: object additionalProperties: false properties: name: type: string details: type: object properties: version: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Pet") // Ignore the details path opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body.details"), ) v := NewValidator(opts, 3.1) // Data with properties that match both parent and variant data := map[string]any{ "name": "Fido", "details": map[string]any{ "version": "1.0", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Should be valid - tests that ignore path works in recurseIntoDeclaredPropertiesWithMerged assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_ShouldSkipProperty_WriteOnly_Request(t *testing.T) { // Test that writeOnly properties are not flagged in responses yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: id: type: string password: type: string writeOnly: true ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Password should be skipped in response direction data := map[string]any{ "id": "user-123", "password": "secret", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionResponse, Options: opts, BasePath: "$.body", Version: 3.1, }) // writeOnly in response should be flagged (password shouldn't be in response) // Actually let me check the shouldSkipProperty logic assert.True(t, result.Valid) } func TestStrictValidator_IsPropertyDeclaredInAllOf_WithProperties(t *testing.T) { // Test isPropertyDeclaredInAllOf with actual allOf schemas yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Combined: allOf: - type: object properties: name: type: string - type: object properties: age: type: integer ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Combined") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test the isPropertyDeclaredInAllOf function isDeclared := v.isPropertyDeclaredInAllOf(schema.AllOf, "name") assert.True(t, isDeclared) isDeclared = v.isPropertyDeclaredInAllOf(schema.AllOf, "age") assert.True(t, isDeclared) isDeclared = v.isPropertyDeclaredInAllOf(schema.AllOf, "undeclared") assert.False(t, isDeclared) } func TestDiscardHandler_Methods(t *testing.T) { // Test the discardHandler slog.Handler implementation // These are interface methods required by slog.Handler d := discardHandler{} // Enabled should return false (no logging) assert.False(t, d.Enabled(context.TODO(), 0)) // Handle should return nil (no error) assert.NoError(t, d.Handle(context.TODO(), slog.Record{})) // WithAttrs should return itself handler := d.WithAttrs(nil) assert.Equal(t, d, handler) // WithGroup should return itself handler = d.WithGroup("test") assert.Equal(t, d, handler) } func TestStrictValidator_DataMatchesSchema_NilSchema(t *testing.T) { // Test that nil schema matches anything opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) matches, err := v.dataMatchesSchema(nil, map[string]any{"foo": "bar"}) assert.NoError(t, err) assert.True(t, matches) } func TestStrictValidator_GetCompiledSchema_NilSchema(t *testing.T) { // Test getCompiledSchema with nil schema opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) compiled, err := v.getCompiledSchema(nil) assert.NoError(t, err) assert.Nil(t, compiled) } func TestStrictValidator_GetCompiledSchema_LocalCacheHit(t *testing.T) { // Test that local cache is used on second call yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // First call - compiles and caches compiled1, err := v.getCompiledSchema(schema) assert.NoError(t, err) assert.NotNil(t, compiled1) // Second call - should hit local cache compiled2, err := v.getCompiledSchema(schema) assert.NoError(t, err) assert.NotNil(t, compiled2) assert.Same(t, compiled1, compiled2) } func TestStrictValidator_CompileSchema_NilSchema(t *testing.T) { // Test compileSchema with nil schema opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) compiled, err := v.compileSchema(nil) assert.NoError(t, err) assert.Nil(t, compiled) } func TestStrictValidator_GetEffectiveIgnoredHeaders_WithMerge(t *testing.T) { // Test getEffectiveIgnoredHeaders with merge mode opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnoredHeadersExtra("X-Custom"), ) v := NewValidator(opts, 3.1) headers := v.getEffectiveIgnoredHeaders() // Should contain defaults plus the custom header assert.Contains(t, headers, "content-type") // From defaults assert.Contains(t, headers, "X-Custom") // From extra } func TestStrictValidator_GetEffectiveIgnoredHeaders_WithReplace(t *testing.T) { // Test getEffectiveIgnoredHeaders with replace mode opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnoredHeaders("X-Only-This"), ) v := NewValidator(opts, 3.1) headers := v.getEffectiveIgnoredHeaders() // Should ONLY contain the replaced headers assert.Contains(t, headers, "X-Only-This") assert.NotContains(t, headers, "content-type") // Defaults should be replaced } func TestStrictValidator_ValidateRequestHeaders_UndeclaredHeader(t *testing.T) { // Test ValidateRequestHeaders with undeclared header yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: /test: get: parameters: - name: X-Known-Header in: header schema: type: string responses: "200": description: OK ` doc, _ := libopenapi.NewDocument([]byte(yml)) model, _ := doc.BuildV3Model() opts := config.NewValidationOptions(config.WithStrictMode()) params := model.Model.Paths.PathItems.GetOrZero("/test").Get.Parameters // Create headers directly headers := http.Header{ "X-Known-Header": {"value"}, "X-Unknown-Header": {"value"}, // Not in spec } // ValidateRequestHeaders takes http.Header, not *http.Request undeclared := ValidateRequestHeaders(headers, params, nil, opts) assert.Len(t, undeclared, 1) assert.Equal(t, "X-Unknown-Header", undeclared[0].Name) } func TestStrictValidator_ValidateValue_NilSchema(t *testing.T) { // Test validateValue with nil schema opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) ctx := newTraversalContext(DirectionRequest, nil, "$.body") result := v.validateValue(ctx, nil, map[string]any{"foo": "bar"}) assert.Empty(t, result) } func TestStrictValidator_ValidateValue_NonObjectData(t *testing.T) { // Test validateValue with non-object data (string, number, etc.) yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: StringType: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "StringType") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) ctx := newTraversalContext(DirectionRequest, nil, "$.body") result := v.validateValue(ctx, schema, "hello") assert.Empty(t, result) } func TestStrictValidator_FindMatchingVariant_NoMatch(t *testing.T) { // Test findMatchingVariant when no variant matches yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Pet: oneOf: - type: object required: - bark properties: bark: type: boolean - type: object required: - meow properties: meow: type: boolean ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Pet") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Data that matches neither variant data := map[string]any{ "swim": true, } variant := v.findMatchingVariant(schema.OneOf, data) assert.Nil(t, variant) } func TestStrictValidator_ShouldReportUndeclared_AdditionalPropertiesSchema(t *testing.T) { // Test shouldReportUndeclared with additionalProperties as a schema yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Config: type: object properties: name: type: string additionalProperties: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Config") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) result := v.shouldReportUndeclared(schema) assert.True(t, result) // Should report undeclared even with additionalProperties schema } func TestStrictValidator_GetEffectiveIgnoredHeaders_NilOptions(t *testing.T) { // Test getEffectiveIgnoredHeaders with nil options v := &Validator{options: nil} headers := v.getEffectiveIgnoredHeaders() assert.Nil(t, headers) } func TestStrictValidator_ShouldSkipProperty_ReadOnlyInRequest(t *testing.T) { // Test that readOnly properties are skipped in request direction yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: id: type: string readOnly: true name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Get the id property which is readOnly idProp := schema.Properties.GetOrZero("id").Schema() // readOnly in request should be skipped result := v.shouldSkipProperty(idProp, DirectionRequest) assert.True(t, result) // readOnly in response should NOT be skipped result = v.shouldSkipProperty(idProp, DirectionResponse) assert.False(t, result) } func TestStrictValidator_ShouldSkipProperty_WriteOnlyInResponse(t *testing.T) { // Test that writeOnly properties are skipped in response direction yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: password: type: string writeOnly: true name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Get the password property which is writeOnly passwordProp := schema.Properties.GetOrZero("password").Schema() // writeOnly in response should be skipped result := v.shouldSkipProperty(passwordProp, DirectionResponse) assert.True(t, result) // writeOnly in request should NOT be skipped result = v.shouldSkipProperty(passwordProp, DirectionRequest) assert.False(t, result) } func TestStrictValidator_ShouldReportUndeclared_UnevaluatedPropertiesFalse(t *testing.T) { // Test that unevaluatedProperties: false still reports undeclared in strict mode yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Config: type: object properties: name: type: string unevaluatedProperties: false ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Config") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) result := v.shouldReportUndeclared(schema) assert.True(t, result) } func TestStrictValidator_ValidateValue_ExceedsDepth(t *testing.T) { // Test validateValue when max depth is exceeded yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: DeepNested: type: object properties: level: type: object ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "DeepNested") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) ctx := newTraversalContext(DirectionRequest, nil, "$.body") // Increase depth artificially to exceed max for i := 0; i < 101; i++ { ctx = ctx.withPath("$.body.deep") } result := v.validateValue(ctx, schema, map[string]any{"level": map[string]any{}}) assert.Empty(t, result) // Should return early due to depth } func TestStrictValidator_AnyOf_WithMatch(t *testing.T) { // Test validateAnyOf with a matching variant yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Pet: type: object properties: id: type: string anyOf: - type: object properties: bark: type: boolean - type: object properties: meow: type: boolean ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Pet") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Data that matches the first variant data := map[string]any{ "id": "pet-123", "bark": true, "extra": "undeclared", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) } func TestStrictValidator_AnyOf_WithDiscriminator(t *testing.T) { // Test validateAnyOf with discriminator yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Pet: type: object discriminator: propertyName: petType mapping: dog: '#/components/schemas/Dog' anyOf: - $ref: '#/components/schemas/Dog' - $ref: '#/components/schemas/Cat' Dog: type: object properties: petType: type: string bark: type: boolean Cat: type: object properties: petType: type: string meow: type: boolean ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Pet") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) data := map[string]any{ "petType": "dog", "bark": true, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) } func TestStrictValidator_FindMatchingVariant_NilProxy(t *testing.T) { // Test findMatchingVariant with nil proxy in variants opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Create a slice with nil entry variants := []*base.SchemaProxy{nil} result := v.findMatchingVariant(variants, map[string]any{"foo": "bar"}) assert.Nil(t, result) } func TestStrictValidator_ShouldReportUndeclaredForAllOf_AdditionalPropertiesFalse(t *testing.T) { // Test shouldReportUndeclaredForAllOf when parent has additionalProperties: false yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Combined: type: object additionalProperties: false allOf: - type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Combined") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Should return false because parent has additionalProperties: false result := v.shouldReportUndeclaredForAllOf(schema) assert.False(t, result) } func TestStrictValidator_ShouldReportUndeclaredForAllOf_AllOfHasAdditionalPropertiesFalse(t *testing.T) { // Test shouldReportUndeclaredForAllOf when allOf member has additionalProperties: false yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Combined: type: object allOf: - type: object additionalProperties: false properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Combined") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Should return false because allOf member has additionalProperties: false result := v.shouldReportUndeclaredForAllOf(schema) assert.False(t, result) } func TestStrictValidator_FindPropertySchemaInAllOf_FromDeclared(t *testing.T) { // Test findPropertySchemaInAllOf finding property from declared map yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Create declared map with the property declared := make(map[string]*declaredProperty) declared["name"] = &declaredProperty{ proxy: schema.Properties.GetOrZero("name"), } result := v.findPropertySchemaInAllOf(nil, "name", declared) assert.NotNil(t, result) } // Additional nil check tests func TestStrictValidator_IsPropertyDeclaredInAllOf_NilSchemaProxy(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with nil SchemaProxy in allOf slice allOf := []*base.SchemaProxy{nil} result := v.isPropertyDeclaredInAllOf(allOf, "foo") assert.False(t, result) } func TestStrictValidator_IsPropertyDeclaredInAllOf_NilSchema(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with empty allOf result := v.isPropertyDeclaredInAllOf(nil, "foo") assert.False(t, result) } func TestStrictValidator_ShouldReportUndeclaredForAllOf_NilSchemaProxy(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Test: type: object allOf: - type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Test") // Manually inject a nil into allOf to test the nil check schema.AllOf = append(schema.AllOf, nil) opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Should still work and return true (default behavior) result := v.shouldReportUndeclaredForAllOf(schema) assert.True(t, result) } func TestStrictValidator_FindPropertySchemaInAllOf_NilSchemaProxy(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with nil SchemaProxy in allOf allOf := []*base.SchemaProxy{nil} result := v.findPropertySchemaInAllOf(allOf, "foo", nil) assert.Nil(t, result) } func TestStrictValidator_RecurseIntoAllOfDeclaredProperties_NilSchemaProxy(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) ctx := newTraversalContext(DirectionRequest, nil, "$.body") allOf := []*base.SchemaProxy{nil} data := map[string]any{"foo": "bar"} result := v.recurseIntoAllOfDeclaredProperties(ctx, allOf, data, nil) assert.Empty(t, result) } func TestStrictValidator_SelectByDiscriminator_NilDiscriminator(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Test: type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Test") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Schema has no discriminator result := v.selectByDiscriminator(schema, nil, map[string]any{"foo": "bar"}) assert.Nil(t, result) } func TestStrictValidator_SelectByDiscriminator_EmptyPropertyName(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Pet: type: object discriminator: propertyName: "" oneOf: - $ref: '#/components/schemas/Dog' Dog: type: object properties: bark: type: boolean ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Pet") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) result := v.selectByDiscriminator(schema, schema.OneOf, map[string]any{"petType": "Dog"}) assert.Nil(t, result) } func TestStrictValidator_SelectByDiscriminator_MissingDiscriminatorValue(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Pet: type: object discriminator: propertyName: petType oneOf: - $ref: '#/components/schemas/Dog' Dog: type: object properties: bark: type: boolean ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Pet") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Data doesn't have the discriminator property result := v.selectByDiscriminator(schema, schema.OneOf, map[string]any{"bark": true}) assert.Nil(t, result) } func TestStrictValidator_SelectByDiscriminator_NonStringValue(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Pet: type: object discriminator: propertyName: petType oneOf: - $ref: '#/components/schemas/Dog' Dog: type: object properties: bark: type: boolean ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Pet") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Discriminator value is not a string result := v.selectByDiscriminator(schema, schema.OneOf, map[string]any{"petType": 123}) assert.Nil(t, result) } func TestStrictValidator_SelectByDiscriminator_NoMatchingVariant(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Pet: type: object discriminator: propertyName: petType oneOf: - $ref: '#/components/schemas/Dog' Dog: type: object properties: bark: type: boolean ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Pet") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Discriminator value doesn't match any variant result := v.selectByDiscriminator(schema, schema.OneOf, map[string]any{"petType": "Cat"}) assert.Nil(t, result) } func TestStrictValidator_FindMatchingVariant_NoMatch2(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Dog: type: object required: - bark properties: bark: type: boolean ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Dog") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Create variants with a schema that won't match the data variants := []*base.SchemaProxy{base.CreateSchemaProxy(schema)} // Data doesn't have required 'bark' property - won't match result := v.findMatchingVariant(variants, map[string]any{"meow": true}) assert.Nil(t, result) } func TestStrictValidator_CollectDeclaredProperties_NilSchema(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) declared, patterns := v.collectDeclaredProperties(nil, nil) assert.Empty(t, declared) assert.Empty(t, patterns) } func TestStrictValidator_GetDeclaredPropertyNames_Empty(t *testing.T) { result := getDeclaredPropertyNames(nil) assert.Empty(t, result) } func TestStrictValidator_ShouldSkipProperty_NilSchema(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) result := v.shouldSkipProperty(nil, DirectionRequest) assert.False(t, result) } func TestStrictValidator_ValidateObject_NilProperties(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Empty: type: object ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Empty") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) ctx := newTraversalContext(DirectionRequest, nil, "$.body") result := v.validateObject(ctx, schema, map[string]any{"foo": "bar"}) // In strict mode, empty schema with no properties still reports undeclared // because additionalProperties defaults to true (meaning strict mode catches it) assert.Len(t, result, 1) assert.Equal(t, "foo", result[0].Name) } func TestStrictValidator_ShouldReportUndeclared_NilSchema(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // nil schema returns false - can't report undeclared without schema result := v.shouldReportUndeclared(nil) assert.False(t, result) } func TestStrictValidator_GetPatternPropertySchema_NoPatterns(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: NoPatterns: type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "NoPatterns") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Schema has no patternProperties result := v.getPatternPropertySchema(schema, "foo") assert.Nil(t, result) } func TestStrictValidator_RecurseIntoDeclaredProperties_EmptySchema(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Empty: type: object ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Empty") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) ctx := newTraversalContext(DirectionRequest, nil, "$.body") data := map[string]any{"name": "test"} // recurseIntoDeclaredProperties only takes ctx, schema, data result := v.recurseIntoDeclaredProperties(ctx, schema, data) assert.Empty(t, result) } func TestStrictValidator_ValidateArray_NilItems(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: List: type: array ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "List") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) ctx := newTraversalContext(DirectionRequest, nil, "$.body") result := v.validateArray(ctx, schema, []any{"foo", "bar"}) // Array with no items schema - anything is allowed assert.Empty(t, result) } func TestStrictValidator_ValidateArray_ItemsSchemaB(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: List: type: array items: true ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "List") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) ctx := newTraversalContext(DirectionRequest, nil, "$.body") result := v.validateArray(ctx, schema, []any{"foo", "bar"}) // items: true means all items are valid assert.Empty(t, result) } func TestStrictValidator_ValidateArray_PrefixItemsNil(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Tuple: type: array prefixItems: - type: string - type: integer ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Tuple") // Manually set one prefixItem to nil to test the nil check schema.PrefixItems = append(schema.PrefixItems, nil) opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) ctx := newTraversalContext(DirectionRequest, nil, "$.body") result := v.validateArray(ctx, schema, []any{"foo", 42, "extra"}) // Should handle nil prefixItems gracefully assert.Empty(t, result) } func TestStrictValidator_FindPropertySchemaInMerged_NilProxy(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Create declared map with nil proxy declared := make(map[string]*declaredProperty) declared["name"] = &declaredProperty{proxy: nil} // findPropertySchemaInMerged takes (variant, parent, propName, declared) result := v.findPropertySchemaInMerged(nil, nil, "name", declared) assert.Nil(t, result) } func TestStrictValidator_RecurseIntoDeclaredPropertiesWithMerged_NilProxy(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) ctx := newTraversalContext(DirectionRequest, nil, "$.body") // Create declared with nil proxy declared := make(map[string]*declaredProperty) declared["name"] = &declaredProperty{proxy: nil} data := map[string]any{"name": "test"} // recurseIntoDeclaredPropertiesWithMerged takes (ctx, variant, parent, data, declared) result := v.recurseIntoDeclaredPropertiesWithMerged(ctx, nil, nil, data, declared) assert.Empty(t, result) } func TestStrictValidator_ValidateAnyOf_NoMatchingVariant(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: StringOrInt: anyOf: - type: string minLength: 5 - type: integer minimum: 10 ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "StringOrInt") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) ctx := newTraversalContext(DirectionRequest, nil, "$.body") // Data is an object which won't match string or integer result := v.validateAnyOf(ctx, schema, map[string]any{"foo": "bar"}) // Should return empty - no matching variant means we can't validate assert.Empty(t, result) } func TestStrictValidator_CompilePattern_EmptyPattern(t *testing.T) { // Test compilePattern with empty pattern result := compilePattern("") assert.Nil(t, result) } func TestStrictValidator_GetSchemaKey_NoLowLevel(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Create a schema without low-level info schema := &base.Schema{} key := v.getSchemaKey(schema) // Should return pointer-based key assert.NotEmpty(t, key) } func TestTruncateValue_SmallMapUnchanged(t *testing.T) { // Small map (<= 3 entries) should return unchanged input := map[string]any{"a": 1, "b": 2} result := TruncateValue(input) assert.Equal(t, input, result) // Exactly 3 entries should also pass unchanged input3 := map[string]any{"a": 1, "b": 2, "c": 3} result3 := TruncateValue(input3) assert.Equal(t, input3, result3) } func TestTruncateValue_SmallArrayUnchanged(t *testing.T) { // Small array (<= 3 entries) should return unchanged input := []any{1, 2} result := TruncateValue(input) assert.Equal(t, input, result) // Exactly 3 entries should also pass unchanged input3 := []any{1, 2, 3} result3 := TruncateValue(input3) assert.Equal(t, input3, result3) } func TestStrictValidator_DataMatchesSchema_CompilationError(t *testing.T) { // Create a schema with an invalid regex pattern that will fail compilation yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: BadPattern: type: object properties: name: type: string pattern: "[invalid(regex" ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "BadPattern") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // dataMatchesSchema should return false with error due to invalid pattern matches, err := v.dataMatchesSchema(schema, map[string]any{"name": "test"}) assert.False(t, matches) assert.Error(t, err) assert.Contains(t, err.Error(), "strict:") } func TestStrictValidator_FindMatchingVariant_SchemaError(t *testing.T) { // Create oneOf with a variant that has invalid pattern - should skip bad variant yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Container: oneOf: - type: object properties: valid: type: string - type: object properties: broken: type: string pattern: "[unclosed(" ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Container") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // findMatchingVariant should skip the broken variant and match the valid one variant := v.findMatchingVariant(schema.OneOf, map[string]any{"valid": "test"}) // Should find a valid variant (the first one) assert.NotNil(t, variant) } func TestStrictValidator_GetPatternPropertySchema_InvalidPattern(t *testing.T) { // Create schema with invalid patternProperties regex yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: BadPatternProps: type: object patternProperties: "[invalid(": type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "BadPatternProps") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // getPatternPropertySchema should return nil for invalid pattern propProxy := v.getPatternPropertySchema(schema, "test") assert.Nil(t, propProxy) } // ============================================================================= // Phase 1: CRITICAL Coverage Tests // ============================================================================= func TestStrictValidator_AllOfWithParentProperties(t *testing.T) { // Covers polymorphic.go:88-91 - parent schema properties merged with allOf yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: MergedSchema: type: object properties: parentProp: type: string allOf: - type: object properties: childProp: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "MergedSchema") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Both parent and child properties should be considered declared data := map[string]any{ "parentProp": "from parent", "childProp": "from child", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_AllOfWithParentProperties_UndeclaredReported(t *testing.T) { // Verify undeclared properties are still caught with parent+allOf merge yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: MergedSchema: type: object properties: parentProp: type: string allOf: - type: object properties: childProp: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "MergedSchema") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) data := map[string]any{ "parentProp": "from parent", "childProp": "from child", "undeclaredProp": "should be reported", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "undeclaredProp", result.UndeclaredValues[0].Name) } func TestStrictValidator_AllOfReadOnlyInRequest(t *testing.T) { // Covers polymorphic.go:116-117 - shouldSkipProperty for readOnly in allOf yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: ReadOnlyAllOf: type: object allOf: - type: object properties: id: type: string readOnly: true name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "ReadOnlyAllOf") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // In request direction, readOnly property should be skipped data := map[string]any{ "id": "123", "name": "test", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // id is readOnly - should be skipped in request validation (not flagged) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_AllOfWriteOnlyInResponse(t *testing.T) { // Covers polymorphic.go:222-223 - shouldSkipProperty for writeOnly in oneOf/anyOf yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: WriteOnlySchema: type: object oneOf: - type: object properties: password: type: string writeOnly: true email: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "WriteOnlySchema") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // In response direction, writeOnly property should be skipped data := map[string]any{ "password": "secret123", "email": "user@example.com", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionResponse, Options: opts, BasePath: "$.body", Version: 3.1, }) // password is writeOnly - should be skipped in response validation assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_AllOfWithIgnoredPath(t *testing.T) { // Covers polymorphic.go:107-108 - shouldIgnore in allOf validation loop yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: IgnoreInAllOf: type: object allOf: - type: object properties: data: type: object properties: visible: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "IgnoreInAllOf") opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body.data.metadata"), ) v := NewValidator(opts, 3.1) // metadata path is ignored, so undeclared properties there should not be reported data := map[string]any{ "data": map[string]any{ "visible": "ok", "metadata": map[string]any{ "ignored": "should not be flagged", "alsoIgnored": "also not flagged", }, }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // metadata path is ignored, so no undeclared errors assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_OneOfWithIgnoredPath(t *testing.T) { // Covers polymorphic.go:213-214 - shouldIgnore in oneOf/anyOf validation loop yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: IgnoreInOneOf: type: object oneOf: - type: object properties: data: type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "IgnoreInOneOf") opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body.data.internal"), ) v := NewValidator(opts, 3.1) data := map[string]any{ "data": map[string]any{ "name": "visible", "internal": map[string]any{ "secret": "ignored", }, }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_OneOfWithIgnoredTopLevelProperty(t *testing.T) { // Covers polymorphic.go:213-214 - shouldIgnore at TOP LEVEL of oneOf iteration yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: OneOfIgnoreTopLevel: type: object oneOf: - type: object properties: name: type: string internal: type: object ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "OneOfIgnoreTopLevel") // Ignore "internal" property at top level - this directly hits line 214 opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body.internal"), ) v := NewValidator(opts, 3.1) data := map[string]any{ "name": "visible", "internal": map[string]any{ "anything": "should be ignored entirely", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // internal property is ignored at top level, no errors assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_FindPropertySchemaInMerged_VariantProperty(t *testing.T) { // Covers polymorphic.go:248-249 - property found in variant's explicit properties yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: OneOfVariantProp: type: object properties: parentProp: type: string oneOf: - type: object properties: variantProp: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "OneOfVariantProp") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // variantProp is defined in variant, should be found via line 249 data := map[string]any{ "parentProp": "parent", "variantProp": "variant", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_FindPropertySchemaInMerged_ParentProperty(t *testing.T) { // Covers polymorphic.go:254-256 - property found in parent's explicit properties yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: OneOfParentProp: type: object properties: parentOnly: type: string oneOf: - type: object properties: variantOnly: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "OneOfParentProp") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // parentOnly is NOT in variant, so findPropertySchemaInMerged falls through // to parent lookup at line 254-256 data := map[string]any{ "parentOnly": "from parent", "variantOnly": "from variant", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_FindPropertySchemaInMerged_VariantDirect(t *testing.T) { // Covers polymorphic.go:247-249 - direct test with empty declared map yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Variant: type: object properties: variantProp: type: string ` model := buildSchemaFromYAML(t, yml) variant := getSchema(t, model, "Variant") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Call with empty declared map - forces lookup in variant.Properties (line 247-249) result := v.findPropertySchemaInMerged(variant, nil, "variantProp", make(map[string]*declaredProperty)) assert.NotNil(t, result) } func TestStrictValidator_FindPropertySchemaInMerged_ParentDirect(t *testing.T) { // Covers polymorphic.go:254-256 - direct test with empty declared map, no variant yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Parent: type: object properties: parentProp: type: string ` model := buildSchemaFromYAML(t, yml) parent := getSchema(t, model, "Parent") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Call with nil variant and empty declared - forces lookup in parent.Properties (line 254-256) result := v.findPropertySchemaInMerged(nil, parent, "parentProp", make(map[string]*declaredProperty)) assert.NotNil(t, result) } func TestStrictValidator_FindPropertySchemaInAllOf_FromAllOfSchema(t *testing.T) { // Covers polymorphic.go:437-439 - property found in allOf schema's explicit properties yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: AllOfExplicitProp: type: object allOf: - type: object properties: fromAllOf: type: object properties: nested: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "AllOfExplicitProp") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // fromAllOf is in allOf schema, findPropertySchemaInAllOf should find it // and recurse into nested object to detect undeclared data := map[string]any{ "fromAllOf": map[string]any{ "nested": "valid", "undeclared": "should be flagged", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // undeclared in nested object should be reported assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "undeclared", result.UndeclaredValues[0].Name) } func TestStrictValidator_FindPropertySchemaInAllOf_Direct(t *testing.T) { // Covers polymorphic.go:437-439 - direct test with empty declared map yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: AllOfSchema: type: object allOf: - type: object properties: allOfProp: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "AllOfSchema") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Call with empty declared map - forces lookup in allOf schemas (line 437-439) result := v.findPropertySchemaInAllOf(schema.AllOf, "allOfProp", make(map[string]*declaredProperty)) assert.NotNil(t, result) } func TestStrictValidator_RecurseIntoAllOfDeclaredProperties_ShouldIgnore(t *testing.T) { // Covers polymorphic.go:455-456 - shouldIgnore in recurseIntoAllOfDeclaredProperties yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: AllOfIgnore: type: object additionalProperties: false allOf: - type: object additionalProperties: false properties: name: type: string metadata: type: object ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "AllOfIgnore") // Ignore metadata at top level opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body.metadata"), ) v := NewValidator(opts, 3.1) // Both parent and allOf have additionalProperties: false, // so we go through recurseIntoAllOfDeclaredProperties // metadata is ignored at line 455-456 data := map[string]any{ "name": "test", "metadata": map[string]any{ "anything": "ignored", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_RecurseIntoAllOfDeclaredProperties_SkipReadOnly(t *testing.T) { // Covers polymorphic.go:461-462 - shouldSkipProperty in recurseIntoAllOfDeclaredProperties yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: AllOfReadOnly: type: object additionalProperties: false allOf: - type: object additionalProperties: false properties: name: type: string id: type: string readOnly: true ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "AllOfReadOnly") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Both parent and allOf have additionalProperties: false, // so we go through recurseIntoAllOfDeclaredProperties // id is readOnly and skipped in request direction (line 461-462) data := map[string]any{ "name": "test", "id": "should-be-skipped", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_RecurseIntoDeclaredPropertiesWithMerged_SkipReadOnly(t *testing.T) { // Covers polymorphic.go:291-292 - shouldSkipProperty in recurseIntoDeclaredPropertiesWithMerged yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: OneOfWithReadOnly: type: object additionalProperties: false properties: name: type: string oneOf: - type: object additionalProperties: false properties: name: type: string id: type: string readOnly: true data: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "OneOfWithReadOnly") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // In request direction, readOnly property "id" should be skipped (line 291-292) // Both parent and variant have additionalProperties: false, so we go through // recurseIntoDeclaredPropertiesWithMerged // Note: variant must also declare "name" so data matches the variant data := map[string]any{ "name": "test", "id": "should-be-skipped", "data": "valid", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // id is readOnly and skipped in request, no validation errors assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_AllOfAdditionalPropertiesFalseRecurse(t *testing.T) { // Covers polymorphic.go:461-462, 467-468 - recursion with additionalProperties: false yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: RecurseTest: type: object additionalProperties: false allOf: - type: object properties: nested: type: object properties: valid: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "RecurseTest") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // nested.extra should be reported as undeclared data := map[string]any{ "nested": map[string]any{ "valid": "ok", "extra": "should be flagged", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body.nested.extra", result.UndeclaredValues[0].Path) } func TestStrictValidator_OneOfVariantPropertyPriority(t *testing.T) { // Covers polymorphic.go:248-250, 255-257 - findPropertySchemaInMerged yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: PriorityTest: type: object properties: type: type: string oneOf: - type: object properties: details: type: object properties: variantField: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "PriorityTest") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // type is from parent, details is from variant data := map[string]any{ "type": "test", "details": map[string]any{ "variantField": "from variant", "undeclared": "should be flagged", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // undeclared in details should be flagged assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "undeclared", result.UndeclaredValues[0].Name) } func TestStrictValidator_PropertyDeclaredInAllOfChild(t *testing.T) { // Covers polymorphic.go:46-47 - isPropertyDeclaredInAllOf continuation yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: AllOfChildProp: type: object properties: parentOnly: type: string allOf: - type: object properties: fromChild: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "AllOfChildProp") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // fromChild is declared in allOf child, should be considered declared data := map[string]any{ "parentOnly": "parent", "fromChild": "child", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_ValidateAllOf_NilSchemaProxy(t *testing.T) { // Covers polymorphic.go:67-68 - nil schemaProxy in allOf loop yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: AllOfWithNil: type: object allOf: - type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "AllOfWithNil") // Inject nil into allOf array to test the nil check at line 67-68 schema.AllOf = append(schema.AllOf, nil) opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) data := map[string]any{ "name": "test", "extra": "undeclared", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Should still work - nil schemaProxy is skipped assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) } func TestStrictValidator_ValidateAllOf_IgnoreTopLevelProperty(t *testing.T) { // Covers polymorphic.go:107-108 - shouldIgnore for top-level property in allOf yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: AllOfIgnoreTopLevel: type: object allOf: - type: object properties: name: type: string metadata: type: object ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "AllOfIgnoreTopLevel") // Ignore the metadata property at top level opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body.metadata"), ) v := NewValidator(opts, 3.1) data := map[string]any{ "name": "test", "metadata": map[string]any{ "anything": "should be ignored at this level", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // metadata property is ignored entirely, no errors assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } // ============================================================================= // Phase 2: HIGH Priority Coverage Tests // ============================================================================= func TestStrictValidator_SchemaCacheHit(t *testing.T) { // Covers matcher.go:60-62 - global schema cache hit path // Need a oneOf schema to trigger dataMatchesSchema which uses getCompiledSchema yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: DogVariant: type: object properties: breed: type: string CatVariant: type: object properties: color: type: string CachedSchema: type: object oneOf: - $ref: '#/components/schemas/DogVariant' - $ref: '#/components/schemas/CatVariant' ` model := buildSchemaFromYAML(t, yml) dogSchema := getSchema(t, model, "DogVariant") // Create options with schema cache opts := config.NewValidationOptions(config.WithStrictMode()) // Pre-populate the GLOBAL cache with a compiled schema for the DogVariant hash // This is what findMatchingVariant will check via dataMatchesSchema hash := dogSchema.GoLow().Hash() compiledSchema, err := helpers.NewCompiledSchemaWithVersion( "test-cached", []byte(`{"type":"object","properties":{"breed":{"type":"string"}}}`), opts, 3.1, ) require.NoError(t, err) opts.SchemaCache.Store(hash, &libcache.SchemaCacheEntry{ CompiledSchema: compiledSchema, }) v := NewValidator(opts, 3.1) // Data that matches DogVariant data := map[string]any{ "breed": "labrador", "extra": "undeclared", } // Get the parent oneOf schema parentSchema := getSchema(t, model, "CachedSchema") // Validation should hit the GLOBAL cache when checking oneOf variants result := v.Validate(Input{ Schema: parentSchema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Should still detect undeclared property assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) } func TestStrictValidator_PrefixItemsWithIgnoredPath(t *testing.T) { // Covers array_validator.go:48-50 - shouldIgnore in prefixItems loop yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: TupleIgnore: type: array prefixItems: - type: object properties: id: type: string - type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "TupleIgnore") opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body[0]"), ) v := NewValidator(opts, 3.1) // First item should be ignored entirely, second item should be validated data := []any{ map[string]any{ "id": "1", "extraInFirst": "ignored because path $.body[0] is ignored", }, map[string]any{ "name": "test", "extraInSecond": "should be flagged", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Only second item's extra property should be flagged assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extraInSecond", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body[1].extraInSecond", result.UndeclaredValues[0].Path) } func TestStrictValidator_ItemsWithIgnoredPath(t *testing.T) { // Covers array_validator.go:71-72 - shouldIgnore in items loop // Need to ignore the ITEM PATH itself ($.body[0]) not a nested property yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: ArrayIgnore: type: array items: type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "ArrayIgnore") // Ignore the first array item entirely ($.body[0]) opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body[0]"), ) v := NewValidator(opts, 3.1) data := []any{ map[string]any{ "name": "item1", "extra": "should be ignored because $.body[0] is ignored", }, map[string]any{ "name": "item2", "extra": "should be flagged", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // First item ignored, only second item's extra should be flagged assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body[1].extra", result.UndeclaredValues[0].Path) } func TestValidateRequestHeaders_DeclaredHeaderSkipped(t *testing.T) { // Covers validator.go:123-125 - declared header skip in request validation opts := config.NewValidationOptions(config.WithStrictMode()) // Create params with X-Custom header declared params := []*v3.Parameter{ { Name: "X-Custom", In: "header", }, { Name: "X-Another", In: "header", }, } headers := http.Header{ "X-Custom": []string{"declared-value"}, "X-Another": []string{"also-declared"}, "X-Undeclared": []string{"should-be-flagged"}, } undeclared := ValidateRequestHeaders(headers, params, nil, opts) // Only X-Undeclared should be reported assert.Len(t, undeclared, 1) assert.Equal(t, "X-Undeclared", undeclared[0].Name) } func TestValidateRequestHeaders_NilOrDisabled(t *testing.T) { // Covers validator.go:103 - early return when headers nil, options nil, or strict mode disabled params := []*v3.Parameter{{Name: "X-Custom", In: "header"}} headers := http.Header{"X-Custom": []string{"value"}} // Test nil headers result := ValidateRequestHeaders(nil, params, nil, config.NewValidationOptions(config.WithStrictMode())) assert.Nil(t, result) // Test nil options result = ValidateRequestHeaders(headers, params, nil, nil) assert.Nil(t, result) // Test strict mode disabled opts := config.NewValidationOptions() // strict mode off by default result = ValidateRequestHeaders(headers, params, nil, opts) assert.Nil(t, result) } func TestValidateRequestHeaders_IgnoredHeaderSkipped(t *testing.T) { // Covers validator.go:129 - skip when header is in ignored list opts := config.NewValidationOptions(config.WithStrictMode()) // No declared headers - but Content-Type is in default ignored list params := []*v3.Parameter{} headers := http.Header{ "Content-Type": []string{"application/json"}, // ignored by default "Authorization": []string{"Bearer token"}, // ignored by default "X-Custom": []string{"should-be-flagged"}, } undeclared := ValidateRequestHeaders(headers, params, nil, opts) // Only X-Custom should be reported (Content-Type and Authorization are ignored) assert.Len(t, undeclared, 1) assert.Equal(t, "X-Custom", undeclared[0].Name) } func TestValidateRequestHeaders_SecurityHeadersRecognized(t *testing.T) { // Covers validator.go:122-125 - security scheme headers are recognized as declared opts := config.NewValidationOptions(config.WithStrictMode()) // No declared params - security headers come from security schemes params := []*v3.Parameter{} // Security headers extracted from security schemes securityHeaders := []string{"X-API-Key", "X-Custom-Auth"} headers := http.Header{ "X-Api-Key": []string{"my-api-key"}, // matches securityHeaders (case-insensitive) "X-Custom-Auth": []string{"custom-token"}, // matches securityHeaders "X-Unknown": []string{"should-be-flagged"}, } undeclared := ValidateRequestHeaders(headers, params, securityHeaders, opts) // Only X-Unknown should be reported; X-Api-Key and X-Custom-Auth are recognized as security headers assert.Len(t, undeclared, 1) assert.Equal(t, "X-Unknown", undeclared[0].Name) } func TestValidateRequestHeaders_SecurityHeadersCaseInsensitive(t *testing.T) { // Verify security header matching is case-insensitive opts := config.NewValidationOptions(config.WithStrictMode()) params := []*v3.Parameter{} securityHeaders := []string{"X-API-KEY"} // uppercase headers := http.Header{ "x-api-key": []string{"my-key"}, // lowercase in request } undeclared := ValidateRequestHeaders(headers, params, securityHeaders, opts) // Should not report x-api-key as undeclared (case-insensitive match) assert.Empty(t, undeclared) } func TestValidateRequestHeaders_BothParamsAndSecurityHeaders(t *testing.T) { // Test that both params and security headers are recognized opts := config.NewValidationOptions(config.WithStrictMode()) params := []*v3.Parameter{ {Name: "X-Request-Id", In: "header"}, } securityHeaders := []string{"X-API-Key"} headers := http.Header{ "X-Request-Id": []string{"123"}, // declared via params "X-Api-Key": []string{"key"}, // declared via security schemes "X-Other": []string{"should-be-flagged"}, } undeclared := ValidateRequestHeaders(headers, params, securityHeaders, opts) // Only X-Other should be reported assert.Len(t, undeclared, 1) assert.Equal(t, "X-Other", undeclared[0].Name) } func TestValidateRequestHeaders_EmptySecurityHeaders(t *testing.T) { // Test with empty security headers slice (not nil) opts := config.NewValidationOptions(config.WithStrictMode()) params := []*v3.Parameter{} securityHeaders := []string{} // empty, not nil headers := http.Header{ "X-Custom": []string{"value"}, } undeclared := ValidateRequestHeaders(headers, params, securityHeaders, opts) // X-Custom should be flagged since there are no declared headers assert.Len(t, undeclared, 1) assert.Equal(t, "X-Custom", undeclared[0].Name) } func TestValidateResponseHeaders_DeclaredHeaderSkipped(t *testing.T) { // Covers validator.go:219-223, 228-230 - declared header handling in response opts := config.NewValidationOptions(config.WithStrictMode()) // Create declared headers map declaredHeaders := make(map[string]*v3.Header) declaredHeaders["X-Response-Id"] = &v3.Header{} headers := http.Header{ "X-Response-Id": []string{"declared"}, "X-Undeclared": []string{"should-be-flagged"}, } undeclared := ValidateResponseHeaders(headers, &declaredHeaders, opts) // Only X-Undeclared should be reported assert.Len(t, undeclared, 1) assert.Equal(t, "X-Undeclared", undeclared[0].Name) } func TestValidateResponseHeaders_WithDeclaredHeaders(t *testing.T) { // Covers validator.go:219-223, 228-230 - building declared names list opts := config.NewValidationOptions(config.WithStrictMode()) // Create declared headers map with multiple headers declaredHeaders := make(map[string]*v3.Header) declaredHeaders["X-Rate-Limit"] = &v3.Header{} declaredHeaders["X-Request-Id"] = &v3.Header{} headers := http.Header{ "X-Rate-Limit": []string{"100"}, "X-Request-Id": []string{"abc123"}, "X-Undeclared": []string{"flagged"}, } undeclared := ValidateResponseHeaders(headers, &declaredHeaders, opts) // Only X-Undeclared should be reported assert.Len(t, undeclared, 1) assert.Equal(t, "X-Undeclared", undeclared[0].Name) } func TestValidateResponseHeaders_IgnorePathMatch(t *testing.T) { // Covers validator.go:239 - skip when header matches ignore path pattern opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.headers.x-internal"), ) declaredHeaders := make(map[string]*v3.Header) headers := http.Header{ "X-Internal": []string{"should-be-ignored"}, // matches ignore path "X-Undeclared": []string{"should-be-flagged"}, } undeclared := ValidateResponseHeaders(headers, &declaredHeaders, opts) // Only X-Undeclared should be reported (X-Internal matches ignore path) assert.Len(t, undeclared, 1) assert.Equal(t, "X-Undeclared", undeclared[0].Name) } func TestNewValidator_WithIgnorePaths(t *testing.T) { // Covers types.go:310-311 - compiledIgnorePaths populated opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body.metadata", "$.body.internal"), ) v := NewValidator(opts, 3.1) // Verify ignore paths are compiled assert.NotNil(t, v) assert.Len(t, v.compiledIgnorePaths, 2) // Test that the patterns work yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: TestSchema: type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "TestSchema") // metadata and internal are undeclared properties that match ignore patterns data := map[string]any{ "name": "test", "metadata": map[string]any{ "ignored": "value", }, "internal": map[string]any{ "deep": map[string]any{ "nested": "also ignored", }, }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // metadata and internal paths are ignored, so no undeclared errors assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestNewValidator_WithCustomLogger(t *testing.T) { // Covers types.go:295 - custom logger from options customLogger := slog.New(slog.NewTextHandler(nil, nil)) opts := config.NewValidationOptions( config.WithStrictMode(), config.WithLogger(customLogger), ) v := NewValidator(opts, 3.1) // Verify the custom logger is used assert.NotNil(t, v) assert.Equal(t, customLogger, v.logger) } // ============================================================================= // Phase 3: MEDIUM Priority Tests // ============================================================================= func TestStrictValidator_PrimitiveValuesIgnored(t *testing.T) { // Covers schema_walker.go:37-38 - validateValue default case for primitives // Primitive values (string, number, boolean) have no properties to check yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: StringSchema: type: string NumberSchema: type: number BooleanSchema: type: boolean ` model := buildSchemaFromYAML(t, yml) opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test string value - no properties to check stringSchema := getSchema(t, model, "StringSchema") result := v.Validate(Input{ Schema: stringSchema, Data: "just a string", Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) // Test number value numberSchema := getSchema(t, model, "NumberSchema") result = v.Validate(Input{ Schema: numberSchema, Data: 42.5, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) // Test boolean value boolSchema := getSchema(t, model, "BooleanSchema") result = v.Validate(Input{ Schema: boolSchema, Data: true, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_AdditionalPropertiesSchemaRecurse(t *testing.T) { // Covers schema_walker.go:72-80 - recurse into additionalProperties schema yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: AddlPropsNested: type: object properties: id: type: string additionalProperties: type: object properties: nested: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "AddlPropsNested") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Data with nested undeclared property inside additionalProperties data := map[string]any{ "id": "1", "extra": map[string]any{ "nested": "ok", "bad": "undeclared inside extra", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Both "extra" at top level AND "bad" inside extra should be reported assert.False(t, result.Valid) assert.GreaterOrEqual(t, len(result.UndeclaredValues), 1) // Find undeclared values foundExtra := false foundBad := false for _, uv := range result.UndeclaredValues { if uv.Name == "extra" { foundExtra = true } if uv.Name == "bad" { foundBad = true } } assert.True(t, foundExtra, "expected 'extra' to be reported as undeclared") assert.True(t, foundBad, "expected 'bad' inside extra to be reported as undeclared") } func TestStrictValidator_AdditionalPropertiesFalseShortCircuit(t *testing.T) { // Covers schema_walker.go:113-115 - shouldReportUndeclared returns false // When additionalProperties: false, JSON Schema handles it, not strict mode yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: NoExtras: type: object additionalProperties: false properties: id: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "NoExtras") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Data with extra property - additionalProperties: false handles this data := map[string]any{ "id": "1", "extra": "should be handled by JSON Schema, not strict", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Strict mode should NOT report this because additionalProperties: false // means JSON Schema will handle it assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_PatternPropertiesWithAdditionalFalse(t *testing.T) { // Covers schema_walker.go:223-228 - patternProperties with additionalProperties: false yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: PatternOnly: type: object additionalProperties: false patternProperties: "^x-": type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "PatternOnly") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // x-custom matches the pattern, so it's declared data := map[string]any{ "x-custom": "ok", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // x-custom matches pattern and additionalProperties: false handles the rest assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_InvalidPatternPropertiesRegex(t *testing.T) { // Covers property_collector.go:46-49 - invalid regex skipped yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: InvalidPattern: type: object properties: id: type: string patternProperties: "[invalid(regex": type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "InvalidPattern") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Property name that would match the invalid pattern if it could compile data := map[string]any{ "id": "1", "[invalid(regex": "value", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Invalid pattern is skipped, so the property is reported as undeclared assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "[invalid(regex", result.UndeclaredValues[0].Name) } func TestStrictValidator_UnevaluatedItemsWithIgnoredPath(t *testing.T) { // Covers array_validator.go:97-98 - shouldIgnore in unevaluatedItems yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: UnevalIgnore: type: array unevaluatedItems: type: object properties: id: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "UnevalIgnore") // Ignore the first array element opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body[0]"), ) v := NewValidator(opts, 3.1) // First item has undeclared 'extra', but it should be ignored data := []any{ map[string]any{ "id": "1", "extra": "should be ignored at index 0", }, map[string]any{ "id": "2", "extra2": "should be reported at index 1", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // First item ignored, second item's extra2 should be reported assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra2", result.UndeclaredValues[0].Name) } func TestStrictValidator_AdditionalPropertiesSchemaReportsUndeclared(t *testing.T) { // Covers schema_walker.go:122-126 - additionalProperties with schema still reports yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: SchemaAddl: type: object additionalProperties: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "SchemaAddl") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Data with extra property allowed by additionalProperties schema data := map[string]any{ "extra": "ok per JSON Schema but flagged by strict", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Strict mode should still flag undeclared properties even when // additionalProperties allows them assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) } func TestStrictValidator_NilSchemaPassesValidation(t *testing.T) { // Covers matcher.go:38-40 - nil schema handling in dataMatchesSchema // When schema is nil, validation passes (no schema means anything matches) opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Test with nil schema directly using dataMatchesSchema matches, err := v.dataMatchesSchema(nil, map[string]any{"key": "value"}) assert.NoError(t, err) assert.True(t, matches, "nil schema should match any data") // Also test with different data types matches, err = v.dataMatchesSchema(nil, "string value") assert.NoError(t, err) assert.True(t, matches) matches, err = v.dataMatchesSchema(nil, 123) assert.NoError(t, err) assert.True(t, matches) matches, err = v.dataMatchesSchema(nil, []any{1, 2, 3}) assert.NoError(t, err) assert.True(t, matches) } func TestStrictValidator_ValidateValue_ShouldIgnore(t *testing.T) { // Covers schema_walker.go:17-18 - shouldIgnore in validateValue at ENTRY point // Need to ignore $.body itself so the check happens at validateValue entry yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: IgnoreTest: type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "IgnoreTest") // Ignore the entire body - validateValue entry should return early at line 18 opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body"), ) v := NewValidator(opts, 3.1) data := map[string]any{ "name": "valid", "undeclared": "should be ignored because entire body is ignored", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Entire body is ignored, so no undeclared errors assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_ValidateValue_CycleDetection(t *testing.T) { // Covers schema_walker.go:27-28 - cycle detection in validateValue // Need to call validateValue directly with a pre-visited context yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: TestSchema: type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "TestSchema") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Create a context and pre-mark the schema as visited at this path ctx := newTraversalContext(DirectionRequest, v.compiledIgnorePaths, "$.body") schemaKey := v.getSchemaKey(schema) ctx.checkAndMarkVisited(schemaKey) // First visit - marks as visited data := map[string]any{ "name": "test", "undeclared": "should not be detected due to cycle", } // Call validateValue directly - should hit line 28 (cycle detected) result := v.validateValue(ctx, schema, data) // Cycle detected, returns early with no errors assert.Empty(t, result) } func TestStrictValidator_ShouldReportUndeclared_AdditionalPropertiesTrue(t *testing.T) { // Covers schema_walker.go:119-120 - additionalProperties: true explicit yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: ExplicitTrue: type: object additionalProperties: true properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "ExplicitTrue") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Extra property allowed by additionalProperties: true but flagged by strict data := map[string]any{ "name": "test", "extra": "should be flagged in strict mode", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // Strict mode should flag undeclared even with additionalProperties: true assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "extra", result.UndeclaredValues[0].Name) } func TestStrictValidator_RecurseIntoDeclaredProperties_PropertyNotInData(t *testing.T) { // Covers schema_walker.go:179-180 - continue when schema property not in data yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: MissingProp: type: object additionalProperties: false properties: required: type: string optional: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "MissingProp") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Only provide 'required', not 'optional' - line 180 should be hit data := map[string]any{ "required": "value", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // No undeclared properties assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_RecurseIntoDeclaredProperties_SkipReadOnly(t *testing.T) { // Covers schema_walker.go:194-195 - shouldSkipProperty in recurseIntoDeclaredProperties yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: ReadOnlyProp: type: object additionalProperties: false properties: name: type: string id: type: string readOnly: true ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "ReadOnlyProp") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // Include readOnly property in request - should be skipped (line 195) data := map[string]any{ "name": "test", "id": "should-be-skipped", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // id is readOnly and skipped, no errors assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_RecurseIntoDeclaredProperties_ShouldIgnore(t *testing.T) { // Covers schema_walker.go:188-189 - shouldIgnore for explicit property yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: IgnoreProp: type: object additionalProperties: false properties: name: type: string metadata: type: object properties: nested: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "IgnoreProp") // Ignore metadata property opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body.metadata"), ) v := NewValidator(opts, 3.1) // metadata has undeclared property but is ignored (line 189) data := map[string]any{ "name": "test", "metadata": map[string]any{ "nested": "ok", "undeclared": "should be ignored", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // metadata is ignored, no errors assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_RecurseIntoDeclaredProperties_PatternNoMatch(t *testing.T) { // Covers schema_walker.go:210-211 - property doesn't match any patternProperty yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: PatternSchema: type: object additionalProperties: false properties: name: type: string patternProperties: "^x-": type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "PatternSchema") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // "other" doesn't match explicit props or pattern "^x-" - line 211 hit data := map[string]any{ "name": "test", "x-custom": "matches pattern", "other": "doesn't match pattern", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // "other" doesn't match pattern, but additionalProperties: false handles it // so strict mode doesn't report it assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_RecurseIntoDeclaredProperties_PatternSkipReadOnly(t *testing.T) { // Covers schema_walker.go:225-226 - shouldSkipProperty for patternProperty yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: PatternReadOnly: type: object additionalProperties: false properties: name: type: string patternProperties: "^x-": type: string readOnly: true ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "PatternReadOnly") opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) // x-custom matches pattern but schema is readOnly - skip in request (line 226) data := map[string]any{ "name": "test", "x-custom": "matches readOnly pattern", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // x-custom matches pattern but is readOnly, skipped in request assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_RecurseIntoDeclaredProperties_PatternShouldIgnore(t *testing.T) { // Covers schema_walker.go:219-220 - shouldIgnore for patternProperty path yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: PatternIgnore: type: object additionalProperties: false properties: name: type: string patternProperties: "^x-": type: object properties: nested: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "PatternIgnore") // Ignore the pattern-matched property path opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictIgnorePaths("$.body.x-custom"), ) v := NewValidator(opts, 3.1) // x-custom matches pattern, path matches ignore pattern - should skip (line 220) data := map[string]any{ "name": "test", "x-custom": map[string]any{ "nested": "valid", "undeclared": "should be ignored because path is ignored", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) // x-custom path is ignored, so no undeclared errors assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_ValidateVariantWithParent_VariantNilGoLow(t *testing.T) { // Covers polymorphic.go:233-234 - fallback to parent when variant.GoLow() is nil // This tests the defensive code path for programmatically created schemas // Build a real parent schema from YAML (has GoLow()) yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: ParentSchema: type: object properties: parentProp: type: string ` model := buildSchemaFromYAML(t, yml) parent := getSchema(t, model, "ParentSchema") require.NotNil(t, parent.GoLow()) // Create a variant schema programmatically (no GoLow()) variant := &base.Schema{ Type: []string{"object"}, } require.Nil(t, variant.GoLow()) opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) ctx := newTraversalContext(DirectionRequest, nil, "$.body") // Data with undeclared property - triggers line 230-237 data := map[string]any{ "parentProp": "value", "undeclared": "triggers undeclared path", } result := v.validateVariantWithParent(ctx, parent, variant, data) // Should detect undeclared property and use parent's location (since variant has no GoLow) assert.Len(t, result, 1) assert.Equal(t, "undeclared", result[0].Name) // Location should come from parent (not crash due to nil variant.GoLow()) assert.Greater(t, result[0].SpecLine, 0) } // ============== readOnly/writeOnly rejection tests ============== func TestStrictValidator_RejectReadOnly_InRequest(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: id: type: string readOnly: true name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly()) v := NewValidator(opts, 3.1) // readOnly property "id" sent in a request should be rejected data := map[string]any{ "id": "user-123", "name": "John", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) require.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "id", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body.id", result.UndeclaredValues[0].Path) assert.Equal(t, TypeReadOnlyProperty, result.UndeclaredValues[0].Type) assert.Equal(t, DirectionRequest, result.UndeclaredValues[0].Direction) } func TestStrictValidator_RejectReadOnly_Disabled(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: id: type: string readOnly: true name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") // Flag off — backward compat: no violation opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) data := map[string]any{ "id": "user-123", "name": "John", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_RejectReadOnly_InResponse_NoEffect(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: id: type: string readOnly: true name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly()) v := NewValidator(opts, 3.1) // readOnly in response is fine — readOnly only applies to requests data := map[string]any{ "id": "user-123", "name": "John", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionResponse, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_RejectReadOnly_NestedObject(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string metadata: type: object properties: createdAt: type: string readOnly: true updatedBy: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly()) v := NewValidator(opts, 3.1) data := map[string]any{ "name": "John", "metadata": map[string]any{ "createdAt": "2024-01-01", "updatedBy": "admin", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) require.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "createdAt", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body.metadata.createdAt", result.UndeclaredValues[0].Path) assert.Equal(t, TypeReadOnlyProperty, result.UndeclaredValues[0].Type) } func TestStrictValidator_RejectReadOnly_AllOf(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Combined: allOf: - type: object properties: id: type: string readOnly: true - type: object properties: name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Combined") opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly()) v := NewValidator(opts, 3.1) data := map[string]any{ "id": "combined-1", "name": "Test", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) require.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "id", result.UndeclaredValues[0].Name) assert.Equal(t, TypeReadOnlyProperty, result.UndeclaredValues[0].Type) } func TestStrictValidator_RejectWriteOnly_InResponse(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string password: type: string writeOnly: true ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectWriteOnly()) v := NewValidator(opts, 3.1) // writeOnly property "password" in response should be rejected data := map[string]any{ "name": "John", "password": "secret", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionResponse, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) require.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "password", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body.password", result.UndeclaredValues[0].Path) assert.Equal(t, TypeWriteOnlyProperty, result.UndeclaredValues[0].Type) assert.Equal(t, DirectionResponse, result.UndeclaredValues[0].Direction) } func TestStrictValidator_RejectWriteOnly_Disabled(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string password: type: string writeOnly: true ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") // Flag off — backward compat: no violation opts := config.NewValidationOptions(config.WithStrictMode()) v := NewValidator(opts, 3.1) data := map[string]any{ "name": "John", "password": "secret", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionResponse, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_RejectWriteOnly_InRequest_NoEffect(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string password: type: string writeOnly: true ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectWriteOnly()) v := NewValidator(opts, 3.1) // writeOnly in request is fine — writeOnly only applies to responses data := map[string]any{ "name": "John", "password": "secret", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.True(t, result.Valid) assert.Empty(t, result.UndeclaredValues) } func TestStrictValidator_RejectWriteOnly_NestedObject(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: name: type: string auth: type: object properties: token: type: string writeOnly: true provider: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectWriteOnly()) v := NewValidator(opts, 3.1) data := map[string]any{ "name": "John", "auth": map[string]any{ "token": "secret-token", "provider": "google", }, } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionResponse, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) require.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "token", result.UndeclaredValues[0].Name) assert.Equal(t, "$.body.auth.token", result.UndeclaredValues[0].Path) assert.Equal(t, TypeWriteOnlyProperty, result.UndeclaredValues[0].Type) } func TestStrictValidator_RejectBoth_Enabled(t *testing.T) { yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: User: type: object properties: id: type: string readOnly: true name: type: string password: type: string writeOnly: true ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "User") // Both flags on opts := config.NewValidationOptions( config.WithStrictMode(), config.WithStrictRejectReadOnly(), config.WithStrictRejectWriteOnly(), ) // Test request: readOnly "id" should be rejected v := NewValidator(opts, 3.1) dataRequest := map[string]any{ "id": "user-123", "name": "John", "password": "secret", } resultRequest := v.Validate(Input{ Schema: schema, Data: dataRequest, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, resultRequest.Valid) require.Len(t, resultRequest.UndeclaredValues, 1) assert.Equal(t, "id", resultRequest.UndeclaredValues[0].Name) assert.Equal(t, TypeReadOnlyProperty, resultRequest.UndeclaredValues[0].Type) // Test response: writeOnly "password" should be rejected v2 := NewValidator(opts, 3.1) dataResponse := map[string]any{ "id": "user-123", "name": "John", "password": "secret", } resultResponse := v2.Validate(Input{ Schema: schema, Data: dataResponse, Direction: DirectionResponse, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, resultResponse.Valid) require.Len(t, resultResponse.UndeclaredValues, 1) assert.Equal(t, "password", resultResponse.UndeclaredValues[0].Name) assert.Equal(t, TypeWriteOnlyProperty, resultResponse.UndeclaredValues[0].Type) } func TestStrictValidator_RejectReadOnly_AdditionalPropertiesFalse(t *testing.T) { // Covers schema_walker.go recurseIntoDeclaredProperties: // explicit property path and patternProperties path yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: Strict: type: object additionalProperties: false properties: id: type: string readOnly: true name: type: string patternProperties: "^x-": type: string readOnly: true ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "Strict") opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly()) v := NewValidator(opts, 3.1) data := map[string]any{ "id": "user-1", "name": "John", "x-custom": "value", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) assert.Len(t, result.UndeclaredValues, 2) names := []string{result.UndeclaredValues[0].Name, result.UndeclaredValues[1].Name} assert.Contains(t, names, "id") assert.Contains(t, names, "x-custom") for _, uv := range result.UndeclaredValues { assert.Equal(t, TypeReadOnlyProperty, uv.Type) } } func TestStrictValidator_RejectReadOnly_OneOf(t *testing.T) { // Covers polymorphic.go validateVariantWithParent path yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: OneOfSchema: type: object oneOf: - type: object properties: id: type: string readOnly: true email: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "OneOfSchema") opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly()) v := NewValidator(opts, 3.1) data := map[string]any{ "id": "user-1", "email": "test@example.com", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) require.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "id", result.UndeclaredValues[0].Name) assert.Equal(t, TypeReadOnlyProperty, result.UndeclaredValues[0].Type) } func TestStrictValidator_RejectReadOnly_OneOfAdditionalPropertiesFalse(t *testing.T) { // Covers polymorphic.go recurseIntoDeclaredPropertiesWithMerged path yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: OneOfStrict: type: object additionalProperties: false properties: name: type: string oneOf: - type: object additionalProperties: false properties: name: type: string id: type: string readOnly: true data: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "OneOfStrict") opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly()) v := NewValidator(opts, 3.1) data := map[string]any{ "name": "test", "id": "should-be-rejected", "data": "valid", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) require.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "id", result.UndeclaredValues[0].Name) assert.Equal(t, TypeReadOnlyProperty, result.UndeclaredValues[0].Type) } func TestStrictValidator_RejectReadOnly_AllOfAdditionalPropertiesFalse(t *testing.T) { // Covers polymorphic.go recurseIntoAllOfDeclaredProperties path yml := `openapi: "3.1.0" info: title: Test version: "1.0" paths: {} components: schemas: AllOfStrict: type: object additionalProperties: false allOf: - type: object properties: id: type: string readOnly: true name: type: string ` model := buildSchemaFromYAML(t, yml) schema := getSchema(t, model, "AllOfStrict") opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly()) v := NewValidator(opts, 3.1) data := map[string]any{ "id": "should-be-rejected", "name": "John", } result := v.Validate(Input{ Schema: schema, Data: data, Direction: DirectionRequest, Options: opts, BasePath: "$.body", Version: 3.1, }) assert.False(t, result.Valid) require.Len(t, result.UndeclaredValues, 1) assert.Equal(t, "id", result.UndeclaredValues[0].Name) assert.Equal(t, TypeReadOnlyProperty, result.UndeclaredValues[0].Type) } func TestStrictValidator_CheckReadWriteOnlyViolation_NilSchema(t *testing.T) { opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly()) v := NewValidator(opts, 3.1) _, ok := v.checkReadWriteOnlyViolation("$.body.x", "x", "val", nil, DirectionRequest) assert.False(t, ok) } libopenapi-validator-0.13.8/test_specs/000077500000000000000000000000001520534042400200635ustar00rootroot00000000000000libopenapi-validator-0.13.8/test_specs/care_request.yaml000066400000000000000000000033311520534042400234310ustar00rootroot00000000000000# Example from https://deliveroo.engineering/2022/06/27/openapi-design-first.html # © All-Rights-Reserved openapi: 3.1.0 info: title: Care Request API version: 0.1.0 paths: "/requests/{request-id}": get: summary: Get all requests operationId: getRequest parameters: - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/TracingId' responses: '200': description: 'Completed successfully' content: application/json: schema: $ref: '#/components/schemas/CareRequest' '404': description: 'The resource could not be found' content: {} # we'd also add other response options here too components: parameters: RequestId: name: request-id in: path required: true schema: $ref: '#/components/schemas/RequestId' x-go-name: RequestIdParameter TracingId: description: A unique tracing ID that can be used for end-to-end tracing name: tracing-id in: header required: false schema: type: string format: uuid pattern: "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}" schemas: CareRequest: type: object properties: id: $ref: '#/components/schemas/RequestId' status: $ref: '#/components/schemas/RequestStatus' required: - id - status RequestId: type: string format: uuid pattern: "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}" RequestStatus: type: string enum: - active - completed libopenapi-validator-0.13.8/test_specs/invalid_31.yaml000066400000000000000000000004201520534042400226740ustar00rootroot00000000000000openapi: 3.1.0 info: title: 1 version: 1.0.0 contact: invalid: not a valid contact object license: invalid: bork bork. components: schemas: Test: type: object properties: test: type: string required: - testlibopenapi-validator-0.13.8/test_specs/nullable_enum.yaml000066400000000000000000000060301520534042400235700ustar00rootroot00000000000000openapi: 3.0.2 info: title: Nullable Enum Test API version: 1.0.0 description: Test specification for nullable enum validation paths: /status: get: summary: Get status with nullable enum operationId: getStatus responses: '200': description: Successful response content: application/json: schema: $ref: '#/components/schemas/StatusResponse' post: summary: Create status operationId: createStatus requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/StatusRequest' responses: '201': description: Created content: application/json: schema: $ref: '#/components/schemas/StatusResponse' /items: get: summary: Get items with nullable enum in array operationId: getItems responses: '200': description: Successful response content: application/json: schema: type: array items: $ref: '#/components/schemas/Item' components: schemas: StatusResponse: type: object required: - id properties: id: type: integer format: int64 status: type: string description: Status field with nullable enum (no null in enum) enum: - active - inactive - pending - archived nullable: true priority: type: string description: Priority field with nullable enum (null already in enum) enum: - high - medium - low - null nullable: true category: type: string description: Non-nullable enum enum: - public - private - internal StatusRequest: type: object required: - status properties: status: type: string enum: - active - inactive - pending - archived nullable: true priority: type: string enum: - high - medium - low - null nullable: true Item: type: object required: - id - name properties: id: type: integer format: int64 name: type: string status: type: string description: Nested nullable enum enum: - available - sold - reserved nullable: true metadata: type: object properties: visibility: type: string description: Deeply nested nullable enum enum: - visible - hidden nullable: true libopenapi-validator-0.13.8/test_specs/petstorev3.json000066400000000000000000000746471520534042400231160ustar00rootroot00000000000000{ "openapi": "3.0.2", "info": { "title": "Swagger Petstore - OpenAPI 3.0", "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", "termsOfService": "http://swagger.io/terms/", "contact": { "email": "apiteam@swagger.io" }, "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, "version": "1.0.11" }, "externalDocs": { "description": "Find out more about Swagger", "url": "http://swagger.io" }, "servers": [ { "url": "/api/v3" } ], "tags": [ { "name": "pet", "description": "Everything about your Pets", "externalDocs": { "description": "Find out more", "url": "http://swagger.io" } }, { "name": "store", "description": "Access to Petstore orders", "externalDocs": { "description": "Find out more about our store", "url": "http://swagger.io" } }, { "name": "user", "description": "Operations about user" } ], "paths": { "/pet": { "put": { "tags": [ "pet" ], "summary": "Update an existing pet", "description": "Update an existing pet by Id", "operationId": "updatePet", "requestBody": { "description": "Update an existent pet in the store", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/Pet" } } }, "required": true }, "responses": { "200": { "description": "Successful operation", "content": { "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } } } }, "400": { "description": "Invalid ID supplied" }, "404": { "description": "Pet not found" }, "405": { "description": "Validation exception" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] }, "post": { "tags": [ "pet" ], "summary": "Add a new pet to the store", "description": "Add a new pet to the store", "operationId": "addPet", "requestBody": { "description": "Create a new pet in the store", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/Pet" } } }, "required": true }, "responses": { "200": { "description": "Successful operation", "content": { "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } } } }, "405": { "description": "Invalid input" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] } }, "/pet/findByStatus": { "get": { "tags": [ "pet" ], "summary": "Finds Pets by status", "description": "Multiple status values can be provided with comma separated strings", "operationId": "findPetsByStatus", "parameters": [ { "name": "status", "in": "query", "description": "Status values that need to be considered for filter", "required": false, "explode": true, "schema": { "type": "string", "default": "available", "enum": [ "available", "pending", "sold" ] } } ], "responses": { "200": { "description": "successful operation", "content": { "application/xml": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } }, "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } } } }, "400": { "description": "Invalid status value" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] } }, "/pet/findByTags": { "get": { "tags": [ "pet" ], "summary": "Finds Pets by tags", "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", "operationId": "findPetsByTags", "parameters": [ { "name": "tags", "in": "query", "description": "Tags to filter by", "required": false, "explode": true, "schema": { "type": "array", "items": { "type": "string" } } } ], "responses": { "200": { "description": "successful operation", "content": { "application/xml": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } }, "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } } } }, "400": { "description": "Invalid tag value" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] } }, "/pet/{petId}": { "get": { "tags": [ "pet" ], "summary": "Find pet by ID", "description": "Returns a single pet", "operationId": "getPetById", "parameters": [ { "name": "petId", "in": "path", "description": "ID of pet to return", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { "200": { "description": "successful operation", "content": { "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } } } }, "400": { "description": "Invalid ID supplied" }, "404": { "description": "Pet not found" } }, "security": [ { "api_key": [] }, { "petstore_auth": [ "write:pets", "read:pets" ] } ] }, "post": { "tags": [ "pet" ], "summary": "Updates a pet in the store with form data", "description": "", "operationId": "updatePetWithForm", "parameters": [ { "name": "petId", "in": "path", "description": "ID of pet that needs to be updated", "required": true, "schema": { "type": "integer", "format": "int64" } }, { "name": "name", "in": "query", "description": "Name of pet that needs to be updated", "schema": { "type": "string" } }, { "name": "status", "in": "query", "description": "Status of pet that needs to be updated", "schema": { "type": "string" } } ], "responses": { "405": { "description": "Invalid input" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] }, "delete": { "tags": [ "pet" ], "summary": "Deletes a pet", "description": "", "operationId": "deletePet", "parameters": [ { "name": "api_key", "in": "header", "description": "", "required": false, "schema": { "type": "string" } }, { "name": "petId", "in": "path", "description": "Pet id to delete", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { "400": { "description": "Invalid pet value" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] } }, "/pet/{petId}/uploadImage": { "post": { "tags": [ "pet" ], "summary": "uploads an image", "description": "", "operationId": "uploadFile", "parameters": [ { "name": "petId", "in": "path", "description": "ID of pet to update", "required": true, "schema": { "type": "integer", "format": "int64" } }, { "name": "additionalMetadata", "in": "query", "description": "Additional Metadata", "required": false, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/octet-stream": { "schema": { "type": "string", "format": "binary" } } } }, "responses": { "200": { "description": "successful operation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] } }, "/store/inventory": { "get": { "tags": [ "store" ], "summary": "Returns pet inventories by status", "description": "Returns a map of status codes to quantities", "operationId": "getInventory", "responses": { "200": { "description": "successful operation", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": { "type": "integer", "format": "int32" } } } } } }, "security": [ { "api_key": [] } ] } }, "/store/order": { "post": { "tags": [ "store" ], "summary": "Place an order for a pet", "description": "Place a new order in the store", "operationId": "placeOrder", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Order" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/Order" } }, "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/Order" } } } }, "responses": { "200": { "description": "successful operation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Order" } } } }, "405": { "description": "Invalid input" } } } }, "/store/order/{orderId}": { "get": { "tags": [ "store" ], "summary": "Find purchase order by ID", "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", "operationId": "getOrderById", "parameters": [ { "name": "orderId", "in": "path", "description": "ID of order that needs to be fetched", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { "200": { "description": "successful operation", "content": { "application/xml": { "schema": { "$ref": "#/components/schemas/Order" } }, "application/json": { "schema": { "$ref": "#/components/schemas/Order" } } } }, "400": { "description": "Invalid ID supplied" }, "404": { "description": "Order not found" } } }, "delete": { "tags": [ "store" ], "summary": "Delete purchase order by ID", "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", "operationId": "deleteOrder", "parameters": [ { "name": "orderId", "in": "path", "description": "ID of the order that needs to be deleted", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { "400": { "description": "Invalid ID supplied" }, "404": { "description": "Order not found" } } } }, "/user": { "post": { "tags": [ "user" ], "summary": "Create user", "description": "This can only be done by the logged in user.", "operationId": "createUser", "requestBody": { "description": "Created user object", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/User" } }, "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/User" } } } }, "responses": { "default": { "description": "successful operation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/User" } } } } } } }, "/user/createWithList": { "post": { "tags": [ "user" ], "summary": "Creates list of users with given input array", "description": "Creates list of users with given input array", "operationId": "createUsersWithListInput", "requestBody": { "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/User" } } } } }, "responses": { "200": { "description": "Successful operation", "content": { "application/xml": { "schema": { "$ref": "#/components/schemas/User" } }, "application/json": { "schema": { "$ref": "#/components/schemas/User" } } } }, "default": { "description": "successful operation" } } } }, "/user/login": { "get": { "tags": [ "user" ], "summary": "Logs user into the system", "description": "", "operationId": "loginUser", "parameters": [ { "name": "username", "in": "query", "description": "The user name for login", "required": false, "schema": { "type": "string" } }, { "name": "password", "in": "query", "description": "The password for login in clear text", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "description": "successful operation", "headers": { "X-Rate-Limit": { "description": "calls per hour allowed by the user", "schema": { "type": "integer", "format": "int32" } }, "X-Expires-After": { "description": "date in UTC when token expires", "schema": { "type": "string", "format": "date-time" } } }, "content": { "application/xml": { "schema": { "type": "string" } }, "application/json": { "schema": { "type": "string" } } } }, "400": { "description": "Invalid username/password supplied" } } } }, "/user/logout": { "get": { "tags": [ "user" ], "summary": "Logs out current logged in user session", "description": "", "operationId": "logoutUser", "parameters": [], "responses": { "default": { "description": "successful operation" } } } }, "/user/{username}": { "get": { "tags": [ "user" ], "summary": "Get user by user name", "description": "", "operationId": "getUserByName", "parameters": [ { "name": "username", "in": "path", "description": "The name that needs to be fetched. Use user1 for testing. ", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "successful operation", "content": { "application/xml": { "schema": { "$ref": "#/components/schemas/User" } }, "application/json": { "schema": { "$ref": "#/components/schemas/User" } } } }, "400": { "description": "Invalid username supplied" }, "404": { "description": "User not found" } } }, "put": { "tags": [ "user" ], "summary": "Update user", "description": "This can only be done by the logged in user.", "operationId": "updateUser", "parameters": [ { "name": "username", "in": "path", "description": "name that need to be deleted", "required": true, "schema": { "type": "string" } } ], "requestBody": { "description": "Update an existent user in the store", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/User" } }, "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/User" } } } }, "responses": { "default": { "description": "successful operation" } } }, "delete": { "tags": [ "user" ], "summary": "Delete user", "description": "This can only be done by the logged in user.", "operationId": "deleteUser", "parameters": [ { "name": "username", "in": "path", "description": "The name that needs to be deleted", "required": true, "schema": { "type": "string" } } ], "responses": { "400": { "description": "Invalid username supplied" }, "404": { "description": "User not found" } } } } }, "components": { "schemas": { "Order": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64", "example": 10 }, "petId": { "type": "integer", "format": "int64", "example": 198772 }, "quantity": { "type": "integer", "format": "int32", "example": 7 }, "shipDate": { "type": "string", "format": "date-time" }, "status": { "type": "string", "description": "Order Status", "example": "approved", "enum": [ "placed", "approved", "delivered" ] }, "complete": { "type": "boolean" } }, "xml": { "name": "order" } }, "Customer": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64", "example": 100000 }, "username": { "type": "string", "example": "fehguy" }, "address": { "type": "array", "xml": { "name": "addresses", "wrapped": true }, "items": { "$ref": "#/components/schemas/Address" } } }, "xml": { "name": "customer" } }, "Address": { "type": "object", "properties": { "street": { "type": "string", "example": "437 Lytton" }, "city": { "type": "string", "example": "Palo Alto" }, "state": { "type": "string", "example": "CA" }, "zip": { "type": "string", "example": "94301" } }, "xml": { "name": "address" } }, "Category": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64", "example": 1 }, "name": { "type": "string", "example": "Dogs" } }, "xml": { "name": "category" } }, "User": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64", "example": 10 }, "username": { "type": "string", "example": "theUser" }, "firstName": { "type": "string", "example": "John" }, "lastName": { "type": "string", "example": "James" }, "email": { "type": "string", "example": "john@email.com" }, "password": { "type": "string", "example": "12345" }, "phone": { "type": "string", "example": "12345" }, "userStatus": { "type": "integer", "description": "User Status", "format": "int32", "example": 1 } }, "xml": { "name": "user" } }, "Tag": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64" }, "name": { "type": "string" } }, "xml": { "name": "tag" } }, "Pet": { "required": [ "name", "photoUrls" ], "type": "object", "properties": { "id": { "type": "integer", "format": "int64", "example": 10 }, "name": { "type": "string", "example": "doggie" }, "category": { "$ref": "#/components/schemas/Category" }, "photoUrls": { "type": "array", "xml": { "wrapped": true }, "items": { "type": "string", "xml": { "name": "photoUrl" } } }, "tags": { "type": "array", "xml": { "wrapped": true }, "items": { "$ref": "#/components/schemas/Tag" } }, "status": { "type": "string", "description": "pet status in the store", "enum": [ "available", "pending", "sold" ] } }, "xml": { "name": "pet" } }, "ApiResponse": { "type": "object", "properties": { "code": { "type": "integer", "format": "int32" }, "type": { "type": "string" }, "message": { "type": "string" } }, "xml": { "name": "##default" } } }, "requestBodies": { "Pet": { "description": "Pet object that needs to be added to the store", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } } } }, "UserArray": { "description": "List of user object", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/User" } } } } } }, "securitySchemes": { "petstore_auth": { "type": "oauth2", "flows": { "implicit": { "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", "scopes": { "write:pets": "modify pets in your account", "read:pets": "read your pets" } } } }, "api_key": { "type": "apiKey", "name": "api_key", "in": "header" } } } }libopenapi-validator-0.13.8/test_specs/valid_31.yaml000066400000000000000000000005161520534042400223530ustar00rootroot00000000000000openapi: 3.1.0 info: title: Test version: 1.0.0 contact: name: Test email: dave@pb33f.io url: https://pb33f.io license: name: MIT url: https://opensource.org/licenses/MIT components: schemas: Test: type: object properties: test: type: string required: - testlibopenapi-validator-0.13.8/validator.go000066400000000000000000000475421520534042400202370ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package validator import ( "fmt" "net/http" "sort" "sync" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/utils" "go.yaml.in/yaml/v4" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/parameters" "github.com/pb33f/libopenapi-validator/paths" "github.com/pb33f/libopenapi-validator/radix" "github.com/pb33f/libopenapi-validator/requests" "github.com/pb33f/libopenapi-validator/responses" "github.com/pb33f/libopenapi-validator/schema_validation" ) // Validator provides a coarse grained interface for validating an OpenAPI 3+ documents. // There are three primary use-cases for validation // // Validating *http.Request objects against and OpenAPI 3+ document // Validating *http.Response objects against an OpenAPI 3+ document // Validating an OpenAPI 3+ document against the OpenAPI 3+ specification type Validator interface { // ValidateHttpRequest will validate an *http.Request object against an OpenAPI 3+ document. // The path, query, cookie and header parameters and request body are validated. ValidateHttpRequest(request *http.Request) (bool, []*errors.ValidationError) // ValidateHttpRequestSync will validate an *http.Request object against an OpenAPI 3+ document synchronously and without spawning any goroutines. // The path, query, cookie and header parameters and request body are validated. ValidateHttpRequestSync(request *http.Request) (bool, []*errors.ValidationError) // ValidateHttpRequestWithPathItem will validate an *http.Request object against an OpenAPI 3+ document. // The path, query, cookie and header parameters and request body are validated. ValidateHttpRequestWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) // ValidateHttpRequestSyncWithPathItem will validate an *http.Request object against an OpenAPI 3+ document synchronously and without spawning any goroutines. // The path, query, cookie and header parameters and request body are validated. ValidateHttpRequestSyncWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) // ValidateHttpResponse will an *http.Response object against an OpenAPI 3+ document. // The response body is validated. The request is only used to extract the correct response from the spec. ValidateHttpResponse(request *http.Request, response *http.Response) (bool, []*errors.ValidationError) // ValidateHttpRequestResponse will validate both the *http.Request and *http.Response objects against an OpenAPI 3+ document. // The path, query, cookie and header parameters and request and response body are validated. ValidateHttpRequestResponse(request *http.Request, response *http.Response) (bool, []*errors.ValidationError) // ValidateDocument will validate an OpenAPI 3+ document against the 3.0 or 3.1 OpenAPI 3+ specification ValidateDocument() (bool, []*errors.ValidationError) // GetParameterValidator will return a parameters.ParameterValidator instance used to validate parameters GetParameterValidator() parameters.ParameterValidator // GetRequestBodyValidator will return a parameters.RequestBodyValidator instance used to validate request bodies GetRequestBodyValidator() requests.RequestBodyValidator // GetResponseBodyValidator will return a parameters.ResponseBodyValidator instance used to validate response bodies GetResponseBodyValidator() responses.ResponseBodyValidator // SetDocument will set the OpenAPI 3+ document to be validated SetDocument(document libopenapi.Document) } // NewValidator will create a new Validator from an OpenAPI 3+ document func NewValidator(document libopenapi.Document, opts ...config.Option) (Validator, []error) { m, errs := document.BuildV3Model() if errs != nil { return nil, []error{errs} } v := NewValidatorFromV3Model(&m.Model, opts...) v.(*validator).document = document return v, nil } // NewValidatorFromV3Model will create a new Validator from an OpenAPI Model func NewValidatorFromV3Model(m *v3.Document, opts ...config.Option) Validator { options := config.NewValidationOptions(opts...) // Build radix tree for O(k) path lookup (where k = path depth) // Skip if path tree is disabled or a custom tree was provided if options.PathTree == nil && !options.IsPathTreeDisabled() { options.PathTree = radix.BuildPathTree(m) } // warm the schema caches by pre-compiling all schemas in the document // (warmSchemaCaches checks for nil cache and skips if disabled) warmSchemaCaches(m, options) v := &validator{options: options, v3Model: m} // create a new parameter validator v.paramValidator = parameters.NewParameterValidator(m, config.WithExistingOpts(options)) // create aq new request body validator v.requestValidator = requests.NewRequestBodyValidator(m, config.WithExistingOpts(options)) // create a response body validator v.responseValidator = responses.NewResponseBodyValidator(m, config.WithExistingOpts(options)) return v } func (v *validator) SetDocument(document libopenapi.Document) { v.document = document } func (v *validator) GetParameterValidator() parameters.ParameterValidator { return v.paramValidator } func (v *validator) GetRequestBodyValidator() requests.RequestBodyValidator { return v.requestValidator } func (v *validator) GetResponseBodyValidator() responses.ResponseBodyValidator { return v.responseValidator } func (v *validator) ValidateDocument() (bool, []*errors.ValidationError) { if v.document == nil { return false, []*errors.ValidationError{{ ValidationType: helpers.DocumentValidation, ValidationSubType: helpers.ValidationMissing, Message: "Document is not set", Reason: "The document cannot be validated as it is not set", SpecLine: 1, SpecCol: 1, HowToFix: "Set the document via `SetDocument` before validating", }} } var validationOpts []config.Option if v.options != nil { validationOpts = append(validationOpts, config.WithRegexEngine(v.options.RegexEngine)) } return schema_validation.ValidateOpenAPIDocument(v.document, validationOpts...) } func (v *validator) ValidateHttpResponse( request *http.Request, response *http.Response, ) (bool, []*errors.ValidationError) { var pathItem *v3.PathItem var pathValue string var errs []*errors.ValidationError pathItem, errs, pathValue = paths.FindPath(request, v.v3Model, v.options) if pathItem == nil || errs != nil { return false, errs } responseBodyValidator := v.responseValidator // validate response _, responseErrors := responseBodyValidator.ValidateResponseBodyWithPathItem(request, response, pathItem, pathValue) if len(responseErrors) > 0 { return false, responseErrors } return true, nil } func (v *validator) ValidateHttpRequestResponse( request *http.Request, response *http.Response, ) (bool, []*errors.ValidationError) { var pathItem *v3.PathItem var pathValue string var errs []*errors.ValidationError pathItem, errs, pathValue = paths.FindPath(request, v.v3Model, v.options) if pathItem == nil || errs != nil { return false, errs } responseBodyValidator := v.responseValidator // validate request and response _, requestErrors := v.ValidateHttpRequestWithPathItem(request, pathItem, pathValue) _, responseErrors := responseBodyValidator.ValidateResponseBodyWithPathItem(request, response, pathItem, pathValue) if len(requestErrors) > 0 || len(responseErrors) > 0 { return false, append(requestErrors, responseErrors...) } return true, nil } func (v *validator) ValidateHttpRequest(request *http.Request) (bool, []*errors.ValidationError) { pathItem, errs, foundPath := paths.FindPath(request, v.v3Model, v.options) if len(errs) > 0 { return false, errs } return v.ValidateHttpRequestWithPathItem(request, pathItem, foundPath) } func (v *validator) ValidateHttpRequestWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { // create a new parameter validator paramValidator := v.paramValidator // create a new request body validator reqBodyValidator := v.requestValidator // create some channels to handle async validation doneChan := make(chan struct{}) errChan := make(chan []*errors.ValidationError) controlChan := make(chan struct{}) // async param validation function. parameterValidationFunc := func(control chan struct{}, errorChan chan []*errors.ValidationError) { paramErrs := make(chan []*errors.ValidationError) paramControlChan := make(chan struct{}) paramFunctionControlChan := make(chan struct{}) var paramValidationErrors []*errors.ValidationError validations := []validationFunction{ paramValidator.ValidatePathParamsWithPathItem, paramValidator.ValidateCookieParamsWithPathItem, paramValidator.ValidateHeaderParamsWithPathItem, paramValidator.ValidateQueryParamsWithPathItem, paramValidator.ValidateSecurityWithPathItem, } // listen for validation errors on parameters. everything will run async. paramListener := func(control chan struct{}, errorChan chan []*errors.ValidationError) { completedValidations := 0 for { select { case vErrs := <-errorChan: paramValidationErrors = append(paramValidationErrors, vErrs...) case <-control: completedValidations++ if completedValidations == len(validations) { paramFunctionControlChan <- struct{}{} return } } } } validateParamFunction := func( control chan struct{}, errorChan chan []*errors.ValidationError, validatorFunc validationFunction, ) { valid, pErrs := validatorFunc(request, pathItem, pathValue) if !valid { errorChan <- pErrs } control <- struct{}{} } go paramListener(paramControlChan, paramErrs) for i := range validations { go validateParamFunction(paramControlChan, paramErrs, validations[i]) } // wait for all the validations to complete <-paramFunctionControlChan if len(paramValidationErrors) > 0 { errorChan <- paramValidationErrors } // let runValidation know we are done with this part. controlChan <- struct{}{} } requestBodyValidationFunc := func(control chan struct{}, errorChan chan []*errors.ValidationError) { valid, pErrs := reqBodyValidator.ValidateRequestBodyWithPathItem(request, pathItem, pathValue) if !valid { errorChan <- pErrs } control <- struct{}{} } // build async functions asyncFunctions := []validationFunctionAsync{ parameterValidationFunc, requestBodyValidationFunc, } var validationErrors []*errors.ValidationError // sit and wait for everything to report back. go runValidation(controlChan, doneChan, errChan, &validationErrors, len(asyncFunctions)) // run async functions for i := range asyncFunctions { go asyncFunctions[i](controlChan, errChan) } // wait for all the validations to complete <-doneChan // sort errors for deterministic ordering (async validation can return errors in any order) sortValidationErrors(validationErrors) return len(validationErrors) == 0, validationErrors } func (v *validator) ValidateHttpRequestSync(request *http.Request) (bool, []*errors.ValidationError) { pathItem, errs, foundPath := paths.FindPath(request, v.v3Model, v.options) if len(errs) > 0 { return false, errs } return v.ValidateHttpRequestSyncWithPathItem(request, pathItem, foundPath) } func (v *validator) ValidateHttpRequestSyncWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { // create a new parameter validator paramValidator := v.paramValidator // create a new request body validator reqBodyValidator := v.requestValidator validationErrors := make([]*errors.ValidationError, 0) paramValidationErrors := make([]*errors.ValidationError, 0) for _, validateFunc := range []validationFunction{ paramValidator.ValidatePathParamsWithPathItem, paramValidator.ValidateCookieParamsWithPathItem, paramValidator.ValidateHeaderParamsWithPathItem, paramValidator.ValidateQueryParamsWithPathItem, paramValidator.ValidateSecurityWithPathItem, } { valid, pErrs := validateFunc(request, pathItem, pathValue) if !valid { paramValidationErrors = append(paramValidationErrors, pErrs...) } } valid, pErrs := reqBodyValidator.ValidateRequestBodyWithPathItem(request, pathItem, pathValue) if !valid { paramValidationErrors = append(paramValidationErrors, pErrs...) } validationErrors = append(validationErrors, paramValidationErrors...) return len(validationErrors) == 0, validationErrors } type validator struct { options *config.ValidationOptions v3Model *v3.Document document libopenapi.Document paramValidator parameters.ParameterValidator requestValidator requests.RequestBodyValidator responseValidator responses.ResponseBodyValidator } func runValidation(control, doneChan chan struct{}, errorChan chan []*errors.ValidationError, validationErrors *[]*errors.ValidationError, total int, ) { var validationLock sync.Mutex completedValidations := 0 for { select { case vErrs := <-errorChan: validationLock.Lock() *validationErrors = append(*validationErrors, vErrs...) validationLock.Unlock() case <-control: completedValidations++ if completedValidations == total { doneChan <- struct{}{} return } } } } type ( validationFunction func(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) validationFunctionAsync func(control chan struct{}, errorChan chan []*errors.ValidationError) ) // sortValidationErrors sorts validation errors for deterministic ordering. // Errors are sorted by validation type first, then by message. func sortValidationErrors(errs []*errors.ValidationError) { sort.Slice(errs, func(i, j int) bool { if errs[i].ValidationType != errs[j].ValidationType { return errs[i].ValidationType < errs[j].ValidationType } return errs[i].Message < errs[j].Message }) } // warmSchemaCaches pre-compiles all schemas in the OpenAPI document and stores them in the validator caches. // This frontloads the compilation cost so that runtime validation doesn't need to compile schemas. func warmSchemaCaches( doc *v3.Document, options *config.ValidationOptions, ) { // Skip warming if cache is nil (explicitly disabled via WithSchemaCache(nil)) if doc == nil || doc.Paths == nil || doc.Paths.PathItems == nil || options.SchemaCache == nil { return } schemaCache := options.SchemaCache // Walk through all paths and operations for pathPair := doc.Paths.PathItems.First(); pathPair != nil; pathPair = pathPair.Next() { pathItem := pathPair.Value() // Get all operations for this path (handles all HTTP methods including OpenAPI 3.2+ extensions) operations := pathItem.GetOperations() if operations == nil { continue } for opPair := operations.First(); opPair != nil; opPair = opPair.Next() { operation := opPair.Value() if operation == nil { continue } // Warm request body schemas if operation.RequestBody != nil && operation.RequestBody.Content != nil { for contentPair := operation.RequestBody.Content.First(); contentPair != nil; contentPair = contentPair.Next() { mediaType := contentPair.Value() if mediaType.Schema != nil { warmMediaTypeSchema(mediaType, schemaCache, options) } } } // Warm response body schemas if operation.Responses != nil { // Warm status code responses if operation.Responses.Codes != nil { for codePair := operation.Responses.Codes.First(); codePair != nil; codePair = codePair.Next() { response := codePair.Value() if response != nil && response.Content != nil { for contentPair := response.Content.First(); contentPair != nil; contentPair = contentPair.Next() { mediaType := contentPair.Value() if mediaType.Schema != nil { warmMediaTypeSchema(mediaType, schemaCache, options) } } } } } // Warm default response schemas if operation.Responses.Default != nil && operation.Responses.Default.Content != nil { for contentPair := operation.Responses.Default.Content.First(); contentPair != nil; contentPair = contentPair.Next() { mediaType := contentPair.Value() if mediaType.Schema != nil { warmMediaTypeSchema(mediaType, schemaCache, options) } } } } // Warm parameter schemas if operation.Parameters != nil { for _, param := range operation.Parameters { if param != nil { warmParameterSchema(param, schemaCache, options) } } } } // Warm path-level parameters if pathItem.Parameters != nil { for _, param := range pathItem.Parameters { if param != nil { warmParameterSchema(param, schemaCache, options) } } } } } // warmMediaTypeSchema warms the cache for a media type schema func warmMediaTypeSchema(mediaType *v3.MediaType, schemaCache cache.SchemaCache, options *config.ValidationOptions) { if mediaType != nil && mediaType.Schema != nil { hash := mediaType.GoLow().Schema.Value.Hash() if _, exists := schemaCache.Load(hash); !exists { schema := mediaType.Schema.Schema() if schema != nil { renderCtx := base.NewInlineRenderContextForValidation() renderedInline, _ := schema.RenderInlineWithContext(renderCtx) referenceSchema := string(renderedInline) renderedJSON, _ := utils.ConvertYAMLtoJSON(renderedInline) if len(renderedInline) > 0 { compiledSchema, _ := helpers.NewCompiledSchema(fmt.Sprintf("%x", hash), renderedJSON, options) // Pre-parse YAML node for error reporting (avoids re-parsing on each error) var renderedNode yaml.Node _ = yaml.Unmarshal(renderedInline, &renderedNode) schemaCache.Store(hash, &cache.SchemaCacheEntry{ Schema: schema, RenderedInline: renderedInline, ReferenceSchema: referenceSchema, RenderedJSON: renderedJSON, CompiledSchema: compiledSchema, RenderedNode: &renderedNode, }) } } } } } // warmParameterSchema warms the cache for a parameter schema func warmParameterSchema(param *v3.Parameter, schemaCache cache.SchemaCache, options *config.ValidationOptions) { if param != nil { var schema *base.Schema var hash uint64 // Parameters can have schemas in two places: schema property or content property if param.Schema != nil { schema = param.Schema.Schema() if schema != nil { hash = param.GoLow().Schema.Value.Hash() } } else if param.Content != nil { // Check content for schema for contentPair := param.Content.First(); contentPair != nil; contentPair = contentPair.Next() { mediaType := contentPair.Value() if mediaType.Schema != nil { schema = mediaType.Schema.Schema() if schema != nil { hash = mediaType.GoLow().Schema.Value.Hash() } break // Only process first content type } } } if schema != nil { if _, exists := schemaCache.Load(hash); !exists { renderCtx := base.NewInlineRenderContextForValidation() renderedInline, _ := schema.RenderInlineWithContext(renderCtx) referenceSchema := string(renderedInline) renderedJSON, _ := utils.ConvertYAMLtoJSON(renderedInline) if len(renderedInline) > 0 { compiledSchema, _ := helpers.NewCompiledSchema(fmt.Sprintf("%x", hash), renderedJSON, options) // Pre-parse YAML node for error reporting (avoids re-parsing on each error) var renderedNode yaml.Node _ = yaml.Unmarshal(renderedInline, &renderedNode) // Store in cache using the shared SchemaCache type schemaCache.Store(hash, &cache.SchemaCacheEntry{ Schema: schema, RenderedInline: renderedInline, ReferenceSchema: referenceSchema, RenderedJSON: renderedJSON, CompiledSchema: compiledSchema, RenderedNode: &renderedNode, }) } } } } } libopenapi-validator-0.13.8/validator_examples_test.go000066400000000000000000000262721520534042400231710ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package validator import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi-validator/helpers" ) func ExampleNewValidator_validateDocument() { // 1. Load the OpenAPI 3+ spec into a byte array petstore, err := os.ReadFile("test_specs/invalid_31.yaml") if err != nil { panic(err) } // 2. Create a new OpenAPI document using libopenapi document, docErrs := libopenapi.NewDocument(petstore) if docErrs != nil { panic(docErrs) } // 3. Create a new validator docValidator, validatorErrs := NewValidator(document) if validatorErrs != nil { panic(validatorErrs) } // 4. Validate! valid, validationErrs := docValidator.ValidateDocument() if !valid { for i, e := range validationErrs { // 5. Handle the error fmt.Printf("%d: Type: %s, Failure: %s\n", i, e.ValidationType, e.Message) fmt.Printf("Fix: %s\n\n", e.HowToFix) } } // Output: 0: Type: schema, Failure: Document does not pass validation // Fix: Ensure that the object being submitted, matches the schema correctly } func ExampleNewValidator_validateHttpRequest() { // 1. Load the OpenAPI 3+ spec into a byte array petstore, err := os.ReadFile("test_specs/petstorev3.json") if err != nil { panic(err) } // 2. Create a new OpenAPI document using libopenapi document, docErrs := libopenapi.NewDocument(petstore) if docErrs != nil { panic(docErrs) } // 3. Create a new validator docValidator, validatorErrs := NewValidator(document) if validatorErrs != nil { panic(validatorErrs) } // 4. Create a new *http.Request (normally, this would be where the host application will pass in the request) // Note: /pet/{petId} requires api_key OR petstore_auth (OAuth2). Since OAuth2 is not validated, // the security check passes. The path parameter validation fails because "NotAValidPetId" is not an integer. request, _ := http.NewRequest(http.MethodGet, "/pet/NotAValidPetId", nil) // 5. Validate! valid, validationErrs := docValidator.ValidateHttpRequest(request) if !valid { for _, e := range validationErrs { // 5. Handle the error fmt.Printf("Type: %s, Failure: %s\n", e.ValidationType, e.Message) } } // Output: Type: parameter, Failure: Path parameter 'petId' is not a valid integer } func ExampleNewValidator_validateHttpRequestSync() { // 1. Load the OpenAPI 3+ spec into a byte array petstore, err := os.ReadFile("test_specs/petstorev3.json") if err != nil { panic(err) } // 2. Create a new OpenAPI document using libopenapi document, docErrs := libopenapi.NewDocument(petstore) if docErrs != nil { panic(docErrs) } // 3. Create a new validator docValidator, validatorErrs := NewValidator(document) if validatorErrs != nil { panic(validatorErrs) } // 4. Create a new *http.Request (normally, this would be where the host application will pass in the request) request, _ := http.NewRequest(http.MethodGet, "/pet/NotAValidPetId", nil) // 5. Validate! valid, validationErrs := docValidator.ValidateHttpRequestSync(request) if !valid { for _, e := range validationErrs { // 5. Handle the error fmt.Printf("Type: %s, Failure: %s\n", e.ValidationType, e.Message) } } // Output: Type: parameter, Failure: Path parameter 'petId' is not a valid integer } func ExampleNewValidator_validateHttpRequestResponse() { // 1. Load the OpenAPI 3+ spec into a byte array petstore, err := os.ReadFile("test_specs/petstorev3.json") if err != nil { panic(err) } // 2. Create a new OpenAPI document using libopenapi document, docErrs := libopenapi.NewDocument(petstore) if docErrs != nil { panic(docErrs) } // 3. Create a new validator docValidator, validatorErrs := NewValidator(document) if validatorErrs != nil { panic(validatorErrs) } // 6. Create a new *http.Request (normally, this would be where the host application will pass in the request) request, _ := http.NewRequest(http.MethodGet, "/pet/findByStatus?status=sold", nil) // 7. Simulate a request/response, in this case the contract returns a 200 with an array of pets. // Normally, this would be where the host application would pass in the response. recorder := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { // set return content type. w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) // create a Pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": "NotAValidPetId", // this will fail, it should be an integer. "name": "dogs", }, "photoUrls": []string{"https://pb33f.io"}, } // marshal the request body into bytes. responseBodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets // return the response. _, _ = w.Write(responseBodyBytes) } // simulate request/response handler(recorder, request) // 7. Validate! valid, validationErrs := docValidator.ValidateHttpRequestResponse(request, recorder.Result()) if !valid { for _, e := range validationErrs { // 5. Handle the error fmt.Printf("Type: %s, Failure: %s\n", e.ValidationType, e.Message) fmt.Printf("Schema Error: %s, Line: %d, Col: %d\n", e.SchemaValidationErrors[0].Reason, e.SchemaValidationErrors[0].Line, e.SchemaValidationErrors[0].Column) } } // Output: Type: response, Failure: 200 response body for '/pet/findByStatus' failed to validate schema // Schema Error: got string, want integer, Line: 19, Col: 27 } func ExampleNewValidator_validateHttpResponse() { // 1. Load the OpenAPI 3+ spec into a byte array petstore, err := os.ReadFile("test_specs/petstorev3.json") if err != nil { panic(err) } // 2. Create a new OpenAPI document using libopenapi document, docErrs := libopenapi.NewDocument(petstore) if docErrs != nil { panic(docErrs) } // 3. Create a new validator docValidator, validatorErrs := NewValidator(document) if validatorErrs != nil { panic(validatorErrs) } // 6. Create a new *http.Request (normally, this would be where the host application will pass in the request) request, _ := http.NewRequest(http.MethodGet, "/pet/findByStatus?status=sold", nil) // 7. Simulate a request/response, in this case the contract returns a 200 with an array of pets. // Normally, this would be where the host application would pass in the response. recorder := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { // set return content type. w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) // create a Pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": "NotAValidPetId", // this will fail, it should be an integer. "name": "dogs", }, "photoUrls": []string{"https://pb33f.io"}, } // marshal the request body into bytes. responseBodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets // return the response. _, _ = w.Write(responseBodyBytes) } // simulate request/response handler(recorder, request) // 7. Validate the response only valid, validationErrs := docValidator.ValidateHttpResponse(request, recorder.Result()) if !valid { for _, e := range validationErrs { // 5. Handle the error fmt.Printf("Type: %s, Failure: %s\n", e.ValidationType, e.Message) fmt.Printf("Schema Error: %s, Line: %d, Col: %d\n", e.SchemaValidationErrors[0].Reason, e.SchemaValidationErrors[0].Line, e.SchemaValidationErrors[0].Column) } } // Output: Type: response, Failure: 200 response body for '/pet/findByStatus' failed to validate schema // Schema Error: got string, want integer, Line: 19, Col: 27 } func ExampleNewValidator_testResponseHeaders() { // 1. Load the OpenAPI 3+ spec into a byte array petstore := []byte(`openapi: "3.0.0" info: title: Healthcheck version: '0.1.0' paths: /health: get: responses: '200': headers: chicken-nuggets: description: chicken nuggets response required: true schema: type: integer description: pet response`) // 2. Create a new OpenAPI document using libopenapi document, docErrs := libopenapi.NewDocument(petstore) if docErrs != nil { panic(docErrs) } // 3. Create a new validator docValidator, validatorErrs := NewValidator(document) if validatorErrs != nil { panic(validatorErrs) } // 6. Create a new *http.Request (normally, this would be where the host application will pass in the request) request, _ := http.NewRequest(http.MethodGet, "/health", nil) // 7. Simulate a request/response, in this case the contract returns a 200 with an array of pets. // Normally, this would be where the host application would pass in the response. recorder := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { // set return content type. w.Header().Set("Chicken-Nuggets", "I am a chicken nugget, and not an integer") w.WriteHeader(http.StatusOK) _, _ = w.Write(nil) } // simulate request/response handler(recorder, request) // 7. Validate the response only valid, validationErrs := docValidator.ValidateHttpResponse(request, recorder.Result()) if !valid { for _, e := range validationErrs { // 5. Handle the error fmt.Printf("Type: %s, Failure: %s\n", e.ValidationType, e.Message) } } // Output: Type: response, Failure: header 'chicken-nuggets' failed to validate } func ExampleNewValidator_responseHeaderNotRequired() { // 1. Load the OpenAPI 3+ spec into a byte array petstore := []byte(`openapi: "3.0.0" info: title: Healthcheck version: '0.1.0' paths: /health: get: responses: '200': headers: chicken-nuggets: description: chicken nuggets response required: false schema: type: integer description: pet response`) // 2. Create a new OpenAPI document using libopenapi document, docErrs := libopenapi.NewDocument(petstore) if docErrs != nil { panic(docErrs) } // 3. Create a new validator docValidator, validatorErrs := NewValidator(document) if validatorErrs != nil { panic(validatorErrs) } // 6. Create a new *http.Request (normally, this would be where the host application will pass in the request) request, _ := http.NewRequest(http.MethodGet, "/health", nil) // 7. Simulate a request/response, in this case the contract returns a 200 with an array of pets. // Normally, this would be where the host application would pass in the response. recorder := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { // set return content type. w.Header().Set("Chicken-Nuggets", "I am a chicken nugget, and not an integer") w.WriteHeader(http.StatusOK) _, _ = w.Write(nil) } // simulate request/response handler(recorder, request) // 7. Validate the response only valid, _ := docValidator.ValidateHttpResponse(request, recorder.Result()) if !valid { panic("the header is not required, it should not fail") } fmt.Println("Header is not required, validation passed") // Output: Header is not required, validation passed } libopenapi-validator-0.13.8/validator_nullable_enum_test.go000066400000000000000000000240451520534042400241710ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package validator import ( "bytes" "encoding/json" "io" "net/http" "os" "testing" "github.com/pb33f/libopenapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestNullableEnum_ResponseValidation_NullValue tests that nullable enum fields // accept null values even when null is not explicitly in the enum definition func TestNullableEnum_ResponseValidation_NullValue(t *testing.T) { spec, err := os.ReadFile("test_specs/nullable_enum.yaml") require.NoError(t, err) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) // Test response with null status (enum doesn't explicitly contain null) responseBody := map[string]interface{}{ "id": 1, "status": nil, // null value for nullable enum } body, _ := json.Marshal(responseBody) request, _ := http.NewRequest(http.MethodGet, "https://example.com/status", nil) response := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(body)), Request: request, } valid, validationErrs := v.ValidateHttpResponse(request, response) assert.True(t, valid, "Response should be valid with null enum value") assert.Empty(t, validationErrs, "Should have no validation errors") } // TestNullableEnum_ResponseValidation_EnumValue tests that nullable enum fields // accept valid enum values func TestNullableEnum_ResponseValidation_EnumValue(t *testing.T) { spec, err := os.ReadFile("test_specs/nullable_enum.yaml") require.NoError(t, err) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) // Test response with valid enum value responseBody := map[string]interface{}{ "id": 1, "status": "active", // valid enum value } body, _ := json.Marshal(responseBody) request, _ := http.NewRequest(http.MethodGet, "https://example.com/status", nil) response := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(body)), Request: request, } valid, validationErrs := v.ValidateHttpResponse(request, response) assert.True(t, valid, "Response should be valid with enum value") assert.Empty(t, validationErrs, "Should have no validation errors") } // TestNullableEnum_ResponseValidation_InvalidEnumValue tests that nullable enum fields // reject invalid enum values func TestNullableEnum_ResponseValidation_InvalidEnumValue(t *testing.T) { spec, err := os.ReadFile("test_specs/nullable_enum.yaml") require.NoError(t, err) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) // Test response with invalid enum value responseBody := map[string]interface{}{ "id": 1, "status": "invalid_status", // invalid enum value } body, _ := json.Marshal(responseBody) request, _ := http.NewRequest(http.MethodGet, "https://example.com/status", nil) response := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(body)), Request: request, } valid, validationErrs := v.ValidateHttpResponse(request, response) assert.False(t, valid, "Response should be invalid with non-enum value") assert.NotEmpty(t, validationErrs, "Should have validation errors") } // TestNullableEnum_ResponseValidation_PriorityWithNullInEnum tests enum that // already has null in the enum definition func TestNullableEnum_ResponseValidation_PriorityWithNullInEnum(t *testing.T) { spec, err := os.ReadFile("test_specs/nullable_enum.yaml") require.NoError(t, err) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) // Test response with null priority (enum explicitly contains null) responseBody := map[string]interface{}{ "id": 1, "priority": nil, // null value for nullable enum (null already in enum) } body, _ := json.Marshal(responseBody) request, _ := http.NewRequest(http.MethodGet, "https://example.com/status", nil) response := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(body)), Request: request, } valid, validationErrs := v.ValidateHttpResponse(request, response) assert.True(t, valid, "Response should be valid with null enum value") assert.Empty(t, validationErrs, "Should have no validation errors") } // TestNullableEnum_ResponseValidation_NonNullableEnum tests that non-nullable // enum fields reject null values func TestNullableEnum_ResponseValidation_NonNullableEnum(t *testing.T) { spec, err := os.ReadFile("test_specs/nullable_enum.yaml") require.NoError(t, err) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) // Test response with null category (non-nullable enum) responseBody := map[string]interface{}{ "id": 1, "category": nil, // null value for NON-nullable enum } body, _ := json.Marshal(responseBody) request, _ := http.NewRequest(http.MethodGet, "https://example.com/status", nil) response := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(body)), Request: request, } valid, validationErrs := v.ValidateHttpResponse(request, response) assert.False(t, valid, "Response should be invalid with null for non-nullable enum") assert.NotEmpty(t, validationErrs, "Should have validation errors") } // TestNullableEnum_RequestValidation_NullValue tests that nullable enum fields // accept null values in request body func TestNullableEnum_RequestValidation_NullValue(t *testing.T) { spec, err := os.ReadFile("test_specs/nullable_enum.yaml") require.NoError(t, err) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) // Test request with null status requestBody := map[string]interface{}{ "status": nil, // null value for nullable enum } body, _ := json.Marshal(requestBody) request, _ := http.NewRequest(http.MethodPost, "https://example.com/status", bytes.NewBuffer(body)) request.Header.Set("Content-Type", "application/json") valid, validationErrs := v.ValidateHttpRequest(request) assert.True(t, valid, "Request should be valid with null enum value") assert.Empty(t, validationErrs, "Should have no validation errors") } // TestNullableEnum_ArrayResponse tests nullable enum in array items func TestNullableEnum_ArrayResponse(t *testing.T) { spec, err := os.ReadFile("test_specs/nullable_enum.yaml") require.NoError(t, err) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) // Test array response with nullable enum responseBody := []map[string]interface{}{ { "id": 1, "name": "Item 1", "status": "available", }, { "id": 2, "name": "Item 2", "status": nil, // null value for nullable enum in array }, { "id": 3, "name": "Item 3", "status": "sold", }, } body, _ := json.Marshal(responseBody) request, _ := http.NewRequest(http.MethodGet, "https://example.com/items", nil) response := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(body)), Request: request, } valid, validationErrs := v.ValidateHttpResponse(request, response) assert.True(t, valid, "Response should be valid with null enum value in array") assert.Empty(t, validationErrs, "Should have no validation errors") } // TestNullableEnum_NestedObject tests nullable enum in nested object func TestNullableEnum_NestedObject(t *testing.T) { spec, err := os.ReadFile("test_specs/nullable_enum.yaml") require.NoError(t, err) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) // Test response with deeply nested nullable enum responseBody := []map[string]interface{}{ { "id": 1, "name": "Item 1", "metadata": map[string]interface{}{ "visibility": nil, // null value for deeply nested nullable enum }, }, } body, _ := json.Marshal(responseBody) request, _ := http.NewRequest(http.MethodGet, "https://example.com/items", nil) response := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(body)), Request: request, } valid, validationErrs := v.ValidateHttpResponse(request, response) assert.True(t, valid, "Response should be valid with null enum value in nested object") assert.Empty(t, validationErrs, "Should have no validation errors") } // TestNullableEnum_MultipleNullableFields tests response with multiple nullable enum fields func TestNullableEnum_MultipleNullableFields(t *testing.T) { spec, err := os.ReadFile("test_specs/nullable_enum.yaml") require.NoError(t, err) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) // Test response with multiple nullable fields set to null responseBody := map[string]interface{}{ "id": 1, "status": nil, // null for status (enum doesn't have null) "priority": nil, // null for priority (enum has null) } body, _ := json.Marshal(responseBody) request, _ := http.NewRequest(http.MethodGet, "https://example.com/status", nil) response := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(body)), Request: request, } valid, validationErrs := v.ValidateHttpResponse(request, response) assert.True(t, valid, "Response should be valid with multiple null enum values") assert.Empty(t, validationErrs, "Should have no validation errors") } libopenapi-validator-0.13.8/validator_test.go000066400000000000000000002360031520534042400212660ustar00rootroot00000000000000// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package validator import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "strings" "sync" "testing" "unicode" "github.com/dlclark/regexp2" "github.com/pb33f/libopenapi" "github.com/santhosh-tekuri/jsonschema/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" ) func TestNewValidator(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) assert.NotNil(t, v.GetParameterValidator()) assert.NotNil(t, v.GetResponseBodyValidator()) assert.NotNil(t, v.GetRequestBodyValidator()) } func TestNewValidator_concurrent(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean /burgers/createBurger/{burgerId}: post: parameters: - in: path name: burgerId required: true schema: type: string requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, err := libopenapi.NewDocument([]byte(spec)) assert.Empty(t, err) v, _ := NewValidator(doc) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequest(request) assert.True(t, valid) assert.Len(t, errors, 0) }() go func() { defer wg.Done() request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger/toto", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequest(request) assert.True(t, valid) assert.Len(t, errors, 0) }() wg.Wait() } func TestNewValidator_ValidateDocument(t *testing.T) { doc, _ := libopenapi.NewDocument(petstoreBytes) v, _ := NewValidator(doc) valid, errs := v.ValidateDocument() assert.True(t, valid) assert.Len(t, errs, 0) } type dlclarkRegexp regexp2.Regexp func (re *dlclarkRegexp) MatchString(s string) bool { matched, err := (*regexp2.Regexp)(re).MatchString(s) return err == nil && matched } func (re *dlclarkRegexp) String() string { return (*regexp2.Regexp)(re).String() } func dlclarkCompile(s string) (jsonschema.Regexp, error) { re, err := regexp2.Compile(s, regexp2.ECMAScript) if err != nil { return nil, err } return (*dlclarkRegexp)(re), nil } func TestNewValidator_WithRegex(t *testing.T) { doc, err := libopenapi.NewDocument(petstoreBytes) require.Nil(t, err, "Failed to load spec") v, errs := NewValidator(doc, config.WithRegexEngine(dlclarkCompile)) require.Empty(t, errs, "Failed to build validator") require.NotNil(t, v, "Failed to build validator") valid, valErrs := v.ValidateDocument() assert.True(t, valid) assert.Empty(t, valErrs) } func TestNewValidator_WithCustomFormat_NoErrors(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string format: capital patties: type: integer vegetarian: type: boolean` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err, "Failed to load spec") require.NotNil(t, doc, "Failed to load spec") v, errs := NewValidator( doc, config.WithFormatAssertions(), config.WithCustomFormat("capital", func(v any) error { s, ok := v.(string) if !ok { return fmt.Errorf("expected string") } if s == "" { return nil } r := []rune(s)[0] if !unicode.IsUpper(r) { return fmt.Errorf("expected first latter to be uppercase") } return nil }), ) require.Empty(t, errs, "Failed to build validator") require.NotNil(t, v, "Failed to build validator") body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequest(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestStrictMode_ValidateHttpRequestIntegration(t *testing.T) { spec := `openapi: 3.1.0 paths: /things/{id}: post: parameters: - in: path name: id required: true schema: type: string - in: query name: q schema: type: string - in: header name: X-Known schema: type: string - in: cookie name: session schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string responses: "200": description: ok content: application/json: schema: type: object properties: ok: type: boolean` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) v, errs := NewValidator(doc, config.WithStrictMode()) require.Empty(t, errs) body := map[string]any{ "name": "ok", "extra": "nope", } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/things/123?q=ok&extra=1", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") request.Header.Set("X-Known", "known") request.Header.Set("X-Extra", "nope") request.AddCookie(&http.Cookie{Name: "session", Value: "ok"}) request.AddCookie(&http.Cookie{Name: "other", Value: "nope"}) valid, valErrs := v.ValidateHttpRequest(request) assert.False(t, valid) strictSubTypes := make(map[string]bool) for _, vErr := range valErrs { if vErr.ValidationType == errors.StrictValidationType { strictSubTypes[vErr.ValidationSubType] = true } } assert.True(t, strictSubTypes[errors.StrictSubTypeProperty]) assert.True(t, strictSubTypes[errors.StrictSubTypeHeader]) assert.True(t, strictSubTypes[errors.StrictSubTypeQuery]) assert.True(t, strictSubTypes[errors.StrictSubTypeCookie]) } func TestStrictMode_ValidateHttpResponseHeadersIntegration(t *testing.T) { spec := `openapi: 3.1.0 paths: /things/{id}: get: parameters: - in: path name: id required: true schema: type: string responses: "200": description: ok headers: X-Res: schema: type: string content: application/json: schema: type: object properties: ok: type: boolean` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) v, errs := NewValidator(doc, config.WithStrictMode()) require.Empty(t, errs) request, _ := http.NewRequest(http.MethodGet, "https://things.com/things/123", http.NoBody) body := map[string]any{"ok": true} bodyBytes, _ := json.Marshal(body) response := &http.Response{ StatusCode: 200, Header: http.Header{ "Content-Type": {"application/json"}, "X-Res": {"ok"}, "X-Extra": {"nope"}, }, Body: io.NopCloser(bytes.NewBuffer(bodyBytes)), } valid, valErrs := v.ValidateHttpResponse(request, response) assert.False(t, valid) foundStrictHeader := false for _, vErr := range valErrs { if vErr.ValidationType == errors.StrictValidationType && vErr.ValidationSubType == errors.StrictSubTypeHeader { foundStrictHeader = true break } } assert.True(t, foundStrictHeader) } func TestNewValidator_WithCustomFormat_FormatError(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string format: capital patties: type: integer vegetarian: type: boolean` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err, "Failed to load spec") require.NotNil(t, doc, "Failed to load spec") v, errs := NewValidator( doc, config.WithFormatAssertions(), config.WithCustomFormat("capital", func(v any) error { s, ok := v.(string) if !ok { return fmt.Errorf("expected string") } if s == "" { return nil } r := []rune(s)[0] if !unicode.IsUpper(r) { return fmt.Errorf("expected first latter to be uppercase") } return nil }), ) require.Empty(t, errs, "Failed to build validator") require.NotNil(t, v, "Failed to build validator") v.GetRequestBodyValidator() body := map[string]interface{}{ "name": "big mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequest(request) assert.False(t, valid) require.Len(t, errors, 1) assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message) require.Len(t, errors[0].SchemaValidationErrors, 1) require.NotNil(t, errors[0].SchemaValidationErrors[0]) assert.Equal(t, "/properties/name/format", errors[0].SchemaValidationErrors[0].KeywordLocation) assert.Equal(t, "'big mac' is not valid capital: expected first latter to be uppercase", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_WithCustomFormat_NoErrorsWhenFormatAssertionDisabled(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string format: capital patties: type: integer vegetarian: type: boolean` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err, "Failed to load spec") require.NotNil(t, doc, "Failed to load spec") v, errs := NewValidator( doc, config.WithCustomFormat("capital", func(v any) error { s, ok := v.(string) if !ok { return fmt.Errorf("expected string") } if s == "" { return nil } r := []rune(s)[0] if !unicode.IsUpper(r) { return fmt.Errorf("expected first latter to be uppercase") } return nil }), ) require.Empty(t, errs, "Failed to build validator") require.NotNil(t, v, "Failed to build validator") v.GetRequestBodyValidator() body := map[string]interface{}{ "name": "big mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequest(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_BadDoc(t *testing.T) { spec := `swagger: 2.0` doc, _ := libopenapi.NewDocument([]byte(spec)) _, errs := NewValidator(doc) assert.Len(t, errs, 1) } func TestNewValidator_ValidateHttpRequest_BadPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/I am a potato man", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequest(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST Path '/I am a potato man' not found", errors[0].Message) } func TestNewValidator_ValidateHttpRequestSync_BadPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/I am a potato man", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequestSync(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST Path '/I am a potato man' not found", errors[0].Message) } func TestNewValidator_ValidateHttpRequest_ValidPostSimpleSchema(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequest(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_ValidateHttpRequestSync_ValidPostSimpleSchema(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequestSync(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_ValidateHttpRequestSync_ValidPostSimpleSchema_FoundPath(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequestSyncWithPathItem(request, &v3.PathItem{ Post: &v3.Operation{}, }, "/burgers/createBurger") assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_slash_server_url(t *testing.T) { spec := `openapi: 3.1.0 servers: - url: / paths: /burgers/{burgerId}/locate: patch: operationId: locateBurger parameters: - name: burgerId in: path required: true schema: type: string format: uuid ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/edd0189c-420b-489c-98f2-0facc5a26f3a/locate", nil) v, _ := NewValidator(doc) valid, errors := v.ValidateHttpRequest(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_ValidateHttpRequest_SetPath_ValidPostSimpleSchema(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequest(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_ValidateHttpRequestSync_SetPath_ValidPostSimpleSchema(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, "vegetarian": true, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequestSync(request) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_ValidateHttpRequest_InvalidPostSchema(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", "patties": false, // wrong. "vegetarian": false, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequest(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "got boolean, want integer", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_ValidateHttpRequestSync_InvalidPostSchema(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", "patties": false, // wrong. "vegetarian": false, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequestSync(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "got boolean, want integer", errors[0].SchemaValidationErrors[0].Reason) } func TestNewValidator_ValidateHttpRequest_InvalidQuery(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: parameters: - in: query name: cheese required: true schema: type: string post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, // wrong. "vegetarian": false, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequest(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Query parameter 'cheese' is missing", errors[0].Message) } func TestNewValidator_ValidateHttpRequestSync_InvalidQuery(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers/createBurger: parameters: - in: query name: cheese required: true schema: type: string post: requestBody: content: application/json: schema: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean` doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) body := map[string]interface{}{ "name": "Big Mac", "patties": 2, // wrong. "vegetarian": false, } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, errors := v.ValidateHttpRequestSync(request) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Query parameter 'cheese' is missing", errors[0].Message) } var petstoreBytes []byte func init() { petstoreBytes, _ = os.ReadFile("test_specs/petstorev3.json") } func TestNewValidator_PetStore_MissingContentType(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": 123, "name": "dogs", }, "photoUrls": []string{"https://example.com"}, } // marshal the body into bytes. bodyBytes, _ := json.Marshal(body) // create a new put request request, _ := http.NewRequest(http.MethodPut, "https://hyperspace-superherbs.com/pet", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, "application/not-json") w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response (should be clean) valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) // should all be perfectly valid. assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "PUT / 200 operation response content type 'application/not-json' does not exist", errors[0].Message) assert.Equal(t, "The content type 'application/not-json' of the PUT response received "+ "has not been defined, it's an unknown type", errors[0].Reason) } func TestNewValidator_PetStore_PetPost200_Valid_PathPreset(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": 123, "name": "dogs", }, "photoUrls": []string{"https://example.com"}, } // marshal the body into bytes. bodyBytes, _ := json.Marshal(body) // create a new put request request, _ := http.NewRequest(http.MethodPut, "https://hyperspace-superherbs.com/pet", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response (should be clean) valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) // should all be perfectly valid. assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_PetStore_PetPost200_Valid(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": 123, "name": "dogs", }, "photoUrls": []string{"https://example.com"}, } // marshal the body into bytes. bodyBytes, _ := json.Marshal(body) // create a new put request request, _ := http.NewRequest(http.MethodPut, "https://hyperspace-superherbs.com/pet", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response (should be clean) valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) // should all be perfectly valid. assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_PetStore_PetPost200_Invalid(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a pet, but is missing the photoUrls field body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": 123, "name": "dogs", }, } // marshal the body into bytes. bodyBytes, _ := json.Marshal(body) // create a new put request request, _ := http.NewRequest(http.MethodPost, "https://hyperspace-superherbs.com/pet", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusProxyAuthRequired) // this is not defined by the contract, so it should fail. _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) // we have a schema violation, and a response code violation, our validator should have picked them // both up. assert.False(t, valid) assert.Len(t, errors, 2) // check errors for i := range errors { if errors[i].SchemaValidationErrors != nil { assert.Equal(t, "missing property 'photoUrls'", errors[i].SchemaValidationErrors[0].Reason) } else { assert.Equal(t, "POST operation request response code '407' does not exist", errors[i].Message) } } } func TestNewValidator_PetStore_PetFindByStatusGet200_Valid(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": 123, "name": "dogs", }, "photoUrls": []string{"https://example.com"}, } // marshal the body into bytes. bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets // create a new put request request, _ := http.NewRequest(http.MethodGet, "https://hyperspace-superherbs.com/pet/findByStatus?status=sold", nil) request.Header.Set("Content-Type", "application/json") // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response (should be clean) valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) // should all be perfectly valid. assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_PetStore_PetFindByStatusGet200_BadEnum(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": 123, "name": "dogs", }, "photoUrls": []string{"https://example.com"}, } // marshal the body into bytes. bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets // create a new put request request, _ := http.NewRequest(http.MethodGet, "https://hyperspace-superherbs.com/pet/findByStatus?status=invalidEnum", nil) // enum is invalid request.Header.Set("Content-Type", "application/json") // simulate a request/response, in this case the contract returns a 200 with a pet res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.Header().Set("Herbs-And-Spice", helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response (should be clean) valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) // should all be perfectly valid. assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Query parameter 'status' does not match allowed values", errors[0].Message) assert.Equal(t, "Instead of 'invalidEnum', use one of the allowed values: 'available, pending, sold'", errors[0].HowToFix) } func TestNewValidator_PetStore_PetFindByTagsGet200_Valid(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": 123, "name": "dogs", }, "photoUrls": []string{"https://example.com"}, } // marshal the body into bytes. bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets // create a new put request request, _ := http.NewRequest(http.MethodGet, "https://hyperspace-superherbs.com/pet/findByTags?tags=fuzzy&tags=wuzzy", nil) request.Header.Set("Content-Type", "application/json") // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response (should be clean) valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) // should all be perfectly valid. assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_PetStore_PetFindByTagsGet200_InvalidExplode(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": 123, "name": "dogs", }, "photoUrls": []string{"https://example.com"}, } // marshal the body into bytes. bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets // create a new put request request, _ := http.NewRequest(http.MethodGet, "https://hyperspace-superherbs.com/pet/findByTags?tags=fuzzy,wuzzy", nil) request.Header.Set("Content-Type", "application/json") // simulate a request/response res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) // will fail. assert.False(t, valid) assert.Len(t, errors, 2) // will fire allow reserved error, and explode error. } func TestNewValidator_PetStore_PetGet200_Valid(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": 123, "name": "dogs", }, "photoUrls": []string{"https://example.com"}, } // marshal the body into bytes. bodyBytes, _ := json.Marshal(body) // operation returns pet // create a new put request request, _ := http.NewRequest(http.MethodGet, "https://hyperspace-superherbs.com/pet/12345", nil) request.Header.Set("Content-Type", "application/json") request.Header.Set("api_key", "12345") // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_PetStore_PetGet200_PathNotFound(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": 123, "name": "dogs", }, "photoUrls": []string{"https://example.com"}, } // marshal the body into bytes. bodyBytes, _ := json.Marshal(body) // operation returns pet // create a new put request request, _ := http.NewRequest(http.MethodGet, "https://hyperspace-superherbs.com/pet/IamNotANumber", nil) request.Header.Set("Content-Type", "application/json") // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) assert.False(t, valid) // Note: /pet/{petId} allows api_key OR petstore_auth (OAuth2). Since OAuth2 is not validated, // security passes. Only the path parameter validation fails. assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'petId' is not a valid integer", errors[0].Message) } func TestNewValidator_PetStore_PetGet200(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a new put request request, _ := http.NewRequest(http.MethodGet, "https://hyperspace-superherbs.com/pet/112233", nil) request.Header.Set("Content-Type", "application/json") request.Header.Set("api_key", "12345") // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) // create a pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": 123, "name": "dogs", }, "photoUrls": []string{"https://example.com"}, } // marshal the body into bytes. bodyBytes, _ := json.Marshal(body) // operation returns pet _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_PetStore_PetGet200_ServerBadMediaType(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a new put request request, _ := http.NewRequest(http.MethodGet, "https://hyperspace-superherbs.com/pet/112233", nil) request.Header.Set("Content-Type", "application/json") request.Header.Set("api_key", "12345") // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, "hot-diggity/coffee; charset=cakes") // wut? w.WriteHeader(http.StatusOK) // create a pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": 123, "name": "dogs", }, "photoUrls": []string{"https://example.com"}, } // marshal the body into bytes. bodyBytes, _ := json.Marshal(body) // operation returns pet _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "GET / 200 operation response content type 'hot-diggity/coffee' does not exist", errors[0].Message) } func TestNewValidator_PetStore_PetWithIdPost200_Missing200(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a new put request request, _ := http.NewRequest(http.MethodPost, "https://hyperspace-superherbs.com/pet/112233?name=peter&query=thing", nil) request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) } // fire the request handler(res, request) // validate the response valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST operation request response code '200' does not exist", errors[0].Message) } func TestNewValidator_PetStore_UploadImage200_InvalidRequestBodyType(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a new put request request, _ := http.NewRequest(http.MethodPost, "https://hyperspace-superherbs.com/pet/112233/uploadImage?additionalMetadata=blem", nil) request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) // forget to write an API response } // fire the request handler(res, request) // validate the response valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) assert.False(t, valid) assert.Len(t, errors, 1) } func TestNewValidator_PetStore_UploadImage200_Valid(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a new put request request, _ := http.NewRequest(http.MethodPost, "https://hyperspace-superherbs.com/pet/112233/uploadImage?additionalMetadata=blem", nil) request.Header.Set(helpers.ContentTypeHeader, "application/octet-stream") // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) // create an API response body := map[string]interface{}{ "code": 200, "type": "herbs", "message": "smoke them every day.", } // marshal the body into bytes. bodyBytes, _ := json.Marshal(body) // operation returns APIResponse _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_PetStore_UploadImage200_InvalidAPIResponse(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a new put request request, _ := http.NewRequest(http.MethodPost, "https://hyperspace-superherbs.com/pet/112233/uploadImage?additionalMetadata=blem", nil) request.Header.Set(helpers.ContentTypeHeader, "application/octet-stream") // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) // create an API response body := map[string]interface{}{ "code": 200, "type": false, "message": "smoke them every day.", } // marshal the body into bytes. bodyBytes, _ := json.Marshal(body) // operation returns APIResponse _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "200 response body for '/pet/112233/uploadImage' failed to validate schema", errors[0].Message) } func TestNewValidator_CareRequest_WrongContentType(t *testing.T) { careRequestBytes, _ := os.ReadFile("test_specs/care_request.yaml") doc, _ := libopenapi.NewDocument(careRequestBytes) // create a doc v, _ := NewValidator(doc) // create a new put request request, _ := http.NewRequest(http.MethodGet, "https://hyperspace-superherbs.com/requests/d4bc1a0c-c4ee-4be5-9281-26b1a041634d", nil) request.Header.Set("Content-Type", "application/json") // simulate a request/response, res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, "application/not-json") w.WriteHeader(http.StatusOK) // create a CareRequest body := map[string]interface{}{ "id": "d4bc1a0c-c4ee-4be5-9281-26b1a041634d", "status": "active", } // marshal the body into bytes. bodyBytes, _ := json.Marshal(body) _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "GET / 200 operation response content type 'application/not-json' does not exist", errors[0].Message) assert.Equal(t, "The content type 'application/not-json' "+ "of the GET response received has not been defined, it's an unknown type", errors[0].Reason) } func TestNewValidator_PetStore_InvalidPath_Response(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a new put request request, _ := http.NewRequest(http.MethodPost, "https://hyperspace-superherbs.com/missing", nil) request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) } // fire the request handler(res, request) // validate the response valid, errors := v.ValidateHttpResponse(request, res.Result()) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST Path '/missing' not found", errors[0].Message) } func TestNewValidator_PetStore_InvalidPath_RequestResponse(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a new put request with an invalid path request, _ := http.NewRequest(http.MethodPost, "https://hyperspace-superherbs.com/nonexistent", nil) request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) // simulate a request/response res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) } // fire the request handler(res, request) // validate both request and response - should fail because path not found valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST Path '/nonexistent' not found", errors[0].Message) } func TestNewValidator_PetStore_PetFindByStatusGet200_Valid_responseOnly(t *testing.T) { // create a new document from the petstore spec doc, _ := libopenapi.NewDocument(petstoreBytes) // create a doc v, _ := NewValidator(doc) // create a pet body := map[string]interface{}{ "id": 123, "name": "cotton", "category": map[string]interface{}{ "id": 123, "name": "dogs", }, "photoUrls": []string{"https://example.com"}, } // marshal the body into bytes. bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets // create a new put request request, _ := http.NewRequest(http.MethodGet, "https://hyperspace-superherbs.com/pet/findByStatus?status=sold", nil) request.Header.Set("Content-Type", "application/json") // simulate a request/response, in this case the contract returns a 200 with the pet we just created. res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) _, _ = w.Write(bodyBytes) } // fire the request handler(res, request) // validate the response (should be clean) valid, errors := v.ValidateHttpResponse(request, res.Result()) // should all be perfectly valid. assert.True(t, valid) assert.Len(t, errors, 0) } func TestNewValidator_ValidateHttpResponse_RangeResponseCode(t *testing.T) { spec := `openapi: 3.1.0 paths: /burgers: get: responses: '200': description: OK content: application/json: schema: type: array items: type: object properties: name: type: string patties: type: integer vegetarian: type: boolean '4XX': description: Bad request '5XX': description: Server error` doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers", nil) request.Header.Set("Content-Type", "application/json") response := &http.Response{ StatusCode: 400, Header: http.Header{"Content-Type": []string{"application/json"}}, } valid, errors := v.ValidateHttpResponse(request, response) assert.True(t, valid) assert.Len(t, errors, 0) } // https://github.com/pb33f/libopenapi-validator/issues/107 // https://github.com/pb33f/libopenapi-validator/issues/103 func TestNewValidator_TestCircularRefsInValidation_Request(t *testing.T) { spec := `openapi: 3.1.0 info: title: Panic at response validation version: 1.0.0 paths: /operations: delete: description: Delete operations responses: default: description: Any response content: application/json: schema: $ref: '#/components/schemas/Error' components: schemas: Error: type: object properties: code: type: string details: type: array items: $ref: '#/components/schemas/Error'` document, err := libopenapi.NewDocument([]byte(spec)) if err != nil { panic(fmt.Sprintf("failed to create new document: %v\n", err)) } model, errs := document.BuildV3Model() circ := model.Index.GetCircularReferences() fmt.Println("Circular references: ", len(circ)) if errs != nil { panic(fmt.Sprintf("failed to create v3 model from document: %e", errs)) } fmt.Println("Successfully parsed OpenAPI spec") oapiValidator, merrs := NewValidator(document) if merrs != nil { panic(fmt.Sprintf("failed to create validator: %v", merrs)) } if ok, merrs := oapiValidator.ValidateDocument(); !ok { panic(fmt.Sprintf("document validation errors: %v", merrs)) } req := &http.Request{ Method: http.MethodDelete, URL: &url.URL{ Path: "/operations", }, } res := &http.Response{ StatusCode: http.StatusOK, Header: map[string][]string{ "Content-Type": {"application/json"}, }, Body: io.NopCloser(strings.NewReader(`{"code":"abc","details":[{"code":"def"}]}`)), } if ok, errs := oapiValidator.ValidateHttpResponse(req, res); !ok { assert.Equal(t, 1, len(errs)) // Error message can vary depending on whether schema was cached during warming or not: // - "schema render failure, circular reference" (if caught during validation) // - "JSON schema compile failed: json-pointer...not found" (if schema was pre-warmed but has circular refs) // Both indicate the same underlying issue - circular reference in the schema assert.True(t, strings.Contains(errs[0].Reason, "circular reference") || strings.Contains(errs[0].Reason, "json-pointer") || strings.Contains(errs[0].Reason, "not found"), "Expected error about circular reference or compilation failure, got: %s", errs[0].Reason) } } // https://github.com/pb33f/libopenapi-validator/issues/107 // https://github.com/pb33f/libopenapi-validator/issues/103 func TestNewValidator_TestCircularRefsInValidation_Response(t *testing.T) { spec := `openapi: 3.1.0 info: title: Panic at response validation version: 1.0.0 paths: /operations: delete: description: Delete operations responses: default: description: Any response content: application/json: schema: $ref: '#/components/schemas/Error' components: schemas: Error: type: object properties: code: type: string details: type: array items: $ref: '#/components/schemas/Error'` document, err := libopenapi.NewDocument([]byte(spec)) if err != nil { panic(fmt.Sprintf("failed to create new document: %v\n", err)) } model, errs := document.BuildV3Model() circ := model.Index.GetCircularReferences() fmt.Println("Circular references: ", len(circ)) if errs != nil { panic(fmt.Sprintf("failed to create v3 model from document: %e", errs)) } fmt.Println("Successfully parsed OpenAPI spec") oapiValidator, merrs := NewValidator(document) if merrs != nil { panic(fmt.Sprintf("failed to create validator: %v", merrs)) } if ok, merrs := oapiValidator.ValidateDocument(); !ok { panic(fmt.Sprintf("document validation errors: %v", merrs)) } req := &http.Request{ Method: http.MethodDelete, URL: &url.URL{ Path: "/operations", }, } if ok, errs := oapiValidator.ValidateHttpRequest(req); !ok { assert.Equal(t, 1, len(errs)) assert.Equal(t, "cannot render circular reference: #/components/schemas/Error", errs[0].Reason) } } // https://github.com/pb33f/libopenapi-validator/issues/86 func TestNewValidator_HaveYourModelAndEatIt(t *testing.T) { spec := `openapi: 3.1.0 info: title: Panic at response validation version: 1.0.0 paths: /operations: delete: description: Delete operations responses: default: description: Any response content: application/json: schema: $ref: '#/components/schemas/Error' components: schemas: Error: type: object properties: code: type: string details: type: array items: $ref: '#/components/schemas/Error'` document, err := libopenapi.NewDocument([]byte(spec)) if err != nil { panic(fmt.Sprintf("failed to create new document: %v\n", err)) } model, errs := document.BuildV3Model() if errs != nil { panic(fmt.Sprintf("failed to create v3 model from document: %e", errs)) } v := NewValidatorFromV3Model(&model.Model) valid, vErrs := v.ValidateDocument() assert.False(t, valid) assert.Len(t, vErrs, 1) assert.Equal(t, "The document cannot be validated as it is not set", vErrs[0].Reason) v.SetDocument(document) valid, vErrs = v.ValidateDocument() assert.True(t, valid) assert.Len(t, vErrs, 0) } func TestCacheWarming_PopulatesCache(t *testing.T) { spec, err := os.ReadFile("test_specs/petstorev3.json") require.NoError(t, err) doc, err := libopenapi.NewDocument(spec) require.NoError(t, err) v, errs := NewValidator(doc) require.Nil(t, errs) validator := v.(*validator) // Check that caches were populated // Access cache directly from validator options require.NotNil(t, validator.options) require.NotNil(t, validator.options.SchemaCache) count := 0 validator.options.SchemaCache.Range(func(key uint64, value *cache.SchemaCacheEntry) bool { count++ assert.NotNil(t, value.CompiledSchema, "Cache entry should have compiled schema") assert.NotEmpty(t, value.ReferenceSchema, "Cache entry should have pre-converted ReferenceSchema string") assert.Equal(t, string(value.RenderedInline), value.ReferenceSchema, "ReferenceSchema should match string conversion of RenderedInline") return true }) assert.Greater(t, count, 0, "Schema cache should have entries from request and response bodies") } func TestCacheWarming_EdgeCases(t *testing.T) { // Test nil document warmSchemaCaches(nil, nil) // Test empty document doc := &v3.Document{} warmSchemaCaches(doc, nil) // Test document with nil PathItems doc = &v3.Document{Paths: &v3.Paths{}} warmSchemaCaches(doc, nil) } func TestCacheWarming_NilOperations(t *testing.T) { spec := `openapi: 3.1.0 paths: /test: get: responses: '200': description: OK content: application/json: schema: type: object` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, _ := doc.BuildV3Model() // Manually set operations to nil to test edge cases for pair := m.Model.Paths.PathItems.First(); pair != nil; pair = pair.Next() { pathItem := pair.Value() // Force GetOperations to return something with nil operation pathItem.Post = nil pathItem.Put = nil pathItem.Delete = nil pathItem.Patch = nil pathItem.Head = nil pathItem.Options = nil pathItem.Trace = nil pathItem.Query = nil pathItem.AdditionalOperations = nil } // This should not panic even with nil operations v := NewValidatorFromV3Model(&m.Model) assert.NotNil(t, v) } func TestCacheWarming_NilSchema(t *testing.T) { spec := `openapi: 3.1.0 paths: /test: post: requestBody: content: application/json: schema: type: object properties: name: type: string responses: '200': description: OK content: application/json: schema: type: object` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) m, _ := doc.BuildV3Model() // Manually set schema to nil to test edge case in warmMediaTypeSchema for pathPair := m.Model.Paths.PathItems.First(); pathPair != nil; pathPair = pathPair.Next() { pathItem := pathPair.Value() if pathItem.Post != nil && pathItem.Post.RequestBody != nil && pathItem.Post.RequestBody.Content != nil { for contentPair := pathItem.Post.RequestBody.Content.First(); contentPair != nil; contentPair = contentPair.Next() { mediaType := contentPair.Value() // Set schema to nil to trigger the schema == nil check mediaType.Schema = nil } } } // This should not panic even with nil schemas v := NewValidatorFromV3Model(&m.Model) assert.NotNil(t, v) } func TestCacheWarming_DefaultResponse(t *testing.T) { spec := `openapi: 3.1.0 paths: /test: get: responses: default: description: Default response content: application/json: schema: type: object properties: message: type: string` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) v, errs := NewValidator(doc) require.Nil(t, errs) validator := v.(*validator) // Check that response cache was populated with default response schema require.NotNil(t, validator.options) require.NotNil(t, validator.options.SchemaCache) count := 0 validator.options.SchemaCache.Range(func(key uint64, value *cache.SchemaCacheEntry) bool { count++ return true }) assert.Greater(t, count, 0, "Schema cache should have entries from default response") } // TestCacheWarming_InvalidSchema tests cache warming gracefully skips invalid schemas func TestCacheWarming_InvalidSchema(t *testing.T) { // This spec intentionally has an invalid schema that will fail to compile spec := `openapi: 3.1.0 paths: /test: post: requestBody: content: application/json: schema: type: invalid-type-that-does-not-exist responses: '200': description: Success content: application/json: schema: type: object` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) // Should not panic even with invalid schema v, errs := NewValidator(doc) require.Nil(t, errs) assert.NotNil(t, v) } // TestCacheWarming_ParameterWithContent tests cache warming for parameters with content property func TestCacheWarming_ParameterWithContent(t *testing.T) { spec := `openapi: 3.1.0 paths: /test: get: parameters: - name: filter in: query content: application/json: schema: type: object properties: value: type: string responses: '200': description: Success` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) v, errs := NewValidator(doc) require.Nil(t, errs) validator := v.(*validator) // Check that parameter cache was populated with content schema require.NotNil(t, validator.options) require.NotNil(t, validator.options.SchemaCache) count := 0 validator.options.SchemaCache.Range(func(key uint64, value *cache.SchemaCacheEntry) bool { count++ return true }) assert.Greater(t, count, 0, "Schema cache should have entries from parameter content property") } // TestCacheWarming_PathLevelParameters tests cache warming for path-level parameters func TestCacheWarming_PathLevelParameters(t *testing.T) { spec := `openapi: 3.1.0 paths: /test/{id}: parameters: - name: id in: path required: true schema: type: string pattern: "^[0-9]+$" get: responses: '200': description: Success` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) v, errs := NewValidator(doc) require.Nil(t, errs) validator := v.(*validator) // Check that parameter cache was populated with path-level parameter require.NotNil(t, validator.options) require.NotNil(t, validator.options.SchemaCache) count := 0 validator.options.SchemaCache.Range(func(key uint64, value *cache.SchemaCacheEntry) bool { count++ return true }) assert.Greater(t, count, 0, "Schema cache should have entries from path-level parameters") } // TestSortValidationErrors tests that validation errors are sorted deterministically func TestSortValidationErrors(t *testing.T) { // Create errors in random order errs := []*errors.ValidationError{ {ValidationType: helpers.SecurityValidation, Message: "API Key missing"}, {ValidationType: helpers.ParameterValidation, Message: "Path param invalid"}, {ValidationType: helpers.RequestValidation, Message: "Body invalid"}, {ValidationType: helpers.ParameterValidation, Message: "Header missing"}, {ValidationType: helpers.SecurityValidation, Message: "Auth header missing"}, } sortValidationErrors(errs) // Verify sorted by validation type first, then by message assert.Equal(t, helpers.ParameterValidation, errs[0].ValidationType) assert.Equal(t, "Header missing", errs[0].Message) assert.Equal(t, helpers.ParameterValidation, errs[1].ValidationType) assert.Equal(t, "Path param invalid", errs[1].Message) assert.Equal(t, helpers.RequestValidation, errs[2].ValidationType) assert.Equal(t, "Body invalid", errs[2].Message) assert.Equal(t, helpers.SecurityValidation, errs[3].ValidationType) assert.Equal(t, "API Key missing", errs[3].Message) assert.Equal(t, helpers.SecurityValidation, errs[4].ValidationType) assert.Equal(t, "Auth header missing", errs[4].Message) } // TestSortValidationErrors_Empty tests sorting empty slice func TestSortValidationErrors_Empty(t *testing.T) { errs := []*errors.ValidationError{} sortValidationErrors(errs) assert.Empty(t, errs) } // TestSortValidationErrors_SingleElement tests sorting single element slice func TestSortValidationErrors_SingleElement(t *testing.T) { errs := []*errors.ValidationError{ {ValidationType: helpers.ParameterValidation, Message: "Invalid value"}, } sortValidationErrors(errs) assert.Len(t, errs, 1) assert.Equal(t, helpers.ParameterValidation, errs[0].ValidationType) } func TestHEAD_ExplicitOperation_ResponseValidation(t *testing.T) { spec := `openapi: 3.1.0 paths: /resource: head: responses: "200": description: ok content: application/json: schema: type: object properties: ok: type: boolean ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) // create a HEAD request request, _ := http.NewRequest(http.MethodHead, "https://example.com/resource", nil) // simulate a server response that includes a JSON body matching the schema bodyBytes, _ := json.Marshal(map[string]bool{"ok": true}) response := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(bodyBytes)), } valid, valErrs := v.ValidateHttpResponse(request, response) assert.False(t, valid) assert.Len(t, valErrs, 1) // Also validate a response without a body (common for HEAD) responseNoBody := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: http.NoBody, } validNoBody, valErrsNoBody := v.ValidateHttpResponse(request, responseNoBody) assert.True(t, validNoBody) assert.Len(t, valErrsNoBody, 0) } func TestHEAD_ImplicitViaGET_ResponseValidation(t *testing.T) { // This spec defines only GET for /resource. Ensure a HEAD request that returns the same body // as GET will still validate against the documented GET response. spec := `openapi: 3.1.0 paths: /resource: get: responses: "200": description: ok content: application/json: schema: type: object properties: ok: type: boolean ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) // create a HEAD request (no explicit HEAD operation in the spec) request, _ := http.NewRequest(http.MethodHead, "https://example.com/resource", nil) // simulate a server response that includes a JSON body like the GET response would. bodyBytes, _ := json.Marshal(map[string]bool{"ok": true}) response := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(bodyBytes)), } valid, valErrs := v.ValidateHttpResponse(request, response) // Expect validation to succeed when HEAD responses are validated against GET response definitions. assert.False(t, valid) assert.Len(t, valErrs, 1) responseNoBody := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: http.NoBody, } validNoBody, valErrsNoBody := v.ValidateHttpResponse(request, responseNoBody) // Expect validation to succeed when HEAD responses are validated against GET response definitions. assert.True(t, validNoBody) assert.Len(t, valErrsNoBody, 0) } func TestHEAD_BothGETAndHEAD_SameSchema_Valid(t *testing.T) { spec := `openapi: 3.1.0 paths: /resource: get: responses: "200": description: ok content: application/json: schema: type: object properties: ok: type: boolean head: responses: "200": description: ok content: application/json: schema: type: object properties: ok: type: boolean ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) // HEAD request to /resource request, _ := http.NewRequest(http.MethodHead, "https://example.com/resource", nil) // server returns a JSON body that matches the schema bodyBytes, _ := json.Marshal(map[string]bool{"ok": true}) response := &http.Response{ StatusCode: 200, Header: http.Header{helpers.ContentTypeHeader: []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(bodyBytes)), } valid, valErrs := v.ValidateHttpResponse(request, response) assert.False(t, valid) assert.Len(t, valErrs, 1) responseNoBody := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: http.NoBody, } validNoBody, valErrsNoBody := v.ValidateHttpResponse(request, responseNoBody) // Expect validation to succeed when HEAD responses are validated against GET response definitions. assert.True(t, validNoBody) assert.Len(t, valErrsNoBody, 0) } func TestHEAD_BothGETAndHEAD_DifferentSchemas_HeadPreferred(t *testing.T) { spec := `openapi: 3.1.0 paths: /resource: get: responses: "200": description: get schema content: application/json: schema: type: object required: ["g"] properties: g: type: string head: responses: "200": description: head schema headers: content-length: description: size of the file schema: type: integer ` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) // Case A: response matches HEAD schema has content-length header but response contains content-type reqA, _ := http.NewRequest(http.MethodHead, "https://example.com/resource", nil) respA := &http.Response{ StatusCode: 200, Header: http.Header{helpers.ContentTypeHeader: []string{"application/json"}}, Body: http.NoBody, } // No support for response headers validation. // Only validates response content-type: json validA, errsA := v.ValidateHttpResponse(reqA, respA) assert.True(t, validA) assert.Len(t, errsA, 0) // Case B: response matches GET schema (has "g") but not HEAD (missing required "h") -> should fail reqB, _ := http.NewRequest(http.MethodGet, "https://example.com/resource", nil) bodyB, _ := json.Marshal(map[string]string{"g": "get-value"}) respB := &http.Response{ StatusCode: 200, Header: http.Header{helpers.ContentTypeHeader: []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(bodyB)), } validB, errsB := v.ValidateHttpResponse(reqB, respB) assert.True(t, validB) assert.Len(t, errsB, 0) } // TestValidateHttpResponse_DiscriminatorOneOf tests that response validation works correctly // when the response schema uses oneOf with a discriminator and $ref to component schemas. // This is a regression test for https://github.com/pb33f/libopenapi-validator/issues/247 // where the validator would fail with "json-pointer not found" because discriminator refs // were preserved (Bundle mode) instead of fully inlined (Validation mode). func TestValidateHttpResponse_DiscriminatorOneOf(t *testing.T) { spec := `openapi: 3.1.0 info: title: Discriminator OneOf Response Test version: 1.0.0 paths: /fields: get: responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/Field' post: requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/Field' responses: '201': description: Created components: schemas: BooleanField: type: object required: - field_type - default_value properties: field_type: type: string const: boolean default_value: type: boolean StringField: type: object required: - field_type - max_length properties: field_type: type: string const: string max_length: type: integer minimum: 1 Field: oneOf: - $ref: '#/components/schemas/BooleanField' - $ref: '#/components/schemas/StringField' discriminator: propertyName: field_type` // Subtest with cache enabled (exercises cache-warming paths in validator.go) t.Run("cached", func(t *testing.T) { doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) v, errs := NewValidator(doc) require.Empty(t, errs) body, _ := json.Marshal(map[string]interface{}{ "field_type": "boolean", "default_value": true, }) request, _ := http.NewRequest(http.MethodGet, "https://example.com/fields", nil) response := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(body)), } valid, validationErrors := v.ValidateHttpResponse(request, response) assert.True(t, valid, "response validation should pass for valid discriminator oneOf payload") assert.Empty(t, validationErrors, "no validation errors expected") }) // Subtest with cache disabled (exercises uncached compile paths in // validate_response.go, validate_request.go) t.Run("uncached", func(t *testing.T) { doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) v, errs := NewValidator(doc, config.WithSchemaCache(nil)) require.Empty(t, errs) // Response validation (validate_response.go cache-miss path) body, _ := json.Marshal(map[string]interface{}{ "field_type": "string", "max_length": 255, }) request, _ := http.NewRequest(http.MethodGet, "https://example.com/fields", nil) response := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer(body)), } valid, validationErrors := v.ValidateHttpResponse(request, response) assert.True(t, valid, "uncached response validation should pass") assert.Empty(t, validationErrors, "no validation errors expected (uncached response)") // Request validation (validate_request.go cache-miss path) reqBody, _ := json.Marshal(map[string]interface{}{ "field_type": "boolean", "default_value": false, }) postReq, _ := http.NewRequest(http.MethodPost, "https://example.com/fields", bytes.NewBuffer(reqBody)) postReq.Header.Set("Content-Type", "application/json") validReq, reqErrors := v.ValidateHttpRequest(postReq) assert.True(t, validReq, "uncached request validation should pass") assert.Empty(t, reqErrors, "no validation errors expected (uncached request)") }) } func TestStrictMode_RejectReadOnly_RequestIntegration(t *testing.T) { spec := `openapi: 3.1.0 paths: /users: post: requestBody: required: true content: application/json: schema: type: object properties: id: type: string readOnly: true name: type: string responses: "201": description: created` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) v, errs := NewValidator(doc, config.WithStrictMode(), config.WithStrictRejectReadOnly()) require.Empty(t, errs) body := map[string]any{ "id": "user-123", "name": "John", } bodyBytes, _ := json.Marshal(body) request, _ := http.NewRequest(http.MethodPost, "https://example.com/users", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") valid, valErrs := v.ValidateHttpRequest(request) assert.False(t, valid) foundReadOnly := false for _, vErr := range valErrs { if vErr.ValidationType == errors.StrictValidationType && vErr.ValidationSubType == errors.StrictSubTypeReadOnlyProperty { foundReadOnly = true break } } assert.True(t, foundReadOnly, "should report readOnly violation") } func TestStrictMode_RejectWriteOnly_ResponseIntegration(t *testing.T) { spec := `openapi: 3.1.0 paths: /users/{id}: get: parameters: - in: path name: id required: true schema: type: string responses: "200": description: ok content: application/json: schema: type: object properties: name: type: string password: type: string writeOnly: true` doc, err := libopenapi.NewDocument([]byte(spec)) require.NoError(t, err) v, errs := NewValidator(doc, config.WithStrictMode(), config.WithStrictRejectWriteOnly()) require.Empty(t, errs) request, _ := http.NewRequest(http.MethodGet, "https://example.com/users/123", http.NoBody) body := map[string]any{ "name": "John", "password": "secret", } bodyBytes, _ := json.Marshal(body) response := &http.Response{ StatusCode: 200, Header: http.Header{ "Content-Type": {"application/json"}, }, Body: io.NopCloser(bytes.NewBuffer(bodyBytes)), } valid, valErrs := v.ValidateHttpResponse(request, response) assert.False(t, valid) foundWriteOnly := false for _, vErr := range valErrs { if vErr.ValidationType == errors.StrictValidationType && vErr.ValidationSubType == errors.StrictSubTypeWriteOnlyProperty { foundWriteOnly = true break } } assert.True(t, foundWriteOnly, "should report writeOnly violation") }