Skip to content

Commit 0c8939c

Browse files
committed
Add OpenApi v3 validator
1 parent d59c1d0 commit 0c8939c

File tree

6 files changed

+286
-6
lines changed

6 files changed

+286
-6
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
and the schema gets more accurate by your tests.** Output the schema
2525
to a file and reuse it as expectations to test the other methods, as
2626
most of them respond similarly with only minor differences. Or
27-
extend the schema further to a full Swagger spec (version 2.0,
28-
OpenAPI 3.0 also planned), which RESTinstance can test requests and
27+
extend the schema further to a full Swagger spec (version 2.0 and
28+
OpenAPI 3.0), which RESTinstance can test requests and
2929
responses against. All this leads to reusability, getting great test
3030
coverage with minimum number of keystrokes and very clean tests.
3131

@@ -175,3 +175,5 @@ Berman, for JSON Schema validator
175175
syntax coloring
176176
- [requests](https://github.com/requests/requests), by Kenneth Reitz
177177
et al., for making HTTP requests
178+
- [openapi-core](https://github.com/python-openapi/openapi-core), by Artur Maciag
179+
et al., for OpenAPI 3 validation

atest/spec_30_yaml.robot

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
*** Settings ***
2+
Library REST localhost:8273/
3+
... spec=${CURDIR}/swagger/spec_30.yaml
4+
5+
*** Test Cases ***
6+
GET to existing
7+
GET /users/1 allow_redirects=${None}
8+
9+
GET to non-existing
10+
GET /users/404 allow_redirects=true
11+
12+
GET many
13+
GET /users allow_redirects=false
14+
15+
GET many with query "_limit"
16+
GET /users?_limit=10 allow_redirects=${False}
17+
18+
GET many with invalid query
19+
GET /users?_invalid=query timeout=2.0
20+
21+
POST with invalid params
22+
POST /users { "name": "Alexander James Murphy" }
23+
24+
POST with valid params
25+
POST /users { "id": 100, "name": "Alexander James Murphy" }
26+
27+
POST with missing params
28+
POST /users
29+
30+
PUT to non-existing
31+
PUT /users/2043
32+
33+
PUT with invalid params
34+
PUT /users/100 { "id": 1801 }
35+
36+
PUT with valid params
37+
PUT /users/100 { "address": { "city": "Delta City" } }
38+
39+
PUT with missing params
40+
PUT /users/100
41+
42+
PATCH to non-existing
43+
PATCH /users/2043
44+
45+
PATCH with invalid params
46+
PATCH /users/100 { "nickname": "murph" }
47+
48+
PATCH with valid params
49+
PATCH /users/100 { "username": "murph" }
50+
51+
PATCH with missing params
52+
PATCH /users/100
53+
54+
DELETE to non-existing
55+
DELETE /users/2043
56+
57+
DELETE to existing
58+
DELETE /users/100
59+
60+
DELETE to invalid, but with no validations
61+
DELETE /invalid validate=false
62+
63+
GET non-documented endpoint
64+
Run Keyword And Expect Error
65+
... Path not found for http://localhost:8273/nope
66+
... GET /nope

atest/swagger/spec_30.yaml

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
---
2+
openapi: '3.0.0'
3+
info:
4+
title: RoboCon RESTinstance API
5+
version: '2018-01-18'
6+
paths:
7+
"/users":
8+
get:
9+
responses:
10+
'200':
11+
description: GET many
12+
content:
13+
application/json:
14+
schema:
15+
$ref: "#/components/schemas/users"
16+
post:
17+
responses:
18+
'201':
19+
description: POST with valid params
20+
content:
21+
application/json:
22+
schema:
23+
"$ref": "#/components/schemas/user"
24+
'400':
25+
description: POST with invalid params
26+
content:
27+
application/json:
28+
schema:
29+
"$ref": "#/components/schemas/post_400"
30+
"/users/{id}":
31+
get:
32+
parameters:
33+
- name: id
34+
in: path
35+
required: true
36+
schema:
37+
type: integer
38+
responses:
39+
'200':
40+
description: GET to existing
41+
content:
42+
application/json:
43+
schema:
44+
"$ref": "#/components/schemas/user"
45+
'404':
46+
description: GET to non-existing
47+
content:
48+
application/json:
49+
schema:
50+
"$ref": "#/components/schemas/404"
51+
put:
52+
parameters:
53+
- name: id
54+
in: path
55+
required: true
56+
schema:
57+
type: integer
58+
responses:
59+
'200':
60+
description: PUT with valid params
61+
content:
62+
application/json:
63+
schema:
64+
"$ref": "#/components/schemas/user"
65+
'400':
66+
description: PUT with invalid params
67+
content:
68+
application/json:
69+
schema:
70+
"$ref": "#/components/schemas/400"
71+
'404':
72+
description: PUT to non-existing
73+
content:
74+
application/json:
75+
schema:
76+
"$ref": "#/components/schemas/404"
77+
patch:
78+
parameters:
79+
- name: id
80+
in: path
81+
required: true
82+
schema:
83+
type: integer
84+
responses:
85+
'200':
86+
description: PATCH with valid params
87+
content:
88+
application/json:
89+
schema:
90+
"$ref": "#/components/schemas/user"
91+
'400':
92+
description: PATCH with invalid params
93+
content:
94+
application/json:
95+
schema:
96+
"$ref": "#/components/schemas/400"
97+
'404':
98+
description: PATCH to non-existing
99+
content:
100+
application/json:
101+
schema:
102+
"$ref": "#/components/schemas/404"
103+
delete:
104+
parameters:
105+
- name: id
106+
in: path
107+
required: true
108+
schema:
109+
type: integer
110+
responses:
111+
'204':
112+
description: DELETE to existing
113+
'404':
114+
description: DELETE to non-existing
115+
content:
116+
application/json:
117+
schema:
118+
"$ref": "#/components/schemas/404"
119+
components:
120+
schemas:
121+
'400':
122+
type: object
123+
properties:
124+
error:
125+
type: string
126+
required:
127+
- error
128+
'404':
129+
type: object
130+
properties:
131+
error:
132+
type: string
133+
example: Not found
134+
required:
135+
- error
136+
post_400:
137+
type: object
138+
properties:
139+
error:
140+
type: string
141+
required:
142+
- error
143+
users:
144+
type: array
145+
items:
146+
"$ref": "#/components/schemas/user"
147+
user:
148+
type: object
149+
properties:
150+
active:
151+
type: boolean
152+
address:
153+
properties:
154+
city:
155+
type: string
156+
geo:
157+
properties:
158+
latitude:
159+
type: number
160+
longitude:
161+
type: number
162+
type: object
163+
postalcode:
164+
type: string
165+
street:
166+
type: string
167+
required:
168+
- city
169+
type: object
170+
email:
171+
type: string
172+
id:
173+
type: integer
174+
name:
175+
type: string
176+
organizationId:
177+
type: integer
178+
phone:
179+
type: string
180+
registeredAt:
181+
type: string
182+
urls:
183+
items:
184+
type: string
185+
type: array
186+
uuid:
187+
type: string
188+
required:
189+
- id
190+
- name

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies = [
1313
"GenSON",
1414
"jsonpath-ng",
1515
"jsonschema",
16+
"openapi-core",
1617
"pygments",
1718
"pytz",
1819
"requests",

src/REST/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ def __init__(
155155
self.schema.update(self._input_object(schema))
156156
self.spec = {}
157157
self.spec.update(self._input_object(spec))
158+
self._spec = None
158159
self.instances = self._input_array(instances)
159160
self.log_level = self._input_log_level(loglevel)
160161
self.auth = None

src/REST/keywords.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
from jsonpath_ng.ext import parse as parse_jsonpath
1818
from jsonschema import FormatChecker, validate
1919
from jsonschema.exceptions import SchemaError, ValidationError
20+
from openapi_core import OpenAPI
21+
from openapi_core.exceptions import OpenAPIError
22+
from openapi_core.contrib.requests import RequestsOpenAPIRequest, \
23+
RequestsOpenAPIResponse
2024
from pytz import UnknownTimeZoneError, utc
2125
from requests import request as client
2226
from requests.auth import HTTPBasicAuth, HTTPDigestAuth, HTTPProxyAuth
@@ -1426,10 +1430,26 @@ def _instantiate(
14261430

14271431
def _assert_spec(self, spec, response):
14281432
request = response.request
1429-
try:
1430-
validate_api_call(spec, raw_request=request, raw_response=response)
1431-
except ValueError as e:
1432-
raise AssertionError(e)
1433+
1434+
spec_version = spec.get("openapi", "0")
1435+
if spec_version.startswith("3"):
1436+
try:
1437+
if self._spec is None:
1438+
self._spec = OpenAPI.from_dict(self.spec)
1439+
1440+
openapi_request = RequestsOpenAPIRequest(request)
1441+
openapi_response = RequestsOpenAPIResponse(response)
1442+
1443+
self._spec.validate_request(openapi_request)
1444+
self._spec.validate_response(openapi_request, openapi_response)
1445+
1446+
except OpenAPIError as e:
1447+
raise AssertionError(e) from e
1448+
else:
1449+
try:
1450+
validate_api_call(spec, raw_request=request, raw_response=response)
1451+
except ValueError as e:
1452+
raise AssertionError(e)
14331453

14341454
def _validate_schema(self, schema, json_dict):
14351455
for field in schema:

0 commit comments

Comments
 (0)