surety-api 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- surety/api/__init__.py +4 -0
- surety/api/caller.py +85 -0
- surety/api/method.py +175 -0
- surety/api/mock/__init__.py +0 -0
- surety/api/mock/data.py +95 -0
- surety/api/mock/service.py +179 -0
- surety/api/schema.py +79 -0
- surety_api-0.0.1.dist-info/METADATA +37 -0
- surety_api-0.0.1.dist-info/RECORD +12 -0
- surety_api-0.0.1.dist-info/WHEEL +5 -0
- surety_api-0.0.1.dist-info/licenses/LICENSE +21 -0
- surety_api-0.0.1.dist-info/top_level.txt +1 -0
surety/api/__init__.py
ADDED
surety/api/caller.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from surety.process import dictionary
|
|
2
|
+
from surety.sdk import Field
|
|
3
|
+
from surety.diff import compare
|
|
4
|
+
|
|
5
|
+
from surety.api.method import ApiMethod
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApiCaller:
|
|
9
|
+
method: ApiMethod
|
|
10
|
+
|
|
11
|
+
def __init__(self, req_body=None, path_params=None, headers=None,
|
|
12
|
+
params=None, cookies=None, updates=None):
|
|
13
|
+
self.req_body = req_body
|
|
14
|
+
self.response = None
|
|
15
|
+
self.path_params = path_params
|
|
16
|
+
self.headers = headers
|
|
17
|
+
self.params = params
|
|
18
|
+
self.cookies = cookies
|
|
19
|
+
self.updates = updates
|
|
20
|
+
|
|
21
|
+
def request(self):
|
|
22
|
+
req_body = self.req_body
|
|
23
|
+
params = self.params
|
|
24
|
+
|
|
25
|
+
if req_body is None:
|
|
26
|
+
pass
|
|
27
|
+
elif isinstance(self.req_body, Field):
|
|
28
|
+
req_body = self.req_body.value
|
|
29
|
+
|
|
30
|
+
if self.updates:
|
|
31
|
+
req_body = dictionary.merge_with_updates(req_body, self.updates)
|
|
32
|
+
|
|
33
|
+
if params is None:
|
|
34
|
+
pass
|
|
35
|
+
elif isinstance(self.params, Field):
|
|
36
|
+
params = self.params.value
|
|
37
|
+
|
|
38
|
+
self.response = self.method.call(
|
|
39
|
+
req_body=req_body,
|
|
40
|
+
path_params=self.path_params,
|
|
41
|
+
headers=self.headers,
|
|
42
|
+
params=params,
|
|
43
|
+
cookies=self.cookies
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def verify_response(self, error_code=None, resp_body=None, normalize=False,
|
|
49
|
+
normalize_keys=None, parse_response=None, rules=None):
|
|
50
|
+
if error_code:
|
|
51
|
+
assert self.response.status_code == error_code, (
|
|
52
|
+
f'Unexpected code: {self.response.status_code}\n'
|
|
53
|
+
f'Response text: {self.response.text}'
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
assert self.response.status_code == 200, (
|
|
57
|
+
f'Unexpected response code: {self.response.status_code}'
|
|
58
|
+
f'\nResponse text: {self.response.text}'
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if resp_body is not None or isinstance(resp_body, Field):
|
|
62
|
+
content_type = self.response.headers.get('Content-Type')
|
|
63
|
+
|
|
64
|
+
if content_type == 'text/csv':
|
|
65
|
+
result_data = self.response.text
|
|
66
|
+
|
|
67
|
+
if parse_response and callable(parse_response):
|
|
68
|
+
result_data = parse_response(result_data)
|
|
69
|
+
else:
|
|
70
|
+
result_data = self.response.text and self.response.json()
|
|
71
|
+
|
|
72
|
+
if isinstance(resp_body, Field):
|
|
73
|
+
resp_body = resp_body.value
|
|
74
|
+
|
|
75
|
+
if normalize or normalize_keys:
|
|
76
|
+
resp_body = dictionary.normalize(resp_body, result_data, keys=normalize_keys)
|
|
77
|
+
|
|
78
|
+
compare(
|
|
79
|
+
expected=resp_body,
|
|
80
|
+
actual=result_data,
|
|
81
|
+
rules=rules,
|
|
82
|
+
target_name='api response'
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return self
|
surety/api/method.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from surety.process import dictionary
|
|
6
|
+
from surety.sdk import Field
|
|
7
|
+
from surety.diff import compare
|
|
8
|
+
|
|
9
|
+
from surety.api.mock.data import JsonFilter, MatchType, StringFilter
|
|
10
|
+
from surety.api.schema import Schema, HttpMethod
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiMethod(Schema):
|
|
14
|
+
@classmethod
|
|
15
|
+
def call(cls, req_body=None, session=None, path_params=None, headers=None,
|
|
16
|
+
params=None, cookies=None):
|
|
17
|
+
session = session or requests.Session()
|
|
18
|
+
|
|
19
|
+
if params is None:
|
|
20
|
+
params = cls.params().value
|
|
21
|
+
|
|
22
|
+
if req_body is None:
|
|
23
|
+
req_body = cls.req_body().value
|
|
24
|
+
|
|
25
|
+
if req_body is not None:
|
|
26
|
+
req_body = json.dumps(req_body)
|
|
27
|
+
|
|
28
|
+
url = cls.get_request_url(path_params=path_params)
|
|
29
|
+
headers = headers or {}
|
|
30
|
+
headers = dictionary.merge_with_updates(headers, {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return session.request(
|
|
35
|
+
str(cls.method),
|
|
36
|
+
url,
|
|
37
|
+
headers=headers,
|
|
38
|
+
params=params,
|
|
39
|
+
data=req_body,
|
|
40
|
+
cookies=cookies
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def reply(cls, path_params=None, params=None, req_body=None, body=None,
|
|
45
|
+
status=200, reset=False, cookies=None, headers=None, times=1,
|
|
46
|
+
no_prefix=False, substr_filter=None, strict=False,
|
|
47
|
+
default_headers=False):
|
|
48
|
+
from surety.api.mock.service import MockServer # pylint: disable=cyclic-import
|
|
49
|
+
reply_body = body
|
|
50
|
+
|
|
51
|
+
if default_headers and headers is None:
|
|
52
|
+
headers = cls.fe_headers()
|
|
53
|
+
|
|
54
|
+
headers = headers or {'Content-Type': 'application/json',}
|
|
55
|
+
|
|
56
|
+
if reply_body is None:
|
|
57
|
+
reply_body = cls.resp_body and cls.resp_body() # pylint: disable=not-callable
|
|
58
|
+
|
|
59
|
+
if reply_body not in [None, {}, ''] and isinstance(reply_body, Field):
|
|
60
|
+
reply_body = reply_body.value
|
|
61
|
+
reply_body = json.dumps(reply_body)
|
|
62
|
+
|
|
63
|
+
if req_body:
|
|
64
|
+
req_body = JsonFilter().with_values({
|
|
65
|
+
JsonFilter.Json.name: req_body,
|
|
66
|
+
JsonFilter.MatchType.name:
|
|
67
|
+
MatchType.Strict if strict else MatchType.Partial
|
|
68
|
+
})
|
|
69
|
+
elif substr_filter:
|
|
70
|
+
req_body = StringFilter().with_values({
|
|
71
|
+
StringFilter.String.name: substr_filter
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
if no_prefix:
|
|
75
|
+
prefix = ''
|
|
76
|
+
else:
|
|
77
|
+
prefix = f'/mockserver/{cls.get_service_name()}'
|
|
78
|
+
|
|
79
|
+
return MockServer().reply(
|
|
80
|
+
method=cls.method,
|
|
81
|
+
url=f'{prefix}{cls.get_relative_url(path_params)}',
|
|
82
|
+
params=params,
|
|
83
|
+
req_body=req_body,
|
|
84
|
+
body=reply_body,
|
|
85
|
+
status=status,
|
|
86
|
+
reset=reset,
|
|
87
|
+
headers=headers or {},
|
|
88
|
+
cookies=cookies,
|
|
89
|
+
times=times
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def reply_corse_options(cls, allow_headers, reset=False, no_prefix=False,
|
|
94
|
+
path_params=None, params=None, req_body=None):
|
|
95
|
+
from surety.api.mock.service import MockServer # pylint: disable=cyclic-import
|
|
96
|
+
headers = cls.fe_headers(
|
|
97
|
+
content_type='Access-Control-Allow-Headers',
|
|
98
|
+
)
|
|
99
|
+
headers['Access-Control-Allow-Headers'] = ','.join(allow_headers)
|
|
100
|
+
|
|
101
|
+
if no_prefix:
|
|
102
|
+
prefix = ''
|
|
103
|
+
else:
|
|
104
|
+
prefix = f'/mockserver/{cls.get_service_name()}'
|
|
105
|
+
|
|
106
|
+
return MockServer().reply(
|
|
107
|
+
method=HttpMethod.OPTIONS,
|
|
108
|
+
url=f'{prefix}{cls.get_relative_url(path_params)}',
|
|
109
|
+
params=params,
|
|
110
|
+
req_body=req_body,
|
|
111
|
+
body=None,
|
|
112
|
+
reset=reset,
|
|
113
|
+
headers=headers
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def verify_called(cls, expected=None, normalize=False, normalize_keys=None,
|
|
118
|
+
path_params=None, timeout=None, body=None, headers=None,
|
|
119
|
+
latest=False, params=None, rules=None, header_rules=None):
|
|
120
|
+
from surety.api.mock.service import MockServer # pylint: disable=cyclic-import
|
|
121
|
+
|
|
122
|
+
expected_value = expected
|
|
123
|
+
|
|
124
|
+
if isinstance(expected_value, Field):
|
|
125
|
+
expected_value = expected_value.value
|
|
126
|
+
if isinstance(params, Field):
|
|
127
|
+
params = params.value
|
|
128
|
+
|
|
129
|
+
if body:
|
|
130
|
+
if isinstance(body, str):
|
|
131
|
+
body = StringFilter().with_values({
|
|
132
|
+
StringFilter.String.name: body
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
actual_value, actual_headers, actual_params = MockServer().catch(
|
|
136
|
+
method=cls.method,
|
|
137
|
+
url=(f'/mockserver/{cls.get_service_name()}'
|
|
138
|
+
f'{cls.get_relative_url(path_params=path_params)}'),
|
|
139
|
+
body=body,
|
|
140
|
+
timeout=timeout,
|
|
141
|
+
latest=latest
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if normalize or normalize_keys:
|
|
145
|
+
actual_value = dictionary.normalize(
|
|
146
|
+
actual_value, expected_value, keys=normalize_keys
|
|
147
|
+
)
|
|
148
|
+
compare(
|
|
149
|
+
actual=actual_value,
|
|
150
|
+
expected=expected_value,
|
|
151
|
+
rules=rules or None,
|
|
152
|
+
target_name='api request'
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if headers:
|
|
156
|
+
filtered_headers = {}
|
|
157
|
+
|
|
158
|
+
for key in headers:
|
|
159
|
+
filtered_headers[key] = actual_headers.get(key)
|
|
160
|
+
|
|
161
|
+
compare(
|
|
162
|
+
actual=filtered_headers,
|
|
163
|
+
expected=headers,
|
|
164
|
+
rules=header_rules or None,
|
|
165
|
+
target_name='api request headers'
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
#FIXME parse into object + verify all together
|
|
169
|
+
|
|
170
|
+
if params:
|
|
171
|
+
compare(
|
|
172
|
+
actual=actual_params,
|
|
173
|
+
expected=params,
|
|
174
|
+
target_name='api request params'
|
|
175
|
+
)
|
|
File without changes
|
surety/api/mock/data.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from surety.sdk import Bool, Dictionary, Enum, Int, Raw, String
|
|
2
|
+
|
|
3
|
+
from surety.api.schema import HttpMethod
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class JsonMatcher(Enum):
|
|
7
|
+
IGNORE_KEY = '${json-unit.ignore-element}'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HttpRequest(Dictionary):
|
|
11
|
+
Method = String(name='method')
|
|
12
|
+
Path = String(name='path')
|
|
13
|
+
Params = Raw(name='queryStringParameters', required=False)
|
|
14
|
+
Body = Raw(name='body', required=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HttpResponse(Dictionary):
|
|
18
|
+
StatusCode = Int(name='statusCode')
|
|
19
|
+
Headers = Raw(name='headers', required=False)
|
|
20
|
+
Cookies = Raw(name='cookies', required=False)
|
|
21
|
+
Body = Raw(name='body', required=False)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Command(Enum):
|
|
25
|
+
Expectation = 'expectation'
|
|
26
|
+
Reset = 'reset'
|
|
27
|
+
Retrieve = 'retrieve'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ObjectType(Enum):
|
|
31
|
+
Requests = 'REQUESTS'
|
|
32
|
+
ActiveExpectations = 'active_expectations'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Params(Dictionary):
|
|
36
|
+
Type = String(name='type', required=False)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PathParams(Dictionary):
|
|
40
|
+
Command = Command(name='command')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Times(Dictionary):
|
|
44
|
+
RemainingTimes = Int(name='remainingTimes')
|
|
45
|
+
Unlimited = Bool(name='unlimited', required=False)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ExpectationBody(Dictionary):
|
|
49
|
+
HttpRequest = HttpRequest(name='httpRequest')
|
|
50
|
+
HttpResponse = HttpResponse(name='httpResponse')
|
|
51
|
+
Times = Times(name='times')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RetrieveHttpRequest(Dictionary):
|
|
55
|
+
Method = HttpMethod(name='method', required=False)
|
|
56
|
+
Body = Raw(name='body', required=False)
|
|
57
|
+
Path = String(name='path')
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class RetrieveBody(Dictionary):
|
|
61
|
+
HttpRequest = RetrieveHttpRequest(name='httpRequest')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DataResponse(Dictionary):
|
|
65
|
+
Data = Raw(name='data')
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class MatchType(Enum):
|
|
69
|
+
Strict = 'STRICT'
|
|
70
|
+
Partial = 'ONLY_MATCHING_FIELDS'
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class JsonFilter(Dictionary):
|
|
74
|
+
Type = String(name='type', default='JSON')
|
|
75
|
+
Json = Raw(name='json')
|
|
76
|
+
MatchType = MatchType(name='matchType', default=MatchType.Partial)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class StringFilter(Dictionary):
|
|
80
|
+
Type = String(name='type', default='STRING')
|
|
81
|
+
String = String(name='string')
|
|
82
|
+
SubString = Bool(name='subString', default=True)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TimeToLive(Dictionary):
|
|
86
|
+
Unlimited = Bool(name='unlimited')
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class UnaddressedRequest(Dictionary):
|
|
90
|
+
HttpRequest = HttpRequest(name='httpRequest')
|
|
91
|
+
HttpResponse = HttpResponse(name='httpResponse', required=False)
|
|
92
|
+
Id = String(name='id')
|
|
93
|
+
Priority = Int(name='priority')
|
|
94
|
+
TimeToLive = TimeToLive(name='timeToLive')
|
|
95
|
+
Times = Times(name='times', required=False)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import waiting
|
|
2
|
+
|
|
3
|
+
from waiting.exceptions import TimeoutExpired
|
|
4
|
+
|
|
5
|
+
from surety.sdk import Field
|
|
6
|
+
from surety.config import Cfg
|
|
7
|
+
from surety.api.caller import ApiCaller
|
|
8
|
+
from surety.api.method import ApiMethod
|
|
9
|
+
from surety.api.mock.data import (
|
|
10
|
+
Command, ExpectationBody, HttpRequest, HttpResponse, ObjectType, Params,
|
|
11
|
+
PathParams, RetrieveBody, RetrieveHttpRequest, Times, UnaddressedRequest,
|
|
12
|
+
)
|
|
13
|
+
from surety.api.schema import HttpMethod
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_mockserver_url():
|
|
17
|
+
if 'MockServer' in Cfg:
|
|
18
|
+
return f'http://{Cfg.MockServer.host}:{Cfg.MockServer.port}'
|
|
19
|
+
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
BASE_URL = get_mockserver_url()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RunCommand(ApiMethod):
|
|
27
|
+
method = HttpMethod.PUT
|
|
28
|
+
url = 'mockserver/{command}'
|
|
29
|
+
path_params = PathParams
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MockClient(ApiCaller):
|
|
33
|
+
method = RunCommand
|
|
34
|
+
|
|
35
|
+
def __init__(self, command, params=None, headers=None, body=None):
|
|
36
|
+
super().__init__(
|
|
37
|
+
headers=headers,
|
|
38
|
+
path_params=PathParams().with_values({
|
|
39
|
+
PathParams.Command.name: command,
|
|
40
|
+
}),
|
|
41
|
+
params=params,
|
|
42
|
+
req_body=body
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MockServer:
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _get_client(command, params=None, headers=None, body=None):
|
|
49
|
+
return MockClient(command, params, headers, body).request()
|
|
50
|
+
|
|
51
|
+
def reset(self):
|
|
52
|
+
self._get_client(command=Command.Reset, headers={})
|
|
53
|
+
|
|
54
|
+
def reply(self, method, url, body, status=200, params=None, req_body=None,
|
|
55
|
+
headers=None, cookies=None, times=1, reset=True):
|
|
56
|
+
if reset:
|
|
57
|
+
self.reset()
|
|
58
|
+
|
|
59
|
+
self._get_client(
|
|
60
|
+
command=Command.Expectation,
|
|
61
|
+
headers=headers,
|
|
62
|
+
body=ExpectationBody().with_values({
|
|
63
|
+
ExpectationBody.HttpRequest.name: {
|
|
64
|
+
HttpRequest.Method.name: method,
|
|
65
|
+
HttpRequest.Path.name: url,
|
|
66
|
+
HttpRequest.Params.name: params,
|
|
67
|
+
HttpRequest.Body.name: req_body,
|
|
68
|
+
},
|
|
69
|
+
ExpectationBody.HttpResponse.name: {
|
|
70
|
+
HttpResponse.StatusCode.name: status,
|
|
71
|
+
HttpResponse.Headers.name: headers,
|
|
72
|
+
HttpResponse.Cookies.name: cookies,
|
|
73
|
+
HttpResponse.Body.name: body
|
|
74
|
+
},
|
|
75
|
+
ExpectationBody.Times.name: {
|
|
76
|
+
Times.RemainingTimes.name: times,
|
|
77
|
+
Times.Unlimited.name: False,
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def catch(self, url, body=None, method=None, timeout=None,
|
|
83
|
+
latest=False):
|
|
84
|
+
req_body = body
|
|
85
|
+
|
|
86
|
+
if isinstance(req_body, Field):
|
|
87
|
+
req_body = req_body.value
|
|
88
|
+
|
|
89
|
+
def catch_request():
|
|
90
|
+
return self._get_client(
|
|
91
|
+
command=Command.Retrieve,
|
|
92
|
+
params=Params().with_values({
|
|
93
|
+
Params.Type.name: ObjectType.Requests,
|
|
94
|
+
}),
|
|
95
|
+
body=RetrieveBody().with_values({
|
|
96
|
+
RetrieveBody.HttpRequest.name:
|
|
97
|
+
RetrieveHttpRequest().with_values({
|
|
98
|
+
RetrieveHttpRequest.Method.name: method,
|
|
99
|
+
RetrieveHttpRequest.Path.name: url,
|
|
100
|
+
RetrieveHttpRequest.Body.name: req_body,
|
|
101
|
+
}),
|
|
102
|
+
})
|
|
103
|
+
).response.json()
|
|
104
|
+
|
|
105
|
+
if timeout is None:
|
|
106
|
+
caught = catch_request()
|
|
107
|
+
else:
|
|
108
|
+
caught = waiting.wait(
|
|
109
|
+
catch_request,
|
|
110
|
+
timeout_seconds=timeout,
|
|
111
|
+
sleep_seconds=0,
|
|
112
|
+
waiting_for='request'
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
assert caught, 'No requests received'
|
|
116
|
+
|
|
117
|
+
if latest:
|
|
118
|
+
incoming = caught[0]
|
|
119
|
+
else:
|
|
120
|
+
assert len(caught) == 1, 'More than one request are caught'
|
|
121
|
+
incoming = caught[0]
|
|
122
|
+
|
|
123
|
+
headers = incoming.get('headers')
|
|
124
|
+
body = incoming.get('body')
|
|
125
|
+
params = incoming.get('queryStringParameters')
|
|
126
|
+
|
|
127
|
+
if isinstance(body, dict) and 'json' in body:
|
|
128
|
+
body = body['json']
|
|
129
|
+
|
|
130
|
+
return body, headers, params
|
|
131
|
+
|
|
132
|
+
def get_unaddressed_requests(self):
|
|
133
|
+
response = self._get_client(
|
|
134
|
+
command=Command.Retrieve,
|
|
135
|
+
params=Params().with_values({
|
|
136
|
+
Params.Type.name: ObjectType.ActiveExpectations,
|
|
137
|
+
})
|
|
138
|
+
).response.json()
|
|
139
|
+
|
|
140
|
+
return [UnaddressedRequest().with_values(resp) for resp in response]
|
|
141
|
+
|
|
142
|
+
def wait_for_mocks_to_be_called(self, timeout_seconds=1):
|
|
143
|
+
try:
|
|
144
|
+
return waiting.wait(
|
|
145
|
+
lambda: not self.get_unaddressed_requests(),
|
|
146
|
+
sleep_seconds=0,
|
|
147
|
+
timeout_seconds=timeout_seconds,
|
|
148
|
+
waiting_for='mocks to be called',
|
|
149
|
+
)
|
|
150
|
+
except TimeoutExpired:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
def verify_all_mocks_called(self, timeout_seconds=1): # pylint:disable=inconsistent-return-statements
|
|
154
|
+
try:
|
|
155
|
+
return waiting.wait(
|
|
156
|
+
lambda: not self.get_unaddressed_requests(),
|
|
157
|
+
sleep_seconds=0,
|
|
158
|
+
timeout_seconds=timeout_seconds,
|
|
159
|
+
waiting_for='mocks to be called',
|
|
160
|
+
)
|
|
161
|
+
except TimeoutExpired:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
requests = self.get_unaddressed_requests()
|
|
165
|
+
parsed_reqs = ''
|
|
166
|
+
|
|
167
|
+
for req in requests:
|
|
168
|
+
body = req.HttpRequest.Body.value
|
|
169
|
+
|
|
170
|
+
if body and 'json' in body:
|
|
171
|
+
body = body['json']
|
|
172
|
+
|
|
173
|
+
parsed_reqs += (
|
|
174
|
+
f'\n{req.HttpRequest.Method.value} '
|
|
175
|
+
f'{req.HttpRequest.Path.value} {body}'
|
|
176
|
+
f'\nTimes: {req.Times.RemainingTimes.value}'
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
assert not requests, f'There are unused mocks: {parsed_reqs}'
|
surety/api/schema.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
from urllib.parse import urljoin
|
|
4
|
+
|
|
5
|
+
from surety.process.common import exclude_none_from_kwargs
|
|
6
|
+
from surety.sdk import Dictionary, Enum
|
|
7
|
+
from surety.config import Cfg
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_default_app_url():
|
|
11
|
+
if 'App' in Cfg:
|
|
12
|
+
return Cfg.App.get('url')
|
|
13
|
+
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HttpMethod(Enum):
|
|
18
|
+
POST = 'POST'
|
|
19
|
+
GET = 'GET'
|
|
20
|
+
HEAD = 'HEAD'
|
|
21
|
+
PATCH = 'PATCH'
|
|
22
|
+
DELETE = 'DELETE'
|
|
23
|
+
PUT = 'PUT'
|
|
24
|
+
TRACE = 'TRACE'
|
|
25
|
+
OPTIONS = 'OPTIONS'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Schema:
|
|
29
|
+
"""
|
|
30
|
+
Additional configuration on module level:
|
|
31
|
+
SERVICE: service name for mocking
|
|
32
|
+
BASE_URL: application base url if not Cfg.App.url (default)
|
|
33
|
+
API_URL: api base url
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
method: HttpMethod
|
|
37
|
+
url: str
|
|
38
|
+
path_params = Dictionary
|
|
39
|
+
params = Dictionary
|
|
40
|
+
req_body = Dictionary
|
|
41
|
+
resp_body = Dictionary
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def get_relative_url(cls, path_params=None):
|
|
45
|
+
path_params = path_params or cls.path_params()
|
|
46
|
+
formatted_url = cls.url.format(**path_params.value)
|
|
47
|
+
return f'{cls.get_api_base_url()}{formatted_url}'
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def get_service_name(cls):
|
|
51
|
+
parent_module = inspect.getmodule(cls)
|
|
52
|
+
return getattr(parent_module, 'SERVICE', None)
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def get_base_url(cls):
|
|
56
|
+
parent_module = inspect.getmodule(cls)
|
|
57
|
+
return getattr(parent_module, 'BASE_URL', get_default_app_url())
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def get_api_base_url(cls):
|
|
61
|
+
parent_module = inspect.getmodule(cls)
|
|
62
|
+
return getattr(parent_module, 'API_URL', '')
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def get_request_url(cls, path_params=None):
|
|
66
|
+
return urljoin(
|
|
67
|
+
f'{cls.get_base_url()}',
|
|
68
|
+
f'{cls.get_relative_url(path_params=path_params)}'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def fe_headers(cls, app_url=None, content_type='application/json',
|
|
73
|
+
set_cookie=None):
|
|
74
|
+
return exclude_none_from_kwargs({
|
|
75
|
+
'Access-Control-Allow-Origin': app_url or get_default_app_url(),
|
|
76
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
77
|
+
'Content-Type': content_type,
|
|
78
|
+
'Set-Cookie': set_cookie,
|
|
79
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: surety-api
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Contract-aware API interaction layer for the Surety ecosystem.
|
|
5
|
+
Author-email: Elena Kulgavaya <elena.kulgavaya@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: api,contract-testing,automation,integration-testing,surety
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: deepdiff==8.0.1
|
|
15
|
+
Requires-Dist: surety<1.0,>=0.0.4
|
|
16
|
+
Requires-Dist: surety-config>=0.0.3
|
|
17
|
+
Requires-Dist: surety-diff>=0.0.1
|
|
18
|
+
Requires-Dist: requests
|
|
19
|
+
Requires-Dist: pyyaml
|
|
20
|
+
Requires-Dist: waiting
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Surety API
|
|
24
|
+
|
|
25
|
+
Contract-aware API interaction layer for the Surety ecosystem.
|
|
26
|
+
|
|
27
|
+
`surety-api` enables structured API testing, mocking, and
|
|
28
|
+
interaction based on Surety contracts.
|
|
29
|
+
|
|
30
|
+
It bridges declarative contracts and real HTTP communication.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install surety-api
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
surety/api/__init__.py,sha256=L9CtiC2lk7fVnzAk5dpT7NsA9EqL_xccpNVIcitZZsY,128
|
|
2
|
+
surety/api/caller.py,sha256=o5ZEgrxIX_FYUgjmhH8O-z9m703isuIvbpxRJ9HTV9o,2701
|
|
3
|
+
surety/api/method.py,sha256=R5GReigCdpEnwepCQuNfvSPT3wP5F1nIMaiLRPrXm2c,5630
|
|
4
|
+
surety/api/schema.py,sha256=GLw2JtrTburkXs_4HtBfgleexyo-oGhCv0PE4FyYQG8,2114
|
|
5
|
+
surety/api/mock/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
surety/api/mock/data.py,sha256=01hqX3MKvDkx-LpYG0Gcp4dUblYzlC1EUemgDspT8OU,2397
|
|
7
|
+
surety/api/mock/service.py,sha256=06OzsPBSZMB-Up7czOWsz5jBiNAu1RMXFHJZWvzOxm4,5700
|
|
8
|
+
surety_api-0.0.1.dist-info/licenses/LICENSE,sha256=3TUbLvR5i9BMk3nE-KUtbyBFoL73ES3YioEMOvxselQ,1072
|
|
9
|
+
surety_api-0.0.1.dist-info/METADATA,sha256=xCTfy2b-jL2z0pAUsFkzh_EnuCP_wDz9-CaPIJ-h_oc,1016
|
|
10
|
+
surety_api-0.0.1.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
11
|
+
surety_api-0.0.1.dist-info/top_level.txt,sha256=a0-FMXVmX6qb_bkdgaQl82FUEQ-xnlMOp3ibsLoxdgs,7
|
|
12
|
+
surety_api-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Elena Kulgavaya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
surety
|