From 279e387d86e98cd4a9acc56d4f30a50bfd07bb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 8 Oct 2021 10:45:49 +0200 Subject: [PATCH] WIP --- httpie/cli/argparser.py | 2 + httpie/cli/definition.py | 12 +- httpie/core.py | 4 + httpie/prompt | 1 + setup.py | 9 +- tests/prompt/__init__.py | 0 tests/prompt/base.py | 59 + tests/prompt/context/test_context.py | 161 +++ tests/prompt/context/test_transform.py | 162 +++ tests/prompt/test_cli.py | 319 +++++ tests/prompt/test_completer.py | 130 ++ tests/prompt/test_config.py | 70 + tests/prompt/test_contextio.py | 24 + tests/prompt/test_execution.py | 1631 ++++++++++++++++++++++++ tests/prompt/test_installation.py | 32 + tests/prompt/test_interaction.py | 79 ++ tests/prompt/test_lexer.py | 793 ++++++++++++ tests/prompt/test_tree.py | 131 ++ tests/prompt/test_utils.py | 92 ++ tests/prompt/test_xdg.py | 59 + tests/prompt/utils.py | 22 + 21 files changed, 3790 insertions(+), 2 deletions(-) create mode 160000 httpie/prompt create mode 100644 tests/prompt/__init__.py create mode 100644 tests/prompt/base.py create mode 100644 tests/prompt/context/test_context.py create mode 100644 tests/prompt/context/test_transform.py create mode 100644 tests/prompt/test_cli.py create mode 100644 tests/prompt/test_completer.py create mode 100644 tests/prompt/test_config.py create mode 100644 tests/prompt/test_contextio.py create mode 100644 tests/prompt/test_execution.py create mode 100644 tests/prompt/test_installation.py create mode 100644 tests/prompt/test_interaction.py create mode 100644 tests/prompt/test_lexer.py create mode 100644 tests/prompt/test_tree.py create mode 100644 tests/prompt/test_utils.py create mode 100644 tests/prompt/test_xdg.py create mode 100644 tests/prompt/utils.py 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 index 00000000..8922a771 --- /dev/null +++ b/httpie/prompt @@ -0,0 +1 @@ +Subproject commit 8922a77156a7dc96bac9e3e94fe900bb17f976c2 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 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') + self.assertEqual(result[0], 'application/json') + + def test_verify_option(self): + result = self.get_completions('--vfy') + self.assertEqual(result[0], '--verify') + + def test_preview_then_action(self): + result = self.get_completions('httpie po') + self.assertEqual(result[0], 'post') + + def test_rm_body_param(self): + self.context.body_params['my_name'] = 'dont_care' + result = self.get_completions('rm -b ') + self.assertEqual(result[0], 'my_name') + + def test_rm_body_json_param(self): + self.context.body_json_params['number'] = 2 + result = self.get_completions('rm -b ') + self.assertEqual(result[0], 'number') + + def test_rm_querystring_param(self): + self.context.querystring_params['my_name'] = 'dont_care' + result = self.get_completions('rm -q ') + self.assertEqual(result[0], 'my_name') + + def test_rm_header(self): + self.context.headers['Accept'] = 'dont_care' + result = self.get_completions('rm -h ') + self.assertEqual(result[0], 'Accept') + + def test_rm_option(self): + self.context.options['--form'] = None + result = self.get_completions('rm -o ') + self.assertEqual(result[0], '--form') + + def test_querystring_with_chinese(self): + result = self.get_completions('name==王') + self.assertFalse(result) + + def test_header_with_spanish(self): + result = self.get_completions('X-Custom-Header:Jesú') + self.assertFalse(result) + + def test_options_method(self): + result = self.get_completions('opt') + self.assertEqual(result[0], 'options') + + def test_ls_no_path(self): + result = self.get_completions('ls ') + self.assertEqual(result, ['orgs', 'users']) + + def test_ls_no_path_substring(self): + result = self.get_completions('ls o') + self.assertEqual(result, ['orgs']) + + def test_ls_absolute_path(self): + result = self.get_completions('ls /users/1/') + self.assertEqual(result, ['events', 'orgs']) + + def test_ls_absolute_path_substring(self): + result = self.get_completions('ls /users/1/e') + self.assertEqual(result, ['events']) + + def test_ls_relative_path(self): + self.context.url = 'http://localhost/orgs' + result = self.get_completions('ls 1/') + self.assertEqual(result, ['events', 'members']) + + def test_cd_no_path(self): + result = self.get_completions('cd ') + self.assertEqual(result, ['orgs', 'users']) + + def test_cd_no_path_substring(self): + result = self.get_completions('cd o') + self.assertEqual(result, ['orgs']) + + def test_cd_absolute_path(self): + result = self.get_completions('cd /users/1/') + self.assertEqual(result, ['events', 'orgs']) + + def test_cd_absolute_path_substring(self): + result = self.get_completions('cd /users/1/e') + self.assertEqual(result, ['events']) + + def test_cd_relative_path(self): + self.context.url = 'http://localhost/orgs' + result = self.get_completions('cd 1/') + self.assertEqual(result, ['events', 'members']) diff --git a/tests/prompt/test_config.py b/tests/prompt/test_config.py new file mode 100644 index 00000000..120223bd --- /dev/null +++ b/tests/prompt/test_config.py @@ -0,0 +1,70 @@ +import hashlib +import os + +from .base import TempAppDirTestCase +from httpie.prompt import config + + +def _hash_file(path): + with open(path, 'rb') as f: + data = f.read() + return hashlib.sha1(data).hexdigest() + + +class TestConfig(TempAppDirTestCase): + + def test_initialize(self): + # Config file doesn't exist at first + expected_path = config.get_user_config_path() + self.assertFalse(os.path.exists(expected_path)) + + # Config file should exist after initialization + copied, actual_path = config.initialize() + self.assertTrue(copied) + self.assertEqual(actual_path, expected_path) + self.assertTrue(os.path.exists(expected_path)) + + # Change config file and hash the content to see if it's changed + with open(expected_path, 'a') as f: + f.write('dont_care\n') + orig_hash = _hash_file(expected_path) + + # Make sure it's fine to call config.initialize() twice + copied, actual_path = config.initialize() + self.assertFalse(copied) + self.assertEqual(actual_path, expected_path) + self.assertTrue(os.path.exists(expected_path)) + + # Make sure config file is unchanged + new_hash = _hash_file(expected_path) + self.assertEqual(new_hash, orig_hash) + + def test_load_default(self): + cfg = config.load_default() + self.assertEqual(cfg['command_style'], 'solarized') + self.assertFalse(cfg['output_style']) + self.assertEqual(cfg['pager'], 'less') + + def test_load_user(self): + copied, path = config.initialize() + self.assertTrue(copied) + + with open(path, 'w') as f: + f.write("\ngreeting = 'hello!'\n") + + cfg = config.load_user() + self.assertEqual(cfg, {'greeting': 'hello!'}) + + def test_load(self): + copied, path = config.initialize() + self.assertTrue(copied) + + with open(path, 'w') as f: + f.write("pager = 'more'\n" + "greeting = 'hello!'\n") + + cfg = config.load() + self.assertEqual(cfg['command_style'], 'solarized') + self.assertFalse(cfg['output_style']) + self.assertEqual(cfg['pager'], 'more') + self.assertEqual(cfg['greeting'], 'hello!') diff --git a/tests/prompt/test_contextio.py b/tests/prompt/test_contextio.py new file mode 100644 index 00000000..43ad0950 --- /dev/null +++ b/tests/prompt/test_contextio.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from .base import TempAppDirTestCase +from httpie.prompt.context import Context +from httpie.prompt.contextio import save_context, load_context + + +class TestContextIO(TempAppDirTestCase): + + def test_save_and_load_context_non_ascii(self): + c = Context('http://localhost') + c.headers.update({ + 'User-Agent': 'Ö', + 'Authorization': '中文' + }) + save_context(c) + + c = Context('http://0.0.0.0') + load_context(c) + + self.assertEqual(c.url, 'http://localhost') + self.assertEqual(c.headers, { + 'User-Agent': 'Ö', + 'Authorization': '中文' + }) diff --git a/tests/prompt/test_execution.py b/tests/prompt/test_execution.py new file mode 100644 index 00000000..e6af1e40 --- /dev/null +++ b/tests/prompt/test_execution.py @@ -0,0 +1,1631 @@ +# -*- coding: utf-8 -*- +import hashlib +import io +import json +import shutil +import os +import sys + +import pytest + +from collections import namedtuple + +from unittest.mock import patch + +from httpie.prompt.context import Context +from httpie.prompt.executionimport execute, HTTPIE_PROGRAM_NAME + +from .base import TempAppDirTestCase + + +class ExecutionTestCase(TempAppDirTestCase): + + def setUp(self): + super(ExecutionTestCase, self).setUp() + self.patchers = [ + ('httpie_main', patch('http_prompt.execution.httpie_main')), + ('echo_via_pager', + patch('http_prompt.output.click.echo_via_pager')), + ('secho', patch('http_prompt.execution.click.secho')), + ('get_terminal_size', patch('http_prompt.utils.get_terminal_size')) + ] + for attr_name, patcher in self.patchers: + setattr(self, attr_name, patcher.start()) + + 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': {} + } + }) + + # pytest mocks to capture stdout so we can't really get_terminal_size() + Size = namedtuple('Size', ['columns', 'rows']) + self.get_terminal_size.return_value = Size(80, 30) + + def tearDown(self): + super(ExecutionTestCase, self).tearDown() + for _, patcher in self.patchers: + patcher.stop() + + def assert_httpie_main_called_with(self, args): + self.assertEqual(self.httpie_main.call_args[0][0], [ + HTTPIE_PROGRAM_NAME, *args]) + + def assert_stdout(self, expected_msg): + # Append '\n' to simulate behavior of click.echo_via_pager(), + # which we use whenever we want to output anything to stdout + printed_msg = self.echo_via_pager.call_args[0][0] + '\n' + self.assertEqual(printed_msg, expected_msg) + + def assert_stdout_startswith(self, expected_prefix): + printed_msg = self.echo_via_pager.call_args[0][0] + self.assertTrue(printed_msg.startswith(expected_prefix)) + + def get_stdout(self): + return self.echo_via_pager.call_args[0][0] + + def assert_stderr(self, expected_msg): + printed_msg = self.secho.call_args[0][0] + print_options = self.secho.call_args[1] + self.assertEqual(printed_msg, expected_msg) + self.assertEqual(print_options, {'err': True, 'fg': 'red'}) + + +class TestExecution_noop(ExecutionTestCase): + + def test_empty_string(self): + execute('', self.context) + self.assertEqual(self.context.url, 'http://localhost') + self.assertFalse(self.context.options) + self.assertFalse(self.context.headers) + self.assertFalse(self.context.querystring_params) + self.assertFalse(self.context.body_params) + self.assertFalse(self.context.should_exit) + + def test_spaces(self): + execute(' \t \t ', self.context) + self.assertEqual(self.context.url, 'http://localhost') + self.assertFalse(self.context.options) + self.assertFalse(self.context.headers) + self.assertFalse(self.context.querystring_params) + self.assertFalse(self.context.body_params) + self.assertFalse(self.context.should_exit) + + +class TestExecution_env(ExecutionTestCase): + + def setUp(self): + super(TestExecution_env, self).setUp() + + self.context.url = 'http://localhost:8000/api' + self.context.headers.update({ + 'Accept': 'text/csv', + 'Authorization': 'ApiKey 1234' + }) + self.context.querystring_params.update({ + 'page': ['1'], + 'limit': ['50'] + }) + self.context.body_params.update({ + 'name': 'John Doe' + }) + self.context.options.update({ + '--verify': 'no', + '--form': None + }) + + def test_env(self): + execute('env', self.context) + self.assert_stdout("--form\n--verify=no\n" + "cd http://localhost:8000/api\n" + "limit==50\npage==1\n" + "'name=John Doe'\n" + "Accept:text/csv\n" + "'Authorization:ApiKey 1234'\n") + + def test_env_with_spaces(self): + execute(' env ', self.context) + self.assert_stdout("--form\n--verify=no\n" + "cd http://localhost:8000/api\n" + "limit==50\npage==1\n" + "'name=John Doe'\n" + "Accept:text/csv\n" + "'Authorization:ApiKey 1234'\n") + + def test_env_non_ascii(self): + self.context.body_params['name'] = '許 功蓋' + execute('env', self.context) + self.assert_stdout("--form\n--verify=no\n" + "cd http://localhost:8000/api\n" + "limit==50\npage==1\n" + "'name=許 功蓋'\n" + "Accept:text/csv\n" + "'Authorization:ApiKey 1234'\n") + + def test_env_write_to_file(self): + filename = self.make_tempfile() + + # write something first to make sure it's a full overwrite + with open(filename, 'w') as f: + f.write('hello world\n') + + execute('env > %s' % filename, self.context) + + with open(filename) as f: + content = f.read() + + self.assertEqual(content, + "--form\n--verify=no\n" + "cd http://localhost:8000/api\n" + "limit==50\npage==1\n" + "'name=John Doe'\n" + "Accept:text/csv\n" + "'Authorization:ApiKey 1234'\n") + + def test_env_write_to_file_with_env_vars(self): + filename = self.make_tempfile('hello world\n', 'testenvvar') + filename_with_var = filename.replace("testenvvar", "${MYPRIVATEVAR}") + + os.environ['MYPRIVATEVAR'] = 'testenvvar' + execute('env > %s' % filename_with_var, self.context) + os.environ['MYPRIVATEVAR'] = '' + + with open(filename) as f: + content = f.read() + + self.assertEqual(content, + "--form\n--verify=no\n" + "cd http://localhost:8000/api\n" + "limit==50\npage==1\n" + "'name=John Doe'\n" + "Accept:text/csv\n" + "'Authorization:ApiKey 1234'\n") + + def test_env_non_ascii_and_write_to_file(self): + filename = self.make_tempfile() + + # write something first to make sure it's a full overwrite + with open(filename, 'w') as f: + f.write('hello world\n') + + self.context.body_params['name'] = '許 功蓋' + execute('env > %s' % filename, self.context) + + with open(filename, encoding='utf-8') as f: + content = f.read() + + self.assertEqual(content, + "--form\n--verify=no\n" + "cd http://localhost:8000/api\n" + "limit==50\npage==1\n" + "'name=許 功蓋'\n" + "Accept:text/csv\n" + "'Authorization:ApiKey 1234'\n") + + def test_env_write_to_quoted_filename(self): + filename = self.make_tempfile() + + # Write something first to make sure it's a full overwrite + with open(filename, 'w') as f: + f.write('hello world\n') + + execute("env > '%s'" % filename, self.context) + + with open(filename) as f: + content = f.read() + + self.assertEqual(content, + "--form\n--verify=no\n" + "cd http://localhost:8000/api\n" + "limit==50\npage==1\n" + "'name=John Doe'\n" + "Accept:text/csv\n" + "'Authorization:ApiKey 1234'\n") + + def test_env_append_to_file(self): + filename = self.make_tempfile() + + # Write something first to make sure it's an append + with open(filename, 'w') as f: + f.write('hello world\n') + + execute('env >> %s' % filename, self.context) + + with open(filename) as f: + content = f.read() + + self.assertEqual(content, + "hello world\n" + "--form\n--verify=no\n" + "cd http://localhost:8000/api\n" + "limit==50\npage==1\n" + "'name=John Doe'\n" + "Accept:text/csv\n" + "'Authorization:ApiKey 1234'\n") + + +class TestExecution_source_and_exec(ExecutionTestCase): + + def setUp(self): + super(TestExecution_source_and_exec, self).setUp() + + self.context.url = 'http://localhost:8000/api' + self.context.headers.update({ + 'Accept': 'text/csv', + 'Authorization': 'ApiKey 1234' + }) + self.context.querystring_params.update({ + 'page': ['1'], + 'limit': ['50'] + }) + self.context.body_params.update({ + 'name': 'John Doe' + }) + self.context.options.update({ + '--verify': 'no', + '--form': None + }) + + # The file that is about to be sourced/exec'd + self.filename = self.make_tempfile( + "Language:en Authorization:'ApiKey 5678'\n" + "name='Jane Doe' username=jane limit==25\n" + "rm -o --form\n" + "cd v2/user\n") + + def test_source(self): + execute('source %s' % self.filename, self.context) + + self.assertEqual(self.context.url, + 'http://localhost:8000/api/v2/user') + self.assertEqual(self.context.headers, { + 'Accept': 'text/csv', + 'Authorization': 'ApiKey 5678', + 'Language': 'en' + }) + self.assertEqual(self.context.querystring_params, { + 'page': ['1'], + 'limit': ['25'] + }) + self.assertEqual(self.context.body_params, { + 'name': 'Jane Doe', + 'username': 'jane' + }) + self.assertEqual(self.context.options, { + '--verify': 'no' + }) + + def test_source_with_spaces(self): + execute(' source %s ' % self.filename, self.context) + + self.assertEqual(self.context.url, + 'http://localhost:8000/api/v2/user') + self.assertEqual(self.context.headers, { + 'Accept': 'text/csv', + 'Authorization': 'ApiKey 5678', + 'Language': 'en' + }) + self.assertEqual(self.context.querystring_params, { + 'page': ['1'], + 'limit': ['25'] + }) + self.assertEqual(self.context.body_params, { + 'name': 'Jane Doe', + 'username': 'jane' + }) + self.assertEqual(self.context.options, { + '--verify': 'no' + }) + + def test_source_non_existing_file(self): + c = self.context.copy() + execute('source no_such_file.txt', self.context) + self.assertEqual(self.context, c) + + # Expect the error message would be the same as when we open the + # non-existing file + try: + with open('no_such_file.txt'): + pass + except OSError as err: + err_msg = str(err) + else: + assert False, 'what?! no_such_file.txt exists!' + + self.assert_stderr(err_msg) + + def test_source_quoted_filename(self): + execute('source "%s"' % self.filename, self.context) + + self.assertEqual(self.context.url, + 'http://localhost:8000/api/v2/user') + self.assertEqual(self.context.headers, { + 'Accept': 'text/csv', + 'Authorization': 'ApiKey 5678', + 'Language': 'en' + }) + self.assertEqual(self.context.querystring_params, { + 'page': ['1'], + 'limit': ['25'] + }) + self.assertEqual(self.context.body_params, { + 'name': 'Jane Doe', + 'username': 'jane' + }) + self.assertEqual(self.context.options, { + '--verify': 'no' + }) + + @pytest.mark.skipif(sys.platform == 'win32', + reason="Windows doesn't use backslashes to escape") + def test_source_escaped_filename(self): + new_filename = self.filename + r' copy' + shutil.copyfile(self.filename, new_filename) + + new_filename = new_filename.replace(' ', r'\ ') + + execute('source %s' % new_filename, self.context) + + self.assertEqual(self.context.url, + 'http://localhost:8000/api/v2/user') + self.assertEqual(self.context.headers, { + 'Accept': 'text/csv', + 'Authorization': 'ApiKey 5678', + 'Language': 'en' + }) + self.assertEqual(self.context.querystring_params, { + 'page': ['1'], + 'limit': ['25'] + }) + self.assertEqual(self.context.body_params, { + 'name': 'Jane Doe', + 'username': 'jane' + }) + self.assertEqual(self.context.options, { + '--verify': 'no' + }) + + def test_exec(self): + execute('exec %s' % self.filename, self.context) + + self.assertEqual(self.context.url, + 'http://localhost:8000/api/v2/user') + self.assertEqual(self.context.headers, { + 'Authorization': 'ApiKey 5678', + 'Language': 'en' + }) + self.assertEqual(self.context.querystring_params, { + 'limit': ['25'] + }) + self.assertEqual(self.context.body_params, { + 'name': 'Jane Doe', + 'username': 'jane' + }) + + def test_exec_with_spaces(self): + execute(' exec %s ' % self.filename, self.context) + + self.assertEqual(self.context.url, + 'http://localhost:8000/api/v2/user') + self.assertEqual(self.context.headers, { + 'Authorization': 'ApiKey 5678', + 'Language': 'en' + }) + self.assertEqual(self.context.querystring_params, { + 'limit': ['25'] + }) + self.assertEqual(self.context.body_params, { + 'name': 'Jane Doe', + 'username': 'jane' + }) + + def test_exec_non_existing_file(self): + c = self.context.copy() + execute('exec no_such_file.txt', self.context) + self.assertEqual(self.context, c) + + # Try to get the error message when opening a non-existing file + try: + with open('no_such_file.txt'): + pass + except OSError as err: + err_msg = str(err) + else: + assert False, 'what?! no_such_file.txt exists!' + + self.assert_stderr(err_msg) + + def test_exec_quoted_filename(self): + execute("exec '%s'" % self.filename, self.context) + + self.assertEqual(self.context.url, + 'http://localhost:8000/api/v2/user') + self.assertEqual(self.context.headers, { + 'Authorization': 'ApiKey 5678', + 'Language': 'en' + }) + self.assertEqual(self.context.querystring_params, { + 'limit': ['25'] + }) + self.assertEqual(self.context.body_params, { + 'name': 'Jane Doe', + 'username': 'jane' + }) + + @pytest.mark.skipif(sys.platform == 'win32', + reason="Windows doesn't use backslashes to escape") + def test_exec_escaped_filename(self): + new_filename = self.filename + r' copy' + shutil.copyfile(self.filename, new_filename) + + new_filename = new_filename.replace(' ', r'\ ') + + execute('exec %s' % new_filename, self.context) + self.assertEqual(self.context.url, + 'http://localhost:8000/api/v2/user') + self.assertEqual(self.context.headers, { + 'Authorization': 'ApiKey 5678', + 'Language': 'en' + }) + self.assertEqual(self.context.querystring_params, { + 'limit': ['25'] + }) + self.assertEqual(self.context.body_params, { + 'name': 'Jane Doe', + 'username': 'jane' + }) + + +class TestExecution_env_and_source(ExecutionTestCase): + + def test_env_and_source(self): + c = Context() + c.url = 'http://localhost:8000/api' + c.headers.update({ + 'Accept': 'text/csv', + 'Authorization': 'ApiKey 1234' + }) + c.querystring_params.update({ + 'page': ['1'], + 'limit': ['50'] + }) + c.body_params.update({ + 'name': 'John Doe' + }) + c.options.update({ + '--verify': 'no', + '--form': None + }) + + c2 = c.copy() + + filename = self.make_tempfile() + execute('env > %s' % filename, c) + execute('rm *', c) + + self.assertFalse(c.headers) + self.assertFalse(c.querystring_params) + self.assertFalse(c.body_params) + self.assertFalse(c.options) + + execute('source %s' % filename, c) + + self.assertEqual(c, c2) + + def test_env_and_source_non_ascii(self): + c = Context() + c.url = 'http://localhost:8000/api' + c.headers.update({ + 'Accept': 'text/csv', + 'Authorization': 'ApiKey 1234' + }) + c.querystring_params.update({ + 'page': ['1'], + 'limit': ['50'] + }) + c.body_params.update({ + 'name': '許 功蓋' + }) + c.options.update({ + '--verify': 'no', + '--form': None + }) + + c2 = c.copy() + + filename = self.make_tempfile() + execute('env > %s' % filename, c) + execute('rm *', c) + + self.assertFalse(c.headers) + self.assertFalse(c.querystring_params) + self.assertFalse(c.body_params) + self.assertFalse(c.options) + + execute('source %s' % filename, c) + + self.assertEqual(c, c2) + + +class TestExecution_help(ExecutionTestCase): + + def test_help(self): + execute('help', self.context) + self.assert_stdout_startswith('Commands:\n\tcd') + + def test_help_with_spaces(self): + execute(' help ', self.context) + self.assert_stdout_startswith('Commands:\n\tcd') + + +class TestExecution_exit(ExecutionTestCase): + + def test_exit(self): + execute('exit', self.context) + self.assertTrue(self.context.should_exit) + + def test_exit_with_spaces(self): + execute(' exit ', self.context) + self.assertTrue(self.context.should_exit) + + +class TestExecution_cd(ExecutionTestCase): + + def test_single_level(self): + execute('cd api', self.context) + self.assertEqual(self.context.url, 'http://localhost/api') + + def test_many_levels(self): + execute('cd api/v2/movie/50', self.context) + self.assertEqual(self.context.url, 'http://localhost/api/v2/movie/50') + + def test_change_base(self): + execute('cd //example.com/api', self.context) + self.assertEqual(self.context.url, 'http://example.com/api') + + def test_root(self): + execute('cd /api/v2', self.context) + self.assertEqual(self.context.url, 'http://localhost/api/v2') + + execute('cd /index.html', self.context) + self.assertEqual(self.context.url, 'http://localhost/index.html') + + def test_dot_dot(self): + execute('cd api/v1', self.context) + self.assertEqual(self.context.url, 'http://localhost/api/v1') + + execute('cd ..', self.context) + self.assertEqual(self.context.url, 'http://localhost/api') + + # If dot-dot has a trailing slash, the resulting URL should have a + # trailing slash + execute('cd ../rest/api/', self.context) + self.assertEqual(self.context.url, 'http://localhost/rest/api/') + + def test_url_with_trailing_slash(self): + self.context.url = 'http://localhost/' + execute('cd api', self.context) + self.assertEqual(self.context.url, 'http://localhost/api') + + execute('cd v2/', self.context) + self.assertEqual(self.context.url, 'http://localhost/api/v2/') + + execute('cd /objects/', self.context) + self.assertEqual(self.context.url, 'http://localhost/objects/') + + def test_path_with_trailing_slash(self): + execute('cd api/', self.context) + self.assertEqual(self.context.url, 'http://localhost/api/') + + execute('cd movie/1/', self.context) + self.assertEqual(self.context.url, 'http://localhost/api/movie/1/') + + def test_without_url(self): + execute('cd api/', self.context) + self.assertEqual(self.context.url, 'http://localhost/api/') + + execute('cd', self.context) + self.assertEqual(self.context.url, 'http://localhost') + + +class TestExecution_rm(ExecutionTestCase): + + def test_header(self): + self.context.headers['Content-Type'] = 'text/html' + execute('rm -h Content-Type', self.context) + self.assertFalse(self.context.headers) + + def test_option(self): + self.context.options['--form'] = None + execute('rm -o --form', self.context) + self.assertFalse(self.context.options) + + def test_querystring(self): + self.context.querystring_params['page'] = '1' + execute('rm -q page', self.context) + self.assertFalse(self.context.querystring_params) + + def test_body_param(self): + self.context.body_params['name'] = 'alice' + execute('rm -b name', self.context) + self.assertFalse(self.context.body_params) + + def test_body_json_param(self): + self.context.body_json_params['name'] = 'bob' + execute('rm -b name', self.context) + self.assertFalse(self.context.body_json_params) + + def test_header_single_quoted(self): + self.context.headers['Content-Type'] = 'text/html' + execute("rm -h 'Content-Type'", self.context) + self.assertFalse(self.context.headers) + + def test_option_double_quoted(self): + self.context.options['--form'] = None + execute('rm -o "--form"', self.context) + self.assertFalse(self.context.options) + + def test_querystring_double_quoted(self): + self.context.querystring_params['page size'] = '10' + execute('rm -q "page size"', self.context) + self.assertFalse(self.context.querystring_params) + + def test_body_param_double_quoted(self): + self.context.body_params['family name'] = 'Doe Doe' + execute('rm -b "family name"', self.context) + self.assertFalse(self.context.body_params) + + def test_body_param_escaped(self): + self.context.body_params['family name'] = 'Doe Doe' + execute(r'rm -b family\ name', self.context) + self.assertFalse(self.context.body_params) + + def test_body_json_param_escaped_colon(self): + self.context.body_json_params[r'where[id\:gt]'] = 2 + execute(r'rm -b where[id\:gt]', self.context) + self.assertFalse(self.context.body_json_params) + + def test_body_param_escaped_equal(self): + self.context.body_params[r'foo\=bar'] = 'hello' + execute(r'rm -b foo\=bar', self.context) + self.assertFalse(self.context.body_params) + + def test_non_existing_key(self): + execute('rm -q abcd', self.context) + self.assert_stderr("Key 'abcd' not found") + + def test_non_existing_key_unicode(self): # See #25 + execute(u'rm -q abcd', self.context) + self.assert_stderr("Key 'abcd' not found") + + def test_body_reset(self): + self.context.body_params.update({ + 'first_name': 'alice', + 'last_name': 'bryne' + }) + execute('rm -b *', self.context) + self.assertFalse(self.context.body_params) + + def test_querystring_reset(self): + self.context.querystring_params.update({ + 'first_name': 'alice', + 'last_name': 'bryne' + }) + execute('rm -q *', self.context) + self.assertFalse(self.context.querystring_params) + + def test_headers_reset(self): + self.context.headers.update({ + 'Content-Type': 'text/html', + 'Accept': 'application/json' + }) + execute('rm -h *', self.context) + self.assertFalse(self.context.headers) + + def test_options_reset(self): + self.context.options.update({ + '--form': None, + '--body': None + }) + execute('rm -o *', self.context) + self.assertFalse(self.context.options) + + def test_reset(self): + self.context.options.update({ + '--form': None, + '--verify': 'no' + }) + self.context.headers.update({ + 'Accept': 'dontcare', + 'Content-Type': 'dontcare' + }) + self.context.querystring_params.update({ + 'name': 'dontcare', + 'email': 'dontcare' + }) + self.context.body_params.update({ + 'name': 'dontcare', + 'email': 'dontcare' + }) + self.context.body_json_params.update({ + 'name': 'dontcare' + }) + + execute('rm *', self.context) + + self.assertFalse(self.context.options) + self.assertFalse(self.context.headers) + self.assertFalse(self.context.querystring_params) + self.assertFalse(self.context.body_params) + self.assertFalse(self.context.body_json_params) + + +class TestExecution_ls(ExecutionTestCase): + + def test_root(self): + execute('ls', self.context) + self.assert_stdout('orgs users\n') + + def test_relative_path(self): + self.context.url = 'http://localhost/users' + execute('ls 101', self.context) + self.assert_stdout('events orgs\n') + + def test_absolute_path(self): + self.context.url = 'http://localhost/users' + execute('ls /orgs/1', self.context) + self.assert_stdout('events members\n') + + def test_redirect_write(self): + filename = self.make_tempfile() + + # Write something first to make sure it's a full overwrite + with open(filename, 'w') as f: + f.write('hello world\n') + + execute('ls > %s' % filename, self.context) + + with open(filename) as f: + content = f.read() + self.assertEqual(content, 'orgs\nusers') + + def test_redirect_append(self): + filename = self.make_tempfile() + + # Write something first to make sure it's an append + with open(filename, 'w') as f: + f.write('hello world\n') + + execute('ls >> %s' % filename, self.context) + + with open(filename) as f: + content = f.read() + self.assertEqual(content, 'hello world\norgs\nusers') + + def test_grep(self): + execute('ls | grep users', self.context) + self.assert_stdout('users\n') + + +class TestMutation(ExecutionTestCase): + + def test_simple_headers(self): + execute('Accept:text/html User-Agent:HttpPrompt', self.context) + self.assertEqual(self.context.headers, { + 'Accept': 'text/html', + 'User-Agent': 'HttpPrompt' + }) + + def test_header_value_with_double_quotes(self): + execute('Accept:text/html User-Agent:"HTTP Prompt"', self.context) + self.assertEqual(self.context.headers, { + 'Accept': 'text/html', + 'User-Agent': 'HTTP Prompt' + }) + + def test_header_value_with_single_quotes(self): + execute("Accept:text/html User-Agent:'HTTP Prompt'", self.context) + self.assertEqual(self.context.headers, { + 'Accept': 'text/html', + 'User-Agent': 'HTTP Prompt' + }) + + def test_header_with_double_quotes(self): + execute('Accept:text/html "User-Agent:HTTP Prompt"', self.context) + self.assertEqual(self.context.headers, { + 'Accept': 'text/html', + 'User-Agent': 'HTTP Prompt' + }) + + def test_header_with_single_quotes(self): + execute("Accept:text/html 'User-Agent:HTTP Prompt'", self.context) + self.assertEqual(self.context.headers, { + 'Accept': 'text/html', + 'User-Agent': 'HTTP Prompt' + }) + + def test_header_escaped_chars(self): + execute(r'X-Name:John\'s\ Doe', self.context) + self.assertEqual(self.context.headers, { + 'X-Name': "John's Doe" + }) + + def test_header_value_escaped_quote(self): + execute(r"'X-Name:John\'s Doe'", self.context) + self.assertEqual(self.context.headers, { + 'X-Name': "John's Doe" + }) + + def test_simple_querystring(self): + execute('page==1 limit==20', self.context) + self.assertEqual(self.context.querystring_params, { + 'page': ['1'], + 'limit': ['20'] + }) + + def test_querystring_with_double_quotes(self): + execute('page==1 name=="John Doe"', self.context) + self.assertEqual(self.context.querystring_params, { + 'page': ['1'], + 'name': ['John Doe'] + }) + + def test_querystring_with_single_quotes(self): + execute("page==1 name=='John Doe'", self.context) + self.assertEqual(self.context.querystring_params, { + 'page': ['1'], + 'name': ['John Doe'] + }) + + def test_querystring_with_chinese(self): + execute("name==王小明", self.context) + self.assertEqual(self.context.querystring_params, { + 'name': ['王小明'] + }) + + def test_querystring_escaped_chars(self): + execute(r'name==John\'s\ Doe', self.context) + self.assertEqual(self.context.querystring_params, { + 'name': ["John's Doe"] + }) + + def test_querytstring_value_escaped_quote(self): + execute(r"'name==John\'s Doe'", self.context) + self.assertEqual(self.context.querystring_params, { + 'name': ["John's Doe"] + }) + + def test_querystring_key_escaped_quote(self): + execute(r"'john\'s last name==Doe'", self.context) + self.assertEqual(self.context.querystring_params, { + "john's last name": ['Doe'] + }) + + def test_simple_body_params(self): + execute('username=john password=123', self.context) + self.assertEqual(self.context.body_params, { + 'username': 'john', + 'password': '123' + }) + + def test_body_param_value_with_double_quotes(self): + execute('name="John Doe" password=123', self.context) + self.assertEqual(self.context.body_params, { + 'name': 'John Doe', + 'password': '123' + }) + + def test_body_param_value_with_single_quotes(self): + execute("name='John Doe' password=123", self.context) + self.assertEqual(self.context.body_params, { + 'name': 'John Doe', + 'password': '123' + }) + + def test_body_param_with_double_quotes(self): + execute('"name=John Doe" password=123', self.context) + self.assertEqual(self.context.body_params, { + 'name': 'John Doe', + 'password': '123' + }) + + def test_body_param_with_spanish(self): + execute('name=Jesús', self.context) + self.assertEqual(self.context.body_params, { + 'name': 'Jesús' + }) + + def test_body_param_escaped_chars(self): + execute(r'name=John\'s\ Doe', self.context) + self.assertEqual(self.context.body_params, { + 'name': "John's Doe" + }) + + def test_body_param_value_escaped_quote(self): + execute(r"'name=John\'s Doe'", self.context) + self.assertEqual(self.context.body_params, { + 'name': "John's Doe" + }) + + def test_body_param_key_escaped_quote(self): + execute(r"'john\'s last name=Doe'", self.context) + self.assertEqual(self.context.body_params, { + "john's last name": 'Doe' + }) + + def test_long_option_names(self): + execute('--auth user:pass --form', self.context) + self.assertEqual(self.context.options, { + '--form': None, + '--auth': 'user:pass' + }) + + def test_long_option_names_with_its_prefix(self): + execute('--auth-type basic --auth user:pass --session user ' + '--session-read-only user', self.context) + self.assertEqual(self.context.options, { + '--auth-type': 'basic', + '--auth': 'user:pass', + '--session-read-only': 'user', + '--session': 'user' + }) + + def test_long_short_option_names_mixed(self): + execute('--style=default -j --stream', self.context) + self.assertEqual(self.context.options, { + '-j': None, + '--stream': None, + '--style': 'default' + }) + + def test_option_and_body_param(self): + execute('--form name="John Doe"', self.context) + self.assertEqual(self.context.options, { + '--form': None + }) + self.assertEqual(self.context.body_params, { + 'name': 'John Doe' + }) + + def test_mixed(self): + execute(' --form name="John Doe" password=1234\\ 5678 ' + 'User-Agent:HTTP\\ Prompt -a \'john:1234 5678\' ' + '"Accept:text/html" ', self.context) + self.assertEqual(self.context.options, { + '--form': None, + '-a': 'john:1234 5678' + }) + self.assertEqual(self.context.headers, { + 'User-Agent': 'HTTP Prompt', + 'Accept': 'text/html' + }) + self.assertEqual(self.context.options, { + '--form': None, + '-a': 'john:1234 5678' + }) + self.assertEqual(self.context.body_params, { + 'name': 'John Doe', + 'password': '1234 5678' + }) + + def test_multi_querystring(self): + execute('name==john name==doe', self.context) + self.assertEqual(self.context.querystring_params, { + 'name': ['john', 'doe'] + }) + + execute('name==jane', self.context) + self.assertEqual(self.context.querystring_params, { + 'name': ['jane'] + }) + + def test_raw_json_object(self): + execute("""definition:={"id":819,"name":"ML"}""", self.context) + self.assertEqual(self.context.body_json_params, { + 'definition': { + 'id': 819, + 'name': 'ML' + } + }) + + def test_raw_json_object_quoted(self): + execute("""definition:='{"id": 819, "name": "ML"}'""", self.context) + self.assertEqual(self.context.body_json_params, { + 'definition': { + 'id': 819, + 'name': 'ML' + } + }) + + def test_raw_json_array(self): + execute("""names:=["foo","bar"]""", self.context) + self.assertEqual(self.context.body_json_params, { + 'names': ["foo", "bar"] + }) + + def test_raw_json_array_quoted(self): + execute("""names:='["foo", "bar"]'""", self.context) + self.assertEqual(self.context.body_json_params, { + 'names': ["foo", "bar"] + }) + + def test_raw_json_integer(self): + execute('number:=999', self.context) + self.assertEqual(self.context.body_json_params, {'number': 999}) + + def test_raw_json_string(self): + execute("""name:='"john doe"'""", self.context) + self.assertEqual(self.context.body_json_params, {'name': 'john doe'}) + + def test_escape_colon(self): + execute(r'where[id\:gt]:=2', self.context) + self.assertEqual(self.context.body_json_params, { + r'where[id\:gt]': 2 + }) + + def test_escape_equal(self): + execute(r'foo\=bar=hello', self.context) + self.assertEqual(self.context.body_params, { + r'foo\=bar': 'hello' + }) + + +class TestHttpAction(ExecutionTestCase): + + def test_get(self): + execute('get', self.context) + self.assert_httpie_main_called_with(['GET', 'http://localhost']) + + def test_get_uppercase(self): + execute('GET', self.context) + self.assert_httpie_main_called_with(['GET', 'http://localhost']) + + def test_get_multi_querystring(self): + execute('get foo==1 foo==2 foo==3', self.context) + self.assert_httpie_main_called_with([ + 'GET', 'http://localhost', 'foo==1', 'foo==2', 'foo==3']) + + def test_post(self): + execute('post page==1', self.context) + self.assert_httpie_main_called_with(['POST', 'http://localhost', + 'page==1']) + self.assertFalse(self.context.querystring_params) + + def test_post_with_absolute_path(self): + execute('post /api/v3 name=bob', self.context) + self.assert_httpie_main_called_with(['POST', 'http://localhost/api/v3', + 'name=bob']) + self.assertFalse(self.context.body_params) + self.assertEqual(self.context.url, 'http://localhost') + + def test_post_with_relative_path(self): + self.context.url = 'http://localhost/api/v3' + execute('post ../v2/movie id=8', self.context) + self.assert_httpie_main_called_with([ + 'POST', 'http://localhost/api/v2/movie', 'id=8']) + self.assertFalse(self.context.body_params) + self.assertEqual(self.context.url, 'http://localhost/api/v3') + + def test_post_with_full_url(self): + execute('post http://httpbin.org/post id=9', self.context) + self.assert_httpie_main_called_with([ + 'POST', 'http://httpbin.org/post', 'id=9']) + self.assertFalse(self.context.body_params) + self.assertEqual(self.context.url, 'http://localhost') + + def test_post_with_full_https_url(self): + execute('post https://httpbin.org/post id=9', self.context) + self.assert_httpie_main_called_with([ + 'POST', 'https://httpbin.org/post', 'id=9']) + self.assertFalse(self.context.body_params) + self.assertEqual(self.context.url, 'http://localhost') + + def test_post_uppercase(self): + execute('POST content=text', self.context) + self.assert_httpie_main_called_with(['POST', 'http://localhost', + 'content=text']) + self.assertFalse(self.context.body_params) + + def test_post_raw_json_object(self): + execute("""post definition:={"id":819,"name":"ML"}""", + self.context) + self.assert_httpie_main_called_with([ + 'POST', 'http://localhost', + """definition:={"id": 819, "name": "ML"}"""]) + self.assertFalse(self.context.body_json_params) + + def test_post_raw_json_object_quoted(self): + execute("""post definition:='{"id": 819, "name": "ML"}'""", + self.context) + self.assert_httpie_main_called_with([ + 'POST', 'http://localhost', + 'definition:={"id": 819, "name": "ML"}']) + self.assertFalse(self.context.body_json_params) + + def test_post_raw_json_array(self): + execute("""post hobbies:=["foo","bar"]""", + self.context) + self.assert_httpie_main_called_with([ + 'POST', 'http://localhost', + 'hobbies:=["foo", "bar"]']) + self.assertFalse(self.context.body_json_params) + + def test_post_raw_json_array_quoted(self): + execute("""post hobbies:='["foo", "bar"]'""", + self.context) + self.assert_httpie_main_called_with([ + 'POST', 'http://localhost', + 'hobbies:=["foo", "bar"]']) + self.assertFalse(self.context.body_json_params) + + def test_post_raw_json_integer(self): + execute('post number:=123', + self.context) + self.assert_httpie_main_called_with([ + 'POST', 'http://localhost', 'number:=123']) + self.assertFalse(self.context.body_json_params) + + def test_post_raw_json_boolean(self): + execute('post foo:=true', + self.context) + self.assert_httpie_main_called_with([ + 'POST', 'http://localhost', 'foo:=true']) + self.assertFalse(self.context.body_json_params) + + def test_delete(self): + execute('delete', self.context) + self.assert_httpie_main_called_with(['DELETE', 'http://localhost']) + + def test_delete_uppercase(self): + execute('DELETE', self.context) + self.assert_httpie_main_called_with(['DELETE', 'http://localhost']) + + def test_patch(self): + execute('patch', self.context) + self.assert_httpie_main_called_with(['PATCH', 'http://localhost']) + + def test_patch_uppercase(self): + execute('PATCH', self.context) + self.assert_httpie_main_called_with(['PATCH', 'http://localhost']) + + def test_head(self): + execute('head', self.context) + self.assert_httpie_main_called_with(['HEAD', 'http://localhost']) + + def test_head_uppercase(self): + execute('HEAD', self.context) + self.assert_httpie_main_called_with(['HEAD', 'http://localhost']) + + def test_options(self): + execute('options', self.context) + self.assert_httpie_main_called_with(['OPTIONS', 'http://localhost']) + + +class TestHttpActionRedirection(ExecutionTestCase): + + def test_get(self): + execute('get > data.json', self.context) + self.assert_httpie_main_called_with(['GET', 'http://localhost']) + + env = self.httpie_main.call_args[1]['env'] + self.assertFalse(env.stdout_isatty) + self.assertEqual(env.stdout.fp.name, 'data.json') + + +@pytest.mark.slow +class TestHttpBin(TempAppDirTestCase): + """Send real requests to http://httpbin.org, save the responses to files, + and asserts on the file content. + """ + + def setUp(self): + super(TestHttpBin, self).setUp() + + # XXX: pytest doesn't allow HTTPie to read stdin while it's capturing + # stdout, so we replace stdin with a file temporarily during the test. + class MockStdin(object): + def __init__(self, fp): + self.fp = fp + + def isatty(self): + return True + + def __getattr__(self, name): + if name == 'isatty': + return self.isatty + return getattr(self.fp, name) + + self.orig_stdin = sys.stdin + filename = self.make_tempfile() + sys.stdin = MockStdin(open(filename, 'rb')) + sys.stdin.isatty = lambda: True + + # Mock echo_via_pager() so that we can catch data fed to stdout + self.patcher = patch('http_prompt.output.click.echo_via_pager') + self.echo_via_pager = self.patcher.start() + + def tearDown(self): + self.patcher.stop() + + sys.stdin.close() + sys.stdin = self.orig_stdin + + super(TestHttpBin, self).tearDown() + + def get_stdout(self): + return self.echo_via_pager.call_args[0][0] + + def execute_redirection(self, command): + context = Context('http://httpbin.org') + filename = self.make_tempfile() + execute('%s > %s' % (command, filename), context) + + with open(filename, 'rb') as f: + return f.read() + + def execute_pipe(self, command): + context = Context('http://httpbin.org') + execute(command, context) + + def test_get_image(self): + data = self.execute_redirection('get /image/png') + self.assertTrue(data) + self.assertEqual(hashlib.sha1(data).hexdigest(), + '379f5137831350c900e757b39e525b9db1426d53') + + def test_get_querystring(self): + data = self.execute_redirection( + 'get /get id==1234 X-Custom-Header:5678') + data = json.loads(data.decode()) + self.assertEqual(data['args'], { + 'id': '1234' + }) + self.assertEqual(data['headers']['X-Custom-Header'], '5678') + + def test_post_json(self): + data = self.execute_redirection( + 'post /post id=1234 X-Custom-Header:5678') + data = json.loads(data.decode()) + self.assertEqual(data['json'], { + 'id': '1234' + }) + self.assertEqual(data['headers']['X-Custom-Header'], '5678') + + def test_post_form(self): + data = self.execute_redirection( + 'post /post --form id=1234 X-Custom-Header:5678') + data = json.loads(data.decode()) + self.assertEqual(data['form'], { + 'id': '1234' + }) + self.assertEqual(data['headers']['X-Custom-Header'], '5678') + + @pytest.mark.skipif(sys.platform == 'win32', reason="Unix only") + def test_get_and_tee(self): + filename = self.make_tempfile() + self.execute_pipe('get /get hello==world | tee %s' % filename) + + with open(filename) as f: + data = json.load(f) + self.assertEqual(data['args'], {'hello': 'world'}) + + printed_msg = self.get_stdout() + data = json.loads(printed_msg) + self.assertEqual(data['args'], {'hello': 'world'}) + + +class TestCommandPreview(ExecutionTestCase): + + def test_httpie_without_args(self): + execute('httpie', self.context) + self.assert_stdout('http http://localhost\n') + + def test_httpie_with_post(self): + execute('httpie post name=alice', self.context) + self.assert_stdout('http POST http://localhost name=alice\n') + self.assertFalse(self.context.body_params) + + def test_httpie_with_absolute_path(self): + execute('httpie post /api name=alice', self.context) + self.assert_stdout('http POST http://localhost/api name=alice\n') + self.assertFalse(self.context.body_params) + + def test_httpie_with_full_url(self): + execute('httpie POST http://httpbin.org/post name=alice', self.context) + self.assert_stdout('http POST http://httpbin.org/post name=alice\n') + self.assertEqual(self.context.url, 'http://localhost') + self.assertFalse(self.context.body_params) + + def test_httpie_with_full_https_url(self): + execute('httpie post https://httpbin.org/post name=alice', + self.context) + self.assert_stdout('http POST https://httpbin.org/post name=alice\n') + self.assertEqual(self.context.url, 'http://localhost') + self.assertFalse(self.context.body_params) + + def test_httpie_with_quotes(self): + execute(r'httpie post http://httpbin.org/post name="john doe" ' + r"apikey==abc\ 123 'Authorization:ApiKey 1234'", + self.context) + self.assert_stdout( + "http POST http://httpbin.org/post 'apikey==abc 123' " + "'name=john doe' 'Authorization:ApiKey 1234'\n") + self.assertEqual(self.context.url, 'http://localhost') + self.assertFalse(self.context.body_params) + self.assertFalse(self.context.querystring_params) + self.assertFalse(self.context.headers) + + def test_httpie_with_multi_querystring(self): + execute('httpie get foo==1 foo==2 foo==3', self.context) + self.assert_stdout('http GET http://localhost foo==1 foo==2 foo==3\n') + self.assertEqual(self.context.url, 'http://localhost') + self.assertFalse(self.context.querystring_params) + + +class TestPipe(ExecutionTestCase): + + @pytest.mark.skipif(sys.platform == 'win32', reason="Unix only") + def test_httpie_sed(self): + execute("httpie get some==data | sed 's/data$/input/'", self.context) + self.assert_stdout('http GET http://localhost some==input\n') + + @pytest.mark.skipif(sys.platform == 'win32', reason="Unix only") + def test_httpie_sed_with_echo(self): + execute("httpie post | `echo \"sed 's/localhost$/127.0.0.1/'\"`", + self.context) + self.assert_stdout("http POST http://127.0.0.1\n") + + @pytest.mark.skipif(sys.platform == 'win32', reason="Unix only") + def test_env_grep(self): + self.context.body_params = { + 'username': 'jane', + 'name': 'Jane', + 'password': '1234' + } + execute('env | grep name', self.context) + self.assert_stdout('name=Jane\nusername=jane\n') + + +class TestShellSubstitution(ExecutionTestCase): + + def test_unquoted_option(self): + execute("--auth `echo user:pass`", self.context) + self.assertEqual(self.context.options, { + '--auth': 'user:pass' + }) + + def test_partial_unquoted_option(self): + execute("--auth user:`echo pass`", self.context) + self.assertEqual(self.context.options, { + '--auth': 'user:pass' + }) + + def test_partial_squoted_option(self): + execute("--auth='user:`echo pass`'", self.context) + self.assertEqual(self.context.options, { + '--auth': 'user:pass' + }) + + def test_partial_dquoted_option(self): + execute('--auth="user:`echo pass`"', self.context) + self.assertEqual(self.context.options, { + '--auth': 'user:pass' + }) + + def test_unquoted_header(self): + execute("`echo 'X-Greeting'`:`echo 'hello world'`", self.context) + if sys.platform == 'win32': + expected_key = "'X-Greeting'" + expected_value = "'hello world'" + else: + expected_key = 'X-Greeting' + expected_value = 'hello world' + + self.assertEqual(self.context.headers, { + expected_key: expected_value + }) + + def test_full_squoted_header(self): + execute("'`echo X-Greeting`:`echo hello`'", self.context) + self.assertEqual(self.context.headers, { + 'X-Greeting': 'hello' + }) + + def test_full_dquoted_header(self): + execute('"`echo X-Greeting`:`echo hello`"', self.context) + self.assertEqual(self.context.headers, { + 'X-Greeting': 'hello' + }) + + def test_value_squoted_header(self): + execute("`echo X-Greeting`:'`echo hello`'", self.context) + self.assertEqual(self.context.headers, { + 'X-Greeting': 'hello' + }) + + def test_value_dquoted_header(self): + execute('`echo X-Greeting`:"`echo hello`"', self.context) + self.assertEqual(self.context.headers, { + 'X-Greeting': 'hello' + }) + + def test_partial_value_dquoted_header(self): + execute('Authorization:"Bearer `echo OAUTH TOKEN`"', self.context) + self.assertEqual(self.context.headers, { + 'Authorization': 'Bearer OAUTH TOKEN' + }) + + def test_partial_full_dquoted_header(self): + execute('"Authorization:Bearer `echo OAUTH TOKEN`"', self.context) + self.assertEqual(self.context.headers, { + 'Authorization': 'Bearer OAUTH TOKEN' + }) + + def test_unquoted_querystring(self): + execute("`echo greeting`==`echo 'hello world'`", self.context) + expected = ("'hello world'" + if sys.platform == 'win32' else 'hello world') + self.assertEqual(self.context.querystring_params, { + 'greeting': [expected] + }) + + def test_full_squoted_querystring(self): + execute("'`echo greeting`==`echo hello`'", self.context) + self.assertEqual(self.context.querystring_params, { + 'greeting': ['hello'] + }) + + def test_value_squoted_querystring(self): + execute("`echo greeting`=='`echo hello`'", self.context) + self.assertEqual(self.context.querystring_params, { + 'greeting': ['hello'] + }) + + def test_value_dquoted_querystring(self): + execute('`echo greeting`=="`echo hello`"', self.context) + self.assertEqual(self.context.querystring_params, { + 'greeting': ['hello'] + }) + + def test_unquoted_body_param(self): + execute("`echo greeting`=`echo 'hello world'`", self.context) + expected = ("'hello world'" + if sys.platform == 'win32' else 'hello world') + self.assertEqual(self.context.body_params, { + 'greeting': expected + }) + + def test_full_squoted_body_param(self): + execute("'`echo greeting`=`echo hello`'", self.context) + self.assertEqual(self.context.body_params, { + 'greeting': 'hello' + }) + + def test_value_squoted_body_param(self): + execute("`echo greeting`='`echo hello`'", self.context) + self.assertEqual(self.context.body_params, { + 'greeting': 'hello' + }) + + def test_full_dquoted_body_param(self): + execute('"`echo greeting`=`echo hello`"', self.context) + self.assertEqual(self.context.body_params, { + 'greeting': 'hello' + }) + + def test_bad_command(self): + execute("name=`bad command test`", self.context) + self.assertEqual(self.context.body_params, {'name': ''}) + + @pytest.mark.skipif(sys.platform == 'win32', reason="Unix only") + def test_pipe_and_grep(self): + execute("greeting=`echo 'hello world\nhihi\n' | grep hello`", + self.context) + self.assertEqual(self.context.body_params, { + 'greeting': 'hello world' + }) + + +class TestCommandPreviewRedirection(ExecutionTestCase): + + def test_httpie_redirect_write(self): + filename = self.make_tempfile() + + # Write something first to make sure it's a full overwrite + with open(filename, 'w') as f: + f.write('hello world\n') + + execute('httpie > %s' % filename, self.context) + + with open(filename) as f: + content = f.read() + self.assertEqual(content, 'http http://localhost\n') + + def test_httpie_redirect_write_quoted_filename(self): + filename = self.make_tempfile() + + # Write something first to make sure it's a full overwrite + with open(filename, 'w') as f: + f.write('hello world\n') + + execute('httpie > "%s"' % filename, self.context) + + with open(filename) as f: + content = f.read() + self.assertEqual(content, 'http http://localhost\n') + + @pytest.mark.skipif(sys.platform == 'win32', + reason="Windows doesn't use backslashes to escape") + def test_httpie_redirect_write_escaped_filename(self): + filename = self.make_tempfile() + filename += r' copy' + + # Write something first to make sure it's a full overwrite + with open(filename, 'w') as f: + f.write('hello world\n') + + execute('httpie > %s' % filename.replace(' ', r'\ '), self.context) + + with open(filename) as f: + content = f.read() + self.assertEqual(content, 'http http://localhost\n') + + def test_httpie_redirect_write_with_args(self): + filename = self.make_tempfile() + + # Write something first to make sure it's a full overwrite + with open(filename, 'w') as f: + f.write('hello world\n') + + execute('httpie post http://example.org name=john > %s' % filename, + self.context) + + with open(filename) as f: + content = f.read() + self.assertEqual(content, 'http POST http://example.org name=john\n') + + def test_httpie_redirect_append(self): + filename = self.make_tempfile() + + # Write something first to make sure it's an append + with open(filename, 'w') as f: + f.write('hello world\n') + + execute('httpie >> %s' % filename, self.context) + + with open(filename) as f: + content = f.read() + self.assertEqual(content, 'hello world\nhttp http://localhost\n') + + def test_httpie_redirect_append_without_spaces(self): + filename = self.make_tempfile() + + # Write something first to make sure it's an append + with open(filename, 'w') as f: + f.write('hello world\n') + + execute('httpie>>%s' % filename, self.context) + + with open(filename) as f: + content = f.read() + self.assertEqual(content, 'hello world\nhttp http://localhost\n') + + def test_httpie_redirect_append_quoted_filename(self): + filename = self.make_tempfile() + + # Write something first to make sure it's an append + with open(filename, 'w') as f: + f.write('hello world\n') + + execute("httpie >> '%s'" % filename, self.context) + + with open(filename) as f: + content = f.read() + self.assertEqual(content, 'hello world\nhttp http://localhost\n') diff --git a/tests/prompt/test_installation.py b/tests/prompt/test_installation.py new file mode 100644 index 00000000..bfa266d5 --- /dev/null +++ b/tests/prompt/test_installation.py @@ -0,0 +1,32 @@ +"""Test if http-prompt is installed correctly.""" + +import subprocess + +import pytest + +from subprocess import PIPE + +from .utils import get_http_prompt_path +from httpie.prompt import __version__ + + +def run_http_prompt(args): + """Run http-prompt from terminal.""" + bin_path = get_http_prompt_path() + p = subprocess.Popen([bin_path] + args, stdin=PIPE, stdout=PIPE) + return p.communicate() + + +@pytest.mark.slow +def test_help(): + out, err = run_http_prompt(['--help']) + assert out.startswith(b'Usage: http-prompt') + + +@pytest.mark.slow +def test_version(): + out, err = run_http_prompt(['--version']) + version = __version__ + if hasattr(version, 'encode'): + version = version.encode('ascii') + assert out.rstrip() == version diff --git a/tests/prompt/test_interaction.py b/tests/prompt/test_interaction.py new file mode 100644 index 00000000..f58c86c5 --- /dev/null +++ b/tests/prompt/test_interaction.py @@ -0,0 +1,79 @@ +import os +import sys + +import pexpect +import pytest + +from .base import TempAppDirTestCase +from .utils import get_http_prompt_path +from httpie.prompt import config + + +class TestInteraction(TempAppDirTestCase): + + def setUp(self): + super(TestInteraction, self).setUp() + + # Use temporary directory as user config home. + # Will restore it in tearDown(). + self.orig_config_home = os.getenv('XDG_CONFIG_HOME') + os.environ['XDG_CONFIG_HOME'] = self.temp_dir + + # Make sure pexpect uses the same terminal environment + self.orig_term = os.getenv('TERM') + os.environ['TERM'] = 'screen-256color' + + def tearDown(self): + super(TestInteraction, self).tearDown() + + os.environ['XDG_CONFIG_HOME'] = self.orig_config_home + + if self.orig_term: + os.environ['TERM'] = self.orig_term + else: + os.environ.pop('TERM', None) + + def write_config(self, content): + config_path = config.get_user_config_path() + with open(config_path, 'a') as f: + f.write(content) + + @pytest.mark.skipif(sys.platform == 'win32', + reason="pexpect doesn't work well on Windows") + @pytest.mark.slow + def test_interaction(self): + bin_path = get_http_prompt_path() + child = pexpect.spawn(bin_path, env=os.environ) + + # TODO: Test more interaction + + child.sendline('exit') + child.expect_exact('Goodbye!', timeout=20) + child.close() + + @pytest.mark.skipif(sys.platform == 'win32', + reason="pexpect doesn't work well on Windows") + @pytest.mark.slow + def test_vi_mode(self): + self.write_config('vi = True\n') + + bin_path = get_http_prompt_path() + child = pexpect.spawn(bin_path, env=os.environ) + + child.expect_exact('http://localhost:8000>') + + # Enter 'htpie', switch to command mode (ESC), + # move two chars left (hh), and insert (i) a 't' + child.send('htpie') + child.send('\x1b') + child.sendline('hhit') + + child.expect_exact('http http://localhost:8000') + + # Enter 'exit' + child.send('\x1b') + child.send('i') + child.sendline('exit') + + child.expect_exact('Goodbye!', timeout=20) + child.close() diff --git a/tests/prompt/test_lexer.py b/tests/prompt/test_lexer.py new file mode 100644 index 00000000..ca1ac594 --- /dev/null +++ b/tests/prompt/test_lexer.py @@ -0,0 +1,793 @@ +import unittest + +from pygments.token import Keyword, String, Text, Error, Name, Operator + +from httpie.prompt.lexer import HttpPromptLexer + + +class LexerTestCase(unittest.TestCase): + + def setUp(self): + self.lexer = HttpPromptLexer() + + def get_tokens(self, text, filter_spaces=True): + tokens = self.lexer.get_tokens(text) + tokens = filter(lambda t: t[1], tokens) + if filter_spaces: + tokens = filter(lambda t: t[1].strip(), tokens) + return list(tokens) + + +class TestLexer_mutation(LexerTestCase): + + def test_querystring(self): + self.assertEqual(self.get_tokens('foo==bar'), [ + (Name, 'foo'), + (Operator, '=='), + (String, 'bar') + ]) + + def test_body_param(self): + self.assertEqual(self.get_tokens('foo=bar'), [ + (Name, 'foo'), + (Operator, '='), + (String, 'bar') + ]) + + def test_header(self): + self.assertEqual(self.get_tokens('Accept:application/json'), [ + (Name, 'Accept'), + (Operator, ':'), + (String, 'application/json') + ]) + + def test_json_integer(self): + self.assertEqual(self.get_tokens('number:=1'), [ + (Name, 'number'), + (Operator, ':='), + (String, '1') + ]) + + def test_json_boolean(self): + self.assertEqual(self.get_tokens('enabled:=true'), [ + (Name, 'enabled'), + (Operator, ':='), + (String, 'true') + ]) + + def test_json_string(self): + self.assertEqual(self.get_tokens('name:="foo bar"'), [ + (Name, 'name'), + (Operator, ':='), + (Text, '"'), + (String, 'foo bar'), + (Text, '"') + ]) + + def test_json_array(self): + self.assertEqual(self.get_tokens('list:=[1,"two"]'), [ + (Name, 'list'), + (Operator, ':='), + (String, '[1,"two"]'), + ]) + + def test_json_array_quoted(self): + self.assertEqual(self.get_tokens("""list:='[1,"two"]'"""), [ + (Name, 'list'), + (Operator, ':='), + (Text, "'"), + (String, '[1,"two"]'), + (Text, "'"), + ]) + + def test_json_object(self): + self.assertEqual(self.get_tokens('object:={"id":123,"name":"foo"}'), [ + (Name, 'object'), + (Operator, ':='), + (String, '{"id":123,"name":"foo"}'), + ]) + + def test_json_object_quoted(self): + self.assertEqual(self.get_tokens("""object:='{"id": 123}'"""), [ + (Name, 'object'), + (Operator, ':='), + (Text, "'"), + (String, '{"id": 123}'), + (Text, "'") + ]) + + def test_json_escaped_colon(self): + self.assertEqual(self.get_tokens(r'where[id\:gt]:=2'), [ + (Name, r'where[id\:gt]'), + (Operator, ':='), + (String, '2') + ]) + + def test_body_param_escaped_equal(self): + self.assertEqual(self.get_tokens(r'foo\=bar=hello'), [ + (Name, r'foo\=bar'), + (Operator, '='), + (String, 'hello') + ]) + + def test_parameter_name_including_http_method_name(self): + self.assertEqual(self.get_tokens('heading==hello'), [ + (Name, 'heading'), + (Operator, '=='), + (String, 'hello') + ]) + + +class TestLexer_cd(LexerTestCase): + + def test_simple(self): + self.assertEqual(self.get_tokens('cd api/v1'), [ + (Keyword, 'cd'), + (String, 'api/v1') + ]) + + def test_double_quoted(self): + self.assertEqual(self.get_tokens('cd "api/v 1"'), [ + (Keyword, 'cd'), + (Text, '"'), + (String, 'api/v 1'), + (Text, '"') + ]) + + def test_single_quoted(self): + self.assertEqual(self.get_tokens("cd 'api/v 1'"), [ + (Keyword, 'cd'), + (Text, "'"), + (String, 'api/v 1'), + (Text, "'") + ]) + + def test_escape(self): + self.assertEqual(self.get_tokens(r"cd api/v\ 1"), [ + (Keyword, 'cd'), + (String, r'api/v\ 1') + ]) + + def test_second_path(self): + self.assertEqual(self.get_tokens(r"cd api v1"), [ + (Keyword, 'cd'), + (String, 'api'), + (Error, 'v'), + (Error, '1') + ]) + + def test_leading_trailing_spaces(self): + self.assertEqual(self.get_tokens(' cd api/v1 '), [ + (Keyword, 'cd'), + (String, 'api/v1') + ]) + + +class TestLexer_ls(LexerTestCase): + + def test_no_path(self): + self.assertEqual(self.get_tokens('ls'), [ + (Keyword, 'ls') + ]) + + def test_path(self): + self.assertEqual(self.get_tokens('ls api/v1'), [ + (Keyword, 'ls'), + (String, 'api/v1') + ]) + + def test_second_path(self): + self.assertEqual(self.get_tokens(r"ls api v1"), [ + (Keyword, 'ls'), + (String, 'api'), + (Error, 'v'), + (Error, '1') + ]) + + def test_leading_trailing_spaces(self): + self.assertEqual(self.get_tokens(' ls api/v1 '), [ + (Keyword, 'ls'), + (String, 'api/v1') + ]) + + def test_redirect(self): + self.assertEqual(self.get_tokens('ls api/v1 > endpoints.txt'), [ + (Keyword, 'ls'), + (String, 'api/v1'), + (Operator, '>'), + (String, 'endpoints.txt') + ]) + + +class TestLexer_env(LexerTestCase): + + def test_env_simple(self): + self.assertEqual(self.get_tokens('env'), [ + (Keyword, 'env'), + ]) + + def test_env_with_spaces(self): + self.assertEqual(self.get_tokens(' env '), [ + (Keyword, 'env'), + ]) + + def test_env_write(self): + self.assertEqual(self.get_tokens('env > /tmp/file.txt'), [ + (Keyword, 'env'), (Operator, '>'), + (String, '/tmp/file.txt') + ]) + + def test_env_append(self): + self.assertEqual(self.get_tokens('env >> /tmp/file.txt'), [ + (Keyword, 'env'), (Operator, '>>'), + (String, '/tmp/file.txt') + ]) + + def test_env_write_quoted_filename(self): + self.assertEqual(self.get_tokens('env > "/tmp/my file.txt"'), [ + (Keyword, 'env'), (Operator, '>'), + (Text, '"'), (String, '/tmp/my file.txt'), (Text, '"') + ]) + + def test_env_append_escaped_filename(self): + self.assertEqual(self.get_tokens(r'env >> /tmp/my\ file.txt'), [ + (Keyword, 'env'), (Operator, '>>'), + (String, r'/tmp/my\ file.txt') + ]) + + def test_env_pipe(self): + self.assertEqual(self.get_tokens('env | grep name'), [ + (Keyword, 'env'), (Operator, '|'), + (Text, 'grep'), (Text, 'name') + ]) + + +class TestLexer_rm(LexerTestCase): + + def test_header(self): + self.assertEqual(self.get_tokens('rm -h Accept'), [ + (Keyword, 'rm'), + (Name, '-h'), + (String, 'Accept') + ]) + + def test_header_escaped(self): + self.assertEqual(self.get_tokens(r'rm -h Custom\ Header'), [ + (Keyword, 'rm'), + (Name, '-h'), + (String, r'Custom\ Header') + ]) + + def test_querystring(self): + self.assertEqual(self.get_tokens('rm -q page'), [ + (Keyword, 'rm'), + (Name, '-q'), + (String, 'page') + ]) + + def test_querystring_double_quoted(self): + self.assertEqual(self.get_tokens('rm -q "page size"'), [ + (Keyword, 'rm'), + (Name, '-q'), + (Text, '"'), + (String, 'page size'), + (Text, '"') + ]) + + def test_body_param(self): + self.assertEqual(self.get_tokens('rm -b name'), [ + (Keyword, 'rm'), + (Name, '-b'), + (String, 'name') + ]) + + def test_body_param_single_quoted(self): + self.assertEqual(self.get_tokens("rm -b 'first name'"), [ + (Keyword, 'rm'), + (Name, '-b'), + (Text, "'"), + (String, 'first name'), + (Text, "'") + ]) + + def test_option(self): + self.assertEqual(self.get_tokens('rm -o --json'), [ + (Keyword, 'rm'), + (Name, '-o'), + (String, '--json') + ]) + + def test_reset(self): + self.assertEqual(self.get_tokens('rm *'), [ + (Keyword, 'rm'), + (Name, '*') + ]) + + def test_option_leading_trailing_spaces(self): + self.assertEqual(self.get_tokens(' rm -o --json '), [ + (Keyword, 'rm'), + (Name, '-o'), + (String, '--json') + ]) + + def test_invalid_type(self): + self.assertEqual(self.get_tokens('rm -a foo'), [ + (Keyword, 'rm'), + (Error, '-'), (Error, 'a'), + (Error, 'f'), (Error, 'o'), (Error, 'o') + ]) + + +class TestLexer_help(LexerTestCase): + + def test_help_simple(self): + self.assertEqual(self.get_tokens('help'), [ + (Keyword, 'help') + ]) + + def test_help_with_spaces(self): + self.assertEqual(self.get_tokens(' help '), [ + (Keyword, 'help') + ]) + + +class TestLexer_source(LexerTestCase): + + def test_source_simple_filename(self): + self.assertEqual(self.get_tokens('source file.txt'), [ + (Keyword, 'source'), (String, 'file.txt') + ]) + + def test_source_with_spaces(self): + self.assertEqual(self.get_tokens(' source file.txt '), [ + (Keyword, 'source'), (String, 'file.txt') + ]) + + def test_source_quoted_filename(self): + self.assertEqual(self.get_tokens("source '/tmp/my file.txt'"), [ + (Keyword, 'source'), + (Text, "'"), (String, '/tmp/my file.txt'), (Text, "'") + ]) + + def test_source_escaped_filename(self): + self.assertEqual(self.get_tokens(r"source /tmp/my\ file.txt"), [ + (Keyword, 'source'), (String, r'/tmp/my\ file.txt') + ]) + + +class TestLexer_exec(LexerTestCase): + + def test_exec_simple_filename(self): + self.assertEqual(self.get_tokens('exec file.txt'), [ + (Keyword, 'exec'), (String, 'file.txt') + ]) + + def test_exec_with_spaces(self): + self.assertEqual(self.get_tokens(' exec file.txt '), [ + (Keyword, 'exec'), (String, 'file.txt') + ]) + + def test_exec_quoted_filename(self): + self.assertEqual(self.get_tokens("exec '/tmp/my file.txt'"), [ + (Keyword, 'exec'), + (Text, "'"), (String, '/tmp/my file.txt'), (Text, "'") + ]) + + def test_exec_escaped_filename(self): + self.assertEqual(self.get_tokens(r"exec /tmp/my\ file.txt"), [ + (Keyword, 'exec'), (String, r'/tmp/my\ file.txt') + ]) + + +class TestLexer_exit(LexerTestCase): + + def test_exit_simple(self): + self.assertEqual(self.get_tokens('exit'), [ + (Keyword, 'exit') + ]) + + def test_exit_with_spaces(self): + self.assertEqual(self.get_tokens(' exit '), [ + (Keyword, 'exit') + ]) + + +class TestLexerPreview(LexerTestCase): + + def test_httpie_without_action(self): + cmd = 'httpie http://example.com name=jack' + self.assertEqual(self.get_tokens(cmd), [ + (Keyword, 'httpie'), + (String, 'http://example.com'), + (Name, 'name'), (Operator, '='), (String, 'jack') + ]) + + def test_httpie_without_action_and_url(self): + cmd = 'httpie name=jack Accept:*/*' + self.assertEqual(self.get_tokens(cmd), [ + (Keyword, 'httpie'), + (Name, 'name'), (Operator, '='), (String, 'jack'), + (Name, 'Accept'), (Operator, ':'), (String, '*/*') + ]) + + def test_httpie_absolute_url(self): + cmd = 'httpie post http://example.com name=jack' + self.assertEqual(self.get_tokens(cmd), [ + (Keyword, 'httpie'), (Keyword, 'post'), + (String, 'http://example.com'), + (Name, 'name'), (Operator, '='), (String, 'jack') + ]) + + def test_httpie_option_first(self): + self.assertEqual(self.get_tokens('httpie post --form name=jack'), [ + (Keyword, 'httpie'), (Keyword, 'post'), + (Name, '--form'), + (Name, 'name'), (Operator, '='), (String, 'jack') + ]) + + def test_httpie_body_param_first(self): + self.assertEqual(self.get_tokens('httpie post name=jack --form'), [ + (Keyword, 'httpie'), (Keyword, 'post'), + (Name, 'name'), (Operator, '='), (String, 'jack'), + (Name, '--form') + ]) + + def test_httpie_options(self): + self.assertEqual(self.get_tokens('httpie options test --body'), [ + (Keyword, 'httpie'), (Keyword, 'options'), + (String, 'test'), (Name, '--body') + ]) + + def test_httpie_relative_path(self): + tokens = self.get_tokens('httpie /api/test name==foo', + filter_spaces=False) + self.assertEqual(tokens, [ + (Keyword, 'httpie'), (Text, ' '), + (String, '/api/test'), (Text, ' '), + (Name, 'name'), (Operator, '=='), (String, 'foo'), + (Text, '\n') + ]) + + +class TestShellCode(LexerTestCase): + + def test_unquoted_querystring(self): + self.assertEqual(self.get_tokens('`echo name`==john'), [ + (Text, '`'), + (Name.Builtin, 'echo'), + (Text, 'name'), + (Text, '`'), + (Operator, '=='), + (String, 'john') + ]) + self.assertEqual(self.get_tokens('name==`echo john`'), [ + (Name, 'name'), + (Operator, '=='), + (Text, '`'), + (Name.Builtin, 'echo'), + (Text, 'john'), + (Text, '`') + ]) + + def test_unquoted_bodystring(self): + self.assertEqual(self.get_tokens('`echo name`=john'), [ + (Text, '`'), + (Name.Builtin, 'echo'), + (Text, 'name'), + (Text, '`'), + (Operator, '='), + (String, 'john') + ]) + self.assertEqual(self.get_tokens('name=`echo john`'), [ + (Name, 'name'), + (Operator, '='), + (Text, '`'), + (Name.Builtin, 'echo'), + (Text, 'john'), + (Text, '`') + ]) + + def test_header_option_value(self): + self.assertEqual(self.get_tokens('Accept:`echo "application/json"`'), [ + (Name, 'Accept'), + (Operator, ':'), + (Text, '`'), + (Name.Builtin, 'echo'), + (String.Double, '"application/json"'), + (Text, '`'), + ]) + + def test_httpie_body_param(self): + self.assertEqual(self.get_tokens('httpie post name=`echo john`'), [ + (Keyword, 'httpie'), + (Keyword, 'post'), + (Name, 'name'), + (Operator, '='), + (Text, '`'), + (Name.Builtin, 'echo'), + (Text, 'john'), + (Text, '`'), + ]) + + def test_httpie_post_pipe(self): + self.assertEqual(self.get_tokens('httpie post | tee "/tmp/test"'), [ + (Keyword, 'httpie'), + (Keyword, 'post'), + (Operator, '|'), + (Text, 'tee'), + (String.Double, '"/tmp/test"'), + ]) + + def test_post_pipe(self): + self.assertEqual(self.get_tokens('post | tee "/tmp/test"'), [ + (Keyword, 'post'), + (Operator, '|'), + (Text, 'tee'), + (String.Double, '"/tmp/test"'), + ]) + + +class TestLexerPreviewRedirection(LexerTestCase): + + def test_httpie_write(self): + self.assertEqual(self.get_tokens('httpie > file.txt'), [ + (Keyword, 'httpie'), + (Operator, '>'), (String, 'file.txt') + ]) + + def test_httpie_write_without_spaces(self): + self.assertEqual(self.get_tokens('httpie>file.txt'), [ + (Keyword, 'httpie'), + (Operator, '>'), (String, 'file.txt') + ]) + + def test_httpie_append(self): + self.assertEqual(self.get_tokens('httpie >> file.txt'), [ + (Keyword, 'httpie'), + (Operator, '>>'), (String, 'file.txt') + ]) + + def test_httpie_append_without_spaces(self): + self.assertEqual(self.get_tokens('httpie>>file.txt'), [ + (Keyword, 'httpie'), + (Operator, '>>'), (String, 'file.txt') + ]) + + def test_httpie_write_with_post_param(self): + self.assertEqual(self.get_tokens('httpie post name=jack > file.txt'), [ + (Keyword, 'httpie'), (Keyword, 'post'), + (Name, 'name'), (Operator, '='), (String, 'jack'), + (Operator, '>'), (String, 'file.txt') + ]) + + def test_httpie_append_with_post_param(self): + self.assertEqual(self.get_tokens('httpie post name=doe >> file.txt'), [ + (Keyword, 'httpie'), (Keyword, 'post'), + (Name, 'name'), (Operator, '='), (String, 'doe'), + (Operator, '>>'), (String, 'file.txt') + ]) + + def test_httpie_write_quoted_filename(self): + self.assertEqual(self.get_tokens("httpie > 'my file.txt'"), [ + (Keyword, 'httpie'), (Operator, '>'), + (Text, "'"), (String, 'my file.txt'), (Text, "'") + ]) + + def test_httpie_append_quoted_filename(self): + self.assertEqual(self.get_tokens('httpie >> "my file.txt"'), [ + (Keyword, 'httpie'), (Operator, '>>'), + (Text, '"'), (String, 'my file.txt'), (Text, '"') + ]) + + def test_httpie_append_with_many_params(self): + command = ("httpie post --auth user:pass --verify=no " + "name='john doe' page==2 >> file.txt") + self.assertEqual(self.get_tokens(command), [ + (Keyword, 'httpie'), (Keyword, 'post'), + (Name, '--auth'), (String, 'user:pass'), + (Name, '--verify'), (Operator, '='), (String, 'no'), + (Name, 'name'), (Operator, '='), + (Text, "'"), (String, 'john doe'), (Text, "'"), + (Name, 'page'), (Operator, '=='), (String, '2'), + (Operator, '>>'), (String, 'file.txt') + ]) + + def test_curl_write(self): + self.assertEqual(self.get_tokens('curl > file.txt'), [ + (Keyword, 'curl'), + (Operator, '>'), (String, 'file.txt') + ]) + + def test_curl_write_without_spaces(self): + self.assertEqual(self.get_tokens('curl>file.txt'), [ + (Keyword, 'curl'), + (Operator, '>'), (String, 'file.txt') + ]) + + def test_curl_append(self): + self.assertEqual(self.get_tokens('curl >> file.txt'), [ + (Keyword, 'curl'), + (Operator, '>>'), (String, 'file.txt') + ]) + + def test_curl_append_without_spaces(self): + self.assertEqual(self.get_tokens('curl>>file.txt'), [ + (Keyword, 'curl'), + (Operator, '>>'), (String, 'file.txt') + ]) + + def test_curl_write_with_post_param(self): + self.assertEqual(self.get_tokens('curl post name=jack > file.txt'), [ + (Keyword, 'curl'), (Keyword, 'post'), + (Name, 'name'), (Operator, '='), (String, 'jack'), + (Operator, '>'), (String, 'file.txt') + ]) + + def test_curl_append_with_post_param(self): + self.assertEqual(self.get_tokens('curl post name=doe >> file.txt'), [ + (Keyword, 'curl'), (Keyword, 'post'), + (Name, 'name'), (Operator, '='), (String, 'doe'), + (Operator, '>>'), (String, 'file.txt') + ]) + + def test_curl_write_quoted_filename(self): + self.assertEqual(self.get_tokens("curl > 'my file.txt'"), [ + (Keyword, 'curl'), (Operator, '>'), + (Text, "'"), (String, 'my file.txt'), (Text, "'") + ]) + + def test_curl_append_quoted_filename(self): + self.assertEqual(self.get_tokens('curl >> "my file.txt"'), [ + (Keyword, 'curl'), (Operator, '>>'), + (Text, '"'), (String, 'my file.txt'), (Text, '"') + ]) + + def test_curl_append_with_many_params(self): + command = ("curl post --auth user:pass --verify=no " + "name='john doe' page==2 >> file.txt") + self.assertEqual(self.get_tokens(command), [ + (Keyword, 'curl'), (Keyword, 'post'), + (Name, '--auth'), (String, 'user:pass'), + (Name, '--verify'), (Operator, '='), (String, 'no'), + (Name, 'name'), (Operator, '='), + (Text, "'"), (String, 'john doe'), (Text, "'"), + (Name, 'page'), (Operator, '=='), (String, '2'), + (Operator, '>>'), (String, 'file.txt') + ]) + + +class TestLexerAction(LexerTestCase): + + def test_get(self): + self.assertEqual(self.get_tokens('get'), [ + (Keyword, 'get') + ]) + + def test_post_with_spaces(self): + self.assertEqual(self.get_tokens(' post '), [ + (Keyword, 'post') + ]) + + def test_capital_head(self): + self.assertEqual(self.get_tokens('HEAD'), [ + (Keyword, 'HEAD') + ]) + + def test_delete_random_capitals(self): + self.assertEqual(self.get_tokens('dElETe'), [ + (Keyword, 'dElETe') + ]) + + def test_patch(self): + self.assertEqual(self.get_tokens('patch'), [ + (Keyword, 'patch') + ]) + + def test_get_with_querystring_params(self): + command = 'get page==10 id==200' + self.assertEqual(self.get_tokens(command), [ + (Keyword, 'get'), + (Name, 'page'), (Operator, '=='), (String, '10'), + (Name, 'id'), (Operator, '=='), (String, '200') + ]) + + def test_capital_get_with_querystring_params(self): + command = 'GET page==10 id==200' + self.assertEqual(self.get_tokens(command), [ + (Keyword, 'GET'), + (Name, 'page'), (Operator, '=='), (String, '10'), + (Name, 'id'), (Operator, '=='), (String, '200') + ]) + + def test_post_with_body_params(self): + command = 'post name="john doe" username=john' + self.assertEqual(self.get_tokens(command), [ + (Keyword, 'post'), (Name, 'name'), (Operator, '='), + (Text, '"'), (String, 'john doe'), (Text, '"'), + (Name, 'username'), (Operator, '='), (String, 'john') + ]) + + def test_post_with_spaces_and_body_params(self): + command = ' post name="john doe" username=john ' + self.assertEqual(self.get_tokens(command), [ + (Keyword, 'post'), (Name, 'name'), (Operator, '='), + (Text, '"'), (String, 'john doe'), (Text, '"'), + (Name, 'username'), (Operator, '='), (String, 'john') + ]) + + def test_options(self): + self.assertEqual(self.get_tokens('options'), [ + (Keyword, 'options') + ]) + + def test_post_relative_path(self): + tokens = self.get_tokens('post /api/test name=foo', + filter_spaces=False) + self.assertEqual(tokens, [ + (Keyword, 'post'), (Text, ' '), + (String, '/api/test'), (Text, ' '), + (Name, 'name'), (Operator, '='), (String, 'foo'), + (Text, '\n') + ]) + + +class TestLexerActionRedirection(LexerTestCase): + + def test_get_write(self): + self.assertEqual(self.get_tokens('get > file.txt'), [ + (Keyword, 'get'), (Operator, '>'), (String, 'file.txt') + ]) + + def test_get_write_quoted_filename(self): + self.assertEqual(self.get_tokens('get > "/tmp/my file.txt"'), [ + (Keyword, 'get'), (Operator, '>'), + (Text, '"'), (String, '/tmp/my file.txt'), (Text, '"') + ]) + + def test_get_append(self): + self.assertEqual(self.get_tokens('get >> file.txt'), [ + (Keyword, 'get'), (Operator, '>>'), (String, 'file.txt') + ]) + + def test_get_append_escaped_filename(self): + self.assertEqual(self.get_tokens(r'get >> /tmp/my\ file.txt'), [ + (Keyword, 'get'), (Operator, '>>'), + (String, r'/tmp/my\ file.txt') + ]) + + def test_post_append_with_spaces(self): + self.assertEqual(self.get_tokens(' post >> file.txt'), [ + (Keyword, 'post'), (Operator, '>>'), (String, 'file.txt') + ]) + + def test_capital_head_write(self): + self.assertEqual(self.get_tokens('HEAD > file.txt'), [ + (Keyword, 'HEAD'), (Operator, '>'), (String, 'file.txt') + ]) + + def test_get_append_with_querystring_params(self): + command = 'get page==10 id==200 >> /tmp/file.txt' + self.assertEqual(self.get_tokens(command), [ + (Keyword, 'get'), + (Name, 'page'), (Operator, '=='), (String, '10'), + (Name, 'id'), (Operator, '=='), (String, '200'), + (Operator, '>>'), (String, '/tmp/file.txt') + ]) + + def test_post_write_escaped_filename_with_body_params(self): + command = r'post name="john doe" username=john > /tmp/my\ file.txt' + self.assertEqual(self.get_tokens(command), [ + (Keyword, 'post'), (Name, 'name'), (Operator, '='), + (Text, '"'), (String, 'john doe'), (Text, '"'), + (Name, 'username'), (Operator, '='), (String, 'john'), + (Operator, '>'), (String, r'/tmp/my\ file.txt') + ]) + + def test_post_append_with_spaces_and_body_params(self): + command = ' post name="john doe" username=john >> /tmp/file.txt ' + self.assertEqual(self.get_tokens(command), [ + (Keyword, 'post'), (Name, 'name'), (Operator, '='), + (Text, '"'), (String, 'john doe'), (Text, '"'), + (Name, 'username'), (Operator, '='), (String, 'john'), + (Operator, '>>'), (String, '/tmp/file.txt') + ]) diff --git a/tests/prompt/test_tree.py b/tests/prompt/test_tree.py new file mode 100644 index 00000000..5b50677a --- /dev/null +++ b/tests/prompt/test_tree.py @@ -0,0 +1,131 @@ +import unittest + +from httpie.prompt.tree import Node + + +class TestNode(unittest.TestCase): + + def setUp(self): + # Make a tree like this: + # root + # a h + # b d i n + # c f e g k o + # l m p + self.root = Node('root') + self.root.add_path('a', 'b', 'c') + self.root.add_path('a', 'b', 'f') + self.root.add_path('a', 'd', 'e') + self.root.add_path('a', 'd', 'g') + self.root.add_path('h', 'i', 'k', 'l') + self.root.add_path('h', 'i', 'k', 'm') + self.root.add_path('h', 'i', 'k', 'p') + self.root.add_path('h', 'n', 'o') + + def test_illegal_name(self): + self.assertRaises(ValueError, Node, '.') + self.assertRaises(ValueError, Node, '..') + + def test_str(self): + node = Node('my node') + self.assertEqual(str(node), 'my node') + + def test_cmp_same_type(self): + a = Node('a', data={'type': 'dir'}) + b = Node('b', data={'type': 'dir'}) + self.assertTrue(a < b) + + def test_cmp_different_type(self): + a = Node('a', data={'type': 'file'}) + b = Node('b', data={'type': 'dir'}) + self.assertTrue(b < a) + + def test_eq(self): + a = Node('a', data={'type': 'file'}) + b = Node('b', data={'type': 'dir'}) + self.assertNotEqual(a, b) + + a = Node('a', data={'type': 'file'}) + b = Node('a', data={'type': 'file'}) + self.assertEqual(a, b) + + def test_add_path_and_find_child(self): + # Level 1 (root) + self.assertEqual(set(c.name for c in self.root.children), set('ah')) + + # Level 2 + node_a = self.root.find_child('a') + node_h = self.root.find_child('h') + self.assertEqual(set(c.name for c in node_a.children), set('bd')) + self.assertEqual(set(c.name for c in node_h.children), set('in')) + + # Level 3 + node_b = node_a.find_child('b') + node_i = node_h.find_child('i') + self.assertEqual(set(c.name for c in node_b.children), set('cf')) + self.assertEqual(set(c.name for c in node_i.children), set('k')) + + # Level 4 + node_c = node_b.find_child('c') + node_k = node_i.find_child('k') + self.assertEqual(set(c.name for c in node_c.children), set()) + self.assertEqual(set(c.name for c in node_k.children), set('lmp')) + + # Return None if child can't be found + self.assertFalse(node_c.find_child('x')) + + def test_find_child_wildcard(self): + root = Node('root') + root.add_path('a') + root.add_path('{b}') + root.add_path('c') + + self.assertEqual(root.find_child('a').name, 'a') + self.assertEqual(root.find_child('c').name, 'c') + self.assertEqual(root.find_child('x').name, '{b}') + self.assertFalse(root.find_child('x', wildcard=False)) + + def test_ls(self): + self.assertEqual([n.name for n in self.root.ls('a')], list('bd')) + self.assertEqual([n.name for n in self.root.ls('a', 'b')], list('cf')) + self.assertEqual([n.name for n in self.root.ls('a', 'b', 'c')], []) + self.assertEqual([n.name for n in self.root.ls('h', 'i', 'k')], + list('lmp')) + + def test_ls_root(self): + self.assertEqual([n.name for n in self.root.ls()], list('ah')) + + def test_ls_non_existing(self): + self.assertEqual([n.name for n in self.root.ls('x')], []) + self.assertEqual([n.name for n in self.root.ls('a', 'b', 'x')], []) + + def test_ls_parent(self): + self.assertEqual([n.name for n in self.root.ls('..')], list('ah')) + self.assertEqual([n.name for n in self.root.ls('..', '..', '..')], + list('ah')) + self.assertEqual([n.name for n in self.root.ls('..', '..', 'h')], + list('in')) + self.assertEqual( + [n.name for n in self.root.ls('..', '..', 'h', '..', 'a')], + list('bd')) + + def test_ls_dot(self): + self.assertEqual([n.name for n in self.root.ls('.')], list('ah')) + self.assertEqual([n.name for n in self.root.ls('.', '.', '.')], + list('ah')) + self.assertEqual([n.name for n in self.root.ls('.', 'a', 'b')], + list('cf')) + self.assertEqual([n.name for n in self.root.ls('.', 'h', '.')], + list('in')) + self.assertEqual( + [n.name for n in self.root.ls('.', 'h', '.', '.', 'n')], ['o']) + + def test_ls_sort_by_types(self): + self.root.add_path('q', 'r') + self.root.add_path('q', 's', node_type='file') + self.root.add_path('q', 't', node_type='file') + self.root.add_path('q', 'u') + self.root.add_path('q', 'v', node_type='file') + + self.assertEqual([n.name for n in self.root.ls('q')], + list('rustv')) diff --git a/tests/prompt/test_utils.py b/tests/prompt/test_utils.py new file mode 100644 index 00000000..3741222c --- /dev/null +++ b/tests/prompt/test_utils.py @@ -0,0 +1,92 @@ +from httpie.prompt import utils + + +def test_colformat_zero_items(): + assert list(utils.colformat([], terminal_width=80)) == [] + + +def test_colformat_one_item(): + assert list(utils.colformat(['hello'], terminal_width=80)) == ['hello'] + + +def test_colformat_single_line(): + items = ['hello', 'world', 'foo', 'bar'] + assert list(utils.colformat(items, terminal_width=80)) == [ + 'hello world foo bar' + ] + + +def test_colformat_single_column(): + items = ['chap1.txt', 'chap2.txt', 'chap3.txt', 'chap4.txt', + 'chap5.txt', 'chap6.txt', 'chap7.txt', 'chap8.txt'] + assert list(utils.colformat(items, terminal_width=10)) == [ + 'chap1.txt', 'chap2.txt', 'chap3.txt', 'chap4.txt', + 'chap5.txt', 'chap6.txt', 'chap7.txt', 'chap8.txt' + ] + + +def test_colformat_multi_columns_no_remainder(): + items = ['chap1.txt', 'chap2.txt', 'chap3.txt', 'chap4.txt', + 'chap5.txt', 'chap6.txt', 'chap7.txt', 'chap8.txt', + 'chap9.txt', 'chap10.txt', 'chap11.txt', 'chap12.txt'] + assert list(utils.colformat(items, terminal_width=50)) == [ + 'chap1.txt chap4.txt chap7.txt chap10.txt', + 'chap2.txt chap5.txt chap8.txt chap11.txt', + 'chap3.txt chap6.txt chap9.txt chap12.txt' + ] + + +def test_colformat_multi_columns_remainder_1(): + items = ['chap1.txt', 'chap2.txt', 'chap3.txt', 'chap4.txt', + 'chap5.txt', 'chap6.txt', 'chap7.txt', 'chap8.txt', + 'chap9.txt', 'chap10.txt', 'chap11.txt', 'chap12.txt', + 'chap13.txt'] + assert list(utils.colformat(items, terminal_width=50)) == [ + 'chap1.txt chap5.txt chap9.txt chap13.txt', + 'chap2.txt chap6.txt chap10.txt', + 'chap3.txt chap7.txt chap11.txt', + 'chap4.txt chap8.txt chap12.txt' + ] + + +def test_colformat_multi_columns_remainder_2(): + items = ['chap1.txt', 'chap2.txt', 'chap3.txt', 'chap4.txt', + 'chap5.txt', 'chap6.txt', 'chap7.txt', 'chap8.txt', + 'chap9.txt', 'chap10.txt', 'chap11.txt', 'chap12.txt', + 'chap13.txt', 'chap14.txt'] + assert list(utils.colformat(items, terminal_width=50)) == [ + 'chap1.txt chap5.txt chap9.txt chap13.txt', + 'chap2.txt chap6.txt chap10.txt chap14.txt', + 'chap3.txt chap7.txt chap11.txt', + 'chap4.txt chap8.txt chap12.txt' + ] + + +def test_colformat_wider_than_terminal(): + items = ['a very long long name', '1111 2222 3333 4444 5555'] + assert list(utils.colformat(items, terminal_width=10)) == [ + 'a very long long name', + '1111 2222 3333 4444 5555' + ] + + +def test_colformat_long_short_mixed(): + items = ['a', '1122334455667788', 'hello world', 'foo bar', + 'b', '8877665544332211', 'abcd', 'yeah'] + assert list(utils.colformat(items, terminal_width=50)) == [ + 'a foo bar abcd', + '1122334455667788 b yeah', + 'hello world 8877665544332211' + ] + + +def test_colformat_github_top_endpoints(): + items = ['emojis', 'events', 'feeds', 'gists', 'gitignore', 'issues', + 'legacy', 'markdown', 'meta', 'networks', 'notifications', + 'orgs', 'rate_limit', 'repos', 'repositories', 'search', + 'teams', 'user', 'users'] + assert list(utils.colformat(items, terminal_width=136)) == [ + 'emojis gists legacy networks rate_limit'' search users', # noqa + 'events gitignore markdown notifications repos teams', # noqa + 'feeds issues meta orgs repositories user' # noqa + ] diff --git a/tests/prompt/test_xdg.py b/tests/prompt/test_xdg.py new file mode 100644 index 00000000..7b3e2884 --- /dev/null +++ b/tests/prompt/test_xdg.py @@ -0,0 +1,59 @@ +import os +import stat +import sys + +from .base import TempAppDirTestCase +from httpie.prompt import xdg + + +class TestXDG(TempAppDirTestCase): + + def test_get_app_data_home(self): + path = xdg.get_data_dir() + expected_path = os.path.join(os.environ[self.homes['data']], + 'http-prompt') + self.assertEqual(path, expected_path) + self.assertTrue(os.path.exists(path)) + + if sys.platform != 'win32': + # Make sure permission for the directory is 700 + mask = stat.S_IMODE(os.stat(path).st_mode) + self.assertTrue(mask & stat.S_IRWXU) + self.assertFalse(mask & stat.S_IRWXG) + self.assertFalse(mask & stat.S_IRWXO) + + def test_get_app_config_home(self): + path = xdg.get_config_dir() + expected_path = os.path.join(os.environ[self.homes['config']], + 'http-prompt') + self.assertEqual(path, expected_path) + self.assertTrue(os.path.exists(path)) + + if sys.platform != 'win32': + # Make sure permission for the directory is 700 + mask = stat.S_IMODE(os.stat(path).st_mode) + self.assertTrue(mask & stat.S_IRWXU) + self.assertFalse(mask & stat.S_IRWXG) + self.assertFalse(mask & stat.S_IRWXO) + + def test_get_resource_data_dir(self): + path = xdg.get_data_dir('something') + expected_path = os.path.join( + os.environ[self.homes['data']], 'http-prompt', 'something') + self.assertEqual(path, expected_path) + self.assertTrue(os.path.exists(path)) + + # Make sure we can write a file to the directory + with open(os.path.join(path, 'test'), 'wb') as f: + f.write(b'hello') + + def test_get_resource_config_dir(self): + path = xdg.get_config_dir('something') + expected_path = os.path.join( + os.environ[self.homes['config']], 'http-prompt', 'something') + self.assertEqual(path, expected_path) + self.assertTrue(os.path.exists(path)) + + # Make sure we can write a file to the directory + with open(os.path.join(path, 'test'), 'wb') as f: + f.write(b'hello') diff --git a/tests/prompt/utils.py b/tests/prompt/utils.py new file mode 100644 index 00000000..f7c872e5 --- /dev/null +++ b/tests/prompt/utils.py @@ -0,0 +1,22 @@ +import os +import sys + + +def get_http_prompt_path(): + """Get the path to http-prompt executable.""" + python_dir = os.path.dirname(sys.executable) + bin_name = 'http-prompt' + if sys.platform == 'win32': + bin_name += '.exe' + + paths = [ + os.path.join(python_dir, bin_name), + os.path.join(python_dir, 'Scripts', bin_name), # Windows + '/usr/bin/http-prompt' # Homebrew installation + ] + for path in paths: + if os.path.exists(path): + return path + + raise OSError("could not locate http-prompt executable, " + "Python directory: %s" % python_dir)