Skip to content

Commit 1d13412

Browse files
byteddaveshanley
authored andcommitted
Add coverage tests for readOnly/writeOnly rejection paths
Cover checkReadWriteOnlyViolation calls in: - recurseIntoDeclaredProperties (explicit + pattern properties) - validateVariantWithParent (oneOf) - recurseIntoDeclaredPropertiesWithMerged (oneOf + additionalProperties: false) - recurseIntoAllOfDeclaredProperties (allOf + additionalProperties: false) - WithStrictRejectReadOnly/WithStrictRejectWriteOnly option functions - WithExistingOpts copy of new fields
1 parent 3bca3df commit 1d13412

5 files changed

Lines changed: 315 additions & 2 deletions

File tree

config/config_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,8 @@ func TestWithLogger(t *testing.T) {
466466
func TestWithExistingOpts_StrictFields(t *testing.T) {
467467
original := &ValidationOptions{
468468
StrictMode: true,
469+
StrictRejectReadOnly: true,
470+
StrictRejectWriteOnly: true,
469471
StrictIgnorePaths: []string{"$.body.*"},
470472
StrictIgnoredHeaders: []string{"x-custom"},
471473
strictIgnoredHeadersMerge: true,
@@ -475,12 +477,24 @@ func TestWithExistingOpts_StrictFields(t *testing.T) {
475477
opts := NewValidationOptions(WithExistingOpts(original))
476478

477479
assert.True(t, opts.StrictMode)
480+
assert.True(t, opts.StrictRejectReadOnly)
481+
assert.True(t, opts.StrictRejectWriteOnly)
478482
assert.Equal(t, original.StrictIgnorePaths, opts.StrictIgnorePaths)
479483
assert.Equal(t, original.StrictIgnoredHeaders, opts.StrictIgnoredHeaders)
480484
assert.True(t, opts.strictIgnoredHeadersMerge)
481485
assert.Equal(t, original.Logger, opts.Logger)
482486
}
483487

488+
func TestWithStrictRejectReadOnly(t *testing.T) {
489+
opts := NewValidationOptions(WithStrictRejectReadOnly())
490+
assert.True(t, opts.StrictRejectReadOnly)
491+
}
492+
493+
func TestWithStrictRejectWriteOnly(t *testing.T) {
494+
opts := NewValidationOptions(WithStrictRejectWriteOnly())
495+
assert.True(t, opts.StrictRejectWriteOnly)
496+
}
497+
484498
func TestStrictModeWithIgnorePaths(t *testing.T) {
485499
paths := []string{"$.body.metadata.*"}
486500
opts := NewValidationOptions(

requests/validate_body_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,50 @@ paths:
16141614
assert.Len(t, errors, 0)
16151615
}
16161616

1617+
func TestValidateBody_StrictMode_ReadOnlyProperty(t *testing.T) {
1618+
spec := `openapi: 3.1.0
1619+
paths:
1620+
/users:
1621+
post:
1622+
requestBody:
1623+
content:
1624+
application/json:
1625+
schema:
1626+
type: object
1627+
properties:
1628+
id:
1629+
type: string
1630+
readOnly: true
1631+
name:
1632+
type: string`
1633+
1634+
doc, _ := libopenapi.NewDocument([]byte(spec))
1635+
1636+
m, _ := doc.BuildV3Model()
1637+
v := NewRequestBodyValidator(&m.Model,
1638+
config.WithStrictMode(),
1639+
config.WithStrictRejectReadOnly(),
1640+
)
1641+
1642+
body := map[string]interface{}{
1643+
"id": "user-123",
1644+
"name": "John",
1645+
}
1646+
1647+
bodyBytes, _ := json.Marshal(body)
1648+
1649+
request, _ := http.NewRequest(http.MethodPost, "https://things.com/users",
1650+
bytes.NewBuffer(bodyBytes))
1651+
request.Header.Set("Content-Type", "application/json")
1652+
1653+
valid, errors := v.ValidateRequestBody(request)
1654+
1655+
assert.False(t, valid)
1656+
assert.Len(t, errors, 1)
1657+
assert.Contains(t, errors[0].Message, "readOnly")
1658+
assert.Contains(t, errors[0].Message, "id")
1659+
}
1660+
16171661
func TestValidateRequestBody_XMLMarshalError(t *testing.T) {
16181662
spec := []byte(`
16191663
openapi: 3.1.0

responses/validate_body_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,6 +1619,50 @@ paths:
16191619
assert.Len(t, errs, 0)
16201620
}
16211621

1622+
func TestValidateBody_StrictMode_WriteOnlyProperty(t *testing.T) {
1623+
spec := `openapi: 3.1.0
1624+
paths:
1625+
/users/123:
1626+
get:
1627+
responses:
1628+
'200':
1629+
content:
1630+
application/json:
1631+
schema:
1632+
type: object
1633+
properties:
1634+
name:
1635+
type: string
1636+
password:
1637+
type: string
1638+
writeOnly: true`
1639+
1640+
doc, _ := libopenapi.NewDocument([]byte(spec))
1641+
1642+
m, _ := doc.BuildV3Model()
1643+
v := NewResponseBodyValidator(&m.Model,
1644+
config.WithStrictMode(),
1645+
config.WithStrictRejectWriteOnly(),
1646+
)
1647+
1648+
request, _ := http.NewRequest(http.MethodGet, "https://things.com/users/123", nil)
1649+
1650+
responseBody := `{"name": "John", "password": "secret"}`
1651+
response := &http.Response{
1652+
Header: http.Header{},
1653+
StatusCode: http.StatusOK,
1654+
Body: io.NopCloser(strings.NewReader(responseBody)),
1655+
}
1656+
response.Header.Set("Content-Type", "application/json")
1657+
1658+
valid, errs := v.ValidateResponseBody(request, response)
1659+
1660+
assert.False(t, valid)
1661+
assert.Len(t, errs, 1)
1662+
assert.Contains(t, errs[0].Message, "writeOnly")
1663+
assert.Contains(t, errs[0].Message, "password")
1664+
}
1665+
16221666
func TestValidateBody_ValidURLEncodedBody(t *testing.T) {
16231667
spec := `openapi: 3.1.0
16241668
paths:

strict/validator_test.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6666,3 +6666,214 @@ components:
66666666
assert.Equal(t, "password", resultResponse.UndeclaredValues[0].Name)
66676667
assert.Equal(t, TypeWriteOnlyProperty, resultResponse.UndeclaredValues[0].Type)
66686668
}
6669+
6670+
func TestStrictValidator_RejectReadOnly_AdditionalPropertiesFalse(t *testing.T) {
6671+
// Covers schema_walker.go recurseIntoDeclaredProperties:
6672+
// explicit property path and patternProperties path
6673+
yml := `openapi: "3.1.0"
6674+
info:
6675+
title: Test
6676+
version: "1.0"
6677+
paths: {}
6678+
components:
6679+
schemas:
6680+
Strict:
6681+
type: object
6682+
additionalProperties: false
6683+
properties:
6684+
id:
6685+
type: string
6686+
readOnly: true
6687+
name:
6688+
type: string
6689+
patternProperties:
6690+
"^x-":
6691+
type: string
6692+
readOnly: true
6693+
`
6694+
model := buildSchemaFromYAML(t, yml)
6695+
schema := getSchema(t, model, "Strict")
6696+
6697+
opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly())
6698+
v := NewValidator(opts, 3.1)
6699+
6700+
data := map[string]any{
6701+
"id": "user-1",
6702+
"name": "John",
6703+
"x-custom": "value",
6704+
}
6705+
6706+
result := v.Validate(Input{
6707+
Schema: schema,
6708+
Data: data,
6709+
Direction: DirectionRequest,
6710+
Options: opts,
6711+
BasePath: "$.body",
6712+
Version: 3.1,
6713+
})
6714+
6715+
assert.False(t, result.Valid)
6716+
assert.Len(t, result.UndeclaredValues, 2)
6717+
6718+
names := []string{result.UndeclaredValues[0].Name, result.UndeclaredValues[1].Name}
6719+
assert.Contains(t, names, "id")
6720+
assert.Contains(t, names, "x-custom")
6721+
for _, uv := range result.UndeclaredValues {
6722+
assert.Equal(t, TypeReadOnlyProperty, uv.Type)
6723+
}
6724+
}
6725+
6726+
func TestStrictValidator_RejectReadOnly_OneOf(t *testing.T) {
6727+
// Covers polymorphic.go validateVariantWithParent path
6728+
yml := `openapi: "3.1.0"
6729+
info:
6730+
title: Test
6731+
version: "1.0"
6732+
paths: {}
6733+
components:
6734+
schemas:
6735+
OneOfSchema:
6736+
type: object
6737+
oneOf:
6738+
- type: object
6739+
properties:
6740+
id:
6741+
type: string
6742+
readOnly: true
6743+
email:
6744+
type: string
6745+
`
6746+
model := buildSchemaFromYAML(t, yml)
6747+
schema := getSchema(t, model, "OneOfSchema")
6748+
6749+
opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly())
6750+
v := NewValidator(opts, 3.1)
6751+
6752+
data := map[string]any{
6753+
"id": "user-1",
6754+
"email": "test@example.com",
6755+
}
6756+
6757+
result := v.Validate(Input{
6758+
Schema: schema,
6759+
Data: data,
6760+
Direction: DirectionRequest,
6761+
Options: opts,
6762+
BasePath: "$.body",
6763+
Version: 3.1,
6764+
})
6765+
6766+
assert.False(t, result.Valid)
6767+
require.Len(t, result.UndeclaredValues, 1)
6768+
assert.Equal(t, "id", result.UndeclaredValues[0].Name)
6769+
assert.Equal(t, TypeReadOnlyProperty, result.UndeclaredValues[0].Type)
6770+
}
6771+
6772+
func TestStrictValidator_RejectReadOnly_OneOfAdditionalPropertiesFalse(t *testing.T) {
6773+
// Covers polymorphic.go recurseIntoDeclaredPropertiesWithMerged path
6774+
yml := `openapi: "3.1.0"
6775+
info:
6776+
title: Test
6777+
version: "1.0"
6778+
paths: {}
6779+
components:
6780+
schemas:
6781+
OneOfStrict:
6782+
type: object
6783+
additionalProperties: false
6784+
properties:
6785+
name:
6786+
type: string
6787+
oneOf:
6788+
- type: object
6789+
additionalProperties: false
6790+
properties:
6791+
name:
6792+
type: string
6793+
id:
6794+
type: string
6795+
readOnly: true
6796+
data:
6797+
type: string
6798+
`
6799+
model := buildSchemaFromYAML(t, yml)
6800+
schema := getSchema(t, model, "OneOfStrict")
6801+
6802+
opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly())
6803+
v := NewValidator(opts, 3.1)
6804+
6805+
data := map[string]any{
6806+
"name": "test",
6807+
"id": "should-be-rejected",
6808+
"data": "valid",
6809+
}
6810+
6811+
result := v.Validate(Input{
6812+
Schema: schema,
6813+
Data: data,
6814+
Direction: DirectionRequest,
6815+
Options: opts,
6816+
BasePath: "$.body",
6817+
Version: 3.1,
6818+
})
6819+
6820+
assert.False(t, result.Valid)
6821+
require.Len(t, result.UndeclaredValues, 1)
6822+
assert.Equal(t, "id", result.UndeclaredValues[0].Name)
6823+
assert.Equal(t, TypeReadOnlyProperty, result.UndeclaredValues[0].Type)
6824+
}
6825+
6826+
func TestStrictValidator_RejectReadOnly_AllOfAdditionalPropertiesFalse(t *testing.T) {
6827+
// Covers polymorphic.go recurseIntoAllOfDeclaredProperties path
6828+
yml := `openapi: "3.1.0"
6829+
info:
6830+
title: Test
6831+
version: "1.0"
6832+
paths: {}
6833+
components:
6834+
schemas:
6835+
AllOfStrict:
6836+
type: object
6837+
additionalProperties: false
6838+
allOf:
6839+
- type: object
6840+
properties:
6841+
id:
6842+
type: string
6843+
readOnly: true
6844+
name:
6845+
type: string
6846+
`
6847+
model := buildSchemaFromYAML(t, yml)
6848+
schema := getSchema(t, model, "AllOfStrict")
6849+
6850+
opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly())
6851+
v := NewValidator(opts, 3.1)
6852+
6853+
data := map[string]any{
6854+
"id": "should-be-rejected",
6855+
"name": "John",
6856+
}
6857+
6858+
result := v.Validate(Input{
6859+
Schema: schema,
6860+
Data: data,
6861+
Direction: DirectionRequest,
6862+
Options: opts,
6863+
BasePath: "$.body",
6864+
Version: 3.1,
6865+
})
6866+
6867+
assert.False(t, result.Valid)
6868+
require.Len(t, result.UndeclaredValues, 1)
6869+
assert.Equal(t, "id", result.UndeclaredValues[0].Name)
6870+
assert.Equal(t, TypeReadOnlyProperty, result.UndeclaredValues[0].Type)
6871+
}
6872+
6873+
func TestStrictValidator_CheckReadWriteOnlyViolation_NilSchema(t *testing.T) {
6874+
opts := config.NewValidationOptions(config.WithStrictMode(), config.WithStrictRejectReadOnly())
6875+
v := NewValidator(opts, 3.1)
6876+
6877+
_, ok := v.checkReadWriteOnlyViolation("$.body.x", "x", "val", nil, DirectionRequest)
6878+
assert.False(t, ok)
6879+
}

validator_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2784,7 +2784,7 @@ components:
27842784
})
27852785
}
27862786

2787-
func TestStrictRejectReadOnly_RequestIntegration(t *testing.T) {
2787+
func TestStrictMode_RejectReadOnly_RequestIntegration(t *testing.T) {
27882788
spec := `openapi: 3.1.0
27892789
paths:
27902790
/users:
@@ -2834,7 +2834,7 @@ paths:
28342834
assert.True(t, foundReadOnly, "should report readOnly violation")
28352835
}
28362836

2837-
func TestStrictRejectWriteOnly_ResponseIntegration(t *testing.T) {
2837+
func TestStrictMode_RejectWriteOnly_ResponseIntegration(t *testing.T) {
28382838
spec := `openapi: 3.1.0
28392839
paths:
28402840
/users/{id}:

0 commit comments

Comments
 (0)