stoobly-agent 1.9.1__py3-none-any.whl → 1.9.3__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 CHANGED
@@ -1,2 +1,2 @@
1
1
  COMMAND = 'stoobly-agent'
2
- VERSION = '1.9.1'
2
+ VERSION = '1.9.3'
@@ -1,8 +1,5 @@
1
1
  services:
2
2
  stoobly_ui.base:
3
- environment:
4
- SERVICE_NAME: ${SERVICE_NAME}
5
- WORKFLOW_NAME: ${WORKFLOW_NAME}
6
3
  extends:
7
4
  file: ../.docker-compose.base.yml
8
5
  service: context_base
@@ -2,4 +2,8 @@
2
2
 
3
3
  # Add custom initialization here
4
4
 
5
- app_path=$1 # Path to application source files
5
+ app_path=$1 # Path to application source files
6
+
7
+ # For example, the below commands copies files inside a dist folder into the public folder.
8
+ # The public folder is used when no recorded mocks are found.
9
+ #cp -r $app_path/dist/. public
@@ -6,6 +6,7 @@ import sys
6
6
 
7
7
  from io import TextIOWrapper
8
8
  from typing import List
9
+ from urllib.parse import urlparse
9
10
 
10
11
  from stoobly_agent.app.cli.helpers.certificate_authority import CertificateAuthority
11
12
  from stoobly_agent.app.cli.helpers.shell import exec_stream
@@ -148,10 +149,10 @@ def create(**kwargs):
148
149
  sys.exit(1)
149
150
 
150
151
  if kwargs.get('hostname'):
151
- hostname_regex = re.compile(r'^[a-zA-Z0-9.-]+$')
152
- if not re.search(hostname_regex, kwargs['hostname']):
153
- print(f"Error: {kwargs['hostname']} is invalid.", file=sys.stderr)
154
- sys.exit(1)
152
+ __validate_hostname(kwargs.get('hostname'))
153
+
154
+ if kwargs.get("proxy_mode"):
155
+ __validate_proxy_mode(kwargs.get("proxy_mode"))
155
156
 
156
157
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
157
158
 
@@ -216,6 +217,13 @@ def delete(**kwargs):
216
217
  @click.option('--port', type=click.IntRange(1, 65535), help='Service port.')
217
218
  @click.option('--priority', default=5, type=click.FloatRange(1.0, 9.0), help='Determines the service run order. Lower values run first.')
218
219
  @click.option('--scheme', type=click.Choice(['http', 'https']), help='Defaults to https if hostname is set.')
220
+ @click.option('--name', type=click.STRING, help='New name of the service to update to.')
221
+ @click.option('--proxy-mode', help='''
222
+ Proxy mode can be "regular", "transparent", "socks5",
223
+ "reverse:SPEC", or "upstream:SPEC". For reverse and
224
+ upstream proxy modes, SPEC is host specification in
225
+ the form of "http[s]://host[:port]".
226
+ ''')
219
227
  @click.argument('service_name')
220
228
  def update(**kwargs):
221
229
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
@@ -228,7 +236,20 @@ def update(**kwargs):
228
236
  service_config = ServiceConfig(service.dir_path)
229
237
 
230
238
  if kwargs['hostname']:
231
- service_config.hostname = kwargs['hostname']
239
+ __validate_hostname(kwargs['hostname'])
240
+
241
+ old_hostname = service_config.hostname
242
+
243
+ if old_hostname != kwargs['hostname']:
244
+ service_config.hostname = kwargs['hostname']
245
+
246
+ # If this is the default proxy_mode and the origin matches the original hostname, assume it is safe to update with the new hostname
247
+ if service_config.proxy_mode.startswith("reverse:"):
248
+ old_origin = service_config.proxy_mode.split("reverse:")[1]
249
+ parsed_origin_url = urlparse(old_origin)
250
+
251
+ if old_hostname == parsed_origin_url.hostname:
252
+ service_config.proxy_mode = service_config.proxy_mode.replace(old_hostname, service_config.hostname)
232
253
 
233
254
  if kwargs['priority']:
234
255
  service_config.priority = kwargs['priority']
@@ -239,6 +260,10 @@ def update(**kwargs):
239
260
  if kwargs['scheme']:
240
261
  service_config.scheme = kwargs['scheme']
241
262
 
263
+ if kwargs['proxy_mode']:
264
+ __validate_proxy_mode(kwargs['proxy_mode'])
265
+ service_config.proxy_mode = kwargs['proxy_mode']
266
+
242
267
  service_config.write()
243
268
 
244
269
  @workflow.command(
@@ -759,7 +784,42 @@ def __validate_app_dir(app_dir_path):
759
784
 
760
785
  def __validate_service_dir(service_dir_path):
761
786
  if not os.path.exists(service_dir_path):
762
- print(f"Error: {service_dir_path} does not exist, please scaffold this service", file=sys.stderr)
787
+ print(f"Error: '{service_dir_path}' does not exist, please scaffold this service", file=sys.stderr)
788
+ sys.exit(1)
789
+
790
+ def __validate_proxy_mode(proxy_mode: str) -> None:
791
+ valid_exact_matches = {
792
+ "regular": None,
793
+ "transparent": None,
794
+ "socks5": None,
795
+ }
796
+
797
+ valid_prefixes = {
798
+ "reverse": None,
799
+ "upstream": None
800
+ }
801
+
802
+ if proxy_mode in valid_exact_matches:
803
+ return
804
+
805
+ split_str = proxy_mode.split(":", 1)
806
+ if len(split_str) != 2:
807
+ print(f"Error: {proxy_mode} is invalid.", file=sys.stderr)
808
+ sys.exit(1)
809
+
810
+ prefix = split_str[0]
811
+ spec = split_str[1]
812
+
813
+ if prefix not in valid_prefixes:
814
+ print(f"Error: {proxy_mode} is invalid.", file=sys.stderr)
815
+ sys.exit(1)
816
+
817
+ # TODO: validate SPEC
818
+
819
+ def __validate_hostname(hostname: str) -> None:
820
+ hostname_regex = re.compile(r'^[a-zA-Z0-9.-]+$')
821
+ if not re.search(hostname_regex, hostname):
822
+ print(f"Error: {hostname} is invalid.", file=sys.stderr)
763
823
  sys.exit(1)
764
824
 
765
825
  def __workflow_create(app, **kwargs):
@@ -17,9 +17,7 @@ class JoinedRequestAdapter():
17
17
  if isinstance(payloads_delimitter, str):
18
18
  payloads_delimitter = payloads_delimitter.encode()
19
19
 
20
- self.__split_joined_request_string = joined_request_string.split(payloads_delimitter)
21
- if len(self.__split_joined_request_string) != 2:
22
- self.__split_joined_request_string = joined_request_string.split(payloads_delimitter.replace(b"\n", b"\r\n"))
20
+ self.__split_joined_request_string = self.raw_request_split(joined_request_string, payloads_delimitter)
23
21
 
24
22
  if len(self.__split_joined_request_string) != 2:
25
23
  raise ValueError(f"Could not split by {payloads_delimitter}")
@@ -47,7 +45,7 @@ class JoinedRequestAdapter():
47
45
  request_string = RequestString(None)
48
46
 
49
47
  delimitter = RequestStringCLRF
50
- request_string_toks = self.__split_joined_request_string[0].split(delimitter)
48
+ request_string_toks = self.repaired_string_toks(self.__split_joined_request_string[0], delimitter)
51
49
  request_string.set(self.raw_request_string or delimitter.join(request_string_toks[1:]))
52
50
  request_string.control = request_string_toks[0]
53
51
 
@@ -57,7 +55,7 @@ class JoinedRequestAdapter():
57
55
  response_string = ResponseString(None, None)
58
56
 
59
57
  delimitter = ResponseStringCLRF
60
- response_string_toks = self.__split_joined_request_string[1].split(delimitter)
58
+ response_string_toks = self.repaired_string_toks(self.__split_joined_request_string[1], delimitter)
61
59
  response_string.set(self.raw_response_string or delimitter.join(response_string_toks[1:]))
62
60
  response_string.control = response_string_toks[0]
63
61
 
@@ -68,4 +66,38 @@ class JoinedRequestAdapter():
68
66
 
69
67
  joined_request.request_string = self.build_request_string()
70
68
  joined_request.response_string = self.build_response_string()
71
- return joined_request
69
+ return joined_request
70
+
71
+ # If all CRLF characters have been replaced with LF e.g. visual studio code
72
+ # Then try to repair the raw string, see https://github.com/Stoobly/stoobly-agent/issues/415
73
+ @staticmethod
74
+ def repaired_string_toks(raw_string: bytes, delimitter: bytes):
75
+ toks = raw_string.split(delimitter)
76
+
77
+ if len(toks) == 1:
78
+ lf = b"\n"
79
+ toks = raw_string.split(lf)
80
+
81
+ # See for request: https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
82
+ # See for response: https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html
83
+ i = 0
84
+ for line in toks:
85
+ i += 1
86
+
87
+ # On two lf characters, then the following lines are the body
88
+ if line == b'':
89
+ break
90
+
91
+ toks = toks[:i] + [lf.join(toks[i:])]
92
+
93
+ if len(toks) == 1:
94
+ raise ValueError(f"Could not split request by {delimitter}")
95
+
96
+ return toks
97
+
98
+ @staticmethod
99
+ def raw_request_split(raw_string: bytes, payloads_delimitter = REQUEST_DELIMITTER):
100
+ toks = raw_string.split(payloads_delimitter)
101
+ if len(toks) != 2:
102
+ toks = raw_string.split(payloads_delimitter.replace(b"\n", b"\r\n"))
103
+ return toks
@@ -70,22 +70,7 @@ class Log():
70
70
 
71
71
  @property
72
72
  def scenario_inverted_index(self):
73
- index = {}
74
-
75
- def handle_snapshot(snapshot: RequestSnapshot):
76
- request_uuid = snapshot.uuid
77
- if not request_uuid in index:
78
- index[request_uuid] = []
79
-
80
- index[request_uuid].append(event.resource_uuid)
81
-
82
- for event in self.target_events:
83
- if not event.is_scenario():
84
- continue
85
-
86
- event.snapshot().iter_request_snapshots(handle_snapshot)
87
-
88
- return index
73
+ return self.build_scenario_inverted_index(self.target_events)
89
74
 
90
75
  @property
91
76
  def unprocessed_events(self) -> List[LogEvent]:
@@ -153,6 +138,22 @@ class Log():
153
138
 
154
139
  def build_log_events(self, raw_events) -> List[LogEvent]:
155
140
  return list(map(lambda raw_event: LogEvent(raw_event), raw_events))
141
+
142
+ def build_scenario_inverted_index(self, events: List[LogEvent], index = {}):
143
+ def handle_snapshot(snapshot: RequestSnapshot):
144
+ request_uuid = snapshot.uuid
145
+ if not request_uuid in index:
146
+ index[request_uuid] = []
147
+
148
+ index[request_uuid].append(event.resource_uuid)
149
+
150
+ for event in events:
151
+ if not event.is_scenario():
152
+ continue
153
+
154
+ event.snapshot().iter_request_snapshots(handle_snapshot)
155
+
156
+ return index
156
157
 
157
158
  def next_version(self, last_processed_uuid: str = None):
158
159
  uuids = self.uuids()
@@ -169,7 +170,7 @@ class Log():
169
170
  def collapse(self, events: List[LogEvent]) -> List[LogEvent]:
170
171
  events_count = {}
171
172
 
172
- # More recent events take precedence over earlier ones, keep only the most recent event
173
+ # More recent events take precedence over earlier ones, only the most recent event
173
174
  for event in events:
174
175
  event_key = event.key
175
176
 
@@ -274,10 +275,10 @@ class Log():
274
275
 
275
276
  def remove_dangling_events(self, processed_events: List[LogEvent], unprocessed_events: List[LogEvent]):
276
277
  '''
277
- Remove DELETE events where the last processed event was a PUT
278
+ Remove DELETE events unless the last processed event was a PUT
278
279
  '''
279
280
 
280
- # Build an index such that if the last event is DELETE_ACTION, then it will NOT exist in the index
281
+ # Build an index to keep track of the last action that occurred for a resource
281
282
  index = {}
282
283
  for event in processed_events:
283
284
  if event.action == PUT_ACTION:
@@ -286,13 +287,27 @@ class Log():
286
287
  if event.resource_uuid in index:
287
288
  del index[event.resource_uuid]
288
289
 
289
- scenario_inverted_index = self.scenario_inverted_index
290
-
290
+ scenario_inverted_index = self.build_scenario_inverted_index(processed_events)
291
+
291
292
  def keep(e: LogEvent):
292
- if e.action != DELETE_ACTION:
293
+ # Keep the event if it's a PUT, it may have been updated
294
+ if e.action == PUT_ACTION:
293
295
  return True
294
-
295
- return e.action == DELETE_ACTION and e.is_request() and e.resource_uuid in scenario_inverted_index
296
+
297
+ if e.action == DELETE_ACTION:
298
+ if e.is_request():
299
+ referenced = e.resource_uuid in scenario_inverted_index
300
+ # Keep the DELETE event if the requests exists in a scenario
301
+ # or if it doesn't exist in a scenario, there was a previous delete event
302
+ return referenced or (e.resource_uuid in index and not referenced)
303
+ elif e.is_scenario():
304
+ # Update scenario_inverted_index with unprocessed event
305
+ self.build_scenario_inverted_index([e], scenario_inverted_index)
306
+
307
+ # Keep DELETE scenario event only if previous event for the resource was PUT
308
+ return e.resource_uuid in index
309
+ else:
310
+ return True
296
311
 
297
312
  return list(
298
313
  filter(
@@ -2,10 +2,10 @@ import pdb
2
2
 
3
3
  from typing import List
4
4
 
5
+ from stoobly_agent.app.models.adapters.joined_request_adapter import JoinedRequestAdapter
5
6
  from stoobly_agent.app.models.factories.resource.local_db.helpers.log import Log
6
7
  from stoobly_agent.app.models.factories.resource.local_db.helpers.request_snapshot import RequestSnapshot
7
8
  from stoobly_agent.app.models.factories.resource.local_db.helpers.scenario_snapshot import ScenarioSnapshot
8
- from stoobly_agent.app.proxy.record import REQUEST_STRING_CLRF
9
9
  from stoobly_agent.app.settings import Settings
10
10
  from stoobly_agent.lib.logger import bcolors
11
11
 
@@ -165,7 +165,12 @@ class Apply():
165
165
  self.__logger(f"{bcolors.WARNING}Skipping Request{bcolors.ENDC} {error}")
166
166
  return error, 301
167
167
 
168
- return self.__put_request(uuid, raw_request)
168
+ res, status = self.__put_request(uuid, raw_request)
169
+
170
+ if status != 200:
171
+ return f"{res} {snapshot.path}", status
172
+
173
+ return res, status
169
174
 
170
175
  def __apply_delete_scenario(self, uuid: str):
171
176
  res, status = self.scenario_model.destroy(uuid, force=self.__force)
@@ -227,8 +232,8 @@ class Apply():
227
232
 
228
233
  if not raw_request:
229
234
  return f"{request_snapshot.path} is missing", 400
230
-
231
- toks = raw_request.split(REQUEST_STRING_CLRF, 1)
235
+
236
+ toks = JoinedRequestAdapter.raw_request_split(raw_request)
232
237
  if len(toks) != 2:
233
238
  return f"{request_snapshot.path} contains an invalid request", 400
234
239
 
@@ -236,7 +241,7 @@ class Apply():
236
241
  res, status = self.__put_request(uuid, raw_request, scenario_id=scenario['id'])
237
242
 
238
243
  if status != 200:
239
- return res, status
244
+ return f"{res} {request_snapshot.path}", status
240
245
 
241
246
  snapshot_requests[uuid] = res
242
247
 
@@ -274,8 +279,6 @@ class Apply():
274
279
 
275
280
  if self.__logger and status == 200:
276
281
  self.__logger(f"{bcolors.OKGREEN}Created Request{bcolors.ENDC} {res['list'][0]['url']}")
277
- else:
278
- self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
279
282
  elif status == 200:
280
283
  params = {
281
284
  'is_deleted': False,
@@ -287,7 +290,5 @@ class Apply():
287
290
  if self.__logger:
288
291
  if status == 200:
289
292
  self.__logger(f"{bcolors.OKCYAN}Updated Request{bcolors.ENDC} {res['url']}")
290
- else:
291
- self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
292
293
 
293
294
  return res, status
@@ -147,15 +147,8 @@ def handle_request_mock(context: MockContext):
147
147
  # 2. AFTER_MOCK gets triggered (if mock found)
148
148
  #
149
149
  def handle_response_mock(context: MockContext):
150
- response = context.flow.response
151
- request_key = response.headers.get(custom_headers.MOCK_REQUEST_KEY)
152
-
153
- if request_key:
154
- request = context.flow.request
155
- Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}Mocked{bcolors.ENDC} {request.url} -> {request_key}")
156
-
157
- __rewrite_response(context)
158
- __mock_hook(lifecycle_hooks.AFTER_MOCK, context)
150
+ __rewrite_response(context)
151
+ __mock_hook(lifecycle_hooks.AFTER_MOCK, context)
159
152
 
160
153
  def __handle_mock_failure(context: MockContext) -> None:
161
154
  flow = context.flow
@@ -186,6 +179,19 @@ def __handle_found_policy(context: MockContext) -> None:
186
179
  reverse_proxy(req, upstream_url, {})
187
180
 
188
181
  def __handle_mock_success(context: MockContext) -> None:
182
+ response = context.response
183
+
184
+ if response:
185
+ request = context.flow.request
186
+
187
+ request_key = response.headers.get(custom_headers.MOCK_REQUEST_KEY)
188
+ if request_key:
189
+ Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}Mocked{bcolors.ENDC} {request.url} -> {request_key}")
190
+
191
+ fixture_path = response.headers.get(custom_headers.MOCK_FIXTURE_PATH)
192
+ if fixture_path:
193
+ Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}Mocked{bcolors.ENDC} {request.url} -> {fixture_path}")
194
+
189
195
  if os.environ.get(env_vars.AGENT_SIMULATE_LATENCY):
190
196
  response = context.response
191
197
  start_time = context.start_time
@@ -10,6 +10,7 @@ from requests.structures import CaseInsensitiveDict
10
10
  from typing import Union
11
11
 
12
12
  from stoobly_agent.lib.logger import bcolors, Logger
13
+ from stoobly_agent.config.constants.custom_headers import MOCK_FIXTURE_PATH
13
14
 
14
15
  from .types import Fixtures
15
16
 
@@ -20,45 +21,50 @@ class Options():
20
21
  response_fixtures: Fixtures
21
22
 
22
23
  def eval_fixtures(request: MitmproxyRequest, **options: Options) -> Union[Response, None]:
23
- fixture_path = None
24
+ fixture_path = request.headers.get(MOCK_FIXTURE_PATH)
24
25
  headers = CaseInsensitiveDict()
25
26
  status_code = 200
26
27
 
27
- response_fixtures = options.get('response_fixtures')
28
- fixture: dict = __eval_response_fixtures(request, response_fixtures)
29
-
30
- if not fixture:
31
- public_directory_path = options.get('public_directory_path')
32
-
33
- if not public_directory_path:
28
+ if fixture_path:
29
+ if not os.path.exists(fixture_path):
34
30
  return
31
+ else:
32
+ response_fixtures = options.get('response_fixtures')
33
+ fixture: dict = __eval_response_fixtures(request, response_fixtures)
35
34
 
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'])
35
+ if not fixture:
36
+ public_directory_path = options.get('public_directory_path')
40
37
 
41
- if not fixture_path:
42
- fixture_path = _fixture_path
38
+ if not public_directory_path:
39
+ return
43
40
 
44
- if not os.path.isfile(fixture_path):
45
- return
46
- else:
47
- fixture_path = fixture.get('path')
48
- if not fixture_path or not os.path.isfile(fixture_path):
49
- return
41
+ request_path = 'index' if request.path == '/' else request.path
42
+ _fixture_path = os.path.join(public_directory_path, request_path.lstrip('/'))
43
+ if request.headers.get('accept'):
44
+ fixture_path = __guess_file_path(_fixture_path, request.headers['accept'])
45
+
46
+ if not fixture_path:
47
+ fixture_path = _fixture_path
50
48
 
51
- _headers = fixture.get('headers')
52
- headers = CaseInsensitiveDict(_headers if isinstance(_headers, dict) else {})
49
+ if not os.path.isfile(fixture_path):
50
+ return
51
+ else:
52
+ fixture_path = fixture.get('path')
53
+ if not fixture_path or not os.path.isfile(fixture_path):
54
+ return
53
55
 
54
- if fixture.get('status_code'):
55
- status_code = fixture.get('status_code')
56
+ _headers = fixture.get('headers')
57
+ headers = CaseInsensitiveDict(_headers if isinstance(_headers, dict) else {})
56
58
 
59
+ if fixture.get('status_code'):
60
+ status_code = fixture.get('status_code')
61
+
57
62
  with open(fixture_path, 'rb') as fp:
58
63
  response = Response()
59
64
 
60
65
  response.status_code = status_code
61
66
  response.raw = BytesIO(fp.read())
67
+ headers[MOCK_FIXTURE_PATH] = fixture_path
62
68
  response.headers = headers
63
69
 
64
70
  if not response.headers.get('content-type'):
@@ -2,6 +2,7 @@ ALIAS_RESOLVE_STRATEGY = 'X-Stoobly-Alias-Resolve-Strategy'
2
2
  CONTENT_TYPE = 'X-Stoobly-Content-Type'
3
3
  CONTENT_TYPE_TEST_RESULTS = 'test/results'
4
4
  REMOTE_PROJECT_KEY = 'X-Stoobly-Endpoints-Project-Id'
5
+ MOCK_FIXTURE_PATH = 'X-Stoobly-Fixture-Path'
5
6
  MOCK_POLICY = 'X-Stoobly-Mock-Policy'
6
7
  MOCK_REQUEST_ID = 'X-Stoobly-Request-Id'
7
8
  MOCK_REQUEST_ENDPOINT_ID = 'X-Stoobly-Request-Endpoint-Id'
@@ -28,6 +28,10 @@ class UuidKey(ResourceKey):
28
28
  toks = key.split(DELIMITTER)
29
29
 
30
30
  d = {}
31
+
32
+ if len(toks) == 0:
33
+ return d
34
+
31
35
  for tok in toks:
32
36
  d[tok[0]] = tok[1:]
33
37
 
@@ -1,16 +1,18 @@
1
1
  import pdb
2
2
  import pytest
3
+ import time
3
4
 
4
5
  from click.testing import CliRunner
5
-
6
- from stoobly_agent.test.test_helper import DETERMINISTIC_GET_REQUEST_URL, reset
6
+ from typing import List
7
7
 
8
8
  from stoobly_agent.app.models.adapters.orm import JoinedRequestStringAdapter
9
9
  from stoobly_agent.app.models.factories.resource.local_db.helpers.log import Log
10
- from stoobly_agent.app.models.factories.resource.local_db.helpers.log_event import DELETE_ACTION
10
+ from stoobly_agent.app.models.factories.resource.local_db.helpers.log_event import DELETE_ACTION, PUT_ACTION
11
11
  from stoobly_agent.app.models.factories.resource.local_db.helpers.request_snapshot import RequestSnapshot
12
- from stoobly_agent.cli import record, request
12
+ from stoobly_agent.cli import record, request, scenario, snapshot
13
13
  from stoobly_agent.lib.orm.request import Request
14
+ from stoobly_agent.lib.orm.scenario import Scenario
15
+ from stoobly_agent.test.test_helper import DETERMINISTIC_GET_REQUEST_URL, NON_DETERMINISTIC_GET_REQUEST_URL, reset
14
16
 
15
17
  @pytest.fixture(scope='module')
16
18
  def runner():
@@ -122,3 +124,51 @@ class TestRequestSnapshot():
122
124
 
123
125
  event = unprocessed_events[0]
124
126
  assert event.resource_uuid == recorded_request_two.uuid
127
+ assert event.action == PUT_ACTION
128
+
129
+ class TestWhenDeleteFirst():
130
+
131
+ @pytest.fixture(scope='class')
132
+ def recorded_request(self, runner: CliRunner):
133
+ record_result = runner.invoke(record, [DETERMINISTIC_GET_REQUEST_URL])
134
+ assert record_result.exit_code == 0
135
+ return Request.last()
136
+
137
+ def test_initial_delete(self, runner: CliRunner, recorded_request: Request):
138
+ snapshot_result = runner.invoke(request, ['snapshot', '--action', DELETE_ACTION, recorded_request.key()])
139
+ assert snapshot_result.exit_code == 0
140
+
141
+ log = Log()
142
+
143
+ events = log.raw_events
144
+ assert len(events) == 1
145
+
146
+ unprocessed_events = log.unprocessed_events
147
+ assert len(unprocessed_events) == 0
148
+
149
+ def test_puts(self, runner: CliRunner, recorded_request: Request):
150
+ snapshot_result = runner.invoke(request, ['snapshot', '--action', PUT_ACTION, recorded_request.key()])
151
+ assert snapshot_result.exit_code == 0
152
+
153
+ log = Log()
154
+
155
+ events = log.raw_events
156
+ assert len(events) == 2
157
+
158
+ unprocessed_events = log.unprocessed_events
159
+ assert len(unprocessed_events) == 1
160
+ assert unprocessed_events[0].action == PUT_ACTION
161
+
162
+ def test_final_delete(self, runner: CliRunner, recorded_request: Request):
163
+ snapshot_result = runner.invoke(request, ['snapshot', '--action', DELETE_ACTION, recorded_request.key()])
164
+ assert snapshot_result.exit_code == 0
165
+
166
+ log = Log()
167
+
168
+ events = log.events
169
+ assert len(events) == 3
170
+
171
+ collapsed_events = log.collapse(events)
172
+ assert len(collapsed_events) == 1
173
+ unprocessed_events = log.unprocessed_events
174
+ assert len(unprocessed_events) == 0
@@ -556,7 +556,7 @@ class TestApply():
556
556
  1. Create scenario
557
557
  2. Add 2 requests to it
558
558
  3. Snapshot scenario
559
- 4. Snapshot requests with action DELETE_ACTION
559
+ 4. Snapshot second request with action DELETE_ACTION
560
560
  5. Apply
561
561
  6. Expect scenario to have 1 request
562
562
  '''
@@ -689,4 +689,124 @@ class TestApply():
689
689
 
690
690
  requests = created_scenario_two.requests
691
691
 
692
- assert_orm_request_equivalent(requests[0], created_scenario_request)
692
+ assert_orm_request_equivalent(requests[0], created_scenario_request)
693
+
694
+ class TestApply():
695
+
696
+ class TestWhenNoApply():
697
+ '''
698
+ 1. Create scenario
699
+ 2. Add 2 requests to it
700
+ 3. Snapshot scenario
701
+ 4. Apply scenario
702
+ 5. Snapshot second request with action DELETE_ACTION
703
+ 6. Prune
704
+ 7. Apply
705
+ 8. Expect scenario to have 1 request, because scenario depends on the request, should not be able to prune
706
+ '''
707
+
708
+ @pytest.fixture(scope='class')
709
+ def created_scenario(self, runner: CliRunner):
710
+ create_result = runner.invoke(scenario, ['create', 'test'])
711
+ assert create_result.exit_code == 0
712
+ return Scenario.last()
713
+
714
+ @pytest.fixture(scope='class', autouse=True)
715
+ def created_scenario_requests(self, runner: CliRunner, created_scenario: Scenario):
716
+ record_result = runner.invoke(record, ['--scenario-key', created_scenario.key(), DETERMINISTIC_GET_REQUEST_URL])
717
+ assert record_result.exit_code == 0
718
+
719
+ record_result = runner.invoke(record, ['--scenario-key', created_scenario.key(), NON_DETERMINISTIC_GET_REQUEST_URL])
720
+ assert record_result.exit_code == 0
721
+
722
+ return created_scenario.requests
723
+
724
+ @pytest.fixture(scope='class', autouse=True)
725
+ def snapshots(self, runner: CliRunner, created_scenario: Scenario, created_scenario_requests: List[Request]):
726
+ snapshot_result = runner.invoke(scenario, ['snapshot', created_scenario.key()])
727
+ assert snapshot_result.exit_code == 0
728
+
729
+ created_request = created_scenario_requests[1]
730
+ snapshot_result = runner.invoke(request, ['snapshot', created_request.key(), '--action', DELETE_ACTION])
731
+ assert snapshot_result.exit_code == 0
732
+
733
+ def test_events(self):
734
+ log = Log()
735
+
736
+ events = log.events
737
+ assert len(events) == 2
738
+
739
+ def test_collapsed_events(self):
740
+ log = Log()
741
+
742
+ collapsed_events = log.collapse(log.events)
743
+ assert len(collapsed_events) == 2
744
+
745
+ def test_unprocessed_events(self):
746
+ log = Log()
747
+
748
+ unprocessed_events = log.unprocessed_events
749
+ assert len(unprocessed_events) == 2
750
+
751
+ class TestWhenRemoveScenarioRequest():
752
+ '''
753
+ 1. Create scenario
754
+ 2. Add 2 requests to it
755
+ 3. Snapshot scenario
756
+ 4. Apply scenario
757
+ 5. Snapshot second request with action DELETE_ACTION
758
+ 6. Prune
759
+ 7. Apply
760
+ 8. Expect scenario to have 1 request, because scenario depends on the request, should not be able to prune
761
+ '''
762
+
763
+ @pytest.fixture(scope='class')
764
+ def created_scenario(self, runner: CliRunner):
765
+ create_result = runner.invoke(scenario, ['create', 'test'])
766
+ assert create_result.exit_code == 0
767
+ return Scenario.last()
768
+
769
+ @pytest.fixture(scope='class', autouse=True)
770
+ def created_scenario_requests(self, runner: CliRunner, created_scenario: Scenario):
771
+ record_result = runner.invoke(record, ['--scenario-key', created_scenario.key(), DETERMINISTIC_GET_REQUEST_URL])
772
+ assert record_result.exit_code == 0
773
+
774
+ record_result = runner.invoke(record, ['--scenario-key', created_scenario.key(), NON_DETERMINISTIC_GET_REQUEST_URL])
775
+ assert record_result.exit_code == 0
776
+
777
+ return created_scenario.requests
778
+
779
+ @pytest.fixture(scope='class', autouse=True)
780
+ def apply_result(self, runner: CliRunner, created_scenario: Scenario, created_scenario_requests: List[Request]):
781
+ snapshot_result = runner.invoke(scenario, ['snapshot', created_scenario.key()])
782
+ assert snapshot_result.exit_code == 0
783
+
784
+ created_scenario = Scenario.find(created_scenario.id)
785
+ assert created_scenario.requests_count == 2
786
+ apply_result = runner.invoke(snapshot, ['apply'])
787
+ assert apply_result.exit_code == 0
788
+
789
+ created_request = created_scenario_requests[1]
790
+ snapshot_result = runner.invoke(request, ['snapshot', created_request.key(), '--action', DELETE_ACTION])
791
+ assert snapshot_result.exit_code == 0
792
+
793
+ return apply_result
794
+
795
+ def test_events(self):
796
+ log = Log()
797
+
798
+ events = log.events
799
+ assert len(events) == 2
800
+
801
+ def test_collapsed_events(self):
802
+ log = Log()
803
+
804
+ collapsed_events = log.collapse(log.events)
805
+ assert len(collapsed_events) == 2
806
+
807
+ def test_unprocessed_events(self):
808
+ log = Log()
809
+
810
+ unprocessed_events = log.unprocessed_events
811
+ assert len(unprocessed_events) == 1
812
+ assert unprocessed_events[0].action == DELETE_ACTION
@@ -116,10 +116,10 @@ class TestPrune():
116
116
  1. Create scenario
117
117
  2. Add 2 requests to it
118
118
  3. Snapshot scenario
119
- 4. Snapshot request with action DELETE_ACTION
120
- 5. Prune, but because scenario depends on the request, should not be able to prune
119
+ 4. Snapshot second request with action DELETE_ACTION
120
+ 5. Prune
121
121
  6. Apply
122
- 7. Expect scenario to have 1 request
122
+ 7. Expect scenario to have 1 request, because scenario depends on the request, should not be able to prune
123
123
  '''
124
124
 
125
125
  @pytest.fixture(scope='class')
@@ -0,0 +1,38 @@
1
+ import pytest
2
+
3
+ from stoobly_agent.app.models.adapters.joined_request_adapter import RequestStringCLRF, JoinedRequestAdapter
4
+
5
+ class TestJoindRequestStringAdapter():
6
+
7
+ @pytest.fixture(scope='class', autouse=True)
8
+ def corrupted_raw_string(self):
9
+ toks = [
10
+ b"control",
11
+ b"header1",
12
+ b"header2",
13
+ b'',
14
+ b'body1',
15
+ b'body2'
16
+ ]
17
+ return b"\n".join(toks)
18
+
19
+ @pytest.fixture(scope='class', autouse=True)
20
+ def repaired_toks(self, corrupted_raw_string: bytes):
21
+ # [b'control', b'header1', b'header2', b'', b'body1\nbody2']
22
+ return JoinedRequestAdapter.repaired_string_toks(corrupted_raw_string, RequestStringCLRF)
23
+
24
+ def test_control(self, repaired_toks: list):
25
+ assert repaired_toks[0] == b'control'
26
+
27
+ def test_header1(self, repaired_toks: list):
28
+ assert repaired_toks[1] == b'header1'
29
+
30
+ def test_header2(self, repaired_toks: list):
31
+ assert repaired_toks[2] == b'header2'
32
+
33
+ def test_clrf(self, repaired_toks: list):
34
+ assert repaired_toks[3] == b''
35
+
36
+ def test_body(self, repaired_toks: list):
37
+ assert repaired_toks[4] == b'body1\nbody2'
38
+
@@ -1 +1 @@
1
- 1.9.0
1
+ 1.9.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: stoobly-agent
3
- Version: 1.9.1
3
+ Version: 1.9.3
4
4
  Summary: Record, mock, and test HTTP(s) requests. CLI agent for Stoobly
5
5
  License: Apache-2.0
6
6
  Author: Matt Le
@@ -1,4 +1,4 @@
1
- stoobly_agent/__init__.py,sha256=ehByLp1z2dzieXsaNQLADxDog0UyQfS9wSZ3n9PqZNs,44
1
+ stoobly_agent/__init__.py,sha256=vWRmKJsuKpHHO4MRnGJ8EzKAw6gjkI7dsgVj0IsJ-DA,44
2
2
  stoobly_agent/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  stoobly_agent/app/api/__init__.py,sha256=ctkB8KR-eXO0SFhj602huHiyvQ3PslFWd8fkcufgrAI,1000
4
4
  stoobly_agent/app/api/application_http_request_handler.py,sha256=Vvz53yB0bR7J-QqMAkLlhcZrA4P64ZEN7w8cMbgl6o0,5261
@@ -142,7 +142,7 @@ stoobly_agent/app/cli/scaffold/templates/app/gateway/mock/.docker-compose.mock.y
142
142
  stoobly_agent/app/cli/scaffold/templates/app/gateway/record/.docker-compose.record.yml,sha256=eyLH2h33Peunus8M1sUKL9AALCG2ABhV_heiJKhvgwo,138
143
143
  stoobly_agent/app/cli/scaffold/templates/app/gateway/test/.docker-compose.test.yml,sha256=oJO6i0lsuQaQeIH80yoPZo3Vs0LzUAH2WRl853yLq6g,136
144
144
  stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.config.yml,sha256=XnLQZMzzMMIwVycjyPN5QXsmRztkTFAna1kIHYuDfJQ,19
145
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml,sha256=bEmlH0ga_p3vsYZFagMGxvw47KscURmbYrfegMZb1CI,202
145
+ stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml,sha256=bxrtZqf3YtaJCukzScslh5PgWC5q8xkGIP1wKJf33LA,111
146
146
  stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/.docker-compose.exec.yml,sha256=JN89sU5uRf6YqHvN_O63K8rwQIAPJHbhFDLFmuUjKNM,304
147
147
  stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml,sha256=FnCn64DjxyAiB2P_1JUwFmXslMR961nVZHkYiEXytlg,232
148
148
  stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml,sha256=t34FNYZboJSfrKnIB2oJ3UuE_mJaW77-hcbSn3sfWec,235
@@ -188,7 +188,7 @@ stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure,sha256=5k
188
188
  stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/init,sha256=EaoFDyoJbHc9Ui8ELYKmfweXAycJptVOQblszeh3XTE,94
189
189
  stoobly_agent/app/cli/scaffold/templates/workflow/record/lifecycle_hooks.py,sha256=4vaVc_gnDTCLEqtcZybIk5dcmXrKmGuesF6gc3-_kX8,473
190
190
  stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure,sha256=GAU7AfSPcyDSI9RJ7mynT83YqgN9r_E9HZYx0RXE1lU,279
191
- stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/init,sha256=EaoFDyoJbHc9Ui8ELYKmfweXAycJptVOQblszeh3XTE,94
191
+ stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/init,sha256=_jnP53I1yyCtv5TXOnMtQcEMbU43tpZ-za7s8Ely6P0,281
192
192
  stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures.yml,sha256=CJlZ_kugygZpmyqIauBjNZxqk7XyLaa3yl3AWj8KV28,259
193
193
  stoobly_agent/app/cli/scaffold/templates/workflow/test/lifecycle_hooks.py,sha256=U7mlzT_wBR3uhHSG6CAyt5tBUNAvdIrCw33gdB-F294,467
194
194
  stoobly_agent/app/cli/scaffold/templates/workflow/test/public/.gitignore,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -203,7 +203,7 @@ stoobly_agent/app/cli/scaffold/workflow_env.py,sha256=x8V5pJmIiklD3f2q2-qq-CORf4
203
203
  stoobly_agent/app/cli/scaffold/workflow_log_command.py,sha256=Bke4lMOMxuDUFuAx9nlXHbKgYMO4KAg9ASHvjz4aVWc,1372
204
204
  stoobly_agent/app/cli/scaffold/workflow_run_command.py,sha256=eF3aaK4OIZXYuSBEAeBnhAL7EZrS1G4mSYrJbEiXt2o,11082
205
205
  stoobly_agent/app/cli/scaffold/workflow_validate_command.py,sha256=Uo_yo6rVR1ZR7xpvsQvlH48AyMBVLRupd4G-bRjzm_Q,5584
206
- stoobly_agent/app/cli/scaffold_cli.py,sha256=zrXf2nUI5jRnf6x-j3kKmia8vH_AngjpweVnjdD_R3Q,30075
206
+ stoobly_agent/app/cli/scaffold_cli.py,sha256=DFGhuCV_vugJj2o_44dypYOIwX-IY5LRWuNt3sImcfY,31946
207
207
  stoobly_agent/app/cli/scenario_cli.py,sha256=3J1EiJOvunkfWrEkOsanw-XrKkOk78ij_GjBlE9p7CE,8229
208
208
  stoobly_agent/app/cli/snapshot_cli.py,sha256=Uf6g6ivsD0hUY8G99eU0fxzS3FymncAhI70PxV7Uaac,11919
209
209
  stoobly_agent/app/cli/trace_cli.py,sha256=K7E-vx3JUcqEDSWOdIOi_AieKNQz7dBfmRrVvKDkzFI,4605
@@ -216,7 +216,7 @@ stoobly_agent/app/cli/types/snapshot_migration.py,sha256=4_Re46FKjsflcTOO3qhNsbW
216
216
  stoobly_agent/app/cli/types/test.py,sha256=1c458B7DFBWsEk5Q1CrZ2CUi84YzEzcs-W4qTcudwAk,714
217
217
  stoobly_agent/app/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
218
218
  stoobly_agent/app/models/adapters/__init__.py,sha256=cEEE--Bvrvk6DAsHx_uPgFhLnZJETP4zSBtWjMqyIKc,233
219
- stoobly_agent/app/models/adapters/joined_request_adapter.py,sha256=PUVhrZL_kYBasFByfcN78OHQ4aCeX9E7y2SLwn07_Vs,2591
219
+ stoobly_agent/app/models/adapters/joined_request_adapter.py,sha256=fSq16n3AAlxi8KJdBESHp3JGio_M9uzMnHbnQU8VI3w,3598
220
220
  stoobly_agent/app/models/adapters/mitmproxy/__init__.py,sha256=f14D_0y3_Pz6NpRXQMIwJQiWBGhwKk0Wk_AmlY3twh0,105
221
221
  stoobly_agent/app/models/adapters/mitmproxy/request/__init__.py,sha256=UZeK_-vxQKD02Tuu-YHBtpR92k5mK3XFgwmd99h2VHA,281
222
222
  stoobly_agent/app/models/adapters/mitmproxy/request/python_adapter.py,sha256=lWSppm63oOv7RDmTsUI0EmQkUWlQhRGdjtViBEDU-AA,377
@@ -258,7 +258,7 @@ stoobly_agent/app/models/factories/resource/local_db/body_adapter.py,sha256=lrnI
258
258
  stoobly_agent/app/models/factories/resource/local_db/header_adapter.py,sha256=NQdCErFtJL7sBaLpKLYfJSEA3AiaaVuU7LUcGJ-dHOI,3104
259
259
  stoobly_agent/app/models/factories/resource/local_db/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
260
260
  stoobly_agent/app/models/factories/resource/local_db/helpers/create_request_columns_service.py,sha256=HABMelW-cvhm2WXaywbQcd4PQhzSrz4vAbN7uOdetXM,1541
261
- stoobly_agent/app/models/factories/resource/local_db/helpers/log.py,sha256=T0PCrMX2s7PMwZSMdr436PzNa0QQTCBDBshosgZ24BI,10899
261
+ stoobly_agent/app/models/factories/resource/local_db/helpers/log.py,sha256=COKyrRFZgxQgLVH6cs_Mvc616BScoldamg8QqG8JV3c,11670
262
262
  stoobly_agent/app/models/factories/resource/local_db/helpers/log_event.py,sha256=30wWhqibTWgnpibosclaB9OCWzArL7R4krGS8WW2bGY,3584
263
263
  stoobly_agent/app/models/factories/resource/local_db/helpers/request_builder.py,sha256=PyVvsYmi5bBQ6PsUPxQ4nJM9rhhjGVOTd7ipuc2tMMM,3227
264
264
  stoobly_agent/app/models/factories/resource/local_db/helpers/request_snapshot.py,sha256=Tpuu7sZ4A2Vc5e5OAyU9pSKzbOpLpZoI-4E2Ty7n4Ac,2672
@@ -287,7 +287,7 @@ stoobly_agent/app/models/factories/resource/stoobly/request_adapter.py,sha256=Zr
287
287
  stoobly_agent/app/models/factories/resource/stoobly/scenario_adapter.py,sha256=HnM4g5Qdv16QXj8u4JCiJm2Dbw9OhAxmn9e_R8oaHG4,1105
288
288
  stoobly_agent/app/models/header_model.py,sha256=m91upRZr8GfE5um0d5dguUESKigBMWhSyu_X3HFk28Y,1406
289
289
  stoobly_agent/app/models/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
290
- stoobly_agent/app/models/helpers/apply.py,sha256=ev8QmDvV7flFo82JUA6kBkofYCJ_lNXQGVIpknB94g8,8501
290
+ stoobly_agent/app/models/helpers/apply.py,sha256=VWHZnZ0BnpyyO1MU6eyohimn1HGQClkDQuvYk51BttA,8491
291
291
  stoobly_agent/app/models/helpers/create_request_params_service.py,sha256=o_VB2FsTGBX55UBBw1yQ8MtsJ4-0YXcxd4kNQ9l21nM,2070
292
292
  stoobly_agent/app/models/model.py,sha256=77ZTByQmH5sWBcSrCF3kG_C4muHggcFyH1DWsOhIgvg,1180
293
293
  stoobly_agent/app/models/query_param_model.py,sha256=EBj76phSJ9_45KgP0vIZGbkkG6-tSn_U1fNW_7qLy_4,1455
@@ -309,7 +309,7 @@ stoobly_agent/app/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
309
309
  stoobly_agent/app/proxy/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
310
310
  stoobly_agent/app/proxy/constants/custom_response_codes.py,sha256=1CaApt_6W7GrxvN8_Ozbf_SEodVEQaNZRR2sMYpI0U8,40
311
311
  stoobly_agent/app/proxy/context.py,sha256=m6iu6QSZsim8meZS8H8DsZeXyjfoC-MRMHhQD1MFKM0,532
312
- stoobly_agent/app/proxy/handle_mock_service.py,sha256=HszD77W3X1UIz0DvBtbRUfuWSgc8-GS6xvWq4VDL-4w,9216
312
+ stoobly_agent/app/proxy/handle_mock_service.py,sha256=qz7rxAg6R8U9aiXbYjuncDkx5eliPYX3x-uVLelwSr0,9451
313
313
  stoobly_agent/app/proxy/handle_record_service.py,sha256=Fe8RAcMVvHhl3-XgjZ41242p4JXooYHQ-MhjcQhJn2E,4223
314
314
  stoobly_agent/app/proxy/handle_replay_service.py,sha256=kBUYd8kj8UPrsYHoBXmq06DPLjCnHo8K-QsKzhDIKcw,2526
315
315
  stoobly_agent/app/proxy/handle_test_service.py,sha256=WkMPbM4argVtl-TQB7VdQIvB8cOwURAahxFX5Vkqwws,8405
@@ -327,7 +327,7 @@ stoobly_agent/app/proxy/mitmproxy/response_facade.py,sha256=0wCSzUULUhDDV93QXUgz
327
327
  stoobly_agent/app/proxy/mock/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
328
328
  stoobly_agent/app/proxy/mock/context.py,sha256=vDo5_3WBL73mVFnsmQWvcxvPg5nWtRJbigSrE3zGc-o,794
329
329
  stoobly_agent/app/proxy/mock/custom_not_found_response_builder.py,sha256=0KWB3KFxVrnJOKDaYxm5eoJEccw7IpJZRyUvBX61-8k,697
330
- stoobly_agent/app/proxy/mock/eval_fixtures_service.py,sha256=hc4VLnN50HBaWvFnrhQUJqdH-r3jBu9DHrpt8gbvkHY,3847
330
+ stoobly_agent/app/proxy/mock/eval_fixtures_service.py,sha256=sVw8l916ouEBj2ViDtHFjgmEA4tLmZGvBfw8wD1Ab-c,4132
331
331
  stoobly_agent/app/proxy/mock/eval_request_service.py,sha256=A1tcE3wmrC1HwLpz0aRuRw-Nucn0dyHD_yHw5BeQEJU,8146
332
332
  stoobly_agent/app/proxy/mock/hashed_request_decorator.py,sha256=h1ma90fdaYI9LBWpMWMqWBz-RjNwI628O4VuS_uUBX4,5061
333
333
  stoobly_agent/app/proxy/mock/ignored_components_response_builder.py,sha256=E32_E1eSdmPn2SeM_e1jWnqu4xh5w_SnmOs32Shx99E,501
@@ -419,7 +419,7 @@ stoobly_agent/cli.py,sha256=sw8Ke5mCvzQ50X-zsb2Ld_zW4T6S58P0fN5GyKNOrcQ,10255
419
419
  stoobly_agent/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
420
420
  stoobly_agent/config/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
421
421
  stoobly_agent/config/constants/alias_resolve_strategy.py,sha256=_R1tVqFnyGxCraVS5-dhSskaDj_X8-NthsY7i_bEt9M,119
422
- stoobly_agent/config/constants/custom_headers.py,sha256=9PhSGOsvoyi-w3sxfYJGl9mdTqtOGaog7fMuHrdu-20,1353
422
+ stoobly_agent/config/constants/custom_headers.py,sha256=8YiKDjrpe0qH8tr4qM_CgonBy6bf_HHTqQ7B_hYv2L0,1398
423
423
  stoobly_agent/config/constants/env_vars.py,sha256=HAR_ZIdXXbpWQgCDaRR5RtpVyGXCsMLr_Fh8n6S12K0,1344
424
424
  stoobly_agent/config/constants/headers.py,sha256=Hfv7R8_NPXAGaMiZPqywGZDnr0qcVUyfenPb4g465rE,169
425
425
  stoobly_agent/config/constants/intercept_policy.py,sha256=5hIgOft8PQmCRdOHb5OEvEj10tU66DIQF3GYltlWyM8,25
@@ -488,7 +488,7 @@ stoobly_agent/lib/api/keys/request_key.py,sha256=68hN0KLdf0FwxSCSjeJZ1uUs526PCbN
488
488
  stoobly_agent/lib/api/keys/resource_key.py,sha256=9qrkp3t8iJsScZvKVcBDvVbcfwmP0yEBWy3b1tHi8CM,689
489
489
  stoobly_agent/lib/api/keys/scenario_key.py,sha256=VAc6gayvJS7shWgDL3SAqVET3fmgBefcygXTsKot07U,629
490
490
  stoobly_agent/lib/api/keys/test_key.py,sha256=-MCWp1oYLkJ3S_Pqs62j8KkkssXpT9quKb4YqMSq1Ks,438
491
- stoobly_agent/lib/api/keys/uuid_key.py,sha256=7_aL5wVTKF68bESHvqeQ2RUeC-Fw9-zpSrl8EWuFTJw,603
491
+ stoobly_agent/lib/api/keys/uuid_key.py,sha256=30ZJIEAQHsR1KCsmeALxsKwCaUQtm7pKvFUhLmk0J4I,643
492
492
  stoobly_agent/lib/api/param_builder.py,sha256=50eq0zkqRz5MVezPZMIu69fYSB6z4CD5FG26_IU3eq0,996
493
493
  stoobly_agent/lib/api/projects_resource.py,sha256=7leBuQnlpgUwbYUX445lUC3jy-dzLMpTumeNPrqRc5c,1166
494
494
  stoobly_agent/lib/api/query_param_names_resource.py,sha256=k-3nlyfCOa_Vwj-TMI7v8_4g9qAs50baP073gkStAKE,1710
@@ -675,7 +675,7 @@ stoobly_agent/test/app/cli/request/request_list_test.py,sha256=tqMjkODU2i2eDvYyk
675
675
  stoobly_agent/test/app/cli/request/request_replay_test.py,sha256=w13NzkXhmBKvoogpQ8CdmBbCzoyqxEQvBZsEkGDCPx0,6237
676
676
  stoobly_agent/test/app/cli/request/request_reset_test.py,sha256=5My6Z452eideAOUun77tWUkuu3_yGhDVxCG1baUF8Zo,1290
677
677
  stoobly_agent/test/app/cli/request/request_response_test.py,sha256=Fu-A8tIn016DKme4WIaPzo3YeFY-CPtTOpaSFigUVVM,1263
678
- stoobly_agent/test/app/cli/request/request_snapshot_test.py,sha256=0013aoiMZin-2YEtHzEmQspAPA3SUd_6XiItbX0U7Ok,4425
678
+ stoobly_agent/test/app/cli/request/request_snapshot_test.py,sha256=3kMmv0CuvnMXLgDQA-_u9S1DIiNOdL63L-IptVuOpf8,6308
679
679
  stoobly_agent/test/app/cli/request/request_test_test.py,sha256=-cJNXKjgryVVfVt-7IN5fIhBwe3NjFoPmeavDH8lAjU,5527
680
680
  stoobly_agent/test/app/cli/scaffold/cli_invoker.py,sha256=_nGDLUsYxqkeqs5DdhvAeXy3IuotpgqKHXKVzu6GDF4,3700
681
681
  stoobly_agent/test/app/cli/scaffold/cli_test.py,sha256=sMNvO845MIu5DVGa1HmwXQDmKDcwrfNTdEb3fK5886w,4557
@@ -688,11 +688,12 @@ stoobly_agent/test/app/cli/scenario/scenario_reset_test.py,sha256=QOyytOoFu1FALn
688
688
  stoobly_agent/test/app/cli/scenario/scenario_snapshot_test.py,sha256=IAe1l69Smd8a9E6I0CVs8lgqnC4mI4M1EFfUjC4ZHs8,3396
689
689
  stoobly_agent/test/app/cli/scenario/scenario_test_integration_test.py,sha256=BdXGe1xfb79tCCTKtp4sqI6CkZL_KamvQKLK8Wwb4u0,5543
690
690
  stoobly_agent/test/app/cli/snapshot/lifecycle_hooks_migrate.py,sha256=x3x5vHfZ1xmoOpZ0yRSB3kbWbPjfbLk2PJFSa1xOLoU,316
691
- stoobly_agent/test/app/cli/snapshot/snapshot_apply_test.py,sha256=YIcaCxJV8CcTnCX08vq9HQyuyYf6YIwVVA3Hhx_RG2A,27752
691
+ stoobly_agent/test/app/cli/snapshot/snapshot_apply_test.py,sha256=mpkTZx8eaFFZU_RKHPcphaNl6zKCIOTJ2i4kY9BZRgQ,32395
692
692
  stoobly_agent/test/app/cli/snapshot/snapshot_copy_test.py,sha256=Yg78-FhSiG_r6Jpm-sN8sn0LjVXTwTOXt6hg8ni2GIY,1953
693
693
  stoobly_agent/test/app/cli/snapshot/snapshot_migrate_test.py,sha256=voEvblK6CMGCrSJDTHVmkUkLXj0auNb78jxlGiiBBQQ,7370
694
- stoobly_agent/test/app/cli/snapshot/snapshot_prune_test.py,sha256=G3vrXQRMoDedNpAuKgDivNMqLK-5xsCAAl80xsgeVaU,6690
694
+ stoobly_agent/test/app/cli/snapshot/snapshot_prune_test.py,sha256=bn4yUU7Eb4-6GnwnRaPZPi5Cn7XEaIsrJ_mB7jydgWw,6693
695
695
  stoobly_agent/test/app/cli/snapshot/snapshot_update_test.py,sha256=fILsX2M5j4wuLRP6LJTHe4CPB8gvaEbsSoYmFCHmKVk,4514
696
+ stoobly_agent/test/app/models/adapters/joined_rquest_adapter_test.py,sha256=bF7WMrAiASQDNzDTvIXGJhsWLNhfYOmdQpSDo0hyWYY,1098
696
697
  stoobly_agent/test/app/models/adapters/orm/joined_request_string_adapter_test.py,sha256=a2IHTk3l7aiLyYF7vtqissrk0MFTF2wlUBiaKWyJKfU,2667
697
698
  stoobly_agent/test/app/models/adapters/orm/request/orm_mitmproxy_request_adapter_test.py,sha256=PbJsAaxPUEbF9vM7DX4z858biWf4qlGnvE8KBuy8SgY,2763
698
699
  stoobly_agent/test/app/models/adapters/orm/request/orm_python_request_adapter_test.py,sha256=1rHywokXUj7z3laHhfnei8j1GVmAHDOULvRWmtIyWuQ,2488
@@ -707,7 +708,7 @@ stoobly_agent/test/app/models/factories/resource/local_db/helpers/log_test.py,sh
707
708
  stoobly_agent/test/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request_test.py,sha256=a1SFLyEyRRLuADvAw6ckQQKORFXvyK1lyrbkaLWx8oU,3399
708
709
  stoobly_agent/test/app/models/factories/resource/local_db/request_adapter_test.py,sha256=Pzq1cBPnP9oSWG-p0c-VoymoHxgp483QmNwmV1b78RA,8453
709
710
  stoobly_agent/test/app/models/factories/resource/local_db/response_adapter_test.py,sha256=9P95EKH5rZGOrmRkRIDlQZqtiLJHk9735og18Ffwpfw,2204
710
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION,sha256=dxfL-Qxjo7CWAIdjoCBmI5kW-pkW-YJU0ao7JHCDD80,5
711
+ stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION,sha256=JyvJHgCQJn-THkr7jL9H-s1Ea2VWcQz-X3BiHK64uxk,6
711
712
  stoobly_agent/test/app/models/schemas/.stoobly/db/stoobly_agent.sqlite3,sha256=ch8gNx6zIelLKQx65gwFx_LRNqUD3EC5xcHZ0ukIQiU,188416
712
713
  stoobly_agent/test/app/models/schemas/.stoobly/settings.yml,sha256=vLwMjweKOdod6tSLtIlyBefPQuNXq9wio4kBaODKtAU,726
713
714
  stoobly_agent/test/app/models/schemas/.stoobly/tmp/options.json,sha256=OTRzarwus48CTrItedXCrgQttJHSEZonEYc7R_knvYg,2212
@@ -748,8 +749,8 @@ stoobly_agent/test/mock_data/scaffold/docker-compose-local-service.yml,sha256=1W
748
749
  stoobly_agent/test/mock_data/scaffold/index.html,sha256=qJwuYajKZ4ihWZrJQ3BNObV5kf1VGnnm_vqlPJzdqLE,258
749
750
  stoobly_agent/test/mock_data/uspto.yaml,sha256=6U5se7C3o-86J4m9xpOk9Npias399f5CbfWzR87WKwE,7835
750
751
  stoobly_agent/test/test_helper.py,sha256=m_oAI7tmRYCNZdKfNqISWhMv3e44tjeYViQ3nTUfnos,1007
751
- stoobly_agent-1.9.1.dist-info/LICENSE,sha256=o93sj12cdoEOsTCjPaPFsw3Xq0SXs3pPcY-9reE2sEw,548
752
- stoobly_agent-1.9.1.dist-info/METADATA,sha256=wlI-HxrR6hqRTUREg2Oo4NJNTsLXe7NPKrnO4mHThs0,3087
753
- stoobly_agent-1.9.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
754
- stoobly_agent-1.9.1.dist-info/entry_points.txt,sha256=aq5wix5oC8MDQtmyPGU0xaFrsjJg7WH28NmXh2sc3Z8,56
755
- stoobly_agent-1.9.1.dist-info/RECORD,,
752
+ stoobly_agent-1.9.3.dist-info/LICENSE,sha256=o93sj12cdoEOsTCjPaPFsw3Xq0SXs3pPcY-9reE2sEw,548
753
+ stoobly_agent-1.9.3.dist-info/METADATA,sha256=2T7T6OEFqeOhMH4V-af3VMftzxq-mIRn-nOcFd6-Crc,3087
754
+ stoobly_agent-1.9.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
755
+ stoobly_agent-1.9.3.dist-info/entry_points.txt,sha256=aq5wix5oC8MDQtmyPGU0xaFrsjJg7WH28NmXh2sc3Z8,56
756
+ stoobly_agent-1.9.3.dist-info/RECORD,,