summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMickaël Schoentgen <contact@tiger-222.fr>2021-10-08 10:45:49 +0200
committerMickaël Schoentgen <contact@tiger-222.fr>2021-10-08 10:45:49 +0200
commit279e387d86e98cd4a9acc56d4f30a50bfd07bb59 (patch)
tree550911b25db12497da618a233b68b8329ac500fe
parent50f57f8c82bb80b9f9dab56f503165536ecceebe (diff)
-rw-r--r--httpie/cli/argparser.py2
-rw-r--r--httpie/cli/definition.py12
-rw-r--r--httpie/core.py4
m---------httpie/prompt0
-rw-r--r--setup.py9
-rw-r--r--tests/prompt/__init__.py0
-rw-r--r--tests/prompt/base.py59
-rw-r--r--tests/prompt/context/test_context.py161
-rw-r--r--tests/prompt/context/test_transform.py162
-rw-r--r--tests/prompt/test_cli.py319
-rw-r--r--tests/prompt/test_completer.py130
-rw-r--r--tests/prompt/test_config.py70
-rw-r--r--tests/prompt/test_contextio.py24
-rw-r--r--tests/prompt/test_execution.py1631
-rw-r--r--tests/prompt/test_installation.py32
-rw-r--r--tests/prompt/test_interaction.py79
-rw-r--r--tests/prompt/test_lexer.py793
-rw-r--r--tests/prompt/test_tree.py131
-rw-r--r--tests/prompt/test_utils.py92
-rw-r--r--tests/prompt/test_xdg.py59
-rw-r--r--tests/prompt/utils.py22
21 files changed, 3789 insertions, 2 deletions
diff --git a/httpie/cli/argparser.py b/httpie/cli/argparser.py
index 0b689410..4b9d0c86 100644
--- a/httpie/cli/argparser.py
+++ b/httpie/cli/argparser.py
@@ -75,6 +75,8 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
) -> argparse.Namespace:
self.env = env
self.args, no_options = super().parse_known_args(args, namespace)
+ if self.args.prompt:
+ return self.args
if self.args.debug:
self.args.traceback = True
self.has_stdin_data = (
diff --git a/httpie/cli/definition.py b/httpie/cli/definition.py
index f6a8c0e6..ab5f9731 100644
--- a/httpie/cli/definition.py
+++ b/httpie/cli/definition.py
@@ -2,7 +2,7 @@
CLI arguments definition.
"""
-from argparse import (FileType, OPTIONAL, SUPPRESS, ZERO_OR_MORE)
+from argparse import FileType, OPTIONAL, SUPPRESS, ZERO_OR_MORE
from textwrap import dedent, wrap
from .. import __doc__, __version__
@@ -73,6 +73,7 @@ positional.add_argument(
positional.add_argument(
dest='url',
metavar='URL',
+ nargs=OPTIONAL,
help='''
The scheme defaults to 'http://' if the URL does not include one.
(You can override this with: --default-scheme=https)
@@ -840,3 +841,12 @@ troubleshooting.add_argument(
'''
)
+troubleshooting.add_argument(
+ '--prompt',
+ action='store_true',
+ default=False,
+ help='''
+ Start the shell!
+
+ '''
+)
diff --git a/httpie/core.py b/httpie/core.py
index c3567219..c153cec7 100644
--- a/httpie/core.py
+++ b/httpie/core.py
@@ -29,6 +29,10 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
Return exit status code.
"""
+ if '--prompt' in args:
+ from .prompt.cli import cli
+ return cli(sys.argv[2:])
+
program_name, *args = args
env.program_name = os.path.basename(program_name)
args = decode_raw_args(args, env.stdin_encoding)
diff --git a/httpie/prompt b/httpie/prompt
new file mode 160000
+Subproject 8922a77156a7dc96bac9e3e94fe900bb17f976c
diff --git a/setup.py b/setup.py
index 45826d2d..9bf6f68b 100644
--- a/setup.py
+++ b/setup.py
@@ -9,6 +9,7 @@ import httpie
# Note: keep requirements here to ease distributions packaging
tests_require = [
+ 'pexpect',
'pytest',
'pytest-httpbin>=0.0.6',
'responses',
@@ -20,12 +21,12 @@ dev_require = [
'flake8-deprecated',
'flake8-mutable',
'flake8-tuple',
+ 'jinja2',
'pyopenssl',
'pytest-cov',
'pyyaml',
'twine',
'wheel',
- 'Jinja2'
]
install_requires = [
'charset_normalizer>=2.0.0',
@@ -34,6 +35,11 @@ install_requires = [
'Pygments>=2.5.2',
'requests-toolbelt>=0.9.1',
'setuptools',
+ # Prompt
+ 'click>=5.0',
+ 'parsimonious>=0.6.2',
+ 'prompt-toolkit>=2.0.0,<3.0.0',
+ 'pyyaml>=3.0',
]
install_requires_win_only = [
'colorama>=0.2.4',
@@ -79,6 +85,7 @@ setup(
'console_scripts': [
'http = httpie.__main__:main',
'https = httpie.__main__:main',
+ 'http-prompt=httpie.prompt.cli:cli',
],
},
python_requires='>=3.6',
diff --git a/tests/prompt/__init__.py b/tests/prompt/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/prompt/__init__.py
diff --git a/tests/prompt/base.py b/tests/prompt/base.py
new file mode 100644
index 00000000..f4918141
--- /dev/null
+++ b/tests/prompt/base.py
@@ -0,0 +1,59 @@
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+
+
+class TempAppDirTestCase(unittest.TestCase):
+ """Set up temporary app data and config directories before every test
+ method, and delete them afterwards.
+ """
+
+ def setUp(self):
+ # Create a temp dir that will contain data and config directories
+ self.temp_dir = tempfile.mkdtemp()
+
+ if sys.platform == 'win32':
+ self.homes = {
+ # subdir_name: envvar_name
+ 'data': 'LOCALAPPDATA',
+ 'config': 'LOCALAPPDATA'
+ }
+ else:
+ self.homes = {
+ # subdir_name: envvar_name
+ 'data': 'XDG_DATA_HOME',
+ 'config': 'XDG_CONFIG_HOME'
+ }
+
+ # Used to restore
+ self.orig_envvars = {}
+
+ for subdir_name, envvar_name in self.homes.items():
+ if envvar_name in os.environ:
+ self.orig_envvars[envvar_name] = os.environ[envvar_name]
+ os.environ[envvar_name] = os.path.join(self.temp_dir, subdir_name)
+
+ def tearDown(self):
+ # Restore envvar values
+ for name in self.homes.values():
+ if name in self.orig_envvars:
+ os.environ[name] = self.orig_envvars[name]
+ else:
+ del os.environ[name]
+
+ shutil.rmtree(self.temp_dir)
+
+ def make_tempfile(self, data='', subdir_name=''):
+ """Create a file under self.temp_dir and return the path."""
+ full_tempdir = os.path.join(self.temp_dir, subdir_name)
+ if not os.path.exists(full_tempdir):
+ os.makedirs(full_tempdir)
+
+ if isinstance(data, str):
+ data = data.encode()
+
+ with tempfile.NamedTemporaryFile(dir=full_tempdir, delete=False) as f:
+ f.write(data)
+ return f.name
diff --git a/tests/prompt/context/test_context.py b/tests/prompt/context/test_context.py
new file mode 100644
index 00000000..fdbe1a65
--- /dev/null
+++ b/tests/prompt/context/test_context.py
@@ -0,0 +1,161 @@
+from httpie.prompt.context import Context
+
+
+def test_creation():
+ context = Context('http://example.com')
+ assert context.url == 'http://example.com'
+ assert context.options == {}
+ assert context.headers == {}
+ assert context.querystring_params == {}
+ assert context.body_params == {}
+ assert not context.should_exit
+
+
+def test_creation_with_longer_url():
+ context = Context('http://example.com/a/b/c/index.html')
+ assert context.url == 'http://example.com/a/b/c/index.html'
+ assert context.options == {}
+ assert context.headers == {}
+ assert context.querystring_params == {}
+ assert context.body_params == {}
+ assert not context.should_exit
+
+
+def test_eq():
+ c1 = Context('http://localhost')
+ c2 = Context('http://localhost')
+ assert c1 == c2
+
+ c1.options['--verify'] = 'no'
+ assert c1 != c2
+
+
+def test_copy():
+ c1 = Context('http://localhost')
+ c2 = c1.copy()
+ assert c1 == c2
+ assert c1 is not c2
+
+
+def test_update():
+ c1 = Context('http://localhost')
+ c1.headers['Accept'] = 'application/json'
+ c1.querystring_params['flag'] = '1'
+ c1.body_params.update({
+ 'name': 'John Doe',
+ 'email': 'john@example.com'
+ })
+
+ c2 = Context('http://example.com')
+ c2.headers['Content-Type'] = 'text/html'
+ c2.body_params['name'] = 'John Smith'
+
+ c1.update(c2)
+
+ assert c1.url == 'http://example.com'
+ assert c1.headers == {
+ 'Accept': 'application/json',
+ 'Content-Type': 'text/html'
+ }
+ assert c1.querystring_params == {'flag': '1'}
+ assert c1.body_params == {
+ 'name': 'John Smith',
+ 'email': 'john@example.com'
+ }
+
+
+def test_spec():
+ c = Context('http://localhost', spec={
+ 'paths': {
+ '/users': {
+ 'get': {
+ 'parameters': [
+ {'name': 'username', 'in': 'path'},
+ {'name': 'since', 'in': 'query'},
+ {'name': 'Accept'}
+ ]
+ }
+ },
+ '/orgs/{org}': {
+ 'get': {
+ 'parameters': [
+ {'name': 'org', 'in': 'path'},
+ {'name': 'featured', 'in': 'query'},
+ {'name': 'X-Foo', 'in': 'header'}
+ ]
+ }
+ }
+ }
+ })
+ assert c.url == 'http://localhost'
+
+ root_children = list(sorted(c.root.children))
+ assert len(root_children) == 2
+ assert root_children[0].name == 'orgs'
+ assert root_children[1].name == 'users'
+
+ orgs_children = list(sorted(root_children[0].children))
+ assert len(orgs_children) == 1
+
+ org_children = list(sorted(list(orgs_children)[0].children))
+ assert len(org_children) == 2
+ assert org_children[0].name == 'X-Foo'
+ assert org_children[1].name == 'featured'
+
+ users_children = list(sorted(root_children[1].children))
+ assert len(users_children) == 2
+ assert users_children[0].name == 'Accept'
+ assert users_children[1].name == 'since'
+
+
+def test_override():
+ """Parameters can be defined at path level
+ """
+ c = Context('http://localhost', spec={
+ 'paths': {
+ '/users': {
+ 'parameters': [
+ {'name': 'username', 'in': 'query'},
+ {'name': 'Accept', 'in': 'header'}
+ ],
+ 'get': {
+ 'parameters': [
+ {'name': 'custom1', 'in': 'query'}
+ ]
+ },
+ 'post': {
+ 'parameters': [
+ {'name': 'custom2', 'in': 'query'},
+ ]
+ },
+ },
+ '/orgs': {
+ 'parameters': [
+ {'name': 'username', 'in': 'query'},
+ {'name': 'Accept', 'in': 'header'}
+ ],
+ 'get': {}
+ }
+ }
+ })
+ assert c.url == 'http://localhost'
+
+ root_children = list(sorted(c.root.children))
+ # one path
+ assert len(root_children) == 2
+ assert root_children[0].name == 'orgs'
+ assert root_children[1].name == 'users'
+
+ orgs_methods = list(sorted(list(root_children)[0].children))
+ # path parameters are used even if no method parameter
+ assert len(orgs_methods) == 2
+ assert next(filter(lambda i: i.name == 'username', orgs_methods), None) is not None
+ assert next(filter(lambda i: i.name == 'Accept', orgs_methods), None) is not None
+
+ users_methods = list(sorted(list(root_children)[1].children))
+ # path and methods parameters are merged
+ assert len(users_methods) == 4
+ assert next(filter(lambda i: i.name == 'username', users_methods), None) is not None
+ assert next(filter(lambda i: i.name == 'custom1', users_methods), None) is not None
+ assert next(filter(lambda i: i.name == 'custom2', users_methods), None) is not None
+ assert next(filter(lambda i: i.name == 'Accept', users_methods), None) is not None
diff --git a/tests/prompt/context/test_transform.py b/tests/prompt/context/test_transform.py
new file mode 100644
index 00000000..c7d32981
--- /dev/null
+++ b/tests/prompt/context/test_transform.py
@@ -0,0 +1,162 @@
+from httpie.prompt.context import Context
+from httpie.prompt.context import transform as t
+
+
+def test_extract_args_for_httpie_main_get():
+ c = Context('http://localhost/things')
+ c.headers.update({
+ 'Authorization': 'ApiKey 1234',
+ 'Accept': 'text/html'
+ })
+ c.querystring_params.update({
+ 'page': '2',
+ 'limit': '10'
+ })
+
+ args = t.extract_args_for_httpie_main(c, method='get')
+ assert args == ['GET', 'http://localhost/things', 'limit==10', 'page==2',
+ 'Accept:text/html', 'Authorization:ApiKey 1234']
+
+
+def test_extract_args_for_httpie_main_post():
+ c = Context('http://localhost/things')
+ c.headers.update({
+ 'Authorization': 'ApiKey 1234',
+ 'Accept': 'text/html'
+ })
+ c.options.update({
+ '--verify': 'no',
+ '--form': None
+ })
+ c.body_params.update({
+ 'full name': 'Jane Doe',
+ 'email': 'jane@example.com'
+ })
+
+ args = t.extract_args_for_httpie_main(c, method='post')
+ assert args == ['--form', '--verify', 'no',
+ 'POST', 'http://localhost/things',
+ 'email=jane@example.com', 'full name=Jane Doe',
+ 'Accept:text/html', 'Authorization:ApiKey 1234']
+
+
+def test_extract_raw_json_args_for_httpie_main_post():
+ c = Context('http://localhost/things')
+ c.body_json_params.update({
+ 'enabled': True,
+ 'items': ['foo', 'bar'],
+ 'object': {
+ 'id': 10,
+ 'name': 'test'
+ }
+ })
+
+ args = t.extract_args_for_httpie_main(c, method='post')
+ assert args == ['POST', 'http://localhost/things',
+ 'enabled:=true', 'items:=["foo", "bar"]',
+ 'object:={"id": 10, "name": "test"}']
+
+
+def test_format_to_httpie_get():
+ c = Context('http://localhost/things')
+ c.headers.update({
+ 'Authorization': 'ApiKey 1234',
+ 'Accept': 'text/html'
+ })
+ c.querystring_params.update({
+ 'page': '2',
+ 'limit': '10',
+ 'name': ['alice', 'bob bob']
+ })
+
+ output = t.format_to_httpie(c, method='get')
+ assert output == ("http GET http://localhost/things "
+ "limit==10 name==alice 'name==bob bob' page==2 "
+ "Accept:text/html 'Authorization:ApiKey 1234'\n")
+
+
+def test_format_to_httpie_post():
+ c = Context('http://localhost/things')
+ c.headers.update({
+ 'Authorization': 'ApiKey 1234',
+ 'Accept': 'text/html'
+ })
+ c.options.update({
+ '--verify': 'no',
+ '--form': None
+ })
+ c.body_params.update({
+ 'full name': 'Jane Doe',
+ 'email': 'jane@example.com'
+ })
+
+ output = t.format_to_httpie(c, method='post')
+ assert output == ("http --form --verify=no POST http://localhost/things "
+ "email=jane@example.com 'full name=Jane Doe' "
+ "Accept:text/html 'Authorization:ApiKey 1234'\n")
+
+
+def test_format_to_http_prompt_1():
+ c = Context('http://localhost/things')
+ c.headers.update({
+ 'Authorization': 'ApiKey 1234',
+ 'Accept': 'text/html'
+ })
+ c.querystring_params.update({
+ 'page': '2',
+ 'limit': '10'
+ })
+
+ output = t.format_to_http_prompt(c)
+ assert output == ("cd http://localhost/things\n"
+ "limit==10\n"
+ "page==2\n"
+ "Accept:text/html\n"
+ "'Authorization:ApiKey 1234'\n")
+
+
+def test_format_to_http_prompt_2():
+ c = Context('http://localhost/things')
+ c.headers.update({
+ 'Authorization': 'ApiKey 1234',
+ 'Accept': 'text/html'
+ })
+ c.options.update({
+ '--verify': 'no',
+ '--form': None
+ })
+ c.body_params.update({
+ 'full name': 'Jane Doe',
+ 'email': 'jane@example.com'
+ })
+
+ output = t.format_to_http_prompt(c)
+ assert output == ("--form\n"
+ "--verify=no\n"
+ "cd http://localhost/things\n"
+ "email=jane@example.com\n"
+ "'full name=Jane Doe'\n"
+ "Accept:text/html\n"
+ "'Authorization:ApiKey 1234'\n")
+
+
+def test_format_raw_json_string_to_http_prompt():
+ c = Context('http://localhost/things')
+ c.body_json_params.update({
+ 'bar': 'baz',
+ })
+
+ output = t.format_to_http_prompt(c)
+ assert output == ("cd http://localhost/things\n"
+ "bar:='\"baz\"'\n")
+
+
+def test_extract_httpie_options():
+ c = Context('http://localhost')
+ c.options.update({
+ '--verify': 'no',
+ '--form': None
+ })
+
+ output = t._extract_httpie_options(c, excluded_keys=['--form'])
+ assert output == ['--verify', 'no']
diff --git a/tests/prompt/test_cli.py b/tests/prompt/test_cli.py
new file mode 100644
index 00000000..af662ed9
--- /dev/null
+++ b/tests/prompt/test_cli.py
@@ -0,0 +1,319 @@
+import json
+import os
+import sys
+import unittest
+from unittest.mock import patch, DEFAULT
+
+from click.testing import CliRunner
+from requests.models import Response
+
+from .base import TempAppDirTestCase
+from httpie.prompt import xdg
+from httpie.prompt.context import Context
+from httpie.prompt.cli import cli, execute, ExecutionListener
+
+
+def run_and_exit(cli_args=None, prompt_commands=None):
+ """Run http-prompt executable, execute some prompt commands, and exit."""
+ if cli_args is None:
+ cli_args = []
+
+ # Make sure last command is 'exit'
+ if prompt_commands is None:
+ prompt_commands = ['exit']
+ else:
+ prompt_commands += ['exit']
+
+ # Fool cli() so that it believes we're running from CLI instead of pytest.
+ # We will restore it at the end of the function.
+ orig_argv = sys.argv
+ sys.argv = ['http-prompt'] + cli_args
+
+ try:
+ with patch.multiple('httpie.prompt.cli',
+ prompt=DEFAULT, execute=DEFAULT) as mocks:
+ mocks['execute'].side_effect = execute
+
+ # prompt() is mocked to return the command in 'prompt_commands' in
+ # sequence, i.e., prompt() returns prompt_commands[i-1] when it is
+ # called for the ith time
+ mocks['prompt'].side_effect = prompt_commands
+
+ result = CliRunner().invoke(cli, cli_args)
+ context = mocks['execute'].call_args[0][1]
+
+ return result, context
+ finally:
+ sys.argv = orig_argv
+
+
+class TestCli(TempAppDirTestCase):
+
+ def test_without_args(self):
+ result, context = run_and_exit(['http://localhost'])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://localhost')
+ self.assertEqual(context.options, {})
+ self.assertEqual(context.body_params, {})
+ self.assertEqual(context.headers, {})
+ self.assertEqual(context.querystring_params, {})
+
+ def test_incomplete_url1(self):
+ result, context = run_and_exit(['://example.com'])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://example.com')
+ self.assertEqual(context.options, {})
+ self.assertEqual(context.body_params, {})
+ self.assertEqual(context.headers, {})
+ self.assertEqual(context.querystring_params, {})
+
+ def test_incomplete_url2(self):
+ result, context = run_and_exit(['//example.com'])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://example.com')
+ self.assertEqual(context.options, {})
+ self.assertEqual(context.body_params, {})
+ self.assertEqual(context.headers, {})
+ self.assertEqual(context.querystring_params, {})
+
+ def test_incomplete_url3(self):
+ result, context = run_and_exit(['example.com'])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://example.com')
+ self.assertEqual(context.options, {})
+ self.assertEqual(context.body_params, {})
+ self.assertEqual(context.headers, {})
+ self.assertEqual(context.querystring_params, {})
+
+ def test_httpie_oprions(self):
+ url = 'http://example.com'
+ custom_args = '--auth value: name=foo'
+ result, context = run_and_exit([url] + custom_args.split())
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://example.com')
+ self.assertEqual(context.options, {'--auth': 'value:'})
+ self.assertEqual(context.body_params, {'name': 'foo'})
+ self.assertEqual(context.headers, {})
+ self.assertEqual(context.querystring_params, {})
+
+ def test_persistent_context(self):
+ result, context = run_and_exit(['//example.com', 'name=bob', 'id==10'])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://example.com')
+ self.assertEqual(context.options, {})
+ self.assertEqual(context.body_params, {'name': 'bob'})
+ self.assertEqual(context.headers, {})
+ self.assertEqual(context.querystring_params, {'id': ['10']})
+
+ result, context = run_and_exit()
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://example.com')
+ self.assertEqual(context.options, {})
+ self.assertEqual(context.body_params, {'name': 'bob'})
+ self.assertEqual(context.headers, {})
+ self.assertEqual(context.querystring_params, {'id': ['10']})
+
+ def test_cli_args_bypasses_persistent_context(self):
+ result, context = run_and_exit(['//example.com', 'name=bob', 'id==10'])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://example.com')
+ self.assertEqual(context.options, {})
+ self.assertEqual(context.body_params, {'name': 'bob'})
+ self.assertEqual(context.headers, {})
+ self.assertEqual(context.querystring_params, {'id': ['10']})
+
+ result, context = run_and_exit(['//example.com', 'sex=M'])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://example.com')
+ self.assertEqual(context.options, {})
+ self.assertEqual(context.body_params, {'sex': 'M'})
+ self.assertEqual(context.headers, {})
+
+ def test_config_file(self):
+ # Config file is not there at the beginning
+ config_path = os.path.join(xdg.get_config_dir(), 'config.py')
+ self.assertFalse(os.path.exists(config_path))
+
+ # After user runs it for the first time, a default config file should
+ # be created
+ result, context = run_and_exit(['//example.com'])
+ self.assertEqual(result.exit_code, 0)
+ self.assertTrue(os.path.exists(config_path))
+
+ def test_cli_arguments_with_spaces(self):
+ result, context = run_and_exit(['example.com', "name=John Doe",
+ "Authorization:Bearer API KEY"])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://example.com')
+ self.assertEqual(context.options, {})
+ self.assertEqual(context.querystring_params, {})
+ self.assertEqual(context.body_params, {'name': 'John Doe'})
+ self.assertEqual(context.headers, {'Authorization': 'Bearer API KEY'})
+
+ def test_spec_from_local(self):
+ spec_filepath = self.make_tempfile(json.dumps({
+ 'paths': {
+ '/users': {},
+ '/orgs': {}
+ }
+ }))
+ result, context = run_and_exit(['example.com', "--spec",
+ spec_filepath])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://example.com')
+ self.assertEqual(set([n.name for n in context.root.children]),
+ set(['users', 'orgs']))
+
+ def test_spec_basePath(self):
+ spec_filepath = self.make_tempfile(json.dumps({
+ 'basePath': '/api/v1',
+ 'paths': {
+ '/users': {},
+ '/orgs': {}
+ }
+ }))
+ result, context = run_and_exit(['example.com', "--spec",
+ spec_filepath])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://example.com')
+
+ lv1_names = set([node.name for node in context.root.ls()])
+ lv2_names = set([node.name for node in context.root.ls('api')])
+ lv3_names = set([node.name for node in context.root.ls('api', 'v1')])
+
+ self.assertEqual(lv1_names, set(['api']))
+ self.assertEqual(lv2_names, set(['v1']))
+ self.assertEqual(lv3_names, set(['users', 'orgs']))
+
+ def test_spec_from_http(self):
+ spec_url = 'https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json'
+ result, context = run_and_exit(['https://api.github.com', '--spec',
+ spec_url])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'https://api.github.com')
+
+ top_level_paths = set([n.name for n in context.root.children])
+ self.assertIn('repos', top_level_paths)
+ self.assertIn('users', top_level_paths)
+
+ def test_spec_from_http_only(self):
+ spec_url = (
+ 'https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.json')
+ result, context = run_and_exit(['--spec', spec_url])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'https://api.medium.com/v1')
+
+ lv1_names = set([node.name for node in context.root.ls()])
+ lv2_names = set([node.name for node in context.root.ls('v1')])
+
+ self.assertEqual(lv1_names, set(['v1']))
+ self.assertEqual(lv2_names, set(['me', 'publications', 'users']))
+
+ def test_spec_with_trailing_slash(self):
+ spec_filepath = self.make_tempfile(json.dumps({
+ 'basePath': '/api',
+ 'paths': {
+ '/': {},
+ '/users/': {}
+ }
+ }))
+ result, context = run_and_exit(['example.com', "--spec",
+ spec_filepath])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://example.com')
+ lv1_names = set([node.name for node in context.root.ls()])
+ lv2_names = set([node.name for node in context.root.ls('api')])
+ self.assertEqual(lv1_names, set(['api']))
+ self.assertEqual(lv2_names, set(['/', 'users/']))
+
+ def test_env_only(self):
+ env_filepath = self.make_tempfile(
+ "cd http://example.com\nname=bob\nid==10")
+ result, context = run_and_exit(["--env", env_filepath])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://example.com')
+ self.assertEqual(context.options, {})
+ self.assertEqual(context.body_params, {'name': 'bob'})
+ self.assertEqual(context.headers, {})
+ self.assertEqual(context.querystring_params, {'id': ['10']})
+
+ def test_env_with_url(self):
+ env_filepath = self.make_tempfile(
+ "cd http://example.com\nname=bob\nid==10")
+ result, context = run_and_exit(["--env", env_filepath,
+ 'other_example.com'])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://other_example.com')
+ self.assertEqual(context.options, {})
+ self.assertEqual(context.body_params, {'name': 'bob'})
+ self.assertEqual(context.headers, {})
+ self.assertEqual(context.querystring_params, {'id': ['10']})
+
+ def test_env_with_options(self):
+ env_filepath = self.make_tempfile(
+ "cd http://example.com\nname=bob\nid==10")
+ result, context = run_and_exit(["--env", env_filepath,
+ 'other_example.com', 'name=alice'])
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(context.url, 'http://other_example.com')
+ self.assertEqual(context.options, {})
+ self.assertEqual(context.body_params, {'name': 'alice'})
+ self.assertEqual(context.headers, {})
+ self.assertEqual(context.querystring_params, {'id': ['10']})
+
+ @patch('httpie.prompt.cli.prompt')
+ @patch('httpie.prompt.cli.execute')
+ def test_press_ctrl_d(self, execute_mock, prompt_mock):
+ prompt_mock.side_effect = EOFError
+ execute_mock.side_effect = execute
+ result = CliRunner().invoke(cli, [])
+ self.assertEqual(result.exit_code, 0)
+
+
+class TestExecutionListenerSetCookies(unittest.TestCase):
+
+ def setUp(self):
+ self.listener = ExecutionListener({})
+
+ self.response = Response()
+ self.response.cookies.update({
+ 'username': 'john',
+ 'sessionid': 'abcd'
+ })
+
+ self.context = Context('http://localhost')
+ self.context.headers['Cookie'] = 'name="John Doe"; sessionid=xyz'
+
+ def test_auto(self):
+ self.listener.cfg['set_cookies'] = 'auto'
+ self.listener.response_returned(self.context, self.response)
+
+ self.assertEqual(self.context.headers['Cookie'],
+ 'name="John Doe"; sessionid=abcd; username=john')
+
+ @patch('httpie.prompt.cli.click.confirm')
+ def test_ask_and_yes(self, confirm_mock):
+ confirm_mock.return_value = True
+
+ self.listener.cfg['set_cookies'] = 'ask'
+ self.listener.response_returned(self.context, self.response)
+
+ self.assertEqual(self.context.headers['Cookie'],
+ 'name="John Doe"; sessionid=abcd; username=john')
+
+ @patch('httpie.prompt.cli.click.confirm')
+ def test_ask_and_no(self, confirm_mock):
+ confirm_mock.return_value = False
+
+ self.listener.cfg['set_cookies'] = 'ask'
+ self.listener.response_returned(self.context, self.response)
+
+ self.assertEqual(self.context.headers['Cookie'],
+ 'name="John Doe"; sessionid=xyz')
+
+ def test_off(self):
+ self.listener.cfg['set_cookies'] = 'off'
+ self.listener.