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.
Files changed (72) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/cli/helpers/handle_config_update_service.py +2 -2
  3. stoobly_agent/app/cli/helpers/handle_mock_service.py +6 -2
  4. stoobly_agent/app/cli/helpers/request_facade.py +5 -1
  5. stoobly_agent/app/cli/scaffold/constants.py +1 -1
  6. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +1 -0
  7. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +19 -19
  8. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  9. stoobly_agent/app/cli/scaffold/templates/constants.py +3 -3
  10. stoobly_agent/app/cli/scaffold/templates/factory.py +5 -5
  11. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +1 -8
  12. stoobly_agent/app/cli/scaffold/templates/workflow/mock/fixtures.yml +1 -1
  13. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +1 -8
  14. stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures.yml +1 -1
  15. stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
  16. stoobly_agent/app/cli/scaffold/workflow_create_command.py +2 -2
  17. stoobly_agent/app/cli/scaffold_cli.py +5 -5
  18. stoobly_agent/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request.py +1 -1
  19. stoobly_agent/app/models/factories/resource/local_db/request_adapter.py +17 -11
  20. stoobly_agent/app/models/types/request.py +1 -2
  21. stoobly_agent/app/proxy/context.py +4 -0
  22. stoobly_agent/app/proxy/handle_mock_service.py +93 -46
  23. stoobly_agent/app/proxy/handle_record_service.py +15 -3
  24. stoobly_agent/app/proxy/handle_replay_service.py +44 -18
  25. stoobly_agent/app/proxy/handle_test_service.py +92 -24
  26. stoobly_agent/app/proxy/intercept_handler.py +11 -16
  27. stoobly_agent/app/proxy/intercept_settings.py +17 -4
  28. stoobly_agent/app/proxy/mitmproxy/request_facade.py +5 -2
  29. stoobly_agent/app/proxy/mitmproxy/response_facade.py +5 -4
  30. stoobly_agent/app/proxy/mock/custom_not_found_response_builder.py +5 -0
  31. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +79 -14
  32. stoobly_agent/app/proxy/mock/eval_request_service.py +18 -13
  33. stoobly_agent/app/proxy/record/join_request_service.py +7 -8
  34. stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
  35. stoobly_agent/app/proxy/replay/replay_request_service.py +4 -4
  36. stoobly_agent/app/proxy/test/helpers/upload_test_service.py +2 -2
  37. stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -3
  38. stoobly_agent/app/proxy/utils/response_handler.py +10 -1
  39. stoobly_agent/app/proxy/utils/rewrite.py +72 -0
  40. stoobly_agent/app/settings/constants/request_component.py +4 -1
  41. stoobly_agent/cli.py +35 -28
  42. stoobly_agent/config/constants/custom_headers.py +1 -0
  43. stoobly_agent/config/constants/intercept_policy.py +2 -0
  44. stoobly_agent/config/constants/mock_policy.py +4 -2
  45. stoobly_agent/config/constants/query_params.py +2 -0
  46. stoobly_agent/config/constants/record_policy.py +4 -2
  47. stoobly_agent/config/constants/replay_policy.py +4 -2
  48. stoobly_agent/public/{18-es2015.583f191cc7ad512ee262.js → 18-es2015.503207073756a9c8211a.js} +1 -1
  49. stoobly_agent/public/{18-es5.583f191cc7ad512ee262.js → 18-es5.503207073756a9c8211a.js} +1 -1
  50. stoobly_agent/public/index.html +1 -1
  51. stoobly_agent/public/{main-es2015.2cc16523aa3fcaba51e5.js → main-es2015.d682619f3d6d53d64c6a.js} +1 -1
  52. stoobly_agent/public/{main-es5.2cc16523aa3fcaba51e5.js → main-es5.d682619f3d6d53d64c6a.js} +1 -1
  53. stoobly_agent/public/{runtime-es2015.b914470164e4d6e75d96.js → runtime-es2015.8c1efed946fc02c923fc.js} +1 -1
  54. stoobly_agent/public/{runtime-es5.b914470164e4d6e75d96.js → runtime-es5.8c1efed946fc02c923fc.js} +1 -1
  55. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_test.py +2 -1
  56. stoobly_agent/test/app/cli/scaffold/e2e_test.py +2 -2
  57. stoobly_agent/test/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request_test.py +4 -4
  58. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  59. stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +140 -71
  60. stoobly_agent/test/cli/lifecycle_hooks_test.py +66 -0
  61. stoobly_agent/test/cli/mock_scenario_lifecycle_hooks.py +5 -0
  62. stoobly_agent/test/cli/mock_scenario_test.py +62 -0
  63. stoobly_agent/test/cli/mock_test.py +54 -38
  64. stoobly_agent/test/cli/record_test.py +67 -0
  65. stoobly_agent/test/mock_data/lifecycle_hooks.py +35 -0
  66. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/LICENSE +1 -1
  67. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/METADATA +7 -12
  68. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/RECORD +72 -65
  69. /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{fixtures/.keep → public/.gitignore} +0 -0
  70. /stoobly_agent/app/cli/scaffold/templates/workflow/test/{fixtures/.keep → public/.gitignore} +0 -0
  71. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/WHEEL +0 -0
  72. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/entry_points.txt +0 -0
@@ -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,36 +14,55 @@ 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),
65
+ error=lambda mock_context: __handle_mock_error(TestContext(context, mock_context)),
45
66
  failure=lambda mock_context: __handle_mock_failure(TestContext(context, mock_context)),
46
67
  #infer=intercept_settings.test_strategy == test_strategy.FUZZY, # For fuzzy testing we can use an inferred response
47
68
  success=lambda mock_context: __handle_mock_success(TestContext(context, mock_context))
@@ -51,10 +72,28 @@ def __decorate_test_id(flow: MitmproxyHTTPFlow, test_response: TestShowResponse)
51
72
  if test_response.get('id'):
52
73
  flow.response.headers[custom_headers.TEST_ID] = str(test_response['id'])
53
74
 
75
+ def __handle_mock_error(test_context: TestContext):
76
+ Logger.instance().warn(f"{LOG_ID}:TestStatus: Mock not enabled")
77
+
78
+ intercept_settings = test_context.intercept_settings
79
+
80
+ if intercept_settings.request_origin == request_origin.CLI:
81
+ return build_response(False, 'No test found')
82
+
83
+ def __handle_mock_failure(test_context: TestContext) -> None:
84
+ Logger.instance().warn(f"{LOG_ID}:TestStatus: No test found")
85
+
86
+ intercept_settings = test_context.intercept_settings
87
+
88
+ if intercept_settings.request_origin == request_origin.CLI:
89
+ return build_response(False, 'No test found')
90
+
54
91
  def __handle_mock_success(test_context: TestContext) -> None:
55
92
  flow: MitmproxyHTTPFlow = test_context.flow
56
- settings = Settings.instance()
57
93
 
94
+ __test_hook(lifecycle_hooks.AFTER_MOCK, test_context.mock_context)
95
+
96
+ settings: Settings = Settings.instance()
58
97
  test_context.with_endpoints_resource(EndpointsResource(settings.remote.api_url, settings.remote.api_key))
59
98
 
60
99
  __test_hook(lifecycle_hooks.BEFORE_TEST, test_context)
@@ -84,13 +123,8 @@ def __handle_mock_success(test_context: TestContext) -> None:
84
123
  if not is_cli or test_context.save:
85
124
  # Re-serialize expected response since it was rewritten
86
125
  upload_test_data['expected_response'] = encode_response(expected, test_context.expected_response.content_type)
87
- upload_test = inject_upload_test(None, intercept_settings)
88
126
 
89
- # Commit test to API
90
- res = upload_test(
91
- flow,
92
- **upload_test_data
93
- )
127
+ res = __record_handler(test_context, upload_test_data)
94
128
 
95
129
  if is_cli:
96
130
  # If the origin was from a CLI, send test ID in response header
@@ -121,14 +155,6 @@ def __handle_mock_success(test_context: TestContext) -> None:
121
155
 
122
156
  return flow.response
123
157
 
124
- def __handle_mock_failure(test_context: TestContext) -> None:
125
- Logger.instance().warn(f"{LOG_ID}:TestStatus: No test found")
126
-
127
- intercept_settings = test_context.intercept_settings
128
-
129
- if intercept_settings.request_origin == request_origin.CLI:
130
- return build_response(False, 'No test found')
131
-
132
158
  def __override_response(flow: MitmproxyHTTPFlow, content: bytes):
133
159
  headers = { 'Content-Type': 'text/plain' }
134
160
  headers[custom_headers.CONTENT_TYPE] = custom_headers.CONTENT_TYPE_TEST_RESULTS
@@ -136,6 +162,48 @@ def __override_response(flow: MitmproxyHTTPFlow, content: bytes):
136
162
  flow.response.set_content(content)
137
163
  flow.response.status_code = 200
138
164
 
165
+ def __record_handler(context: TestContext, upload_test_data):
166
+ flow = context.flow
167
+ flow_copy = deepcopy(flow) # Deep copy flow to prevent response modifications from persisting
168
+ intercept_settings = context.intercept_settings
169
+
170
+ context.flow = flow_copy
171
+
172
+ # Since we are "uploading" the request, use record_write_rules
173
+ rewrite_request_response(flow_copy, intercept_settings.record_rewrite_rules)
174
+ __test_hook(lifecycle_hooks.BEFORE_RECORD, context)
175
+
176
+ # Commit test to API
177
+ upload_test = inject_upload_test(None, intercept_settings)
178
+ res = upload_test(
179
+ flow_copy, **upload_test_data
180
+ )
181
+
182
+ __test_hook(lifecycle_hooks.AFTER_RECORD, context)
183
+ context.flow = flow
184
+
185
+ return res
186
+
187
+ def __rewrite_request(replay_context: ReplayContext):
188
+ """
189
+ Before replaying a request, see if the request needs to be rewritten
190
+ """
191
+ intercept_settings: InterceptSettings = replay_context.intercept_settings
192
+ rewrite_rules = intercept_settings.test_rewrite_rules
193
+
194
+ if len(rewrite_rules) > 0:
195
+ rewrite_request(replay_context.flow, rewrite_rules)
196
+
197
+ def __rewrite_response(replay_context: ReplayContext):
198
+ """
199
+ After replaying a request, see if the request needs to be rewritten
200
+ """
201
+ intercept_settings: InterceptSettings = replay_context.intercept_settings
202
+ rewrite_rules = intercept_settings.test_rewrite_rules
203
+
204
+ if len(rewrite_rules) > 0:
205
+ rewrite_response(replay_context.flow, rewrite_rules)
206
+
139
207
  def __test_hook(hook: str, context: TestContext):
140
208
  intercept_settings = context.intercept_settings
141
209
  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)
@@ -12,4 +12,9 @@ class CustomNotFoundResponseBuilder():
12
12
  def build(self):
13
13
  self.__response.status_code = custom_response_codes.NOT_FOUND
14
14
  self.__response.raw = BytesIO('Request not found'.encode())
15
+ self.__response.headers = {
16
+ 'Access-Control-Allow-Origin': '*',
17
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS, POST, PATCH, PUT, DELETE',
18
+ 'Content-Type': 'text/plain',
19
+ }
15
20
  return self.__response
@@ -1,3 +1,4 @@
1
+ import mimetypes
1
2
  import os
2
3
  import pdb
3
4
  import re
@@ -5,6 +6,7 @@ import re
5
6
  from io import BytesIO
6
7
  from mitmproxy.http import Request as MitmproxyRequest
7
8
  from requests import Response
9
+ from requests.structures import CaseInsensitiveDict
8
10
  from typing import Union
9
11
 
10
12
  from stoobly_agent.lib.logger import bcolors, Logger
@@ -19,45 +21,89 @@ class Options():
19
21
 
20
22
  def eval_fixtures(request: MitmproxyRequest, **options: Options) -> Union[Response, None]:
21
23
  fixture_path = None
22
- headers = {}
24
+ headers = CaseInsensitiveDict()
25
+ status_code = 200
23
26
 
24
27
  response_fixtures = options.get('response_fixtures')
25
- fixture = __eval_response_fixtures(request, response_fixtures)
28
+ fixture: dict = __eval_response_fixtures(request, response_fixtures)
26
29
 
27
30
  if not fixture:
28
31
  public_directory_path = options.get('public_directory_path')
29
32
 
30
- if public_directory_path and os.path.exists(public_directory_path):
31
- static_file_path = os.path.join(public_directory_path, request.path.lstrip('/'))
33
+ if not public_directory_path:
34
+ return
32
35
 
33
- if os.path.exists(static_file_path):
34
- fixture_path = static_file_path
36
+ request_path = 'index' if request.path == '/' else request.path
37
+ _fixture_path = os.path.join(public_directory_path, request_path.lstrip('/'))
38
+ if request.headers.get('accept'):
39
+ fixture_path = __guess_file_path(_fixture_path, request.headers['accept'])
40
+
41
+ if not fixture_path:
42
+ fixture_path = _fixture_path
43
+
44
+ if not os.path.isfile(fixture_path):
45
+ return
35
46
  else:
36
47
  fixture_path = fixture.get('path')
37
- headers = fixture.get('headers') or {}
48
+ if not fixture_path or not os.path.isfile(fixture_path):
49
+ return
38
50
 
39
- if not fixture_path:
40
- return
51
+ _headers = fixture.get('headers')
52
+ headers = CaseInsensitiveDict(_headers if isinstance(_headers, dict) else {})
53
+
54
+ if fixture.get('status_code'):
55
+ status_code = fixture.get('status_code')
41
56
 
42
57
  with open(fixture_path, 'rb') as fp:
43
58
  response = Response()
44
59
 
45
- response.status_code = 200
60
+ response.status_code = status_code
46
61
  response.raw = BytesIO(fp.read())
47
62
  response.headers = headers
48
63
 
64
+ if not response.headers.get('content-type'):
65
+ content_type = __guess_content_type(fixture_path)
66
+ if content_type:
67
+ response.headers['content-type'] = content_type
68
+
49
69
  Logger.instance(LOG_ID).debug(f"{bcolors.OKBLUE}Resolved{bcolors.ENDC} fixture {fixture_path}")
50
70
 
51
71
  return response
52
72
 
73
+ def __guess_content_type(file_path):
74
+ file_extension = os.path.splitext(file_path)[1]
75
+ if not file_extension:
76
+ return
77
+ return mimetypes.types_map.get(file_extension)
78
+
79
+ def __guess_file_path(file_path, content_type):
80
+ file_extension = os.path.splitext(file_path)[1]
81
+ if file_extension:
82
+ return file_path
83
+
84
+ if not content_type:
85
+ return
86
+
87
+ content_types = __parse_accept_header(content_type)
88
+
89
+ for content_type in content_types:
90
+ file_extension = mimetypes.guess_extension(content_type)
91
+
92
+ if not file_extension:
93
+ continue
94
+
95
+ _file_path = f"{file_path}{file_extension}"
96
+ if os.path.isfile(_file_path):
97
+ return _file_path
98
+
53
99
  def __eval_response_fixtures(request: MitmproxyRequest, response_fixtures: Fixtures):
54
- if not response_fixtures:
100
+ if not isinstance(response_fixtures, dict):
55
101
  return
56
102
 
57
103
  method = request.method
58
104
  routes = response_fixtures.get(method)
59
105
 
60
- if not routes:
106
+ if not isinstance(routes, dict):
61
107
  return
62
108
 
63
109
  for path_pattern in routes:
@@ -65,7 +111,26 @@ def __eval_response_fixtures(request: MitmproxyRequest, response_fixtures: Fixtu
65
111
  continue
66
112
 
67
113
  fixture = routes[path_pattern]
114
+ if not isinstance(fixture, dict):
115
+ continue
116
+
68
117
  path = fixture.get('path')
69
118
 
70
- if path and os.path.exists(path):
71
- return fixture
119
+ if path:
120
+ return fixture
121
+
122
+ def __parse_accept_header(accept_header):
123
+ types = []
124
+ for item in accept_header.split(","):
125
+ parts = item.split(";")
126
+ content_type = parts[0].strip()
127
+ q_value = 1.0 # Default quality value
128
+ if len(parts) > 1 and parts[1].strip().startswith("q="):
129
+ try:
130
+ q_value = float(parts[1].strip()[2:])
131
+ except ValueError:
132
+ pass # Keep default q_value if parsing fails
133
+ types.append((content_type, q_value))
134
+
135
+ # Sort by quality factor in descending order
136
+ return [content_type for content_type, _ in sorted(types, key=lambda x: x[1], reverse=True)]