diff options
-rw-r--r-- | httpie/cli/argparser.py | 2 | ||||
-rw-r--r-- | httpie/cli/definition.py | 12 | ||||
-rw-r--r-- | httpie/core.py | 4 | ||||
m--------- | httpie/prompt | 0 | ||||
-rw-r--r-- | setup.py | 9 | ||||
-rw-r--r-- | tests/prompt/__init__.py | 0 | ||||
-rw-r--r-- | tests/prompt/base.py | 59 | ||||
-rw-r--r-- | tests/prompt/context/test_context.py | 161 | ||||
-rw-r--r-- | tests/prompt/context/test_transform.py | 162 | ||||
-rw-r--r-- | tests/prompt/test_cli.py | 319 | ||||
-rw-r--r-- | tests/prompt/test_completer.py | 130 | ||||
-rw-r--r-- | tests/prompt/test_config.py | 70 | ||||
-rw-r--r-- | tests/prompt/test_contextio.py | 24 | ||||
-rw-r--r-- | tests/prompt/test_execution.py | 1631 | ||||
-rw-r--r-- | tests/prompt/test_installation.py | 32 | ||||
-rw-r--r-- | tests/prompt/test_interaction.py | 79 | ||||
-rw-r--r-- | tests/prompt/test_lexer.py | 793 | ||||
-rw-r--r-- | tests/prompt/test_tree.py | 131 | ||||
-rw-r--r-- | tests/prompt/test_utils.py | 92 | ||||
-rw-r--r-- | tests/prompt/test_xdg.py | 59 | ||||
-rw-r--r-- | tests/prompt/utils.py | 22 |
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 @@ -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.response_returned(self.context, self.response) + + self.assertEqual(self.context.headers['Cookie'], + 'name="John Doe"; sessionid=xyz') diff --git a/tests/prompt/test_completer.py b/tests/prompt/test_completer.py new file mode 100644 index 00000000..0d49f63e --- /dev/null +++ b/tests/prompt/test_completer.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +import unittest + +from prompt_toolkit.document import Document + +from httpie.prompt.completer import HttpPromptCompleter +from httpie.prompt.context import Context + + +class TestCompleter(unittest.TestCase): + + def setUp(self): + self.context = Context('http://localhost', spec={ + 'paths': { + '/users': {}, + '/users/{username}': {}, + '/users/{username}/events': {}, + '/users/{username}/orgs': {}, + '/orgs': {}, + '/orgs/{org}': {}, + '/orgs/{org}/events': {}, + '/orgs/{org}/members': {} + } + }) + self.completer = HttpPromptCompleter(self.context) + self.completer_event = None + + def get_completions(self, command): + if not isinstance(command, str): + command = command.decode() + position = len(command) + completions = self.completer.get_completions( + Document(text=command, cursor_position=position), + self.completer_event) + return [c.text for c in completions] + + def test_header_name(self): + result = self.get_completions('ctype') + self.assertEqual(result[0], 'Content-Type') + + def test_header_value(self): + result = self.get_completions('Content-Type:json') |