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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/__init__.py +4 -20
  3. stoobly_agent/app/api/configs_controller.py +3 -3
  4. stoobly_agent/app/cli/decorators/exec.py +1 -1
  5. stoobly_agent/app/cli/helpers/handle_config_update_service.py +4 -0
  6. stoobly_agent/app/cli/helpers/shell.py +0 -10
  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 -0
  12. stoobly_agent/app/cli/scaffold/docker/constants.py +4 -6
  13. stoobly_agent/app/cli/scaffold/docker/service/builder.py +19 -4
  14. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -18
  15. stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +24 -0
  16. stoobly_agent/app/cli/scaffold/docker/workflow/decorators_factory.py +7 -2
  17. stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +42 -0
  18. stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +26 -0
  19. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +9 -10
  20. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +5 -8
  21. stoobly_agent/app/cli/scaffold/service_config.py +144 -21
  22. stoobly_agent/app/cli/scaffold/service_create_command.py +11 -2
  23. stoobly_agent/app/cli/scaffold/service_dependency.py +51 -0
  24. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  25. stoobly_agent/app/cli/scaffold/templates/app/build/mock/docker-compose.yml +16 -6
  26. stoobly_agent/app/cli/scaffold/templates/app/build/record/docker-compose.yml +16 -6
  27. stoobly_agent/app/cli/scaffold/templates/app/build/test/docker-compose.yml +16 -6
  28. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +16 -10
  29. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +16 -10
  30. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +16 -10
  31. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml +2 -1
  32. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +6 -3
  33. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +6 -4
  34. stoobly_agent/app/cli/scaffold/templates/constants.py +4 -0
  35. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.Dockerfile.cypress +22 -0
  36. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.docker-compose.test.yml +19 -0
  37. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.Dockerfile.playwright +33 -0
  38. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.docker-compose.test.yml +18 -0
  39. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.entrypoint.sh +11 -0
  40. stoobly_agent/app/cli/scaffold/templates/workflow/mock/docker-compose.yml +17 -0
  41. stoobly_agent/app/cli/scaffold/templates/workflow/record/docker-compose.yml +17 -0
  42. stoobly_agent/app/cli/scaffold/templates/workflow/test/docker-compose.yml +17 -0
  43. stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
  44. stoobly_agent/app/cli/scaffold/workflow_namesapce.py +8 -2
  45. stoobly_agent/app/cli/scaffold/workflow_run_command.py +1 -1
  46. stoobly_agent/app/cli/scaffold_cli.py +77 -83
  47. stoobly_agent/app/proxy/handle_record_service.py +12 -3
  48. stoobly_agent/app/proxy/handle_replay_service.py +14 -2
  49. stoobly_agent/app/proxy/intercept_settings.py +11 -7
  50. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +33 -2
  51. stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
  52. stoobly_agent/app/proxy/replay/replay_request_service.py +3 -0
  53. stoobly_agent/app/proxy/run.py +3 -28
  54. stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -2
  55. stoobly_agent/app/proxy/utils/minimize_headers.py +47 -0
  56. stoobly_agent/app/proxy/utils/publish_change_service.py +5 -4
  57. stoobly_agent/app/proxy/utils/strategy.py +16 -0
  58. stoobly_agent/app/settings/__init__.py +9 -3
  59. stoobly_agent/app/settings/data_rules.py +25 -1
  60. stoobly_agent/app/settings/intercept_settings.py +5 -2
  61. stoobly_agent/app/settings/types/__init__.py +0 -1
  62. stoobly_agent/app/settings/ui_settings.py +5 -5
  63. stoobly_agent/cli.py +41 -16
  64. stoobly_agent/config/constants/custom_headers.py +1 -0
  65. stoobly_agent/config/constants/env_vars.py +4 -3
  66. stoobly_agent/config/constants/record_strategy.py +6 -0
  67. stoobly_agent/config/settings.yml.sample +2 -3
  68. stoobly_agent/lib/logger.py +15 -5
  69. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +231 -1
  70. stoobly_agent/test/app/cli/scaffold/cli_invoker.py +3 -2
  71. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  72. stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +14 -2
  73. stoobly_agent/test/app/proxy/utils/minimize_headers_test.py +342 -0
  74. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/METADATA +2 -1
  75. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/RECORD +78 -62
  76. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/LICENSE +0 -0
  77. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/WHEEL +0 -0
  78. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/entry_points.txt +0 -0
@@ -27,6 +27,7 @@ LogLevel = Literal[DEBUG, ERROR, INFO, WARNING]
27
27
 
28
28
  class Logger:
29
29
  _instance = None
30
+ _instances = {}
30
31
 
31
32
  def __init__(self):
32
33
  raise RuntimeError('Call instance() instead')
@@ -34,14 +35,23 @@ class Logger:
34
35
  @classmethod
35
36
  def instance(cls, name = None):
36
37
  if cls._instance is None:
38
+ logging.getLogger("mitmproxy").setLevel(logging.ERROR)
37
39
  cls._instance = cls.__new__(cls)
38
-
39
- cls._instance.load()
40
+ cls._instance.load()
40
41
 
41
42
  if not name:
42
- return logging
43
- else:
44
- return logging.getLogger(name)
43
+ name = 'root'
44
+
45
+ logger = logging.getLogger(name)
46
+
47
+ if name not in cls._instances:
48
+ handler = logging.StreamHandler()
49
+ handler.setFormatter(logging.Formatter(f"[%(levelname)s] {name} %(message)s"))
50
+ logger.addHandler(handler)
51
+ logger.propagate = False
52
+ cls._instances[name] = logger
53
+
54
+ return logger
45
55
 
46
56
  @classmethod
47
57
  def reload(cls):
@@ -7,8 +7,10 @@ from stoobly_agent.test.test_helper import reset
7
7
 
8
8
  from stoobly_agent.cli import config, intercept, scenario
9
9
  from stoobly_agent.lib.orm.scenario import Scenario
10
+ from stoobly_agent.app.settings import Settings
11
+ from stoobly_agent.lib.api.keys.project_key import ProjectKey
10
12
 
11
- from stoobly_agent.config.constants import mode, record_order
13
+ from stoobly_agent.config.constants import mode, mock_policy, record_order, record_policy, record_strategy, replay_policy, test_strategy
12
14
 
13
15
  @pytest.fixture(scope='module')
14
16
  def runner():
@@ -51,3 +53,231 @@ class TestInterceptConfigure():
51
53
  assert configure_result.exit_code == 0
52
54
 
53
55
  assert not Scenario.find(scenario.id).overwritable
56
+
57
+ class TestPolicy():
58
+
59
+ class TestMockPolicy():
60
+
61
+ def test_policy_mock_mode_all(self, runner: CliRunner):
62
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.MOCK, '--policy', mock_policy.ALL])
63
+ assert configure_result.exit_code == 0
64
+
65
+ settings = Settings.instance()
66
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
67
+ data_rule = settings.proxy.data.data_rules(project_key.id)
68
+ assert data_rule.mock_policy == mock_policy.ALL
69
+
70
+ def test_policy_mock_mode_found(self, runner: CliRunner):
71
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.MOCK, '--policy', mock_policy.FOUND])
72
+ assert configure_result.exit_code == 0
73
+
74
+ settings = Settings.instance()
75
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
76
+ data_rule = settings.proxy.data.data_rules(project_key.id)
77
+ assert data_rule.mock_policy == mock_policy.FOUND
78
+
79
+ def test_policy_without_mode_mock_existing(self, runner: CliRunner):
80
+ runner.invoke(intercept, ['configure', '--mode', mode.MOCK])
81
+ configure_result = runner.invoke(intercept, ['configure', '--policy', mock_policy.ALL])
82
+ assert configure_result.exit_code == 0
83
+
84
+ settings = Settings.instance()
85
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
86
+ data_rule = settings.proxy.data.data_rules(project_key.id)
87
+ assert data_rule.mock_policy == mock_policy.ALL
88
+
89
+ class TestRecordPolicy():
90
+
91
+ def test_policy_record_mode_all(self, runner: CliRunner):
92
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.RECORD, '--policy', record_policy.ALL])
93
+ assert configure_result.exit_code == 0
94
+
95
+ settings = Settings.instance()
96
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
97
+ data_rule = settings.proxy.data.data_rules(project_key.id)
98
+ assert data_rule.record_policy == record_policy.ALL
99
+
100
+ def test_policy_record_mode_api(self, runner: CliRunner):
101
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.RECORD, '--policy', record_policy.API])
102
+ assert configure_result.exit_code == 0
103
+
104
+ settings = Settings.instance()
105
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
106
+ data_rule = settings.proxy.data.data_rules(project_key.id)
107
+ assert data_rule.record_policy == record_policy.API
108
+
109
+ def test_policy_record_mode_found(self, runner: CliRunner):
110
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.RECORD, '--policy', record_policy.FOUND])
111
+ assert configure_result.exit_code == 0
112
+
113
+ settings = Settings.instance()
114
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
115
+ data_rule = settings.proxy.data.data_rules(project_key.id)
116
+ assert data_rule.record_policy == record_policy.FOUND
117
+
118
+ def test_policy_record_mode_not_found(self, runner: CliRunner):
119
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.RECORD, '--policy', record_policy.NOT_FOUND])
120
+ assert configure_result.exit_code == 0
121
+
122
+ settings = Settings.instance()
123
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
124
+ data_rule = settings.proxy.data.data_rules(project_key.id)
125
+ assert data_rule.record_policy == record_policy.NOT_FOUND
126
+
127
+ def test_policy_without_mode_record_existing(self, runner: CliRunner):
128
+ runner.invoke(intercept, ['configure', '--mode', mode.RECORD])
129
+ configure_result = runner.invoke(intercept, ['configure', '--policy', record_policy.API])
130
+ assert configure_result.exit_code == 0
131
+
132
+ settings = Settings.instance()
133
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
134
+ data_rule = settings.proxy.data.data_rules(project_key.id)
135
+ assert data_rule.record_policy == record_policy.API
136
+
137
+ class TestReplayPolicy():
138
+
139
+ def test_policy_replay_mode_all(self, runner: CliRunner):
140
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.REPLAY, '--policy', replay_policy.ALL])
141
+ assert configure_result.exit_code == 0
142
+
143
+ settings = Settings.instance()
144
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
145
+ data_rule = settings.proxy.data.data_rules(project_key.id)
146
+ assert data_rule.replay_policy == replay_policy.ALL
147
+
148
+ def test_policy_without_mode_replay_existing(self, runner: CliRunner):
149
+ runner.invoke(intercept, ['configure', '--mode', mode.REPLAY])
150
+ configure_result = runner.invoke(intercept, ['configure', '--policy', replay_policy.ALL])
151
+ assert configure_result.exit_code == 0
152
+
153
+ settings = Settings.instance()
154
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
155
+ data_rule = settings.proxy.data.data_rules(project_key.id)
156
+ assert data_rule.replay_policy == replay_policy.ALL
157
+
158
+ class TestTestPolicy():
159
+
160
+ def test_policy_test_mode_found(self, runner: CliRunner):
161
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.TEST, '--policy', mock_policy.FOUND])
162
+ assert configure_result.exit_code == 0
163
+
164
+ settings = Settings.instance()
165
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
166
+ data_rule = settings.proxy.data.data_rules(project_key.id)
167
+ assert data_rule.test_policy == mock_policy.FOUND
168
+
169
+ def test_policy_without_mode_test_existing(self, runner: CliRunner):
170
+ runner.invoke(intercept, ['configure', '--mode', mode.TEST])
171
+ configure_result = runner.invoke(intercept, ['configure', '--policy', mock_policy.FOUND])
172
+ assert configure_result.exit_code == 0
173
+
174
+ settings = Settings.instance()
175
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
176
+ data_rule = settings.proxy.data.data_rules(project_key.id)
177
+ assert data_rule.test_policy == mock_policy.FOUND
178
+
179
+ class TestInvalidPolicyInput():
180
+ # Since all modes use 'all', we need to check what's unique to each mode
181
+
182
+ def test_policy_invalid_for_mock_mode(self, runner: CliRunner):
183
+ # Use record_policy.API which is not valid for MOCK mode
184
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.MOCK, '--policy', record_policy.API])
185
+ assert configure_result.exit_code == 1
186
+ assert "Error: Valid policies for" in configure_result.output
187
+
188
+ def test_policy_invalid_for_record_mode(self, runner: CliRunner):
189
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.MOCK, '--policy', record_policy.NOT_FOUND])
190
+ assert configure_result.exit_code == 1
191
+ assert "Error: Valid policies for" in configure_result.output
192
+
193
+ def test_policy_invalid_for_replay_mode(self, runner: CliRunner):
194
+ # Use record_policy.API which is not valid for REPLAY mode
195
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.REPLAY, '--policy', record_policy.API])
196
+ assert configure_result.exit_code == 1
197
+ assert "Error: Valid policies for" in configure_result.output
198
+
199
+ def test_policy_invalid_for_test_mode(self, runner: CliRunner):
200
+ # Use record_policy.API which is not valid for TEST mode
201
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.TEST, '--policy', record_policy.API])
202
+ assert configure_result.exit_code == 1
203
+ assert "Error: Valid policies for" in configure_result.output
204
+
205
+ class TestStrategy():
206
+
207
+ class TestRecordStrategy():
208
+
209
+ def test_strategy_record_mode_full(self, runner: CliRunner):
210
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.RECORD, '--strategy', record_strategy.FULL])
211
+ assert configure_result.exit_code == 0
212
+
213
+ settings = Settings.instance()
214
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
215
+ data_rule = settings.proxy.data.data_rules(project_key.id)
216
+ assert data_rule.record_strategy == record_strategy.FULL
217
+
218
+ def test_strategy_record_mode_minimal(self, runner: CliRunner):
219
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.RECORD, '--strategy', record_strategy.MINIMAL])
220
+ assert configure_result.exit_code == 0
221
+
222
+ settings = Settings.instance()
223
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
224
+ data_rule = settings.proxy.data.data_rules(project_key.id)
225
+ assert data_rule.record_strategy == record_strategy.MINIMAL
226
+
227
+ def test_strategy_without_mode_record_existing(self, runner: CliRunner):
228
+ runner.invoke(intercept, ['configure', '--mode', mode.RECORD])
229
+ configure_result = runner.invoke(intercept, ['configure', '--strategy', record_strategy.MINIMAL])
230
+ assert configure_result.exit_code == 0
231
+
232
+ settings = Settings.instance()
233
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
234
+ data_rule = settings.proxy.data.data_rules(project_key.id)
235
+ assert data_rule.record_strategy == record_strategy.MINIMAL
236
+
237
+ @pytest.mark.skip(reason="Switching modes during tests is finnicky and leads to test failures")
238
+ class TestTestStrategy():
239
+
240
+ def test_strategy_test_mode_contract(self, runner: CliRunner):
241
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.TEST, '--strategy', test_strategy.CONTRACT])
242
+ assert configure_result.exit_code == 0
243
+
244
+ settings = Settings.instance()
245
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
246
+ data_rule = settings.proxy.data.data_rules(project_key.id)
247
+ assert data_rule.test_strategy == test_strategy.CONTRACT
248
+
249
+ def test_strategy_test_mode_diff(self, runner: CliRunner):
250
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.TEST, '--strategy', test_strategy.DIFF])
251
+ assert configure_result.exit_code == 0
252
+
253
+ settings = Settings.instance()
254
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
255
+ data_rule = settings.proxy.data.data_rules(project_key.id)
256
+ assert data_rule.test_strategy == test_strategy.DIFF
257
+
258
+ def test_strategy_without_mode_test_existing(self, runner: CliRunner):
259
+ runner.invoke(intercept, ['configure', '--mode', mode.TEST])
260
+ configure_result = runner.invoke(intercept, ['configure', '--strategy', test_strategy.FUZZY])
261
+ assert configure_result.exit_code == 0
262
+
263
+ settings = Settings.instance()
264
+ project_key = ProjectKey(settings.proxy.intercept.project_key)
265
+ data_rule = settings.proxy.data.data_rules(project_key.id)
266
+ assert data_rule.test_strategy == test_strategy.FUZZY
267
+
268
+ class TestInvalidInput():
269
+ def test_strategy_mock_mode_error(self, runner: CliRunner):
270
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.MOCK, '--strategy', record_strategy.FULL])
271
+ assert configure_result.exit_code == 1
272
+ assert "Error: set --strategy to a intercept mode that supports the strategy option" in configure_result.output
273
+
274
+ def test_strategy_replay_mode_error(self, runner: CliRunner):
275
+ configure_result = runner.invoke(intercept, ['configure', '--mode', mode.REPLAY, '--strategy', record_strategy.FULL])
276
+ assert configure_result.exit_code == 1
277
+ assert "Error: set --strategy to a intercept mode that supports the strategy option" in configure_result.output
278
+
279
+ def test_strategy_without_mode_unsupported_existing(self, runner: CliRunner):
280
+ runner.invoke(intercept, ['configure', '--mode', mode.MOCK])
281
+ configure_result = runner.invoke(intercept, ['configure', '--strategy', record_strategy.FULL])
282
+ assert configure_result.exit_code == 1
283
+ assert "Error: set --strategy to a intercept mode that supports the strategy option" in configure_result.output
@@ -66,14 +66,15 @@ class ScaffoldCliInvoker():
66
66
  if https == True:
67
67
  scheme = 'https'
68
68
  port = '443'
69
- proxy_mode_reverse_spec = f"reverse:{scheme}://{hostname}:8080"
70
69
 
71
70
  result = runner.invoke(scaffold, ['service', 'create',
72
71
  '--app-dir-path', app_dir_path,
73
72
  '--hostname', hostname,
74
73
  '--scheme', scheme,
75
74
  '--port', port,
76
- '--proxy-mode', proxy_mode_reverse_spec,
75
+ '--proxy-mode', 'reverse',
76
+ '--upstream-hostname', hostname,
77
+ '--upstream-port', 8080,
77
78
  '--detached',
78
79
  '--quiet',
79
80
  '--workflow', 'test',
@@ -1 +1 @@
1
- 1.9.6
1
+ 1.9.12
@@ -2,6 +2,7 @@ import os
2
2
  import pdb
3
3
  import pytest
4
4
  import requests
5
+ import shutil
5
6
 
6
7
  from click.testing import CliRunner
7
8
  from mitmproxy.http import Request as MitmproxyRequest
@@ -156,6 +157,14 @@ class TestEvalFixturesService():
156
157
  assert res != None
157
158
  return res
158
159
 
160
+ @pytest.fixture()
161
+ def public_directory_default_response(self, index_file_path: str, mitmproxy_request: MitmproxyRequest, public_directory: str):
162
+ path = os.path.join(public_directory, 'index')
163
+ shutil.move(index_file_path, path)
164
+ res: requests.Response = eval_fixtures(mitmproxy_request, public_directory_path=public_directory)
165
+ assert res != None
166
+ return res
167
+
159
168
  def test_it_sets_contents(
160
169
  self, public_directory_response: requests.Response, index_file_contents: str
161
170
  ):
@@ -164,5 +173,8 @@ class TestEvalFixturesService():
164
173
  def test_it_headers(self, public_directory_response: requests.Response):
165
174
  assert public_directory_response.headers['Content-Type'] == 'text/html'
166
175
 
167
- def test_it_sets_status_code(self, public_directory_response: requests.Response):
168
- assert public_directory_response.status_code == 200
176
+ def test_it_sets_status_code(self, public_directory_response: requests.Response):
177
+ assert public_directory_response.status_code == 200
178
+
179
+ def test_default_it_headers(self, public_directory_default_response: requests.Response):
180
+ assert public_directory_default_response.headers['Content-Type'] == 'application/json'
@@ -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