stoobly-agent 1.4.2__py3-none-any.whl → 1.5.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.
- stoobly_agent/__init__.py +1 -1
- stoobly_agent/app/cli/helpers/handle_config_update_service.py +2 -2
- stoobly_agent/app/cli/helpers/handle_mock_service.py +6 -2
- stoobly_agent/app/cli/helpers/request_facade.py +5 -1
- stoobly_agent/app/cli/scaffold/constants.py +1 -1
- stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +1 -0
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +19 -19
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
- stoobly_agent/app/cli/scaffold/templates/constants.py +3 -3
- stoobly_agent/app/cli/scaffold/templates/factory.py +5 -5
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +1 -8
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/fixtures.yml +1 -1
- stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +1 -8
- stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures.yml +1 -1
- stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
- stoobly_agent/app/cli/scaffold/workflow_create_command.py +2 -2
- stoobly_agent/app/cli/scaffold_cli.py +5 -5
- stoobly_agent/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request.py +1 -1
- stoobly_agent/app/models/factories/resource/local_db/request_adapter.py +17 -11
- stoobly_agent/app/models/types/request.py +1 -2
- stoobly_agent/app/proxy/context.py +4 -0
- stoobly_agent/app/proxy/handle_mock_service.py +93 -46
- stoobly_agent/app/proxy/handle_record_service.py +15 -3
- stoobly_agent/app/proxy/handle_replay_service.py +44 -18
- stoobly_agent/app/proxy/handle_test_service.py +92 -24
- stoobly_agent/app/proxy/intercept_handler.py +11 -16
- stoobly_agent/app/proxy/intercept_settings.py +17 -4
- stoobly_agent/app/proxy/mitmproxy/request_facade.py +5 -2
- stoobly_agent/app/proxy/mitmproxy/response_facade.py +5 -4
- stoobly_agent/app/proxy/mock/custom_not_found_response_builder.py +5 -0
- stoobly_agent/app/proxy/mock/eval_fixtures_service.py +79 -14
- stoobly_agent/app/proxy/mock/eval_request_service.py +18 -13
- stoobly_agent/app/proxy/record/join_request_service.py +7 -8
- stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
- stoobly_agent/app/proxy/replay/replay_request_service.py +4 -4
- stoobly_agent/app/proxy/test/helpers/upload_test_service.py +2 -2
- stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -3
- stoobly_agent/app/proxy/utils/response_handler.py +10 -1
- stoobly_agent/app/proxy/utils/rewrite.py +72 -0
- stoobly_agent/app/settings/constants/request_component.py +4 -1
- stoobly_agent/cli.py +35 -28
- stoobly_agent/config/constants/custom_headers.py +1 -0
- stoobly_agent/config/constants/intercept_policy.py +2 -0
- stoobly_agent/config/constants/mock_policy.py +4 -2
- stoobly_agent/config/constants/query_params.py +2 -0
- stoobly_agent/config/constants/record_policy.py +4 -2
- stoobly_agent/config/constants/replay_policy.py +4 -2
- stoobly_agent/public/{18-es2015.583f191cc7ad512ee262.js → 18-es2015.503207073756a9c8211a.js} +1 -1
- stoobly_agent/public/{18-es5.583f191cc7ad512ee262.js → 18-es5.503207073756a9c8211a.js} +1 -1
- stoobly_agent/public/index.html +1 -1
- stoobly_agent/public/{main-es2015.2cc16523aa3fcaba51e5.js → main-es2015.d682619f3d6d53d64c6a.js} +1 -1
- stoobly_agent/public/{main-es5.2cc16523aa3fcaba51e5.js → main-es5.d682619f3d6d53d64c6a.js} +1 -1
- stoobly_agent/public/{runtime-es2015.b914470164e4d6e75d96.js → runtime-es2015.8c1efed946fc02c923fc.js} +1 -1
- stoobly_agent/public/{runtime-es5.b914470164e4d6e75d96.js → runtime-es5.8c1efed946fc02c923fc.js} +1 -1
- stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_test.py +2 -1
- stoobly_agent/test/app/cli/scaffold/e2e_test.py +2 -2
- stoobly_agent/test/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request_test.py +4 -4
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +140 -71
- stoobly_agent/test/cli/lifecycle_hooks_test.py +66 -0
- stoobly_agent/test/cli/mock_scenario_lifecycle_hooks.py +5 -0
- stoobly_agent/test/cli/mock_scenario_test.py +62 -0
- stoobly_agent/test/cli/mock_test.py +54 -38
- stoobly_agent/test/cli/record_test.py +67 -0
- stoobly_agent/test/mock_data/lifecycle_hooks.py +35 -0
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/LICENSE +1 -1
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/METADATA +7 -12
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/RECORD +72 -65
- /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{fixtures/.keep → public/.gitignore} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/workflow/test/{fixtures/.keep → public/.gitignore} +0 -0
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/WHEEL +0 -0
- {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/entry_points.txt +0 -0
@@ -5,16 +5,17 @@ import re
|
|
5
5
|
from mitmproxy.http import Request as MitmproxyRequest
|
6
6
|
from requests import Response
|
7
7
|
from typing import List, TypedDict, Union
|
8
|
-
from stoobly_agent.config.constants import custom_headers
|
9
8
|
|
10
|
-
from stoobly_agent.lib.api.param_builder import ParamBuilder
|
11
|
-
from stoobly_agent.lib.api.interfaces.requests import RequestResponseShowQueryParams
|
12
|
-
from stoobly_agent.lib.logger import Logger
|
13
9
|
from stoobly_agent.app.models.request_model import RequestModel
|
14
10
|
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
11
|
+
from stoobly_agent.app.proxy.mock.custom_not_found_response_builder import CustomNotFoundResponseBuilder
|
15
12
|
from stoobly_agent.app.settings import Settings
|
16
13
|
from stoobly_agent.app.settings.constants import request_component
|
17
14
|
from stoobly_agent.app.settings.match_rule import MatchRule
|
15
|
+
from stoobly_agent.config.constants import custom_headers, query_params as request_query_params
|
16
|
+
from stoobly_agent.lib.api.param_builder import ParamBuilder
|
17
|
+
from stoobly_agent.lib.api.interfaces.requests import RequestResponseShowQueryParams
|
18
|
+
from stoobly_agent.lib.logger import Logger
|
18
19
|
|
19
20
|
from .hashed_request_decorator import HashedRequestDecorator
|
20
21
|
from .search_endpoint import inject_search_endpoint
|
@@ -40,10 +41,6 @@ def inject_eval_request(
|
|
40
41
|
request_model, intercept_settings, request, ignored_components or [], **options
|
41
42
|
)
|
42
43
|
|
43
|
-
###
|
44
|
-
#
|
45
|
-
# @param settings [Settings.mode.mock | Settings.mode.record]
|
46
|
-
#
|
47
44
|
def eval_request(
|
48
45
|
request_model: RequestModel,
|
49
46
|
intercept_settings: InterceptSettings,
|
@@ -52,9 +49,14 @@ def eval_request(
|
|
52
49
|
**options: EvalRequestOptions
|
53
50
|
) -> Response:
|
54
51
|
query_params_builder = ParamBuilder({})
|
55
|
-
query_params_builder.with_resource_scoping(intercept_settings.project_key, intercept_settings.scenario_key)
|
56
52
|
|
57
|
-
|
53
|
+
try:
|
54
|
+
query_params_builder.with_resource_scoping(intercept_settings.project_key, intercept_settings.scenario_key)
|
55
|
+
except:
|
56
|
+
# If project_key or scenario_key are invalid, assume custom not found
|
57
|
+
return CustomNotFoundResponseBuilder().build()
|
58
|
+
|
59
|
+
# Tease out API returning ignored components on custom not found
|
58
60
|
if request_model.is_local and not options.get('retry'):
|
59
61
|
remote_project_key = intercept_settings.parsed_remote_project_key
|
60
62
|
|
@@ -63,7 +65,7 @@ def eval_request(
|
|
63
65
|
remote_project_id = remote_project_key.id
|
64
66
|
endpoint_promise = lambda: search_endpoint(remote_project_id, request.method, request.url, ignored_components=1)
|
65
67
|
|
66
|
-
query_params_builder.with_param(
|
68
|
+
query_params_builder.with_param(request_query_params.ENDPOINT_PROMISE, endpoint_promise)
|
67
69
|
|
68
70
|
ignored_components = __build_ignored_components(ignored_components_list or [])
|
69
71
|
query_params_builder.with_params(__build_request_params(request, ignored_components))
|
@@ -142,6 +144,9 @@ def __build_optional_params(request: MitmproxyRequest, options: EvalRequestOptio
|
|
142
144
|
if custom_headers.MOCK_REQUEST_ID in headers:
|
143
145
|
optional_params['request_id'] = headers[custom_headers.MOCK_REQUEST_ID]
|
144
146
|
|
147
|
+
if custom_headers.SESSION_ID in headers:
|
148
|
+
optional_params[request_query_params.SESSION_ID] = headers[custom_headers.SESSION_ID]
|
149
|
+
|
145
150
|
return optional_params
|
146
151
|
|
147
152
|
def __filter_by_match_rules(request: MitmproxyRequest, match_rules: List[MatchRule], query_params: RequestResponseShowQueryParams):
|
@@ -149,9 +154,9 @@ def __filter_by_match_rules(request: MitmproxyRequest, match_rules: List[MatchRu
|
|
149
154
|
method = request.method.upper()
|
150
155
|
|
151
156
|
keep = {
|
152
|
-
request_component.BODY_PARAM:
|
157
|
+
request_component.BODY_PARAM: False,
|
153
158
|
request_component.HEADER: False,
|
154
|
-
request_component.QUERY_PARAM:
|
159
|
+
request_component.QUERY_PARAM: False,
|
155
160
|
}
|
156
161
|
|
157
162
|
match_rules = list(filter(lambda rule: method in rule.methods, match_rules))
|
@@ -1,10 +1,14 @@
|
|
1
1
|
import pdb
|
2
|
+
|
2
3
|
from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
|
4
|
+
from typing import List
|
3
5
|
|
4
6
|
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
7
|
+
from stoobly_agent.app.settings.rewrite_rule import RewriteRule
|
5
8
|
|
6
9
|
from ..mitmproxy.request_facade import MitmproxyRequestFacade
|
7
10
|
from ..mitmproxy.response_facade import MitmproxyResponseFacade
|
11
|
+
from ..utils.rewrite import rewrite_request_response
|
8
12
|
from .joined_request import JoinedRequest
|
9
13
|
from .proxy_request import ProxyRequest
|
10
14
|
|
@@ -18,15 +22,10 @@ def join_request(
|
|
18
22
|
# Create JoinedRequest
|
19
23
|
return JoinedRequest(proxy_request).with_response(adapted_response)
|
20
24
|
|
21
|
-
def
|
22
|
-
|
25
|
+
def join_request_from_flow(
|
26
|
+
flow: MitmproxyHTTPFlow, intercept_settings: InterceptSettings
|
27
|
+
) -> JoinedRequest:
|
23
28
|
request = MitmproxyRequestFacade(flow.request)
|
24
|
-
|
25
|
-
# Adapt flow.response
|
26
29
|
response = MitmproxyResponseFacade(flow.response)
|
27
|
-
rewrite_rules = intercept_settings.record_rewrite_rules
|
28
|
-
|
29
|
-
request.with_parameter_rules(rewrite_rules).with_url_rules(rewrite_rules).rewrite()
|
30
|
-
response.with_parameter_rules(rewrite_rules, request).rewrite()
|
31
30
|
|
32
31
|
return join_request(request, response, intercept_settings)
|
@@ -17,7 +17,7 @@ from stoobly_agent.lib.logger import Logger, bcolors
|
|
17
17
|
from stoobly_agent.lib.orm.request import Request
|
18
18
|
|
19
19
|
from ..utils.publish_change_service import publish_requests_modified
|
20
|
-
from .join_request_service import
|
20
|
+
from .join_request_service import join_request_from_flow
|
21
21
|
|
22
22
|
AGENT_STATUSES = {
|
23
23
|
'REQUESTS_MODIFIED': 'requests-modified'
|
@@ -50,7 +50,7 @@ def upload_request(
|
|
50
50
|
):
|
51
51
|
Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Recording{bcolors.ENDC} {flow.request.url}")
|
52
52
|
|
53
|
-
joined_request =
|
53
|
+
joined_request = join_request_from_flow(flow, intercept_settings)
|
54
54
|
|
55
55
|
project_key = intercept_settings.project_key
|
56
56
|
scenario_key = intercept_settings.scenario_key
|
@@ -32,6 +32,7 @@ class ReplayRequestOptions(TypedDict):
|
|
32
32
|
report_key: Union[str, None]
|
33
33
|
request_origin: Union[request_origin.CLI, None]
|
34
34
|
response_fixtures_path: str
|
35
|
+
response_mode: Union[mode.RECORD, None]
|
35
36
|
scenario_key: Union[str, None]
|
36
37
|
scheme: str
|
37
38
|
test_filter: test_filter.TestFilter
|
@@ -83,6 +84,9 @@ def replay(context: ReplayContext, options: ReplayRequestOptions) -> requests.Re
|
|
83
84
|
if options.get('response_fixtures_path'):
|
84
85
|
__handle_path_header(custom_headers.RESPONSE_FIXTURES_PATH, options['response_fixtures_path'], headers)
|
85
86
|
|
87
|
+
if options.get('response_mode'):
|
88
|
+
headers[custom_headers.RESPONSE_PROXY_MODE] = options['response_mode']
|
89
|
+
|
86
90
|
if options.get('scenario_key'):
|
87
91
|
headers[custom_headers.SCENARIO_KEY] = options['scenario_key']
|
88
92
|
|
@@ -173,10 +177,6 @@ def __handle_mode_option(_mode, request: Request, headers):
|
|
173
177
|
headers[custom_headers.MOCK_REQUEST_ID] = str(request.id)
|
174
178
|
|
175
179
|
headers[custom_headers.MOCK_POLICY] = mock_policy.ALL
|
176
|
-
elif _mode == mode.RECORD:
|
177
|
-
# If recording, then it's actually a replay and record
|
178
|
-
headers[custom_headers.PROXY_MODE] = mode.REPLAY
|
179
|
-
headers[custom_headers.RESPONSE_PROXY_MODE] = mode.RECORD
|
180
180
|
|
181
181
|
def __create_replayed_response(request_id: int, res: requests.Response, latency: int):
|
182
182
|
replayed_response_model = ReplayedResponseModel(Settings.instance())
|
@@ -11,7 +11,7 @@ from stoobly_agent.lib.logger import Logger, bcolors
|
|
11
11
|
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
12
12
|
|
13
13
|
from ...intercept_settings import InterceptSettings
|
14
|
-
from ...record.join_request_service import
|
14
|
+
from ...record.join_request_service import join_request_from_flow
|
15
15
|
|
16
16
|
LOG_ID = 'Test'
|
17
17
|
|
@@ -43,7 +43,7 @@ def upload_test(
|
|
43
43
|
flow: MitmproxyHTTPFlow,
|
44
44
|
**kwargs: UploadTestData
|
45
45
|
) -> Response:
|
46
|
-
joined_request =
|
46
|
+
joined_request = join_request_from_flow(flow, 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
|
|
@@ -6,7 +6,7 @@ from typing import List
|
|
6
6
|
|
7
7
|
from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
|
8
8
|
from stoobly_agent.app.settings.firewall_rule import FirewallRule
|
9
|
-
from stoobly_agent.config.constants import
|
9
|
+
from stoobly_agent.config.constants import intercept_policy, request_origin
|
10
10
|
from stoobly_agent.lib.logger import bcolors, Logger
|
11
11
|
|
12
12
|
LOG_ID = 'Firewall'
|
@@ -18,8 +18,8 @@ def get_active_mode_policy(request: MitmproxyRequest, intercept_settings: Interc
|
|
18
18
|
if allowed_request(request, intercept_settings):
|
19
19
|
return intercept_settings.policy
|
20
20
|
else:
|
21
|
-
# If the request path does not match accepted paths, do not
|
22
|
-
return
|
21
|
+
# If the request path does not match accepted paths, do not intercept
|
22
|
+
return intercept_policy.NONE
|
23
23
|
|
24
24
|
def allowed_request(request: MitmproxyRequest, intercept_settings: InterceptSettings) -> bool:
|
25
25
|
# If an exclude rule(s) exists, then only requests not matching these pattern(s) are allowed
|
@@ -36,7 +36,16 @@ def bad_request(flow: MitmproxyHTTPFlow, message: str):
|
|
36
36
|
{'Content-Type': 'text/plain'} # (optional) headers
|
37
37
|
)
|
38
38
|
|
39
|
-
|
39
|
+
def enable_cors(flow: MitmproxyHTTPFlow):
|
40
|
+
flow.response = MitmproxyResponse.make(
|
41
|
+
200,
|
42
|
+
'',
|
43
|
+
{
|
44
|
+
'Access-Control-Allow-Origin': '*',
|
45
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS, POST, PATCH, PUT, DELETE',
|
46
|
+
'Access-Control-Allow-Headers': '*'
|
47
|
+
}
|
48
|
+
)
|
40
49
|
|
41
50
|
# Without deleting this header, causes parsing issues when reading response
|
42
51
|
def disable_transfer_encoding(response: MitmproxyResponse) -> None:
|
@@ -0,0 +1,72 @@
|
|
1
|
+
from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
from stoobly_agent.app.settings.constants.request_component import REQUEST_COMPONENTS, RESPONSE_COMPONENTS
|
5
|
+
from stoobly_agent.app.settings.rewrite_rule import RewriteRule
|
6
|
+
|
7
|
+
from ..mitmproxy.request_facade import MitmproxyRequestFacade
|
8
|
+
from ..mitmproxy.response_facade import MitmproxyResponseFacade
|
9
|
+
|
10
|
+
def select_request_rewrite_rules(rewrite_rules: List[RewriteRule]):
|
11
|
+
rules = []
|
12
|
+
|
13
|
+
for rewrite_rule in rewrite_rules:
|
14
|
+
parameter_rules = list(filter(
|
15
|
+
lambda parameter: parameter.type in REQUEST_COMPONENTS and parameter.name,
|
16
|
+
rewrite_rule.parameter_rules or []
|
17
|
+
))
|
18
|
+
|
19
|
+
if len(parameter_rules) > 0:
|
20
|
+
rewrite_rule = RewriteRule(rewrite_rule.to_dict())
|
21
|
+
rewrite_rule.url_rules = rewrite_rule.url_rules
|
22
|
+
rewrite_rule.parameter_rules = parameter_rules
|
23
|
+
rules.append(rewrite_rule)
|
24
|
+
|
25
|
+
return rules
|
26
|
+
|
27
|
+
def select_response_rewrite_rules(rewrite_rules: List[RewriteRule]):
|
28
|
+
rules = []
|
29
|
+
|
30
|
+
for rewrite_rule in rewrite_rules:
|
31
|
+
parameter_rules = list(filter(
|
32
|
+
lambda parameter: parameter.type in RESPONSE_COMPONENTS and parameter.name,
|
33
|
+
rewrite_rule.parameter_rules or []
|
34
|
+
))
|
35
|
+
|
36
|
+
if len(parameter_rules) > 0:
|
37
|
+
rewrite_rule = RewriteRule(rewrite_rule.to_dict())
|
38
|
+
rewrite_rule.url_rules = []
|
39
|
+
rewrite_rule.parameter_rules = parameter_rules
|
40
|
+
rules.append(rewrite_rule)
|
41
|
+
|
42
|
+
return rules
|
43
|
+
|
44
|
+
def rewrite_request_response(flow: MitmproxyHTTPFlow, rewrite_rules: List[RewriteRule]):
|
45
|
+
request = rewrite_request(flow, rewrite_rules)
|
46
|
+
response = rewrite_response(flow, rewrite_rules, request)
|
47
|
+
return request, response
|
48
|
+
|
49
|
+
def rewrite_request(flow: MitmproxyHTTPFlow, rewrite_rules: List[RewriteRule]):
|
50
|
+
request = None
|
51
|
+
|
52
|
+
# Adapt flow.request
|
53
|
+
request = MitmproxyRequestFacade(flow.request)
|
54
|
+
|
55
|
+
_rewrite_rules = select_request_rewrite_rules(rewrite_rules)
|
56
|
+
if len(_rewrite_rules):
|
57
|
+
request.with_parameter_rules(_rewrite_rules).with_url_rules(_rewrite_rules).rewrite()
|
58
|
+
|
59
|
+
return request
|
60
|
+
|
61
|
+
def rewrite_response(flow: MitmproxyHTTPFlow, rewrite_rules: List[RewriteRule], request = None):
|
62
|
+
# Adapt flow.request
|
63
|
+
request = request or MitmproxyRequestFacade(flow.request)
|
64
|
+
|
65
|
+
# Adapt flow.response
|
66
|
+
response = MitmproxyResponseFacade(flow.response)
|
67
|
+
|
68
|
+
_rewrite_rules = select_response_rewrite_rules(rewrite_rules)
|
69
|
+
if len(_rewrite_rules):
|
70
|
+
response.with_parameter_rules(_rewrite_rules, request).rewrite()
|
71
|
+
|
72
|
+
return response
|
@@ -6,4 +6,7 @@ QUERY_PARAM = 'Query Param'
|
|
6
6
|
RESPONSE_HEADER = 'Response Header'
|
7
7
|
RESPONSE_PARAM = 'Response Param'
|
8
8
|
|
9
|
-
|
9
|
+
REQUEST_COMPONENTS = [BODY_PARAM, HEADER, QUERY_PARAM]
|
10
|
+
RESPONSE_COMPONENTS = [RESPONSE_HEADER, RESPONSE_PARAM]
|
11
|
+
|
12
|
+
RequestComponent = Literal[BODY_PARAM, HEADER, QUERY_PARAM, RESPONSE_HEADER, RESPONSE_PARAM]
|
stoobly_agent/cli.py
CHANGED
@@ -159,6 +159,7 @@ def run(**kwargs):
|
|
159
159
|
@click.option('--format', type=click.Choice([RAW_FORMAT]), help='Format response')
|
160
160
|
@click.option('-H', '--header', multiple=True, help='Pass custom header(s) to server')
|
161
161
|
@click.option('--lifecycle-hooks-path', help='Path to lifecycle hooks script.')
|
162
|
+
@click.option('-o', '--output', help='Write to file instead of stdout')
|
162
163
|
@ConditionalDecorator(lambda f: click.option('--project-key')(f), is_remote)
|
163
164
|
@click.option('--public-directory-path', help='Path to public files. Used for mocking requests.')
|
164
165
|
@click.option('--response-fixtures-path', help='Path to response fixtures yaml. Used for mocking requests.')
|
@@ -169,16 +170,7 @@ def mock(**kwargs):
|
|
169
170
|
if kwargs.get('remote_project_key'):
|
170
171
|
validate_project_key(kwargs['remote_project_key'])
|
171
172
|
|
172
|
-
|
173
|
-
validate_scenario_key(kwargs['scenario_key'])
|
174
|
-
|
175
|
-
request = __build_request_from_curl(**kwargs)
|
176
|
-
|
177
|
-
context = ReplayContext.from_python_request(request)
|
178
|
-
response: requests.Response = replay_request(context, {
|
179
|
-
**kwargs,
|
180
|
-
'mode': mode.MOCK,
|
181
|
-
})
|
173
|
+
response = __replay(mode.MOCK, **kwargs)
|
182
174
|
|
183
175
|
if response.status_code == custom_response_codes.NOT_FOUND:
|
184
176
|
content = response.content
|
@@ -186,10 +178,15 @@ def mock(**kwargs):
|
|
186
178
|
sys.exit(1)
|
187
179
|
|
188
180
|
if kwargs['format'] == RAW_FORMAT:
|
189
|
-
print_raw_response(response)
|
181
|
+
print_raw_response(response, kwargs['output'])
|
190
182
|
else:
|
191
183
|
content = response.content
|
192
|
-
|
184
|
+
|
185
|
+
if not kwargs['output']:
|
186
|
+
print(decode(content), end='')
|
187
|
+
else:
|
188
|
+
with open(kwargs['output'], 'w') as fp:
|
189
|
+
fp.write(decode(content))
|
193
190
|
|
194
191
|
@main.command(
|
195
192
|
help="Record request"
|
@@ -197,30 +194,28 @@ def mock(**kwargs):
|
|
197
194
|
@click.option('-d', '--data', default='', help='HTTP POST data')
|
198
195
|
@click.option('--format', type=click.Choice([RAW_FORMAT]), help='Format response')
|
199
196
|
@click.option('-H', '--header', multiple=True, help='Pass custom header(s) to server')
|
197
|
+
@click.option('--lifecycle-hooks-path', help='Path to lifecycle hooks script.')
|
198
|
+
@click.option('-o', '--output', help='Write to file instead of stdout')
|
200
199
|
@ConditionalDecorator(lambda f: click.option('--project-key')(f), is_remote)
|
201
200
|
@click.option('-X', '--request', default='GET', help='Specify request command to use')
|
202
201
|
@click.option('--scenario-key')
|
203
202
|
@click.argument('url')
|
204
203
|
def record(**kwargs):
|
205
|
-
|
206
|
-
validate_scenario_key(kwargs['scenario_key'])
|
207
|
-
|
208
|
-
request = __build_request_from_curl(**kwargs)
|
209
|
-
|
210
|
-
context = ReplayContext.from_python_request(request)
|
211
|
-
response: requests.Response = replay_request(context, {
|
212
|
-
**kwargs,
|
213
|
-
'mode': mode.RECORD,
|
214
|
-
})
|
204
|
+
response = __replay(mode.RECORD, **kwargs)
|
215
205
|
|
216
206
|
if kwargs['format'] == RAW_FORMAT:
|
217
|
-
print_raw_response(response)
|
207
|
+
print_raw_response(response, kwargs['output'])
|
218
208
|
else:
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
209
|
+
content: bytes = response.raw.data
|
210
|
+
|
211
|
+
if not kwargs['output']:
|
212
|
+
try:
|
213
|
+
print(content.decode(json.detect_encoding(content)), end='')
|
214
|
+
except UnicodeDecodeError:
|
215
|
+
print('Warning: Binary output can mess up your terminal.')
|
216
|
+
else:
|
217
|
+
with open(kwargs['output'], 'w') as fp:
|
218
|
+
fp.write(content.decode(json.detect_encoding(content)))
|
224
219
|
|
225
220
|
def __build_request_from_curl(**kwargs):
|
226
221
|
headers = {}
|
@@ -238,3 +233,15 @@ def __build_request_from_curl(**kwargs):
|
|
238
233
|
method=kwargs['request'],
|
239
234
|
url=kwargs['url']
|
240
235
|
)
|
236
|
+
|
237
|
+
def __replay(mode, **kwargs):
|
238
|
+
if kwargs.get('scenario_key'):
|
239
|
+
validate_scenario_key(kwargs['scenario_key'])
|
240
|
+
|
241
|
+
request = __build_request_from_curl(**kwargs)
|
242
|
+
|
243
|
+
context = ReplayContext.from_python_request(request)
|
244
|
+
return replay_request(context, {
|
245
|
+
**kwargs,
|
246
|
+
'mode': mode,
|
247
|
+
})
|
@@ -19,6 +19,7 @@ RESPONSE_ID = 'X-Stoobly-Response-Id'
|
|
19
19
|
RESPONSE_LATENCY = 'X-Stoobly-Request-Response-Latency'
|
20
20
|
RESPONSE_PROXY_MODE = 'X-Stoobly-Response-Proxy-Mode'
|
21
21
|
SCENARIO_KEY = 'X-Stoobly-Scenario-Key'
|
22
|
+
SESSION_ID = 'X-Stoobly-Session-Id'
|
22
23
|
SERVICE_URL = 'X-Stoobly-Service-Url'
|
23
24
|
TEST_FILTER = 'X-Stoobly-Test-Filter'
|
24
25
|
TEST_ID = 'X-Stoobly-Test-Id'
|