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.
Files changed (67) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/configs_controller.py +3 -3
  3. stoobly_agent/app/api/headers_controller.py +1 -1
  4. stoobly_agent/app/api/query_params_controller.py +4 -4
  5. stoobly_agent/app/cli/helpers/handle_config_update_service.py +12 -12
  6. stoobly_agent/app/cli/intercept_cli.py +30 -7
  7. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  8. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +6 -0
  9. stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/.overwrite +13 -0
  10. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +16 -0
  11. stoobly_agent/app/cli/snapshot_cli.py +57 -0
  12. stoobly_agent/app/cli/types/snapshot_migration.py +61 -0
  13. stoobly_agent/app/models/adapters/joined_request_adapter.py +1 -1
  14. stoobly_agent/app/models/adapters/orm/request/mitmproxy_adapter.py +1 -1
  15. stoobly_agent/app/models/adapters/python/request/__init__.py +2 -2
  16. stoobly_agent/app/models/adapters/python/request/mitmproxy_adapter.py +1 -1
  17. stoobly_agent/app/models/adapters/python/request/raw_adapter.py +2 -2
  18. stoobly_agent/app/models/factories/resource/local_db/helpers/log.py +7 -0
  19. stoobly_agent/app/models/factories/resource/local_db/helpers/log_event.py +10 -0
  20. stoobly_agent/app/models/factories/resource/local_db/helpers/request_snapshot.py +20 -0
  21. stoobly_agent/app/models/factories/resource/local_db/helpers/scenario_snapshot.py +8 -1
  22. stoobly_agent/app/models/factories/resource/local_db/helpers/snapshot.py +1 -1
  23. stoobly_agent/app/models/factories/resource/local_db/query_param_adapter.py +2 -2
  24. stoobly_agent/app/models/helpers/apply.py +7 -6
  25. stoobly_agent/app/models/helpers/create_request_params_service.py +4 -2
  26. stoobly_agent/app/proxy/handle_mock_service.py +19 -6
  27. stoobly_agent/app/proxy/handle_record_service.py +16 -7
  28. stoobly_agent/app/proxy/intercept_settings.py +12 -1
  29. stoobly_agent/app/proxy/mitmproxy/request_facade.py +1 -1
  30. stoobly_agent/app/proxy/record/join_request_service.py +22 -10
  31. stoobly_agent/app/proxy/record/proxy_request.py +9 -0
  32. stoobly_agent/app/proxy/record/request_string.py +19 -10
  33. stoobly_agent/app/proxy/record/upload_request_service.py +7 -5
  34. stoobly_agent/app/proxy/replay/body_parser_service.py +15 -4
  35. stoobly_agent/app/proxy/replay/replay_request_service.py +1 -0
  36. stoobly_agent/app/proxy/test/helpers/upload_test_service.py +1 -1
  37. stoobly_agent/app/settings/data_rules.py +16 -1
  38. stoobly_agent/config/constants/custom_headers.py +1 -0
  39. stoobly_agent/config/constants/lifecycle_hooks.py +2 -0
  40. stoobly_agent/config/constants/record_order.py +6 -0
  41. stoobly_agent/config/constants/record_policy.py +2 -2
  42. stoobly_agent/lib/orm/transformers/orm_to_request_transformer.py +1 -1
  43. stoobly_agent/public/18-es2015.beb31fe4a4dee3007cb2.js +1 -0
  44. stoobly_agent/public/18-es5.beb31fe4a4dee3007cb2.js +1 -0
  45. stoobly_agent/public/index.html +2 -2
  46. stoobly_agent/public/main-es2015.089b46f303768fbe864f.js +1 -0
  47. stoobly_agent/public/main-es5.089b46f303768fbe864f.js +1 -0
  48. stoobly_agent/public/{runtime-es2015.b13c22b834b51724d30a.js → runtime-es2015.f8c814b38b27708e91c1.js} +1 -1
  49. stoobly_agent/public/{runtime-es5.b13c22b834b51724d30a.js → runtime-es5.f8c814b38b27708e91c1.js} +1 -1
  50. stoobly_agent/public/{styles.ab281309cf423b2cdcb0.css → styles.817f011ab81b18b0e5c2.css} +1 -1
  51. stoobly_agent/test/app/cli/config/scenario/config_scenario_set_test.py +3 -3
  52. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +5 -7
  53. stoobly_agent/test/app/cli/intercept/intercept_enable_test.py +3 -5
  54. stoobly_agent/test/app/cli/scenario/scenario_snapshot_test.py +1 -1
  55. stoobly_agent/test/app/cli/snapshot/lifecycle_hooks_migrate.py +10 -0
  56. stoobly_agent/test/app/cli/snapshot/snapshot_migrate_test.py +181 -0
  57. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  58. stoobly_agent/test/cli/record_test.py +3 -3
  59. {stoobly_agent-1.8.4.dist-info → stoobly_agent-1.9.0.dist-info}/METADATA +1 -1
  60. {stoobly_agent-1.8.4.dist-info → stoobly_agent-1.9.0.dist-info}/RECORD +63 -58
  61. stoobly_agent/public/18-es2015.d07dd29def7e2574c5b7.js +0 -1
  62. stoobly_agent/public/18-es5.d07dd29def7e2574c5b7.js +0 -1
  63. stoobly_agent/public/main-es2015.ce00115b0520fa030f01.js +0 -1
  64. stoobly_agent/public/main-es5.ce00115b0520fa030f01.js +0 -1
  65. {stoobly_agent-1.8.4.dist-info → stoobly_agent-1.9.0.dist-info}/LICENSE +0 -0
  66. {stoobly_agent-1.8.4.dist-info → stoobly_agent-1.9.0.dist-info}/WHEEL +0 -0
  67. {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 HTTPFlow as MitmproxyHTTPFlow, Request as MitmproxyRequest
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 env MOCK_POLICY: %s, Got: %s" %
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 in [custom_response_codes.NOT_FOUND, custom_response_codes.IGNORE_COMPONENTS]:
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 env RECORD_POLICY: %s, Got: %s" %
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
@@ -72,7 +72,7 @@ class MitmproxyRequestFacade(Request):
72
72
  def body(self):
73
73
  content = self.request.raw_content or b''
74
74
 
75
- return decode(content)
75
+ return content
76
76
 
77
77
  @property
78
78
  def parsed_body(self):
@@ -1,31 +1,43 @@
1
1
  import pdb
2
2
 
3
- from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
4
- from typing import List
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, intercept_settings: InterceptSettings
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, intercept_settings: InterceptSettings
33
+ flow: MitmproxyHTTPFlow, **options: JoinRequestOptions
27
34
  ) -> JoinedRequest:
28
- request = MitmproxyRequestFacade(flow.request)
29
- response = MitmproxyResponseFacade(flow.response)
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, intercept_settings)
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
- self.request_id = self.__generate_request_id()
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).encode(self.ENCODING)
39
+ return CLRF.join([self.control] + self.lines)
37
40
  else:
38
- return CLRF.join(self.lines).encode(self.ENCODING)
41
+ return CLRF.join(self.lines)
39
42
 
40
43
  def set(self, s: bytes):
41
- decoded_s = decode(s, self.ENCODING)
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
- self.lines.append("{} {} HTTP/1.1".format(self.request.method, self.proxy_request.url()))
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("{}{}".format(CLRF, self.request.body))
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
- joined_request = join_request_from_flow(flow, intercept_settings)
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
- _content_type = content_type.lower()
118
- return _content_type == JSON or _content_type.startswith('application/x-amz-json')
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'
@@ -0,0 +1,6 @@
1
+ from typing import Literal
2
+
3
+ APPEND = 'append'
4
+ OVERWRITE = 'overwrite'
5
+
6
+ RecordOrder = Literal[APPEND, OVERWRITE]
@@ -1,7 +1,7 @@
1
1
  from .intercept_policy import ALL as INTERCEPT_ALL, NONE as INTERCEPT_NONE
2
2
 
3
3
  ALL = INTERCEPT_ALL
4
+ API = 'api'
4
5
  FOUND = 'found'
5
6
  NONE = INTERCEPT_NONE
6
- NOT_FOUND = 'not_found'
7
- OVERWRITE = 'overwrite'
7
+ NOT_FOUND = 'not_found'
@@ -94,7 +94,7 @@ class ORMToRequestTransformer():
94
94
  return self
95
95
 
96
96
  def with_scheme(self, scheme: str):
97
- if scheme != 'https' or 'http':
97
+ if scheme != 'https' and scheme != 'http':
98
98
  raise ValueError('Scheme must be https or http')
99
99
 
100
100
  self.__scheme = scheme