stoobly-agent 1.10.2__py3-none-any.whl → 1.10.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.10.2'
2
+ VERSION = '1.10.3'
@@ -134,7 +134,7 @@ class ApplicationHTTPRequestHandler(SimpleHTTPRequestHandler):
134
134
  for endpoint_handler in ROUTES[method]:
135
135
  path = endpoint_handler[0]
136
136
 
137
- matches = self.path == path if isinstance(path, str) else bool(re.match(path, self.path))
137
+ matches = self.path == path if isinstance(path, str) else bool(re.fullmatch(path, self.path))
138
138
 
139
139
  if matches:
140
140
  handler = endpoint_handler[1]
@@ -126,7 +126,7 @@ def validate_aliases(validations, **kwargs) -> Union[Alias, None]:
126
126
  handle_missing_alias(parsed_validation, kwargs.get('format'))
127
127
 
128
128
  if parsed_validation['value'] != None:
129
- if not re.match(parsed_validation['value'], aliases_map[name]):
129
+ if not re.fullmatch(parsed_validation['value'], aliases_map[name]):
130
130
  handle_invalid_alias(parsed_validation, aliases_map[name], kwargs.get('format'))
131
131
 
132
132
  def filter_response(res, status: int):
@@ -111,7 +111,7 @@ class App():
111
111
 
112
112
  # Skip files that match the ignore list pattern, use regex
113
113
  for ignore_pattern in ignore:
114
- if re.match(os.path.join(src, ignore_pattern), src_file_path):
114
+ if re.fullmatch(os.path.join(src, ignore_pattern), src_file_path):
115
115
  ignored = True
116
116
  break
117
117
 
@@ -37,7 +37,7 @@ class DockerImage:
37
37
  (?::(?P<tag>[^@]+))? # optional :tag
38
38
  (?:@(?P<digest>.+))? # optional @digest
39
39
  """
40
- match = re.match(pattern, ref, re.VERBOSE)
40
+ match = re.fullmatch(pattern, ref, re.VERBOSE)
41
41
  if not match:
42
42
  raise ValueError(f"Invalid Docker image reference: {ref}")
43
43
 
@@ -4,6 +4,6 @@ services:
4
4
  service: stoobly_base
5
5
  volumes:
6
6
  - ${CONTEXT_DIR}/.stoobly:/home/stoobly/.stoobly
7
- - ${APP_DIR}/.stoobly/docker:/home/stoobly/.stoobly/docker
7
+ - ${APP_DIR}/.stoobly/services:/home/stoobly/.stoobly/services
8
8
  stoobly_base:
9
9
  image: stoobly.${USER_ID}
@@ -170,7 +170,7 @@ class MitmproxyRequestFacade(Request):
170
170
  pattern = rewrite_rule.pattern
171
171
 
172
172
  try:
173
- url_matches = re.match(pattern, self.url)
173
+ url_matches = re.fullmatch(pattern, self.url)
174
174
  except re.error as e:
175
175
  Logger.instance().error(f"RegExp error '{e}' for {pattern}")
176
176
  return False
@@ -58,12 +58,9 @@ def eval_fixtures(request: MitmproxyRequest, **options: MockOptions) -> Union[Re
58
58
  continue
59
59
 
60
60
  # Try to find the file in this directory
61
- _fixture_path = os.path.join(dir_path_config['path'], request_path.lstrip('/'))
61
+ fixture_path = os.path.join(dir_path_config['path'], request_path.lstrip('/'))
62
62
  if request.headers.get('accept'):
63
- fixture_path = __guess_file_path(_fixture_path, request.headers['accept'])
64
-
65
- if not fixture_path:
66
- fixture_path = _fixture_path
63
+ fixture_path = __guess_file_path(fixture_path, request.headers['accept'])
67
64
 
68
65
  if os.path.isfile(fixture_path):
69
66
  break
@@ -74,15 +71,31 @@ def eval_fixtures(request: MitmproxyRequest, **options: MockOptions) -> Union[Re
74
71
  return
75
72
  else:
76
73
  fixture_path = fixture.get('path')
77
- if not fixture_path or not os.path.isfile(fixture_path):
74
+ if not fixture_path:
78
75
  return
79
76
 
77
+ if os.path.isdir(fixture_path):
78
+ request_path = request.path
79
+ match = re.match(fixture.get('path_pattern', request_path), request_path)
80
+
81
+ if not match or match.end() == len(request_path):
82
+ sub_path = 'index'
83
+ else:
84
+ sub_path = request_path[match.end():]
85
+
86
+ fixture_path = os.path.join(fixture_path, sub_path.lstrip('/'))
87
+ if request.headers.get('accept'):
88
+ fixture_path = __guess_file_path(fixture_path, request.headers['accept'])
89
+
90
+ if not os.path.isfile(fixture_path):
91
+ return
92
+
80
93
  _headers = fixture.get('headers')
81
94
  headers = CaseInsensitiveDict(_headers if isinstance(_headers, dict) else {})
82
95
 
83
96
  if fixture.get('status_code'):
84
97
  status_code = fixture.get('status_code')
85
-
98
+
86
99
  with open(fixture_path, 'rb') as fp:
87
100
  response = Response()
88
101
 
@@ -158,13 +171,13 @@ def __guess_content_type(file_path):
158
171
  return
159
172
  return mimetypes.types_map.get(file_extension)
160
173
 
161
- def __guess_file_path(file_path, content_type):
174
+ def __guess_file_path(file_path: str, content_type):
162
175
  file_extension = os.path.splitext(file_path)[1]
163
176
  if file_extension:
164
177
  return file_path
165
178
 
166
179
  if not content_type:
167
- return
180
+ return file_path
168
181
 
169
182
  content_types = __parse_accept_header(content_type)
170
183
 
@@ -178,6 +191,8 @@ def __guess_file_path(file_path, content_type):
178
191
  if os.path.isfile(_file_path):
179
192
  return _file_path
180
193
 
194
+ return file_path
195
+
181
196
  def __find_fixture_for_request(request: MitmproxyRequest, fixtures: dict, method: str):
182
197
  """Find a fixture for the given request in the provided fixtures."""
183
198
  if not fixtures:
@@ -193,7 +208,7 @@ def __find_fixture_in_routes(fixtures: dict, method: str, request_path: str):
193
208
  return None
194
209
 
195
210
  for path_pattern in routes:
196
- if not re.match(path_pattern, request_path):
211
+ if not re.fullmatch(path_pattern, request_path):
197
212
  continue
198
213
 
199
214
  fixture = routes[path_pattern]
@@ -203,6 +218,7 @@ def __find_fixture_in_routes(fixtures: dict, method: str, request_path: str):
203
218
  path = fixture.get('path')
204
219
 
205
220
  if path:
221
+ fixture['path_pattern'] = path_pattern
206
222
  return fixture
207
223
 
208
224
  return None
@@ -211,6 +227,9 @@ def __choose_highest_priority_content_type(accept_header: str) -> Optional[str]:
211
227
  if not accept_header:
212
228
  return None
213
229
 
230
+ if accept_header == '*/*':
231
+ return 'text/plain'
232
+
214
233
  types = []
215
234
  for part in accept_header.split(","):
216
235
  media_range = part.strip()
@@ -234,9 +253,13 @@ def __choose_highest_priority_content_type(accept_header: str) -> Optional[str]:
234
253
  return types[0][0] if types else None
235
254
 
236
255
  def __origin_matches(pattern: str, request_origin: str) -> bool:
237
- return bool(re.match(pattern, request_origin))
256
+ return bool(re.fullmatch(pattern, request_origin))
238
257
 
239
258
  def __parse_accept_header(accept_header):
259
+ # In the case accept_header is */*, default to html and json file types
260
+ if accept_header == '*/*':
261
+ return ['text/html', 'application/json']
262
+
240
263
  types = []
241
264
  for item in accept_header.split(","):
242
265
  parts = item.split(";")
@@ -202,7 +202,7 @@ def __filter_by_match_rules(request: MitmproxyRequest, match_rules: List[MatchRu
202
202
 
203
203
  def __matches(url: str, pattern: str):
204
204
  try:
205
- return re.match(pattern, url)
205
+ return re.fullmatch(pattern, url)
206
206
  except re.error as e:
207
207
  Logger.instance().error(f"RegExp error '{e}' for {pattern}")
208
208
  return False
@@ -80,7 +80,7 @@ def __include(request: MitmproxyRequest, patterns: List[str]) -> bool:
80
80
 
81
81
  for pattern in patterns:
82
82
  try:
83
- if re.match(pattern, request.url):
83
+ if re.fullmatch(pattern, request.url):
84
84
  return True
85
85
  except re.error as e:
86
86
  Logger.instance(LOG_ID).error(f"RegExp error '{e}' for {pattern}")
@@ -94,7 +94,7 @@ def __exclude(request: MitmproxyRequest, patterns: List[str]) -> bool:
94
94
 
95
95
  for pattern in patterns:
96
96
  try:
97
- if re.match(pattern, request.url):
97
+ if re.fullmatch(pattern, request.url):
98
98
  return True
99
99
  except re.error as e:
100
100
  Logger.instance(LOG_ID).error(f"RegExp error '{e}' for {pattern}")
@@ -32,7 +32,7 @@ class Cache:
32
32
  else:
33
33
  delete_list = []
34
34
  for key in self.data.keys():
35
- if re.match(pattern, key):
35
+ if re.fullmatch(pattern, key):
36
36
  delete_list.append(key)
37
37
 
38
38
  for key in delete_list:
@@ -1 +1 @@
1
- 1.10.1
1
+ 1.10.2
@@ -27,6 +27,24 @@ def mitmproxy_get_request():
27
27
  now + 1,
28
28
  )
29
29
 
30
+ @pytest.fixture
31
+ def mitmproxy_get_request_details():
32
+ now = time()
33
+ return MitmproxyRequest(
34
+ 'stoobly.com',
35
+ 443,
36
+ b'GET',
37
+ b'https',
38
+ b'stoobly.com:443',
39
+ b'/requests/1?project_id=1',
40
+ b'HTTP/1.1',
41
+ Headers(**{'access-token': b'123'}),
42
+ b'',
43
+ Headers(), # Trailers
44
+ now,
45
+ now + 1,
46
+ )
47
+
30
48
  @pytest.fixture
31
49
  def mitmproxy_post_request():
32
50
  now = time()
@@ -45,6 +63,28 @@ def mitmproxy_post_request():
45
63
  now + 1,
46
64
  )
47
65
 
66
+ class TestDoesNotRewrite():
67
+
68
+ def test_does_not_rewrite_header(self, mitmproxy_get_request_details: MitmproxyRequest):
69
+ parameter_rule = {
70
+ 'modes': [intercept_mode.RECORD],
71
+ 'name': 'access-token',
72
+ 'type': request_component.HEADER,
73
+ 'value': '',
74
+ }
75
+ rewrite_rule = RewriteRule({
76
+ 'methods': ['GET'],
77
+ 'pattern': '.*?/requests',
78
+ 'parameter_rules': [parameter_rule]
79
+ })
80
+
81
+ facade = MitmproxyRequestFacade(mitmproxy_get_request_details)
82
+ facade.with_parameter_rules([rewrite_rule])
83
+ facade.rewrite()
84
+
85
+ headers = facade.headers
86
+ assert headers['access-token'] != ''
87
+
48
88
  class TestRewriteParams():
49
89
 
50
90
  def test_rewrites_header(self, mitmproxy_get_request: MitmproxyRequest):
@@ -56,7 +96,7 @@ class TestRewriteParams():
56
96
  }
57
97
  rewrite_rule = RewriteRule({
58
98
  'methods': ['GET'],
59
- 'pattern': '.*?/requests',
99
+ 'pattern': '.*?/requests(?:\?.*)?',
60
100
  'parameter_rules': [parameter_rule]
61
101
  })
62
102
 
@@ -76,7 +116,7 @@ class TestRewriteParams():
76
116
  }
77
117
  rewrite_rule = RewriteRule({
78
118
  'methods': ['GET'],
79
- 'pattern': '.*?/requests',
119
+ 'pattern': '.*?/requests(?:\?.*)?',
80
120
  'parameter_rules': [parameter_rule]
81
121
  })
82
122
 
@@ -96,7 +136,7 @@ class TestRewriteParams():
96
136
  }
97
137
  rewrite_rule = RewriteRule({
98
138
  'methods': ['POST'],
99
- 'pattern': '.*?/users',
139
+ 'pattern': '.*?/users(?:\?.*)?',
100
140
  'parameter_rules': [parameter_rule]
101
141
  })
102
142
 
@@ -118,7 +158,7 @@ class TestRewriteUrl():
118
158
  }
119
159
  rewrite_rule = RewriteRule({
120
160
  'methods': ['GET'],
121
- 'pattern': '.*?/requests',
161
+ 'pattern': '.*?/requests(?:\?.*)?',
122
162
  'url_rules': [url_rule]
123
163
  })
124
164
 
@@ -139,7 +179,7 @@ class TestRewriteUrl():
139
179
  }
140
180
  rewrite_rule = RewriteRule({
141
181
  'methods': ['GET'],
142
- 'pattern': '.*?/requests',
182
+ 'pattern': '.*?/requests(?:\?.*)?',
143
183
  'url_rules': [url_rule]
144
184
  })
145
185
 
@@ -162,7 +202,7 @@ class TestRewriteUrl():
162
202
  }
163
203
  rewrite_rule = RewriteRule({
164
204
  'methods': ['GET'],
165
- 'pattern': '.*?/requests',
205
+ 'pattern': '.*?/requests(?:\?.*)?',
166
206
  'url_rules': [url_rule]
167
207
  })
168
208
 
@@ -185,7 +225,7 @@ class TestRewriteUrl():
185
225
  }
186
226
  rewrite_rule = RewriteRule({
187
227
  'methods': ['GET'],
188
- 'pattern': '.*?/requests',
228
+ 'pattern': '.*?/requests(?:\?.*)?',
189
229
  'url_rules': [url_rule]
190
230
  })
191
231
 
@@ -215,7 +255,7 @@ class TestRewriteUrl():
215
255
  }
216
256
  rewrite_rule = RewriteRule({
217
257
  'methods': ['GET'],
218
- 'pattern': '.*?/requests',
258
+ 'pattern': '.*?/requests(?:\?.*)?',
219
259
  'url_rules': [url_rule1, url_rule2]
220
260
  })
221
261
 
@@ -51,10 +51,18 @@ class TestEvalFixturesService():
51
51
 
52
52
  return Request.last()
53
53
 
54
+ @pytest.fixture(scope='class')
55
+ def default_file_contents(self):
56
+ return b'Default'
57
+
54
58
  @pytest.fixture(scope='class')
55
59
  def not_found_file_contents(self):
56
60
  return b'Not Found'
57
61
 
62
+ @pytest.fixture(scope='class')
63
+ def user_file_contents(self):
64
+ return b'{"id": 1, "name": "John Doe"}'
65
+
58
66
  @pytest.fixture(scope='class')
59
67
  def public_directory(self):
60
68
  tmp_dir_path = DataDir.instance().tmp_dir_path
@@ -64,14 +72,28 @@ class TestEvalFixturesService():
64
72
  return public_dir_path
65
73
 
66
74
  @pytest.fixture(autouse=True, scope='class')
67
- def not_found_file_path(self, public_directory: str, not_found_file_contents: str):
75
+ def not_found_file_path(self, public_directory: str, not_found_file_contents: bytes):
68
76
  path = os.path.join(public_directory, '404.html')
69
77
  with open(path, 'wb') as fp:
70
78
  fp.write(not_found_file_contents)
71
79
  return path
72
80
 
81
+ @pytest.fixture(autouse=True, scope='class')
82
+ def user_file_path(self, public_directory: str, user_file_contents: bytes):
83
+ path = os.path.join(public_directory, 'user.json')
84
+ with open(path, 'wb') as fp:
85
+ fp.write(user_file_contents)
86
+ return path
87
+
88
+ @pytest.fixture(autouse=True, scope='class')
89
+ def default_file_path(self, public_directory: str, default_file_contents: bytes):
90
+ path = os.path.join(public_directory, 'index.html')
91
+ with open(path, 'wb') as fp:
92
+ fp.write(default_file_contents)
93
+ return path
94
+
73
95
  @pytest.fixture(scope='class')
74
- def response_fixtures(self, request_method: str, not_found_file_path: str) -> Fixtures:
96
+ def response_fixtures(self, public_directory: str, request_method: str, not_found_file_path: str) -> Fixtures:
75
97
  fixtures = {}
76
98
  fixtures[request_method] = {
77
99
  '/404.html': {
@@ -80,6 +102,9 @@ class TestEvalFixturesService():
80
102
  },
81
103
  'path': not_found_file_path,
82
104
  'status_code': 404,
105
+ },
106
+ '/.*?': {
107
+ 'path': public_directory
83
108
  }
84
109
  }
85
110
  return fixtures
@@ -95,14 +120,31 @@ class TestEvalFixturesService():
95
120
  fixtures_file = os.path.join(tmp_dir_path, 'test_response_fixtures.yml')
96
121
 
97
122
  with open(fixtures_file, 'w') as f:
98
- yaml.dump(response_fixtures, f)
99
-
123
+ yaml.dump(response_fixtures, f, sort_keys=False)
124
+
100
125
  res: requests.Response = eval_fixtures(mitmproxy_request, response_fixtures_path=fixtures_file)
101
126
  assert res != None
102
127
  return res
103
128
 
129
+ @pytest.fixture()
130
+ def default_fixtures_response(self, mitmproxy_request: MitmproxyRequest, response_fixtures: Fixtures):
131
+ mitmproxy_request.path = '/'
132
+ mitmproxy_request.headers['accept'] = '*/*'
133
+
134
+ res: requests.Response = eval_fixtures(mitmproxy_request, response_fixtures=response_fixtures)
135
+ assert res != None
136
+ return res
137
+
138
+ @pytest.fixture()
139
+ def user_fixtures_response(self, mitmproxy_request: MitmproxyRequest, response_fixtures: Fixtures):
140
+ mitmproxy_request.path = '/user.json'
141
+ mitmproxy_request.headers['accept'] = 'application/json'
142
+ res: requests.Response = eval_fixtures(mitmproxy_request, response_fixtures=response_fixtures)
143
+ assert res != None
144
+ return res
145
+
104
146
  def test_it_sets_response(
105
- self, fixtures_response: requests.Response, not_found_file_contents: str
147
+ self, fixtures_response: requests.Response, not_found_file_contents: bytes
106
148
  ):
107
149
  assert fixtures_response.raw.read() == not_found_file_contents
108
150
 
@@ -113,6 +155,12 @@ class TestEvalFixturesService():
113
155
  def test_it_sets_status_code(self, fixtures_response: requests.Response):
114
156
  assert fixtures_response.status_code == 404
115
157
 
158
+ def test_fixture_directory(self, default_fixtures_response: requests.Response, default_file_contents: bytes):
159
+ assert default_fixtures_response.raw.read() == default_file_contents
160
+
161
+ def test_it_sets_user_response(self, user_fixtures_response: requests.Response, user_file_contents: bytes):
162
+ assert user_fixtures_response.raw.read() == user_file_contents
163
+
116
164
  class TestPublicDirectory():
117
165
  @pytest.fixture(scope='class')
118
166
  def request_method(self):
@@ -460,6 +508,404 @@ class TestEvalFixturesService():
460
508
  assert res is not None
461
509
  assert res.raw.read() == b'Hostname Port Content'
462
510
 
511
+ class TestErrorHandling():
512
+ """Test error handling and edge cases."""
513
+
514
+ @pytest.fixture(scope='class')
515
+ def request_method(self):
516
+ return 'GET'
517
+
518
+ @pytest.fixture(scope='class')
519
+ def request_url(self):
520
+ return 'https://example.com/test'
521
+
522
+ @pytest.fixture(scope='class')
523
+ def created_request(self, settings: Settings, request_method: str, request_url: str):
524
+ status = RequestBuilder(
525
+ method=request_method,
526
+ request_headers={'accept': 'application/json'},
527
+ response_body='',
528
+ status_code=200,
529
+ url=request_url,
530
+ ).with_settings(settings).build()[1]
531
+ assert status == 200
532
+ return Request.last()
533
+
534
+ @pytest.fixture()
535
+ def mitmproxy_request(self, created_request: Request) -> MitmproxyRequest:
536
+ return MitmproxyRequestAdapter(created_request).adapt()
537
+
538
+ def test_missing_path_pattern_in_fixture(self, mitmproxy_request: MitmproxyRequest):
539
+ """Test fixture without path_pattern key - should handle KeyError gracefully."""
540
+ # Create fixture without path_pattern
541
+ fixture_without_pattern = {
542
+ 'GET': {
543
+ '/test': {
544
+ 'path': '/tmp/nonexistent_dir' # Directory that doesn't exist
545
+ }
546
+ }
547
+ }
548
+
549
+ # This should not raise a KeyError when accessing fixture['path_pattern']
550
+ res = eval_fixtures(mitmproxy_request, response_fixtures=fixture_without_pattern)
551
+ # Should return None since directory doesn't exist
552
+ assert res is None
553
+
554
+ def test_nonexistent_fixture_file(self, mitmproxy_request: MitmproxyRequest):
555
+ """Test fixture pointing to nonexistent file."""
556
+ fixture = {
557
+ 'GET': {
558
+ '/test': {
559
+ 'path': '/tmp/nonexistent_file.json'
560
+ }
561
+ }
562
+ }
563
+
564
+ res = eval_fixtures(mitmproxy_request, response_fixtures=fixture)
565
+ assert res is None
566
+
567
+ def test_nonexistent_public_directory(self, mitmproxy_request: MitmproxyRequest):
568
+ """Test public directory that doesn't exist."""
569
+ res = eval_fixtures(mitmproxy_request, public_directory_path='/tmp/nonexistent_directory')
570
+ assert res is None
571
+
572
+ def test_invalid_yaml_in_response_fixtures_path(self, mitmproxy_request: MitmproxyRequest):
573
+ """Test handling of invalid YAML in response fixtures file."""
574
+ tmp_dir_path = DataDir.instance().tmp_dir_path
575
+ invalid_yaml_file = os.path.join(tmp_dir_path, 'invalid.yml')
576
+
577
+ # Create file with invalid YAML
578
+ with open(invalid_yaml_file, 'w') as f:
579
+ f.write('invalid: yaml: content: [')
580
+
581
+ res = eval_fixtures(mitmproxy_request, response_fixtures_path=invalid_yaml_file)
582
+ assert res is None
583
+
584
+ def test_empty_response_fixtures(self, mitmproxy_request: MitmproxyRequest):
585
+ """Test with empty response fixtures."""
586
+ res = eval_fixtures(mitmproxy_request, response_fixtures={})
587
+ assert res is None
588
+
589
+ def test_none_response_fixtures(self, mitmproxy_request: MitmproxyRequest):
590
+ """Test with None response fixtures."""
591
+ res = eval_fixtures(mitmproxy_request, response_fixtures=None)
592
+ assert res is None
593
+
594
+ def test_malformed_fixture_structure(self, mitmproxy_request: MitmproxyRequest):
595
+ """Test fixture with malformed structure."""
596
+ malformed_fixture = {
597
+ 'GET': 'not_a_dict' # Should be a dict
598
+ }
599
+
600
+ res = eval_fixtures(mitmproxy_request, response_fixtures=malformed_fixture)
601
+ assert res is None
602
+
603
+ def test_fixture_without_path(self, mitmproxy_request: MitmproxyRequest):
604
+ """Test fixture without path key."""
605
+ fixture_without_path = {
606
+ 'GET': {
607
+ '/test': {
608
+ 'status_code': 200
609
+ # Missing 'path' key
610
+ }
611
+ }
612
+ }
613
+
614
+ res = eval_fixtures(mitmproxy_request, response_fixtures=fixture_without_path)
615
+ assert res is None
616
+
617
+ def test_directory_fixture_without_matching_subpath(self, mitmproxy_request: MitmproxyRequest):
618
+ """Test directory fixture when no matching subpath file exists."""
619
+ tmp_dir_path = DataDir.instance().tmp_dir_path
620
+ test_dir = os.path.join(tmp_dir_path, 'test_dir')
621
+ os.makedirs(test_dir, exist_ok=True)
622
+
623
+ # Don't create any files in the directory
624
+
625
+ fixture = {
626
+ 'GET': {
627
+ '/test': {
628
+ 'path': test_dir
629
+ }
630
+ }
631
+ }
632
+
633
+ res = eval_fixtures(mitmproxy_request, response_fixtures=fixture)
634
+ assert res is None
635
+
636
+ class TestCustomHeaders():
637
+ """Test custom header handling."""
638
+
639
+ @pytest.fixture(scope='class')
640
+ def request_method(self):
641
+ return 'GET'
642
+
643
+ @pytest.fixture(scope='class')
644
+ def request_url(self):
645
+ return 'https://example.com/test'
646
+
647
+ @pytest.fixture(scope='class')
648
+ def test_file_contents(self):
649
+ return b'Test content'
650
+
651
+ @pytest.fixture(scope='class')
652
+ def test_file_path(self, test_file_contents: bytes):
653
+ tmp_dir_path = DataDir.instance().tmp_dir_path
654
+ file_path = os.path.join(tmp_dir_path, 'custom_headers_test.txt')
655
+ with open(file_path, 'wb') as f:
656
+ f.write(test_file_contents)
657
+ return file_path
658
+
659
+ @pytest.fixture(scope='class')
660
+ def created_request(self, settings: Settings, request_method: str, request_url: str):
661
+ from stoobly_agent.config.constants.custom_headers import MOCK_FIXTURE_PATH
662
+ status = RequestBuilder(
663
+ method=request_method,
664
+ request_headers={MOCK_FIXTURE_PATH: '/custom/path'},
665
+ response_body='',
666
+ status_code=200,
667
+ url=request_url,
668
+ ).with_settings(settings).build()[1]
669
+ assert status == 200
670
+ return Request.last()
671
+
672
+ @pytest.fixture()
673
+ def mitmproxy_request(self, created_request: Request, test_file_path: str) -> MitmproxyRequest:
674
+ from stoobly_agent.config.constants.custom_headers import MOCK_FIXTURE_PATH
675
+ request = MitmproxyRequestAdapter(created_request).adapt()
676
+ # Override the custom header to point to our test file
677
+ request.headers[MOCK_FIXTURE_PATH] = test_file_path
678
+ return request
679
+
680
+ def test_custom_fixture_path_header(self, mitmproxy_request: MitmproxyRequest, test_file_contents: bytes):
681
+ """Test fixture path specified via custom header."""
682
+ res = eval_fixtures(mitmproxy_request)
683
+ assert res is not None
684
+ assert res.raw.read() == test_file_contents
685
+
686
+ def test_custom_fixture_path_header_nonexistent_file(self, mitmproxy_request: MitmproxyRequest):
687
+ """Test custom fixture path header pointing to nonexistent file."""
688
+ from stoobly_agent.config.constants.custom_headers import MOCK_FIXTURE_PATH
689
+ mitmproxy_request.headers[MOCK_FIXTURE_PATH] = '/tmp/nonexistent_file.txt'
690
+
691
+ res = eval_fixtures(mitmproxy_request)
692
+ assert res is None
693
+
694
+ class TestContentTypeGuessing():
695
+ """Test content type guessing and file extension handling."""
696
+
697
+ @pytest.fixture(scope='class')
698
+ def request_method(self):
699
+ return 'GET'
700
+
701
+ @pytest.fixture(scope='class')
702
+ def request_url(self):
703
+ return 'https://example.com/api/data'
704
+
705
+ @pytest.fixture(scope='class')
706
+ def created_request(self, settings: Settings, request_method: str, request_url: str):
707
+ status = RequestBuilder(
708
+ method=request_method,
709
+ request_headers={'accept': 'application/json,text/html;q=0.9,*/*;q=0.8'},
710
+ response_body='',
711
+ status_code=200,
712
+ url=request_url,
713
+ ).with_settings(settings).build()[1]
714
+ assert status == 200
715
+ return Request.last()
716
+
717
+ @pytest.fixture()
718
+ def mitmproxy_request(self, created_request: Request) -> MitmproxyRequest:
719
+ return MitmproxyRequestAdapter(created_request).adapt()
720
+
721
+ def test_json_file_content_type(self, mitmproxy_request: MitmproxyRequest):
722
+ """Test that JSON files get correct content type."""
723
+ tmp_dir_path = DataDir.instance().tmp_dir_path
724
+ json_file = os.path.join(tmp_dir_path, 'test.json')
725
+
726
+ with open(json_file, 'w') as f:
727
+ json.dump({'test': 'data'}, f)
728
+
729
+ fixture = {
730
+ 'GET': {
731
+ '/api/data': {
732
+ 'path': json_file
733
+ }
734
+ }
735
+ }
736
+
737
+ res = eval_fixtures(mitmproxy_request, response_fixtures=fixture)
738
+ assert res is not None
739
+ assert res.headers.get('content-type') == 'application/json'
740
+
741
+ def test_html_file_content_type(self, mitmproxy_request: MitmproxyRequest):
742
+ """Test that HTML files get correct content type."""
743
+ tmp_dir_path = DataDir.instance().tmp_dir_path
744
+ html_file = os.path.join(tmp_dir_path, 'test.html')
745
+
746
+ with open(html_file, 'w') as f:
747
+ f.write('<html><body>Test</body></html>')
748
+
749
+ fixture = {
750
+ 'GET': {
751
+ '/api/data': {
752
+ 'path': html_file
753
+ }
754
+ }
755
+ }
756
+
757
+ res = eval_fixtures(mitmproxy_request, response_fixtures=fixture)
758
+ assert res is not None
759
+ assert res.headers.get('content-type') == 'text/html'
760
+
761
+ def test_unknown_extension_uses_accept_header(self, mitmproxy_request: MitmproxyRequest):
762
+ """Test that files with unknown extensions use accept header for content type."""
763
+ tmp_dir_path = DataDir.instance().tmp_dir_path
764
+ unknown_file = os.path.join(tmp_dir_path, 'test.unknown')
765
+
766
+ with open(unknown_file, 'w') as f:
767
+ f.write('test content')
768
+
769
+ fixture = {
770
+ 'GET': {
771
+ '/api/data': {
772
+ 'path': unknown_file
773
+ }
774
+ }
775
+ }
776
+
777
+ res = eval_fixtures(mitmproxy_request, response_fixtures=fixture)
778
+ assert res is not None
779
+ # Should use highest priority from accept header (application/json)
780
+ assert res.headers.get('content-type') == 'application/json'
781
+
782
+ def test_wildcard_accept_header_defaults(self, mitmproxy_request: MitmproxyRequest):
783
+ """Test that wildcard accept header gets reasonable defaults."""
784
+ tmp_dir_path = DataDir.instance().tmp_dir_path
785
+ test_file = os.path.join(tmp_dir_path, 'test_wildcard')
786
+
787
+ with open(test_file, 'w') as f:
788
+ f.write('test content')
789
+
790
+ # Set accept header to */*
791
+ mitmproxy_request.headers['accept'] = '*/*'
792
+
793
+ fixture = {
794
+ 'GET': {
795
+ '/api/data': {
796
+ 'path': test_file
797
+ }
798
+ }
799
+ }
800
+
801
+ res = eval_fixtures(mitmproxy_request, response_fixtures=fixture)
802
+ assert res is not None
803
+ # Should default to text/html based on the wildcard handling
804
+ assert res.headers.get('content-type') == 'text/plain'
805
+
806
+ class TestRegexPatternMatching():
807
+ """Test regex pattern matching for fixture routes."""
808
+
809
+ @pytest.fixture(scope='class')
810
+ def request_method(self):
811
+ return 'GET'
812
+
813
+ @pytest.fixture(scope='class')
814
+ def request_url(self):
815
+ return 'https://example.com/api/users/123'
816
+
817
+ @pytest.fixture(scope='class')
818
+ def test_file_contents(self):
819
+ return b'User 123 data'
820
+
821
+ @pytest.fixture(scope='class')
822
+ def test_file_path(self, test_file_contents: bytes):
823
+ tmp_dir_path = DataDir.instance().tmp_dir_path
824
+ file_path = os.path.join(tmp_dir_path, 'user_data.json')
825
+ with open(file_path, 'wb') as f:
826
+ f.write(test_file_contents)
827
+ return file_path
828
+
829
+ @pytest.fixture(scope='class')
830
+ def created_request(self, settings: Settings, request_method: str, request_url: str):
831
+ status = RequestBuilder(
832
+ method=request_method,
833
+ request_headers={'accept': 'application/json'},
834
+ response_body='',
835
+ status_code=200,
836
+ url=request_url,
837
+ ).with_settings(settings).build()[1]
838
+ assert status == 200
839
+ return Request.last()
840
+
841
+ @pytest.fixture()
842
+ def mitmproxy_request(self, created_request: Request) -> MitmproxyRequest:
843
+ return MitmproxyRequestAdapter(created_request).adapt()
844
+
845
+ def test_regex_pattern_matching(self, mitmproxy_request: MitmproxyRequest, test_file_path: str, test_file_contents: bytes):
846
+ """Test that regex patterns work for matching routes."""
847
+ fixture = {
848
+ 'GET': {
849
+ r'/api/users/\d+': { # Regex pattern to match user IDs
850
+ 'path': test_file_path
851
+ }
852
+ }
853
+ }
854
+
855
+ res = eval_fixtures(mitmproxy_request, response_fixtures=fixture)
856
+ assert res is not None
857
+ assert res.raw.read() == test_file_contents
858
+
859
+ def test_exact_path_matching(self, mitmproxy_request: MitmproxyRequest, test_file_path: str, test_file_contents: bytes):
860
+ """Test exact path matching."""
861
+ fixture = {
862
+ 'GET': {
863
+ '/api/users/123': { # Exact path match
864
+ 'path': test_file_path
865
+ }
866
+ }
867
+ }
868
+
869
+ res = eval_fixtures(mitmproxy_request, response_fixtures=fixture)
870
+ assert res is not None
871
+ assert res.raw.read() == test_file_contents
872
+
873
+ def test_non_matching_pattern(self, mitmproxy_request: MitmproxyRequest, test_file_path: str):
874
+ """Test that non-matching patterns return None."""
875
+ fixture = {
876
+ 'GET': {
877
+ r'/api/posts/\d+': { # Different pattern that shouldn't match
878
+ 'path': test_file_path
879
+ }
880
+ }
881
+ }
882
+
883
+ res = eval_fixtures(mitmproxy_request, response_fixtures=fixture)
884
+ assert res is None
885
+
886
+ def test_multiple_patterns_first_match_wins(self, mitmproxy_request: MitmproxyRequest, test_file_path: str):
887
+ """Test that first matching pattern is used."""
888
+ tmp_dir_path = DataDir.instance().tmp_dir_path
889
+ second_file = os.path.join(tmp_dir_path, 'second_file.json')
890
+ with open(second_file, 'w') as f:
891
+ f.write('Second file content')
892
+
893
+ fixture = {
894
+ 'GET': {
895
+ r'/api/users/\d+': { # First pattern - should match
896
+ 'path': test_file_path
897
+ },
898
+ r'/api/.*': { # Second pattern - also matches but shouldn't be used
899
+ 'path': second_file
900
+ }
901
+ }
902
+ }
903
+
904
+ res = eval_fixtures(mitmproxy_request, response_fixtures=fixture)
905
+ assert res is not None
906
+ # Should use first matching pattern
907
+ assert res.raw.read() == b'User 123 data'
908
+
463
909
  class TestMultipleResponseFixtures():
464
910
  @pytest.fixture(scope='class')
465
911
  def request_method(self):
@@ -649,10 +1095,10 @@ class TestEvalFixturesService():
649
1095
  }
650
1096
 
651
1097
  with open(api_fixtures_file, 'w') as f:
652
- yaml.dump(api_fixtures_content, f)
1098
+ yaml.dump(api_fixtures_content, f, sort_keys=False)
653
1099
 
654
1100
  with open(main_fixtures_file, 'w') as f:
655
- yaml.dump(main_fixtures_content, f)
1101
+ yaml.dump(main_fixtures_content, f, sort_keys=False)
656
1102
 
657
1103
  # Use response_fixtures_path with origin specification
658
1104
  fixtures_paths = f"{api_fixtures_file}:https://api\\.example\\.com,{main_fixtures_file}:https://petstore\\.swagger\\.io"
@@ -681,7 +1127,7 @@ class TestEvalFixturesService():
681
1127
  }
682
1128
 
683
1129
  with open(fallback_fixtures_file, 'w') as f:
684
- yaml.dump(fallback_fixtures_content, f)
1130
+ yaml.dump(fallback_fixtures_content, f, sort_keys=False)
685
1131
 
686
1132
  # Use a fixture file without origin specification (fallback)
687
1133
  res: requests.Response = eval_fixtures(main_mitmproxy_request, response_fixtures_path=fallback_fixtures_file)
@@ -721,10 +1167,10 @@ class TestEvalFixturesService():
721
1167
  }
722
1168
 
723
1169
  with open(api_fixtures_file, 'w') as f:
724
- yaml.dump(api_fixtures_content, f)
1170
+ yaml.dump(api_fixtures_content, f, sort_keys=False)
725
1171
 
726
1172
  with open(fallback_fixtures_file, 'w') as f:
727
- yaml.dump(fallback_fixtures_content, f)
1173
+ yaml.dump(fallback_fixtures_content, f, sort_keys=False)
728
1174
 
729
1175
  # Use response_fixtures_path with origin-specific first, fallback second
730
1176
  fixtures_paths = f"{api_fixtures_file}:https://api\\.example\\.com,{fallback_fixtures_file}"
@@ -766,7 +1212,7 @@ class TestEvalFixturesService():
766
1212
  }
767
1213
 
768
1214
  with open(wildcard_fixtures_file, 'w') as f:
769
- yaml.dump(wildcard_fixtures_content, f)
1215
+ yaml.dump(wildcard_fixtures_content, f, sort_keys=False)
770
1216
 
771
1217
  # Use regex wildcard pattern in response_fixtures_path
772
1218
  fixtures_paths = f"{wildcard_fixtures_file}:https://.*\\.example\\.com"
@@ -795,7 +1241,7 @@ class TestEvalFixturesService():
795
1241
  }
796
1242
 
797
1243
  with open(fixtures_file_path, 'w') as fp:
798
- yaml.dump(fixtures_content, fp)
1244
+ yaml.dump(fixtures_content, fp, sort_keys=False)
799
1245
 
800
1246
  # Test loading fixtures via response_fixtures_path
801
1247
  res: requests.Response = eval_fixtures(api_mitmproxy_request, response_fixtures_path=fixtures_file_path)
@@ -834,10 +1280,10 @@ class TestEvalFixturesService():
834
1280
  }
835
1281
 
836
1282
  with open(api_fixtures_file, 'w') as fp:
837
- yaml.dump(api_fixtures_content, fp)
1283
+ yaml.dump(api_fixtures_content, fp, sort_keys=False)
838
1284
 
839
1285
  with open(main_fixtures_file, 'w') as fp:
840
- yaml.dump(main_fixtures_content, fp)
1286
+ yaml.dump(main_fixtures_content, fp, sort_keys=False)
841
1287
 
842
1288
  # Test with comma-separated paths with origins
843
1289
  fixtures_paths = f"{api_fixtures_file}:https://api\\.example\\.com,{main_fixtures_file}:https://petstore\\.swagger\\.io"
@@ -885,10 +1331,10 @@ class TestEvalFixturesService():
885
1331
  }
886
1332
 
887
1333
  with open(first_fixtures_file, 'w') as fp:
888
- yaml.dump(first_fixtures_content, fp)
1334
+ yaml.dump(first_fixtures_content, fp, sort_keys=False)
889
1335
 
890
1336
  with open(second_fixtures_file, 'w') as fp:
891
- yaml.dump(second_fixtures_content, fp)
1337
+ yaml.dump(second_fixtures_content, fp, sort_keys=False)
892
1338
 
893
1339
  # Create an invalid YAML file that would cause an error if processed
894
1340
  with open(invalid_fixtures_file, 'w') as fp:
@@ -936,10 +1382,10 @@ class TestEvalFixturesService():
936
1382
  }
937
1383
 
938
1384
  with open(wrong_origin_file, 'w') as fp:
939
- yaml.dump(wrong_origin_content, fp)
1385
+ yaml.dump(wrong_origin_content, fp, sort_keys=False)
940
1386
 
941
1387
  with open(correct_origin_file, 'w') as fp:
942
- yaml.dump(correct_origin_content, fp)
1388
+ yaml.dump(correct_origin_content, fp, sort_keys=False)
943
1389
 
944
1390
  # Test with API request - wrong origin should be skipped, correct origin should be used
945
1391
  fixtures_paths = f"{wrong_origin_file}:https://petstore\\.swagger\\.io,{correct_origin_file}:https://api\\.example\\.com"
@@ -974,7 +1420,7 @@ class TestEvalFixturesService():
974
1420
  }
975
1421
  }
976
1422
  with open(api_fixtures_file, 'w') as f:
977
- yaml.dump(fixtures_content, f)
1423
+ yaml.dump(fixtures_content, f, sort_keys=False)
978
1424
 
979
1425
  # Test full URL with port
980
1426
  status = RequestBuilder(
@@ -1015,7 +1461,7 @@ class TestEvalFixturesService():
1015
1461
  }
1016
1462
  }
1017
1463
  with open(api_fixtures_file, 'w') as f:
1018
- yaml.dump(fixtures_content, f)
1464
+ yaml.dump(fixtures_content, f, sort_keys=False)
1019
1465
 
1020
1466
  # Test HTTP request
1021
1467
  status = RequestBuilder(
@@ -1056,7 +1502,7 @@ class TestEvalFixturesService():
1056
1502
  }
1057
1503
  }
1058
1504
  with open(wildcard_fixtures_file, 'w') as f:
1059
- yaml.dump(fixtures_content, f)
1505
+ yaml.dump(fixtures_content, f, sort_keys=False)
1060
1506
 
1061
1507
  # Test subdomain request
1062
1508
  status = RequestBuilder(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stoobly-agent
3
- Version: 1.10.2
3
+ Version: 1.10.3
4
4
  Summary: Record, mock, and test HTTP(s) requests. CLI agent for Stoobly
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -1,8 +1,8 @@
1
- stoobly_agent/__init__.py,sha256=CRXzbO8ujhHYHbvJS89guJ-Nk8RIZn8xmdumu_uAfBg,45
1
+ stoobly_agent/__init__.py,sha256=G6RHX8ESNEXsGSpOUuCoZooxoXtbFgnLwqa8NHC683M,45
2
2
  stoobly_agent/__main__.py,sha256=tefOkFZeCFU4l3C-Y4R_lR9Yt-FETISiXGUnbh6Os54,146
3
3
  stoobly_agent/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  stoobly_agent/app/api/__init__.py,sha256=NIgcbX7iiWrApsCITXlmhr4SYbWS0fwb01x-F3jTFdo,666
5
- stoobly_agent/app/api/application_http_request_handler.py,sha256=Tn1aUZr1EH4NtwG7el_ZH2P4baMBZSnS2qW3dkEw_FE,5359
5
+ stoobly_agent/app/api/application_http_request_handler.py,sha256=h8y3gjPOgbFK3z0UIX3t6aGtRMAiQt3QH9qZ_Gduq_M,5363
6
6
  stoobly_agent/app/api/bodies_controller.py,sha256=tiuBIcLsRIUrVlStFwhGj_8AzsURSmW0E_MDQb09WXY,2267
7
7
  stoobly_agent/app/api/configs_controller.py,sha256=S8BYn5PHMlc_vdnJTU0qE6w0lL7A7swEiV5_LJJvMOU,4736
8
8
  stoobly_agent/app/api/headers_controller.py,sha256=qUM0U0WNn_87outx1y4r8Mfr6RqHO0UR91JP0-LNajc,2781
@@ -59,7 +59,7 @@ stoobly_agent/app/cli/helpers/test_facade.py,sha256=gRnbjYNxnzPmtvFp2J6TZgxaMSzD
59
59
  stoobly_agent/app/cli/helpers/test_replay_context.py,sha256=Tx5fenr14mXqHvCpl7o9c2qTJw8Kl9XgxzqqDhqFj1I,4754
60
60
  stoobly_agent/app/cli/helpers/trace_aliases.py,sha256=8N8pnsnONwVv-fabCTvDlXec-WbSu1zfCNHTuzl8Tzc,874
61
61
  stoobly_agent/app/cli/helpers/trace_context_facade.py,sha256=3MDjY_bdhvE2dad_B_w9gemCZZoiVjotbE8o9hrmVYo,1027
62
- stoobly_agent/app/cli/helpers/validations.py,sha256=nvcrEHvjMOAyHlJK0bQIwyzBUciJ0qNkNESa2zZ5ZFo,6232
62
+ stoobly_agent/app/cli/helpers/validations.py,sha256=o9_DqlAIw98QcMNXGsAnELQ6DeXdSRa1TLiTZu3V7ns,6236
63
63
  stoobly_agent/app/cli/helpers/verify_raw_request_service.py,sha256=tmLeRBYhNgAQeJD-rF6Nj22F_TfVktVSb1wjRPFblBQ,658
64
64
  stoobly_agent/app/cli/intercept_cli.py,sha256=tgGtl1EZerIdcuMR8KKHdrrn_7-XWSg0D6XdTuSq8k0,6698
65
65
  stoobly_agent/app/cli/main_group.py,sha256=3UzBxin2f2p5KNqo6Oayo_I1VI2pHpqOoRexUnJ74a4,2187
@@ -67,7 +67,7 @@ stoobly_agent/app/cli/project_cli.py,sha256=EXjeLjbnq9PhfCjvyfZ0UnJ2tejeCS0SIAo3
67
67
  stoobly_agent/app/cli/report_cli.py,sha256=ZxJw0Xkx7KFZJn9e45BSKRKon8AD0Msrwy1fbPfbv0c,2543
68
68
  stoobly_agent/app/cli/request_cli.py,sha256=NVO6alj2acwaYLvU2445yB5qAQIK32Ll19tOErxzNR8,7863
69
69
  stoobly_agent/app/cli/scaffold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
- stoobly_agent/app/cli/scaffold/app.py,sha256=CRnYQbIee8vB6El8eOM1hlQ3wDlNtxRpZ7gy21ZiBqE,3918
70
+ stoobly_agent/app/cli/scaffold/app.py,sha256=7d5nHnQwyBxEwSC2Jrvbk2U3dj9OnhaHrE2zmC37oOs,3922
71
71
  stoobly_agent/app/cli/scaffold/app_command.py,sha256=x--ejtVSBL0Jz8OiakBtxfI2IZFAWJWCwuSJo7TEU9Y,2386
72
72
  stoobly_agent/app/cli/scaffold/app_config.py,sha256=oG06y9yuAo05ROpOhDC_LFPJ0KKNrhfvYLOgHx5nE4Y,2788
73
73
  stoobly_agent/app/cli/scaffold/app_create_command.py,sha256=G4C-XSp4hN0FvaSKLpGGvTp8iXHqKYdqoxxRPHgO0a0,8253
@@ -110,7 +110,7 @@ stoobly_agent/app/cli/scaffold/service_command.py,sha256=j-lkG5Zth_CBHa6Z9Kv3dJw
110
110
  stoobly_agent/app/cli/scaffold/service_config.py,sha256=A8kQU3n3IhOn64PAQo-O_WHGYdjz8AAER1UF4z0KKPw,6977
111
111
  stoobly_agent/app/cli/scaffold/service_create_command.py,sha256=PwydEKBNodrGZVmfWifNjrYRDHifAnYR7gpeYMoTU3s,3726
112
112
  stoobly_agent/app/cli/scaffold/service_delete_command.py,sha256=_nBDQjm8eL62MQpzSCxgUHlW04ZXKG8MDlN1BXxlqww,986
113
- stoobly_agent/app/cli/scaffold/service_dependency.py,sha256=7fD7BMjyBs19ZsWvsV8z3ViBXbuBmt-ytYvUboaqsww,1851
113
+ stoobly_agent/app/cli/scaffold/service_dependency.py,sha256=olr_s_cfn51Pz5FlIihlydEl7g4CHKn-YJT0EP2h8eA,1855
114
114
  stoobly_agent/app/cli/scaffold/service_docker_compose.py,sha256=fVUZ-oo-bn5GVZp8JgGq7AkiQQ6-JkxwK_OMlinS9WM,915
115
115
  stoobly_agent/app/cli/scaffold/service_update_command.py,sha256=oWusBKfvjt4RnK03_V3CJYWrfsCI4_LcR7W12eLXMR4,2579
116
116
  stoobly_agent/app/cli/scaffold/service_workflow.py,sha256=sQ_Edy_wGHKMXpD0DmhnOWkGEKz7gSgEGNI8f7aXOdg,444
@@ -118,7 +118,7 @@ stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py,sha256=80pX8
118
118
  stoobly_agent/app/cli/scaffold/templates/__init__.py,sha256=x8C_a0VoO_vUbosp4_6IC1U7Ge9NnUdVKDPpVMtMkeY,171
119
119
  stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context,sha256=9DQK-OXnRKjjKWsUSIRAio6dkR4eGxD1vizPT7Q5sp8,159
120
120
  stoobly_agent/app/cli/scaffold/templates/app/.Makefile,sha256=OnY_3D9nxl3HxfUxvCgk7PY3XntEbgO9j1myx_ETK7w,9161
121
- stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml,sha256=6tFqXh3ine8vaD0FCL5TMoY5NjKx2wLUR8XpW3tJtew,245
121
+ stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml,sha256=SABod6_InBFyOm-uulK7r27SR0sWRUiL0h69g_jvNJA,249
122
122
  stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.networks.yml,sha256=I4PbJpQjFHb5IbAUWNvYM6okDEtmwtKFDQg-yog05WM,141
123
123
  stoobly_agent/app/cli/scaffold/templates/app/Makefile,sha256=TEmPG7Bf0KZOnmfsgdzza3UdwcVMmM5Lj1YdLc4cgjA,79
124
124
  stoobly_agent/app/cli/scaffold/templates/app/build/.config.yml,sha256=8Wt8ZZ5irvBYYS44xGrR_EWlZDuXH9kyWmquzsh7s8g,19
@@ -361,15 +361,15 @@ stoobly_agent/app/proxy/mitmproxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeR
361
361
  stoobly_agent/app/proxy/mitmproxy/flow_mock.py,sha256=YxQV3pXIzicvLOCFn7zmnSoAstAmR8fzONYOXAkYbT4,1153
362
362
  stoobly_agent/app/proxy/mitmproxy/request.py,sha256=niy38718CAe4Li4k40GXnWDDmUTCPkXesDZTaC8BeLc,878
363
363
  stoobly_agent/app/proxy/mitmproxy/request_body_facade.py,sha256=kMJ8YH2FKW9qV68QcUbOCLfiGgKrFV5krzt7usGf0as,649
364
- stoobly_agent/app/proxy/mitmproxy/request_facade.py,sha256=ab16pg551XipGyAvP_2GtY5gMrWihC1sBkSmU-s5NQ8,8533
364
+ stoobly_agent/app/proxy/mitmproxy/request_facade.py,sha256=9yvTOPWQw7epg-wxvlT66lKR0-ZgLHEOVfX-xeumpJM,8537
365
365
  stoobly_agent/app/proxy/mitmproxy/response.py,sha256=BFaJlB1D98EOjrc-kUmunUCTUuiZsPDqq2QgAHDocQI,339
366
366
  stoobly_agent/app/proxy/mitmproxy/response_body_facade.py,sha256=vIu6cuCSiZDocvnMNyi0Zjf0qsC0enm2Glp671eWnb4,659
367
367
  stoobly_agent/app/proxy/mitmproxy/response_facade.py,sha256=0wCSzUULUhDDV93QXUgzMNxiiSZwcu-kUB2C5z1Ck0Y,4311
368
368
  stoobly_agent/app/proxy/mock/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
369
369
  stoobly_agent/app/proxy/mock/context.py,sha256=vDo5_3WBL73mVFnsmQWvcxvPg5nWtRJbigSrE3zGc-o,794
370
370
  stoobly_agent/app/proxy/mock/custom_not_found_response_builder.py,sha256=0KWB3KFxVrnJOKDaYxm5eoJEccw7IpJZRyUvBX61-8k,697
371
- stoobly_agent/app/proxy/mock/eval_fixtures_service.py,sha256=qs_R87SV2ya6eQoJ3vMxqM87yNm-C-NSCZKloUVEPVM,11305
372
- stoobly_agent/app/proxy/mock/eval_request_service.py,sha256=A1tcE3wmrC1HwLpz0aRuRw-Nucn0dyHD_yHw5BeQEJU,8146
371
+ stoobly_agent/app/proxy/mock/eval_fixtures_service.py,sha256=YNe1NnpmTvhtjfAXL2Vs3cmYmd4fcNCSVYeun4ANdoc,12068
372
+ stoobly_agent/app/proxy/mock/eval_request_service.py,sha256=JLmFzMrtbphPGPfUznaGW-_cc6sMZDehhdqY3tXrgzI,8150
373
373
  stoobly_agent/app/proxy/mock/hashed_request_decorator.py,sha256=h1ma90fdaYI9LBWpMWMqWBz-RjNwI628O4VuS_uUBX4,5061
374
374
  stoobly_agent/app/proxy/mock/ignored_components_response_builder.py,sha256=E32_E1eSdmPn2SeM_e1jWnqu4xh5w_SnmOs32Shx99E,501
375
375
  stoobly_agent/app/proxy/mock/request_hasher.py,sha256=WeOeksmW_b5Ql0Xb0fL2WMH3MVHrm-9GYWYoKYS1teg,3755
@@ -422,7 +422,7 @@ stoobly_agent/app/proxy/test/matchers/fuzzy.py,sha256=tTnsPDaK0HFJ5ST5ThZIlHkZNz
422
422
  stoobly_agent/app/proxy/test/matchers/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
423
423
  stoobly_agent/app/proxy/test/test_service.py,sha256=EWDdrcoewgCzsuWmbra-dDEZLfuWBiX6X9LF_xwqglw,4314
424
424
  stoobly_agent/app/proxy/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
425
- stoobly_agent/app/proxy/utils/allowed_request_service.py,sha256=VzT_l2lFXl8ErTr5vijt-DKhbjQG_15YNsK5Wazb3VE,3709
425
+ stoobly_agent/app/proxy/utils/allowed_request_service.py,sha256=V977CB7Ee6kc566PX0V7rGk4pJsy0SgRmXm8CC78d6c,3717
426
426
  stoobly_agent/app/proxy/utils/minimize_headers.py,sha256=hL5-cpNgqilOXY0s7sjUL76Ixym4I1bUws9yDlrsXGs,1175
427
427
  stoobly_agent/app/proxy/utils/publish_change_service.py,sha256=ljKaVz1i3ClUNk0_UHvwpoJdUhapZhwQXs4UB13LsM8,1292
428
428
  stoobly_agent/app/proxy/utils/request_handler.py,sha256=VscAXf2_F1C1yrt2gNImPkatosuGj356pARTd_WnTUE,846
@@ -545,7 +545,7 @@ stoobly_agent/lib/api/stoobly_api.py,sha256=PacuNx3-RzqXHJSXwYXoKl9JAVhefd5A7Qqy
545
545
  stoobly_agent/lib/api/test_responses_resource.py,sha256=RdNj7N6xC7jW5zUoKfgNg8Nenk7wdSMaL4dv_DIXih0,541
546
546
  stoobly_agent/lib/api/tests_resource.py,sha256=EKyI0xTgzRSPyGdepgteVH3EHUkXIzrt3HXahBau6CY,855
547
547
  stoobly_agent/lib/api/users_resource.py,sha256=xR0ug5xifuvRw4jdF4cVjrWBdkFccmL9_fy8ZxOPX74,477
548
- stoobly_agent/lib/cache.py,sha256=nHQuo3xA9pFa5n9Fp9FwrSeSOb8CcdL_CfsFnCW6XZM,2262
548
+ stoobly_agent/lib/cache.py,sha256=7sYq2wh6I5LmtKqvJYoY89h_PvZ21zvS9pCBCEutuCM,2266
549
549
  stoobly_agent/lib/logger.py,sha256=6s_8UxXLJt1jor-Ez0xCBWhuAD932sTEBQBnNXpFTtQ,1971
550
550
  stoobly_agent/lib/orm/__init__.py,sha256=UCAL40_L9qf1sUqvUWKzc551jKLR4HKqRNLPdOuXl2w,1199
551
551
  stoobly_agent/lib/orm/base.py,sha256=3PZx0OWK241vrr8pLLQL0GFhKxvYVDFyVMMdYzhxMPM,358
@@ -755,13 +755,13 @@ stoobly_agent/test/app/models/factories/resource/local_db/helpers/log_test.py,sh
755
755
  stoobly_agent/test/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request_test.py,sha256=a1SFLyEyRRLuADvAw6ckQQKORFXvyK1lyrbkaLWx8oU,3399
756
756
  stoobly_agent/test/app/models/factories/resource/local_db/request_adapter_test.py,sha256=Pzq1cBPnP9oSWG-p0c-VoymoHxgp483QmNwmV1b78RA,8453
757
757
  stoobly_agent/test/app/models/factories/resource/local_db/response_adapter_test.py,sha256=9P95EKH5rZGOrmRkRIDlQZqtiLJHk9735og18Ffwpfw,2204
758
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION,sha256=a1becxu6fSo0CzXh-GOeiAZjwhklQeSrgYlb8OHxF5g,6
758
+ stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION,sha256=NNWX20PKU7L9csy90a96D-I4wsC4Mh2tj0OhYTFD_GI,6
759
759
  stoobly_agent/test/app/models/schemas/.stoobly/db/stoobly_agent.sqlite3,sha256=ch8gNx6zIelLKQx65gwFx_LRNqUD3EC5xcHZ0ukIQiU,188416
760
760
  stoobly_agent/test/app/models/schemas/.stoobly/settings.yml,sha256=vLwMjweKOdod6tSLtIlyBefPQuNXq9wio4kBaODKtAU,726
761
761
  stoobly_agent/test/app/models/schemas/.stoobly/tmp/options.json,sha256=OTRzarwus48CTrItedXCrgQttJHSEZonEYc7R_knvYg,2212
762
762
  stoobly_agent/test/app/models/schemas/request_test.py,sha256=9SF43KXbjO-vMr2uObPJlyLeop_JQstl6Jrh0M1A70c,1116
763
- stoobly_agent/test/app/proxy/mitmproxy/request_facade_test.py,sha256=sXlzjKVvpORJ8fxEs5Qfc4IC0itTWxqDIzMujGjlW7I,5765
764
- stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py,sha256=TZq0h0BlS7mjVyA-Z9_oPgercd1V1WU5Yo25UEYLfA8,42056
763
+ stoobly_agent/test/app/proxy/mitmproxy/request_facade_test.py,sha256=z5_4VdSBVtcox5gZsiit-9EGgHfvqaPRHHsXSBXaVUc,6791
764
+ stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py,sha256=BMn1EmBSnkEUlFCbcz5BsQuBM_qCgLmAUPdnL5E7cL8,58429
765
765
  stoobly_agent/test/app/proxy/replay/body_parser_service_test.py,sha256=5clzjjkdve0Ctggnw-ZLmZSUtEfWK-Ejtf1x7zCu7z4,4994
766
766
  stoobly_agent/test/app/proxy/replay/rewrite_params_service_test.py,sha256=4KVaP48KjCeoZKqY3IdrFAP5Pnb3jO86k8L7ffvz2ZI,3770
767
767
  stoobly_agent/test/app/proxy/replay/trace_context_test.py,sha256=y0oBNC89sp3BG8biOBTiaTopk1LtqjThlA4d6BwAHzM,14462
@@ -797,8 +797,8 @@ stoobly_agent/test/mock_data/scaffold/docker-compose-local-service.yml,sha256=1W
797
797
  stoobly_agent/test/mock_data/scaffold/index.html,sha256=qJwuYajKZ4ihWZrJQ3BNObV5kf1VGnnm_vqlPJzdqLE,258
798
798
  stoobly_agent/test/mock_data/uspto.yaml,sha256=6U5se7C3o-86J4m9xpOk9Npias399f5CbfWzR87WKwE,7835
799
799
  stoobly_agent/test/test_helper.py,sha256=6v4AHeqYPw7vtRoxET_ubmRWPJoSmTR_DVHay3FxNbQ,1299
800
- stoobly_agent-1.10.2.dist-info/METADATA,sha256=XFNMBWLZRQ5Upii0fVCRJuMYTOInbrp9H_oR7yQYgt4,3203
801
- stoobly_agent-1.10.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
802
- stoobly_agent-1.10.2.dist-info/entry_points.txt,sha256=aq5wix5oC8MDQtmyPGU0xaFrsjJg7WH28NmXh2sc3Z8,56
803
- stoobly_agent-1.10.2.dist-info/licenses/LICENSE,sha256=o93sj12cdoEOsTCjPaPFsw3Xq0SXs3pPcY-9reE2sEw,548
804
- stoobly_agent-1.10.2.dist-info/RECORD,,
800
+ stoobly_agent-1.10.3.dist-info/METADATA,sha256=WOtXuRZ-PH8eZ38X2Z63_zddgLbLMbtLJ6Mbq3-vo6I,3203
801
+ stoobly_agent-1.10.3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
802
+ stoobly_agent-1.10.3.dist-info/entry_points.txt,sha256=aq5wix5oC8MDQtmyPGU0xaFrsjJg7WH28NmXh2sc3Z8,56
803
+ stoobly_agent-1.10.3.dist-info/licenses/LICENSE,sha256=o93sj12cdoEOsTCjPaPFsw3Xq0SXs3pPcY-9reE2sEw,548
804
+ stoobly_agent-1.10.3.dist-info/RECORD,,