stoobly-agent 1.9.12__py3-none-any.whl → 1.10.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/__init__.py +4 -20
  3. stoobly_agent/app/api/application_http_request_handler.py +5 -2
  4. stoobly_agent/app/api/configs_controller.py +3 -3
  5. stoobly_agent/app/cli/decorators/exec.py +1 -1
  6. stoobly_agent/app/cli/helpers/handle_config_update_service.py +4 -0
  7. stoobly_agent/app/cli/intercept_cli.py +40 -7
  8. stoobly_agent/app/cli/scaffold/app_command.py +4 -0
  9. stoobly_agent/app/cli/scaffold/app_config.py +21 -3
  10. stoobly_agent/app/cli/scaffold/app_create_command.py +109 -2
  11. stoobly_agent/app/cli/scaffold/constants.py +14 -3
  12. stoobly_agent/app/cli/scaffold/docker/constants.py +4 -6
  13. stoobly_agent/app/cli/scaffold/docker/service/build_decorator.py +2 -2
  14. stoobly_agent/app/cli/scaffold/docker/service/builder.py +36 -10
  15. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -27
  16. stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +25 -0
  17. stoobly_agent/app/cli/scaffold/docker/workflow/decorators_factory.py +7 -2
  18. stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +42 -0
  19. stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +26 -0
  20. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +9 -10
  21. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +5 -8
  22. stoobly_agent/app/cli/scaffold/service_config.py +133 -34
  23. stoobly_agent/app/cli/scaffold/service_create_command.py +11 -2
  24. stoobly_agent/app/cli/scaffold/service_dependency.py +51 -0
  25. stoobly_agent/app/cli/scaffold/service_docker_compose.py +3 -3
  26. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +10 -7
  27. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  28. stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +2 -2
  29. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/configure +1 -1
  30. stoobly_agent/app/cli/scaffold/templates/app/build/mock/docker-compose.yml +16 -6
  31. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/configure +26 -1
  32. stoobly_agent/app/cli/scaffold/templates/app/build/record/docker-compose.yml +16 -6
  33. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/configure +1 -1
  34. stoobly_agent/app/cli/scaffold/templates/app/build/test/docker-compose.yml +16 -6
  35. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -2
  36. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/bin/configure +1 -1
  37. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +16 -10
  38. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/bin/configure +1 -1
  39. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +16 -10
  40. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/bin/configure +1 -1
  41. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +16 -10
  42. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml +2 -1
  43. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +6 -3
  44. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +6 -4
  45. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.configure +21 -1
  46. stoobly_agent/app/cli/scaffold/templates/constants.py +4 -0
  47. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.Dockerfile.cypress +22 -0
  48. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.docker-compose.test.yml +19 -0
  49. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.Dockerfile.playwright +33 -0
  50. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.docker-compose.test.yml +18 -0
  51. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.entrypoint.sh +11 -0
  52. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +2 -10
  53. stoobly_agent/app/cli/scaffold/templates/workflow/mock/docker-compose.yml +17 -0
  54. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +19 -45
  55. stoobly_agent/app/cli/scaffold/templates/workflow/record/docker-compose.yml +17 -0
  56. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +2 -10
  57. stoobly_agent/app/cli/scaffold/templates/workflow/test/docker-compose.yml +17 -0
  58. stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
  59. stoobly_agent/app/cli/scaffold/workflow_run_command.py +1 -1
  60. stoobly_agent/app/cli/scaffold_cli.py +85 -96
  61. stoobly_agent/app/proxy/handle_record_service.py +12 -3
  62. stoobly_agent/app/proxy/handle_replay_service.py +14 -2
  63. stoobly_agent/app/proxy/intercept_settings.py +12 -8
  64. stoobly_agent/app/proxy/record/upload_request_service.py +5 -8
  65. stoobly_agent/app/proxy/replay/replay_request_service.py +3 -0
  66. stoobly_agent/app/proxy/run.py +3 -28
  67. stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -2
  68. stoobly_agent/app/proxy/utils/minimize_headers.py +47 -0
  69. stoobly_agent/app/proxy/utils/publish_change_service.py +22 -24
  70. stoobly_agent/app/proxy/utils/strategy.py +16 -0
  71. stoobly_agent/app/settings/__init__.py +15 -6
  72. stoobly_agent/app/settings/data_rules.py +25 -1
  73. stoobly_agent/app/settings/intercept_settings.py +5 -2
  74. stoobly_agent/app/settings/types/__init__.py +0 -1
  75. stoobly_agent/app/settings/ui_settings.py +5 -5
  76. stoobly_agent/cli.py +41 -16
  77. stoobly_agent/config/constants/custom_headers.py +1 -0
  78. stoobly_agent/config/constants/env_vars.py +4 -3
  79. stoobly_agent/config/constants/record_strategy.py +6 -0
  80. stoobly_agent/config/data_dir.py +1 -0
  81. stoobly_agent/config/settings.yml.sample +2 -3
  82. stoobly_agent/lib/logger.py +15 -5
  83. stoobly_agent/public/index.html +1 -1
  84. stoobly_agent/public/main-es2015.5a9aa16433404c3f423a.js +1 -0
  85. stoobly_agent/public/main-es5.5a9aa16433404c3f423a.js +1 -0
  86. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +231 -1
  87. stoobly_agent/test/app/cli/scaffold/cli_invoker.py +3 -2
  88. stoobly_agent/test/app/cli/scaffold/cli_test.py +3 -3
  89. stoobly_agent/test/app/cli/scaffold/e2e_test.py +11 -11
  90. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  91. stoobly_agent/test/app/proxy/utils/minimize_headers_test.py +342 -0
  92. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/METADATA +2 -1
  93. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/RECORD +96 -80
  94. stoobly_agent/public/main-es2015.089b46f303768fbe864f.js +0 -1
  95. stoobly_agent/public/main-es5.089b46f303768fbe864f.js +0 -1
  96. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/LICENSE +0 -0
  97. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/WHEEL +0 -0
  98. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,342 @@
1
+ import copy
2
+ import time
3
+
4
+ from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
5
+ from mitmproxy.http import Request, Response, Headers
6
+ from stoobly_agent.app.proxy.utils.minimize_headers import (
7
+ minimize_headers,
8
+ minimize_request_headers,
9
+ minimize_response_headers,
10
+ REQUEST_HEADERS_ALLOWLIST,
11
+ RESPONSE_HEADERS_ALLOWLIST,
12
+ )
13
+
14
+
15
+ class TestMinimizeHeaders():
16
+
17
+ def test_minimize_request_headers(self):
18
+ headers_stub = [
19
+ # Essential headers (should be preserved)
20
+ (b"Host", b"api.example.com"),
21
+ (b"User-Agent", b"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"),
22
+ (b"Accept", b"application/json, text/plain, */*"),
23
+ (b"Accept-Language", b"en-US,en;q=0.9"),
24
+ (b"Accept-Encoding", b"gzip, deflate, br"),
25
+ (b"Content-Type", b"application/json"),
26
+ (b"Content-Length", b"42"),
27
+ (b"Origin", b"https://example.com"),
28
+ (b"Referer", b"https://example.com/page"),
29
+
30
+ # Headers that should be removed
31
+ (b"Cookie", b"session=abc123; user=john"),
32
+ (b"Authorization", b"Bearer token123"),
33
+ (b"X-Request-ID", b"req-uuid-123"),
34
+ (b"X-Forwarded-For", b"192.168.1.1"),
35
+ (b"X-Custom-Header", b"custom-value"),
36
+ (b"Sec-Fetch-Mode", b"cors"),
37
+ (b"Sec-Fetch-Dest", b"empty"),
38
+ (b"DNT", b"1"),
39
+ ]
40
+ headers = Headers(headers_stub)
41
+
42
+ flow_stub = MitmproxyHTTPFlow(client_conn=None, server_conn=None)
43
+ flow_stub.request = Request(
44
+ host="api.example.com",
45
+ port=443,
46
+ method="POST",
47
+ scheme="https",
48
+ authority="api.example.com",
49
+ path="/api/v1/data",
50
+ http_version="HTTP/1.1",
51
+ headers=headers,
52
+ content=b'{"data": "test"}',
53
+ trailers=None,
54
+ timestamp_start=time.time(),
55
+ timestamp_end=time.time() + 1,
56
+ )
57
+
58
+ old_headers = copy.deepcopy(flow_stub.request.headers)
59
+
60
+ minimize_request_headers(flow_stub)
61
+
62
+ new_headers = flow_stub.request.headers
63
+
64
+ # Non-essential headers should be removed
65
+ assert old_headers != new_headers
66
+ assert "Cookie" in old_headers
67
+ assert "Cookie" not in new_headers
68
+ assert "Authorization" not in new_headers
69
+ assert "X-Request-ID" not in new_headers
70
+ assert "X-Forwarded-For" not in new_headers
71
+ assert "X-Custom-Header" not in new_headers
72
+ assert "Sec-Fetch-Mode" not in new_headers
73
+ assert "Sec-Fetch-Dest" not in new_headers
74
+ assert "DNT" not in new_headers
75
+
76
+ # Essential headers should remain
77
+ assert "Host" in new_headers
78
+ assert "User-Agent" in new_headers
79
+ assert "Accept" in new_headers
80
+ assert "Accept-Language" in new_headers
81
+ assert "Accept-Encoding" in new_headers
82
+ assert "Content-Type" in new_headers
83
+ assert "Content-Length" in new_headers
84
+ assert "Origin" in new_headers
85
+ assert "Referer" in new_headers
86
+
87
+ def test_minimize_response_headers(self):
88
+ headers_stub = [
89
+ # Essential headers (should be preserved)
90
+ (b"Content-Type", b"application/json"),
91
+ (b"Content-Length", b"1234"),
92
+ (b"Date", b"Wed, 21 Oct 2015 07:28:00 GMT"),
93
+ (b"Server", b"nginx/1.18.0"),
94
+ (b"Transfer-Encoding", b"chunked"),
95
+
96
+ # Headers that should be removed
97
+ (b"Set-Cookie", b"session=new; HttpOnly; Secure"),
98
+ (b"X-Powered-By", b"Express"),
99
+ (b"X-Request-ID", b"resp-uuid-456"),
100
+ (b"Access-Control-Allow-Origin", b"*"),
101
+ (b"Vary", b"Accept-Encoding"),
102
+ (b"ETag", b'"abc123"'),
103
+ (b"Last-Modified", b"Tue, 20 Oct 2015 07:28:00 GMT"),
104
+ ]
105
+ headers = Headers(headers_stub)
106
+ flow_stub = MitmproxyHTTPFlow(client_conn=None, server_conn=None)
107
+ flow_stub.response = Response(
108
+ http_version="HTTP/1.1",
109
+ status_code=200,
110
+ reason="OK",
111
+ headers=headers,
112
+ content=b"{}",
113
+ trailers=None,
114
+ timestamp_start=time.time(),
115
+ timestamp_end=time.time() + 1,
116
+ )
117
+ old_headers = copy.deepcopy(flow_stub.response.headers)
118
+
119
+ minimize_response_headers(flow_stub)
120
+
121
+ new_headers = flow_stub.response.headers
122
+ # Non-essential headers should be removed
123
+ assert old_headers != new_headers
124
+ assert "Set-Cookie" in old_headers
125
+ assert "Set-Cookie" not in new_headers
126
+ assert "X-Powered-By" not in new_headers
127
+ assert "X-Request-ID" not in new_headers
128
+ assert "Access-Control-Allow-Origin" not in new_headers
129
+ assert "Vary" not in new_headers
130
+ assert "ETag" not in new_headers
131
+ assert "Last-Modified" not in new_headers
132
+
133
+ # Essential headers should remain
134
+ assert "Content-Type" in new_headers
135
+ assert "Content-Length" in new_headers
136
+ assert "Date" in new_headers
137
+ assert "Server" in new_headers
138
+ assert "Transfer-Encoding" in new_headers
139
+
140
+ # Verify exact header count (Content-Type, Content-Length, Date, Server, Transfer-Encoding)
141
+ assert len(new_headers) == 5
142
+
143
+ def test_minimize_headers(self):
144
+ req_headers = Headers([
145
+ (b"Host", b"example.com"),
146
+ (b"X-Remove-Me", b"bye")
147
+ ])
148
+ res_headers = Headers([
149
+ (b"Content-Type", b"text/html"),
150
+ (b"X-Remove-Me", b"bye")
151
+ ])
152
+ flow_stub = MitmproxyHTTPFlow(client_conn=None, server_conn=None)
153
+ flow_stub.request = Request(
154
+ host="example.com",
155
+ port=80,
156
+ method="GET",
157
+ scheme="http",
158
+ authority="example.com",
159
+ path="/",
160
+ http_version="HTTP/1.1",
161
+ headers=req_headers,
162
+ content=None,
163
+ trailers=None,
164
+ timestamp_start=time.time(),
165
+ timestamp_end=time.time() + 1,
166
+ )
167
+ flow_stub.response = Response(
168
+ http_version="HTTP/1.1",
169
+ status_code=200,
170
+ reason="OK",
171
+ headers=res_headers,
172
+ content=b"<html></html>",
173
+ trailers=None,
174
+ timestamp_start=time.time(),
175
+ timestamp_end=time.time() + 1,
176
+ )
177
+ old_req_headers = copy.deepcopy(flow_stub.request.headers)
178
+ old_res_headers = copy.deepcopy(flow_stub.response.headers)
179
+
180
+ minimize_headers(flow_stub)
181
+
182
+ # Verify changes occurred
183
+ assert old_req_headers != flow_stub.request.headers
184
+ assert old_res_headers != flow_stub.response.headers
185
+
186
+ # Verify specific header removal and retention
187
+ assert "X-Remove-Me" not in flow_stub.request.headers
188
+ assert "X-Remove-Me" not in flow_stub.response.headers
189
+ assert "Host" in flow_stub.request.headers
190
+ assert "Content-Type" in flow_stub.response.headers
191
+
192
+ # Verify header counts
193
+ assert len(flow_stub.request.headers) == 1
194
+ assert len(flow_stub.response.headers) == 1
195
+
196
+ def test_empty_headers(self):
197
+ """Test minimize functions with empty headers"""
198
+ flow_stub = MitmproxyHTTPFlow(client_conn=None, server_conn=None)
199
+ flow_stub.request = Request(
200
+ host="example.com",
201
+ port=80,
202
+ method="GET",
203
+ scheme="http",
204
+ authority="example.com",
205
+ path="/",
206
+ http_version="HTTP/1.1",
207
+ headers=Headers(),
208
+ content=None,
209
+ trailers=None,
210
+ timestamp_start=time.time(),
211
+ timestamp_end=time.time() + 1,
212
+ )
213
+ flow_stub.response = Response(
214
+ http_version="HTTP/1.1",
215
+ status_code=200,
216
+ reason="OK",
217
+ headers=Headers(),
218
+ content=b"",
219
+ trailers=None,
220
+ timestamp_start=time.time(),
221
+ timestamp_end=time.time() + 1,
222
+ )
223
+
224
+ # Should not raise any errors
225
+ minimize_headers(flow_stub)
226
+ assert len(flow_stub.request.headers) == 0
227
+ assert len(flow_stub.response.headers) == 0
228
+
229
+ def test_case_sensitivity(self):
230
+ """Test that header matching is case-insensitive"""
231
+ headers_stub = [
232
+ (b"host", b"example.com"), # lowercase
233
+ (b"USER-AGENT", b"test-agent"), # uppercase
234
+ (b"Content-Type", b"application/json"), # mixed case
235
+ (b"x-remove-me", b"bye"), # should be removed
236
+ ]
237
+ headers = Headers(headers_stub)
238
+
239
+ flow_stub = MitmproxyHTTPFlow(client_conn=None, server_conn=None)
240
+ flow_stub.request = Request(
241
+ host="example.com",
242
+ port=80,
243
+ method="POST",
244
+ scheme="http",
245
+ authority="example.com",
246
+ path="/",
247
+ http_version="HTTP/1.1",
248
+ headers=headers,
249
+ content=b'{"test": "data"}',
250
+ trailers=None,
251
+ timestamp_start=time.time(),
252
+ timestamp_end=time.time() + 1,
253
+ )
254
+
255
+ minimize_request_headers(flow_stub)
256
+
257
+ # Case-insensitive matching should preserve these headers
258
+ assert "host" in flow_stub.request.headers
259
+ assert "USER-AGENT" in flow_stub.request.headers
260
+ assert "Content-Type" in flow_stub.request.headers
261
+
262
+ # Non-allowed header should be removed regardless of case
263
+ assert "x-remove-me" not in flow_stub.request.headers
264
+
265
+ def test_minimize_headers_no_response(self):
266
+ """Test minimize_headers when response is None"""
267
+ flow_stub = MitmproxyHTTPFlow(client_conn=None, server_conn=None)
268
+ flow_stub.request = Request(
269
+ host="example.com",
270
+ port=80,
271
+ method="GET",
272
+ scheme="http",
273
+ authority="example.com",
274
+ path="/",
275
+ http_version="HTTP/1.1",
276
+ headers=Headers([(b"Host", b"example.com"), (b"X-Remove", b"me")]),
277
+ content=None,
278
+ trailers=None,
279
+ timestamp_start=time.time(),
280
+ timestamp_end=time.time() + 1,
281
+ )
282
+ flow_stub.response = None
283
+
284
+ # Should not raise an error when response is None
285
+ minimize_request_headers(flow_stub)
286
+ assert "Host" in flow_stub.request.headers
287
+ assert "X-Remove" not in flow_stub.request.headers
288
+
289
+ def test_minimize_headers_no_request(self):
290
+ """Test minimize_headers when request is None"""
291
+ flow_stub = MitmproxyHTTPFlow(client_conn=None, server_conn=None)
292
+ flow_stub.request = None
293
+ flow_stub.response = Response(
294
+ http_version="HTTP/1.1",
295
+ status_code=200,
296
+ reason="OK",
297
+ headers=Headers([(b"Content-Type", b"text/html"), (b"X-Remove", b"me")]),
298
+ content=b"<html></html>",
299
+ trailers=None,
300
+ timestamp_start=time.time(),
301
+ timestamp_end=time.time() + 1,
302
+ )
303
+
304
+ # Should not raise an error when request is None
305
+ minimize_response_headers(flow_stub)
306
+ assert "Content-Type" in flow_stub.response.headers
307
+ assert "X-Remove" not in flow_stub.response.headers
308
+
309
+ def test_header_ordering_preserved(self):
310
+ """Test that header ordering is preserved after minimization"""
311
+ headers_stub = [
312
+ (b"Host", b"example.com"),
313
+ (b"User-Agent", b"test-agent"),
314
+ (b"Accept", b"application/json"),
315
+ (b"X-Remove-1", b"bye"),
316
+ (b"Content-Type", b"application/json"),
317
+ (b"X-Remove-2", b"bye"),
318
+ ]
319
+ headers = Headers(headers_stub)
320
+
321
+ flow_stub = MitmproxyHTTPFlow(client_conn=None, server_conn=None)
322
+ flow_stub.request = Request(
323
+ host="example.com",
324
+ port=80,
325
+ method="POST",
326
+ scheme="http",
327
+ authority="example.com",
328
+ path="/",
329
+ http_version="HTTP/1.1",
330
+ headers=headers,
331
+ content=b'{"test": "data"}',
332
+ trailers=None,
333
+ timestamp_start=time.time(),
334
+ timestamp_end=time.time() + 1,
335
+ )
336
+
337
+ minimize_request_headers(flow_stub)
338
+
339
+ # Check that remaining headers maintain relative order
340
+ remaining_headers = list(flow_stub.request.headers.keys())
341
+ expected_order = ["Host", "User-Agent", "Accept", "Content-Type"]
342
+ assert remaining_headers == expected_order
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: stoobly-agent
3
- Version: 1.9.12
3
+ Version: 1.10.1
4
4
  Summary: Record, mock, and test HTTP(s) requests. CLI agent for Stoobly
5
5
  License: Apache-2.0
6
6
  Author: Matt Le
@@ -17,6 +17,7 @@ Requires-Dist: diff-match-patch (>=v20241021,<20241022)
17
17
  Requires-Dist: distro (>=1.9.0,<1.10)
18
18
  Requires-Dist: dnspython (>=2.7.0,<2.8)
19
19
  Requires-Dist: docker (>=7.1.0,<8.0)
20
+ Requires-Dist: filelock (>=3.19.1,<4.0.0)
20
21
  Requires-Dist: httptools (>=0.4.0)
21
22
  Requires-Dist: jmespath (>=1.0.0)
22
23
  Requires-Dist: mergedeep (>=1.3,<1.4)