diff --git a/cache/cache_test.go b/cache/cache_test.go index 16be42ec..ea3f6789 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -7,9 +7,9 @@ import ( "testing" "github.com/pb33f/libopenapi/datamodel/high/base" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" "github.com/santhosh-tekuri/jsonschema/v6" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestNewDefaultCache(t *testing.T) { diff --git a/config/config_test.go b/config/config_test.go index c1cea053..71776204 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -9,8 +9,8 @@ import ( "sync" "testing" + "github.com/pb33f/testify/assert" "github.com/santhosh-tekuri/jsonschema/v6" - "github.com/stretchr/testify/assert" ) func TestNewValidationOptions_Defaults(t *testing.T) { diff --git a/errors/error_utilities_test.go b/errors/error_utilities_test.go index b45e3700..ce1f6f57 100644 --- a/errors/error_utilities_test.go +++ b/errors/error_utilities_test.go @@ -7,7 +7,7 @@ import ( "net/http" "testing" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/require" ) // Helper function to create a mock ValidationError diff --git a/errors/parameter_errors_test.go b/errors/parameter_errors_test.go index e9a5e51c..0e9e83cc 100644 --- a/errors/parameter_errors_test.go +++ b/errors/parameter_errors_test.go @@ -10,7 +10,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/orderedmap" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/require" "go.yaml.in/yaml/v4" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" diff --git a/errors/request_errors_test.go b/errors/request_errors_test.go index 033f2d5c..cc41d72d 100644 --- a/errors/request_errors_test.go +++ b/errors/request_errors_test.go @@ -9,7 +9,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/orderedmap" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/require" "go.yaml.in/yaml/v4" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" diff --git a/errors/response_errors_test.go b/errors/response_errors_test.go index 69323a0c..1cd4b1c2 100644 --- a/errors/response_errors_test.go +++ b/errors/response_errors_test.go @@ -9,7 +9,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/orderedmap" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/require" "go.yaml.in/yaml/v4" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" diff --git a/errors/strict_errors_test.go b/errors/strict_errors_test.go index 127fd825..b12d5e9e 100644 --- a/errors/strict_errors_test.go +++ b/errors/strict_errors_test.go @@ -6,7 +6,7 @@ package errors import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" ) func TestUndeclaredPropertyError(t *testing.T) { diff --git a/errors/urlencoded_errors_test.go b/errors/urlencoded_errors_test.go index d6ad984e..d1902667 100644 --- a/errors/urlencoded_errors_test.go +++ b/errors/urlencoded_errors_test.go @@ -6,7 +6,7 @@ import ( "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" ) func getURLEncodingTestSchema() *base.Schema { diff --git a/errors/validation_error_test.go b/errors/validation_error_test.go index 82e0d6d6..57b1d698 100644 --- a/errors/validation_error_test.go +++ b/errors/validation_error_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/pb33f/libopenapi-validator/helpers" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/require" ) func TestSchemaValidationFailure_Error(t *testing.T) { diff --git a/errors/xml_errors_test.go b/errors/xml_errors_test.go index 3b05da04..2f4ce1fc 100644 --- a/errors/xml_errors_test.go +++ b/errors/xml_errors_test.go @@ -9,7 +9,7 @@ import ( "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" ) func getTestSchema() *base.Schema { diff --git a/go.mod b/go.mod index 87c36534..60b38f7b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/pb33f/libopenapi-validator -go 1.25.0 +go 1.25.7 require ( github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad @@ -8,23 +8,18 @@ require ( 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.37.2 + github.com/pb33f/libopenapi v0.38.1 + github.com/pb33f/testify v0.1.0 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 + go.yaml.in/yaml/v4 v4.0.0-rc.5 + golang.org/x/text v0.38.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 + golang.org/x/sync v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index d8a0ec28..09e5a075 100644 --- a/go.sum +++ b/go.sum @@ -19,23 +19,16 @@ github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLu 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.37.2 h1:4Kb4w/h2BVKb099oYIZqeDxEBhUioWA+z6WJhBOk2r8= -github.com/pb33f/libopenapi v0.37.2/go.mod h1:MsDdUlQ1CdrIDO5v26JfgBxQs7kcaOUEpMP3EqU6bI4= +github.com/pb33f/libopenapi v0.38.1 h1:F4mlPaex6MugO1DoGjIy8lnevyzclfGX5lWZrT9LszE= +github.com/pb33f/libopenapi v0.38.1/go.mod h1:OIh31Zxvw3z0OnLGKqhnVlSQ80swwddph1+xedWZjdU= 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/pb33f/testify v0.1.0 h1:g48/HDU/jn2COspS4nM0scptxiKTJ4DnbX/4ehK6IZ8= +github.com/pb33f/testify v0.1.0/go.mod h1:nq283P/jJ8hXMmdhAqfj7BJIz0y+6IOHj9q0044rKt4= 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= @@ -43,8 +36,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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= +go.yaml.in/yaml/v4 v4.0.0-rc.5 h1:JVliQq9EGOYaTgMi+k8BhUJyqcGk4ZqeuiN1Cirba9c= +go.yaml.in/yaml/v4 v4.0.0-rc.5/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= @@ -61,8 +54,8 @@ 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/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.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= @@ -82,16 +75,14 @@ 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/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= 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= diff --git a/helpers/json_pointer_test.go b/helpers/json_pointer_test.go index 5e08b2d3..b5f5e45b 100644 --- a/helpers/json_pointer_test.go +++ b/helpers/json_pointer_test.go @@ -6,7 +6,7 @@ package helpers import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" ) func TestEscapeJSONPointerSegment(t *testing.T) { diff --git a/helpers/operation_utilities_test.go b/helpers/operation_utilities_test.go index c6433ad0..fcb28ec9 100644 --- a/helpers/operation_utilities_test.go +++ b/helpers/operation_utilities_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/pb33f/libopenapi/datamodel/high/v3" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/require" ) // Test ExtractOperation for each HTTP method diff --git a/helpers/parameter_utilities_test.go b/helpers/parameter_utilities_test.go index e3908668..29776e05 100644 --- a/helpers/parameter_utilities_test.go +++ b/helpers/parameter_utilities_test.go @@ -9,7 +9,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/orderedmap" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/require" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" ) diff --git a/helpers/path_finder_test.go b/helpers/path_finder_test.go index 8f9c4be0..f6d78c6f 100644 --- a/helpers/path_finder_test.go +++ b/helpers/path_finder_test.go @@ -6,8 +6,8 @@ package helpers import ( "testing" + "github.com/pb33f/testify/assert" "github.com/santhosh-tekuri/jsonschema/v6" - "github.com/stretchr/testify/assert" ) func TestDiveIntoValidationError(t *testing.T) { diff --git a/helpers/regex_maker_test.go b/helpers/regex_maker_test.go index b2cff441..99315d11 100644 --- a/helpers/regex_maker_test.go +++ b/helpers/regex_maker_test.go @@ -3,7 +3,7 @@ package helpers import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" ) func TestGetRegexForPath(t *testing.T) { diff --git a/helpers/schema_compiler_test.go b/helpers/schema_compiler_test.go index 72cbbe22..c78cbaba 100644 --- a/helpers/schema_compiler_test.go +++ b/helpers/schema_compiler_test.go @@ -6,8 +6,8 @@ import ( "testing" "unicode" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" "github.com/pb33f/libopenapi-validator/config" ) diff --git a/helpers/url_loader_test.go b/helpers/url_loader_test.go index 18276c6f..4c4e6eb9 100644 --- a/helpers/url_loader_test.go +++ b/helpers/url_loader_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/require" ) // Test the Load function for a successful case diff --git a/helpers/version_test.go b/helpers/version_test.go index 56bae297..2254ee52 100644 --- a/helpers/version_test.go +++ b/helpers/version_test.go @@ -3,7 +3,7 @@ package helpers import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" ) func TestVersionToFloat(t *testing.T) { diff --git a/openapi_vocabulary/coercion_simple_test.go b/openapi_vocabulary/coercion_simple_test.go index bf3a6f05..736c9e74 100644 --- a/openapi_vocabulary/coercion_simple_test.go +++ b/openapi_vocabulary/coercion_simple_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" + "github.com/pb33f/testify/assert" "github.com/santhosh-tekuri/jsonschema/v6" - "github.com/stretchr/testify/assert" ) func TestCoercion_Vocabulary_CompilationSuccess(t *testing.T) { diff --git a/openapi_vocabulary/vocabulary_test.go b/openapi_vocabulary/vocabulary_test.go index 750ef3dd..a951f16c 100644 --- a/openapi_vocabulary/vocabulary_test.go +++ b/openapi_vocabulary/vocabulary_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" + "github.com/pb33f/testify/assert" "github.com/santhosh-tekuri/jsonschema/v6" - "github.com/stretchr/testify/assert" "golang.org/x/text/message" ) diff --git a/parameters/cookie_parameters_test.go b/parameters/cookie_parameters_test.go index b4b7479c..4d8139e5 100644 --- a/parameters/cookie_parameters_test.go +++ b/parameters/cookie_parameters_test.go @@ -9,8 +9,8 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/helpers" diff --git a/parameters/header_parameters_test.go b/parameters/header_parameters_test.go index 2e276898..62cb7be4 100644 --- a/parameters/header_parameters_test.go +++ b/parameters/header_parameters_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/paths" diff --git a/parameters/path_parameters_test.go b/parameters/path_parameters_test.go index 5972439b..c295315d 100644 --- a/parameters/path_parameters_test.go +++ b/parameters/path_parameters_test.go @@ -10,8 +10,8 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/helpers" diff --git a/parameters/query_parameters_test.go b/parameters/query_parameters_test.go index 1bc9ca77..9cff8693 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -11,8 +11,8 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/paths" diff --git a/parameters/validate_parameter_test.go b/parameters/validate_parameter_test.go index 6f58e6e1..f3585473 100644 --- a/parameters/validate_parameter_test.go +++ b/parameters/validate_parameter_test.go @@ -9,8 +9,8 @@ import ( "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/testify/assert" + "github.com/pb33f/testify/require" "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi-validator/config" diff --git a/parameters/validate_security_test.go b/parameters/validate_security_test.go index 4ec236a3..60837f86 100644 --- a/parameters/validate_security_test.go +++ b/parameters/validate_security_test.go @@ -11,7 +11,7 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/paths" diff --git a/paths/paths_test.go b/paths/paths_test.go index c3bfbdcf..e46ea49d 100644 --- a/paths/paths_test.go +++ b/paths/paths_test.go @@ -14,7 +14,7 @@ import ( "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/radix" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" ) func TestNewValidator_BadParam(t *testing.T) { diff --git a/paths/specificity_test.go b/paths/specificity_test.go index 4c1f52c8..c5902a35 100644 --- a/paths/specificity_test.go +++ b/paths/specificity_test.go @@ -7,7 +7,7 @@ import ( "testing" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" ) func TestComputeSpecificityScore(t *testing.T) { diff --git a/radix/path_tree_test.go b/radix/path_tree_test.go index d8dabf40..9f3cc1de 100644 --- a/radix/path_tree_test.go +++ b/radix/path_tree_test.go @@ -8,8 +8,8 @@ import ( "github.com/pb33f/libopenapi" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" ) func TestNewPathTree(t *testing.T) { diff --git a/radix/tree_test.go b/radix/tree_test.go index 62467834..0f6d3dfe 100644 --- a/radix/tree_test.go +++ b/radix/tree_test.go @@ -8,8 +8,8 @@ import ( "sort" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" ) func TestNew(t *testing.T) { diff --git a/requests/validate_body_test.go b/requests/validate_body_test.go index 36daa63d..21e59467 100644 --- a/requests/validate_body_test.go +++ b/requests/validate_body_test.go @@ -13,8 +13,8 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/helpers" diff --git a/requests/validate_request.go b/requests/validate_request.go index b43387fe..66d3cc92 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -15,7 +15,6 @@ import ( "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" @@ -83,7 +82,7 @@ func requestBodyReader(body io.ReadCloser) io.Reader { } value := reflect.ValueOf(body) - if value.Kind() == reflect.Ptr { + if value.Kind() == reflect.Pointer { if value.IsNil() { return nil } @@ -159,7 +158,11 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*liberror } if validationOptions.SchemaCache != nil { - hash := input.Schema.GoLow().Hash() + hash := schema_validation.SchemaCacheKey( + input.Schema.GoLow().Hash(), + input.Version, + schema_validation.SchemaValidationPurposeRequestBody, + ) if cached, ok := validationOptions.SchemaCache.Load(hash); ok && cached != nil && cached.CompiledSchema != nil { renderedSchema = cached.RenderedInline referenceSchema = cached.ReferenceSchema @@ -171,10 +174,16 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*liberror // 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) + rendered, renderErr := schema_validation.RenderSchemaForValidation( + input.Schema, + schema_validation.SchemaValidationPurposeRequestBody, + ) + if rendered != nil { + renderedSchema = rendered.RenderedInline + referenceSchema = rendered.ReferenceSchema + jsonSchema = rendered.RenderedJSON + cachedNode = rendered.RenderedNode + } // If rendering failed (e.g., circular reference), return the render error if renderErr != nil { @@ -198,10 +207,13 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*liberror return false, validationErrors } - jsonSchema, _ = utils.ConvertYAMLtoJSON(renderedSchema) - var err error - schemaName := fmt.Sprintf("%x", input.Schema.GoLow().Hash()) + hash := schema_validation.SchemaCacheKey( + input.Schema.GoLow().Hash(), + input.Version, + schema_validation.SchemaValidationPurposeRequestBody, + ) + schemaName := fmt.Sprintf("%x", hash) compiledSchema, err = helpers.NewCompiledSchemaWithVersion( schemaName, jsonSchema, @@ -224,13 +236,13 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*liberror } 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, + RenderedNode: cachedNode, }) } } diff --git a/requests/validate_request_test.go b/requests/validate_request_test.go index fd4ea8da..0394c6f4 100644 --- a/requests/validate_request_test.go +++ b/requests/validate_request_test.go @@ -10,10 +10,11 @@ import ( "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" "github.com/pb33f/libopenapi-validator/config" + "github.com/pb33f/libopenapi-validator/schema_validation" ) func TestValidateRequestSchema(t *testing.T) { @@ -153,7 +154,8 @@ properties: assert.Len(t, errors, 0) // Verify cache was populated - hash := schema.GoLow().Hash() + hash := schema_validation.SchemaCacheKey(schema.GoLow().Hash(), openAPIVersion, + schema_validation.SchemaValidationPurposeRequestBody) 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") @@ -162,6 +164,99 @@ properties: assert.NotNil(t, cached.RenderedJSON, "JSON schema should be cached") } +func TestValidateRequestSchema_ReadOnlyRequiredIgnored(t *testing.T) { + schema := parseSchemaFromSpec(t, `type: object +required: + - id + - name +properties: + id: + type: string + readOnly: true + name: + type: string`, 3.1) + + valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{ + Request: postRequestWithBody(`{"name":"John"}`), + Schema: schema, + Version: 3.1, + }) + + assert.True(t, valid) + assert.Empty(t, errors) +} + +func TestValidateRequestSchema_WriteOnlyRequiredStillApplies(t *testing.T) { + schema := parseSchemaFromSpec(t, `type: object +required: + - password +properties: + password: + type: string + writeOnly: true`, 3.1) + + valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{ + Request: postRequestWithBody(`{}`), + Schema: schema, + Version: 3.1, + }) + + assert.False(t, valid) + require.Len(t, errors, 1) + require.Len(t, errors[0].SchemaValidationErrors, 1) + assert.Equal(t, "missing property 'password'", errors[0].SchemaValidationErrors[0].Reason) +} + +func TestValidateRequestSchema_NestedReadOnlyRequiredIgnored(t *testing.T) { + schema := parseSchemaFromSpec(t, `type: object +required: + - profile +properties: + profile: + type: object + required: + - id + - email + properties: + id: + type: string + readOnly: true + email: + type: string`, 3.1) + + valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{ + Request: postRequestWithBody(`{"profile":{"email":"john@example.com"}}`), + Schema: schema, + Version: 3.1, + }) + + assert.True(t, valid) + assert.Empty(t, errors) +} + +func TestValidateRequestSchema_AllOfReadOnlyRequiredIgnored(t *testing.T) { + schema := parseSchemaFromSpec(t, `allOf: + - type: object + required: + - id + - name + properties: + id: + type: string + readOnly: true + name: + type: string`, 3.1) + + valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{ + Request: postRequestWithBody(`{"name":"John"}`), + Schema: schema, + Version: 3.1, + }) + + assert.True(t, valid) + assert.Empty(t, errors) +} + func TestValidateRequestSchema_NilSchema(t *testing.T) { // Test when schema is nil valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{ diff --git a/responses/validate_body_test.go b/responses/validate_body_test.go index 1a0a51ab..1e11b3a8 100644 --- a/responses/validate_body_test.go +++ b/responses/validate_body_test.go @@ -16,8 +16,8 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/helpers" diff --git a/responses/validate_headers_test.go b/responses/validate_headers_test.go index 290d7d13..b8bf5c3d 100644 --- a/responses/validate_headers_test.go +++ b/responses/validate_headers_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" "github.com/pb33f/libopenapi-validator/config" ) diff --git a/responses/validate_response.go b/responses/validate_response.go index 93527e5a..c57f10df 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -15,7 +15,6 @@ import ( "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" @@ -71,10 +70,15 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*liberr } if validationOptions.SchemaCache != nil { - hash := input.Schema.GoLow().Hash() + hash := schema_validation.SchemaCacheKey( + input.Schema.GoLow().Hash(), + input.Version, + schema_validation.SchemaValidationPurposeResponseBody, + ) 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 } @@ -82,10 +86,16 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*liberr // 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) + rendered, renderErr := schema_validation.RenderSchemaForValidation( + input.Schema, + schema_validation.SchemaValidationPurposeResponseBody, + ) + if rendered != nil { + renderedSchema = rendered.RenderedInline + referenceSchema = rendered.ReferenceSchema + jsonSchema = rendered.RenderedJSON + cachedNode = rendered.RenderedNode + } // If rendering failed (e.g., circular reference), return the render error if renderErr != nil { @@ -109,10 +119,13 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*liberr return false, validationErrors } - jsonSchema, _ = utils.ConvertYAMLtoJSON(renderedSchema) - var err error - schemaName := fmt.Sprintf("%x", input.Schema.GoLow().Hash()) + hash := schema_validation.SchemaCacheKey( + input.Schema.GoLow().Hash(), + input.Version, + schema_validation.SchemaValidationPurposeResponseBody, + ) + schemaName := fmt.Sprintf("%x", hash) compiledSchema, err = helpers.NewCompiledSchemaWithVersion( schemaName, jsonSchema, @@ -136,13 +149,13 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*liberr } 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, + RenderedNode: cachedNode, }) } } diff --git a/responses/validate_response_test.go b/responses/validate_response_test.go index 91e3aaea..3f54b662 100644 --- a/responses/validate_response_test.go +++ b/responses/validate_response_test.go @@ -10,10 +10,11 @@ import ( "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" "github.com/pb33f/libopenapi-validator/config" + "github.com/pb33f/libopenapi-validator/schema_validation" ) func TestValidateResponseSchema(t *testing.T) { @@ -161,7 +162,8 @@ properties: assert.Len(t, errors, 0) // Verify cache was populated - hash := schema.GoLow().Hash() + hash := schema_validation.SchemaCacheKey(schema.GoLow().Hash(), 3.1, + schema_validation.SchemaValidationPurposeResponseBody) 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") @@ -170,6 +172,103 @@ properties: assert.NotNil(t, cached.RenderedJSON, "JSON schema should be cached") } +func TestValidateResponseSchema_WriteOnlyRequiredIgnored(t *testing.T) { + schema := parseSchemaFromSpec(t, `type: object +required: + - password + - name +properties: + name: + type: string + password: + type: string + writeOnly: true`, 3.1) + + valid, errors := ValidateResponseSchema(&ValidateResponseSchemaInput{ + Request: postRequest(), + Response: responseWithBody(`{"name":"John"}`), + Schema: schema, + Version: 3.1, + }) + + assert.True(t, valid) + assert.Empty(t, errors) +} + +func TestValidateResponseSchema_ReadOnlyRequiredStillApplies(t *testing.T) { + schema := parseSchemaFromSpec(t, `type: object +required: + - id +properties: + id: + type: string + readOnly: true`, 3.1) + + valid, errors := ValidateResponseSchema(&ValidateResponseSchemaInput{ + Request: postRequest(), + Response: responseWithBody(`{}`), + Schema: schema, + Version: 3.1, + }) + + assert.False(t, valid) + require.Len(t, errors, 1) + require.Len(t, errors[0].SchemaValidationErrors, 1) + assert.Equal(t, "missing property 'id'", errors[0].SchemaValidationErrors[0].Reason) +} + +func TestValidateResponseSchema_NestedWriteOnlyRequiredIgnored(t *testing.T) { + schema := parseSchemaFromSpec(t, `type: object +required: + - profile +properties: + profile: + type: object + required: + - password + - email + properties: + password: + type: string + writeOnly: true + email: + type: string`, 3.1) + + valid, errors := ValidateResponseSchema(&ValidateResponseSchemaInput{ + Request: postRequest(), + Response: responseWithBody(`{"profile":{"email":"john@example.com"}}`), + Schema: schema, + Version: 3.1, + }) + + assert.True(t, valid) + assert.Empty(t, errors) +} + +func TestValidateResponseSchema_AllOfWriteOnlyRequiredIgnored(t *testing.T) { + schema := parseSchemaFromSpec(t, `allOf: + - type: object + required: + - password + - name + properties: + password: + type: string + writeOnly: true + name: + type: string`, 3.1) + + valid, errors := ValidateResponseSchema(&ValidateResponseSchemaInput{ + Request: postRequest(), + Response: responseWithBody(`{"name":"John"}`), + Schema: schema, + Version: 3.1, + }) + + assert.True(t, valid) + assert.Empty(t, errors) +} + func postRequest() *http.Request { req, _ := http.NewRequest(http.MethodPost, "/test", io.NopCloser(strings.NewReader(""))) return req diff --git a/schema_validation/directional_schema.go b/schema_validation/directional_schema.go new file mode 100644 index 00000000..c3ecfb28 --- /dev/null +++ b/schema_validation/directional_schema.go @@ -0,0 +1,211 @@ +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley +// SPDX-License-Identifier: MIT + +package schema_validation + +import ( + "fmt" + "math" + + "github.com/pb33f/libopenapi/datamodel/high/base" + "github.com/pb33f/libopenapi/utils" + "go.yaml.in/yaml/v4" +) + +// SchemaValidationPurpose identifies the context in which a schema is compiled. +// Request and response bodies need distinct cache entries because readOnly and +// writeOnly annotations change required-property semantics by direction. +type SchemaValidationPurpose uint64 + +const ( + SchemaValidationPurposeGeneric SchemaValidationPurpose = iota + SchemaValidationPurposeRequestBody + SchemaValidationPurposeResponseBody +) + +const schemaCachePurposeSalt uint64 = 0x9e3779b97f4a7c15 + +// RenderedValidationSchema contains a rendered schema and its JSON equivalent. +type RenderedValidationSchema struct { + RenderedInline []byte + ReferenceSchema string + RenderedJSON []byte + RenderedNode *yaml.Node +} + +// SchemaCacheKey returns a cache key for a schema compiled in a validation context. +func SchemaCacheKey(schemaHash uint64, version float32, purpose SchemaValidationPurpose) uint64 { + if purpose == SchemaValidationPurposeGeneric { + return schemaHash + } + versionBits := uint64(math.Float32bits(version)) + return schemaHash ^ (versionBits << 32) ^ versionBits ^ (uint64(purpose) * schemaCachePurposeSalt) +} + +// RenderSchemaForValidation renders schema for the supplied validation purpose. +// For request bodies it removes readOnly properties from required lists, and for +// response bodies it removes writeOnly properties from required lists. +func RenderSchemaForValidation(schema *base.Schema, purpose SchemaValidationPurpose) (*RenderedValidationSchema, error) { + if schema == nil { + return nil, nil + } + + renderCtx := base.NewInlineRenderContextForValidation() + renderedInline, err := schema.RenderInlineWithContext(renderCtx) + if err != nil { + return &RenderedValidationSchema{ + RenderedInline: renderedInline, + ReferenceSchema: string(renderedInline), + }, err + } + + return renderSchemaBytesForValidation(renderedInline, purpose) +} + +func renderSchemaBytesForValidation(renderedInline []byte, purpose SchemaValidationPurpose) (*RenderedValidationSchema, error) { + renderedNode := new(yaml.Node) + if err := yaml.Unmarshal(renderedInline, renderedNode); err != nil { + return nil, fmt.Errorf("schema render decode failed: %w", err) + } + + if len(renderedNode.Content) > 0 { + pruneDirectionalRequired(renderedNode.Content[0], purpose) + } + + if purpose != SchemaValidationPurposeGeneric { + renderedInline, _ = yaml.Marshal(renderedNode) + } + + renderedJSON, _ := utils.ConvertYAMLtoJSON(renderedInline) + + return &RenderedValidationSchema{ + RenderedInline: renderedInline, + ReferenceSchema: string(renderedInline), + RenderedJSON: renderedJSON, + RenderedNode: renderedNode, + }, nil +} + +func pruneDirectionalRequired(schemaNode *yaml.Node, purpose SchemaValidationPurpose) { + if schemaNode == nil || schemaNode.Kind != yaml.MappingNode { + return + } + + pruneRequiredAtSchema(schemaNode, purpose) + + for _, key := range []string{"properties", "patternProperties", "$defs", "definitions", "dependentSchemas"} { + if childMap := mappingValue(schemaNode, key); childMap != nil && childMap.Kind == yaml.MappingNode { + for i := 1; i < len(childMap.Content); i += 2 { + pruneDirectionalRequired(childMap.Content[i], purpose) + } + } + } + + for _, key := range []string{"items", "contains", "additionalProperties", "unevaluatedProperties", "propertyNames", "not", "if", "then", "else"} { + pruneDirectionalRequired(mappingValue(schemaNode, key), purpose) + } + + for _, key := range []string{"prefixItems", "allOf", "anyOf", "oneOf"} { + if childSeq := mappingValue(schemaNode, key); childSeq != nil && childSeq.Kind == yaml.SequenceNode { + for _, item := range childSeq.Content { + pruneDirectionalRequired(item, purpose) + } + } + } +} + +func pruneRequiredAtSchema(schemaNode *yaml.Node, purpose SchemaValidationPurpose) { + if purpose != SchemaValidationPurposeRequestBody && purpose != SchemaValidationPurposeResponseBody { + return + } + + requiredIndex, requiredNode := mappingPair(schemaNode, "required") + if requiredNode == nil || requiredNode.Kind != yaml.SequenceNode { + return + } + propertiesNode := mappingValue(schemaNode, "properties") + if propertiesNode == nil || propertiesNode.Kind != yaml.MappingNode { + return + } + + prunedRequired := make([]*yaml.Node, 0, len(requiredNode.Content)) + for _, item := range requiredNode.Content { + if item == nil || item.Kind != yaml.ScalarNode { + prunedRequired = append(prunedRequired, item) + continue + } + propertySchema := mappingValue(propertiesNode, item.Value) + if propertySchemaHasDirectionalAnnotation(propertySchema, purpose) { + continue + } + prunedRequired = append(prunedRequired, item) + } + + if len(prunedRequired) == 0 { + removeMappingPair(schemaNode, requiredIndex) + return + } + requiredNode.Content = prunedRequired +} + +func propertySchemaHasDirectionalAnnotation(schemaNode *yaml.Node, purpose SchemaValidationPurpose) bool { + if schemaNode == nil || schemaNode.Kind != yaml.MappingNode { + return false + } + + switch purpose { + case SchemaValidationPurposeRequestBody: + if boolMappingValue(schemaNode, "readOnly") { + return true + } + case SchemaValidationPurposeResponseBody: + if boolMappingValue(schemaNode, "writeOnly") { + return true + } + } + + for _, key := range []string{"allOf", "anyOf", "oneOf"} { + if childSeq := mappingValue(schemaNode, key); childSeq != nil && childSeq.Kind == yaml.SequenceNode { + for _, item := range childSeq.Content { + if propertySchemaHasDirectionalAnnotation(item, purpose) { + return true + } + } + } + } + + return false +} + +func mappingPair(node *yaml.Node, key string) (int, *yaml.Node) { + if node == nil || node.Kind != yaml.MappingNode { + return -1, nil + } + for i := 0; i+1 < len(node.Content); i += 2 { + keyNode := node.Content[i] + if keyNode != nil && keyNode.Value == key { + return i, node.Content[i+1] + } + } + return -1, nil +} + +func mappingValue(node *yaml.Node, key string) *yaml.Node { + _, value := mappingPair(node, key) + return value +} + +func boolMappingValue(node *yaml.Node, key string) bool { + value := mappingValue(node, key) + if value == nil || value.Kind != yaml.ScalarNode { + return false + } + return value.Tag == "!!bool" && value.Value == "true" +} + +func removeMappingPair(node *yaml.Node, keyIndex int) { + if node == nil || keyIndex < 0 || keyIndex+1 >= len(node.Content) { + return + } + node.Content = append(node.Content[:keyIndex], node.Content[keyIndex+2:]...) +} diff --git a/schema_validation/directional_schema_test.go b/schema_validation/directional_schema_test.go new file mode 100644 index 00000000..ff234868 --- /dev/null +++ b/schema_validation/directional_schema_test.go @@ -0,0 +1,261 @@ +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley +// SPDX-License-Identifier: MIT + +package schema_validation + +import ( + "encoding/json" + "fmt" + "strings" + "testing" + + "github.com/pb33f/libopenapi" + "github.com/pb33f/libopenapi/datamodel/high/base" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" + "go.yaml.in/yaml/v4" +) + +func TestRenderSchemaForValidation_DirectionalRequiredProperties(t *testing.T) { + schema := parseDirectionalTestSchema(t, `type: object +required: + - id + - name + - password +properties: + id: + type: string + readOnly: true + name: + type: string + password: + type: string + writeOnly: true`) + + for _, tc := range []struct { + name string + purpose SchemaValidationPurpose + expected []string + }{ + { + name: "generic keeps all required properties", + purpose: SchemaValidationPurposeGeneric, + expected: []string{"id", "name", "password"}, + }, + { + name: "request removes readOnly required properties", + purpose: SchemaValidationPurposeRequestBody, + expected: []string{"name", "password"}, + }, + { + name: "response removes writeOnly required properties", + purpose: SchemaValidationPurposeResponseBody, + expected: []string{"id", "name"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + rendered, err := RenderSchemaForValidation(schema, tc.purpose) + require.NoError(t, err) + require.NotNil(t, rendered) + + assert.Equal(t, tc.expected, renderedRequired(t, rendered.RenderedJSON)) + }) + } +} + +func TestSchemaCacheKey_DirectionalKeysAreDistinct(t *testing.T) { + const schemaHash = uint64(100) + const version = float32(3.1) + + genericKey := SchemaCacheKey(schemaHash, version, SchemaValidationPurposeGeneric) + requestKey := SchemaCacheKey(schemaHash, version, SchemaValidationPurposeRequestBody) + responseKey := SchemaCacheKey(schemaHash, version, SchemaValidationPurposeResponseBody) + request30Key := SchemaCacheKey(schemaHash, 3.0, SchemaValidationPurposeRequestBody) + + assert.Equal(t, schemaHash, genericKey) + assert.NotEqual(t, genericKey, requestKey) + assert.NotEqual(t, genericKey, responseKey) + assert.NotEqual(t, requestKey, responseKey) + assert.NotEqual(t, requestKey, request30Key) +} + +func TestRenderSchemaForValidation_EdgeCases(t *testing.T) { + rendered, err := RenderSchemaForValidation(nil, SchemaValidationPurposeRequestBody) + require.NoError(t, err) + assert.Nil(t, rendered) + + schema := parseDirectionalSpecSchema(t, `openapi: 3.1.0 +info: + title: Test + version: 1.0.0 +components: + schemas: + Error: + type: object + properties: + details: + type: array + items: + $ref: '#/components/schemas/Error'`, "Error") + + rendered, err = RenderSchemaForValidation(schema, SchemaValidationPurposeRequestBody) + require.Error(t, err) + require.NotNil(t, rendered) +} + +func TestRenderSchemaBytesForValidation_Errors(t *testing.T) { + rendered, err := renderSchemaBytesForValidation([]byte(":\n"), SchemaValidationPurposeRequestBody) + require.Error(t, err) + assert.Nil(t, rendered) + assert.Contains(t, err.Error(), "schema render decode failed") +} + +func TestRenderSchemaBytesForValidation_RemovesEmptyRequired(t *testing.T) { + rendered, err := renderSchemaBytesForValidation([]byte(`type: object +required: + - id +properties: + id: + type: string + readOnly: true +`), SchemaValidationPurposeRequestBody) + require.NoError(t, err) + require.NotNil(t, rendered) + + assert.Nil(t, renderedRequired(t, rendered.RenderedJSON)) +} + +func TestRenderSchemaBytesForValidation_PrunesPrefixItems(t *testing.T) { + rendered, err := renderSchemaBytesForValidation([]byte(`type: array +prefixItems: + - type: object + required: + - id + properties: + id: + type: string + readOnly: true +`), SchemaValidationPurposeRequestBody) + require.NoError(t, err) + require.NotNil(t, rendered) + + var renderedMap map[string]any + require.NoError(t, json.Unmarshal(rendered.RenderedJSON, &renderedMap)) + prefixItems := renderedMap["prefixItems"].([]any) + firstItem := prefixItems[0].(map[string]any) + assert.NotContains(t, firstItem, "required") +} + +func TestDirectionalSchemaHelpers_EdgeCases(t *testing.T) { + pruneDirectionalRequired(nil, SchemaValidationPurposeRequestBody) + pruneDirectionalRequired(&yaml.Node{Kind: yaml.ScalarNode, Value: "scalar"}, SchemaValidationPurposeRequestBody) + + pruneRequiredAtSchema(&yaml.Node{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{ + {Kind: yaml.ScalarNode, Value: "required"}, + {Kind: yaml.SequenceNode, Content: []*yaml.Node{{Kind: yaml.ScalarNode, Value: "id"}}}, + }, + }, SchemaValidationPurposeRequestBody) + + pruneRequiredAtSchema(&yaml.Node{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{ + {Kind: yaml.ScalarNode, Value: "required"}, + {Kind: yaml.SequenceNode, Content: []*yaml.Node{nil, {Kind: yaml.MappingNode}}}, + {Kind: yaml.ScalarNode, Value: "properties"}, + {Kind: yaml.MappingNode}, + }, + }, SchemaValidationPurposeRequestBody) + + assert.False(t, propertySchemaHasDirectionalAnnotation(nil, SchemaValidationPurposeRequestBody)) + assert.False(t, propertySchemaHasDirectionalAnnotation(&yaml.Node{Kind: yaml.ScalarNode}, SchemaValidationPurposeRequestBody)) + + composed := &yaml.Node{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{ + {Kind: yaml.ScalarNode, Value: "oneOf"}, + { + Kind: yaml.SequenceNode, + Content: []*yaml.Node{{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{ + {Kind: yaml.ScalarNode, Value: "writeOnly"}, + {Kind: yaml.ScalarNode, Tag: "!!bool", Value: "true"}, + }, + }}, + }, + }, + } + assert.True(t, propertySchemaHasDirectionalAnnotation(composed, SchemaValidationPurposeResponseBody)) + + index, value := mappingPair(nil, "missing") + assert.Equal(t, -1, index) + assert.Nil(t, value) + + removeMappingPair(nil, -1) + node := &yaml.Node{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{ + {Kind: yaml.ScalarNode, Value: "required"}, + {Kind: yaml.SequenceNode}, + }, + } + removeMappingPair(node, 0) + assert.Empty(t, node.Content) +} + +func renderedRequired(t *testing.T, renderedJSON []byte) []string { + t.Helper() + + var rendered map[string]any + require.NoError(t, json.Unmarshal(renderedJSON, &rendered)) + + required, ok := rendered["required"].([]any) + if !ok { + return nil + } + + values := make([]string, 0, len(required)) + for _, item := range required { + values = append(values, item.(string)) + } + return values +} + +func parseDirectionalTestSchema(t *testing.T, schemaYAML string) *base.Schema { + t.Helper() + + return parseDirectionalSpecSchema(t, fmt.Sprintf(`openapi: 3.1.0 +info: + title: Test + version: 1.0.0 +components: + schemas: + TestSchema: +%s`, indentDirectionalSchemaLines(schemaYAML, " ")), "TestSchema") +} + +func parseDirectionalSpecSchema(t *testing.T, spec string, schemaName string) *base.Schema { + t.Helper() + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + model, errs := doc.BuildV3Model() + require.Empty(t, errs) + + schema := model.Model.Components.Schemas.GetOrZero(schemaName) + require.NotNil(t, schema) + return schema.Schema() +} + +func indentDirectionalSchemaLines(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") +} diff --git a/schema_validation/locate_schema_property_test.go b/schema_validation/locate_schema_property_test.go index a589069b..452c1b7a 100644 --- a/schema_validation/locate_schema_property_test.go +++ b/schema_validation/locate_schema_property_test.go @@ -6,7 +6,7 @@ package schema_validation import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" ) func TestLocateSchemaPropertyNodeByJSONPath_BadNode(t *testing.T) { diff --git a/schema_validation/openapi_schemas/load_schema_test.go b/schema_validation/openapi_schemas/load_schema_test.go index 76444f41..d5e8220f 100644 --- a/schema_validation/openapi_schemas/load_schema_test.go +++ b/schema_validation/openapi_schemas/load_schema_test.go @@ -9,8 +9,8 @@ import ( "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" ) // Mock server to simulate fetching remote files diff --git a/schema_validation/property_locator_test.go b/schema_validation/property_locator_test.go index 56b74ad3..d7aa2df4 100644 --- a/schema_validation/property_locator_test.go +++ b/schema_validation/property_locator_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" "go.yaml.in/yaml/v4" ) diff --git a/schema_validation/validate_document.go b/schema_validation/validate_document.go index d539db48..c98d92d8 100644 --- a/schema_validation/validate_document.go +++ b/schema_validation/validate_document.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation @@ -235,8 +235,17 @@ func ValidateOpenAPIDocumentWithPrecompiled(doc libopenapi.Document, compiledSch loadedSchema := info.APISchema var validationErrors []*liberrors.ValidationError - // Check if both JSON representations are nil before proceeding - if info.SpecJSON == nil && info.SpecJSONBytes == nil { + // libopenapi builds the JSON view of the document lazily: the deprecated + // SpecJSON / SpecJSONBytes fields stay nil until an accessor runs, and the + // accessors return nil when conversion is disabled (SkipJSONConversion) or + // the document cannot be represented as JSON. + specJSON := info.GetSpecJSON() + specJSONBytes := info.GetSpecJSONBytes() + + // Check if both JSON representations are unavailable before proceeding. + // Empty bytes count as unavailable: neither branch of the normalization + // ladder below would fire, and a nil document must not reach Validate. + if specJSON == nil && (specJSONBytes == nil || len(*specJSONBytes) == 0) { validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.Schema, ValidationSubType: "document", @@ -279,13 +288,13 @@ func ValidateOpenAPIDocumentWithPrecompiled(doc libopenapi.Document, compiledSch // 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 { + if specJSONBytes != nil && len(*specJSONBytes) > 0 { var err error - normalized, err = jsonschema.UnmarshalJSON(bytes.NewReader(*info.SpecJSONBytes)) + normalized, err = jsonschema.UnmarshalJSON(bytes.NewReader(*specJSONBytes)) if err != nil { // Fall back to normalizeJSON if UnmarshalJSON fails - if info.SpecJSON != nil { - normalized, err = normalizeJSON(*info.SpecJSON) + if specJSON != nil { + normalized, err = normalizeJSON(*specJSON) if err != nil { return false, []*liberrors.ValidationError{buildDocumentDecodeError( fmt.Sprintf("The OpenAPI document cannot be converted to JSON: %s", err.Error()), @@ -299,9 +308,9 @@ func ValidateOpenAPIDocumentWithPrecompiled(doc libopenapi.Document, compiledSch )} } } - } else if info.SpecJSON != nil { + } else if specJSON != nil { var err error - normalized, err = normalizeJSON(*info.SpecJSON) + normalized, err = normalizeJSON(*specJSON) if err != nil { return false, []*liberrors.ValidationError{buildDocumentDecodeError( fmt.Sprintf("The OpenAPI document cannot be converted to JSON: %s", err.Error()), @@ -310,6 +319,15 @@ func ValidateOpenAPIDocumentWithPrecompiled(doc libopenapi.Document, compiledSch } } + // belt and braces: never validate a nil document - it produces misleading + // "got null, want object" schema errors instead of a clear reason. + if normalized == nil { + return false, []*liberrors.ValidationError{buildDocumentDecodeError( + "The document has no usable JSON representation to validate", + "SpecJSON", + )} + } + // Validate the document scErrs := jsch.Validate(normalized) diff --git a/schema_validation/validate_document_test.go b/schema_validation/validate_document_test.go index 6f8e2f11..a02ef597 100644 --- a/schema_validation/validate_document_test.go +++ b/schema_validation/validate_document_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation @@ -9,7 +9,8 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" + "github.com/pb33f/libopenapi/datamodel" + "github.com/pb33f/testify/assert" "go.yaml.in/yaml/v4" "github.com/pb33f/libopenapi-validator/config" @@ -210,8 +211,12 @@ paths: {}` }, } info := doc.GetSpecInfo() - info.SpecJSON = &badSpecJSON - info.SpecJSONBytes = nil + // the JSON view is built lazily behind a sync.Once; latch it before + // injecting the poisoned values, otherwise the first accessor call inside + // the validator rebuilds the real JSON view and overwrites the injection. + _ = info.GetSpecJSONBytes() + info.SpecJSON = &badSpecJSON //nolint:staticcheck // test intentionally poisons lazy JSON cache fields + info.SpecJSONBytes = nil //nolint:staticcheck // test intentionally poisons lazy JSON cache fields valid, errors := ValidateOpenAPIDocument(doc) @@ -240,8 +245,10 @@ paths: {}` } corrupt := []byte(`{not valid json!!!}`) info := doc.GetSpecInfo() - info.SpecJSON = &badSpecJSON - info.SpecJSONBytes = &corrupt + // latch the lazy JSON build before injecting (see note in the test above) + _ = info.GetSpecJSONBytes() + info.SpecJSON = &badSpecJSON //nolint:staticcheck // test intentionally poisons lazy JSON cache fields + info.SpecJSONBytes = &corrupt //nolint:staticcheck // test intentionally poisons lazy JSON cache fields valid, errors := ValidateOpenAPIDocument(doc) @@ -427,12 +434,10 @@ info: 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 + // SkipJSONConversion disables the JSON view entirely: the lazy accessors + // return nil, which is the production scenario this guard protects against. + docConfig := &datamodel.DocumentConfiguration{SkipJSONConversion: true} + doc, _ := libopenapi.NewDocumentWithConfiguration([]byte(spec), docConfig) // validate! valid, errors := ValidateOpenAPIDocument(doc) @@ -507,9 +512,12 @@ func TestValidateDocument_SpecJSONBytesPath(t *testing.T) { info := doc.GetSpecInfo() + // The JSON view builds lazily; latch it before manipulating the fields so + // the validator's accessor calls don't rebuild and overwrite the setup. + assert.NotNil(t, info.GetSpecJSONBytes(), "SpecJSONBytes should be populated by libopenapi") + // 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 + info.SpecJSON = nil //nolint:staticcheck // test intentionally poisons lazy JSON cache fields valid, errs := ValidateOpenAPIDocument(doc) assert.True(t, valid) @@ -522,12 +530,16 @@ func TestValidateDocument_SpecJSONBytesCorrupt_NilSpecJSON(t *testing.T) { info := doc.GetSpecInfo() + // latch the lazy JSON build before injecting, so the validator's accessor + // calls return the injected values instead of rebuilding the real view. + _ = info.GetSpecJSONBytes() + // 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 + info.SpecJSONBytes = &corrupt //nolint:staticcheck // test intentionally poisons lazy JSON cache fields + info.SpecJSON = nil //nolint:staticcheck // test intentionally poisons lazy JSON cache fields // Validation should fail before JSON Schema validation instead of validating nil. valid, errs := ValidateOpenAPIDocument(doc) @@ -537,16 +549,40 @@ func TestValidateDocument_SpecJSONBytesCorrupt_NilSpecJSON(t *testing.T) { assert.Empty(t, errs[0].SchemaValidationErrors) } +func TestValidateDocument_SpecJSONBytesNullDoesNotValidateNil(t *testing.T) { + petstore, _ := os.ReadFile("../test_specs/petstorev3.json") + doc, _ := libopenapi.NewDocument(petstore) + + info := doc.GetSpecInfo() + _ = info.GetSpecJSONBytes() + + nullJSON := []byte("null") + info.SpecJSONBytes = &nullJSON //nolint:staticcheck // test intentionally poisons lazy JSON cache fields + info.SpecJSON = nil //nolint:staticcheck // test intentionally poisons lazy JSON cache fields + + valid, errs := ValidateOpenAPIDocument(doc) + + assert.False(t, valid) + assert.Len(t, errs, 1) + assert.Contains(t, errs[0].Reason, "no usable JSON representation") + assert.NotContains(t, errs[0].Reason, "got null, want object") + 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() + // latch the lazy JSON build before injecting, so the corrupt bytes + // actually reach the validator instead of being rebuilt over. + _ = info.GetSpecJSONBytes() + // 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 + info.SpecJSONBytes = &corrupt //nolint:staticcheck // test intentionally poisons lazy JSON cache fields // Should still validate successfully via the SpecJSON fallback valid, errs := ValidateOpenAPIDocument(doc) @@ -560,9 +596,10 @@ func TestValidateDocument_SpecJSONBytesPath_Invalid(t *testing.T) { info := doc.GetSpecInfo() - // Nil out SpecJSON but leave SpecJSONBytes intact - assert.NotNil(t, info.SpecJSONBytes, "SpecJSONBytes should be populated by libopenapi") - info.SpecJSON = nil + // latch the lazy JSON build, then nil out SpecJSON but leave + // SpecJSONBytes intact + assert.NotNil(t, info.GetSpecJSONBytes(), "SpecJSONBytes should be populated by libopenapi") + info.SpecJSON = nil //nolint:staticcheck // test intentionally poisons lazy JSON cache fields valid, errs := ValidateOpenAPIDocument(doc) assert.False(t, valid) diff --git a/schema_validation/validate_schema_coercion_test.go b/schema_validation/validate_schema_coercion_test.go index a8036dc3..a0a99bd8 100644 --- a/schema_validation/validate_schema_coercion_test.go +++ b/schema_validation/validate_schema_coercion_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" "github.com/pb33f/libopenapi-validator/config" ) diff --git a/schema_validation/validate_schema_extract_errors_test.go b/schema_validation/validate_schema_extract_errors_test.go index 825bffaa..1f263fed 100644 --- a/schema_validation/validate_schema_extract_errors_test.go +++ b/schema_validation/validate_schema_extract_errors_test.go @@ -6,8 +6,8 @@ package schema_validation import ( "testing" + "github.com/pb33f/testify/assert" "github.com/santhosh-tekuri/jsonschema/v6" - "github.com/stretchr/testify/assert" "go.yaml.in/yaml/v4" "golang.org/x/text/message" ) diff --git a/schema_validation/validate_schema_openapi_test.go b/schema_validation/validate_schema_openapi_test.go index a1932487..482e7e56 100644 --- a/schema_validation/validate_schema_openapi_test.go +++ b/schema_validation/validate_schema_openapi_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" "github.com/pb33f/libopenapi-validator/config" ) diff --git a/schema_validation/validate_schema_test.go b/schema_validation/validate_schema_test.go index 29ca82b0..6ed66dfb 100644 --- a/schema_validation/validate_schema_test.go +++ b/schema_validation/validate_schema_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" "go.yaml.in/yaml/v4" ) diff --git a/schema_validation/validate_urlencoded_test.go b/schema_validation/validate_urlencoded_test.go index 4fb928ca..59ecd4e1 100644 --- a/schema_validation/validate_urlencoded_test.go +++ b/schema_validation/validate_urlencoded_test.go @@ -9,7 +9,7 @@ import ( "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" + "github.com/pb33f/testify/assert" ) func TestIsURLEncodedContentType(t *testing.T) { diff --git a/schema_validation/validate_xml_test.go b/schema_validation/validate_xml_test.go index 1b6292f5..48f62f5c 100644 --- a/schema_validation/validate_xml_test.go +++ b/schema_validation/validate_xml_test.go @@ -9,7 +9,7 @@ import ( "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/orderedmap" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" ) func TestValidateXML_Issue346_BasicXMLWithName(t *testing.T) { diff --git a/strict/types_test.go b/strict/types_test.go index fcd30916..15cd1468 100644 --- a/strict/types_test.go +++ b/strict/types_test.go @@ -8,8 +8,8 @@ import ( "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" ) func TestExtractSchemaLocation_NilSchema(t *testing.T) { diff --git a/strict/utils_test.go b/strict/utils_test.go index cdf4be17..d2bf3c3b 100644 --- a/strict/utils_test.go +++ b/strict/utils_test.go @@ -6,7 +6,7 @@ package strict import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/pb33f/testify/assert" ) func TestCompilePattern_EscapedDoubleAsterisk(t *testing.T) { diff --git a/strict/validator_test.go b/strict/validator_test.go index 6e88b432..0f82bba4 100644 --- a/strict/validator_test.go +++ b/strict/validator_test.go @@ -12,8 +12,8 @@ import ( "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" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" libcache "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi-validator/config" diff --git a/validator.go b/validator.go index 96b9f899..820fafd4 100644 --- a/validator.go +++ b/validator.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package validator @@ -11,8 +11,6 @@ import ( "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" @@ -408,6 +406,7 @@ func warmSchemaCaches( } schemaCache := options.SchemaCache + version := helpers.VersionToFloat(doc.Version) // Walk through all paths and operations for pathPair := doc.Paths.PathItems.First(); pathPair != nil; pathPair = pathPair.Next() { @@ -415,22 +414,17 @@ func warmSchemaCaches( // 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) + warmMediaTypeSchema(mediaType, schemaCache, options, version, + schema_validation.SchemaValidationPurposeRequestBody) } } } @@ -445,7 +439,8 @@ func warmSchemaCaches( for contentPair := response.Content.First(); contentPair != nil; contentPair = contentPair.Next() { mediaType := contentPair.Value() if mediaType.Schema != nil { - warmMediaTypeSchema(mediaType, schemaCache, options) + warmMediaTypeSchema(mediaType, schemaCache, options, version, + schema_validation.SchemaValidationPurposeResponseBody) } } } @@ -457,7 +452,8 @@ func warmSchemaCaches( for contentPair := operation.Responses.Default.Content.First(); contentPair != nil; contentPair = contentPair.Next() { mediaType := contentPair.Value() if mediaType.Schema != nil { - warmMediaTypeSchema(mediaType, schemaCache, options) + warmMediaTypeSchema(mediaType, schemaCache, options, version, + schema_validation.SchemaValidationPurposeResponseBody) } } } @@ -467,7 +463,7 @@ func warmSchemaCaches( if operation.Parameters != nil { for _, param := range operation.Parameters { if param != nil { - warmParameterSchema(param, schemaCache, options) + warmParameterSchema(param, schemaCache, options, version) } } } @@ -477,7 +473,7 @@ func warmSchemaCaches( if pathItem.Parameters != nil { for _, param := range pathItem.Parameters { if param != nil { - warmParameterSchema(param, schemaCache, options) + warmParameterSchema(param, schemaCache, options, version) } } } @@ -485,87 +481,94 @@ func warmSchemaCaches( } // warmMediaTypeSchema warms the cache for a media type schema -func warmMediaTypeSchema(mediaType *v3.MediaType, schemaCache cache.SchemaCache, options *config.ValidationOptions) { +func warmMediaTypeSchema( + mediaType *v3.MediaType, + schemaCache cache.SchemaCache, + options *config.ValidationOptions, + version float32, + purpose schema_validation.SchemaValidationPurpose, +) { if mediaType != nil && mediaType.Schema != nil { - hash := mediaType.GoLow().Schema.Value.Hash() + schema := mediaType.Schema.Schema() + if schema == nil || schema.GoLow() == nil { + return + } + hash := schema_validation.SchemaCacheKey(schema.GoLow().Hash(), version, purpose) 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, - }) - } + rendered, err := schema_validation.RenderSchemaForValidation(schema, purpose) + if err != nil || rendered == nil || len(rendered.RenderedInline) == 0 { + return } + compiledSchema, compileErr := helpers.NewCompiledSchemaWithVersion( + fmt.Sprintf("%x", hash), + rendered.RenderedJSON, + options, + version, + ) + if compileErr != nil || compiledSchema == nil { + return + } + + schemaCache.Store(hash, &cache.SchemaCacheEntry{ + Schema: schema, + RenderedInline: rendered.RenderedInline, + ReferenceSchema: rendered.ReferenceSchema, + RenderedJSON: rendered.RenderedJSON, + CompiledSchema: compiledSchema, + RenderedNode: rendered.RenderedNode, + }) } } } // warmParameterSchema warms the cache for a parameter schema -func warmParameterSchema(param *v3.Parameter, schemaCache cache.SchemaCache, options *config.ValidationOptions) { +func warmParameterSchema(param *v3.Parameter, schemaCache cache.SchemaCache, options *config.ValidationOptions, version float32) { 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 schema != nil && schema.GoLow() != nil { + hash := schema_validation.SchemaCacheKey(schema.GoLow().Hash(), version, + schema_validation.SchemaValidationPurposeGeneric) 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, - }) + rendered, err := schema_validation.RenderSchemaForValidation(schema, + schema_validation.SchemaValidationPurposeGeneric) + if err != nil || rendered == nil || len(rendered.RenderedInline) == 0 { + return + } + compiledSchema, compileErr := helpers.NewCompiledSchemaWithVersion( + fmt.Sprintf("%x", hash), + rendered.RenderedJSON, + options, + version, + ) + if compileErr != nil || compiledSchema == nil { + return } + + // Store in cache using the shared SchemaCache type + schemaCache.Store(hash, &cache.SchemaCacheEntry{ + Schema: schema, + RenderedInline: rendered.RenderedInline, + ReferenceSchema: rendered.ReferenceSchema, + RenderedJSON: rendered.RenderedJSON, + CompiledSchema: compiledSchema, + RenderedNode: rendered.RenderedNode, + }) } } } diff --git a/validator_nullable_enum_test.go b/validator_nullable_enum_test.go index 26931659..feedff42 100644 --- a/validator_nullable_enum_test.go +++ b/validator_nullable_enum_test.go @@ -12,8 +12,8 @@ import ( "testing" "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" ) // TestNullableEnum_ResponseValidation_NullValue tests that nullable enum fields diff --git a/validator_test.go b/validator_test.go index 126bb631..3567c378 100644 --- a/validator_test.go +++ b/validator_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package validator @@ -19,16 +19,18 @@ import ( "github.com/dlclark/regexp2" "github.com/pb33f/libopenapi" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" "github.com/santhosh-tekuri/jsonschema/v6" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/pb33f/libopenapi/datamodel/high/base" 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/schema_validation" ) func TestNewValidator(t *testing.T) { @@ -2160,6 +2162,134 @@ func TestCacheWarming_PopulatesCache(t *testing.T) { assert.Greater(t, count, 0, "Schema cache should have entries from request and response bodies") } +func TestDirectionalRequiredProperties_RequestResponseSharedSchema(t *testing.T) { + spec := `openapi: 3.1.0 +info: + title: Test + version: 1.0.0 +paths: + /users/123: + post: + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/User' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/User' +components: + schemas: + User: + type: object + required: + - id + - name + - password + properties: + id: + type: string + readOnly: true + name: + type: string + password: + type: string + writeOnly: true` + + for _, tc := range []struct { + name string + run func(t *testing.T, v Validator) + }{ + { + name: "request then response", + run: func(t *testing.T, v Validator) { + req := issue281Request(t, `{"name":"John","password":"secret"}`) + valid, errs := v.ValidateHttpRequest(req) + require.True(t, valid) + require.Empty(t, errs) + + res := issue281Response(`{"id":"123","name":"John"}`) + valid, errs = v.ValidateHttpResponse(req, res) + require.True(t, valid) + require.Empty(t, errs) + }, + }, + { + name: "response then request", + run: func(t *testing.T, v Validator) { + req := issue281Request(t, "") + res := issue281Response(`{"id":"123","name":"John"}`) + valid, errs := v.ValidateHttpResponse(req, res) + require.True(t, valid) + require.Empty(t, errs) + + req = issue281Request(t, `{"name":"John","password":"secret"}`) + valid, errs = v.ValidateHttpRequest(req) + require.True(t, valid) + require.Empty(t, errs) + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + v, errs := NewValidator(doc) + require.Nil(t, errs) + + validator := v.(*validator) + userSchema := validator.v3Model.Components.Schemas.GetOrZero("User") + require.NotNil(t, userSchema) + require.NotNil(t, userSchema.Schema()) + + requestKey := schema_validation.SchemaCacheKey( + userSchema.Schema().GoLow().Hash(), + 3.1, + schema_validation.SchemaValidationPurposeRequestBody, + ) + responseKey := schema_validation.SchemaCacheKey( + userSchema.Schema().GoLow().Hash(), + 3.1, + schema_validation.SchemaValidationPurposeResponseBody, + ) + + _, requestWarmed := validator.options.SchemaCache.Load(requestKey) + _, responseWarmed := validator.options.SchemaCache.Load(responseKey) + require.True(t, requestWarmed, "request schema variant should be warmed") + require.True(t, responseWarmed, "response schema variant should be warmed") + require.NotEqual(t, requestKey, responseKey) + + tc.run(t, v) + }) + } +} + +func issue281Request(t *testing.T, payload string) *http.Request { + var body io.Reader = http.NoBody + if payload != "" { + body = strings.NewReader(payload) + } + req, err := http.NewRequest(http.MethodPost, "https://things.com/users/123", body) + require.NoError(t, err) + req.Header.Set(helpers.ContentTypeHeader, "application/json") + return req +} + +func issue281Response(payload string) *http.Response { + return &http.Response{ + StatusCode: http.StatusCreated, + Header: http.Header{ + helpers.ContentTypeHeader: []string{"application/json"}, + }, + Body: io.NopCloser(strings.NewReader(payload)), + } +} + func TestCacheWarming_EdgeCases(t *testing.T) { // Test nil document warmSchemaCaches(nil, nil) @@ -2254,6 +2384,51 @@ paths: assert.NotNil(t, v) } +func TestCacheWarming_MediaTypeSchemaWithNilGoLow(t *testing.T) { + options := config.NewValidationOptions() + mediaType := &v3.MediaType{ + Schema: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + }), + } + + warmMediaTypeSchema(mediaType, options.SchemaCache, options, 3.1, + schema_validation.SchemaValidationPurposeRequestBody) + + assert.Equal(t, 0, schemaCacheEntryCount(options.SchemaCache)) +} + +func TestCacheWarming_ParameterRenderFailure(t *testing.T) { + param := cacheWarmingTestParameter(t, `schema: + $ref: '#/components/schemas/Error'`, ` +components: + schemas: + Error: + type: object + properties: + code: + type: string + details: + type: array + items: + $ref: '#/components/schemas/Error'`) + options := config.NewValidationOptions() + + warmParameterSchema(param, options.SchemaCache, options, 3.1) + + assert.Equal(t, 0, schemaCacheEntryCount(options.SchemaCache)) +} + +func TestCacheWarming_ParameterCompileFailure(t *testing.T) { + param := cacheWarmingTestParameter(t, `schema: + type: invalid-type-that-does-not-exist`, "") + options := config.NewValidationOptions() + + warmParameterSchema(param, options.SchemaCache, options, 3.1) + + assert.Equal(t, 0, schemaCacheEntryCount(options.SchemaCache)) +} + func TestCacheWarming_DefaultResponse(t *testing.T) { spec := `openapi: 3.1.0 paths: @@ -2396,6 +2571,48 @@ paths: assert.Greater(t, count, 0, "Schema cache should have entries from path-level parameters") } +func cacheWarmingTestParameter(t *testing.T, schemaYAML string, componentsYAML string) *v3.Parameter { + t.Helper() + + spec := fmt.Sprintf(`openapi: 3.1.0 +info: + title: Test + version: 1.0.0 +paths: + /test: + get: + parameters: + - name: filter + in: query + %s + responses: + '200': + description: Success +%s`, schemaYAML, componentsYAML) + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + model, errs := doc.BuildV3Model() + require.Empty(t, errs) + + pathItem := model.Model.Paths.PathItems.GetOrZero("/test") + require.NotNil(t, pathItem) + require.NotNil(t, pathItem.Get) + require.Len(t, pathItem.Get.Parameters, 1) + + return pathItem.Get.Parameters[0] +} + +func schemaCacheEntryCount(schemaCache cache.SchemaCache) int { + count := 0 + schemaCache.Range(func(key uint64, value *cache.SchemaCacheEntry) bool { + count++ + return true + }) + return count +} + // TestSortValidationErrors tests that validation errors are sorted deterministically func TestSortValidationErrors(t *testing.T) { // Create errors in random order