Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions source/OpenAPI-Core-Tests/OADocumentValidatorTests.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,58 @@ Class {
#package : 'OpenAPI-Core-Tests'
}

{ #category : 'tests' }
OADocumentValidatorTests >> testApiKeySecuritySchemeIsValid [
self assert: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{},"components":{"securitySchemes":{"apiKeyAuth":{"type":"apiKey","name":"X-API-Key","in":"header"}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testBadPathsTypeIsInvalid [
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":"nope"}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testComponentSchemaAdditionalPropertiesBooleanFalseIsValid [
self assert: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{},"components":{"schemas":{"Foo":{"type":"object","additionalProperties":false}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testComponentSchemaAdditionalPropertiesInvalidTypeIsInvalid [
"additionalProperties must be a Schema, a Reference, or a boolean - a plain string is none of those."
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{},"components":{"schemas":{"Foo":{"type":"object","additionalProperties":"nope"}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testComponentSchemaAdditionalPropertiesSchemaObjectIsValid [
self assert: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{},"components":{"schemas":{"Foo":{"type":"object","additionalProperties":{"type":"string"}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testCookieParameterIsValid [
self assert: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo":{"get":{"parameters":[{"name":"c","in":"cookie","schema":{"type":"string"}}],"responses":{"200":{"description":"ok"}}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testExplicitVersion30 [
self shouldnt: [ OADocumentValidator validateDocument: self validMinimalDocument version: '3.0.3' ] raise: Error
]

{ #category : 'tests' }
OADocumentValidatorTests >> testHeaderParameterIsValid [
self assert: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo":{"get":{"parameters":[{"name":"h","in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"ok"}}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testHttpBearerSecuritySchemeIsValid [
self assert: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testHttpSchemeWithBearerFormatOnNonBearerSchemeIsInvalid [
"bearerFormat is only meaningful (and only allowed by the meta-schema) when scheme:bearer."
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{},"components":{"securitySchemes":{"basicAuth":{"type":"http","scheme":"basic","bearerFormat":"JWT"}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testMissingInfoIsInvalid [
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","paths":{}}')
Expand All @@ -30,6 +72,88 @@ OADocumentValidatorTests >> testMissingPathsIsInvalid [
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testOAuth2SecuritySchemeMissingFlowsIsInvalid [
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{},"components":{"securitySchemes":{"oauth":{"type":"oauth2"}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testParameterWithBothExampleAndExamplesIsInvalid [
"ExampleXORExamples: example and examples are mutually exclusive."
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo":{"get":{"parameters":[{"name":"q","in":"query","schema":{"type":"string"},"example":"a","examples":{"x":{"value":"a"}}}],"responses":{"200":{"description":"ok"}}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testParameterWithBothSchemaAndContentIsInvalid [
"SchemaXORContent: schema and content are mutually exclusive, exactly one is required."
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo":{"get":{"parameters":[{"name":"q","in":"query","schema":{"type":"string"},"content":{"application/json":{}}}],"responses":{"200":{"description":"ok"}}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testParameterWithContentAndStyleIsInvalid [
"When content is used instead of schema, style/explode/allowReserved/example/examples are disallowed."
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo":{"get":{"parameters":[{"name":"q","in":"query","content":{"application/json":{}},"style":"form"}],"responses":{"200":{"description":"ok"}}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testParameterWithNeitherSchemaNorContentIsInvalid [
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo":{"get":{"parameters":[{"name":"q","in":"query"}],"responses":{"200":{"description":"ok"}}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testParameterWithOnlyContentIsValid [
self assert: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo":{"get":{"parameters":[{"name":"q","in":"query","content":{"application/json":{}}}],"responses":{"200":{"description":"ok"}}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testParameterWithOnlyExampleIsValid [
self assert: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo":{"get":{"parameters":[{"name":"q","in":"query","schema":{"type":"string"},"example":"a"}],"responses":{"200":{"description":"ok"}}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testParameterWithSwagger2StyleBodyLocationIsInvalid [
"in: body is Swagger 2.0, not OpenAPI 3.0 (body moved to requestBody) - matches none of the oneOf location branches."
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo":{"get":{"parameters":[{"name":"q","in":"body","schema":{"type":"string"}}],"responses":{"200":{"description":"ok"}}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testPathNotStartingWithSlashIsInvalid [
"Paths keys must match ^\/ (or the ^x- vendor-extension pattern); anything else is rejected by additionalProperties:false."
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"foo":{"get":{"responses":{"200":{"description":"ok"}}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testPathParameterMissingRequiredIsInvalid [
"PathParameter in the OAI meta-schema mandates required:true - a path parameter without it does not match any of the Parameter oneOf branches (path/query/header/cookie)."
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo/{id}":{"get":{"parameters":[{"name":"id","in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"ok"}}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testPathParameterWithRequiredTrueIsValid [
self assert: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo/{id}":{"get":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok"}}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testPathWithVendorExtensionIsValid [
self assert: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo":{"get":{"responses":{"200":{"description":"ok"}}}},"x-internal-note":"draft"}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testQueryParameterOptionalIsValid [
self assert: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo":{"get":{"parameters":[{"name":"q","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"ok"}}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testResponsesObjectRequiresAtLeastOneEntry [
self deny: (OADocumentValidator isValidDocument: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/foo":{"get":{"responses":{}}}}}')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testSchemaForVersionIsCachedAcrossPatchVersions [
OADocumentValidator resetSchemaCache.
self assert: (OADocumentValidator schemaForVersion: '3.0.0') == (OADocumentValidator schemaForVersion: '3.0.3')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testValidMinimalDocumentIsValid [
self assert: (OADocumentValidator isValidDocument: self validMinimalDocument)
Expand All @@ -43,6 +167,33 @@ OADocumentValidatorTests >> testValidateReturnsParsedDocument [
self assert: ((doc at: 'info') at: 'title') equals: 'Test API'
]

{ #category : 'tests' }
OADocumentValidatorTests >> testValidationErrorForBadPathsTypeIsJSONSchemaError [
| error |
error := OADocumentValidator validationErrorFor: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":"nope"}'.
self assert: (error isKindOf: JSONSchemaError)
]

{ #category : 'tests' }
OADocumentValidatorTests >> testValidationErrorForValidDocumentIsNil [
self assert: (OADocumentValidator validationErrorFor: self validMinimalDocument) isNil
]

{ #category : 'tests' }
OADocumentValidatorTests >> testValidationErrorMessageForBadPathsTypeDescribesFailure [
| message |
message := OADocumentValidator validationErrorMessageFor: '{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":"nope"}'.
self assert: (message includesSubstring: 'JSONTypeError').
self assert: (message includesSubstring: 'not an object')
]

{ #category : 'tests' }
OADocumentValidatorTests >> testValidationErrorMessageForMissingInfoDescribesFailure [
| message |
message := OADocumentValidator validationErrorMessageFor: '{"openapi":"3.0.0","paths":{}}'.
self assert: (message includesSubstring: 'JSONSchemaMissingRequiredProperty')
]

{ #category : 'fixtures' }
OADocumentValidatorTests >> validMinimalDocument [
^ '{"openapi":"3.0.0","info":{"title":"Test API","version":"1.0.0"},"paths":{}}'
Expand Down
52 changes: 50 additions & 2 deletions source/OpenAPI-Core/OADocumentValidator.class.st
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
Class {
#name : 'OADocumentValidator',
#superclass : 'Object',
#classVars : [
'SchemaCache'
],
#category : 'OpenAPI-Core',
#package : 'OpenAPI-Core'
}

{ #category : 'schemas' }
OADocumentValidator class >> cacheKeyForVersion: aVersionString [
"Mehrere Patch-Versionen (z.B. 3.0.0, 3.0.3) teilen sich dasselbe Meta-Schema. Auf den major.minor-Bucket normalisieren, damit der Cache nicht pro Patch-Version waechst."
(aVersionString beginsWith: '3.0') ifTrue: [ ^ '3.0' ].
^ aVersionString
]

{ #category : 'validating' }
OADocumentValidator class >> isValidDocument: aDocument [
"Antwortet, ob aDocument ein gültiges OpenAPI-Dokument ist (keine Exception bei der Validierung)."
^ [ self validateDocument: aDocument. true ] on: Error do: [ :e | false ]
]

{ #category : 'schemas' }
OADocumentValidator class >> resetSchemaCache [
"Leert den Schema-Cache (z.B. für Tests oder wenn sich gebündelte Meta-Schemas ändern)."
SchemaCache := nil
]

{ #category : 'schemas' }
OADocumentValidator class >> schemaCache [
"Lazily initialisierter Cache: normalisierter Versions-Bucket -> gebautes JSONSchema. Vermeidet, das teure OAI-Meta-Schema bei jeder Validierung neu zu parsen/bauen."
^ SchemaCache ifNil: [ SchemaCache := Dictionary new ]
]

{ #category : 'schemas' }
OADocumentValidator class >> schemaForVersion: aVersionString [
"Baut ein JSONSchema aus dem gebündelten OAI-Meta-Schema für aVersionString."
^ JSONSchema fromString: (OAMetaSchema schemaJsonForVersion: aVersionString)
"Baut (und cached, siehe #schemaCache) ein JSONSchema aus dem gebündelten OAI-Meta-Schema für aVersionString."
^ self schemaCache
at: (self cacheKeyForVersion: aVersionString)
ifAbsentPut: [ JSONSchema fromString: (OAMetaSchema schemaJsonForVersion: aVersionString) ]
]

{ #category : 'validating' }
Expand All @@ -38,3 +62,27 @@ OADocumentValidator class >> validateDocument: aDocument version: aVersionString
(self schemaForVersion: aVersionString) validate: doc.
^ doc
]

{ #category : 'validating' }
OADocumentValidator class >> validationErrorFor: aDocument [
"Wie #isValidDocument:, liefert bei Ungültigkeit aber den tatsächlichen JSONSchemaError statt ihn zu verschlucken (welche Regel wurde verletzt). Antwortet nil, wenn das Dokument gültig ist."
^ [ self validateDocument: aDocument. nil ]
on: JSONSchemaError
do: [ :e | e ]
]

{ #category : 'validating' }
OADocumentValidator class >> validationErrorFor: aDocument version: aVersionString [
"Wie #validationErrorFor:, aber mit expliziter Version statt Auto-Erkennung aus dem openapi-Feld."
^ [ self validateDocument: aDocument version: aVersionString. nil ]
on: JSONSchemaError
do: [ :e | e ]
]

{ #category : 'validating' }
OADocumentValidator class >> validationErrorMessageFor: aDocument [
"Bequemlichkeit über #validationErrorFor: - liefert eine lesbare Beschreibung der verletzten Regel (Fehlerklasse + messageText), oder nil, wenn das Dokument gültig ist."
| error |
error := self validationErrorFor: aDocument.
^ error ifNotNil: [ error class name , ': ', error messageText ]
]
6 changes: 2 additions & 4 deletions source/OpenAPI-Core/OAParameter.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -300,11 +300,9 @@ OAParameter >> validateContraints: value [

{ #category : 'writing' }
OAParameter >> writeParameterValueOn: anObject request: request [
"required/nicht-required ist bereits durch #extractParameter: abgedeckt: signalisiert OAMissingRequiredParameter, wenn required und fehlend; liefert nil nur, wenn optional und fehlend - dann wird hier bewusst nichts geschrieben."
| value |
self flag: #todo.
"needs to be improved for required or not. If not required we don't write the
value. Needs to be checked if that constraint is ok"
value := self extractParameter: request.
value := self extractParameter: request.
value ifNil: [ ^ self ].
self shouldUseAccessors
ifTrue: [
Expand Down
Loading