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
@@ -9,7 +9,7 @@ from stoobly_agent.app.settings.constants.mode import TEST
9
9
  from stoobly_agent.app.models.request_model import RequestModel
10
10
  from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
11
11
  from stoobly_agent.config.constants.env_vars import ENV
12
- from stoobly_agent.config.constants import lifecycle_hooks, record_order, record_policy
12
+ from stoobly_agent.config.constants import lifecycle_hooks, record_order, record_policy, record_strategy
13
13
  from stoobly_agent.lib.logger import Logger
14
14
 
15
15
  from .constants import custom_response_codes
@@ -19,8 +19,11 @@ from .record.overwrite_scenario_service import overwrite_scenario
19
19
  from .record.upload_request_service import inject_upload_request
20
20
  from .replay.body_parser_service import is_json, is_xml
21
21
  from .utils.allowed_request_service import get_active_mode_policy
22
+ from .utils.minimize_headers import minimize_headers
22
23
  from .utils.response_handler import bad_request, disable_transfer_encoding
23
24
  from .utils.rewrite import rewrite_request_response
25
+ from .utils.strategy import get_active_mode_strategy
26
+
24
27
 
25
28
  LOG_ID = 'Record'
26
29
 
@@ -54,7 +57,7 @@ def handle_response_record(context: RecordContext):
54
57
  res = inject_eval_request(request_model, intercept_settings)(request, [])
55
58
 
56
59
  if res.status_code != custom_response_codes.NOT_FOUND:
57
- __record_request(context , request_model)
60
+ __record_request(context, request_model)
58
61
  elif active_record_policy == record_policy.NOT_FOUND:
59
62
  res = inject_eval_request(request_model, intercept_settings)(request, [])
60
63
 
@@ -74,7 +77,13 @@ def __record_handler(context: RecordContext, request_model: RequestModel):
74
77
  intercept_settings = context.intercept_settings
75
78
 
76
79
  context.flow = flow_copy # Deep copy flow to prevent response modifications from persisting
77
- rewrite_request_response(flow_copy, intercept_settings.record_rewrite_rules)
80
+
81
+ active_record_strategy = get_active_mode_strategy(intercept_settings)
82
+ if active_record_strategy == record_strategy.MINIMAL:
83
+ minimize_headers(flow_copy)
84
+
85
+ rewrite_request_response(flow_copy, intercept_settings.record_rewrite_rules)
86
+
78
87
  __record_hook(lifecycle_hooks.BEFORE_RECORD, context)
79
88
 
80
89
  inject_upload_request(request_model, intercept_settings)(flow_copy)
@@ -5,9 +5,10 @@ from typing import TypedDict
5
5
 
6
6
  from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
7
7
  from stoobly_agent.app.proxy.replay.context import ReplayContext
8
- from stoobly_agent.config.constants import lifecycle_hooks, replay_policy
8
+ from stoobly_agent.config.constants import lifecycle_hooks, replay_policy, custom_headers, mode, record_strategy
9
9
 
10
10
  from .utils.allowed_request_service import get_active_mode_policy
11
+ from .utils.minimize_headers import minimize_response_headers
11
12
  from .utils.rewrite import rewrite_request, rewrite_response
12
13
 
13
14
  LOG_ID = 'HandleReplay'
@@ -66,7 +67,18 @@ def __rewrite_response(replay_context: ReplayContext):
66
67
  After replaying a request, see if the request needs to be rewritten
67
68
  """
68
69
  intercept_settings: InterceptSettings = replay_context.intercept_settings
70
+ flow = replay_context.flow
71
+ request = flow.request
72
+ response = flow.response
73
+
74
+ request_proxy_mode_header = request.headers.get(custom_headers.PROXY_MODE)
75
+ response_proxy_mode_header = response.headers.get(custom_headers.RESPONSE_PROXY_MODE)
76
+
77
+ if request_proxy_mode_header == mode.REPLAY and response_proxy_mode_header == mode.RECORD:
78
+ if intercept_settings.record_strategy == record_strategy.MINIMAL:
79
+ minimize_response_headers(flow)
80
+
69
81
  rewrite_rules = intercept_settings.replay_rewrite_rules
70
82
 
71
83
  if len(rewrite_rules) > 0:
72
- rewrite_response(replay_context.flow, rewrite_rules)
84
+ rewrite_response(flow, rewrite_rules)
@@ -57,14 +57,11 @@ class InterceptSettings:
57
57
 
58
58
  @property
59
59
  def active(self):
60
- if self.__intercept_settings.active:
61
- return True
60
+ if self.__headers and custom_headers.PROXY_MODE in self.__headers:
61
+ return not not self.__headers[custom_headers.PROXY_MODE]
62
62
 
63
- if not self.__headers:
64
- return False
63
+ return self.__intercept_settings.active
65
64
 
66
- return custom_headers.PROXY_MODE in self.__headers
67
-
68
65
  @property
69
66
  def lifecycle_hooks_path(self):
70
67
  if self.__headers and custom_headers.LIFECYCLE_HOOKS_PATH in self.__headers:
@@ -182,6 +179,13 @@ class InterceptSettings:
182
179
 
183
180
  return self.policy
184
181
 
182
+ @property
183
+ def record_strategy(self):
184
+ if self.__headers and custom_headers.RECORD_STRATEGY in self.__headers:
185
+ return self.__headers[custom_headers.RECORD_STRATEGY]
186
+
187
+ return self.__data_rules.record_strategy
188
+
185
189
  @property
186
190
  def exclude_rules(self) -> List[FirewallRule]:
187
191
  _mode = self.mode
@@ -365,4 +369,4 @@ class InterceptSettings:
365
369
 
366
370
  return self.__data_rules.test_policy
367
371
  elif mode == intercept_mode.REPLAY:
368
- return self.__data_rules.replay_policy
372
+ return self.__data_rules.replay_policy
@@ -7,7 +7,7 @@ from io import BytesIO
7
7
  from mitmproxy.http import Request as MitmproxyRequest
8
8
  from requests import Response
9
9
  from requests.structures import CaseInsensitiveDict
10
- from typing import Union
10
+ from typing import Optional, Union
11
11
 
12
12
  from stoobly_agent.lib.logger import bcolors, Logger
13
13
  from stoobly_agent.config.constants.custom_headers import MOCK_FIXTURE_PATH
@@ -62,7 +62,7 @@ def eval_fixtures(request: MitmproxyRequest, **options: Options) -> Union[Respon
62
62
  with open(fixture_path, 'rb') as fp:
63
63
  response = Response()
64
64
 
65
- response.status_code = status_code
65
+ response.status_code = int(status_code)
66
66
  response.raw = BytesIO(fp.read())
67
67
  headers[MOCK_FIXTURE_PATH] = fixture_path
68
68
  response.headers = headers
@@ -71,6 +71,11 @@ def eval_fixtures(request: MitmproxyRequest, **options: Options) -> Union[Respon
71
71
  content_type = __guess_content_type(fixture_path)
72
72
  if content_type:
73
73
  response.headers['content-type'] = content_type
74
+ else:
75
+ # Default to highest priority accept header
76
+ content_type = __choose_highest_priority_content_type(request.headers.get('accept'))
77
+ if content_type:
78
+ response.headers['content-type'] = content_type
74
79
 
75
80
  Logger.instance(LOG_ID).debug(f"{bcolors.OKBLUE}Resolved{bcolors.ENDC} fixture {fixture_path}")
76
81
 
@@ -125,6 +130,32 @@ def __eval_response_fixtures(request: MitmproxyRequest, response_fixtures: Fixtu
125
130
  if path:
126
131
  return fixture
127
132
 
133
+ def __choose_highest_priority_content_type(accept_header: str) -> Optional[str]:
134
+ if not accept_header:
135
+ return None
136
+
137
+ types = []
138
+ for part in accept_header.split(","):
139
+ media_range = part.strip()
140
+ if ";" in media_range:
141
+ mime, *params = media_range.split(";")
142
+ q = 1.0 # default
143
+ for param in params:
144
+ param = param.strip()
145
+ if param.startswith("q="):
146
+ try:
147
+ q = float(param[2:])
148
+ except ValueError:
149
+ q = 0.0 # invalid q values treated as lowest
150
+ else:
151
+ mime = media_range
152
+ q = 1.0
153
+ types.append((mime.strip(), q))
154
+
155
+ # Sort by descending q
156
+ types.sort(key=lambda x: -x[1])
157
+ return types[0][0] if types else None
158
+
128
159
  def __parse_accept_header(accept_header):
129
160
  types = []
130
161
  for item in accept_header.split(","):
@@ -49,7 +49,7 @@ def inject_upload_request(request_model: RequestModel, intercept_settings: Inter
49
49
  def upload_request(
50
50
  request_model: RequestModel, intercept_settings: InterceptSettings, flow: MitmproxyHTTPFlow = None
51
51
  ):
52
- Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Recording{bcolors.ENDC} {flow.request.url}")
52
+ Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}Recording{bcolors.ENDC} {flow.request.url}")
53
53
 
54
54
  flow_copy = deepcopy(flow) # When applying modifications we don't want to persist them in the response
55
55
  joined_request = join_request_from_flow(flow_copy, intercept_settings=intercept_settings)
@@ -80,7 +80,7 @@ def upload_request(
80
80
  def upload_staged_request(
81
81
  request: Request, request_model: RequestModel, project_key: str, scenario_key: str = None
82
82
  ):
83
- Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Recording{bcolors.ENDC} {request.url}")
83
+ Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}Recording{bcolors.ENDC} {request.url}")
84
84
 
85
85
  response = request.response
86
86
 
@@ -72,6 +72,9 @@ def replay(context: ReplayContext, options: ReplayRequestOptions) -> requests.Re
72
72
  if options.get('public_directory_path'):
73
73
  __handle_path_header(custom_headers.PUBLIC_DIRECTORY_PATH, options['public_directory_path'], headers)
74
74
 
75
+ if options.get('record_strategy'):
76
+ headers[custom_headers.RECORD_STRATEGY] = options['record_strategy']
77
+
75
78
  if options.get('report_key'):
76
79
  headers[custom_headers.REPORT_KEY] = options['report_key']
77
80
 
@@ -16,8 +16,6 @@ tls.DEFAULT_OPTIONS |= 0x4
16
16
  from mitmproxy.options import Options
17
17
  from mitmproxy.tools.dump import DumpMaster
18
18
 
19
- from stoobly_agent.app.settings import Settings
20
-
21
19
  INTERCEPT_HANDLER_FILENAME = 'intercept_handler.py'
22
20
 
23
21
  def run(**kwargs):
@@ -67,7 +65,6 @@ def __with_static_options(config: MitmproxyConfig, cli_options):
67
65
  config.set(options)
68
66
 
69
67
  def __with_cli_options(config: MitmproxyConfig, cli_options: dict):
70
- __commit_options(cli_options)
71
68
  __filter_options(cli_options)
72
69
 
73
70
  options = []
@@ -86,31 +83,6 @@ def __with_cli_options(config: MitmproxyConfig, cli_options: dict):
86
83
 
87
84
  config.set(tuple(options))
88
85
 
89
- def __commit_options(options: dict):
90
- changed = False
91
- service_name = os.environ.get(SERVICE_NAME_ENV)
92
- settings = Settings.instance()
93
-
94
- if not service_name or options.get('intercept'):
95
- # In the case when service name is set,
96
- # only update if intercept is explicitly enabled
97
- intercept = not not options.get('intercept')
98
- changed = settings.proxy.intercept.active != intercept
99
- settings.proxy.intercept.active = intercept
100
-
101
- if not service_name or service_name == CORE_MOCK_UI_SERVICE_NAME:
102
- # Causes potentially unintended side effects when run as part of scaffold
103
- # Defer to ui service for configuration in this case
104
- if options.get('proxy_host') and options.get('proxy_port'):
105
- settings.proxy.url = f"http://{options.get('proxy_host')}:{options.get('proxy_port')}"
106
-
107
- settings.ui.active = not options.get('headless')
108
-
109
- changed = True
110
-
111
- if changed:
112
- settings.commit()
113
-
114
86
  def __filter_options(options):
115
87
  '''
116
88
  Filter out non-mitmproxy options
@@ -139,6 +111,9 @@ def __filter_options(options):
139
111
  if 'intercept' in options:
140
112
  del options['intercept']
141
113
 
114
+ if 'intercept_mode' in options:
115
+ del options['intercept_mode']
116
+
142
117
  if 'lifecycle_hooks_path' in options:
143
118
  del options['lifecycle_hooks_path']
144
119
 
@@ -5,13 +5,14 @@ from mitmproxy.http import Request as MitmproxyRequest
5
5
  from typing import List
6
6
 
7
7
  from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
8
+ from stoobly_agent.app.settings.constants import intercept_mode
8
9
  from stoobly_agent.app.settings.firewall_rule import FirewallRule
9
- from stoobly_agent.config.constants import intercept_policy, request_origin
10
+ from stoobly_agent.config.constants import mode, intercept_policy, request_origin
10
11
  from stoobly_agent.lib.logger import bcolors, Logger
11
12
 
12
13
  LOG_ID = 'Firewall'
13
14
 
14
- def get_active_mode_policy(request: MitmproxyRequest, intercept_settings: InterceptSettings):
15
+ def get_active_mode_policy(request: MitmproxyRequest, intercept_settings: InterceptSettings) -> str:
15
16
  if intercept_settings.request_origin == request_origin.CLI:
16
17
  return intercept_settings.policy
17
18
 
@@ -0,0 +1,47 @@
1
+ import pdb
2
+
3
+ from mitmproxy.http import Headers
4
+ from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
5
+
6
+ from typing import Final
7
+
8
+
9
+ REQUEST_HEADERS_ALLOWLIST: Final[dict[str]] = {
10
+ "Accept",
11
+ "Accept-Encoding",
12
+ "Accept-Language",
13
+ "Content-Length",
14
+ "Content-Type",
15
+ "Host",
16
+ "Origin",
17
+ "Referer",
18
+ "User-Agent",
19
+ }
20
+
21
+ RESPONSE_HEADERS_ALLOWLIST: Final[dict[str]] = {
22
+ "Content-Length",
23
+ "Content-Type",
24
+ "Date",
25
+ "Transfer-Encoding",
26
+ "Server", # Sometimes required for HTTP/1.0, but not strictly mandatory
27
+ }
28
+
29
+ def minimize_headers(flow: MitmproxyHTTPFlow):
30
+ minimize_request_headers(flow)
31
+ minimize_response_headers(flow)
32
+
33
+ def minimize_request_headers(flow: MitmproxyHTTPFlow) -> None:
34
+ remove_headers(flow.request.headers, REQUEST_HEADERS_ALLOWLIST)
35
+
36
+ def minimize_response_headers(flow: MitmproxyHTTPFlow) -> None:
37
+ remove_headers(flow.response.headers, RESPONSE_HEADERS_ALLOWLIST)
38
+
39
+ def remove_headers(headers: Headers, allowlist: dict[str]):
40
+ keys_to_remove = []
41
+
42
+ for key in headers:
43
+ if key.lower() not in {allowed_header.lower() for allowed_header in allowlist}:
44
+ keys_to_remove.append(key)
45
+
46
+ for key in keys_to_remove:
47
+ headers.pop(key)
@@ -1,8 +1,10 @@
1
+ import os
1
2
  import pdb
2
3
  import threading
3
4
 
4
5
  from typing import TypedDict
5
6
 
7
+ from stoobly_agent.app.settings import Settings
6
8
  from stoobly_agent.config.constants.statuses import REQUESTS_MODIFIED
7
9
  from stoobly_agent.lib.api.agent_api import AgentApi
8
10
  from stoobly_agent.lib.cache import Cache
@@ -12,15 +14,14 @@ class Options(TypedDict):
12
14
  sync: bool
13
15
 
14
16
  # Announce that a new request has been created
15
- def publish_change(status: str, value: any, **options: Options):
17
+ def publish_change(status: str, value, **options: Options):
16
18
  if options.get('sync'):
17
19
  return __publish_change_sync(status, value)
18
20
 
19
- from stoobly_agent.app.settings import Settings
20
- settings = Settings.instance()
21
+ settings: Settings = Settings.instance()
21
22
 
22
23
  # If ui is not active, return
23
- if not settings.ui.active:
24
+ if not settings.ui.active:
24
25
  return False
25
26
 
26
27
  ui_url = settings.ui.url
@@ -0,0 +1,16 @@
1
+ import pdb
2
+
3
+ from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
4
+ from stoobly_agent.app.settings.constants import intercept_mode
5
+ from stoobly_agent.config.constants import request_origin
6
+
7
+
8
+ def get_active_mode_strategy(intercept_settings: InterceptSettings) -> str:
9
+ strategy = ""
10
+
11
+ if intercept_settings.mode == intercept_mode.RECORD:
12
+ strategy = intercept_settings.record_strategy
13
+ elif intercept_settings.mode == intercept_mode.TEST:
14
+ strategy = intercept_settings.test_strategy
15
+
16
+ return strategy
@@ -3,7 +3,7 @@ import pdb
3
3
  import time
4
4
  import yaml
5
5
 
6
- from shutil import copyfile
6
+ from filelock import FileLock
7
7
  from watchdog.observers import Observer
8
8
  from watchdog.events import PatternMatchingEventHandler
9
9
  from yamale import *
@@ -177,8 +177,14 @@ class Settings:
177
177
  self.__load_settings()
178
178
 
179
179
  def write(self, contents):
180
- if contents:
181
- with open(self.__settings_file_path, 'w') as fp:
180
+ if not contents:
181
+ return
182
+
183
+ path = self.__settings_file_path
184
+ lock = FileLock(path + ".lock") # lock file alongside the target
185
+
186
+ with lock:
187
+ with open(path, 'w') as fp:
182
188
  yaml.dump(contents, fp, allow_unicode=True)
183
189
 
184
190
  ### Helpers
@@ -1,4 +1,4 @@
1
- from stoobly_agent.config.constants import mock_policy, record_order, record_policy, replay_policy, test_strategy
1
+ from stoobly_agent.config.constants import mock_policy, record_order, record_policy, record_strategy, replay_policy, test_strategy
2
2
 
3
3
  from .types.proxy_settings import DataRules as IDataRules
4
4
 
@@ -10,6 +10,7 @@ class DataRules:
10
10
  self.__mock_policy = self.__data_rules.get('mock_policy') or mock_policy.FOUND
11
11
  self.__record_order = self.__data_rules.get('record_order') or record_order.APPEND
12
12
  self.__record_policy = self.__data_rules.get('record_policy') or record_policy.ALL
13
+ self.__record_strategy = self.__data_rules.get('record_strategy') or record_strategy.FULL
13
14
  self.__replay_policy = self.__data_rules.get('replay_policy') or replay_policy.ALL
14
15
  self.__scenario_key = self.__data_rules.get('scenario_key')
15
16
  self.__test_policy = self.__data_rules.get('test_policy') or mock_policy.FOUND
@@ -46,6 +47,19 @@ class DataRules:
46
47
  self.__record_order = v
47
48
  self.__data_rules['record_order'] = v
48
49
 
50
+ @property
51
+ def record_strategy(self):
52
+ return self.__record_strategy
53
+
54
+ @record_strategy.setter
55
+ def record_strategy(self, v):
56
+ valid_strategies = [record_strategy.FULL, record_strategy.MINIMAL]
57
+ if v not in valid_strategies:
58
+ raise TypeError(f"record_strategy has to be one of {valid_strategies}, got {v}")
59
+
60
+ self.__record_strategy = v
61
+ self.__data_rules['record_strategy'] = v
62
+
49
63
  @property
50
64
  def replay_policy(self):
51
65
  return self.__replay_policy
@@ -79,11 +93,21 @@ class DataRules:
79
93
  def test_strategy(self):
80
94
  return self.__test_strategy
81
95
 
96
+ @test_strategy.setter
97
+ def test_strategy(self, v):
98
+ valid_strategies = [test_strategy.CONTRACT, test_strategy.CUSTOM, test_strategy.DIFF, test_strategy.FUZZY]
99
+ if v not in valid_strategies:
100
+ raise TypeError(f"test_strategy has to be one of {valid_strategies}, got {v}")
101
+
102
+ self.__test_strategy = v
103
+ self.__data_rules['test_strategy'] = v
104
+
82
105
  def to_dict(self) -> IDataRules:
83
106
  return {
84
107
  'mock_policy': self.__mock_policy,
85
108
  'record_order': self.__record_order,
86
109
  'record_policy': self.__record_policy,
110
+ 'record_strategy': self.__record_strategy,
87
111
  'replay_policy': self.__replay_policy,
88
112
  'scenario_key': self.__scenario_key,
89
113
  'test_policy': self.__test_policy,
@@ -19,6 +19,9 @@ class InterceptSettings:
19
19
 
20
20
  @property
21
21
  def active(self):
22
+ if os.environ.get(env_vars.AGENT_INTERCEPT_ACTIVE):
23
+ return os.environ[env_vars.AGENT_INTERCEPT_ACTIVE]
24
+
22
25
  return self.__active
23
26
 
24
27
  @active.setter
@@ -35,8 +38,8 @@ class InterceptSettings:
35
38
  if self.__mode != self.mode_before_change:
36
39
  return self.__mode
37
40
 
38
- if os.environ.get(env_vars.AGENT_ACTIVE_MODE):
39
- return os.environ[env_vars.AGENT_ACTIVE_MODE]
41
+ if os.environ.get(env_vars.AGENT_INTERCEPT_MODE):
42
+ return os.environ[env_vars.AGENT_INTERCEPT_MODE]
40
43
 
41
44
  return self.__mode
42
45
 
@@ -84,7 +84,6 @@ class ISettings(TypedDict):
84
84
  api_url: str
85
85
  api_key: str
86
86
  mode: ISettingsMode
87
- proxy_config_path: str
88
87
 
89
88
  Component = {
90
89
  'Header': 'Header',
@@ -21,8 +21,8 @@ class UISettings:
21
21
  if self.__active != self.active_before_change:
22
22
  return self.__active
23
23
 
24
- if os.environ.get(env_vars.AGENT_IS_HEADLESS):
25
- return True
24
+ if os.environ.get(env_vars.AGENT_HEADLESS):
25
+ return False
26
26
 
27
27
  return self.__active or False
28
28
 
@@ -39,8 +39,8 @@ class UISettings:
39
39
  if self.__url != self.url_before_change:
40
40
  return self.__url
41
41
 
42
- if os.environ.get(env_vars.AGENT_URL):
43
- return os.environ[env_vars.AGENT_URL]
42
+ if os.environ.get(env_vars.AGENT_UI_URL):
43
+ return os.environ[env_vars.AGENT_UI_URL]
44
44
 
45
45
  return self.__url or ''
46
46
 
@@ -52,4 +52,4 @@ class UISettings:
52
52
  return {
53
53
  'active': self.__active,
54
54
  'url': self.__url,
55
- }
55
+ }
stoobly_agent/cli.py CHANGED
@@ -13,9 +13,10 @@ from stoobly_agent.app.proxy.constants import custom_response_codes
13
13
  from stoobly_agent.app.proxy.replay.replay_request_service import replay as replay_request
14
14
  from stoobly_agent.config.constants import env_vars, mode
15
15
  from stoobly_agent.config.data_dir import DataDir
16
+ from stoobly_agent.lib.logger import Logger
16
17
  from stoobly_agent.lib.utils.conditional_decorator import ConditionalDecorator
17
18
 
18
- from .app.api import initialize as initialize_api, run as run_api
19
+ from .app.api import run as run_api
19
20
  from .app.cli import ca_cert, config, endpoint, feature, intercept, MainGroup, request, scenario, scaffold, snapshot, trace
20
21
  from .app.cli.helpers.feature_flags import local, remote
21
22
  from .app.settings import Settings
@@ -99,6 +100,7 @@ def init(**kwargs):
99
100
  ''')
100
101
  @click.option('--headless', is_flag=True, default=False, help='Disable starting UI.')
101
102
  @click.option('--intercept', is_flag=True, default=False, help='Enable intercept on run.')
103
+ @click.option('--intercept-mode', help='Set intercept mode.')
102
104
  @click.option('--log-level', default=logger.INFO, type=click.Choice([logger.DEBUG, logger.INFO, logger.WARNING, logger.ERROR]), help='''
103
105
  Log levels can be "debug", "info", "warning", or "error"
104
106
  ''')
@@ -124,7 +126,19 @@ def init(**kwargs):
124
126
  def run(**kwargs):
125
127
  from .app.proxy.run import run as run_proxy
126
128
 
127
- os.environ[env_vars.AGENT_PROXY_URL] = f"http://{kwargs['proxy_host']}:{kwargs['proxy_port']}"
129
+ # Observe config for changes
130
+ settings: Settings = Settings.instance()
131
+ settings.watch()
132
+
133
+ if kwargs.get('headless'):
134
+ os.environ[env_vars.AGENT_HEADLESS] = '1'
135
+
136
+ if kwargs.get('intercept'):
137
+ os.environ[env_vars.AGENT_INTERCEPT_ACTIVE] = '1'
138
+
139
+ if kwargs.get('intercept_mode'):
140
+ os.environ[env_vars.AGENT_INTERCEPT_MODE] = kwargs['intercept_mode']
141
+ settings.proxy.intercept.mode = kwargs['intercept_mode']
128
142
 
129
143
  if kwargs.get('lifecycle_hooks_path'):
130
144
  os.environ[env_vars.AGENT_LIFECYCLE_HOOKS_PATH] = kwargs['lifecycle_hooks_path']
@@ -135,21 +149,32 @@ def run(**kwargs):
135
149
  if kwargs.get('response_fixtures_path'):
136
150
  os.environ[env_vars.AGENT_RESPONSE_FIXTURES_PATH] = kwargs['response_fixtures_path']
137
151
 
138
- # Observe config for changes
139
- Settings.instance().watch()
140
-
141
152
  if not os.getenv(env_vars.LOG_LEVEL):
142
- os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
143
-
144
- if 'api_url' in kwargs and kwargs['api_url']:
145
- os.environ[env_vars.API_URL] = kwargs['api_url']
146
-
147
- url = initialize_api(**kwargs)
148
- if 'headless' in kwargs and not kwargs['headless']:
149
- run_api(url)
150
-
151
- if 'proxyless' in kwargs and not kwargs['proxyless']:
152
- run_proxy(**kwargs)
153
+ os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
154
+
155
+ if kwargs.get('api_url'):
156
+ os.environ[env_vars.API_URL] = kwargs['api_url']
157
+
158
+ if not kwargs.get('headless'):
159
+ ui_url = f"http://{kwargs['ui_host']}:{kwargs['ui_port']}"
160
+ os.environ[env_vars.AGENT_UI_URL] = ui_url
161
+ settings.ui.active = True
162
+ settings.ui.url = ui_url
163
+
164
+ if not kwargs.get('proxyless'):
165
+ proxy_url = f"http://{kwargs['proxy_host']}:{kwargs['proxy_port']}"
166
+ os.environ[env_vars.AGENT_PROXY_URL] = proxy_url
167
+ settings.proxy.url = proxy_url
168
+
169
+ if not kwargs.get('headless'):
170
+ settings.commit()
171
+ run_api(**kwargs)
172
+
173
+ if not kwargs.get('proxyless'):
174
+ log_id = 'Proxy'
175
+ Logger.instance(log_id).info(f"starting with mode {kwargs['proxy_mode']} and listening at {kwargs['proxy_host']}:{kwargs['proxy_port']}")
176
+ Logger.instance(log_id).info(f"{'' if settings.proxy.intercept.active else 'not yet '}configured to {settings.proxy.intercept.mode}")
177
+ run_proxy(**kwargs)
153
178
 
154
179
  @main.command(
155
180
  help="Mock request"
@@ -13,6 +13,7 @@ PROJECT_KEY = 'X-Stoobly-Project-Key'
13
13
  PROXY_MODE = 'X-Stoobly-Proxy-Mode'
14
14
  PUBLIC_DIRECTORY_PATH = 'X-Stoobly-Public-Directory-Path'
15
15
  RECORD_POLICY = 'X-Stoobly-Record-Policy'
16
+ RECORD_STRATEGY = 'X-Stoobly-Record-Strategy'
16
17
  REPORT_KEY = 'X-Stoobly-Report-Key'
17
18
  REQUEST_ORIGIN = 'X-Stoobly-Request-Origin'
18
19
  RESPONSE_FIXTURES_PATH = 'X-Stoobly-Response-Fixtures-Path'
@@ -1,9 +1,10 @@
1
- AGENT_ACTIVE_MODE = 'STOOBLY_AGENT_ACTIVE_MODE'
2
1
  AGENT_CONFIG_PATH = 'STOOBLY_AGENT_CONFIG_PATH'
3
2
  AGENT_ENABLED = 'STOOBLY_AGENT_ENABLED'
4
3
  AGENT_SELF_INTERCEPT_ENABLED = 'STOOBLY_AGENT_SELF_INTERCEPT_ENABLED'
4
+ AGENT_HEADLESS = 'STOOBLY_AGENT_HEADLESS'
5
5
  AGENT_INCLUDE_PATTERNS = 'STOOBLY_AGENT_INCLUDE_PATTERNS'
6
- AGENT_IS_HEADLESS = 'STOOBLY_AGENT_IS_HEADLESS'
6
+ AGENT_INTERCEPT_ACTIVE = 'STOOBLY_AGENT_INTERCEPT_ACTIVE'
7
+ AGENT_INTERCEPT_MODE = 'STOOBLY_AGENT_INTERCEPT_MODE'
7
8
  AGENT_EXCLUDE_PATTERNS = 'STOOBLY_AGENT_EXCLUDE_PATTERNS'
8
9
  AGENT_LIFECYCLE_HOOKS_PATH = 'STOOBLY_AGENT_LIFECYCLE_HOOKS_PATH'
9
10
  AGENT_POLICY = 'STOOBLY_AGENT_POLICY'
@@ -18,7 +19,7 @@ AGENT_RESPONSE_FIXTURES_PATH = 'STOOBLY_AGENT_RESPONSE_FIXTURES_PATH'
18
19
  AGENT_SERVICE_URL = 'STOOBLY_AGENT_SERVICE_URL'
19
20
  AGENT_SCENARIO_KEY = 'STOOBLY_AGENT_SCENARIO_KEY'
20
21
  AGENT_SIMULATE_LATENCY = 'STOOBLY_AGENT_SIMULATE_LATENCY'
21
- AGENT_URL = 'STOOBLY_AGENT_URL'
22
+ AGENT_UI_URL = 'STOOBLY_AGENT_UI_URL'
22
23
  API_URL = 'STOOBLY_API_URL'
23
24
  API_KEY = 'STOOBLY_API_KEY'
24
25
  ENV = 'STOOBLY_AGENT_ENV'
@@ -0,0 +1,6 @@
1
+ from typing import Literal
2
+
3
+ FULL = 'full'
4
+ MINIMAL = 'minimal'
5
+
6
+ RecordStrategy = Literal[FULL, MINIMAL]
@@ -12,10 +12,9 @@ proxy:
12
12
  project_key: eyJpIjowLCJvIjowfQ==
13
13
  firewall: {}
14
14
  rewrite: {}
15
- proxy_config_path: config/settings.yml
16
15
  remote:
17
16
  api_key: ''
18
- api_url: 'http://localhost:3000'
17
+ api_url: ''
19
18
  ui:
20
19
  active: false
21
- url: http://localhost:4200
20
+ url: ''