stoobly-agent 1.4.1__py3-none-any.whl → 1.5.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.
Files changed (65) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/cli/helpers/handle_mock_service.py +6 -2
  3. stoobly_agent/app/cli/helpers/request_facade.py +5 -1
  4. stoobly_agent/app/cli/scaffold/constants.py +1 -1
  5. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +3 -2
  6. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +2 -2
  7. stoobly_agent/app/cli/scaffold/service_config.py +16 -2
  8. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +19 -19
  9. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  10. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +12 -5
  11. stoobly_agent/app/cli/scaffold/templates/constants.py +3 -3
  12. stoobly_agent/app/cli/scaffold/templates/factory.py +5 -5
  13. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +1 -8
  14. stoobly_agent/app/cli/scaffold/templates/workflow/mock/fixtures.yml +1 -1
  15. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +1 -8
  16. stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures.yml +1 -1
  17. stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
  18. stoobly_agent/app/cli/scaffold/workflow_create_command.py +2 -2
  19. stoobly_agent/app/cli/scaffold_cli.py +65 -50
  20. stoobly_agent/app/proxy/context.py +4 -0
  21. stoobly_agent/app/proxy/handle_mock_service.py +81 -54
  22. stoobly_agent/app/proxy/handle_record_service.py +15 -3
  23. stoobly_agent/app/proxy/handle_replay_service.py +44 -18
  24. stoobly_agent/app/proxy/handle_test_service.py +75 -16
  25. stoobly_agent/app/proxy/intercept_handler.py +11 -16
  26. stoobly_agent/app/proxy/intercept_settings.py +17 -4
  27. stoobly_agent/app/proxy/mitmproxy/request_facade.py +5 -2
  28. stoobly_agent/app/proxy/mitmproxy/response_facade.py +5 -4
  29. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +78 -14
  30. stoobly_agent/app/proxy/mock/eval_request_service.py +2 -2
  31. stoobly_agent/app/proxy/record/join_request_service.py +7 -8
  32. stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
  33. stoobly_agent/app/proxy/replay/replay_request_service.py +4 -4
  34. stoobly_agent/app/proxy/test/helpers/upload_test_service.py +2 -2
  35. stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -3
  36. stoobly_agent/app/proxy/utils/response_handler.py +0 -2
  37. stoobly_agent/app/proxy/utils/rewrite.py +72 -0
  38. stoobly_agent/app/settings/constants/request_component.py +4 -1
  39. stoobly_agent/cli.py +35 -28
  40. stoobly_agent/config/constants/intercept_policy.py +2 -0
  41. stoobly_agent/config/constants/mock_policy.py +4 -2
  42. stoobly_agent/config/constants/record_policy.py +4 -2
  43. stoobly_agent/config/constants/replay_policy.py +4 -2
  44. stoobly_agent/public/{18-es2015.583f191cc7ad512ee262.js → 18-es2015.503207073756a9c8211a.js} +1 -1
  45. stoobly_agent/public/{18-es5.583f191cc7ad512ee262.js → 18-es5.503207073756a9c8211a.js} +1 -1
  46. stoobly_agent/public/index.html +1 -1
  47. stoobly_agent/public/{main-es2015.2cc16523aa3fcaba51e5.js → main-es2015.d682619f3d6d53d64c6a.js} +1 -1
  48. stoobly_agent/public/{main-es5.2cc16523aa3fcaba51e5.js → main-es5.d682619f3d6d53d64c6a.js} +1 -1
  49. stoobly_agent/public/{runtime-es2015.b914470164e4d6e75d96.js → runtime-es2015.8c1efed946fc02c923fc.js} +1 -1
  50. stoobly_agent/public/{runtime-es5.b914470164e4d6e75d96.js → runtime-es5.8c1efed946fc02c923fc.js} +1 -1
  51. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_test.py +2 -1
  52. stoobly_agent/test/app/cli/scaffold/e2e_test.py +2 -2
  53. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  54. stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +140 -71
  55. stoobly_agent/test/cli/lifecycle_hooks_test.py +66 -0
  56. stoobly_agent/test/cli/mock_test.py +53 -29
  57. stoobly_agent/test/cli/record_test.py +67 -0
  58. stoobly_agent/test/mock_data/lifecycle_hooks.py +35 -0
  59. {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/LICENSE +1 -1
  60. {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/METADATA +7 -12
  61. {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/RECORD +65 -61
  62. /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{fixtures/.keep → public/.gitignore} +0 -0
  63. /stoobly_agent/app/cli/scaffold/templates/workflow/test/{fixtures/.keep → public/.gitignore} +0 -0
  64. {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/WHEEL +0 -0
  65. {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/entry_points.txt +0 -0
@@ -19,6 +19,7 @@ from .mock.eval_request_service import inject_eval_request
19
19
  from .utils.allowed_request_service import get_active_mode_policy
20
20
  from .utils.request_handler import reverse_proxy
21
21
  from .utils.response_handler import bad_request, pass_on
22
+ from .utils.rewrite import rewrite_request, rewrite_response
22
23
 
23
24
  LOG_ID = 'Mock'
24
25
 
@@ -26,77 +27,77 @@ class MockOptions(TypedDict):
26
27
  failure: Callable
27
28
  ignored_components: list
28
29
  infer: bool
30
+ no_rewrite: bool
29
31
  success: Callable
30
32
 
33
+ def handle_request_mock_generic_without_rewrite(context: MockContext, **options: MockOptions):
34
+ options['no_rewrite'] = True
35
+ handle_request_mock_generic(context, **options)
36
+
31
37
  ###
32
38
  #
39
+ # 1. Rewrites mock request by default
40
+ # 2. BEFORE_MOCK gets triggered
41
+ # 3. AFTER_MOCK gets triggered
42
+ #
33
43
  # @param request [mitmproxy.http.Request]
34
44
  # @param settings [Dict]
35
45
  #
36
46
  def handle_request_mock_generic(context: MockContext, **options: MockOptions):
37
- __mock_hook(lifecycle_hooks.BEFORE_MOCK, context)
38
-
39
47
  intercept_settings = context.intercept_settings
40
48
  request: MitmproxyRequest = context.flow.request
41
- request_model = RequestModel(intercept_settings.settings)
42
-
43
- policy = get_active_mode_policy(request, intercept_settings)
44
-
45
- rewrite_rules = intercept_settings.mock_rewrite_rules
46
- if len(rewrite_rules) > 0:
47
- # Rewrite request with paramter rules for mock
48
- request: MitmproxyRequest = context.flow.request
49
- request_facade = MitmproxyRequestFacade(request)
50
- request_facade.with_parameter_rules(rewrite_rules).with_url_rules(rewrite_rules).rewrite()
51
-
52
- # If ignore rules are set, then ignore specified request parameters
53
- ignore_rules = intercept_settings.ignore_rules
54
- if len(ignore_rules) > 0:
55
- request_facade = MitmproxyRequestFacade(request)
56
- _ignore_rules = request_facade.select_parameter_rules(ignore_rules)
57
- ignored_components = rewrite_rules_to_ignored_components(_ignore_rules)
58
- options['ignored_components'] += ignored_components if 'ignored_components' in options else ignored_components
59
-
60
49
  handle_success = options['success'] if 'success' in options and callable(options['success']) else None
61
50
  handle_failure = options['failure'] if 'failure' in options and callable(options['failure']) else None
62
-
63
- eval_request = inject_eval_request(request_model, intercept_settings)
64
-
51
+
52
+ policy = get_active_mode_policy(request, intercept_settings)
65
53
  if policy == mock_policy.NONE:
66
54
  if handle_failure:
67
55
  res = handle_failure(context)
68
- elif policy == mock_policy.ALL:
69
- res = eval_request_with_retry(context, eval_request, **options)
70
-
71
- context.with_response(res)
72
-
73
- if handle_success:
74
- # TODO: rewrite response, see #332
75
- res = handle_success(context) or res
76
- elif policy == mock_policy.FOUND:
77
- res = eval_request_with_retry(context, eval_request, **options)
78
-
79
- context.with_response(res)
80
-
81
- if res.status_code in [custom_response_codes.NOT_FOUND, custom_response_codes.IGNORE_COMPONENTS]:
82
- if handle_failure:
83
- try:
84
- res = handle_failure(context)
85
- except RuntimeError:
86
- # Do nothing, return custom error response
87
- pass
88
- else:
89
- if handle_success:
90
- # TODO: rewrite response, see #332
91
- res = handle_success(context) or res
92
56
  else:
93
- return bad_request(
94
- context.flow,
95
- "Valid env MOCK_POLICY: %s, Got: %s" %
96
- ([mock_policy.ALL, mock_policy.FOUND, mock_policy.NONE], policy)
97
- )
57
+ if not options.get('no_rewrite'):
58
+ __rewrite_request(context)
59
+
60
+ __mock_hook(lifecycle_hooks.BEFORE_MOCK, context)
61
+
62
+ # If ignore rules are set, then ignore specified request parameters
63
+ ignore_rules = intercept_settings.ignore_rules
64
+ if len(ignore_rules) > 0:
65
+ request_facade = MitmproxyRequestFacade(request)
66
+ _ignore_rules = request_facade.select_parameter_rules(ignore_rules)
67
+ ignored_components = rewrite_rules_to_ignored_components(_ignore_rules)
68
+ options['ignored_components'] += ignored_components if 'ignored_components' in options else ignored_components
69
+
70
+ request_model = RequestModel(intercept_settings.settings)
71
+ eval_request = inject_eval_request(request_model, intercept_settings)
72
+
73
+ if policy == mock_policy.ALL:
74
+ res = eval_request_with_retry(context, eval_request, **options)
75
+
76
+ context.with_response(res)
98
77
 
99
- __mock_hook(lifecycle_hooks.AFTER_MOCK, context)
78
+ if handle_success:
79
+ res = handle_success(context) or res
80
+ elif policy == mock_policy.FOUND:
81
+ res = eval_request_with_retry(context, eval_request, **options)
82
+
83
+ context.with_response(res)
84
+
85
+ if res.status_code in [custom_response_codes.NOT_FOUND, custom_response_codes.IGNORE_COMPONENTS]:
86
+ if handle_failure:
87
+ try:
88
+ res = handle_failure(context)
89
+ except RuntimeError:
90
+ # Do nothing, return custom error response
91
+ pass
92
+ else:
93
+ if handle_success:
94
+ res = handle_success(context) or res
95
+ else:
96
+ return bad_request(
97
+ context.flow,
98
+ "Valid env MOCK_POLICY: %s, Got: %s" %
99
+ ([mock_policy.ALL, mock_policy.FOUND, mock_policy.NONE], policy)
100
+ )
100
101
 
101
102
  return pass_on(context.flow, res)
102
103
 
@@ -130,6 +131,11 @@ def handle_request_mock(context: MockContext):
130
131
  success=lambda context: __handle_mock_success(context)
131
132
  )
132
133
 
134
+ ###
135
+ #
136
+ # 1. Rewrites mock response (if mock found)
137
+ # 2. AFTER_MOCK gets triggered (if mock found)
138
+ #
133
139
  def handle_response_mock(context: MockContext):
134
140
  response = context.flow.response
135
141
  request_key = response.headers.get(custom_headers.MOCK_REQUEST_KEY)
@@ -138,6 +144,9 @@ def handle_response_mock(context: MockContext):
138
144
  request = context.flow.request
139
145
  Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Mocked{bcolors.ENDC} {request.url} -> {request_key}")
140
146
 
147
+ __rewrite_response(context)
148
+ __mock_hook(lifecycle_hooks.AFTER_MOCK, context)
149
+
141
150
  def __handle_mock_success(context: MockContext) -> None:
142
151
  if os.environ.get(env_vars.AGENT_SIMULATE_LATENCY):
143
152
  response = context.response
@@ -160,6 +169,24 @@ def __handle_mock_failure(context: MockContext) -> None:
160
169
 
161
170
  reverse_proxy(req, upstream_url, {})
162
171
 
172
+ def __rewrite_request(context: MockContext):
173
+ # Rewrite request with paramter rules for mock
174
+
175
+ intercept_settings = context.intercept_settings
176
+ rewrite_rules = intercept_settings.mock_rewrite_rules
177
+
178
+ if len(rewrite_rules) > 0:
179
+ rewrite_request(context.flow, rewrite_rules)
180
+
181
+ def __rewrite_response(context: MockContext):
182
+ # Rewrite request with paramter rules for mock
183
+
184
+ intercept_settings = context.intercept_settings
185
+ rewrite_rules = intercept_settings.mock_rewrite_rules
186
+
187
+ if len(rewrite_rules) > 0:
188
+ rewrite_response(context.flow, rewrite_rules)
189
+
163
190
  ###
164
191
  #
165
192
  # Try to simulate expected response latency
@@ -2,6 +2,7 @@ import os
2
2
  import pdb
3
3
  import threading
4
4
 
5
+ from copy import deepcopy
5
6
  from mitmproxy.http import Request as MitmproxyRequest
6
7
 
7
8
  from stoobly_agent.app.settings.constants.mode import TEST
@@ -18,15 +19,20 @@ from .record.overwrite_scenario_service import overwrite_scenario
18
19
  from .record.upload_request_service import inject_upload_request
19
20
  from .utils.allowed_request_service import get_active_mode_policy
20
21
  from .utils.response_handler import bad_request, disable_transfer_encoding
22
+ from .utils.rewrite import rewrite_request_response
21
23
 
22
24
  LOG_ID = 'Record'
23
25
 
26
+ ###
27
+ #
28
+ # 1. Rewrites a copy of request and response
29
+ # 2. BEFORE_RECORD gets triggered
30
+ # 3. AFTER_RECORD gets triggered
31
+ #
24
32
  def handle_response_record(context: RecordContext):
25
33
  flow = context.flow
26
34
  disable_transfer_encoding(flow.response)
27
35
 
28
- __record_hook(lifecycle_hooks.BEFORE_RECORD, context)
29
-
30
36
  intercept_settings = context.intercept_settings
31
37
  request: MitmproxyRequest = flow.request
32
38
  request_model = RequestModel(intercept_settings.settings)
@@ -60,11 +66,17 @@ def handle_response_record(context: RecordContext):
60
66
 
61
67
  def __record_handler(context: RecordContext, request_model: RequestModel):
62
68
  flow = context.flow
69
+ flow_copy = deepcopy(flow)
63
70
  intercept_settings = context.intercept_settings
64
71
 
65
- inject_upload_request(request_model, intercept_settings)(flow)
72
+ context.flow = flow_copy # Deep copy flow to prevent response modifications from persisting
73
+ rewrite_request_response(flow_copy, intercept_settings.record_rewrite_rules)
74
+ __record_hook(lifecycle_hooks.BEFORE_RECORD, context)
75
+
76
+ inject_upload_request(request_model, intercept_settings)(flow_copy)
66
77
 
67
78
  __record_hook(lifecycle_hooks.AFTER_RECORD, context)
79
+ context.flow = flow # Reset flow
68
80
 
69
81
  def __record_request(context: RecordContext, request_model: RequestModel):
70
82
  if os.environ.get(ENV) == TEST:
@@ -1,46 +1,72 @@
1
1
  import pdb
2
2
 
3
3
  from mitmproxy.http import Request as MitmproxyRequest
4
+ from typing import TypedDict
4
5
 
5
6
  from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
6
- from stoobly_agent.app.proxy.mitmproxy.request_facade import MitmproxyRequestFacade
7
7
  from stoobly_agent.app.proxy.replay.context import ReplayContext
8
8
  from stoobly_agent.config.constants import lifecycle_hooks, replay_policy
9
9
 
10
10
  from .utils.allowed_request_service import get_active_mode_policy
11
+ from .utils.rewrite import rewrite_request, rewrite_response
11
12
 
12
13
  LOG_ID = 'HandleReplay'
13
14
 
14
- def handle_request_replay(replay_context: ReplayContext):
15
- __replay_hook(lifecycle_hooks.BEFORE_REPLAY, replay_context)
15
+ class ReplayOptions(TypedDict):
16
+ no_rewrite: bool
16
17
 
17
- request: MitmproxyRequest = replay_context.flow.request
18
- intercept_settings: InterceptSettings = replay_context.intercept_settings
18
+ ###
19
+ #
20
+ # 1. Rewrites replay request by default
21
+ # 2. BEFORE_REPLAY gets triggered
22
+ #
23
+ def handle_request_replay_without_rewrite(replay_context: ReplayContext):
24
+ options = { 'no_rewrite': True }
25
+ handle_request_replay(replay_context, **options)
19
26
 
20
- policy = get_active_mode_policy(request, intercept_settings)
21
- if policy != replay_policy.NONE:
22
- __replay_request(replay_context)
27
+ def handle_request_replay(replay_context: ReplayContext, **options: ReplayOptions):
28
+ request: MitmproxyRequest = replay_context.flow.request
29
+ intercept_settings: InterceptSettings = replay_context.intercept_settings
30
+
31
+ policy = get_active_mode_policy(request, intercept_settings)
32
+ if policy != replay_policy.NONE:
33
+ if not options.get('no_rewrite'):
34
+ __rewrite_request(replay_context)
23
35
 
36
+ __replay_hook(lifecycle_hooks.BEFORE_REPLAY, replay_context)
37
+
38
+ ###
39
+ #
40
+ # 1. Rewrites replay response
41
+ # 2. AFTER_REPLAY gets triggered
42
+ #
24
43
  def handle_response_replay(replay_context: ReplayContext):
44
+ __rewrite_response(replay_context)
25
45
  __replay_hook(lifecycle_hooks.AFTER_REPLAY, replay_context)
26
46
 
27
- # TODO: rewrite response, see #332
47
+ def __replay_hook(hook: str, replay_context: ReplayContext):
48
+ intercept_settings: InterceptSettings = replay_context.intercept_settings
49
+
50
+ lifecycle_hooks_module = intercept_settings.lifecycle_hooks
51
+ if hook in lifecycle_hooks_module:
52
+ lifecycle_hooks_module[hook](replay_context)
28
53
 
29
- def __replay_request(replay_context: ReplayContext):
54
+ def __rewrite_request(replay_context: ReplayContext):
30
55
  """
31
56
  Before replaying a request, see if the request needs to be rewritten
32
57
  """
33
58
  intercept_settings: InterceptSettings = replay_context.intercept_settings
34
- rewrite_rules = intercept_settings.rewrite_rules
59
+ rewrite_rules = intercept_settings.replay_rewrite_rules
35
60
 
36
61
  if len(rewrite_rules) > 0:
37
- request: MitmproxyRequest = replay_context.flow.request
38
- request_facade = MitmproxyRequestFacade(request)
39
- request_facade.with_parameter_rules(rewrite_rules).with_url_rules(rewrite_rules).rewrite()
62
+ rewrite_request(replay_context.flow, rewrite_rules)
40
63
 
41
- def __replay_hook(hook: str, replay_context: ReplayContext):
64
+ def __rewrite_response(replay_context: ReplayContext):
65
+ """
66
+ After replaying a request, see if the request needs to be rewritten
67
+ """
42
68
  intercept_settings: InterceptSettings = replay_context.intercept_settings
69
+ rewrite_rules = intercept_settings.replay_rewrite_rules
43
70
 
44
- lifecycle_hooks_module = intercept_settings.lifecycle_hooks
45
- if hook in lifecycle_hooks_module:
46
- lifecycle_hooks_module[hook](replay_context)
71
+ if len(rewrite_rules) > 0:
72
+ rewrite_response(replay_context.flow, rewrite_rules)
@@ -1,7 +1,9 @@
1
1
  import pdb
2
2
 
3
+ from copy import deepcopy
3
4
  from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
4
5
 
6
+ from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
5
7
  from stoobly_agent.app.proxy.replay.body_parser_service import encode_response
6
8
  from stoobly_agent.app.proxy.replay.context import ReplayContext
7
9
  from stoobly_agent.app.proxy.utils.request_handler import build_response
@@ -12,35 +14,53 @@ from stoobly_agent.lib.api.endpoints_resource import EndpointsResource
12
14
  from stoobly_agent.lib.api.interfaces.tests import TestShowResponse
13
15
  from stoobly_agent.lib.logger import Logger
14
16
 
15
- from .handle_mock_service import handle_request_mock_generic
16
- from .handle_replay_service import handle_request_replay
17
+ from .handle_mock_service import handle_request_mock_generic_without_rewrite
18
+ from .handle_replay_service import handle_request_replay_without_rewrite
17
19
  from .mock.context import MockContext
18
20
  from .test.helpers.test_results_builder import TestResultsBuilder
19
21
  from .test.helpers.upload_test_service import inject_upload_test
20
22
  from .test.context_abc import TestContextABC as TestContext
21
23
  from .test.test_service import test
24
+ from .utils.rewrite import rewrite_request, rewrite_response, rewrite_request_response
22
25
 
23
26
  LOG_ID = 'HandleTest'
24
27
 
28
+ ###
29
+ #
30
+ # 1. Rewrites test request
31
+ # 2. BEFORE_REPLAY gets triggered
32
+ #
25
33
  def handle_request_test(context: ReplayContext) -> None:
26
- handle_request_replay(context)
34
+ __rewrite_request(context)
35
+ handle_request_replay_without_rewrite(context)
27
36
 
28
37
  ###
29
38
  #
30
- # Mock and Test modes share the same policies
39
+ # 1. Rewrites test response (response from live service)
40
+ # 2. AFTER_REPLAY gets triggered
41
+ # 3. Uses rewritten test request to obtain mock response
42
+ # 4. BEFORE_MOCK gets triggered
43
+ # 5. AFTER_MOCK gets triggered
44
+ # 6. BEFORE_TEST gets triggered
45
+ # 7. Tests against rewritten test response and mock response (expected response)
46
+ # 8. Rewrites a copy of request and response
47
+ # 9. BEFORE_RECORD gets triggered (if not from CLI)
48
+ # 10. AFTER_RECORD gets triggered (if not from CLI)
49
+ # 11. AFTER_TEST gets triggered
31
50
  #
32
51
  def handle_response_test(context: ReplayContext) -> None:
33
52
  from .test.context import TestContext
34
53
 
35
54
  flow: MitmproxyHTTPFlow = context.flow
36
- intercept_settings = context.intercept_settings
37
-
38
55
  disable_transfer_encoding(flow.response)
39
56
 
40
- # At this point, the request may already been rewritten during replay
41
- # Request will be rewritten again for mocking purposes
57
+ __rewrite_response(context)
58
+ __test_hook(lifecycle_hooks.AFTER_REPLAY, context)
42
59
 
43
- handle_request_mock_generic(
60
+ intercept_settings = context.intercept_settings
61
+
62
+ # At this point, the request may already been rewritten during replay, do not rewrite again
63
+ handle_request_mock_generic_without_rewrite(
44
64
  MockContext(flow, intercept_settings),
45
65
  failure=lambda mock_context: __handle_mock_failure(TestContext(context, mock_context)),
46
66
  #infer=intercept_settings.test_strategy == test_strategy.FUZZY, # For fuzzy testing we can use an inferred response
@@ -53,8 +73,10 @@ def __decorate_test_id(flow: MitmproxyHTTPFlow, test_response: TestShowResponse)
53
73
 
54
74
  def __handle_mock_success(test_context: TestContext) -> None:
55
75
  flow: MitmproxyHTTPFlow = test_context.flow
56
- settings = Settings.instance()
57
76
 
77
+ __test_hook(lifecycle_hooks.AFTER_MOCK, test_context.mock_context)
78
+
79
+ settings: Settings = Settings.instance()
58
80
  test_context.with_endpoints_resource(EndpointsResource(settings.remote.api_url, settings.remote.api_key))
59
81
 
60
82
  __test_hook(lifecycle_hooks.BEFORE_TEST, test_context)
@@ -84,13 +106,8 @@ def __handle_mock_success(test_context: TestContext) -> None:
84
106
  if not is_cli or test_context.save:
85
107
  # Re-serialize expected response since it was rewritten
86
108
  upload_test_data['expected_response'] = encode_response(expected, test_context.expected_response.content_type)
87
- upload_test = inject_upload_test(None, intercept_settings)
88
109
 
89
- # Commit test to API
90
- res = upload_test(
91
- flow,
92
- **upload_test_data
93
- )
110
+ res = __record_handler(test_context, upload_test_data)
94
111
 
95
112
  if is_cli:
96
113
  # If the origin was from a CLI, send test ID in response header
@@ -136,6 +153,48 @@ def __override_response(flow: MitmproxyHTTPFlow, content: bytes):
136
153
  flow.response.set_content(content)
137
154
  flow.response.status_code = 200
138
155
 
156
+ def __record_handler(context: TestContext, upload_test_data):
157
+ flow = context.flow
158
+ flow_copy = deepcopy(flow) # Deep copy flow to prevent response modifications from persisting
159
+ intercept_settings = context.intercept_settings
160
+
161
+ context.flow = flow_copy
162
+
163
+ # Since we are "uploading" the request, use record_write_rules
164
+ rewrite_request_response(flow_copy, intercept_settings.record_rewrite_rules)
165
+ __test_hook(lifecycle_hooks.BEFORE_RECORD, context)
166
+
167
+ # Commit test to API
168
+ upload_test = inject_upload_test(None, intercept_settings)
169
+ res = upload_test(
170
+ flow_copy, **upload_test_data
171
+ )
172
+
173
+ __test_hook(lifecycle_hooks.AFTER_RECORD, context)
174
+ context.flow = flow
175
+
176
+ return res
177
+
178
+ def __rewrite_request(replay_context: ReplayContext):
179
+ """
180
+ Before replaying a request, see if the request needs to be rewritten
181
+ """
182
+ intercept_settings: InterceptSettings = replay_context.intercept_settings
183
+ rewrite_rules = intercept_settings.test_rewrite_rules
184
+
185
+ if len(rewrite_rules) > 0:
186
+ rewrite_request(replay_context.flow, rewrite_rules)
187
+
188
+ def __rewrite_response(replay_context: ReplayContext):
189
+ """
190
+ After replaying a request, see if the request needs to be rewritten
191
+ """
192
+ intercept_settings: InterceptSettings = replay_context.intercept_settings
193
+ rewrite_rules = intercept_settings.test_rewrite_rules
194
+
195
+ if len(rewrite_rules) > 0:
196
+ rewrite_response(replay_context.flow, rewrite_rules)
197
+
139
198
  def __test_hook(hook: str, context: TestContext):
140
199
  intercept_settings = context.intercept_settings
141
200
  lifecycle_hooks_module = intercept_settings.lifecycle_hooks
@@ -33,6 +33,7 @@ def request(flow: MitmproxyHTTPFlow):
33
33
  if not intercept_settings.active:
34
34
  return
35
35
 
36
+ __disable_web_cache(request)
36
37
  __intercept_hook(lifecycle_hooks.BEFORE_REQUEST, flow, intercept_settings)
37
38
 
38
39
  active_mode = intercept_settings.mode
@@ -42,7 +43,7 @@ def request(flow: MitmproxyHTTPFlow):
42
43
  context = MockContext(flow, intercept_settings)
43
44
  handle_request_mock(context)
44
45
  elif active_mode == mode.RECORD:
45
- __disable_web_cache(request)
46
+ pass
46
47
  elif active_mode == mode.REPLAY:
47
48
  context = ReplayContext(flow, intercept_settings)
48
49
  handle_request_replay(context)
@@ -65,35 +66,29 @@ def response(flow: MitmproxyHTTPFlow):
65
66
  if not intercept_settings.active:
66
67
  return
67
68
 
68
- __intercept_hook(lifecycle_hooks.BEFORE_RESPONSE, flow, intercept_settings)
69
-
70
69
  active_mode = intercept_settings.mode
71
70
 
72
71
  if active_mode == mode.MOCK:
73
72
  context = MockContext(flow, intercept_settings)
74
- return handle_response_mock(context)
73
+ handle_response_mock(context)
75
74
  elif active_mode == mode.RECORD:
76
75
  context = RecordContext(flow, intercept_settings)
77
- return handle_response_record(context)
76
+ handle_response_record(context)
78
77
  elif active_mode == mode.REPLAY:
79
78
  context = ReplayContext(flow, intercept_settings)
80
- return handle_response_replay(context)
79
+ handle_response_replay(context)
81
80
  elif active_mode == mode.TEST:
82
81
  context = ReplayContext(flow, intercept_settings)
83
- return handle_response_test(context)
82
+ handle_response_test(context)
83
+
84
+ __intercept_hook(lifecycle_hooks.BEFORE_RESPONSE, flow, intercept_settings)
84
85
 
85
86
  ### PRIVATE
86
87
 
88
+ # Prevent 304 status
89
+ # Because this header will get recorded, should add during mocking as well in the case where headers are used for matching
87
90
  def __disable_web_cache(request: MitmproxyRequest) -> None:
88
- request.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
89
- request.headers['Expires'] = '0'
90
- request.headers['Pragma'] = 'no-cache'
91
-
92
- if 'IF-NONE-MATCH' in request.headers:
93
- del request.headers['IF-NONE-MATCH']
94
-
95
- if 'IF-MODIFIED-SINCE' in request.headers:
96
- del request.headers['IF-MODIFIED-SINCE']
91
+ request.headers['Cache-Control'] = 'no-cache, no-store'
97
92
 
98
93
  # Fix issue where multi-value cookies become comma separated
99
94
  def __patch_cookie(request: MitmproxyRequest):
@@ -41,6 +41,11 @@ class InterceptSettings:
41
41
  self.__response_fixtures = None
42
42
  self.__initialize_response_fixtures()
43
43
 
44
+ self._mock_rewrite_rules = None
45
+ self._record_rewrite_rules = None
46
+ self._replay_rewrite_rules = None
47
+ self._test_rewrite_rules = None
48
+
44
49
  @property
45
50
  def settings(self):
46
51
  return self.__settings
@@ -200,19 +205,27 @@ class InterceptSettings:
200
205
 
201
206
  @property
202
207
  def record_rewrite_rules(self) -> List[RewriteRule]:
203
- return self.__select_rewrite_rules(mode.RECORD)
208
+ if not self._record_rewrite_rules:
209
+ self._record_rewrite_rules = self.__select_rewrite_rules(mode.RECORD)
210
+ return self._record_rewrite_rules
204
211
 
205
212
  @property
206
213
  def mock_rewrite_rules(self) -> List[RewriteRule]:
207
- return self.__select_rewrite_rules(mode.MOCK)
214
+ if not self._mock_rewrite_rules:
215
+ self._mock_rewrite_rules = self.__select_rewrite_rules(mode.MOCK)
216
+ return self._mock_rewrite_rules
208
217
 
209
218
  @property
210
219
  def replay_rewrite_rules(self) -> List[RewriteRule]:
211
- return self.__select_rewrite_rules(mode.REPLAY)
220
+ if not self._replay_rewrite_rules:
221
+ self._replay_rewrite_rules = self.__select_rewrite_rules(mode.REPLAY)
222
+ return self._replay_rewrite_rules
212
223
 
213
224
  @property
214
225
  def test_rewrite_rules(self) -> List[RewriteRule]:
215
- return self.__select_rewrite_rules(mode.TEST)
226
+ if not self._test_rewrite_rules:
227
+ self._test_rewrite_rules = self.__select_rewrite_rules(mode.TEST)
228
+ return self._test_rewrite_rules
216
229
 
217
230
  @property
218
231
  def upstream_url(self):
@@ -239,7 +239,7 @@ class MitmproxyRequestFacade(Request):
239
239
  content_type = self.content_type
240
240
  parsed_content = self.__body.get(content_type)
241
241
 
242
- if not isinstance(parsed_content, dict) and not isinstance(parsed_content, multidict.MultiDictView):
242
+ if not self.__is_iterable(parsed_content):
243
243
  content_type = 'application/json'
244
244
  self.request.headers['content-type'] = content_type
245
245
  parsed_content = {}
@@ -265,4 +265,7 @@ class MitmproxyRequestFacade(Request):
265
265
 
266
266
  _request_headers.pop(name)
267
267
 
268
- return _request_headers
268
+ return _request_headers
269
+
270
+ def __is_iterable(self, v):
271
+ return isinstance(v, dict) or isinstance(v, multidict.MultiDictView) or isinstance(v, list)
@@ -95,10 +95,8 @@ class MitmproxyResponseFacade(Response):
95
95
  content_type = self.content_type
96
96
  parsed_content = self.__body.get(content_type)
97
97
 
98
- if not isinstance(parsed_content, dict) and not isinstance(parsed_content, multidict.MultiDictView):
99
- content_type = 'application/json'
100
- self.response.headers['content-type'] = content_type
101
- parsed_content = {}
98
+ if not self.__is_iterable(parsed_content):
99
+ return
102
100
 
103
101
  self.__apply_rewrites(parsed_content, rewrites, handler)
104
102
  self.__body.set(parsed_content, content_type)
@@ -122,3 +120,6 @@ class MitmproxyResponseFacade(Response):
122
120
  _response_headers.pop(name)
123
121
 
124
122
  return _response_headers
123
+
124
+ def __is_iterable(self, v):
125
+ return isinstance(v, dict) or isinstance(v, multidict.MultiDictView) or isinstance(v, list)