stoobly-agent 1.10.0__py3-none-any.whl → 1.10.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/__main__.py +10 -0
  3. stoobly_agent/app/api/application_http_request_handler.py +5 -2
  4. stoobly_agent/app/cli/ca_cert_cli.py +9 -5
  5. stoobly_agent/app/cli/helpers/replay_facade.py +2 -2
  6. stoobly_agent/app/cli/intercept_cli.py +5 -5
  7. stoobly_agent/app/cli/request_cli.py +2 -2
  8. stoobly_agent/app/cli/scaffold/app.py +14 -5
  9. stoobly_agent/app/cli/scaffold/app_command.py +0 -4
  10. stoobly_agent/app/cli/scaffold/app_config.py +49 -2
  11. stoobly_agent/app/cli/scaffold/app_create_command.py +145 -76
  12. stoobly_agent/app/cli/scaffold/constants.py +9 -4
  13. stoobly_agent/app/cli/scaffold/docker/constants.py +3 -1
  14. stoobly_agent/app/cli/scaffold/docker/service/build_decorator.py +4 -4
  15. stoobly_agent/app/cli/scaffold/docker/service/builder.py +31 -54
  16. stoobly_agent/app/cli/scaffold/docker/service/configure_gateway.py +3 -0
  17. stoobly_agent/app/cli/scaffold/docker/template_files.py +112 -0
  18. stoobly_agent/app/cli/scaffold/docker/workflow/build_decorator.py +1 -1
  19. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +30 -47
  20. stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +3 -2
  21. stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +1 -1
  22. stoobly_agent/app/cli/scaffold/docker/workflow/dns_decorator.py +2 -3
  23. stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +1 -1
  24. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +1 -1
  25. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +1 -1
  26. stoobly_agent/app/cli/scaffold/docker/workflow/run_command.py +423 -0
  27. stoobly_agent/app/cli/scaffold/local/__init__.py +0 -0
  28. stoobly_agent/app/cli/scaffold/local/service/__init__.py +0 -0
  29. stoobly_agent/app/cli/scaffold/local/service/builder.py +72 -0
  30. stoobly_agent/app/cli/scaffold/local/workflow/__init__.py +0 -0
  31. stoobly_agent/app/cli/scaffold/local/workflow/builder.py +35 -0
  32. stoobly_agent/app/cli/scaffold/local/workflow/run_command.py +339 -0
  33. stoobly_agent/app/cli/scaffold/service_command.py +9 -1
  34. stoobly_agent/app/cli/scaffold/service_config.py +9 -25
  35. stoobly_agent/app/cli/scaffold/service_create_command.py +18 -6
  36. stoobly_agent/app/cli/scaffold/service_docker_compose.py +3 -3
  37. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +10 -7
  38. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +2 -2
  39. stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +4 -4
  40. stoobly_agent/app/cli/scaffold/templates/app/build/mock/configure +3 -0
  41. stoobly_agent/app/cli/scaffold/templates/app/build/record/configure +28 -0
  42. stoobly_agent/app/cli/scaffold/templates/app/build/test/configure +3 -0
  43. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +4 -4
  44. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/configure +3 -0
  45. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/run +3 -0
  46. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/configure +3 -0
  47. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/run +3 -0
  48. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/configure +3 -0
  49. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/run +3 -0
  50. stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.configure +5 -1
  51. stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.init +5 -1
  52. stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.run +14 -0
  53. stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.configure +5 -1
  54. stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.init +5 -1
  55. stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.run +14 -0
  56. stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.configure +5 -1
  57. stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.init +5 -1
  58. stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.run +14 -0
  59. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.configure +5 -1
  60. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.init +5 -1
  61. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.run +19 -0
  62. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.configure +5 -1
  63. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.init +5 -1
  64. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.run +19 -0
  65. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.configure +5 -1
  66. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.init +5 -1
  67. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.run +19 -0
  68. stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scaffold/.up +0 -1
  69. stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.configure +5 -1
  70. stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.init +5 -1
  71. stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.run +14 -0
  72. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.configure +25 -1
  73. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.init +5 -1
  74. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.run +14 -0
  75. stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.configure +5 -1
  76. stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.init +5 -1
  77. stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.run +14 -0
  78. stoobly_agent/app/cli/scaffold/templates/constants.py +35 -19
  79. stoobly_agent/app/cli/scaffold/templates/factory.py +34 -18
  80. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.run +21 -0
  81. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.run +21 -0
  82. stoobly_agent/app/cli/scaffold/templates/workflow/mock/configure +5 -0
  83. stoobly_agent/app/cli/scaffold/templates/workflow/mock/run +3 -0
  84. stoobly_agent/app/cli/scaffold/templates/workflow/record/configure +21 -0
  85. stoobly_agent/app/cli/scaffold/templates/workflow/record/run +3 -0
  86. stoobly_agent/app/cli/scaffold/templates/workflow/test/configure +5 -0
  87. stoobly_agent/app/cli/scaffold/templates/workflow/test/run +3 -0
  88. stoobly_agent/app/cli/scaffold/workflow_command.py +18 -4
  89. stoobly_agent/app/cli/scaffold/workflow_copy_command.py +5 -4
  90. stoobly_agent/app/cli/scaffold/workflow_create_command.py +31 -29
  91. stoobly_agent/app/cli/scaffold/workflow_run_command.py +18 -151
  92. stoobly_agent/app/cli/scaffold_cli.py +134 -182
  93. stoobly_agent/app/cli/scenario_cli.py +2 -2
  94. stoobly_agent/app/cli/types/test.py +2 -2
  95. stoobly_agent/app/cli/types/workflow_run_command.py +52 -3
  96. stoobly_agent/app/proxy/handle_mock_service.py +1 -1
  97. stoobly_agent/app/proxy/intercept_settings.py +6 -26
  98. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +177 -27
  99. stoobly_agent/app/proxy/mock/types/__init__.py +22 -1
  100. stoobly_agent/app/proxy/record/upload_request_service.py +3 -6
  101. stoobly_agent/app/proxy/replay/body_parser_service.py +8 -5
  102. stoobly_agent/app/proxy/replay/multipart.py +15 -13
  103. stoobly_agent/app/proxy/replay/replay_request_service.py +2 -2
  104. stoobly_agent/app/proxy/run.py +3 -0
  105. stoobly_agent/app/proxy/test/context.py +0 -4
  106. stoobly_agent/app/proxy/test/context_abc.py +0 -5
  107. stoobly_agent/app/proxy/utils/publish_change_service.py +20 -23
  108. stoobly_agent/app/settings/__init__.py +10 -7
  109. stoobly_agent/cli.py +61 -16
  110. stoobly_agent/config/data_dir.py +1 -8
  111. stoobly_agent/public/12-es2015.618ecfd5f735b801b50f.js +1 -0
  112. stoobly_agent/public/12-es5.618ecfd5f735b801b50f.js +1 -0
  113. stoobly_agent/public/index.html +1 -1
  114. stoobly_agent/public/main-es2015.5a9aa16433404c3f423a.js +1 -0
  115. stoobly_agent/public/main-es5.5a9aa16433404c3f423a.js +1 -0
  116. stoobly_agent/public/runtime-es2015.77bcd31efed9e5d5d431.js +1 -0
  117. stoobly_agent/public/runtime-es5.77bcd31efed9e5d5d431.js +1 -0
  118. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +17 -6
  119. stoobly_agent/test/app/cli/scaffold/docker/cli_invoker.py +177 -0
  120. stoobly_agent/test/app/cli/scaffold/{cli_test.py → docker/cli_test.py} +4 -11
  121. stoobly_agent/test/app/cli/scaffold/{e2e_test.py → docker/e2e_test.py} +42 -27
  122. stoobly_agent/test/app/cli/scaffold/local/__init__.py +0 -0
  123. stoobly_agent/test/app/cli/scaffold/{cli_invoker.py → local/cli_invoker.py} +38 -32
  124. stoobly_agent/test/app/cli/scaffold/local/e2e_test.py +342 -0
  125. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  126. stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +903 -2
  127. stoobly_agent/test/app/proxy/replay/body_parser_service_test.py +95 -3
  128. stoobly_agent/test/config/data_dir_test.py +2 -7
  129. stoobly_agent/test/test_helper.py +16 -5
  130. {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info}/METADATA +4 -2
  131. {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info}/RECORD +157 -129
  132. {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info}/WHEEL +1 -1
  133. stoobly_agent/app/cli/helpers/shell.py +0 -26
  134. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/configure +0 -3
  135. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/configure +0 -3
  136. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/configure +0 -3
  137. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/bin/configure +0 -3
  138. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/bin/configure +0 -3
  139. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/bin/configure +0 -3
  140. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +0 -13
  141. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +0 -47
  142. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +0 -13
  143. stoobly_agent/public/12-es2015.be58ed0ef449008b932e.js +0 -1
  144. stoobly_agent/public/12-es5.be58ed0ef449008b932e.js +0 -1
  145. stoobly_agent/public/main-es2015.089b46f303768fbe864f.js +0 -1
  146. stoobly_agent/public/main-es5.089b46f303768fbe864f.js +0 -1
  147. stoobly_agent/public/runtime-es2015.f8c814b38b27708e91c1.js +0 -1
  148. stoobly_agent/public/runtime-es5.f8c814b38b27708e91c1.js +0 -1
  149. /stoobly_agent/app/cli/scaffold/templates/app/build/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  150. /stoobly_agent/app/cli/scaffold/templates/app/build/mock/{bin/init → init} +0 -0
  151. /stoobly_agent/app/cli/scaffold/templates/app/build/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  152. /stoobly_agent/app/cli/scaffold/templates/app/build/record/{bin/init → init} +0 -0
  153. /stoobly_agent/app/cli/scaffold/templates/app/build/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  154. /stoobly_agent/app/cli/scaffold/templates/app/build/test/{bin/init → init} +0 -0
  155. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  156. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/{bin/init → init} +0 -0
  157. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  158. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/{bin/init → init} +0 -0
  159. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  160. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/{bin/init → init} +0 -0
  161. /stoobly_agent/app/cli/scaffold/templates/app/gateway/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  162. /stoobly_agent/app/cli/scaffold/templates/app/gateway/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  163. /stoobly_agent/app/cli/scaffold/templates/app/gateway/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  164. /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/{.docker-compose.exec.yml → .docker-compose.yml} +0 -0
  165. /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  166. /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  167. /stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  168. /stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  169. /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{bin/init → init} +0 -0
  170. /stoobly_agent/app/cli/scaffold/templates/workflow/record/{bin/init → init} +0 -0
  171. /stoobly_agent/app/cli/scaffold/templates/workflow/test/{bin/init → init} +0 -0
  172. {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info}/entry_points.txt +0 -0
  173. {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info/licenses}/LICENSE +0 -0
@@ -1,8 +1,10 @@
1
+ import json
1
2
  import os
2
3
  import pdb
3
4
  import pytest
4
5
  import requests
5
6
  import shutil
7
+ import yaml
6
8
 
7
9
  from click.testing import CliRunner
8
10
  from mitmproxy.http import Request as MitmproxyRequest
@@ -88,7 +90,14 @@ class TestEvalFixturesService():
88
90
 
89
91
  @pytest.fixture()
90
92
  def fixtures_response(self, mitmproxy_request: MitmproxyRequest, response_fixtures: Fixtures):
91
- res: requests.Response = eval_fixtures(mitmproxy_request, response_fixtures=response_fixtures)
93
+ # Convert response_fixtures to a temporary YAML file
94
+ tmp_dir_path = DataDir.instance().tmp_dir_path
95
+ fixtures_file = os.path.join(tmp_dir_path, 'test_response_fixtures.yml')
96
+
97
+ with open(fixtures_file, 'w') as f:
98
+ yaml.dump(response_fixtures, f)
99
+
100
+ res: requests.Response = eval_fixtures(mitmproxy_request, response_fixtures_path=fixtures_file)
92
101
  assert res != None
93
102
  return res
94
103
 
@@ -177,4 +186,896 @@ class TestEvalFixturesService():
177
186
  assert public_directory_response.status_code == 200
178
187
 
179
188
  def test_default_it_headers(self, public_directory_default_response: requests.Response):
180
- assert public_directory_default_response.headers['Content-Type'] == 'application/json'
189
+ assert public_directory_default_response.headers['Content-Type'] == 'application/json'
190
+
191
+ class TestMultiplePublicDirectories():
192
+ @pytest.fixture(scope='class')
193
+ def request_method(self):
194
+ return 'GET'
195
+
196
+ @pytest.fixture(scope='class')
197
+ def request_url(self):
198
+ return 'https://petstore.swagger.io'
199
+
200
+ @pytest.fixture(scope='class')
201
+ def api_request_url(self):
202
+ return 'https://api.example.com'
203
+
204
+ @pytest.fixture(scope='class')
205
+ def created_request(
206
+ self, settings: Settings, request_method: str, request_url: str
207
+ ):
208
+ status = RequestBuilder(
209
+ method=request_method,
210
+ request_headers={'accept': 'text/html;q=0.1,application/json;q=0.9'},
211
+ response_body='',
212
+ status_code=200,
213
+ url=request_url,
214
+ ).with_settings(settings).build()[1]
215
+ assert status == 200
216
+
217
+ return Request.last()
218
+
219
+ @pytest.fixture(scope='class')
220
+ def api_created_request(
221
+ self, settings: Settings, request_method: str, api_request_url: str
222
+ ):
223
+ status = RequestBuilder(
224
+ method=request_method,
225
+ request_headers={'accept': 'text/html;q=0.1,application/json;q=0.9'},
226
+ response_body='',
227
+ status_code=200,
228
+ url=api_request_url,
229
+ ).with_settings(settings).build()[1]
230
+ assert status == 200
231
+
232
+ return Request.last()
233
+
234
+ @pytest.fixture()
235
+ def mitmproxy_request(self, created_request: Request) -> MitmproxyRequest:
236
+ return MitmproxyRequestAdapter(created_request).adapt()
237
+
238
+ @pytest.fixture()
239
+ def api_mitmproxy_request(self, api_created_request: Request) -> MitmproxyRequest:
240
+ return MitmproxyRequestAdapter(api_created_request).adapt()
241
+
242
+ @pytest.fixture(scope='class')
243
+ def main_public_directory(self):
244
+ tmp_dir_path = DataDir.instance().tmp_dir_path
245
+ public_dir_path = os.path.join(tmp_dir_path, 'main_public')
246
+ if not os.path.exists(public_dir_path):
247
+ os.mkdir(public_dir_path)
248
+ return public_dir_path
249
+
250
+ @pytest.fixture(scope='class')
251
+ def api_public_directory(self):
252
+ tmp_dir_path = DataDir.instance().tmp_dir_path
253
+ public_dir_path = os.path.join(tmp_dir_path, 'api_public')
254
+ if not os.path.exists(public_dir_path):
255
+ os.mkdir(public_dir_path)
256
+ return public_dir_path
257
+
258
+ @pytest.fixture(scope='class')
259
+ def fallback_public_directory(self):
260
+ tmp_dir_path = DataDir.instance().tmp_dir_path
261
+ public_dir_path = os.path.join(tmp_dir_path, 'fallback_public')
262
+ if not os.path.exists(public_dir_path):
263
+ os.mkdir(public_dir_path)
264
+ return public_dir_path
265
+
266
+ @pytest.fixture(scope='class')
267
+ def main_file_contents(self):
268
+ return b'Main Site Content'
269
+
270
+ @pytest.fixture(scope='class')
271
+ def api_file_contents(self):
272
+ return b'API Documentation'
273
+
274
+ @pytest.fixture(scope='class')
275
+ def fallback_file_contents(self):
276
+ return b'Fallback Content'
277
+
278
+ @pytest.fixture(autouse=True, scope='class')
279
+ def setup_files(self, main_public_directory: str, api_public_directory: str, fallback_public_directory: str,
280
+ main_file_contents: bytes, api_file_contents: bytes, fallback_file_contents: bytes):
281
+ # Create index.html in main directory
282
+ main_path = os.path.join(main_public_directory, 'index.html')
283
+ with open(main_path, 'wb') as fp:
284
+ fp.write(main_file_contents)
285
+
286
+ # Create index.html in API directory
287
+ api_path = os.path.join(api_public_directory, 'index.html')
288
+ with open(api_path, 'wb') as fp:
289
+ fp.write(api_file_contents)
290
+
291
+ # Create index.html in fallback directory
292
+ fallback_path = os.path.join(fallback_public_directory, 'index.html')
293
+ with open(fallback_path, 'wb') as fp:
294
+ fp.write(fallback_file_contents)
295
+
296
+ def test_origin_specific_routing(self, api_mitmproxy_request: MitmproxyRequest,
297
+ main_public_directory: str, api_public_directory: str, fallback_public_directory: str,
298
+ api_file_contents: bytes):
299
+ """Test that origin-specific paths are used when origin matches."""
300
+ # Multiple paths with origin specification
301
+ public_paths = f"{main_public_directory}:https://petstore\\.swagger\\.io,{api_public_directory}:https://api\\.example\\.com,{fallback_public_directory}"
302
+
303
+ res: requests.Response = eval_fixtures(api_mitmproxy_request, public_directory_path=public_paths)
304
+ assert res is not None
305
+ assert res.raw.read() == api_file_contents
306
+
307
+ def test_fallback_routing(self, mitmproxy_request: MitmproxyRequest,
308
+ main_public_directory: str, api_public_directory: str, fallback_public_directory: str,
309
+ main_file_contents: bytes):
310
+ """Test that origin-specific paths are used when origin matches."""
311
+ # Multiple paths with origin specification - https://petstore.swagger.io should match first path
312
+ public_paths = f"{main_public_directory}:https://petstore\\.swagger\\.io,{api_public_directory}:https://api\\.example\\.com,{fallback_public_directory}"
313
+
314
+ res: requests.Response = eval_fixtures(mitmproxy_request, public_directory_path=public_paths)
315
+ assert res is not None
316
+ assert res.raw.read() == main_file_contents
317
+
318
+ def test_wildcard_origin_matching(self, settings: Settings, main_public_directory: str, api_public_directory: str,
319
+ fallback_public_directory: str, api_file_contents: bytes):
320
+ """Test wildcard origin matching (*.example.com)."""
321
+ # Create request for subdomain
322
+ status = RequestBuilder(
323
+ method='GET',
324
+ request_headers={'accept': 'text/html'},
325
+ response_body='',
326
+ status_code=200,
327
+ url='https://sub.example.com',
328
+ ).with_settings(settings).build()[1]
329
+ assert status == 200
330
+
331
+ subdomain_request = Request.last()
332
+ subdomain_mitmproxy_request = MitmproxyRequestAdapter(subdomain_request).adapt()
333
+
334
+ # Use regex wildcard pattern
335
+ public_paths = f"{main_public_directory}:https://petstore\\.swagger\\.io,{api_public_directory}:https://.*\\.example\\.com,{fallback_public_directory}"
336
+
337
+ res: requests.Response = eval_fixtures(subdomain_mitmproxy_request, public_directory_path=public_paths)
338
+ assert res is not None
339
+ assert res.raw.read() == api_file_contents
340
+
341
+ def test_no_origin_match_uses_fallback(self, settings: Settings, main_public_directory: str,
342
+ api_public_directory: str, fallback_public_directory: str,
343
+ fallback_file_contents: bytes):
344
+ """Test that requests with no matching origin use fallback path."""
345
+ # Create request for unmatched origin
346
+ status = RequestBuilder(
347
+ method='GET',
348
+ request_headers={'accept': 'text/html'},
349
+ response_body='',
350
+ status_code=200,
351
+ url='https://unknown.com',
352
+ ).with_settings(settings).build()[1]
353
+ assert status == 200
354
+
355
+ unknown_request = Request.last()
356
+ unknown_mitmproxy_request = MitmproxyRequestAdapter(unknown_request).adapt()
357
+
358
+ public_paths = f"{main_public_directory}:https://petstore\\.swagger\\.io,{api_public_directory}:https://api\\.example\\.com,{fallback_public_directory}"
359
+
360
+ res: requests.Response = eval_fixtures(unknown_mitmproxy_request, public_directory_path=public_paths)
361
+ assert res is not None
362
+ assert res.raw.read() == fallback_file_contents
363
+
364
+ def test_multiple_fallback_paths(self, settings: Settings, fallback_public_directory: str,
365
+ fallback_file_contents: bytes):
366
+ """Test multiple fallback paths (no origin specified)."""
367
+ # Create request for unknown origin
368
+ status = RequestBuilder(
369
+ method='GET',
370
+ request_headers={'accept': 'text/html'},
371
+ response_body='',
372
+ status_code=200,
373
+ url='https://unknown.com',
374
+ ).with_settings(settings).build()[1]
375
+ assert status == 200
376
+
377
+ unknown_request = Request.last()
378
+ unknown_mitmproxy_request = MitmproxyRequestAdapter(unknown_request).adapt()
379
+
380
+ # Multiple fallback paths (no origin specified)
381
+ public_paths = f"/nonexistent/path,{fallback_public_directory}"
382
+
383
+ res: requests.Response = eval_fixtures(unknown_mitmproxy_request, public_directory_path=public_paths)
384
+ assert res is not None
385
+ assert res.raw.read() == fallback_file_contents
386
+
387
+ def test_full_url_origin_parsing_public_directories(self, settings: Settings):
388
+ """Test parsing of full URL origins (scheme://hostname:port) for public directories."""
389
+ # Create test directories
390
+ tmp_dir_path = DataDir.instance().tmp_dir_path
391
+ api_dir = os.path.join(tmp_dir_path, 'full_url_api')
392
+ fallback_dir = os.path.join(tmp_dir_path, 'full_url_fallback')
393
+ os.makedirs(api_dir, exist_ok=True)
394
+ os.makedirs(fallback_dir, exist_ok=True)
395
+
396
+ # Create test files
397
+ api_file = os.path.join(api_dir, 'index.html')
398
+ fallback_file = os.path.join(fallback_dir, 'index.html')
399
+
400
+ with open(api_file, 'w') as f:
401
+ f.write('API Content')
402
+ with open(fallback_file, 'w') as f:
403
+ f.write('Fallback Content')
404
+
405
+ # Test full URL with port
406
+ status = RequestBuilder(
407
+ method='GET',
408
+ request_headers={'accept': 'text/html'},
409
+ response_body='',
410
+ status_code=200,
411
+ url='https://api.example.com:8080/',
412
+ ).with_settings(settings).build()[1]
413
+ assert status == 200
414
+
415
+ request = Request.last()
416
+ mitmproxy_request = MitmproxyRequestAdapter(request).adapt()
417
+
418
+ # Test with regex pattern for scheme:hostname:port (request origin is https://api.example.com:8080)
419
+ public_paths = f"{api_dir}:https://api\\.example\\.com:8080,{fallback_dir}"
420
+
421
+ res: requests.Response = eval_fixtures(mitmproxy_request, public_directory_path=public_paths)
422
+ assert res is not None
423
+ assert res.raw.read() == b'API Content'
424
+
425
+ def test_hostname_port_origin_parsing_public_directories(self, settings: Settings):
426
+ """Test parsing of hostname:port origins for public directories."""
427
+ # Create test directories
428
+ tmp_dir_path = DataDir.instance().tmp_dir_path
429
+ api_dir = os.path.join(tmp_dir_path, 'hostname_port_api')
430
+ fallback_dir = os.path.join(tmp_dir_path, 'hostname_port_fallback')
431
+ os.makedirs(api_dir, exist_ok=True)
432
+ os.makedirs(fallback_dir, exist_ok=True)
433
+
434
+ # Create test files
435
+ api_file = os.path.join(api_dir, 'index.html')
436
+ fallback_file = os.path.join(fallback_dir, 'index.html')
437
+
438
+ with open(api_file, 'w') as f:
439
+ f.write('Hostname Port Content')
440
+ with open(fallback_file, 'w') as f:
441
+ f.write('Fallback Content')
442
+
443
+ # Test hostname:port format
444
+ status = RequestBuilder(
445
+ method='GET',
446
+ request_headers={'accept': 'text/html'},
447
+ response_body='',
448
+ status_code=200,
449
+ url='https://api.example.com:9000/',
450
+ ).with_settings(settings).build()[1]
451
+ assert status == 200
452
+
453
+ request = Request.last()
454
+ mitmproxy_request = MitmproxyRequestAdapter(request).adapt()
455
+
456
+ # Test with scheme:hostname:port origin format
457
+ public_paths = f"{api_dir}:https://api\\.example\\.com:9000,{fallback_dir}"
458
+
459
+ res: requests.Response = eval_fixtures(mitmproxy_request, public_directory_path=public_paths)
460
+ assert res is not None
461
+ assert res.raw.read() == b'Hostname Port Content'
462
+
463
+ class TestMultipleResponseFixtures():
464
+ @pytest.fixture(scope='class')
465
+ def request_method(self):
466
+ return 'GET'
467
+
468
+ @pytest.fixture(scope='class')
469
+ def api_request_url(self):
470
+ return 'https://api.example.com/users'
471
+
472
+ @pytest.fixture(scope='class')
473
+ def main_request_url(self):
474
+ return 'https://petstore.swagger.io/users'
475
+
476
+ @pytest.fixture(scope='class')
477
+ def api_created_request(self, settings: Settings, request_method: str, api_request_url: str):
478
+ status = RequestBuilder(
479
+ method=request_method,
480
+ request_headers={'accept': 'application/json'},
481
+ response_body='',
482
+ status_code=200,
483
+ url=api_request_url,
484
+ ).with_settings(settings).build()[1]
485
+ assert status == 200
486
+ return Request.last()
487
+
488
+ @pytest.fixture(scope='class')
489
+ def main_created_request(self, settings: Settings, request_method: str, main_request_url: str):
490
+ status = RequestBuilder(
491
+ method=request_method,
492
+ request_headers={'accept': 'application/json'},
493
+ response_body='',
494
+ status_code=200,
495
+ url=main_request_url,
496
+ ).with_settings(settings).build()[1]
497
+ assert status == 200
498
+ return Request.last()
499
+
500
+ @pytest.fixture()
501
+ def api_mitmproxy_request(self, api_created_request: Request) -> MitmproxyRequest:
502
+ return MitmproxyRequestAdapter(api_created_request).adapt()
503
+
504
+ @pytest.fixture()
505
+ def main_mitmproxy_request(self, main_created_request: Request) -> MitmproxyRequest:
506
+ return MitmproxyRequestAdapter(main_created_request).adapt()
507
+
508
+ @pytest.fixture(scope='class')
509
+ def api_fixture_file_path(self):
510
+ tmp_dir_path = DataDir.instance().tmp_dir_path
511
+ file_path = os.path.join(tmp_dir_path, 'api_users.json')
512
+ with open(file_path, 'w') as fp:
513
+ json.dump({"users": [{"id": 1, "name": "API User"}]}, fp)
514
+ return file_path
515
+
516
+ @pytest.fixture(scope='class')
517
+ def main_fixture_file_path(self):
518
+ tmp_dir_path = DataDir.instance().tmp_dir_path
519
+ file_path = os.path.join(tmp_dir_path, 'main_users.json')
520
+ with open(file_path, 'w') as fp:
521
+ json.dump({"users": [{"id": 1, "name": "Main User"}]}, fp)
522
+ return file_path
523
+
524
+ @pytest.fixture(scope='class')
525
+ def fallback_fixture_file_path(self):
526
+ tmp_dir_path = DataDir.instance().tmp_dir_path
527
+ file_path = os.path.join(tmp_dir_path, 'fallback_users.json')
528
+ with open(file_path, 'w') as fp:
529
+ json.dump({"users": [{"id": 1, "name": "Fallback User"}]}, fp)
530
+ return file_path
531
+
532
+ @pytest.fixture(scope='class')
533
+ def origin_specific_fixtures(self, api_fixture_file_path: str, main_fixture_file_path: str):
534
+ """Response fixtures with origin-specific routing."""
535
+ return {
536
+ '_origins': {
537
+ 'api.example.com': {
538
+ 'GET': {
539
+ '/users': {
540
+ 'path': api_fixture_file_path,
541
+ 'status_code': 200
542
+ }
543
+ }
544
+ },
545
+ 'petstore.swagger.io': {
546
+ 'GET': {
547
+ '/users': {
548
+ 'path': main_fixture_file_path,
549
+ 'status_code': 200
550
+ }
551
+ }
552
+ }
553
+ }
554
+ }
555
+
556
+ @pytest.fixture(scope='class')
557
+ def mixed_fixtures(self, api_fixture_file_path: str, fallback_fixture_file_path: str):
558
+ """Response fixtures with both origin-specific and fallback routes."""
559
+ return {
560
+ '_origins': {
561
+ 'api.example.com': {
562
+ 'GET': {
563
+ '/users': {
564
+ 'path': api_fixture_file_path,
565
+ 'status_code': 200
566
+ }
567
+ }
568
+ }
569
+ },
570
+ 'GET': {
571
+ '/users': {
572
+ 'path': fallback_fixture_file_path,
573
+ 'status_code': 200
574
+ }
575
+ }
576
+ }
577
+
578
+ def test_origin_specific_routing(self, api_mitmproxy_request: MitmproxyRequest,
579
+ api_fixture_file_path: str, main_fixture_file_path: str):
580
+ """Test that origin-specific fixtures are used when origin matches."""
581
+ # Create separate fixture files for each origin
582
+ tmp_dir_path = DataDir.instance().tmp_dir_path
583
+ api_fixtures_file = os.path.join(tmp_dir_path, 'api_origin_fixtures.yml')
584
+ main_fixtures_file = os.path.join(tmp_dir_path, 'main_origin_fixtures.yml')
585
+
586
+ # API-specific fixtures
587
+ api_fixtures_content = {
588
+ 'GET': {
589
+ '/users': {
590
+ 'path': api_fixture_file_path,
591
+ 'status_code': 200
592
+ }
593
+ }
594
+ }
595
+
596
+ # Main-specific fixtures
597
+ main_fixtures_content = {
598
+ 'GET': {
599
+ '/users': {
600
+ 'path': main_fixture_file_path,
601
+ 'status_code': 200
602
+ }
603
+ }
604
+ }
605
+
606
+ with open(api_fixtures_file, 'w') as f:
607
+ yaml.dump(api_fixtures_content, f)
608
+
609
+ with open(main_fixtures_file, 'w') as f:
610
+ yaml.dump(main_fixtures_content, f)
611
+
612
+ # Use response_fixtures_path with origin specification
613
+ fixtures_paths = f"{api_fixtures_file}:https://api\\.example\\.com,{main_fixtures_file}:https://petstore\\.swagger\\.io"
614
+
615
+ res: requests.Response = eval_fixtures(api_mitmproxy_request, response_fixtures_path=fixtures_paths)
616
+ assert res is not None
617
+ assert res.status_code == 200
618
+
619
+ # Check that API-specific content is returned
620
+ content = json.loads(res.raw.read().decode())
621
+ assert content['users'][0]['name'] == 'API User'
622
+
623
+ def test_different_origin_routing(self, main_mitmproxy_request: MitmproxyRequest,
624
+ api_fixture_file_path: str, main_fixture_file_path: str):
625
+ """Test that different origin gets different fixtures."""
626
+ # Create separate fixture files for each origin
627
+ tmp_dir_path = DataDir.instance().tmp_dir_path
628
+ api_fixtures_file = os.path.join(tmp_dir_path, 'api_diff_fixtures.yml')
629
+ main_fixtures_file = os.path.join(tmp_dir_path, 'main_diff_fixtures.yml')
630
+
631
+ # API-specific fixtures
632
+ api_fixtures_content = {
633
+ 'GET': {
634
+ '/users': {
635
+ 'path': api_fixture_file_path,
636
+ 'status_code': 200
637
+ }
638
+ }
639
+ }
640
+
641
+ # Main-specific fixtures
642
+ main_fixtures_content = {
643
+ 'GET': {
644
+ '/users': {
645
+ 'path': main_fixture_file_path,
646
+ 'status_code': 200
647
+ }
648
+ }
649
+ }
650
+
651
+ with open(api_fixtures_file, 'w') as f:
652
+ yaml.dump(api_fixtures_content, f)
653
+
654
+ with open(main_fixtures_file, 'w') as f:
655
+ yaml.dump(main_fixtures_content, f)
656
+
657
+ # Use response_fixtures_path with origin specification
658
+ fixtures_paths = f"{api_fixtures_file}:https://api\\.example\\.com,{main_fixtures_file}:https://petstore\\.swagger\\.io"
659
+
660
+ res: requests.Response = eval_fixtures(main_mitmproxy_request, response_fixtures_path=fixtures_paths)
661
+ assert res is not None
662
+ assert res.status_code == 200
663
+
664
+ # Check that main-specific content is returned
665
+ content = json.loads(res.raw.read().decode())
666
+ assert content['users'][0]['name'] == 'Main User'
667
+
668
+ def test_fallback_routing(self, main_mitmproxy_request: MitmproxyRequest, fallback_fixture_file_path: str):
669
+ """Test that fallback fixtures are used when no origin-specific match."""
670
+ # Create a fallback-only fixtures file
671
+ tmp_dir_path = DataDir.instance().tmp_dir_path
672
+ fallback_fixtures_file = os.path.join(tmp_dir_path, 'fallback_only_fixtures.yml')
673
+
674
+ fallback_fixtures_content = {
675
+ 'GET': {
676
+ '/users': {
677
+ 'path': fallback_fixture_file_path,
678
+ 'status_code': 200
679
+ }
680
+ }
681
+ }
682
+
683
+ with open(fallback_fixtures_file, 'w') as f:
684
+ yaml.dump(fallback_fixtures_content, f)
685
+
686
+ # Use a fixture file without origin specification (fallback)
687
+ res: requests.Response = eval_fixtures(main_mitmproxy_request, response_fixtures_path=fallback_fixtures_file)
688
+ assert res is not None
689
+ assert res.status_code == 200
690
+
691
+ # Check that fallback content is returned
692
+ content = json.loads(res.raw.read().decode())
693
+ assert content['users'][0]['name'] == 'Fallback User'
694
+
695
+ def test_origin_match_takes_precedence(self, api_mitmproxy_request: MitmproxyRequest,
696
+ api_fixture_file_path: str, fallback_fixture_file_path: str):
697
+ """Test that origin-specific fixtures take precedence over fallback."""
698
+ # Create separate fixture files
699
+ tmp_dir_path = DataDir.instance().tmp_dir_path
700
+ api_fixtures_file = os.path.join(tmp_dir_path, 'api_precedence_fixtures.yml')
701
+ fallback_fixtures_file = os.path.join(tmp_dir_path, 'fallback_precedence_fixtures.yml')
702
+
703
+ # API-specific fixtures
704
+ api_fixtures_content = {
705
+ 'GET': {
706
+ '/users': {
707
+ 'path': api_fixture_file_path,
708
+ 'status_code': 200
709
+ }
710
+ }
711
+ }
712
+
713
+ # Fallback fixtures
714
+ fallback_fixtures_content = {
715
+ 'GET': {
716
+ '/users': {
717
+ 'path': fallback_fixture_file_path,
718
+ 'status_code': 200
719
+ }
720
+ }
721
+ }
722
+
723
+ with open(api_fixtures_file, 'w') as f:
724
+ yaml.dump(api_fixtures_content, f)
725
+
726
+ with open(fallback_fixtures_file, 'w') as f:
727
+ yaml.dump(fallback_fixtures_content, f)
728
+
729
+ # Use response_fixtures_path with origin-specific first, fallback second
730
+ fixtures_paths = f"{api_fixtures_file}:https://api\\.example\\.com,{fallback_fixtures_file}"
731
+
732
+ res: requests.Response = eval_fixtures(api_mitmproxy_request, response_fixtures_path=fixtures_paths)
733
+ assert res is not None
734
+ assert res.status_code == 200
735
+
736
+ # Check that API-specific content is returned, not fallback
737
+ content = json.loads(res.raw.read().decode())
738
+ assert content['users'][0]['name'] == 'API User'
739
+
740
+ def test_wildcard_origin_matching(self, settings: Settings, api_fixture_file_path: str):
741
+ """Test wildcard origin matching for subdomains."""
742
+ # Create request for subdomain
743
+ status = RequestBuilder(
744
+ method='GET',
745
+ request_headers={'accept': 'application/json'},
746
+ response_body='',
747
+ status_code=200,
748
+ url='https://sub.example.com/users',
749
+ ).with_settings(settings).build()[1]
750
+ assert status == 200
751
+
752
+ subdomain_request = Request.last()
753
+ subdomain_mitmproxy_request = MitmproxyRequestAdapter(subdomain_request).adapt()
754
+
755
+ # Create wildcard fixtures file
756
+ tmp_dir_path = DataDir.instance().tmp_dir_path
757
+ wildcard_fixtures_file = os.path.join(tmp_dir_path, 'wildcard_fixtures.yml')
758
+
759
+ wildcard_fixtures_content = {
760
+ 'GET': {
761
+ '/users': {
762
+ 'path': api_fixture_file_path,
763
+ 'status_code': 200
764
+ }
765
+ }
766
+ }
767
+
768
+ with open(wildcard_fixtures_file, 'w') as f:
769
+ yaml.dump(wildcard_fixtures_content, f)
770
+
771
+ # Use regex wildcard pattern in response_fixtures_path
772
+ fixtures_paths = f"{wildcard_fixtures_file}:https://.*\\.example\\.com"
773
+
774
+ res: requests.Response = eval_fixtures(subdomain_mitmproxy_request, response_fixtures_path=fixtures_paths)
775
+ assert res is not None
776
+ assert res.status_code == 200
777
+
778
+ # Check that API content is returned for wildcard match
779
+ content = json.loads(res.raw.read().decode())
780
+ assert content['users'][0]['name'] == 'API User'
781
+
782
+ def test_response_fixtures_path_loading(self, api_mitmproxy_request: MitmproxyRequest, api_fixture_file_path: str):
783
+ """Test that response fixtures are loaded from response_fixtures_path parameter."""
784
+ # Create a temporary fixtures file
785
+ tmp_dir_path = DataDir.instance().tmp_dir_path
786
+ fixtures_file_path = os.path.join(tmp_dir_path, 'test_fixtures.yml')
787
+
788
+ fixtures_content = {
789
+ 'GET': {
790
+ '/users': {
791
+ 'path': api_fixture_file_path,
792
+ 'status_code': 200
793
+ }
794
+ }
795
+ }
796
+
797
+ with open(fixtures_file_path, 'w') as fp:
798
+ yaml.dump(fixtures_content, fp)
799
+
800
+ # Test loading fixtures via response_fixtures_path
801
+ res: requests.Response = eval_fixtures(api_mitmproxy_request, response_fixtures_path=fixtures_file_path)
802
+ assert res is not None
803
+ assert res.status_code == 200
804
+
805
+ # Check that correct content is returned
806
+ content = json.loads(res.raw.read().decode())
807
+ assert content['users'][0]['name'] == 'API User'
808
+
809
+ def test_response_fixtures_path_with_origin(self, api_mitmproxy_request: MitmproxyRequest,
810
+ main_mitmproxy_request: MitmproxyRequest,
811
+ api_fixture_file_path: str, main_fixture_file_path: str):
812
+ """Test that response_fixtures_path supports origin-specific routing."""
813
+ # Create temporary fixtures files
814
+ tmp_dir_path = DataDir.instance().tmp_dir_path
815
+ api_fixtures_file = os.path.join(tmp_dir_path, 'api_fixtures.yml')
816
+ main_fixtures_file = os.path.join(tmp_dir_path, 'main_fixtures.yml')
817
+
818
+ api_fixtures_content = {
819
+ 'GET': {
820
+ '/users': {
821
+ 'path': api_fixture_file_path,
822
+ 'status_code': 200
823
+ }
824
+ }
825
+ }
826
+
827
+ main_fixtures_content = {
828
+ 'GET': {
829
+ '/users': {
830
+ 'path': main_fixture_file_path,
831
+ 'status_code': 200
832
+ }
833
+ }
834
+ }
835
+
836
+ with open(api_fixtures_file, 'w') as fp:
837
+ yaml.dump(api_fixtures_content, fp)
838
+
839
+ with open(main_fixtures_file, 'w') as fp:
840
+ yaml.dump(main_fixtures_content, fp)
841
+
842
+ # Test with comma-separated paths with origins
843
+ fixtures_paths = f"{api_fixtures_file}:https://api\\.example\\.com,{main_fixtures_file}:https://petstore\\.swagger\\.io"
844
+
845
+ # Test API request
846
+ api_res: requests.Response = eval_fixtures(api_mitmproxy_request, response_fixtures_path=fixtures_paths)
847
+ assert api_res is not None
848
+ assert api_res.status_code == 200
849
+ api_content = json.loads(api_res.raw.read().decode())
850
+ assert api_content['users'][0]['name'] == 'API User'
851
+
852
+ # Test main request
853
+ main_res: requests.Response = eval_fixtures(main_mitmproxy_request, response_fixtures_path=fixtures_paths)
854
+ assert main_res is not None
855
+ assert main_res.status_code == 200
856
+ main_content = json.loads(main_res.raw.read().decode())
857
+ assert main_content['users'][0]['name'] == 'Main User'
858
+
859
+ def test_lazy_evaluation_stops_at_first_match(self, api_mitmproxy_request: MitmproxyRequest, api_fixture_file_path: str):
860
+ """Test that lazy evaluation stops at the first matching fixture and doesn't process subsequent files."""
861
+ # Create multiple fixtures files
862
+ tmp_dir_path = DataDir.instance().tmp_dir_path
863
+ first_fixtures_file = os.path.join(tmp_dir_path, 'first_fixtures.yml')
864
+ second_fixtures_file = os.path.join(tmp_dir_path, 'second_fixtures.yml')
865
+ invalid_fixtures_file = os.path.join(tmp_dir_path, 'invalid_fixtures.yml')
866
+
867
+ # First file - should match and be used
868
+ first_fixtures_content = {
869
+ 'GET': {
870
+ '/users': {
871
+ 'path': api_fixture_file_path,
872
+ 'status_code': 201 # Different status to verify this file is used
873
+ }
874
+ }
875
+ }
876
+
877
+ # Second file - should not be processed since first file matches
878
+ second_fixtures_content = {
879
+ 'GET': {
880
+ '/users': {
881
+ 'path': api_fixture_file_path,
882
+ 'status_code': 202
883
+ }
884
+ }
885
+ }
886
+
887
+ with open(first_fixtures_file, 'w') as fp:
888
+ yaml.dump(first_fixtures_content, fp)
889
+
890
+ with open(second_fixtures_file, 'w') as fp:
891
+ yaml.dump(second_fixtures_content, fp)
892
+
893
+ # Create an invalid YAML file that would cause an error if processed
894
+ with open(invalid_fixtures_file, 'w') as fp:
895
+ fp.write("invalid: yaml: content: [unclosed")
896
+
897
+ # Test with multiple paths - first should match, others should be ignored
898
+ fixtures_paths = f"{first_fixtures_file}:https://api\\.example\\.com,{second_fixtures_file}:https://api\\.example\\.com,{invalid_fixtures_file}:https://api\\.example\\.com"
899
+
900
+ # Should use first file and return status 201
901
+ res: requests.Response = eval_fixtures(api_mitmproxy_request, response_fixtures_path=fixtures_paths)
902
+ assert res is not None
903
+ assert res.status_code == 201 # Should be from first file, not second
904
+
905
+ # Verify content is from the expected file
906
+ content = json.loads(res.raw.read().decode())
907
+ assert content['users'][0]['name'] == 'API User'
908
+
909
+ def test_origin_mismatch_skips_file(self, api_mitmproxy_request: MitmproxyRequest,
910
+ main_mitmproxy_request: MitmproxyRequest,
911
+ api_fixture_file_path: str, main_fixture_file_path: str):
912
+ """Test that files with non-matching origins are skipped without being loaded."""
913
+ # Create fixtures files
914
+ tmp_dir_path = DataDir.instance().tmp_dir_path
915
+ wrong_origin_file = os.path.join(tmp_dir_path, 'wrong_origin.yml')
916
+ correct_origin_file = os.path.join(tmp_dir_path, 'correct_origin.yml')
917
+
918
+ # File with wrong origin - should be skipped
919
+ wrong_origin_content = {
920
+ 'GET': {
921
+ '/users': {
922
+ 'path': main_fixture_file_path, # Different content
923
+ 'status_code': 404 # Different status to verify this isn't used
924
+ }
925
+ }
926
+ }
927
+
928
+ # File with correct origin - should be used
929
+ correct_origin_content = {
930
+ 'GET': {
931
+ '/users': {
932
+ 'path': api_fixture_file_path,
933
+ 'status_code': 200
934
+ }
935
+ }
936
+ }
937
+
938
+ with open(wrong_origin_file, 'w') as fp:
939
+ yaml.dump(wrong_origin_content, fp)
940
+
941
+ with open(correct_origin_file, 'w') as fp:
942
+ yaml.dump(correct_origin_content, fp)
943
+
944
+ # Test with API request - wrong origin should be skipped, correct origin should be used
945
+ fixtures_paths = f"{wrong_origin_file}:https://petstore\\.swagger\\.io,{correct_origin_file}:https://api\\.example\\.com"
946
+
947
+ res: requests.Response = eval_fixtures(api_mitmproxy_request, response_fixtures_path=fixtures_paths)
948
+ assert res is not None
949
+ assert res.status_code == 200 # Should be from correct origin file, not wrong origin
950
+
951
+ # Verify content is from the API file (correct origin), not main file (wrong origin)
952
+ content = json.loads(res.raw.read().decode())
953
+ assert content['users'][0]['name'] == 'API User'
954
+
955
+ class TestFullOriginFormatParsing():
956
+ @pytest.fixture(scope='class')
957
+ def tmp_dir_path(self):
958
+ return DataDir.instance().tmp_dir_path
959
+
960
+ def test_full_url_origin_parsing_response_fixtures(self, settings: Settings, tmp_dir_path: str):
961
+ """Test parsing of full URL origins for response fixtures."""
962
+ # Create test fixture files
963
+ api_fixture_file = os.path.join(tmp_dir_path, 'full_url_api_fixture.json')
964
+ with open(api_fixture_file, 'w') as f:
965
+ json.dump({"message": "Full URL API Response"}, f)
966
+
967
+ api_fixtures_file = os.path.join(tmp_dir_path, 'full_url_api_fixtures.yml')
968
+ fixtures_content = {
969
+ 'GET': {
970
+ '/api/data': {
971
+ 'path': api_fixture_file,
972
+ 'status_code': 200
973
+ }
974
+ }
975
+ }
976
+ with open(api_fixtures_file, 'w') as f:
977
+ yaml.dump(fixtures_content, f)
978
+
979
+ # Test full URL with port
980
+ status = RequestBuilder(
981
+ method='GET',
982
+ request_headers={'accept': 'application/json'},
983
+ response_body='',
984
+ status_code=200,
985
+ url='https://api.example.com:8080/api/data',
986
+ ).with_settings(settings).build()[1]
987
+ assert status == 200
988
+
989
+ request = Request.last()
990
+ mitmproxy_request = MitmproxyRequestAdapter(request).adapt()
991
+
992
+ # Test with regex pattern for scheme:hostname:port (request origin is https://api.example.com:8080)
993
+ fixtures_paths = f"{api_fixtures_file}:https://api\\.example\\.com:8080"
994
+
995
+ res: requests.Response = eval_fixtures(mitmproxy_request, response_fixtures_path=fixtures_paths)
996
+ assert res is not None
997
+ assert res.status_code == 200
998
+ content = json.loads(res.raw.read().decode())
999
+ assert content['message'] == 'Full URL API Response'
1000
+
1001
+ def test_origin_matching_with_different_schemes(self, settings: Settings, tmp_dir_path: str):
1002
+ """Test that origin matching works regardless of scheme differences."""
1003
+ # Create test fixture files
1004
+ api_fixture_file = os.path.join(tmp_dir_path, 'scheme_test_fixture.json')
1005
+ with open(api_fixture_file, 'w') as f:
1006
+ json.dump({"message": "Scheme Test Response"}, f)
1007
+
1008
+ api_fixtures_file = os.path.join(tmp_dir_path, 'scheme_test_fixtures.yml')
1009
+ fixtures_content = {
1010
+ 'GET': {
1011
+ '/test': {
1012
+ 'path': api_fixture_file,
1013
+ 'status_code': 200
1014
+ }
1015
+ }
1016
+ }
1017
+ with open(api_fixtures_file, 'w') as f:
1018
+ yaml.dump(fixtures_content, f)
1019
+
1020
+ # Test HTTP request
1021
+ status = RequestBuilder(
1022
+ method='GET',
1023
+ request_headers={'accept': 'application/json'},
1024
+ response_body='',
1025
+ status_code=200,
1026
+ url='http://api.example.com:8080/test', # HTTP request
1027
+ ).with_settings(settings).build()[1]
1028
+ assert status == 200
1029
+
1030
+ request = Request.last()
1031
+ mitmproxy_request = MitmproxyRequestAdapter(request).adapt()
1032
+
1033
+ # Test with regex pattern for scheme:hostname:port (request origin is http://api.example.com:8080)
1034
+ fixtures_paths = f"{api_fixtures_file}:http://api\\.example\\.com:8080"
1035
+
1036
+ res: requests.Response = eval_fixtures(mitmproxy_request, response_fixtures_path=fixtures_paths)
1037
+ assert res is not None
1038
+ assert res.status_code == 200
1039
+ content = json.loads(res.raw.read().decode())
1040
+ assert content['message'] == 'Scheme Test Response'
1041
+
1042
+ def test_wildcard_matching_with_full_origins(self, settings: Settings, tmp_dir_path: str):
1043
+ """Test wildcard matching with full origin URLs."""
1044
+ # Create test fixture files
1045
+ wildcard_fixture_file = os.path.join(tmp_dir_path, 'wildcard_test_fixture.json')
1046
+ with open(wildcard_fixture_file, 'w') as f:
1047
+ json.dump({"message": "Wildcard Match Response"}, f)
1048
+
1049
+ wildcard_fixtures_file = os.path.join(tmp_dir_path, 'wildcard_test_fixtures.yml')
1050
+ fixtures_content = {
1051
+ 'GET': {
1052
+ '/wildcard': {
1053
+ 'path': wildcard_fixture_file,
1054
+ 'status_code': 200
1055
+ }
1056
+ }
1057
+ }
1058
+ with open(wildcard_fixtures_file, 'w') as f:
1059
+ yaml.dump(fixtures_content, f)
1060
+
1061
+ # Test subdomain request
1062
+ status = RequestBuilder(
1063
+ method='GET',
1064
+ request_headers={'accept': 'application/json'},
1065
+ response_body='',
1066
+ status_code=200,
1067
+ url='https://sub.example.com:8080/wildcard',
1068
+ ).with_settings(settings).build()[1]
1069
+ assert status == 200
1070
+
1071
+ request = Request.last()
1072
+ mitmproxy_request = MitmproxyRequestAdapter(request).adapt()
1073
+
1074
+ # Test with regex wildcard origin pattern
1075
+ fixtures_paths = f"{wildcard_fixtures_file}:https://.*\\.example\\.com:8080"
1076
+
1077
+ res: requests.Response = eval_fixtures(mitmproxy_request, response_fixtures_path=fixtures_paths)
1078
+ assert res is not None
1079
+ assert res.status_code == 200
1080
+ content = json.loads(res.raw.read().decode())
1081
+ assert content['message'] == 'Wildcard Match Response'