stoobly-agent 1.8.4__py3-none-any.whl → 1.9.0__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.
- stoobly_agent/__init__.py +1 -1
- stoobly_agent/app/api/configs_controller.py +3 -3
- stoobly_agent/app/api/headers_controller.py +1 -1
- stoobly_agent/app/api/query_params_controller.py +4 -4
- stoobly_agent/app/cli/helpers/handle_config_update_service.py +12 -12
- stoobly_agent/app/cli/intercept_cli.py +30 -7
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/.Makefile +6 -0
- stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/.overwrite +13 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +16 -0
- stoobly_agent/app/cli/snapshot_cli.py +57 -0
- stoobly_agent/app/cli/types/snapshot_migration.py +61 -0
- stoobly_agent/app/models/adapters/joined_request_adapter.py +1 -1
- stoobly_agent/app/models/adapters/orm/request/mitmproxy_adapter.py +1 -1
- stoobly_agent/app/models/adapters/python/request/__init__.py +2 -2
- stoobly_agent/app/models/adapters/python/request/mitmproxy_adapter.py +1 -1
- stoobly_agent/app/models/adapters/python/request/raw_adapter.py +2 -2
- stoobly_agent/app/models/factories/resource/local_db/helpers/log.py +7 -0
- stoobly_agent/app/models/factories/resource/local_db/helpers/log_event.py +10 -0
- stoobly_agent/app/models/factories/resource/local_db/helpers/request_snapshot.py +20 -0
- stoobly_agent/app/models/factories/resource/local_db/helpers/scenario_snapshot.py +8 -1
- stoobly_agent/app/models/factories/resource/local_db/helpers/snapshot.py +1 -1
- stoobly_agent/app/models/factories/resource/local_db/query_param_adapter.py +2 -2
- stoobly_agent/app/models/helpers/apply.py +7 -6
- stoobly_agent/app/models/helpers/create_request_params_service.py +4 -2
- stoobly_agent/app/proxy/handle_mock_service.py +19 -6
- stoobly_agent/app/proxy/handle_record_service.py +16 -7
- stoobly_agent/app/proxy/intercept_settings.py +12 -1
- stoobly_agent/app/proxy/mitmproxy/request_facade.py +1 -1
- stoobly_agent/app/proxy/record/join_request_service.py +22 -10
- stoobly_agent/app/proxy/record/proxy_request.py +9 -0
- stoobly_agent/app/proxy/record/request_string.py +19 -10
- stoobly_agent/app/proxy/record/upload_request_service.py +7 -5
- stoobly_agent/app/proxy/replay/body_parser_service.py +15 -4
- stoobly_agent/app/proxy/replay/replay_request_service.py +1 -0
- stoobly_agent/app/proxy/test/helpers/upload_test_service.py +1 -1
- stoobly_agent/app/settings/data_rules.py +16 -1
- stoobly_agent/config/constants/custom_headers.py +1 -0
- stoobly_agent/config/constants/lifecycle_hooks.py +2 -0
- stoobly_agent/config/constants/record_order.py +6 -0
- stoobly_agent/config/constants/record_policy.py +2 -2
- stoobly_agent/lib/orm/transformers/orm_to_request_transformer.py +1 -1
- stoobly_agent/public/18-es2015.beb31fe4a4dee3007cb2.js +1 -0
- stoobly_agent/public/18-es5.beb31fe4a4dee3007cb2.js +1 -0
- stoobly_agent/public/index.html +2 -2
- stoobly_agent/public/main-es2015.089b46f303768fbe864f.js +1 -0
- stoobly_agent/public/main-es5.089b46f303768fbe864f.js +1 -0
- stoobly_agent/public/{runtime-es2015.b13c22b834b51724d30a.js → runtime-es2015.f8c814b38b27708e91c1.js} +1 -1
- stoobly_agent/public/{runtime-es5.b13c22b834b51724d30a.js → runtime-es5.f8c814b38b27708e91c1.js} +1 -1
- stoobly_agent/public/{styles.ab281309cf423b2cdcb0.css → styles.817f011ab81b18b0e5c2.css} +1 -1
- stoobly_agent/test/app/cli/config/scenario/config_scenario_set_test.py +3 -3
- stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +5 -7
- stoobly_agent/test/app/cli/intercept/intercept_enable_test.py +3 -5
- stoobly_agent/test/app/cli/scenario/scenario_snapshot_test.py +1 -1
- stoobly_agent/test/app/cli/snapshot/lifecycle_hooks_migrate.py +10 -0
- stoobly_agent/test/app/cli/snapshot/snapshot_migrate_test.py +181 -0
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/cli/record_test.py +3 -3
- {stoobly_agent-1.8.4.dist-info → stoobly_agent-1.9.0.dist-info}/METADATA +1 -1
- {stoobly_agent-1.8.4.dist-info → stoobly_agent-1.9.0.dist-info}/RECORD +63 -58
- stoobly_agent/public/18-es2015.d07dd29def7e2574c5b7.js +0 -1
- stoobly_agent/public/18-es5.d07dd29def7e2574c5b7.js +0 -1
- stoobly_agent/public/main-es2015.ce00115b0520fa030f01.js +0 -1
- stoobly_agent/public/main-es5.ce00115b0520fa030f01.js +0 -1
- {stoobly_agent-1.8.4.dist-info → stoobly_agent-1.9.0.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.8.4.dist-info → stoobly_agent-1.9.0.dist-info}/WHEEL +0 -0
- {stoobly_agent-1.8.4.dist-info → stoobly_agent-1.9.0.dist-info}/entry_points.txt +0 -0
@@ -3,7 +3,7 @@ import pdb
|
|
3
3
|
import requests
|
4
4
|
import time
|
5
5
|
|
6
|
-
from mitmproxy.http import
|
6
|
+
from mitmproxy.http import Request as MitmproxyRequest
|
7
7
|
from typing import Callable, TypedDict
|
8
8
|
|
9
9
|
from stoobly_agent.app.models.request_model import RequestModel
|
@@ -64,7 +64,7 @@ def handle_request_mock_generic(context: MockContext, **options: MockOptions):
|
|
64
64
|
|
65
65
|
return bad_request(
|
66
66
|
context.flow,
|
67
|
-
"Valid
|
67
|
+
"Valid mock policies: %s, Got: %s" %
|
68
68
|
([mock_policy.ALL, mock_policy.FOUND, mock_policy.NONE], policy)
|
69
69
|
)
|
70
70
|
|
@@ -88,18 +88,20 @@ def handle_request_mock_generic(context: MockContext, **options: MockOptions):
|
|
88
88
|
res = eval_request_with_retry(context, eval_request, **options)
|
89
89
|
|
90
90
|
context.with_response(res)
|
91
|
+
res = __after_mock_not_found(context)
|
91
92
|
elif policy == mock_policy.FOUND:
|
92
93
|
res = eval_request_with_retry(context, eval_request, **options)
|
93
94
|
|
94
95
|
context.with_response(res)
|
96
|
+
res = __after_mock_not_found(context)
|
95
97
|
|
96
|
-
if res.status_code
|
98
|
+
if res.status_code == custom_response_codes.NOT_FOUND:
|
97
99
|
try:
|
98
100
|
return __handle_found_policy(context) # Continue proxying the request
|
99
101
|
except RuntimeError:
|
100
102
|
# Do nothing, return custom error response
|
101
|
-
pass
|
102
|
-
|
103
|
+
pass
|
104
|
+
|
103
105
|
if res.status_code == custom_response_codes.NOT_FOUND:
|
104
106
|
if handle_failure:
|
105
107
|
res = handle_failure(context) or res
|
@@ -241,4 +243,15 @@ def __mock_hook(hook: str, context: MockContext):
|
|
241
243
|
lifecycle_hooks_module = intercept_settings.lifecycle_hooks
|
242
244
|
|
243
245
|
if hook in lifecycle_hooks_module:
|
244
|
-
lifecycle_hooks_module[hook](context)
|
246
|
+
lifecycle_hooks_module[hook](context)
|
247
|
+
|
248
|
+
def __after_mock_not_found(context: MockContext):
|
249
|
+
res = context.response
|
250
|
+
|
251
|
+
if res.status_code == custom_response_codes.NOT_FOUND:
|
252
|
+
__mock_hook(lifecycle_hooks.AFTER_MOCK_NOT_FOUND, context)
|
253
|
+
|
254
|
+
# context.response may have been modified by the hook
|
255
|
+
res = context.response
|
256
|
+
|
257
|
+
return res
|
@@ -9,7 +9,7 @@ from stoobly_agent.app.settings.constants.mode import TEST
|
|
9
9
|
from stoobly_agent.app.models.request_model import RequestModel
|
10
10
|
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
11
11
|
from stoobly_agent.config.constants.env_vars import ENV
|
12
|
-
from stoobly_agent.config.constants import lifecycle_hooks, record_policy
|
12
|
+
from stoobly_agent.config.constants import lifecycle_hooks, record_order, record_policy
|
13
13
|
from stoobly_agent.lib.logger import Logger
|
14
14
|
|
15
15
|
from .constants import custom_response_codes
|
@@ -17,6 +17,7 @@ from .mock.eval_request_service import inject_eval_request
|
|
17
17
|
from .record.context import RecordContext
|
18
18
|
from .record.overwrite_scenario_service import overwrite_scenario
|
19
19
|
from .record.upload_request_service import inject_upload_request
|
20
|
+
from .replay.body_parser_service import is_json, is_xml
|
20
21
|
from .utils.allowed_request_service import get_active_mode_policy
|
21
22
|
from .utils.response_handler import bad_request, disable_transfer_encoding
|
22
23
|
from .utils.rewrite import rewrite_request_response
|
@@ -42,6 +43,13 @@ def handle_response_record(context: RecordContext):
|
|
42
43
|
|
43
44
|
if active_record_policy == record_policy.ALL:
|
44
45
|
__record_request(context, request_model)
|
46
|
+
elif active_record_policy == record_policy.API:
|
47
|
+
response = flow.response
|
48
|
+
content_type: str = response.headers.get('content-type')
|
49
|
+
|
50
|
+
if content_type:
|
51
|
+
if is_json(content_type) or is_xml(content_type) or content_type.startswith('text/plain'):
|
52
|
+
__record_request(context, request_model)
|
45
53
|
elif active_record_policy == record_policy.FOUND:
|
46
54
|
res = inject_eval_request(request_model, intercept_settings)(request, [])
|
47
55
|
|
@@ -52,16 +60,12 @@ def handle_response_record(context: RecordContext):
|
|
52
60
|
|
53
61
|
if res.status_code == custom_response_codes.NOT_FOUND:
|
54
62
|
__record_request(context, request_model)
|
55
|
-
elif active_record_policy == record_policy.OVERWRITE:
|
56
|
-
overwrite_scenario(intercept_settings.scenario_key)
|
57
|
-
|
58
|
-
__record_request(context, request_model)
|
59
63
|
else:
|
60
64
|
if active_record_policy != record_policy.NONE:
|
61
65
|
return bad_request(
|
62
66
|
flow,
|
63
|
-
"Valid
|
64
|
-
([record_policy.ALL, record_policy.FOUND, record_policy.NOT_FOUND], active_record_policy)
|
67
|
+
"Valid record policies: %s, Got: %s" %
|
68
|
+
([record_policy.ALL, record_policy.API, record_policy.FOUND, record_policy.NOT_FOUND], active_record_policy)
|
65
69
|
)
|
66
70
|
|
67
71
|
def __record_handler(context: RecordContext, request_model: RequestModel):
|
@@ -79,6 +83,11 @@ def __record_handler(context: RecordContext, request_model: RequestModel):
|
|
79
83
|
context.flow = flow # Reset flow
|
80
84
|
|
81
85
|
def __record_request(context: RecordContext, request_model: RequestModel):
|
86
|
+
intercept_settings = context.intercept_settings
|
87
|
+
|
88
|
+
if intercept_settings.order == record_order.OVERWRITE:
|
89
|
+
overwrite_scenario(intercept_settings.scenario_key)
|
90
|
+
|
82
91
|
if os.environ.get(ENV) == TEST:
|
83
92
|
__record_handler(context, request_model)
|
84
93
|
else:
|
@@ -166,6 +166,10 @@ class InterceptSettings:
|
|
166
166
|
if self.__headers and custom_headers.REPORT_KEY in self.__headers:
|
167
167
|
return self.__headers[custom_headers.REPORT_KEY]
|
168
168
|
|
169
|
+
@property
|
170
|
+
def order(self):
|
171
|
+
return self.__order(self.mode)
|
172
|
+
|
169
173
|
@property
|
170
174
|
def policy(self):
|
171
175
|
return self.__policy(self.mode)
|
@@ -339,7 +343,11 @@ class InterceptSettings:
|
|
339
343
|
self.__response_fixtures = yaml.safe_load(stream)
|
340
344
|
except yaml.YAMLError as exc:
|
341
345
|
Logger.instance().error(exc)
|
342
|
-
|
346
|
+
|
347
|
+
def __order(self, mode):
|
348
|
+
if mode == intercept_mode.RECORD:
|
349
|
+
return self.__data_rules.record_order
|
350
|
+
|
343
351
|
def __policy(self, mode):
|
344
352
|
if mode == intercept_mode.MOCK:
|
345
353
|
if self.__headers and custom_headers.MOCK_POLICY in self.__headers:
|
@@ -352,6 +360,9 @@ class InterceptSettings:
|
|
352
360
|
|
353
361
|
return self.__data_rules.record_policy
|
354
362
|
elif mode == intercept_mode.TEST:
|
363
|
+
if self.__headers and custom_headers.TEST_POLICY in self.__headers:
|
364
|
+
return self.__headers[custom_headers.TEST_POLICY]
|
365
|
+
|
355
366
|
return self.__data_rules.test_policy
|
356
367
|
elif mode == intercept_mode.REPLAY:
|
357
368
|
return self.__data_rules.replay_policy
|
@@ -1,31 +1,43 @@
|
|
1
1
|
import pdb
|
2
2
|
|
3
|
-
from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
|
4
|
-
from typing import
|
3
|
+
from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow, Request as MitmproxyRequest, Response as MitmproxyResponse
|
4
|
+
from typing import TypedDict
|
5
5
|
|
6
6
|
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
7
|
-
from stoobly_agent.app.settings.rewrite_rule import RewriteRule
|
8
7
|
|
9
8
|
from ..mitmproxy.request_facade import MitmproxyRequestFacade
|
10
9
|
from ..mitmproxy.response_facade import MitmproxyResponseFacade
|
11
|
-
from ..utils.rewrite import rewrite_request_response
|
12
10
|
from .joined_request import JoinedRequest
|
13
11
|
from .proxy_request import ProxyRequest
|
14
12
|
|
13
|
+
class JoinRequestOptions(TypedDict):
|
14
|
+
id: str
|
15
|
+
intercept_settings: InterceptSettings
|
16
|
+
|
15
17
|
def join_request(
|
16
|
-
adapted_request: MitmproxyRequestFacade, adapted_response: MitmproxyResponseFacade,
|
18
|
+
adapted_request: MitmproxyRequestFacade, adapted_response: MitmproxyResponseFacade, **options: JoinRequestOptions
|
17
19
|
) -> JoinedRequest:
|
20
|
+
intercept_settings: InterceptSettings = options.get('intercept_settings')
|
21
|
+
|
18
22
|
# Decorate request with service_url
|
19
|
-
upstream_url = intercept_settings.upstream_url
|
23
|
+
upstream_url = intercept_settings.upstream_url if intercept_settings else None
|
20
24
|
proxy_request = ProxyRequest(adapted_request, upstream_url)
|
21
25
|
|
26
|
+
if options.get('id'):
|
27
|
+
proxy_request.id = options['id']
|
28
|
+
|
22
29
|
# Create JoinedRequest
|
23
30
|
return JoinedRequest(proxy_request).with_response(adapted_response)
|
24
31
|
|
25
32
|
def join_request_from_flow(
|
26
|
-
flow: MitmproxyHTTPFlow,
|
33
|
+
flow: MitmproxyHTTPFlow, **options: JoinRequestOptions
|
27
34
|
) -> JoinedRequest:
|
28
|
-
|
29
|
-
|
35
|
+
return join_request_from_request_response(flow.request, flow.response, **options)
|
36
|
+
|
37
|
+
def join_request_from_request_response(
|
38
|
+
request: MitmproxyRequest, response: MitmproxyResponse, **options: JoinRequestOptions
|
39
|
+
):
|
40
|
+
request = MitmproxyRequestFacade(request)
|
41
|
+
response = MitmproxyResponseFacade(response)
|
30
42
|
|
31
|
-
return join_request(request, response,
|
43
|
+
return join_request(request, response, **options)
|
@@ -2,10 +2,19 @@ from ..mitmproxy.request_facade import MitmproxyRequestFacade
|
|
2
2
|
|
3
3
|
class ProxyRequest:
|
4
4
|
def __init__(self, request: MitmproxyRequestFacade, upstream_url: str = None):
|
5
|
+
self._id = None
|
5
6
|
self.request = request
|
6
7
|
|
7
8
|
self.upstream_url = upstream_url
|
8
9
|
|
10
|
+
@property
|
11
|
+
def id(self):
|
12
|
+
return self._id
|
13
|
+
|
14
|
+
@id.setter
|
15
|
+
def id(self, v):
|
16
|
+
self._id = v
|
17
|
+
|
9
18
|
def url(self):
|
10
19
|
url = self.request.url
|
11
20
|
|
@@ -9,12 +9,13 @@ from stoobly_agent.lib.utils.decode import decode
|
|
9
9
|
from .proxy_request import ProxyRequest
|
10
10
|
from .request_string_control import RequestStringControl
|
11
11
|
|
12
|
-
CLRF = "\r\n"
|
12
|
+
CLRF = b"\r\n"
|
13
13
|
|
14
14
|
class RequestString:
|
15
15
|
ENCODING = 'utf-8'
|
16
16
|
|
17
17
|
__current_time = None
|
18
|
+
request_id = None
|
18
19
|
|
19
20
|
def __init__(self, proxy_request: ProxyRequest):
|
20
21
|
self.__current_time = self.__get_current_time()
|
@@ -29,24 +30,25 @@ class RequestString:
|
|
29
30
|
self.__headers()
|
30
31
|
self.__body()
|
31
32
|
|
32
|
-
|
33
|
+
self.request_id = proxy_request.id
|
34
|
+
|
35
|
+
self.request_id = self.request_id or self.__generate_request_id()
|
33
36
|
|
34
37
|
def get(self, **kwargs):
|
35
38
|
if kwargs.get('control'):
|
36
|
-
return CLRF.join([self.control] + self.lines)
|
39
|
+
return CLRF.join([self.control] + self.lines)
|
37
40
|
else:
|
38
|
-
return CLRF.join(self.lines)
|
41
|
+
return CLRF.join(self.lines)
|
39
42
|
|
40
43
|
def set(self, s: bytes):
|
41
|
-
|
42
|
-
self.lines = decoded_s.split(CLRF)
|
44
|
+
self.lines = s.split(CLRF)
|
43
45
|
|
44
46
|
@property
|
45
47
|
def control(self):
|
46
48
|
control = RequestStringControl()
|
47
49
|
control.id = self.request_id
|
48
50
|
control.timestamp = self.__current_time
|
49
|
-
return control.serialize()
|
51
|
+
return control.serialize().encode(self.ENCODING)
|
50
52
|
|
51
53
|
@control.setter
|
52
54
|
def control(self, c: str):
|
@@ -55,20 +57,27 @@ class RequestString:
|
|
55
57
|
self.__current_time = control.timestamp
|
56
58
|
|
57
59
|
def __request_line(self):
|
58
|
-
|
60
|
+
line = "{} {} HTTP/1.1".format(self.request.method, self.proxy_request.url())
|
61
|
+
self.lines.append(line.encode(self.ENCODING))
|
59
62
|
|
60
63
|
def __headers(self):
|
61
64
|
headers = self.request.headers
|
62
65
|
|
63
66
|
for name, val in headers.items():
|
67
|
+
if 'content-encoding' in headers:
|
68
|
+
# self.request.body will decode content-encoding
|
69
|
+
# to maintain internal consistency, remove this header since it will be decoded
|
70
|
+
print(self.request.body)
|
71
|
+
pass
|
72
|
+
|
64
73
|
line = ' '.join([
|
65
74
|
"{}:".format(self.__to_header_case(self.__to_str(name))),
|
66
75
|
self.__to_str(val)
|
67
76
|
])
|
68
|
-
self.lines.append(line)
|
77
|
+
self.lines.append(line.encode(self.ENCODING))
|
69
78
|
|
70
79
|
def __body(self):
|
71
|
-
self.lines.append(
|
80
|
+
self.lines.append(CLRF + self.request.body)
|
72
81
|
|
73
82
|
def __to_header_case(self, header: str) -> str:
|
74
83
|
toks = header.split('_')
|
@@ -4,6 +4,7 @@ import pdb
|
|
4
4
|
import time
|
5
5
|
import tempfile
|
6
6
|
|
7
|
+
from copy import deepcopy
|
7
8
|
from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
|
8
9
|
from mitmproxy.http import Request as MitmproxyRequest
|
9
10
|
|
@@ -50,22 +51,23 @@ def upload_request(
|
|
50
51
|
):
|
51
52
|
Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Recording{bcolors.ENDC} {flow.request.url}")
|
52
53
|
|
53
|
-
|
54
|
+
flow_copy = deepcopy(flow) # When applying modifications we don't want to persist them in the response
|
55
|
+
joined_request = join_request_from_flow(flow_copy, intercept_settings=intercept_settings)
|
54
56
|
|
55
57
|
project_key = intercept_settings.project_key
|
56
58
|
scenario_key = intercept_settings.scenario_key
|
57
59
|
body_params = __build_body_params(
|
58
60
|
project_key,
|
59
|
-
joined_request,
|
61
|
+
joined_request,
|
60
62
|
flow=flow,
|
61
63
|
scenario_key=scenario_key
|
62
64
|
)
|
63
65
|
|
64
66
|
# If request_origin is WEB, then we are in proxy
|
65
67
|
# This means that we have access to Cache singleton and do not need send a request to update the status
|
66
|
-
sync = intercept_settings.request_origin == request_origin.WEB
|
68
|
+
sync = intercept_settings.request_origin == request_origin.WEB
|
67
69
|
res = __upload_request_with_body_params(request_model, body_params, sync)
|
68
|
-
|
70
|
+
|
69
71
|
if intercept_settings.settings.is_debug():
|
70
72
|
file_path = __debug_request(flow.request, joined_request.build())
|
71
73
|
Logger.instance(LOG_ID).debug(f"Writing request to {file_path}")
|
@@ -142,4 +144,4 @@ def __debug_request(request: MitmproxyRequest, raw_requests: bytes):
|
|
142
144
|
with open(file_path, 'wb') as f:
|
143
145
|
f.write(raw_requests)
|
144
146
|
|
145
|
-
return file_path
|
147
|
+
return file_path
|
@@ -14,6 +14,7 @@ from .multipart import decode as multipart_decode, encode as multipart_encode
|
|
14
14
|
JSON = 'application/json'
|
15
15
|
MULTIPART_FORM = 'multipart/form-data'
|
16
16
|
WWW_FORM_URLENCODED = 'application/x-www-form-urlencoded'
|
17
|
+
XML = 'application/xml'
|
17
18
|
|
18
19
|
def compress(body: Union[bytes, str], content_encoding: Union[None, str]) -> Union[bytes, str]:
|
19
20
|
if content_encoding:
|
@@ -113,14 +114,24 @@ def normalize_header(header):
|
|
113
114
|
def is_traversable(content):
|
114
115
|
return isinstance(content, list) or isinstance(content, dict) or isinstance(content, MultiDict)
|
115
116
|
|
116
|
-
def is_json(content_type):
|
117
|
-
|
118
|
-
|
117
|
+
def is_json(content_type: str):
|
118
|
+
if not content_type:
|
119
|
+
return False
|
120
|
+
|
121
|
+
_content_type = content_type.lower().split(';')[0]
|
122
|
+
# e.g. custom json content-type: application/x-amz-json
|
123
|
+
return _content_type == JSON or (_content_type.startswith('application/') and _content_type.endswith('json'))
|
119
124
|
|
125
|
+
def is_xml(content_type: str):
|
126
|
+
if not content_type:
|
127
|
+
return False
|
128
|
+
|
129
|
+
_content_type = content_type.lower().split(';')[0]
|
130
|
+
# e.g. custom json content-type: application/x-amz-json
|
131
|
+
return _content_type == XML or (_content_type.startswith('application/') and _content_type.endswith('xml'))
|
120
132
|
|
121
133
|
def __parse_separated_header(header: str):
|
122
134
|
# Adapted from https://peps.python.org/pep-0594/#cgi
|
123
135
|
message = Message()
|
124
136
|
message['content-type'] = header
|
125
137
|
return message.get_content_type()
|
126
|
-
|
@@ -150,6 +150,7 @@ def replay(context: ReplayContext, options: ReplayRequestOptions) -> requests.Re
|
|
150
150
|
if options.get('save') or options.get('overwrite'):
|
151
151
|
replayed_response, status = __create_replayed_response(context.request.id, res, latency)
|
152
152
|
|
153
|
+
# TODO: allow overwriting if status is the same
|
153
154
|
if status < 400 and options.get('overwrite'):
|
154
155
|
__overwrite_response(replayed_response.get('id'))
|
155
156
|
|
@@ -43,7 +43,7 @@ def upload_test(
|
|
43
43
|
flow: MitmproxyHTTPFlow,
|
44
44
|
**kwargs: UploadTestData
|
45
45
|
) -> Response:
|
46
|
-
joined_request = join_request_from_flow(flow, intercept_settings)
|
46
|
+
joined_request = join_request_from_flow(flow, intercept_settings=intercept_settings)
|
47
47
|
|
48
48
|
Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Uploading{bcolors.ENDC} test results for {joined_request.proxy_request.url()}")
|
49
49
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from stoobly_agent.config.constants import mock_policy, record_policy, replay_policy, test_strategy
|
1
|
+
from stoobly_agent.config.constants import mock_policy, record_order, record_policy, replay_policy, test_strategy
|
2
2
|
|
3
3
|
from .types.proxy_settings import DataRules as IDataRules
|
4
4
|
|
@@ -8,6 +8,7 @@ class DataRules:
|
|
8
8
|
self.__data_rules = data_rules or {}
|
9
9
|
|
10
10
|
self.__mock_policy = self.__data_rules.get('mock_policy') or mock_policy.FOUND
|
11
|
+
self.__record_order = self.__data_rules.get('record_order') or record_order.APPEND
|
11
12
|
self.__record_policy = self.__data_rules.get('record_policy') or record_policy.ALL
|
12
13
|
self.__replay_policy = self.__data_rules.get('replay_policy') or replay_policy.ALL
|
13
14
|
self.__scenario_key = self.__data_rules.get('scenario_key')
|
@@ -32,6 +33,19 @@ class DataRules:
|
|
32
33
|
self.__record_policy = v
|
33
34
|
self.__data_rules['record_policy'] = v
|
34
35
|
|
36
|
+
@property
|
37
|
+
def record_order(self):
|
38
|
+
return self.__record_order
|
39
|
+
|
40
|
+
@record_order.setter
|
41
|
+
def record_order(self, v):
|
42
|
+
valid_orders = [record_order.APPEND, record_order.OVERWRITE]
|
43
|
+
if v not in valid_orders:
|
44
|
+
raise TypeError(f"record_order has to be one of {valid_orders}, got {v}")
|
45
|
+
|
46
|
+
self.__record_order = v
|
47
|
+
self.__data_rules['record_order'] = v
|
48
|
+
|
35
49
|
@property
|
36
50
|
def replay_policy(self):
|
37
51
|
return self.__replay_policy
|
@@ -68,6 +82,7 @@ class DataRules:
|
|
68
82
|
def to_dict(self) -> IDataRules:
|
69
83
|
return {
|
70
84
|
'mock_policy': self.__mock_policy,
|
85
|
+
'record_order': self.__record_order,
|
71
86
|
'record_policy': self.__record_policy,
|
72
87
|
'replay_policy': self.__replay_policy,
|
73
88
|
'scenario_key': self.__scenario_key,
|
@@ -23,6 +23,7 @@ SESSION_ID = 'X-Stoobly-Session-Id'
|
|
23
23
|
SERVICE_URL = 'X-Stoobly-Service-Url'
|
24
24
|
TEST_FILTER = 'X-Stoobly-Test-Filter'
|
25
25
|
TEST_ID = 'X-Stoobly-Test-Id'
|
26
|
+
TEST_POLICY = 'X-Stoobly-Test-Policy'
|
26
27
|
TEST_SAVE_RESULTS = 'X-Stoobly-Test-Save-Results'
|
27
28
|
TEST_SKIP = 'X-Stoobly-Test-Skip'
|
28
29
|
TEST_STRATEGY = 'X-Stoobly-Test-Strategy'
|
@@ -1,8 +1,10 @@
|
|
1
1
|
AFTER_MOCK = 'handle_after_mock'
|
2
|
+
AFTER_MOCK_NOT_FOUND = 'handle_after_mock_not_found'
|
2
3
|
AFTER_RECORD = 'handle_after_record'
|
3
4
|
AFTER_REPLAY = 'handle_after_replay'
|
4
5
|
AFTER_TEST = 'handle_after_test'
|
5
6
|
BEFORE_MOCK = 'handle_before_mock'
|
7
|
+
BEFORE_MIGRATE = 'handle_before_migrate'
|
6
8
|
BEFORE_RECORD = 'handle_before_record'
|
7
9
|
BEFORE_REPLAY = 'handle_before_replay'
|
8
10
|
BEFORE_REQUEST = 'handle_before_request'
|