stoobly-agent 1.4.2__py3-none-any.whl → 1.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/cli/helpers/handle_config_update_service.py +2 -2
  3. stoobly_agent/app/cli/helpers/handle_mock_service.py +6 -2
  4. stoobly_agent/app/cli/helpers/request_facade.py +5 -1
  5. stoobly_agent/app/cli/scaffold/constants.py +1 -1
  6. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +1 -0
  7. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +19 -19
  8. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  9. stoobly_agent/app/cli/scaffold/templates/constants.py +3 -3
  10. stoobly_agent/app/cli/scaffold/templates/factory.py +5 -5
  11. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +1 -8
  12. stoobly_agent/app/cli/scaffold/templates/workflow/mock/fixtures.yml +1 -1
  13. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +1 -8
  14. stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures.yml +1 -1
  15. stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
  16. stoobly_agent/app/cli/scaffold/workflow_create_command.py +2 -2
  17. stoobly_agent/app/cli/scaffold_cli.py +5 -5
  18. stoobly_agent/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request.py +1 -1
  19. stoobly_agent/app/models/factories/resource/local_db/request_adapter.py +17 -11
  20. stoobly_agent/app/models/types/request.py +1 -2
  21. stoobly_agent/app/proxy/context.py +4 -0
  22. stoobly_agent/app/proxy/handle_mock_service.py +93 -46
  23. stoobly_agent/app/proxy/handle_record_service.py +15 -3
  24. stoobly_agent/app/proxy/handle_replay_service.py +44 -18
  25. stoobly_agent/app/proxy/handle_test_service.py +92 -24
  26. stoobly_agent/app/proxy/intercept_handler.py +11 -16
  27. stoobly_agent/app/proxy/intercept_settings.py +17 -4
  28. stoobly_agent/app/proxy/mitmproxy/request_facade.py +5 -2
  29. stoobly_agent/app/proxy/mitmproxy/response_facade.py +5 -4
  30. stoobly_agent/app/proxy/mock/custom_not_found_response_builder.py +5 -0
  31. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +79 -14
  32. stoobly_agent/app/proxy/mock/eval_request_service.py +18 -13
  33. stoobly_agent/app/proxy/record/join_request_service.py +7 -8
  34. stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
  35. stoobly_agent/app/proxy/replay/replay_request_service.py +4 -4
  36. stoobly_agent/app/proxy/test/helpers/upload_test_service.py +2 -2
  37. stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -3
  38. stoobly_agent/app/proxy/utils/response_handler.py +10 -1
  39. stoobly_agent/app/proxy/utils/rewrite.py +72 -0
  40. stoobly_agent/app/settings/constants/request_component.py +4 -1
  41. stoobly_agent/cli.py +35 -28
  42. stoobly_agent/config/constants/custom_headers.py +1 -0
  43. stoobly_agent/config/constants/intercept_policy.py +2 -0
  44. stoobly_agent/config/constants/mock_policy.py +4 -2
  45. stoobly_agent/config/constants/query_params.py +2 -0
  46. stoobly_agent/config/constants/record_policy.py +4 -2
  47. stoobly_agent/config/constants/replay_policy.py +4 -2
  48. stoobly_agent/public/{18-es2015.583f191cc7ad512ee262.js → 18-es2015.503207073756a9c8211a.js} +1 -1
  49. stoobly_agent/public/{18-es5.583f191cc7ad512ee262.js → 18-es5.503207073756a9c8211a.js} +1 -1
  50. stoobly_agent/public/index.html +1 -1
  51. stoobly_agent/public/{main-es2015.2cc16523aa3fcaba51e5.js → main-es2015.d682619f3d6d53d64c6a.js} +1 -1
  52. stoobly_agent/public/{main-es5.2cc16523aa3fcaba51e5.js → main-es5.d682619f3d6d53d64c6a.js} +1 -1
  53. stoobly_agent/public/{runtime-es2015.b914470164e4d6e75d96.js → runtime-es2015.8c1efed946fc02c923fc.js} +1 -1
  54. stoobly_agent/public/{runtime-es5.b914470164e4d6e75d96.js → runtime-es5.8c1efed946fc02c923fc.js} +1 -1
  55. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_test.py +2 -1
  56. stoobly_agent/test/app/cli/scaffold/e2e_test.py +2 -2
  57. stoobly_agent/test/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request_test.py +4 -4
  58. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  59. stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +140 -71
  60. stoobly_agent/test/cli/lifecycle_hooks_test.py +66 -0
  61. stoobly_agent/test/cli/mock_scenario_lifecycle_hooks.py +5 -0
  62. stoobly_agent/test/cli/mock_scenario_test.py +62 -0
  63. stoobly_agent/test/cli/mock_test.py +54 -38
  64. stoobly_agent/test/cli/record_test.py +67 -0
  65. stoobly_agent/test/mock_data/lifecycle_hooks.py +35 -0
  66. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/LICENSE +1 -1
  67. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/METADATA +7 -12
  68. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/RECORD +72 -65
  69. /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{fixtures/.keep → public/.gitignore} +0 -0
  70. /stoobly_agent/app/cli/scaffold/templates/workflow/test/{fixtures/.keep → public/.gitignore} +0 -0
  71. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/WHEEL +0 -0
  72. {stoobly_agent-1.4.2.dist-info → stoobly_agent-1.5.1.dist-info}/entry_points.txt +0 -0
stoobly_agent/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  COMMAND = 'stoobly-agent'
2
- VERSION = '1.4.2'
2
+ VERSION = '1.5.1'
@@ -58,9 +58,9 @@ def handle_intercept_active_update(new_settings: Settings, context: Context = No
58
58
  scenario_model.update(_scenario_key.id, **{ 'overwritable': False })[1]
59
59
  elif _mode == intercept_mode.MOCK:
60
60
  # When mock is stopped, clear request access counts
61
- from stoobly_agent.app.models.factories.resource.local_db.helpers.tiebreak_scenario_request import reset
61
+ from stoobly_agent.app.models.factories.resource.local_db.helpers.tiebreak_scenario_request import reset_sessions
62
62
 
63
- reset()
63
+ reset_sessions()
64
64
 
65
65
  def handle_scenario_update(new_settings: Settings, context = None):
66
66
  new_scenario_key = __scenario_key(new_settings.proxy)
@@ -6,9 +6,13 @@ from stoobly_agent.app.proxy.record.response_string import ResponseString
6
6
 
7
7
  RAW_FORMAT = 'raw'
8
8
 
9
- def print_raw_response(response: requests.Response):
9
+ def print_raw_response(response: requests.Response, file_path = None):
10
10
  mitmproxy_response = PythonResponseAdapterFactory(response).mitmproxy_response()
11
11
  facade = MitmproxyResponseFacade(mitmproxy_response)
12
12
  response_string = ResponseString(facade, None)
13
13
 
14
- print(response_string.get().decode(), end="")
14
+ if not file_path:
15
+ print(response_string.get().decode(), end="")
16
+ else:
17
+ with open(file_path, 'w') as fp:
18
+ fp.write(response_string.get().decode())
@@ -53,10 +53,14 @@ class RequestFacade(ReplayFacade):
53
53
  def replay(self, request_key: str, cli_options: ReplayCliOptions):
54
54
  replay_context = self.__build_replay_context(request_key)
55
55
  replay_options = {
56
- 'mode': mode.RECORD if cli_options.get('record') else mode.REPLAY,
56
+ 'mode': mode.REPLAY,
57
57
  **self.__common_replay_options(request_key),
58
58
  **self.common_replay_cli_options(cli_options)
59
59
  }
60
+
61
+ if cli_options.get('record'):
62
+ replay_options['response_mode'] = mode.RECORD
63
+
60
64
  trace_context = replay_options.get('trace_context')
61
65
 
62
66
  return self.__replay(replay_context, trace_context, replay_options)
@@ -14,8 +14,8 @@ CONFIG_FILE = '.config.yml'
14
14
  CONTEXT_DIR_ENV = 'CONTEXT_DIR'
15
15
  DOCKER_NAMESPACE = 'docker'
16
16
  ENV_FILE = '.env'
17
- FIXTURES_FOLDER_NAME = 'fixtures'
18
17
  NAMESERVERS_FILE = '.nameservers'
18
+ PUBLIC_FOLDER_NAME = 'public'
19
19
  SERVICE_DETACHED = '${SERVICE_DETACHED}'
20
20
  SERVICE_DETACHED_ENV = 'SERVICE_DETACHED'
21
21
  SERVICE_HOSTNAME = '${SERVICE_HOSTNAME}'
@@ -26,6 +26,7 @@ class MockDecorator():
26
26
  '--lifecycle-hooks-path', 'lifecycle_hooks.py',
27
27
  '--proxy-mode', SERVICE_PROXY_MODE,
28
28
  '--proxy-port', f"{SERVICE_PORT}",
29
+ '--public-directory-path', 'public',
29
30
  '--response-fixtures-path', 'fixtures.yml',
30
31
  '--ssl-insecure'
31
32
  ]
@@ -10,7 +10,7 @@ import yaml
10
10
  from docker.models.containers import Container
11
11
 
12
12
  from stoobly_agent.app.cli.scaffold.constants import (
13
- FIXTURES_FOLDER_NAME,
13
+ PUBLIC_FOLDER_NAME,
14
14
  STOOBLY_DATA_DIR,
15
15
  VIRTUAL_HOST_ENV,
16
16
  VIRTUAL_PORT_ENV,
@@ -40,8 +40,8 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
40
40
  )
41
41
 
42
42
  @property
43
- def fixtures_dir_path(self):
44
- return os.path.join(self.workflow_path, FIXTURES_FOLDER_NAME)
43
+ def public_dir_path(self):
44
+ return os.path.join(self.workflow_path, PUBLIC_FOLDER_NAME)
45
45
 
46
46
  @property
47
47
  def workflow_path(self):
@@ -124,14 +124,14 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
124
124
  if ('200 OK' not in logs) and ('499' not in logs):
125
125
  raise ScaffoldValidateException(f"Error reaching {url} from inside Docker network")
126
126
 
127
- # Check fixtures folder mounted into container
128
- def validate_fixtures_folder(self, container: Container):
127
+ # Check public folder exists in container
128
+ def validate_public_folder(self, container: Container):
129
129
 
130
130
  if self.workflow_name == WORKFLOW_RECORD_TYPE:
131
- print(f"Skipping validating fixtures folder in workflow: {self.workflow_name}, container: {container.name}")
131
+ print(f"Skipping validating public folder in workflow: {self.workflow_name}, container: {container.name}")
132
132
  return
133
133
 
134
- print(f"Validating fixtures folder in container: {container.name}")
134
+ print(f"Validating public folder in container: {container.name}")
135
135
 
136
136
  data_dir_mounted = False
137
137
  volume_mounts = container.attrs['Mounts']
@@ -145,21 +145,21 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
145
145
 
146
146
  # Only the running proxy containers will be checkable
147
147
  if container.status == 'exited':
148
- print(f"Skipping validating fixtures folder contents because container is exited: {container.name}")
148
+ print(f"Skipping validating public folder contents because container is exited: {container.name}")
149
149
  return
150
150
 
151
- # Check contents of fixtures folder to confirm it's shared
152
- fixtures_folder_path = f"{FIXTURES_FOLDER_NAME}"
153
- exec_result = container.exec_run(f"ls -A {fixtures_folder_path}")
151
+ # Check contents of public folder to confirm it's shared
152
+ public_folder_path = f"{PUBLIC_FOLDER_NAME}"
153
+ exec_result = container.exec_run(f"ls -A {public_folder_path}")
154
154
  output = exec_result.output
155
155
 
156
- fixtures_folder_contents_container = output.decode('ascii').split('\n')
157
- if fixtures_folder_contents_container[-1] == '':
158
- fixtures_folder_contents_container.pop()
159
- fixtures_folder_contents_scaffold = os.listdir(self.fixtures_dir_path)
156
+ public_folder_contents_container = output.decode('ascii').split('\n')
157
+ if public_folder_contents_container[-1] == '':
158
+ public_folder_contents_container.pop()
159
+ public_folder_contents_scaffold = os.listdir(self.public_dir_path)
160
160
 
161
- if Counter(fixtures_folder_contents_container) != Counter(fixtures_folder_contents_scaffold):
162
- raise ScaffoldValidateException(f"Fixtures was not mounted properly, expected {self.fixtures_dir_path} to exist in container path {fixtures_folder_path}")
161
+ if Counter(public_folder_contents_container) != Counter(public_folder_contents_scaffold):
162
+ raise ScaffoldValidateException(f"public folder was not mounted properly, expected {self.public_dir_path} to exist in container path {public_folder_path}")
163
163
 
164
164
  # Note: might not need this if the hostname is reachable and working
165
165
  def proxy_environment_variables_exist(self, container: Container) -> None:
@@ -191,7 +191,7 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
191
191
  raise ScaffoldValidateException(f"Container attributes are missing for: {container.name}")
192
192
 
193
193
  if not self.service_config.detached:
194
- self.validate_fixtures_folder(service_proxy_container)
194
+ self.validate_public_folder(service_proxy_container)
195
195
 
196
196
  self.proxy_environment_variables_exist(service_proxy_container)
197
197
 
@@ -215,7 +215,7 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
215
215
 
216
216
  # Service init containers have a mounted dist folder unlike the core init container
217
217
  init_container = self.docker_client.containers.get(self.service_docker_compose.init_container_name)
218
- self.validate_fixtures_folder(init_container)
218
+ self.validate_public_folder(init_container)
219
219
 
220
220
  if self.service_config.hostname:
221
221
  service_proxy_container = self.docker_client.containers.get(self.service_docker_compose.proxy_container_name)
@@ -1,4 +1,4 @@
1
- FROM stoobly/agent:1.4
1
+ FROM stoobly/agent:1.5
2
2
 
3
3
  ARG USER_ID
4
4
 
@@ -18,12 +18,12 @@ CUSTOM_FIXTURES = 'fixtures.yml'
18
18
  CUSTOM_LIFECYCLE_HOOKS = os.path.join('lifecycle_hooks.py')
19
19
  MAINTAINED_CONFIGURE = os.path.join('bin', '.configure')
20
20
  MAINTAINED_INIT = os.path.join('bin', '.init')
21
- MAINTAINED_FIXTURES = os.path.join('fixtures', '.keep')
21
+ MAINTAINED_PUBLIC = os.path.join('public', '.gitignore')
22
22
 
23
23
  MOCK_WORKFLOW_MAINTAINED_FILES = [
24
24
  MAINTAINED_CONFIGURE,
25
25
  MAINTAINED_INIT,
26
- MAINTAINED_FIXTURES
26
+ MAINTAINED_PUBLIC
27
27
  ]
28
28
 
29
29
  MOCK_WORKFLOW_CUSTOM_FILES = [
@@ -49,7 +49,7 @@ RECORD_WORKFLOW_CUSTOM_FILES = [
49
49
  TEST_WORKFLOW_MAINTAINED_FILES = [
50
50
  MAINTAINED_CONFIGURE,
51
51
  MAINTAINED_INIT,
52
- MAINTAINED_FIXTURES
52
+ MAINTAINED_PUBLIC
53
53
  ]
54
54
 
55
55
  TEST_WORKFLOW_CUSTOM_FILES = [
@@ -1,7 +1,7 @@
1
1
  from ..constants import WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
2
2
  from ..docker.workflow.builder import WorkflowBuilder
3
3
  from .constants import (
4
- CUSTOM_CONFIGURE, CUSTOM_INIT, MAINTAINED_CONFIGURE, MAINTAINED_FIXTURES, MOCK_WORKFLOW_CUSTOM_FILES, MOCK_WORKFLOW_MAINTAINED_FILES, RECORD_WORKFLOW_CUSTOM_FILES, RECORD_WORKFLOW_MAINTAINED_FILES, TEST_WORKFLOW_CUSTOM_FILES, TEST_WORKFLOW_MAINTAINED_FILES
4
+ CUSTOM_CONFIGURE, CUSTOM_INIT, MAINTAINED_CONFIGURE, MAINTAINED_PUBLIC, MOCK_WORKFLOW_CUSTOM_FILES, MOCK_WORKFLOW_MAINTAINED_FILES, RECORD_WORKFLOW_CUSTOM_FILES, RECORD_WORKFLOW_MAINTAINED_FILES, TEST_WORKFLOW_CUSTOM_FILES, TEST_WORKFLOW_MAINTAINED_FILES
5
5
  )
6
6
 
7
7
  def custom_files(workflow: str, workflow_builder: WorkflowBuilder):
@@ -21,8 +21,8 @@ def custom_files(workflow: str, workflow_builder: WorkflowBuilder):
21
21
 
22
22
  # Fixtures are only relevant if the workflow is mock/test and if the service has a hostname
23
23
  if not workflow_builder.config.hostname:
24
- if MAINTAINED_FIXTURES in files:
25
- files.remove(MAINTAINED_FIXTURES)
24
+ if MAINTAINED_PUBLIC in files:
25
+ files.remove(MAINTAINED_PUBLIC)
26
26
 
27
27
  return files
28
28
 
@@ -40,7 +40,7 @@ def maintained_files(workflow: str, workflow_builder: WorkflowBuilder):
40
40
  files.append(MAINTAINED_CONFIGURE)
41
41
 
42
42
  if not workflow_builder.config.hostname:
43
- if MAINTAINED_FIXTURES in files:
44
- files.remove(MAINTAINED_FIXTURES)
43
+ if MAINTAINED_PUBLIC in files:
44
+ files.remove(MAINTAINED_PUBLIC)
45
45
 
46
46
  return files
@@ -10,11 +10,4 @@ url="$scheme://$hostname"
10
10
 
11
11
  if [ "$scheme" = 'http' -a "$port" != '80' ] || [ "$scheme" = 'https' -a "$port" != '443' ]; then
12
12
  url="$url:$port"
13
- fi
14
-
15
- # Match Rules
16
- echo "Configuring match rules"
17
- stoobly-agent config match set \
18
- --method GET --method POST --method OPTIONS --method PUT --method DELETE \
19
- --mode mock \
20
- --pattern ".*?"
13
+ fi
@@ -1,5 +1,5 @@
1
1
  # The following example matches requests for GET /users/d+ (e.g. /users/1) with the contents of user-1.json
2
- # Assumes that 'user-1.json' is created in the 'fixtures' folder
2
+ # Assumes that 'user-1.json' is created in a 'fixtures' folder in the same directory as this file
3
3
  #GET:
4
4
  # /users/d+:
5
5
  # path: ./fixtures/user-1.json
@@ -10,11 +10,4 @@ url="$scheme://$hostname"
10
10
 
11
11
  if [ "$scheme" = 'http' -a "$port" != '80' ] || [ "$scheme" = 'https' -a "$port" != '443' ]; then
12
12
  url="$url:$port"
13
- fi
14
-
15
- # Match
16
- echo "Configuring match rules"
17
- stoobly-agent config match set \
18
- --method GET --method POST --method OPTIONS --method PUT --method DELETE \
19
- --mode mock \
20
- --pattern ".*?"
13
+ fi
@@ -1,5 +1,5 @@
1
1
  # The following example matches requests for GET /users/d+ (e.g. /users/1) with the contents of user-1.json
2
- # Assumes that 'user-1.json' is created in the 'fixtures' folder
2
+ # Assumes that 'user-1.json' is created in a 'fixtures' folder in the same directory as this file
3
3
  #GET:
4
4
  # /users/d+:
5
5
  # path: ./fixtures/user-1.json
@@ -6,7 +6,7 @@ from stoobly_agent.lib.logger import Logger
6
6
 
7
7
  from .app import App
8
8
  from .config import Config
9
- from .constants import BIN_FOLDER_NAME, COMPOSE_TEMPLATE, CONFIG_FILE, ENV_FILE, FIXTURES_FOLDER_NAME
9
+ from .constants import BIN_FOLDER_NAME, COMPOSE_TEMPLATE, CONFIG_FILE, ENV_FILE, PUBLIC_FOLDER_NAME
10
10
  from .docker.constants import DOCKER_COMPOSE_CUSTOM
11
11
  from .service_command import ServiceCommand
12
12
 
@@ -88,8 +88,8 @@ class WorkflowCommand(ServiceCommand):
88
88
  return services
89
89
 
90
90
  @property
91
- def fixtures_dir_path(self):
92
- return os.path.join(self.workflow_path, FIXTURES_FOLDER_NAME)
91
+ def public_dir_path(self):
92
+ return os.path.join(self.workflow_path, PUBLIC_FOLDER_NAME)
93
93
 
94
94
  @property
95
95
  def workflow_config(self):
@@ -67,11 +67,11 @@ class WorkflowCreateCommand(WorkflowCommand):
67
67
  return
68
68
 
69
69
  # Maintained files are files that will always be overwritten
70
- maintained_workflow_files = maintained_files(self.workflow_name, workflow_builder)
70
+ maintained_workflow_files = maintained_files(template or self.workflow_name, workflow_builder)
71
71
  self.copy_files(templates_path, maintained_workflow_files, self.workflow_path)
72
72
 
73
73
  # Custom files are files that may be modified by the user
74
- custom_workflow_files = custom_files(self.workflow_name, workflow_builder)
74
+ custom_workflow_files = custom_files(template or self.workflow_name, workflow_builder)
75
75
  self.copy_files_no_replace(templates_path, custom_workflow_files, self.workflow_path)
76
76
 
77
77
  def __write_docker_compose_file(self, **kwargs: BuildOptions):
@@ -140,19 +140,19 @@ def mkcert(**kwargs):
140
140
  help="Scaffold a service",
141
141
  )
142
142
  @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
143
- @click.option('--detached', is_flag=True)
143
+ @click.option('--detached', is_flag=True, help='Use isolated and non-persistent context directory.')
144
144
  @click.option('--env', multiple=True, help='Specify an environment variable.')
145
145
  @click.option('--force', is_flag=True, help='Overwrite maintained scaffolded service files.')
146
- @click.option('--hostname')
147
- @click.option('--port')
148
- @click.option('--priority', default='5.0', help='Determines the service run order.')
146
+ @click.option('--hostname', help='Service hostname.')
147
+ @click.option('--port', help='Service port.')
148
+ @click.option('--priority', default=5, type=click.FloatRange(1.0, 9.0), help='Determines the service run order. Lower values run first.')
149
149
  @click.option('--proxy-mode', help='''
150
150
  Proxy mode can be "regular", "transparent", "socks5",
151
151
  "reverse:SPEC", or "upstream:SPEC". For reverse and
152
152
  upstream proxy modes, SPEC is host specification in
153
153
  the form of "http[s]://host[:port]".
154
154
  ''')
155
- @click.option('--scheme', type=click.Choice(['http', 'https']))
155
+ @click.option('--scheme', type=click.Choice(['http', 'https']), help='Defaults to https if hostname is set.')
156
156
  @click.option('--workflow', multiple=True, type=click.Choice([WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE]), help='Include pre-defined workflows.')
157
157
  @click.argument('service_name')
158
158
  def create(**kwargs):
@@ -20,7 +20,7 @@ def generate_session_id(query: dict):
20
20
 
21
21
  return hashlib.md5(b'.'.join(toks)).hexdigest()
22
22
 
23
- def reset():
23
+ def reset_sessions():
24
24
  Cache.instance().clear(f".+\.{PREFIX}")
25
25
 
26
26
  def tiebreak_scenario_request(session_id: str, requests: List[Request]):
@@ -10,7 +10,7 @@ from stoobly_agent.app.models.types import RequestCreateParams, RequestDestroyPa
10
10
  from stoobly_agent.app.proxy.mock.custom_not_found_response_builder import CustomNotFoundResponseBuilder
11
11
  from stoobly_agent.app.proxy.mock.ignored_components_response_builder import IgnoreComponentsResponseBuilder
12
12
  from stoobly_agent.app.proxy.record.joined_request import JoinedRequest
13
- from stoobly_agent.config.constants import custom_headers
13
+ from stoobly_agent.config.constants import custom_headers, query_params as request_query_params
14
14
  from stoobly_agent.lib.orm import ORM
15
15
  from stoobly_agent.lib.orm.request import Request
16
16
  from stoobly_agent.lib.orm.response import Response
@@ -73,7 +73,6 @@ class LocalDBRequestAdapter(LocalDBAdapter):
73
73
  def response(self, **query_params: RequestColumns) -> requests.Response:
74
74
  self.__adapt_scenario_id(query_params)
75
75
 
76
- endpoint_promise = query_params.get('endpoint_promise')
77
76
  request = None
78
77
 
79
78
  if not query_params.get('request_id'):
@@ -85,16 +84,19 @@ class LocalDBRequestAdapter(LocalDBAdapter):
85
84
  requests = self.__request_orm.where_for(**request_columns).get()
86
85
 
87
86
  if 'scenario_id' in query_params:
88
- # TODO: Would need an additional ID to distinguish different scenario sessions
89
- session_id = generate_session_id(request_columns)
90
-
91
87
  if len(requests) > 1:
92
- request = tiebreak_scenario_request(session_id, requests)
88
+ session_id = query_params.get(request_query_params.SESSION_ID)
89
+ request_session_id_components = { **request_columns }
90
+
91
+ if session_id:
92
+ request_session_id_components[request_query_params.SESSION_ID] = session_id
93
+
94
+ # When multiple requests are matched for a scenario, return them in sequence
95
+ request_session_id = generate_session_id(request_session_id_components)
96
+ request = tiebreak_scenario_request(request_session_id, requests)
97
+ access_request(request_session_id, request.id)
93
98
  else:
94
99
  request = requests.last()
95
-
96
- if request:
97
- access_request(session_id, request.id)
98
100
  else:
99
101
  request = requests.last()
100
102
  else:
@@ -104,6 +106,7 @@ class LocalDBRequestAdapter(LocalDBAdapter):
104
106
  request = None
105
107
 
106
108
  if not request:
109
+ endpoint_promise = query_params.get(request_query_params.ENDPOINT_PROMISE)
107
110
  return self.__handle_request_not_found(endpoint_promise)
108
111
 
109
112
  response_record = request.response
@@ -315,8 +318,8 @@ class LocalDBRequestAdapter(LocalDBAdapter):
315
318
  return candidates.get()
316
319
 
317
320
  def __filter_request_response_columns(self, request_columns: RequestCreateParams):
318
- if request_columns.get('endpoint_promise'):
319
- del request_columns['endpoint_promise']
321
+ if request_columns.get(request_query_params.ENDPOINT_PROMISE):
322
+ del request_columns[request_query_params.ENDPOINT_PROMISE]
320
323
 
321
324
  if request_columns.get('infer'):
322
325
  del request_columns['infer']
@@ -327,6 +330,9 @@ class LocalDBRequestAdapter(LocalDBAdapter):
327
330
  if request_columns.get('retry'):
328
331
  del request_columns['retry']
329
332
 
333
+ if request_columns.get(request_query_params.SESSION_ID):
334
+ del request_columns[request_query_params.SESSION_ID]
335
+
330
336
  def __request(self, request_id: str):
331
337
  if self.validate_uuid(request_id):
332
338
  return self.__request_orm.find_by(uuid=request_id)
@@ -29,5 +29,4 @@ class RequestShowParams(TypedDict):
29
29
  headers: bool
30
30
  project_id: str
31
31
  query_params: bool
32
- response: bool
33
-
32
+ response: bool
@@ -12,6 +12,10 @@ class InterceptContext():
12
12
  def flow(self):
13
13
  return self.__flow
14
14
 
15
+ @flow.setter
16
+ def flow(self, v: MitmproxyHTTPFlow):
17
+ self.__flow = v
18
+
15
19
  @property
16
20
  def intercept_settings(self):
17
21
  return self.__intercept_settings
@@ -3,7 +3,7 @@ import pdb
3
3
  import requests
4
4
  import time
5
5
 
6
- from mitmproxy.http import Request as MitmproxyRequest
6
+ from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow, Request as MitmproxyRequest
7
7
  from typing import Callable, TypedDict
8
8
 
9
9
  from stoobly_agent.app.models.request_model import RequestModel
@@ -18,7 +18,8 @@ from .mock.eval_fixtures_service import eval_fixtures
18
18
  from .mock.eval_request_service import inject_eval_request
19
19
  from .utils.allowed_request_service import get_active_mode_policy
20
20
  from .utils.request_handler import reverse_proxy
21
- from .utils.response_handler import bad_request, pass_on
21
+ from .utils.response_handler import bad_request, enable_cors, pass_on
22
+ from .utils.rewrite import rewrite_request, rewrite_response
22
23
 
23
24
  LOG_ID = 'Mock'
24
25
 
@@ -26,28 +27,51 @@ class MockOptions(TypedDict):
26
27
  failure: Callable
27
28
  ignored_components: list
28
29
  infer: bool
30
+ no_rewrite: bool
29
31
  success: Callable
30
32
 
33
+ def handle_request_mock_generic_without_rewrite(context: MockContext, **options: MockOptions):
34
+ options['no_rewrite'] = True
35
+ handle_request_mock_generic(context, **options)
36
+
31
37
  ###
32
38
  #
39
+ # 1. Rewrites mock request by default
40
+ # 2. BEFORE_MOCK gets triggered
41
+ # 3. AFTER_MOCK gets triggered
42
+ #
33
43
  # @param request [mitmproxy.http.Request]
34
44
  # @param settings [Dict]
35
45
  #
36
46
  def handle_request_mock_generic(context: MockContext, **options: MockOptions):
37
- __mock_hook(lifecycle_hooks.BEFORE_MOCK, context)
38
-
47
+ handle_error = options['error'] if 'error' in options and callable(options['error']) else None
48
+ handle_failure = options['failure'] if 'failure' in options and callable(options['failure']) else None
49
+ handle_success = options['success'] if 'success' in options and callable(options['success']) else None
39
50
  intercept_settings = context.intercept_settings
40
51
  request: MitmproxyRequest = context.flow.request
41
- request_model = RequestModel(intercept_settings.settings)
52
+ res = None
42
53
 
43
54
  policy = get_active_mode_policy(request, intercept_settings)
55
+ if policy == mock_policy.NONE:
56
+ if handle_error:
57
+ res = handle_error(context)
44
58
 
45
- rewrite_rules = intercept_settings.mock_rewrite_rules
46
- if len(rewrite_rules) > 0:
47
- # Rewrite request with paramter rules for mock
48
- request: MitmproxyRequest = context.flow.request
49
- request_facade = MitmproxyRequestFacade(request)
50
- request_facade.with_parameter_rules(rewrite_rules).with_url_rules(rewrite_rules).rewrite()
59
+ return pass_on(context.flow, res)
60
+
61
+ if policy not in [mock_policy.ALL, mock_policy.FOUND]:
62
+ if handle_error:
63
+ res = handle_error(context)
64
+
65
+ return bad_request(
66
+ context.flow,
67
+ "Valid env MOCK_POLICY: %s, Got: %s" %
68
+ ([mock_policy.ALL, mock_policy.FOUND, mock_policy.NONE], policy)
69
+ )
70
+
71
+ if not options.get('no_rewrite'):
72
+ __rewrite_request(context)
73
+
74
+ __mock_hook(lifecycle_hooks.BEFORE_MOCK, context)
51
75
 
52
76
  # If ignore rules are set, then ignore specified request parameters
53
77
  ignore_rules = intercept_settings.ignore_rules
@@ -55,48 +79,33 @@ def handle_request_mock_generic(context: MockContext, **options: MockOptions):
55
79
  request_facade = MitmproxyRequestFacade(request)
56
80
  _ignore_rules = request_facade.select_parameter_rules(ignore_rules)
57
81
  ignored_components = rewrite_rules_to_ignored_components(_ignore_rules)
58
- options['ignored_components'] += ignored_components if 'ignored_components' in options else ignored_components
82
+ options['ignored_components'] += ignored_components if 'ignored_components' in options else ignored_components
59
83
 
60
- handle_success = options['success'] if 'success' in options and callable(options['success']) else None
61
- handle_failure = options['failure'] if 'failure' in options and callable(options['failure']) else None
62
-
84
+ request_model = RequestModel(intercept_settings.settings)
63
85
  eval_request = inject_eval_request(request_model, intercept_settings)
64
-
65
- if policy == mock_policy.NONE:
66
- if handle_failure:
67
- res = handle_failure(context)
68
- elif policy == mock_policy.ALL:
86
+
87
+ if policy == mock_policy.ALL:
69
88
  res = eval_request_with_retry(context, eval_request, **options)
70
89
 
71
90
  context.with_response(res)
72
-
73
- if handle_success:
74
- # TODO: rewrite response, see #332
75
- res = handle_success(context) or res
76
91
  elif policy == mock_policy.FOUND:
77
92
  res = eval_request_with_retry(context, eval_request, **options)
78
93
 
79
94
  context.with_response(res)
80
95
 
81
96
  if res.status_code in [custom_response_codes.NOT_FOUND, custom_response_codes.IGNORE_COMPONENTS]:
82
- if handle_failure:
83
- try:
84
- res = handle_failure(context)
85
- except RuntimeError:
86
- # Do nothing, return custom error response
87
- pass
88
- else:
89
- if handle_success:
90
- # TODO: rewrite response, see #332
91
- res = handle_success(context) or res
97
+ try:
98
+ res = __handle_found_policy(context)
99
+ except RuntimeError:
100
+ # Do nothing, return custom error response
101
+ pass
102
+
103
+ if res.status_code == custom_response_codes.NOT_FOUND:
104
+ if handle_failure:
105
+ res = handle_failure(context) or res
92
106
  else:
93
- return bad_request(
94
- context.flow,
95
- "Valid env MOCK_POLICY: %s, Got: %s" %
96
- ([mock_policy.ALL, mock_policy.FOUND, mock_policy.NONE], policy)
97
- )
98
-
99
- __mock_hook(lifecycle_hooks.AFTER_MOCK, context)
107
+ if handle_success:
108
+ res = handle_success(context) or res
100
109
 
101
110
  return pass_on(context.flow, res)
102
111
 
@@ -130,6 +139,11 @@ def handle_request_mock(context: MockContext):
130
139
  success=lambda context: __handle_mock_success(context)
131
140
  )
132
141
 
142
+ ###
143
+ #
144
+ # 1. Rewrites mock response (if mock found)
145
+ # 2. AFTER_MOCK gets triggered (if mock found)
146
+ #
133
147
  def handle_response_mock(context: MockContext):
134
148
  response = context.flow.response
135
149
  request_key = response.headers.get(custom_headers.MOCK_REQUEST_KEY)
@@ -138,13 +152,22 @@ def handle_response_mock(context: MockContext):
138
152
  request = context.flow.request
139
153
  Logger.instance(LOG_ID).info(f"{bcolors.OKCYAN}Mocked{bcolors.ENDC} {request.url} -> {request_key}")
140
154
 
141
- def __handle_mock_success(context: MockContext) -> None:
142
- if os.environ.get(env_vars.AGENT_SIMULATE_LATENCY):
143
- response = context.response
144
- start_time = context.start_time
145
- __simulate_latency(response.headers.get(custom_headers.RESPONSE_LATENCY), start_time)
155
+ __rewrite_response(context)
156
+ __mock_hook(lifecycle_hooks.AFTER_MOCK, context)
146
157
 
147
158
  def __handle_mock_failure(context: MockContext) -> None:
159
+ flow = context.flow
160
+ request = flow.request
161
+
162
+ if request.method.upper() != 'OPTIONS':
163
+ return False
164
+
165
+ # Default OPTIONS request to allow CORS
166
+ enable_cors(flow)
167
+
168
+ return True
169
+
170
+ def __handle_found_policy(context: MockContext) -> None:
148
171
  req = context.flow.request
149
172
  intercept_settings = context.intercept_settings
150
173
  upstream_url = intercept_settings.upstream_url
@@ -160,6 +183,30 @@ def __handle_mock_failure(context: MockContext) -> None:
160
183
 
161
184
  reverse_proxy(req, upstream_url, {})
162
185
 
186
+ def __handle_mock_success(context: MockContext) -> None:
187
+ if os.environ.get(env_vars.AGENT_SIMULATE_LATENCY):
188
+ response = context.response
189
+ start_time = context.start_time
190
+ __simulate_latency(response.headers.get(custom_headers.RESPONSE_LATENCY), start_time)
191
+
192
+ def __rewrite_request(context: MockContext):
193
+ # Rewrite request with paramter rules for mock
194
+
195
+ intercept_settings = context.intercept_settings
196
+ rewrite_rules = intercept_settings.mock_rewrite_rules
197
+
198
+ if len(rewrite_rules) > 0:
199
+ rewrite_request(context.flow, rewrite_rules)
200
+
201
+ def __rewrite_response(context: MockContext):
202
+ # Rewrite request with paramter rules for mock
203
+
204
+ intercept_settings = context.intercept_settings
205
+ rewrite_rules = intercept_settings.mock_rewrite_rules
206
+
207
+ if len(rewrite_rules) > 0:
208
+ rewrite_response(context.flow, rewrite_rules)
209
+
163
210
  ###
164
211
  #
165
212
  # Try to simulate expected response latency