stoobly-agent 1.4.1__py3-none-any.whl → 1.5.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/cli/helpers/handle_mock_service.py +6 -2
- stoobly_agent/app/cli/helpers/request_facade.py +5 -1
- stoobly_agent/app/cli/scaffold/constants.py +1 -1
- stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +3 -2
- stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +2 -2
- stoobly_agent/app/cli/scaffold/service_config.py +16 -2
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +19 -19
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/.Makefile +12 -5
- stoobly_agent/app/cli/scaffold/templates/constants.py +3 -3
- stoobly_agent/app/cli/scaffold/templates/factory.py +5 -5
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +1 -8
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/fixtures.yml +1 -1
- stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +1 -8
- stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures.yml +1 -1
- stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
- stoobly_agent/app/cli/scaffold/workflow_create_command.py +2 -2
- stoobly_agent/app/cli/scaffold_cli.py +65 -50
- stoobly_agent/app/proxy/context.py +4 -0
- stoobly_agent/app/proxy/handle_mock_service.py +81 -54
- stoobly_agent/app/proxy/handle_record_service.py +15 -3
- stoobly_agent/app/proxy/handle_replay_service.py +44 -18
- stoobly_agent/app/proxy/handle_test_service.py +75 -16
- stoobly_agent/app/proxy/intercept_handler.py +11 -16
- stoobly_agent/app/proxy/intercept_settings.py +17 -4
- stoobly_agent/app/proxy/mitmproxy/request_facade.py +5 -2
- stoobly_agent/app/proxy/mitmproxy/response_facade.py +5 -4
- stoobly_agent/app/proxy/mock/eval_fixtures_service.py +78 -14
- stoobly_agent/app/proxy/mock/eval_request_service.py +2 -2
- stoobly_agent/app/proxy/record/join_request_service.py +7 -8
- stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
- stoobly_agent/app/proxy/replay/replay_request_service.py +4 -4
- stoobly_agent/app/proxy/test/helpers/upload_test_service.py +2 -2
- stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -3
- stoobly_agent/app/proxy/utils/response_handler.py +0 -2
- stoobly_agent/app/proxy/utils/rewrite.py +72 -0
- stoobly_agent/app/settings/constants/request_component.py +4 -1
- stoobly_agent/cli.py +35 -28
- stoobly_agent/config/constants/intercept_policy.py +2 -0
- stoobly_agent/config/constants/mock_policy.py +4 -2
- stoobly_agent/config/constants/record_policy.py +4 -2
- stoobly_agent/config/constants/replay_policy.py +4 -2
- stoobly_agent/public/{18-es2015.583f191cc7ad512ee262.js → 18-es2015.503207073756a9c8211a.js} +1 -1
- stoobly_agent/public/{18-es5.583f191cc7ad512ee262.js → 18-es5.503207073756a9c8211a.js} +1 -1
- stoobly_agent/public/index.html +1 -1
- stoobly_agent/public/{main-es2015.2cc16523aa3fcaba51e5.js → main-es2015.d682619f3d6d53d64c6a.js} +1 -1
- stoobly_agent/public/{main-es5.2cc16523aa3fcaba51e5.js → main-es5.d682619f3d6d53d64c6a.js} +1 -1
- stoobly_agent/public/{runtime-es2015.b914470164e4d6e75d96.js → runtime-es2015.8c1efed946fc02c923fc.js} +1 -1
- stoobly_agent/public/{runtime-es5.b914470164e4d6e75d96.js → runtime-es5.8c1efed946fc02c923fc.js} +1 -1
- stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_test.py +2 -1
- stoobly_agent/test/app/cli/scaffold/e2e_test.py +2 -2
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +140 -71
- stoobly_agent/test/cli/lifecycle_hooks_test.py +66 -0
- stoobly_agent/test/cli/mock_test.py +53 -29
- stoobly_agent/test/cli/record_test.py +67 -0
- stoobly_agent/test/mock_data/lifecycle_hooks.py +35 -0
- {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/LICENSE +1 -1
- {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/METADATA +7 -12
- {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/RECORD +65 -61
- /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{fixtures/.keep → public/.gitignore} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/workflow/test/{fixtures/.keep → public/.gitignore} +0 -0
- {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/WHEEL +0 -0
- {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/entry_points.txt +0 -0
@@ -1 +1 @@
|
|
1
|
-
!function(e){function r(r){for(var a,f,o=r[0],b=r[1],d=r[2],i=0,l=[];i<o.length;i++)f=o[i],Object.prototype.hasOwnProperty.call(c,f)&&c[f]&&l.push(c[f][0]),c[f]=0;for(a in b)Object.prototype.hasOwnProperty.call(b,a)&&(e[a]=b[a]);for(u&&u(r);l.length;)l.shift()();return n.push.apply(n,d||[]),t()}function t(){for(var e,r=0;r<n.length;r++){for(var t=n[r],a=!0,o=1;o<t.length;o++)0!==c[t[o]]&&(a=!1);a&&(n.splice(r--,1),e=f(f.s=t[0]))}return e}var a={},c={11:0},n=[];function f(r){if(a[r])return a[r].exports;var t=a[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.e=function(e){var r=[],t=c[e];if(0!==t)if(t)r.push(t[2]);else{var a=new Promise(function(r,a){t=c[e]=[r,a]});r.push(t[2]=a);var n,o=document.createElement("script");o.charset="utf-8",o.timeout=120,f.nc&&o.setAttribute("nonce",f.nc),o.src=function(e){return f.p+""+({3:"common"}[e]||e)+"-es2015."+{0:"c7c5a6d51b98cb85b9fc",1:"bb4492eeabe1fe937ada",2:"51eb8544ea9a21f2874c",3:"388805227aa99c527fba",4:"f8ac5475bf103969b2d2",5:"90ea7bd4439d9749e052",6:"5fb726c0555664300974",7:"19ccb84e62e2ea874f53",8:"b901639e2aeff1358c3d",9:"b7bcad8238f58e214f03",10:"56da22458086513ba0c7",12:"be58ed0ef449008b932e",13:"343b0261a8b3b3f4a1fc",14:"b6619d7742671d2a37fb",15:"d8855701408b0e1d7a3e",16:"98fa59c2c96d2caac3c3",17:"7f60b56d1fd66d4d5544",18:"
|
1
|
+
!function(e){function r(r){for(var a,f,o=r[0],b=r[1],d=r[2],i=0,l=[];i<o.length;i++)f=o[i],Object.prototype.hasOwnProperty.call(c,f)&&c[f]&&l.push(c[f][0]),c[f]=0;for(a in b)Object.prototype.hasOwnProperty.call(b,a)&&(e[a]=b[a]);for(u&&u(r);l.length;)l.shift()();return n.push.apply(n,d||[]),t()}function t(){for(var e,r=0;r<n.length;r++){for(var t=n[r],a=!0,o=1;o<t.length;o++)0!==c[t[o]]&&(a=!1);a&&(n.splice(r--,1),e=f(f.s=t[0]))}return e}var a={},c={11:0},n=[];function f(r){if(a[r])return a[r].exports;var t=a[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.e=function(e){var r=[],t=c[e];if(0!==t)if(t)r.push(t[2]);else{var a=new Promise(function(r,a){t=c[e]=[r,a]});r.push(t[2]=a);var n,o=document.createElement("script");o.charset="utf-8",o.timeout=120,f.nc&&o.setAttribute("nonce",f.nc),o.src=function(e){return f.p+""+({3:"common"}[e]||e)+"-es2015."+{0:"c7c5a6d51b98cb85b9fc",1:"bb4492eeabe1fe937ada",2:"51eb8544ea9a21f2874c",3:"388805227aa99c527fba",4:"f8ac5475bf103969b2d2",5:"90ea7bd4439d9749e052",6:"5fb726c0555664300974",7:"19ccb84e62e2ea874f53",8:"b901639e2aeff1358c3d",9:"b7bcad8238f58e214f03",10:"56da22458086513ba0c7",12:"be58ed0ef449008b932e",13:"343b0261a8b3b3f4a1fc",14:"b6619d7742671d2a37fb",15:"d8855701408b0e1d7a3e",16:"98fa59c2c96d2caac3c3",17:"7f60b56d1fd66d4d5544",18:"503207073756a9c8211a",19:"78ec0e9fd228b5104712",20:"90544c488f1f0900bab4",21:"63ed4e6b242fbc047bd6",22:"1e96ef651f96d51191e2",23:"6d3052555b4fac38f16d",28:"a2bf42f38e4dd771ccea",29:"18b515f07c88753c7eb6",30:"d4d3dc4101400cadf3f3",31:"e3ea815f75c19c217b7d",32:"57553995bff0ae518501",33:"da5ba1e5baa164921198",34:"87acbb95cfb824650157",35:"8f79ff8748d4ff06ab03",36:"e3a104020eeec02cb8c4",37:"3b472e6cf303201bbf2c",38:"39dbf43d46cdc94f78c6",39:"e5475ea4fb6ec5c60741",40:"47b2873a8439717af6a7",41:"69f2771002745c066976",42:"b590b92ede890155dcd6",43:"ad1d956bffba561ba64c",44:"3ef2c17996ed5be95ced",45:"3d65d27e0a7085e96356",46:"e4f0a5b127eae13c27c0",47:"c7d45334a70131f133b5"}[e]+".js"}(e);var b=new Error;n=function(r){o.onerror=o.onload=null,clearTimeout(d);var t=c[e];if(0!==t){if(t){var a=r&&("load"===r.type?"missing":r.type),n=r&&r.target&&r.target.src;b.message="Loading chunk "+e+" failed.\n("+a+": "+n+")",b.name="ChunkLoadError",b.type=a,b.request=n,t[1](b)}c[e]=void 0}};var d=setTimeout(function(){n({type:"timeout",target:o})},12e4);o.onerror=o.onload=n,document.head.appendChild(o)}return Promise.all(r)},f.m=e,f.c=a,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,r){if(1&r&&(e=f(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var a in e)f.d(t,a,(function(r){return e[r]}).bind(null,a));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="",f.oe=function(e){throw console.error(e),e};var o=window.webpackJsonp=window.webpackJsonp||[],b=o.push.bind(o);o.push=r,o=o.slice();for(var d=0;d<o.length;d++)r(o[d]);var u=b;t()}([]);
|
stoobly_agent/public/{runtime-es5.b914470164e4d6e75d96.js → runtime-es5.8c1efed946fc02c923fc.js}
RENAMED
@@ -1 +1 @@
|
|
1
|
-
!function(e){function r(r){for(var a,f,o=r[0],b=r[1],d=r[2],i=0,l=[];i<o.length;i++)f=o[i],Object.prototype.hasOwnProperty.call(c,f)&&c[f]&&l.push(c[f][0]),c[f]=0;for(a in b)Object.prototype.hasOwnProperty.call(b,a)&&(e[a]=b[a]);for(u&&u(r);l.length;)l.shift()();return n.push.apply(n,d||[]),t()}function t(){for(var e,r=0;r<n.length;r++){for(var t=n[r],a=!0,o=1;o<t.length;o++)0!==c[t[o]]&&(a=!1);a&&(n.splice(r--,1),e=f(f.s=t[0]))}return e}var a={},c={11:0},n=[];function f(r){if(a[r])return a[r].exports;var t=a[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.e=function(e){var r=[],t=c[e];if(0!==t)if(t)r.push(t[2]);else{var a=new Promise(function(r,a){t=c[e]=[r,a]});r.push(t[2]=a);var n,o=document.createElement("script");o.charset="utf-8",o.timeout=120,f.nc&&o.setAttribute("nonce",f.nc),o.src=function(e){return f.p+""+({3:"common"}[e]||e)+"-es5."+{0:"c7c5a6d51b98cb85b9fc",1:"bb4492eeabe1fe937ada",2:"51eb8544ea9a21f2874c",3:"388805227aa99c527fba",4:"f8ac5475bf103969b2d2",5:"90ea7bd4439d9749e052",6:"5fb726c0555664300974",7:"19ccb84e62e2ea874f53",8:"b901639e2aeff1358c3d",9:"b7bcad8238f58e214f03",10:"56da22458086513ba0c7",12:"be58ed0ef449008b932e",13:"343b0261a8b3b3f4a1fc",14:"b6619d7742671d2a37fb",15:"d8855701408b0e1d7a3e",16:"98fa59c2c96d2caac3c3",17:"7f60b56d1fd66d4d5544",18:"
|
1
|
+
!function(e){function r(r){for(var a,f,o=r[0],b=r[1],d=r[2],i=0,l=[];i<o.length;i++)f=o[i],Object.prototype.hasOwnProperty.call(c,f)&&c[f]&&l.push(c[f][0]),c[f]=0;for(a in b)Object.prototype.hasOwnProperty.call(b,a)&&(e[a]=b[a]);for(u&&u(r);l.length;)l.shift()();return n.push.apply(n,d||[]),t()}function t(){for(var e,r=0;r<n.length;r++){for(var t=n[r],a=!0,o=1;o<t.length;o++)0!==c[t[o]]&&(a=!1);a&&(n.splice(r--,1),e=f(f.s=t[0]))}return e}var a={},c={11:0},n=[];function f(r){if(a[r])return a[r].exports;var t=a[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.e=function(e){var r=[],t=c[e];if(0!==t)if(t)r.push(t[2]);else{var a=new Promise(function(r,a){t=c[e]=[r,a]});r.push(t[2]=a);var n,o=document.createElement("script");o.charset="utf-8",o.timeout=120,f.nc&&o.setAttribute("nonce",f.nc),o.src=function(e){return f.p+""+({3:"common"}[e]||e)+"-es5."+{0:"c7c5a6d51b98cb85b9fc",1:"bb4492eeabe1fe937ada",2:"51eb8544ea9a21f2874c",3:"388805227aa99c527fba",4:"f8ac5475bf103969b2d2",5:"90ea7bd4439d9749e052",6:"5fb726c0555664300974",7:"19ccb84e62e2ea874f53",8:"b901639e2aeff1358c3d",9:"b7bcad8238f58e214f03",10:"56da22458086513ba0c7",12:"be58ed0ef449008b932e",13:"343b0261a8b3b3f4a1fc",14:"b6619d7742671d2a37fb",15:"d8855701408b0e1d7a3e",16:"98fa59c2c96d2caac3c3",17:"7f60b56d1fd66d4d5544",18:"503207073756a9c8211a",19:"78ec0e9fd228b5104712",20:"90544c488f1f0900bab4",21:"63ed4e6b242fbc047bd6",22:"1e96ef651f96d51191e2",23:"6d3052555b4fac38f16d",28:"a2bf42f38e4dd771ccea",29:"18b515f07c88753c7eb6",30:"d4d3dc4101400cadf3f3",31:"e3ea815f75c19c217b7d",32:"57553995bff0ae518501",33:"da5ba1e5baa164921198",34:"87acbb95cfb824650157",35:"8f79ff8748d4ff06ab03",36:"e3a104020eeec02cb8c4",37:"3b472e6cf303201bbf2c",38:"39dbf43d46cdc94f78c6",39:"e5475ea4fb6ec5c60741",40:"47b2873a8439717af6a7",41:"69f2771002745c066976",42:"b590b92ede890155dcd6",43:"ad1d956bffba561ba64c",44:"3ef2c17996ed5be95ced",45:"3d65d27e0a7085e96356",46:"e4f0a5b127eae13c27c0",47:"c7d45334a70131f133b5"}[e]+".js"}(e);var b=new Error;n=function(r){o.onerror=o.onload=null,clearTimeout(d);var t=c[e];if(0!==t){if(t){var a=r&&("load"===r.type?"missing":r.type),n=r&&r.target&&r.target.src;b.message="Loading chunk "+e+" failed.\n("+a+": "+n+")",b.name="ChunkLoadError",b.type=a,b.request=n,t[1](b)}c[e]=void 0}};var d=setTimeout(function(){n({type:"timeout",target:o})},12e4);o.onerror=o.onload=n,document.head.appendChild(o)}return Promise.all(r)},f.m=e,f.c=a,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,r){if(1&r&&(e=f(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var a in e)f.d(t,a,(function(r){return e[r]}).bind(null,a));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="",f.oe=function(e){throw console.error(e),e};var o=window.webpackJsonp=window.webpackJsonp||[],b=o.push.bind(o);o.push=r,o=o.slice();for(var d=0;d<o.length;d++)r(o[d]);var u=b;t()}([]);
|
@@ -124,7 +124,7 @@ class TestScaffoldE2e():
|
|
124
124
|
# Add user defined Docker Compose file for the local service
|
125
125
|
shutil.copyfile(local_service_mock_docker_compose_path, destination_path)
|
126
126
|
|
127
|
-
# Record workflow doesn't have a
|
127
|
+
# Record workflow doesn't have a public folder
|
128
128
|
|
129
129
|
# Generate certs
|
130
130
|
ScaffoldCliInvoker.cli_app_mkcert(runner, app_dir_path)
|
@@ -249,7 +249,7 @@ class TestScaffoldE2e():
|
|
249
249
|
'service_name': external_service_docker_compose.service_name
|
250
250
|
}
|
251
251
|
command = ServiceWorkflowValidateCommand(app, **config)
|
252
|
-
with open(f"{command.
|
252
|
+
with open(f"{command.public_dir_path}/shared_file.txt", 'w') as file:
|
253
253
|
file.write('this is a shared file')
|
254
254
|
|
255
255
|
ScaffoldCliInvoker.cli_workflow_up(runner, app_dir_path, target_workflow_name=target_workflow_name)
|
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.5.0
|
@@ -25,75 +25,144 @@ def settings():
|
|
25
25
|
return reset()
|
26
26
|
|
27
27
|
class TestEvalFixturesService():
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
'
|
75
|
-
'
|
76
|
-
|
77
|
-
|
28
|
+
class TestResponseFixtures():
|
29
|
+
@pytest.fixture(scope='class')
|
30
|
+
def request_method(self):
|
31
|
+
return 'POST'
|
32
|
+
|
33
|
+
@pytest.fixture(scope='class')
|
34
|
+
def request_url(self):
|
35
|
+
return 'https://petstore.swagger.io/404.html'
|
36
|
+
|
37
|
+
@pytest.fixture(scope='class')
|
38
|
+
def created_request(
|
39
|
+
self, settings: Settings, request_method: str, request_url: str
|
40
|
+
):
|
41
|
+
status = RequestBuilder(
|
42
|
+
method=request_method,
|
43
|
+
response_body='',
|
44
|
+
status_code=200,
|
45
|
+
url=request_url,
|
46
|
+
).with_settings(settings).build()[1]
|
47
|
+
assert status == 200
|
48
|
+
|
49
|
+
return Request.last()
|
50
|
+
|
51
|
+
@pytest.fixture(scope='class')
|
52
|
+
def not_found_file_contents(self):
|
53
|
+
return b'Not Found'
|
54
|
+
|
55
|
+
@pytest.fixture(scope='class')
|
56
|
+
def public_directory(self):
|
57
|
+
tmp_dir_path = DataDir.instance().tmp_dir_path
|
58
|
+
public_dir_path = os.path.join(tmp_dir_path, 'public')
|
59
|
+
if not os.path.exists(public_dir_path):
|
60
|
+
os.mkdir(public_dir_path)
|
61
|
+
return public_dir_path
|
62
|
+
|
63
|
+
@pytest.fixture(autouse=True, scope='class')
|
64
|
+
def not_found_file_path(self, public_directory: str, not_found_file_contents: str):
|
65
|
+
path = os.path.join(public_directory, '404.html')
|
66
|
+
with open(path, 'wb') as fp:
|
67
|
+
fp.write(not_found_file_contents)
|
68
|
+
return path
|
69
|
+
|
70
|
+
@pytest.fixture(scope='class')
|
71
|
+
def response_fixtures(self, request_method: str, not_found_file_path: str) -> Fixtures:
|
72
|
+
fixtures = {}
|
73
|
+
fixtures[request_method] = {
|
74
|
+
'/404.html': {
|
75
|
+
'headers': {
|
76
|
+
'test': '1',
|
77
|
+
},
|
78
|
+
'path': not_found_file_path,
|
79
|
+
'status_code': 404,
|
80
|
+
}
|
78
81
|
}
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
82
|
+
return fixtures
|
83
|
+
|
84
|
+
@pytest.fixture()
|
85
|
+
def mitmproxy_request(self, created_request: Request) -> MitmproxyRequest:
|
86
|
+
return MitmproxyRequestAdapter(created_request).adapt()
|
87
|
+
|
88
|
+
@pytest.fixture()
|
89
|
+
def fixtures_response(self, mitmproxy_request: MitmproxyRequest, response_fixtures: Fixtures):
|
90
|
+
res: requests.Response = eval_fixtures(mitmproxy_request, response_fixtures=response_fixtures)
|
91
|
+
assert res != None
|
92
|
+
return res
|
93
|
+
|
94
|
+
def test_it_sets_response(
|
95
|
+
self, fixtures_response: requests.Response, not_found_file_contents: str
|
96
|
+
):
|
97
|
+
assert fixtures_response.raw.read() == not_found_file_contents
|
98
|
+
|
99
|
+
def test_it_sets_headers(self, fixtures_response: requests.Response):
|
100
|
+
assert fixtures_response.headers['test'] == '1'
|
101
|
+
assert fixtures_response.headers['Content-Type'] == 'text/html'
|
102
|
+
|
103
|
+
def test_it_sets_status_code(self, fixtures_response: requests.Response):
|
104
|
+
assert fixtures_response.status_code == 404
|
105
|
+
|
106
|
+
class TestPublicDirectory():
|
107
|
+
@pytest.fixture(scope='class')
|
108
|
+
def request_method(self):
|
109
|
+
return 'GET'
|
110
|
+
|
111
|
+
@pytest.fixture(scope='class')
|
112
|
+
def request_url(self):
|
113
|
+
return 'https://petstore.swagger.io'
|
114
|
+
|
115
|
+
@pytest.fixture(scope='class')
|
116
|
+
def created_request(
|
117
|
+
self, settings: Settings, request_method: str, request_url: str
|
118
|
+
):
|
119
|
+
status = RequestBuilder(
|
120
|
+
method=request_method,
|
121
|
+
request_headers={'accept': 'text/html;q=0.1,application/json;q=0.9'},
|
122
|
+
response_body='',
|
123
|
+
status_code=200,
|
124
|
+
url=request_url,
|
125
|
+
).with_settings(settings).build()[1]
|
126
|
+
assert status == 200
|
127
|
+
|
128
|
+
return Request.last()
|
129
|
+
|
130
|
+
@pytest.fixture()
|
131
|
+
def mitmproxy_request(self, created_request: Request) -> MitmproxyRequest:
|
132
|
+
return MitmproxyRequestAdapter(created_request).adapt()
|
133
|
+
|
134
|
+
@pytest.fixture(scope='class')
|
135
|
+
def public_directory(self):
|
136
|
+
tmp_dir_path = DataDir.instance().tmp_dir_path
|
137
|
+
public_dir_path = os.path.join(tmp_dir_path, 'public')
|
138
|
+
if not os.path.exists(public_dir_path):
|
139
|
+
os.mkdir(public_dir_path)
|
140
|
+
return public_dir_path
|
141
|
+
|
142
|
+
@pytest.fixture(scope='class')
|
143
|
+
def index_file_contents(self):
|
144
|
+
return b'Hello World!'
|
145
|
+
|
146
|
+
@pytest.fixture(autouse=True, scope='class')
|
147
|
+
def index_file_path(self, public_directory: str, index_file_contents: str):
|
148
|
+
path = os.path.join(public_directory, 'index.html')
|
149
|
+
with open(path, 'wb') as fp:
|
150
|
+
fp.write(index_file_contents)
|
151
|
+
return path
|
152
|
+
|
153
|
+
@pytest.fixture()
|
154
|
+
def public_directory_response(self, mitmproxy_request: MitmproxyRequest, public_directory: str):
|
155
|
+
res: requests.Response = eval_fixtures(mitmproxy_request, public_directory_path=public_directory)
|
156
|
+
assert res != None
|
157
|
+
return res
|
158
|
+
|
159
|
+
def test_it_sets_contents(
|
160
|
+
self, public_directory_response: requests.Response, index_file_contents: str
|
161
|
+
):
|
162
|
+
assert public_directory_response.raw.read() == index_file_contents
|
163
|
+
|
164
|
+
def test_it_headers(self, public_directory_response: requests.Response):
|
165
|
+
assert public_directory_response.headers['Content-Type'] == 'text/html'
|
166
|
+
|
167
|
+
def test_it_sets_status_code(self, public_directory_response: requests.Response):
|
168
|
+
assert public_directory_response.status_code == 200
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import pdb
|
2
|
+
import pytest
|
3
|
+
|
4
|
+
from click.testing import CliRunner
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
from stoobly_agent.cli import mock, record, request
|
8
|
+
from stoobly_agent.lib.orm.request import Request
|
9
|
+
from stoobly_agent.test.test_helper import DETERMINISTIC_GET_REQUEST_URL, reset
|
10
|
+
|
11
|
+
@pytest.fixture(scope='module')
|
12
|
+
def runner():
|
13
|
+
return CliRunner()
|
14
|
+
|
15
|
+
class TestLifecycleHooks():
|
16
|
+
@pytest.fixture(scope='class', autouse=True)
|
17
|
+
def settings(self):
|
18
|
+
return reset()
|
19
|
+
|
20
|
+
@pytest.fixture(scope='class')
|
21
|
+
def lifecycle_hooks_path(self):
|
22
|
+
return str(Path(__file__).parent.parent / 'mock_data' / 'lifecycle_hooks.py')
|
23
|
+
|
24
|
+
@pytest.fixture(scope='class')
|
25
|
+
def record_result(self, runner: CliRunner, lifecycle_hooks_path: str):
|
26
|
+
record_result = runner.invoke(record, [
|
27
|
+
'--lifecycle-hooks-path', lifecycle_hooks_path, '--output', '/dev/null', DETERMINISTIC_GET_REQUEST_URL
|
28
|
+
])
|
29
|
+
assert record_result.exit_code == 0
|
30
|
+
return record_result
|
31
|
+
|
32
|
+
@pytest.fixture(scope='class')
|
33
|
+
def recorded_request(self):
|
34
|
+
return Request.last()
|
35
|
+
|
36
|
+
@pytest.fixture(scope='class')
|
37
|
+
def mock_result(self, runner: CliRunner, lifecycle_hooks_path: str):
|
38
|
+
mock_result = runner.invoke(mock, [
|
39
|
+
'--lifecycle-hooks-path', lifecycle_hooks_path, '--output', '/dev/null', DETERMINISTIC_GET_REQUEST_URL
|
40
|
+
])
|
41
|
+
assert mock_result.exit_code == 0
|
42
|
+
return mock_result
|
43
|
+
|
44
|
+
@pytest.fixture(scope='class')
|
45
|
+
def test_result(self, runner: CliRunner, lifecycle_hooks_path: str, recorded_request: Request):
|
46
|
+
test_result = runner.invoke(request, [
|
47
|
+
'test', '--format', 'json', '--lifecycle-hooks-path', lifecycle_hooks_path, recorded_request.key()
|
48
|
+
])
|
49
|
+
assert test_result.exit_code == 0
|
50
|
+
return test_result
|
51
|
+
|
52
|
+
def test_calls_record_hooks(self, record_result):
|
53
|
+
expected_stdout = ['before_request', 'before_record', 'after_record', 'before_response']
|
54
|
+
assert record_result.stdout.strip() == "\n".join(expected_stdout)
|
55
|
+
|
56
|
+
def test_calls_mock_hooks(self, mock_result):
|
57
|
+
expected_stdout = ['before_request', 'before_mock', 'after_mock', 'before_response']
|
58
|
+
assert mock_result.stdout.strip() == "\n".join(expected_stdout)
|
59
|
+
|
60
|
+
def test_calls_test_hooks(self, test_result):
|
61
|
+
expected_stdout = [
|
62
|
+
'before_request', 'before_replay', 'after_replay', 'before_mock', 'after_mock', 'before_test', 'after_test', 'before_response'
|
63
|
+
]
|
64
|
+
stdout = test_result.stdout
|
65
|
+
lifecycle_hooks_output = stdout.split('{')[0]
|
66
|
+
assert lifecycle_hooks_output.strip() == "\n".join(expected_stdout)
|
@@ -10,8 +10,9 @@ from stoobly_agent.config.constants import mode
|
|
10
10
|
from stoobly_agent.app.settings.constants import request_component
|
11
11
|
from stoobly_agent.cli import config, mock, record, scenario
|
12
12
|
from stoobly_agent.lib.api.keys.scenario_key import ScenarioKey
|
13
|
+
from stoobly_agent.lib.orm.request import Request
|
13
14
|
|
14
|
-
@pytest.fixture()
|
15
|
+
@pytest.fixture(scope='module')
|
15
16
|
def runner():
|
16
17
|
return CliRunner()
|
17
18
|
|
@@ -23,46 +24,60 @@ class TestMocking():
|
|
23
24
|
return reset()
|
24
25
|
|
25
26
|
class TestWhenHeaders():
|
27
|
+
header_name = 'foo'
|
28
|
+
header_value = 'bar'
|
29
|
+
url = DETERMINISTIC_GET_REQUEST_URL
|
26
30
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
url = DETERMINISTIC_GET_REQUEST_URL
|
32
|
-
record_result = runner.invoke(record, [url, '-H', f"{header_name}: {header_value}"])
|
31
|
+
@pytest.fixture(autouse=True, scope='class')
|
32
|
+
def recorded_request(self, runner: CliRunner):
|
33
|
+
record_result = runner.invoke(record, [self.url, '-H', f"{self.header_name}: {self.header_value}"])
|
33
34
|
assert record_result.exit_code == 0
|
35
|
+
return Request.last()
|
34
36
|
|
37
|
+
def test_it_ignores_headers_default(self, runner: CliRunner):
|
38
|
+
# Expect to fail since we have not set up rewrite rule for mock mode
|
39
|
+
mock_result = runner.invoke(mock, [self.url])
|
40
|
+
assert mock_result.exit_code == 0
|
41
|
+
|
42
|
+
def test_it_fails_when_enabled(self, runner: CliRunner):
|
35
43
|
# Enforce matching by headers only
|
36
44
|
match_result = runner.invoke(config, [
|
37
45
|
'match', 'set',
|
38
46
|
'--method', 'GET', '--mode', mode.MOCK, '--pattern', '.*?', '--component', request_component.HEADER
|
39
47
|
])
|
40
48
|
assert match_result.exit_code == 0
|
41
|
-
|
49
|
+
|
42
50
|
# Expect to fail since we have not set up rewrite rule for mock mode
|
43
|
-
mock_result = runner.invoke(mock, [url])
|
51
|
+
mock_result = runner.invoke(mock, [self.url])
|
44
52
|
assert mock_result.exit_code == 1
|
45
53
|
|
54
|
+
def test_it_succeeds_after_rewrite(self, runner: CliRunner):
|
46
55
|
rewrite_result = runner.invoke(config, [
|
47
56
|
'rewrite', 'set',
|
48
|
-
'--method', 'GET', '--mode', mode.MOCK, '--name', header_name, '--value', header_value, '--pattern', '.*?', '--type', request_component.HEADER
|
57
|
+
'--method', 'GET', '--mode', mode.MOCK, '--name', self.header_name, '--value', self.header_value, '--pattern', '.*?', '--type', request_component.HEADER
|
49
58
|
]
|
50
59
|
)
|
51
60
|
assert rewrite_result.exit_code == 0
|
52
61
|
|
53
|
-
mock_result = runner.invoke(mock, [url])
|
62
|
+
mock_result = runner.invoke(mock, [self.url])
|
54
63
|
assert mock_result.exit_code == 0
|
55
64
|
|
56
65
|
class TestWhenQueryParams():
|
66
|
+
query_param_name = 'foo'
|
67
|
+
query_param_value = 'bar'
|
68
|
+
url = DETERMINISTIC_GET_REQUEST_URL
|
57
69
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
url = DETERMINISTIC_GET_REQUEST_URL
|
63
|
-
record_result = runner.invoke(record, [f"{url}?{query_param_name}={query_param_value}"])
|
70
|
+
@pytest.fixture(autouse=True, scope='class')
|
71
|
+
def recorded_request(self, runner: CliRunner):
|
72
|
+
record_result = runner.invoke(record, [f"{self.url}?{self.query_param_name}={self.query_param_value}"])
|
64
73
|
assert record_result.exit_code == 0
|
74
|
+
return Request.last()
|
75
|
+
|
76
|
+
def test_it_ignores_query_params_by_default(self, runner: CliRunner):
|
77
|
+
mock_result = runner.invoke(mock, [self.url])
|
78
|
+
assert mock_result.exit_code == 0
|
65
79
|
|
80
|
+
def test_it_fails_when_enabled(self, runner: CliRunner):
|
66
81
|
# Enforce matching by query params only
|
67
82
|
match_result = runner.invoke(config, [
|
68
83
|
'match', 'set',
|
@@ -71,34 +86,42 @@ class TestMocking():
|
|
71
86
|
assert match_result.exit_code == 0
|
72
87
|
|
73
88
|
# Expect to fail since we have not set up rewrite rule for mock mode
|
74
|
-
mock_result = runner.invoke(mock, [url])
|
89
|
+
mock_result = runner.invoke(mock, [self.url])
|
75
90
|
assert mock_result.exit_code == 1
|
76
91
|
|
92
|
+
def test_it_succeeds_after_rewrites(self, runner: CliRunner):
|
77
93
|
rewrite_result = runner.invoke(config, [
|
78
94
|
'rewrite', 'set',
|
79
|
-
'--method', 'GET', '--mode', mode.MOCK, '--name', query_param_name, '--value', query_param_value, '--pattern', '.*?', '--type', request_component.QUERY_PARAM
|
95
|
+
'--method', 'GET', '--mode', mode.MOCK, '--name', self.query_param_name, '--value', self.query_param_value, '--pattern', '.*?', '--type', request_component.QUERY_PARAM
|
80
96
|
]
|
81
97
|
)
|
82
98
|
assert rewrite_result.exit_code == 0
|
83
99
|
|
84
|
-
mock_result = runner.invoke(mock, [url])
|
100
|
+
mock_result = runner.invoke(mock, [self.url])
|
85
101
|
assert mock_result.exit_code == 0
|
86
102
|
|
87
103
|
class TestWhenBodyParams():
|
104
|
+
body_param_name = 'foo'
|
105
|
+
body_param_value = 'bar'
|
106
|
+
url = DETERMINISTIC_GET_REQUEST_URL
|
88
107
|
|
89
|
-
|
90
|
-
|
91
|
-
body_param_value = 'bar'
|
92
|
-
|
108
|
+
@pytest.fixture(autouse=True, scope='class')
|
109
|
+
def recorded_request(self, runner: CliRunner):
|
93
110
|
content = {}
|
94
|
-
content[body_param_name] = body_param_value
|
111
|
+
content[self.body_param_name] = self.body_param_value
|
95
112
|
body = json.dumps(content)
|
96
113
|
|
97
114
|
url = DETERMINISTIC_GET_REQUEST_URL
|
98
115
|
record_result = runner.invoke(record, [url, '-d', body, '-H', 'Content-Type: application/json'])
|
99
116
|
assert record_result.exit_code == 0
|
117
|
+
return Request.last()
|
100
118
|
|
101
|
-
|
119
|
+
def test_it_ignores_body_params_by_default(self, runner: CliRunner):
|
120
|
+
mock_result = runner.invoke(mock, [self.url])
|
121
|
+
assert mock_result.exit_code == 0
|
122
|
+
|
123
|
+
def test_it_fails_when_enabled(self, runner: CliRunner):
|
124
|
+
# Enforce matching by bodyy params only
|
102
125
|
match_result = runner.invoke(config, [
|
103
126
|
'match', 'set',
|
104
127
|
'--method', 'GET', '--mode', mode.MOCK, '--pattern', '.*?', '--component', request_component.BODY_PARAM
|
@@ -106,17 +129,18 @@ class TestMocking():
|
|
106
129
|
assert match_result.exit_code == 0
|
107
130
|
|
108
131
|
# Expect to fail since we have not set up rewrite rule for mock mode
|
109
|
-
mock_result = runner.invoke(mock, [url])
|
132
|
+
mock_result = runner.invoke(mock, [self.url])
|
110
133
|
assert mock_result.exit_code == 1
|
111
134
|
|
135
|
+
def test_it_succeeds_after_rewrites(self, runner: CliRunner):
|
112
136
|
rewrite_result = runner.invoke(config, [
|
113
137
|
'rewrite', 'set',
|
114
|
-
'--method', 'GET', '--mode', mode.MOCK, '--name', body_param_name, '--value', body_param_value, '--pattern', '.*?', '--type', request_component.BODY_PARAM
|
138
|
+
'--method', 'GET', '--mode', mode.MOCK, '--name', self.body_param_name, '--value', self.body_param_value, '--pattern', '.*?', '--type', request_component.BODY_PARAM
|
115
139
|
]
|
116
140
|
)
|
117
141
|
assert rewrite_result.exit_code == 0
|
118
142
|
|
119
|
-
mock_result = runner.invoke(mock, [url])
|
143
|
+
mock_result = runner.invoke(mock, [self.url])
|
120
144
|
assert mock_result.exit_code == 0
|
121
145
|
|
122
146
|
class TestScenario():
|
@@ -9,11 +9,13 @@ from urllib.parse import parse_qs
|
|
9
9
|
from stoobly_agent.test.test_helper import DETERMINISTIC_GET_REQUEST_URL, NON_DETERMINISTIC_GET_REQUEST_URL, reset
|
10
10
|
|
11
11
|
from stoobly_agent.app.models.adapters.raw_http_request_adapter import RawHttpRequestAdapter
|
12
|
+
from stoobly_agent.app.models.adapters.raw_http_response_adapter import RawHttpResponseAdapter
|
12
13
|
from stoobly_agent.app.settings.constants import firewall_action, request_component
|
13
14
|
from stoobly_agent.config.constants import custom_headers, mode, record_policy, request_origin
|
14
15
|
from stoobly_agent.cli import config, intercept, mock, record, scenario
|
15
16
|
from stoobly_agent.lib.api.keys.scenario_key import ScenarioKey
|
16
17
|
from stoobly_agent.lib.orm.request import Request
|
18
|
+
from stoobly_agent.lib.orm.response import Response
|
17
19
|
from stoobly_agent.lib.orm.scenario import Scenario
|
18
20
|
from stoobly_agent.lib.utils.decode import decode
|
19
21
|
|
@@ -130,6 +132,71 @@ class TestRecording():
|
|
130
132
|
body = decode(python_request.data)
|
131
133
|
assert json.loads(body).get(body_param) == body_param_value
|
132
134
|
|
135
|
+
class TestWhenResponseHeaders():
|
136
|
+
|
137
|
+
def test_it_rewrites(self, runner: CliRunner):
|
138
|
+
header_name = 'foo'
|
139
|
+
header_value = 'bar'
|
140
|
+
|
141
|
+
rewrite_result = runner.invoke(config, [
|
142
|
+
'rewrite', 'set',
|
143
|
+
'--method', 'GET', '--mode', mode.RECORD, '--name', header_name, '--value', header_value, '--pattern', '.*?', '--type', request_component.RESPONSE_HEADER
|
144
|
+
]
|
145
|
+
)
|
146
|
+
assert rewrite_result.exit_code == 0
|
147
|
+
|
148
|
+
record_result = runner.invoke(record, [DETERMINISTIC_GET_REQUEST_URL])
|
149
|
+
assert record_result.exit_code == 0
|
150
|
+
|
151
|
+
_response = Response.last()
|
152
|
+
python_response = RawHttpResponseAdapter(_response.raw).to_response()
|
153
|
+
|
154
|
+
assert python_response.headers.get(header_name.title()) == header_value
|
155
|
+
|
156
|
+
class TestWhenResponseParams():
|
157
|
+
|
158
|
+
def test_it_rewrites(self, runner: CliRunner):
|
159
|
+
body_param = 'foo'
|
160
|
+
body_param_value = 'bar'
|
161
|
+
|
162
|
+
rewrite_result = runner.invoke(config, [
|
163
|
+
'rewrite', 'set',
|
164
|
+
'--method', 'GET', '--mode', mode.RECORD, '--name', body_param, '--value', body_param_value, '--pattern', '.*?', '--type', request_component.RESPONSE_PARAM
|
165
|
+
]
|
166
|
+
)
|
167
|
+
assert rewrite_result.exit_code == 0
|
168
|
+
|
169
|
+
record_result = runner.invoke(record, [DETERMINISTIC_GET_REQUEST_URL])
|
170
|
+
assert record_result.exit_code == 0
|
171
|
+
|
172
|
+
_response = Response.last()
|
173
|
+
python_response = RawHttpResponseAdapter(_response.raw).to_response()
|
174
|
+
|
175
|
+
body = decode(python_response.content)
|
176
|
+
assert json.loads(body).get(body_param) == body_param_value
|
177
|
+
|
178
|
+
def test_it_does_not_rewrite(self, runner: CliRunner):
|
179
|
+
body_param = 'foo'
|
180
|
+
body_param_value = 'bar'
|
181
|
+
|
182
|
+
rewrite_result = runner.invoke(config, [
|
183
|
+
'rewrite', 'set',
|
184
|
+
'--method', 'GET', '--mode', mode.RECORD, '--name', body_param, '--value', body_param_value, '--pattern', '.*?', '--type', request_component.RESPONSE_PARAM
|
185
|
+
]
|
186
|
+
)
|
187
|
+
assert rewrite_result.exit_code == 0
|
188
|
+
|
189
|
+
record_result = runner.invoke(record, [NON_DETERMINISTIC_GET_REQUEST_URL])
|
190
|
+
assert record_result.exit_code == 0
|
191
|
+
|
192
|
+
_response = Response.last()
|
193
|
+
python_response = RawHttpResponseAdapter(_response.raw).to_response()
|
194
|
+
|
195
|
+
body = decode(python_response.content)
|
196
|
+
# For non iterable resposne content, do not rewrite
|
197
|
+
with pytest.raises(json.decoder.JSONDecodeError):
|
198
|
+
json.loads(body)
|
199
|
+
|
133
200
|
class TestFirewall():
|
134
201
|
@pytest.fixture(scope='function', autouse=True)
|
135
202
|
def settings(self):
|
@@ -0,0 +1,35 @@
|
|
1
|
+
from stoobly_agent.app.proxy.context import InterceptContext
|
2
|
+
from stoobly_agent.app.proxy.mock.context import MockContext
|
3
|
+
from stoobly_agent.app.proxy.record.context import RecordContext
|
4
|
+
from stoobly_agent.app.proxy.replay.context import ReplayContext
|
5
|
+
from stoobly_agent.app.proxy.test.context import TestContext
|
6
|
+
|
7
|
+
def handle_before_request(context: InterceptContext):
|
8
|
+
print('before_request')
|
9
|
+
|
10
|
+
def handle_before_record(context: RecordContext):
|
11
|
+
print('before_record')
|
12
|
+
|
13
|
+
def handle_before_mock(context: MockContext):
|
14
|
+
print('before_mock')
|
15
|
+
|
16
|
+
def handle_before_replay(context: ReplayContext):
|
17
|
+
print('before_replay')
|
18
|
+
|
19
|
+
def handle_before_test(context: ReplayContext):
|
20
|
+
print('before_test')
|
21
|
+
|
22
|
+
def handle_after_record(context: RecordContext):
|
23
|
+
print('after_record')
|
24
|
+
|
25
|
+
def handle_after_mock(context: MockContext):
|
26
|
+
print('after_mock')
|
27
|
+
|
28
|
+
def handle_after_replay(context: ReplayContext):
|
29
|
+
print('after_replay')
|
30
|
+
|
31
|
+
def handle_after_test(context: TestContext):
|
32
|
+
print('after_test')
|
33
|
+
|
34
|
+
def handle_before_response(context: InterceptContext):
|
35
|
+
print('before_response')
|