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.
- stoobly_agent/__init__.py +1 -1
- stoobly_agent/app/api/__init__.py +4 -20
- stoobly_agent/app/api/configs_controller.py +3 -3
- stoobly_agent/app/cli/decorators/exec.py +1 -1
- stoobly_agent/app/cli/helpers/handle_config_update_service.py +4 -0
- stoobly_agent/app/cli/helpers/shell.py +0 -10
- stoobly_agent/app/cli/intercept_cli.py +40 -7
- stoobly_agent/app/cli/scaffold/app_command.py +4 -0
- stoobly_agent/app/cli/scaffold/app_config.py +21 -3
- stoobly_agent/app/cli/scaffold/app_create_command.py +109 -2
- stoobly_agent/app/cli/scaffold/constants.py +14 -0
- stoobly_agent/app/cli/scaffold/docker/constants.py +4 -6
- stoobly_agent/app/cli/scaffold/docker/service/builder.py +19 -4
- stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -18
- stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +24 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/decorators_factory.py +7 -2
- stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +42 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +26 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +9 -10
- stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +5 -8
- stoobly_agent/app/cli/scaffold/service_config.py +144 -21
- stoobly_agent/app/cli/scaffold/service_create_command.py +11 -2
- stoobly_agent/app/cli/scaffold/service_dependency.py +51 -0
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/record/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/test/docker-compose.yml +16 -6
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +16 -10
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +16 -10
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +16 -10
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml +2 -1
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +6 -3
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +6 -4
- stoobly_agent/app/cli/scaffold/templates/constants.py +4 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.Dockerfile.cypress +22 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.docker-compose.test.yml +19 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.Dockerfile.playwright +33 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.docker-compose.test.yml +18 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.entrypoint.sh +11 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/docker-compose.yml +17 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/record/docker-compose.yml +17 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/test/docker-compose.yml +17 -0
- stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
- stoobly_agent/app/cli/scaffold/workflow_namesapce.py +8 -2
- stoobly_agent/app/cli/scaffold/workflow_run_command.py +1 -1
- stoobly_agent/app/cli/scaffold_cli.py +77 -83
- stoobly_agent/app/proxy/handle_record_service.py +12 -3
- stoobly_agent/app/proxy/handle_replay_service.py +14 -2
- stoobly_agent/app/proxy/intercept_settings.py +11 -7
- stoobly_agent/app/proxy/mock/eval_fixtures_service.py +33 -2
- stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
- stoobly_agent/app/proxy/replay/replay_request_service.py +3 -0
- stoobly_agent/app/proxy/run.py +3 -28
- stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -2
- stoobly_agent/app/proxy/utils/minimize_headers.py +47 -0
- stoobly_agent/app/proxy/utils/publish_change_service.py +5 -4
- stoobly_agent/app/proxy/utils/strategy.py +16 -0
- stoobly_agent/app/settings/__init__.py +9 -3
- stoobly_agent/app/settings/data_rules.py +25 -1
- stoobly_agent/app/settings/intercept_settings.py +5 -2
- stoobly_agent/app/settings/types/__init__.py +0 -1
- stoobly_agent/app/settings/ui_settings.py +5 -5
- stoobly_agent/cli.py +41 -16
- stoobly_agent/config/constants/custom_headers.py +1 -0
- stoobly_agent/config/constants/env_vars.py +4 -3
- stoobly_agent/config/constants/record_strategy.py +6 -0
- stoobly_agent/config/settings.yml.sample +2 -3
- stoobly_agent/lib/logger.py +15 -5
- stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +231 -1
- stoobly_agent/test/app/cli/scaffold/cli_invoker.py +3 -2
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +14 -2
- stoobly_agent/test/app/proxy/utils/minimize_headers_test.py +342 -0
- {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/METADATA +2 -1
- {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/RECORD +78 -62
- {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/WHEEL +0 -0
- {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/entry_points.txt +0 -0
stoobly_agent/lib/logger.py
CHANGED
@@ -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
|
-
|
43
|
-
|
44
|
-
|
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',
|
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.
|
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,
|
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
|