@@ -12,6 +12,7 @@ import (
1212
1313 lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3"
1414
15+ "github.com/pb33f/libopenapi-validator/cache"
1516 "github.com/pb33f/libopenapi-validator/config"
1617 "github.com/pb33f/libopenapi-validator/helpers"
1718)
@@ -678,3 +679,161 @@ func BenchmarkValidationWithRegexCache(b *testing.B) {
678679 validator .ValidatePathParams (req )
679680 }
680681}
682+
683+ // cacheTestSpec is an OpenAPI spec for testing cache behavior
684+ var cacheTestSpec = []byte (`{
685+ "openapi": "3.1.0",
686+ "info": {
687+ "title": "Cache Test API",
688+ "version": "1.0.0"
689+ },
690+ "paths": {
691+ "/items/{id}": {
692+ "get": {
693+ "operationId": "getItem",
694+ "parameters": [
695+ {
696+ "name": "id",
697+ "in": "path",
698+ "required": true,
699+ "schema": {
700+ "type": "string",
701+ "minLength": 1,
702+ "maxLength": 64
703+ }
704+ },
705+ {
706+ "name": "limit",
707+ "in": "query",
708+ "schema": {
709+ "type": "integer",
710+ "minimum": 1,
711+ "maximum": 100
712+ }
713+ }
714+ ],
715+ "responses": {
716+ "200": {
717+ "description": "OK"
718+ }
719+ }
720+ }
721+ }
722+ }
723+ }` )
724+
725+ // Test_ParameterValidation_CacheUsage verifies that parameter validation uses the schema cache.
726+ // This test validates that:
727+ // 1. Cache is populated after the first validation
728+ // 2. Subsequent validations reuse the cached compiled schemas
729+ // 3. Validation still produces correct results when using cached schemas
730+ func Test_ParameterValidation_CacheUsage (t * testing.T ) {
731+ doc , err := libopenapi .NewDocument (cacheTestSpec )
732+ require .NoError (t , err , "Failed to create document" )
733+
734+ v3Model , errs := doc .BuildV3Model ()
735+ require .Nil (t , errs , "Failed to build v3 model" )
736+
737+ // Create options with cache (default behavior)
738+ opts := config .NewValidationOptions ()
739+ require .NotNil (t , opts .SchemaCache , "Schema cache should be initialized by default" )
740+
741+ validator := NewParameterValidator (& v3Model .Model , config .WithExistingOpts (opts ))
742+
743+ // First request - should populate cache
744+ req1 , _ := http .NewRequest ("GET" , "/items/abc123?limit=50" , nil )
745+ isSuccess1 , errors1 := validator .ValidateQueryParams (req1 )
746+ assert .True (t , isSuccess1 , "First validation should succeed" )
747+ assert .Empty (t , errors1 , "First validation should have no errors" )
748+
749+ // Count cached entries (should have at least the limit parameter schema)
750+ cacheCount := 0
751+ opts .SchemaCache .Range (func (key uint64 , value * cache.SchemaCacheEntry ) bool {
752+ cacheCount ++
753+ return true
754+ })
755+ assert .Greater (t , cacheCount , 0 , "Cache should have entries after first validation" )
756+
757+ // Second request with different valid value - should use cached schema
758+ req2 , _ := http .NewRequest ("GET" , "/items/xyz789?limit=75" , nil )
759+ isSuccess2 , errors2 := validator .ValidateQueryParams (req2 )
760+ assert .True (t , isSuccess2 , "Second validation should succeed" )
761+ assert .Empty (t , errors2 , "Second validation should have no errors" )
762+
763+ // Third request with invalid value - should still use cached schema but fail validation
764+ req3 , _ := http .NewRequest ("GET" , "/items/test?limit=999" , nil )
765+ isSuccess3 , errors3 := validator .ValidateQueryParams (req3 )
766+ assert .False (t , isSuccess3 , "Third validation should fail (limit > maximum)" )
767+ assert .NotEmpty (t , errors3 , "Third validation should have errors" )
768+ }
769+
770+ // Test_ParameterValidation_WithoutCache verifies that validation works when cache is disabled.
771+ func Test_ParameterValidation_WithoutCache (t * testing.T ) {
772+ doc , err := libopenapi .NewDocument (cacheTestSpec )
773+ require .NoError (t , err , "Failed to create document" )
774+
775+ v3Model , errs := doc .BuildV3Model ()
776+ require .Nil (t , errs , "Failed to build v3 model" )
777+
778+ // Create options without cache
779+ opts := config .NewValidationOptions (config .WithSchemaCache (nil ))
780+ require .Nil (t , opts .SchemaCache , "Schema cache should be nil" )
781+
782+ validator := NewParameterValidator (& v3Model .Model , config .WithExistingOpts (opts ))
783+
784+ // Validation should still work without cache
785+ req , _ := http .NewRequest ("GET" , "/items/abc123?limit=50" , nil )
786+ isSuccess , errors := validator .ValidateQueryParams (req )
787+ assert .True (t , isSuccess , "Validation should succeed without cache" )
788+ assert .Empty (t , errors , "Validation should have no errors" )
789+
790+ // Validation with invalid value should fail
791+ req2 , _ := http .NewRequest ("GET" , "/items/abc123?limit=999" , nil )
792+ isSuccess2 , errors2 := validator .ValidateQueryParams (req2 )
793+ assert .False (t , isSuccess2 , "Validation should fail for invalid value" )
794+ assert .NotEmpty (t , errors2 , "Validation should report errors" )
795+ }
796+
797+ // Test_ParameterValidation_CacheConsistency verifies that cached schemas produce
798+ // the same validation results as freshly compiled schemas.
799+ func Test_ParameterValidation_CacheConsistency (t * testing.T ) {
800+ doc , err := libopenapi .NewDocument (cacheTestSpec )
801+ require .NoError (t , err , "Failed to create document" )
802+
803+ v3Model , errs := doc .BuildV3Model ()
804+ require .Nil (t , errs , "Failed to build v3 model" )
805+
806+ // Run the same validations with and without cache
807+ testCases := []struct {
808+ name string
809+ url string
810+ expected bool
811+ }{
812+ {"valid_limit" , "/items/abc?limit=50" , true },
813+ {"limit_at_max" , "/items/abc?limit=100" , true },
814+ {"limit_at_min" , "/items/abc?limit=1" , true },
815+ {"limit_too_high" , "/items/abc?limit=101" , false },
816+ {"limit_too_low" , "/items/abc?limit=0" , false },
817+ }
818+
819+ // First run with cache
820+ optsWithCache := config .NewValidationOptions ()
821+ validatorWithCache := NewParameterValidator (& v3Model .Model , config .WithExistingOpts (optsWithCache ))
822+
823+ // Second run without cache
824+ optsNoCache := config .NewValidationOptions (config .WithSchemaCache (nil ))
825+ validatorNoCache := NewParameterValidator (& v3Model .Model , config .WithExistingOpts (optsNoCache ))
826+
827+ for _ , tc := range testCases {
828+ t .Run (tc .name , func (t * testing.T ) {
829+ req , _ := http .NewRequest ("GET" , tc .url , nil )
830+
831+ successWithCache , errorsWithCache := validatorWithCache .ValidateQueryParams (req )
832+ successNoCache , errorsNoCache := validatorNoCache .ValidateQueryParams (req )
833+
834+ assert .Equal (t , tc .expected , successWithCache , "Cached validation result mismatch for %s" , tc .name )
835+ assert .Equal (t , successWithCache , successNoCache , "Cache vs no-cache results should match for %s" , tc .name )
836+ assert .Equal (t , len (errorsWithCache ), len (errorsNoCache ), "Error count should match for %s" , tc .name )
837+ })
838+ }
839+ }
0 commit comments