# coding=utf-8 """CLI subcommands test cases.""" import json import textwrap from collections import OrderedDict import pytest from click import Context from click.testing import CliRunner from mock import patch from requests.exceptions import RequestException from six import StringIO from greynoise.__version__ import __version__ from greynoise.cli import main, subcommand from greynoise.exceptions import RequestFailure from greynoise.util import CONFIG_FILE, DEFAULT_CONFIG @pytest.fixture def api_client(): load_config_patcher = patch("greynoise.cli.decorator.load_config") api_client_cls_patcher = patch("greynoise.cli.decorator.GreyNoise") with load_config_patcher as load_config: load_config.return_value = { "api_key": "<api_key>", "api_server": "<api_server>", "timeout": DEFAULT_CONFIG["timeout"], } with api_client_cls_patcher as api_client_cls: api_client = api_client_cls() yield api_client class TestAccount(object): """Account subcommand test cases.""" def test_not_implemented(self, api_client): """Not implemented error message returned.""" runner = CliRunner() expected_output = "Error: 'account' subcommand is not implemented yet.\n" api_client.not_implemented.side_effect = RequestFailure(501) result = runner.invoke(subcommand.account) api_client.not_implemented.assert_called_with("account") assert result.exit_code == 1 assert result.output == expected_output class TestAlerts(object): """Alerts subcommand test cases.""" def test_not_implemented(self, api_client): """Not implemented error message returned.""" runner = CliRunner() expected_output = "Error: 'alerts' subcommand is not implemented yet.\n" api_client.not_implemented.side_effect = RequestFailure(501) result = runner.invoke(subcommand.alerts) api_client.not_implemented.assert_called_with("alerts") assert result.exit_code == 1 assert result.output == expected_output class TestAnalyze(object): """Analyze subcommand test cases.""" DEFAULT_API_RESPONSE = { "query": ["<ip_address_1>", "<ip_address_2>"], "count": 0, "stats": {}, "summary": { "ip_count": 0, "noise_ip_count": 0, "not_noise_ip_count": 0, "noise_ip_ratio": 0, }, } DEFAULT_OUTPUT = textwrap.dedent( u"""\ ╔═══════════════════════════╗ ║ Analyze ║ ╚═══════════════════════════╝ Summary: - IP count: 0 - Noise IP count: 0 - Not noise IP count: 0 - Noise IP ratio: 0.00 Queries: - <ip_address_1> - <ip_address_2> No results found for this query. """ ) @pytest.mark.parametrize( "expected_output", [ ( "Error: at least one text file must be passed " "either through the -i/--input_file option or through a shell pipe." ), ], ) def test_no_input_file(self, api_client, expected_output): """No input text passed.""" runner = CliRunner() api_client.analyze.return_value = expected_output with patch("greynoise.cli.subcommand.sys") as sys: sys.stdin.isatty.return_value = True result = runner.invoke(subcommand.analyze) assert result.exit_code == -1 assert expected_output in result.output api_client.analyze.assert_not_called() @pytest.mark.parametrize("text", ["<input_text>"]) def test_input_file(self, api_client, text): """Analyze text with IP addresses from file.""" runner = CliRunner() input_text = StringIO(text) api_client.analyze.return_value = self.DEFAULT_API_RESPONSE result = runner.invoke(subcommand.analyze, ["-i", input_text]) assert result.exit_code == 0 assert result.output == self.DEFAULT_OUTPUT api_client.analyze.assert_called_with(input_text) @pytest.mark.parametrize("text", ["<input_text>"]) def test_stdin_input(self, api_client, text): """Analyze text with IP addresses from stdin.""" runner = CliRunner() api_client.analyze.return_value = self.DEFAULT_API_RESPONSE result = runner.invoke(subcommand.analyze, input=text) assert result.exit_code == 0 assert result.output == self.DEFAULT_OUTPUT assert api_client.analyze.call_args[0][0].read() == text @pytest.mark.parametrize("text", ["<input_text>"]) def test_explicit_stdin_input(self, api_client, text): """Analyze text with IP addresses from stdin passed explicitly.""" runner = CliRunner() api_client.analyze.return_value = self.DEFAULT_API_RESPONSE result = runner.invoke(subcommand.analyze, ["-i", "-"], input=text) assert result.exit_code == 0 assert result.output == self.DEFAULT_OUTPUT assert api_client.analyze.call_args[0][0].read() == text def test_requests_exception(self, api_client): """Error is displayed on requests library exception.""" runner = CliRunner() expected = "API error: <error message>\n" api_client.analyze.side_effect = RequestException("<error message>") result = runner.invoke(subcommand.analyze, input="some text") assert result.exit_code == -1 assert result.output == expected def test_api_key_not_found(self): """Error is displayed if API key is not found.""" runner = CliRunner() with patch("greynoise.cli.decorator.load_config") as load_config: load_config.return_value = {"api_key": ""} result = runner.invoke( subcommand.analyze, input="some text", parent=Context(main, info_name="greynoise"), ) assert result.exit_code == -1 assert "Error: API key not found" in result.output class TestFeedback(object): """Feedback subcommand test cases.""" def test_not_implemented(self, api_client): """Not implemented error message returned.""" runner = CliRunner() expected_output = "Error: 'feedback' subcommand is not implemented yet.\n" api_client.not_implemented.side_effect = RequestFailure(501) result = runner.invoke(subcommand.feedback) api_client.not_implemented.assert_called_with("feedback") assert result.exit_code == 1 assert result.output == expected_output class TestFilter(object): """Filter subcommand test cases.""" @pytest.mark.parametrize( "expected_output", [ ( "Error: at least one text file must be passed " "either through the -i/--input_file option or through a shell pipe." ), ], ) def test_no_input_file(self, api_client, expected_output): """No input text passed.""" runner = CliRunner() api_client.filter.return_value = expected_output with patch("greynoise.cli.subcommand.sys") as sys: sys.stdin.isatty.return_value = True result = runner.invoke(subcommand.filter) assert result.exit_code == -1 assert expected_output in result.output api_client.filter.assert_not_called() @pytest.mark.parametrize( "text, expected_output", [ ("<input_text>", "<output_text>"), ("<input_text>", ("<chunk_1>\n", "<chunk_2>\n")), ], ) def test_input_file(self, api_client, text, expected_output): """Filter text with IP addresses from file.""" runner = CliRunner() input_text = StringIO(text) api_client.filter.return_value = expected_output result = runner.invoke(subcommand.filter, ["-i", input_text]) assert result.exit_code == 0 assert result.output == "".join(expected_output) api_client.filter.assert_called_with(input_text, noise_only=False) @pytest.mark.parametrize( "text, expected_output", [ ("<input_text>", "<output_text>"), ("<input_text>", ("<chunk_1>\n", "<chunk_2>\n")), ], ) def test_stdin_input(self, api_client, text, expected_output): """Filter text with IP addresses from stdin.""" runner = CliRunner() api_client.filter.return_value = expected_output result = runner.invoke(subcommand.filter, input=text) assert result.exit_code == 0 assert result.output == "".join(expected_output) assert api_client.filter.call_args[0][0].read() == text assert api_client.filter.call_args[1] == {"noise_only": False} @pytest.mark.parametrize( "text, expected_output", [ ("<input_text>", "<output_text>"), ("<input_text>", ("<chunk_1>\n", "<chunk_2>\n")), ], ) def test_noise_only(self, api_client, text, expected_output): """Filter text with IP addresses from stdin using noise only flag.""" runner = CliRunner() api_client.filter.return_value = expected_output result = runner.invoke(subcommand.filter, ["--noise-only"], input=text) assert result.exit_code == 0 assert result.output == "".join(expected_output) assert api_client.filter.call_args[0][0].read() == text assert api_client.filter.call_args[1] == {"noise_only": True} @pytest.mark.parametrize( "text, expected_output", [ ("<input_text>", "<output_text>"), ("<input_text>", ("<chunk_1>\n", "<chunk_2>\n")), ], ) def test_explicit_stdin_input(self, api_client, text, expected_output): """Filter text with IP addresses from stdin passed explicitly.""" runner = CliRunner() api_client.filter.return_value = expected_output result = runner.invoke(subcommand.filter, ["-i", "-"], input=text) assert result.exit_code == 0 assert result.output == "".join(expected_output) assert api_client.filter.call_args[0][0].read() == text assert api_client.filter.call_args[1] == {"noise_only": False} def test_request_failure(self, api_client): """Error is displayed on API request failure.""" runner = CliRunner() api_client.filter.side_effect = RequestFailure( 401, {"error": "forbidden", "status": "error"} ) expected = "API error: forbidden\n" result = runner.invoke(subcommand.filter, input="some text") assert result.exit_code == -1 assert result.output == expected def test_requests_exception(self, api_client): """Error is displayed on requests library exception.""" runner = CliRunner() expected = "API error: <error message>\n" api_client.filter.side_effect = RequestException("<error message>") result = runner.invoke(subcommand.filter, input="some text") assert result.exit_code == -1 assert result.output == expected def test_api_key_not_found(self): """Error is displayed if API key is not found.""" runner = CliRunner() with patch("greynoise.cli.decorator.load_config") as load_config: load_config.return_value = {"api_key": ""} result = runner.invoke( subcommand.filter, input="some text", parent=Context(main, info_name="greynoise"), ) assert result.exit_code == -1 assert "Error: API key not found" in result.output class TestHelp(object): """Help subcommand test cases.""" def test_help(self): """Get help.""" runner = CliRunner() expected_output = "Usage: greynoise [OPTIONS] COMMAND [ARGS]..." result = runner.invoke( subcommand.help_, parent=Context(main, info_name="greynoise") ) assert result.exit_code == 0 assert expected_output in result.output class TestInteresting(object): """Interesting subcommand test cases.""" @pytest.mark.parametrize("ip_address, expected_response", [("0.0.0.0", {})]) def test_interesting(self, api_client, ip_address, expected_response): """Report IP address as "interesting".""" runner = CliRunner() api_client.interesting.return_value = expected_response result = runner.invoke(subcommand.interesting, [ip_address]) assert result.exit_code == 0 assert result.output == "" api_client.interesting.assert_called_with(ip_address=ip_address) @pytest.mark.parametrize("ip_address, expected_response", [("0.0.0.0", {})]) def test_input_file(self, api_client, ip_address, expected_response): """Report IP address as "interesting" from input file.""" runner = CliRunner() api_client.interesting.return_value = expected_response result = runner.invoke(subcommand.interesting, ["-i", StringIO(ip_address)]) assert result.exit_code == 0 assert result.output == "" api_client.interesting.assert_called_with(ip_address=ip_address) @pytest.mark.parametrize("ip_address, expected_response", [("0.0.0.0", {})]) def test_stdin_input(self, api_client, ip_address, expected_response): """Report IP address as "interesting" from stdin.""" runner = CliRunner() api_client.interesting.return_value = expected_response result = runner.invoke(subcommand.interesting, input=ip_address) assert result.exit_code == 0 assert result.output == "" api_client.interesting.assert_called_with(ip_address=ip_address) def test_no_ip_address_passed(self, api_client): """Usage is returned if no IP address or input file is passed.""" runner = CliRunner() with patch("greynoise.cli.helper.sys") as sys: sys.stdin.isatty.return_value = True result = runner.invoke( subcommand.interesting, parent=Context(main, info_name="greynoise") ) assert result.exit_code == -1 assert "Usage: greynoise interesting" in result.output api_client.interesting.assert_not_called() def test_input_file_invalid_ip_addresses_passsed(self, api_client): """Error returned if only invalid IP addresses are passed in input file.""" runner = CliRunner() expected = ( "Error: at least one valid IP address must be passed either as an " "argument (IP_ADDRESS) or through the -i/--input_file option." ) result = runner.invoke( subcommand.interesting, ["-i", StringIO("not-an-ip")], parent=Context(main, info_name="greynoise"), ) assert result.exit_code == -1 assert "Usage: greynoise interesting" in result.output assert expected in result.output api_client.interesting.assert_not_called() def test_invalid_ip_address_as_argument(self, api_client): """Interesting subcommand fails when ip_address is invalid.""" runner = CliRunner() expected = 'Error: Invalid value for "[IP_ADDRESS]...": not-an-ip\n' result = runner.invoke(subcommand.interesting, ["not-an-ip"]) assert result.exit_code == 2 assert expected in result.output api_client.interesting.assert_not_called() def test_request_failure(self, api_client): """Error is displayed on API request failure.""" runner = CliRunner() api_client.interesting.side_effect = RequestFailure( 401, {"error": "forbidden", "status": "error"} ) expected = "API error: forbidden\n" result = runner.invoke(subcommand.interesting, ["0.0.0.0"]) assert result.exit_code == -1 assert result.output == expected def test_requests_exception(self, api_client): """Error is displayed on requests library exception.""" runner = CliRunner() expected = "API error: <error message>\n" api_client.interesting.side_effect = RequestException("<error message>") result = runner.invoke(subcommand.interesting, ["0.0.0.0"]) assert result.exit_code == -1 assert result.output == expected def test_api_key_not_found(self): """Error is displayed if API key is not found.""" runner = CliRunner() with patch("greynoise.cli.decorator.load_config") as load_config: load_config.return_value = {"api_key": ""} result = runner.invoke( subcommand.interesting, ["0.0.0.0"], parent=Context(main, info_name="greynoise"), ) assert result.exit_code == -1 assert "Error: API key not found" in result.output class TestIP(object): """IP subcommand tests.""" @pytest.mark.parametrize("ip_address, expected_response", [("0.0.0.0", {})]) def test_ip(self, api_client, ip_address, expected_response): """Get IP address information.""" runner = CliRunner() api_client.ip.return_value = expected_response result = runner.invoke(subcommand.ip, ["-f", "json", ip_address]) assert result.exit_code == 0 assert result.output.strip("\n") == json.dumps( [expected_response], indent=4, sort_keys=True ) api_client.ip.assert_called_with(ip_address=ip_address) @pytest.mark.parametrize("ip_address, expected_response", [("0.0.0.0", {})]) def test_input_file(self, api_client, ip_address, expected_response): """Get IP address information from input file.""" runner = CliRunner() api_client.ip.return_value = expected_response result = runner.invoke( subcommand.ip, ["-f", "json", "-i", StringIO(ip_address)] ) assert result.exit_code == 0 assert result.output.strip("\n") == json.dumps( [expected_response], indent=4, sort_keys=True ) api_client.ip.assert_called_with(ip_address=ip_address) @pytest.mark.parametrize("ip_address, expected_response", [("0.0.0.0", {})]) def test_stdin_input(self, api_client, ip_address, expected_response): """Get IP address information from stdin.""" runner = CliRunner() api_client.ip.return_value = expected_response result = runner.invoke(subcommand.ip, ["-f", "json"], input=ip_address) assert result.exit_code == 0 assert result.output.strip("\n") == json.dumps( [expected_response], indent=4, sort_keys=True ) api_client.ip.assert_called_with(ip_address=ip_address) def test_no_ip_address_passed(self, api_client): """Usage is returned if no IP address or input file is passed.""" runner = CliRunner() with patch("greynoise.cli.helper.sys") as sys: sys.stdin.isatty.return_value = True result = runner.invoke( subcommand.ip, parent=Context(main, info_name="greynoise") ) assert result.exit_code == -1 assert "Usage: greynoise ip" in result.output api_client.ip.assert_not_called() def test_input_file_invalid_ip_addresses_passsed(self, api_client): """Error returned if only invalid IP addresses are passed in input file.""" runner = CliRunner() expected = ( "Error: at least one valid IP address must be passed either as an " "argument (IP_ADDRESS) or through the -i/--input_file option." ) result = runner.invoke( subcommand.ip, ["-i", StringIO("not-an-ip")], parent=Context(main, info_name="greynoise"), ) assert result.exit_code == -1 assert "Usage: greynoise ip" in result.output assert expected in result.output api_client.ip.assert_not_called() def test_invalid_ip_address_as_argument(self, api_client): """IP subcommand fails when ip_address is invalid.""" runner = CliRunner() expected = 'Error: Invalid value for "[IP_ADDRESS]...": not-an-ip\n' result = runner.invoke(subcommand.ip, ["not-an-ip"]) assert result.exit_code == 2 assert expected in result.output api_client.ip.assert_not_called() def test_request_failure(self, api_client): """Error is displayed on API request failure.""" runner = CliRunner() api_client.ip.side_effect = RequestFailure( 401, {"error": "forbidden", "status": "error"} ) expected = "API error: forbidden\n" result = runner.invoke(subcommand.ip, ["0.0.0.0"]) assert result.exit_code == -1 assert result.output == expected def test_requests_exception(self, api_client): """Error is displayed on requests library exception.""" runner = CliRunner() expected = "API error: <error message>\n" api_client.ip.side_effect = RequestException("<error message>") result = runner.invoke(subcommand.ip, ["0.0.0.0"]) assert result.exit_code == -1 assert result.output == expected def test_api_key_not_found(self): """Error is displayed if API key is not found.""" runner = CliRunner() with patch("greynoise.cli.decorator.load_config") as load_config: load_config.return_value = {"api_key": ""} result = runner.invoke( subcommand.ip, ["0.0.0.0"], parent=Context(main, info_name="greynoise") ) assert result.exit_code == -1 assert "Error: API key not found" in result.output class TestPCAP(object): """PCAP subcommand test cases.""" def test_not_implemented(self, api_client): """Not implemented error message returned.""" runner = CliRunner() expected_output = "Error: 'pcap' subcommand is not implemented yet.\n" api_client.not_implemented.side_effect = RequestFailure(501) result = runner.invoke(subcommand.pcap) api_client.not_implemented.assert_called_with("pcap") assert result.exit_code == 1 assert result.output == expected_output class TestQuery(object): """"Query subcommand tests.""" def test_query(self, api_client): """Run query.""" runner = CliRunner() query = "<query>" api_client.query.return_value = [] expected = json.dumps([[]], indent=4, sort_keys=True) result = runner.invoke(subcommand.query, ["-f", "json", query]) assert result.exit_code == 0 assert result.output.strip("\n") == expected api_client.query.assert_called_with(query=query) def test_input_file(self, api_client): """Run query from input file.""" runner = CliRunner() query = "<query>" api_client.query.return_value = [] expected = json.dumps([[]], indent=4, sort_keys=True) result = runner.invoke(subcommand.query, ["-f", "json", "-i", StringIO(query)]) assert result.exit_code == 0 assert result.output.strip("\n") == expected api_client.query.assert_called_with(query=query) def test_stdin_input(self, api_client): """Run query from stdin.""" runner = CliRunner() query = "<query>" api_client.query.return_value = [] expected = json.dumps([[]], indent=4, sort_keys=True) result = runner.invoke(subcommand.query, ["-f", "json"], input=query) assert result.exit_code == 0 assert result.output.strip("\n") == expected api_client.query.assert_called_with(query=query) def test_no_query_passed(self, api_client): """Usage is returned if no query or input file is passed.""" runner = CliRunner() with patch("greynoise.cli.helper.sys") as sys: sys.stdin.isatty.return_value = True result = runner.invoke( subcommand.query, parent=Context(main, info_name="greynoise") ) assert result.exit_code == -1 assert "Usage: greynoise query" in result.output api_client.query.assert_not_called() def test_empty_input_file(self, api_client): """Error is returned if empty input fle is passed.""" runner = CliRunner() expected = ( "Error: at least one query must be passed either as an argument " "(QUERY) or through the -i/--input_file option." ) result = runner.invoke( subcommand.query, ["-i", StringIO()], parent=Context(main, info_name="greynoise"), ) assert result.exit_code == -1 assert "Usage: greynoise query" in result.output assert expected in result.output api_client.query.assert_not_called() def test_request_failure(self, api_client): """Error is displayed on API request failure.""" runner = CliRunner() api_client.query.side_effect = RequestFailure( 401, {"error": "forbidden", "status": "error"} ) expected = "API error: forbidden" result = runner.invoke(subcommand.query, ["<query>"]) assert result.exit_code == -1 assert expected in result.output def test_api_key_not_found(self): """Error is displayed if API key is not found.""" runner = CliRunner() with patch("greynoise.cli.decorator.load_config") as load_config: load_config.return_value = {"api_key": ""} result = runner.invoke( subcommand.query, ["<query>"], parent=Context(main, info_name="greynoise"), ) assert result.exit_code == -1 assert "Error: API key not found" in result.output class TestQuick(object): """Quick subcommand tests.""" @pytest.mark.parametrize( "ip_address, output_format, expected", ( ( "0.0.0.0", "json", json.dumps( [{"ip": "0.0.0.0", "noise": True}], indent=4, sort_keys=True ), ), ( "0.0.0.0", "xml", textwrap.dedent( """\ <?xml version="1.0" ?> <root> \t<item type="dict"> \t\t<ip type="str">0.0.0.0</ip> \t\t<noise type="bool">True</noise> \t</item> </root>""" ), ), ("0.0.0.0", "txt", "0.0.0.0 is classified as NOISE."), ), ) def test_quick(self, api_client, ip_address, output_format, expected): """Quickly check IP address.""" runner = CliRunner() api_client.quick.return_value = [ OrderedDict((("ip", ip_address), ("noise", True))) ] result = runner.invoke(subcommand.quick, ["-f", output_format, ip_address]) assert result.exit_code == 0 assert result.output.strip("\n") == expected api_client.quick.assert_called_with(ip_addresses=[ip_address]) @pytest.mark.parametrize( "ip_addresses, mock_response, expected", ( ( ["0.0.0.0", "0.0.0.1"], [ OrderedDict([("ip", "0.0.0.0"), ("noise", True)]), OrderedDict([("ip", "0.0.0.1"), ("noise", False)]), ], json.dumps( [ {"ip": "0.0.0.0", "noise": True}, {"ip": "0.0.0.1", "noise": False}, ], indent=4, sort_keys=True, ), ), ), ) def test_input_file(self, api_client, ip_addresses, mock_response, expected): """Quickly check IP address from input file.""" runner = CliRunner() api_client.quick.return_value = mock_response result = runner.invoke( subcommand.quick, ["-f", "json", "-i", StringIO("\n".join(ip_addresses))] ) assert result.exit_code == 0 assert result.output.strip("\n") == expected api_client.quick.assert_called_with(ip_addresses=ip_addresses) @pytest.mark.parametrize( "ip_addresses, mock_response, expected", ( ( ["0.0.0.0", "0.0.0.1"], [ OrderedDict([("ip", "0.0.0.0"), ("noise", True)]), OrderedDict([("ip", "0.0.0.1"), ("noise", False)]), ], json.dumps( [ {"ip": "0.0.0.0", "noise": True}, {"ip": "0.0.0.1", "noise": False}, ], indent=4, sort_keys=True, ), ), ), ) def test_stdin_input(self, api_client, ip_addresses, mock_response, expected): """Quickly check IP address from stdin.""" runner = CliRunner() api_client.quick.return_value = mock_response result = runner.invoke( subcommand.quick, ["-f", "json"], input="\n".join(ip_addresses) ) assert result.exit_code == 0 assert result.output.strip("\n") == expected api_client.quick.assert_called_with(ip_addresses=ip_addresses) def test_no_ip_address_passed(self, api_client): """Usage is returned if no IP address or input file is passed.""" runner = CliRunner() with patch("greynoise.cli.helper.sys") as sys: sys.stdin.isatty.return_value = True result = runner.invoke( subcommand.quick, parent=Context(main, info_name="greynoise") ) assert result.exit_code == -1 assert "Usage: greynoise quick" in result.output api_client.quick.assert_not_called() def test_input_file_invalid_ip_addresses_passsed(self, api_client): """Error returned if only invalid IP addresses are passed in input file.""" runner = CliRunner() expected = ( "Error: at least one valid IP address must be passed either as an " "argument (IP_ADDRESS) or through the -i/--input_file option." ) result = runner.invoke( subcommand.quick, ["-i", StringIO("not-an-ip")], parent=Context(main, info_name="greynoise"), ) assert result.exit_code == -1 assert "Usage: greynoise quick" in result.output assert expected in result.output api_client.quick.assert_not_called() def test_invalid_ip_address_as_argument(self, api_client): """Quick subcommand fails when ip_address is invalid.""" runner = CliRunner() expected = 'Error: Invalid value for "[IP_ADDRESS]...": not-an-ip\n' result = runner.invoke(subcommand.quick, ["not-an-ip"]) assert result.exit_code == 2 assert expected in result.output api_client.quick.assert_not_called() def test_request_failure(self, api_client): """Error is displayed on API request failure.""" runner = CliRunner() api_client.quick.side_effect = RequestFailure( 401, {"error": "forbidden", "status": "error"} ) expected = "API error: forbidden" result = runner.invoke(subcommand.quick, ["0.0.0.0"]) assert result.exit_code == -1 assert expected in result.output def test_api_key_not_found(self): """Error is displayed if API key is not found.""" runner = CliRunner() with patch("greynoise.cli.decorator.load_config") as load_config: load_config.return_value = {"api_key": ""} result = runner.invoke( subcommand.quick, ["0.0.0.0"], parent=Context(main, info_name="greynoise"), ) assert result.exit_code == -1 assert "Error: API key not found" in result.output class TestSignature(object): """Signature subcommand test cases.""" def test_not_implemented(self, api_client): """Not implemented error message returned.""" runner = CliRunner() expected_output = "Error: 'signature' subcommand is not implemented yet.\n" api_client.not_implemented.side_effect = RequestFailure(501) result = runner.invoke(subcommand.signature) api_client.not_implemented.assert_called_with("signature") assert result.exit_code == 1 assert result.output == expected_output class TestSetup(object): """Setup subcommand test cases.""" @pytest.mark.parametrize("key_option", ["-k", "--api-key"]) def test_save_api_key(self, key_option): """Save API key to configuration file.""" runner = CliRunner() api_key = "<api_key>" expected_config = { "api_key": api_key, "api_server": DEFAULT_CONFIG["api_server"], "timeout": DEFAULT_CONFIG["timeout"], } expected_output = "Configuration saved to {!r}\n".format(CONFIG_FILE) with patch("greynoise.cli.subcommand.save_config") as save_config: result = runner.invoke(subcommand.setup, [key_option, api_key]) assert result.exit_code == 0 assert result.output == expected_output save_config.assert_called_with(expected_config) @pytest.mark.parametrize("key_option", ["-k", "--api-key"]) @pytest.mark.parametrize("server_option", ["-s", "--api-server"]) @pytest.mark.parametrize("timeout_option", ["-t", "--timeout"]) def test_save_api_key_and_timeout(self, key_option, server_option, timeout_option): """Save API key and timeout to configuration file.""" runner = CliRunner() api_key = "<api_key>" api_server = "<api_server>" timeout = 123456 expected_config = { "api_key": api_key, "api_server": api_server, "timeout": timeout, } expected_output = "Configuration saved to {!r}\n".format(CONFIG_FILE) with patch("greynoise.cli.subcommand.save_config") as save_config: result = runner.invoke( subcommand.setup, [ key_option, api_key, server_option, api_server, timeout_option, timeout, ], ) assert result.exit_code == 0 assert result.output == expected_output save_config.assert_called_with(expected_config) def test_missing_api_key(self): """Setup fails when api_key is not passed.""" runner = CliRunner() expected_error = 'Error: Missing option "-k" / "--api-key"' result = runner.invoke(subcommand.setup, []) assert result.exit_code == 2 assert expected_error in result.output class TestStats(object): """"Stats subcommand tests.""" def test_stats(self, api_client): """Run stats query.""" runner = CliRunner() query = "<query>" api_client.stats.return_value = [] expected = json.dumps([[]], indent=4, sort_keys=True) result = runner.invoke(subcommand.stats, ["-f", "json", query]) assert result.exit_code == 0 assert result.output.strip("\n") == expected api_client.stats.assert_called_with(query=query) def test_input_file(self, api_client): """Run stats query from input file.""" runner = CliRunner() query = "<query>" api_client.stats.return_value = [] expected = json.dumps([[]], indent=4, sort_keys=True) result = runner.invoke(subcommand.stats, ["-f", "json", "-i", StringIO(query)]) assert result.exit_code == 0 assert result.output.strip("\n") == expected api_client.stats.assert_called_with(query=query) def test_stdin_input(self, api_client): """Run stats query from input file.""" runner = CliRunner() query = "<query>" api_client.stats.return_value = [] expected = json.dumps([[]], indent=4, sort_keys=True) result = runner.invoke(subcommand.stats, ["-f", "json"], input=query) assert result.exit_code == 0 assert result.output.strip("\n") == expected api_client.stats.assert_called_with(query=query) def test_no_query_passed(self, api_client): """Usage is returned if no query or input file is passed.""" runner = CliRunner() with patch("greynoise.cli.helper.sys") as sys: sys.stdin.isatty.return_value = True result = runner.invoke( subcommand.stats, parent=Context(main, info_name="greynoise") ) assert result.exit_code == -1 assert "Usage: greynoise stats" in result.output api_client.stats.assert_not_called() def test_empty_input_file(self, api_client): """Error is returned if empty input fle is passed.""" runner = CliRunner() expected = ( "Error: at least one query must be passed either as an argument " "(QUERY) or through the -i/--input_file option." ) result = runner.invoke( subcommand.stats, ["-i", StringIO()], parent=Context(main, info_name="greynoise"), ) assert result.exit_code == -1 assert "Usage: greynoise stats" in result.output assert expected in result.output api_client.query.assert_not_called() def test_request_failure(self, api_client): """Error is displayed on API request failure.""" runner = CliRunner() api_client.stats.side_effect = RequestFailure( 401, {"error": "forbidden", "status": "error"} ) expected = "API error: forbidden" result = runner.invoke(subcommand.stats, ["<query>"]) assert result.exit_code == -1 assert expected in result.output def test_api_key_not_found(self): """Error is displayed if API key is not found.""" runner = CliRunner() with patch("greynoise.cli.decorator.load_config") as load_config: load_config.return_value = {"api_key": ""} result = runner.invoke( subcommand.stats, ["query"], parent=Context(main, info_name="greynoise") ) assert result.exit_code == -1 assert "Error: API key not found" in result.output class TestVersion(object): """Version subcommand test cases.""" def test_version(self): """Version returned.""" runner = CliRunner() expected_output = "greynoise {}".format(__version__) result = runner.invoke(subcommand.version) assert result.exit_code == 0 assert result.output.startswith(expected_output)