Skip to content

Commit

Permalink
string and number path param validation
Browse files Browse the repository at this point in the history
  • Loading branch information
emilien-puget committed Mar 6, 2024
1 parent 13c8231 commit 56c3b8b
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 127 deletions.
89 changes: 57 additions & 32 deletions parameters/path_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ package parameters

import (
"fmt"
"net/http"
"strconv"
"strings"

"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/libopenapi-validator/helpers"
"github.com/pb33f/libopenapi-validator/paths"
"github.com/pb33f/libopenapi/datamodel/high/base"
"github.com/pb33f/libopenapi/datamodel/high/v3"
"net/http"
"strconv"
"strings"
)

func (v *paramValidator) ValidatePathParams(request *http.Request) (bool, []*errors.ValidationError) {
Expand Down Expand Up @@ -41,7 +43,7 @@ func (v *paramValidator) ValidatePathParams(request *http.Request) (bool, []*err
submittedSegments := strings.Split(request.URL.Path, helpers.Slash)
pathSegments := strings.Split(foundPath, helpers.Slash)

//var paramTemplate string
// var paramTemplate string
for x := range pathSegments {
if pathSegments[x] == "" { // skip empty segments
continue
Expand All @@ -50,13 +52,13 @@ func (v *paramValidator) ValidatePathParams(request *http.Request) (bool, []*err
if i > -1 {
isMatrix := false
isLabel := false
//isExplode := false
// isExplode := false
isSimple := true
paramTemplate := pathSegments[x][i+1 : len(pathSegments[x])-1]
paramName := paramTemplate
// check for an asterisk on the end of the parameter (explode)
if strings.HasSuffix(paramTemplate, helpers.Asterisk) {
//isExplode = true
// isExplode = true
paramName = paramTemplate[:len(paramTemplate)-1]
}
if strings.HasPrefix(paramTemplate, helpers.Period) {
Expand Down Expand Up @@ -116,41 +118,40 @@ func (v *paramValidator) ValidatePathParams(request *http.Request) (bool, []*err
// check if the param is within the enum
if sch.Enum != nil {
enumCheck(paramValue)
break
}
validationErrors = append(validationErrors,
ValidateSingleParameterSchema(
sch,
paramValue,
"Path parameter",
"The path parameter",
p.Name,
helpers.ParameterValidation,
helpers.ParameterValidationPath,
)...)

case helpers.Integer, helpers.Number:
// simple use case is already handled in find param.
if isLabel && p.Style == helpers.LabelStyle {
if _, err := strconv.ParseFloat(paramValue[1:], 64); err != nil {
validationErrors = append(validationErrors,
errors.IncorrectPathParamNumber(p, paramValue[1:], sch))
break
}
// check if the param is within the enum
if sch.Enum != nil {
enumCheck(paramValue[1:])
break
}
}
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)
if _, err := strconv.ParseFloat(paramValue, 64); err != nil {
validationErrors = append(validationErrors,
errors.IncorrectPathParamNumber(p, paramValue[1:], sch))
break
}
// check if the param is within the enum
if sch.Enum != nil {
enumCheck(paramValue)
break
}
rawParamValue, paramValueParsed, err := v.resolveNumber(sch, p, isLabel, isMatrix, paramValue)
if err != nil {
validationErrors = append(validationErrors, err...)
break
}
// check if the param is within the enum
if sch.Enum != nil {
enumCheck(paramValue)
enumCheck(rawParamValue)
break
}
validationErrors = append(validationErrors, ValidateSingleParameterSchema(
sch,
paramValueParsed,
"Path parameter",
"The path parameter",
p.Name,
helpers.ParameterValidation,
helpers.ParameterValidationPath,
)...)

case helpers.Boolean:
if isLabel && p.Style == helpers.LabelStyle {
Expand Down Expand Up @@ -280,3 +281,27 @@ func (v *paramValidator) ValidatePathParams(request *http.Request) (bool, []*err
}
return true, nil
}

func (v *paramValidator) resolveNumber(sch *base.Schema, p *v3.Parameter, isLabel bool, isMatrix bool, paramValue 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)}
}
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)}
}
return paramValue, paramValueParsed, nil
}
paramValueParsed, err := strconv.ParseFloat(paramValue, 64)
if err != nil {
return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue[1:], sch)}
}
return paramValue, paramValueParsed, nil
}
157 changes: 154 additions & 3 deletions parameters/path_parameters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
package parameters

import (
"net/http"
"testing"

"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi-validator/paths"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
"github.com/stretchr/testify/require"
)

func TestNewValidator_SimpleArrayEncodedPath(t *testing.T) {
Expand Down Expand Up @@ -280,7 +282,64 @@ paths:

assert.False(t, valid)
assert.Len(t, errors, 1)
assert.Equal(t, "GET Path '/burgers/hello/locate' not found", errors[0].Message)
assert.Equal(t, "Path parameter 'burgerId' is not a valid number", errors[0].Message)
}

func TestNewValidator_SimpleEncodedPath_IntegerViolation(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: must be >= 10 but found 1, Location: /minimum", errors[0].SchemaValidationErrors[0].Error())
}

func TestNewValidator_SimpleEncodedPath_Integer(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_InvalidBoolean(t *testing.T) {
Expand Down Expand Up @@ -338,6 +397,37 @@ paths:
assert.Equal(t, "Path parameter 'burgerId' is not a valid number", errors[0].Message)
}

func TestNewValidator_LabelEncodedPath_IntegerViolation(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: must be >= 10 but found 3, Location: /minimum", errors[0].SchemaValidationErrors[0].Error())
}

func TestNewValidator_LabelEncodedPath_InvalidBoolean(t *testing.T) {

spec := `openapi: 3.1.0
Expand Down Expand Up @@ -684,6 +774,37 @@ paths:
assert.Equal(t, "Path parameter 'burgerId' is not a valid number", errors[0].Message)
}

func TestNewValidator_MatrixEncodedPath_PrimitiveNumberViolation(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: must be >= 5 but found 3, Location: /minimum", errors[0].SchemaValidationErrors[0].Error())
}

func TestNewValidator_MatrixEncodedPath_ValidPrimitiveBoolean(t *testing.T) {

spec := `openapi: 3.1.0
Expand Down Expand Up @@ -1075,6 +1196,36 @@ paths:

}

func TestNewValidator_PathParamStringViolation(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: length must be >= 4, but got 3, Location: /minLength", errors[0].SchemaValidationErrors[0].Error())
}

func TestNewValidator_PathParamIntegerEnumValid(t *testing.T) {

spec := `openapi: 3.1.0
Expand Down
Loading

0 comments on commit 56c3b8b

Please sign in to comment.