# -*- coding: utf-8 -*- # # Copyright (C) 2018 Branko Majic # # This file is part of Gimmecert. # # Gimmecert is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # Gimmecert is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # Gimmecert. If not, see . # import argparse import sys import gimmecert.cli import gimmecert.decorators import pytest from unittest import mock def test_get_parser_returns_parser(): parser = gimmecert.cli.get_parser() assert isinstance(parser, argparse.ArgumentParser) @mock.patch('gimmecert.cli.get_parser') def test_main_invokes_get_parser(mock_get_parser, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() # Ignore system exit. Dirty hack to avoid mocking the default # function. We care only about whether the get_parser is invoked. try: gimmecert.cli.main() except SystemExit: pass mock_get_parser.assert_called_once_with() @mock.patch('gimmecert.cli.get_parser') def test_main_invokes_argument_parsing(mock_get_parser, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_parser = mock.Mock() mock_get_parser.return_value = mock_parser # Ignore system exit. Dirty hack to avoid mocking the default # function. We care only about whether the parsing of arguments # got called.x try: gimmecert.cli.main() except SystemExit: pass mock_parser.parse_args.assert_called_once_with() def test_cli_parser_has_description(): parser = gimmecert.cli.get_parser() assert parser.description def test_parser_sets_up_default_callback_function(): parser = gimmecert.cli.get_parser() assert callable(parser.get_default('func')) @mock.patch('gimmecert.cli.argparse.ArgumentParser.print_usage') def test_parser_default_callback_function_calls_print_usage(mock_print_usage): parser = gimmecert.cli.get_parser() func = parser.get_default('func') func(mock.Mock()) assert mock_print_usage.called @mock.patch('gimmecert.cli.get_parser') def test_main_invokes_parser_function(mock_get_parser, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_parser = mock.Mock() mock_args = mock.Mock() # Avoid throws of SystemExit exception. mock_args.func.return_value = gimmecert.commands.ExitCode.SUCCESS mock_parser.parse_args.return_value = mock_args mock_get_parser.return_value = mock_parser gimmecert.cli.main() mock_args.func.assert_called_once_with(mock_args) def test_parser_help_contains_examples(): parser = gimmecert.cli.get_parser() assert 'Examples' in parser.description def test_setup_help_subcommand_parser_registered(): registered_functions = gimmecert.decorators.get_subcommand_parser_setup_functions() assert gimmecert.cli.setup_help_subcommand_parser in registered_functions @mock.patch('gimmecert.cli.get_subcommand_parser_setup_functions') def test_get_parser_calls_setup_subcommand_parser_functions(mock_get_subcommand_parser_setup_functions): mock_setup1 = mock.Mock() mock_setup2 = mock.Mock() mock_get_subcommand_parser_setup_functions.return_value = [mock_setup1, mock_setup2] gimmecert.cli.get_parser() assert mock_setup1.called assert mock_setup2.called def test_setup_help_subcommand_parser_adds_parser(): mock_parser = mock.Mock() mock_subparsers = mock.Mock() gimmecert.cli.setup_help_subcommand_parser(mock_parser, mock_subparsers) assert mock_subparsers.add_parser.called def test_help_subcommand_returns_parser(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() subparser = gimmecert.cli.setup_help_subcommand_parser(parser, subparsers) assert isinstance(subparser, argparse.ArgumentParser) def test_help_subcommand_sets_function_callback(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() subparser = gimmecert.cli.setup_help_subcommand_parser(parser, subparsers) assert callable(subparser.get_default('func')) def test_setup_init_subcommand_parser_registered(): registered_functions = gimmecert.decorators.get_subcommand_parser_setup_functions() assert gimmecert.cli.setup_init_subcommand_parser in registered_functions def test_setup_init_subcommand_returns_parser(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() subparser = gimmecert.cli.setup_init_subcommand_parser(parser, subparsers) assert isinstance(subparser, argparse.ArgumentParser) def test_setup_init_subcommand_sets_function_callback(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() subparser = gimmecert.cli.setup_init_subcommand_parser(parser, subparsers) assert callable(subparser.get_default('func')) # List of valid CLI invocations to use in # test_parser_commands_and_options_are_available. # # Each element in this list should be a tuple where first element is # the command function (relative to CLI module) that should be mocked, # while second element is list of CLI arguments for invoking the # command from CLI. See test documentation for more details. VALID_CLI_INVOCATIONS = [ # help, no options ("gimmecert.cli.help", ["gimmecert", "help"]), # init, no options ("gimmecert.cli.init", ["gimmecert", "init"]), # init, CA base name long and short option ("gimmecert.cli.init", ["gimmecert", "init", "--ca-base-name", "My Project"]), ("gimmecert.cli.init", ["gimmecert", "init", "-b", "My Project"]), # init, CA hierarchy depth long and short option ("gimmecert.cli.init", ["gimmecert", "init", "--ca-hierarchy-depth", "3"]), ("gimmecert.cli.init", ["gimmecert", "init", "-d", "3"]), # server, no options ("gimmecert.cli.server", ["gimmecert", "server", "myserver"]), # server, multiple DNS names, no options ("gimmecert.cli.server", ["gimmecert", "server", "myserver", "myserver.example.com"]), ("gimmecert.cli.server", ["gimmecert", "server", "myserver", "myserver1.example.com", "myserver2.example.com", "myserver3.example.com", "myserver4.example.com"]), # server, update DNS names long and short option ("gimmecert.cli.server", ["gimmecert", "server", "--update-dns-names", "myserver"]), ("gimmecert.cli.server", ["gimmecert", "server", "-u", "myserver"]), # client, no options ("gimmecert.cli.client", ["gimmecert", "client", "myclient"]), # renew, no options ("gimmecert.cli.renew", ["gimmecert", "renew", "server", "myserver"]), ("gimmecert.cli.renew", ["gimmecert", "renew", "client", "myclient"]), # renew, generate new private key long and short option ("gimmecert.cli.renew", ["gimmecert", "renew", "--new-private-key", "server", "myserver"]), ("gimmecert.cli.renew", ["gimmecert", "renew", "--new-private-key", "client", "myclient"]), ("gimmecert.cli.renew", ["gimmecert", "renew", "-p", "server", "myserver"]), ("gimmecert.cli.renew", ["gimmecert", "renew", "-p", "client", "myclient"]), ] @pytest.mark.parametrize("command_function, cli_invocation", VALID_CLI_INVOCATIONS) def test_parser_commands_and_options_are_available(tmpdir, command_function, cli_invocation): """ Tests handling of valid CLI invocations by top-level and command parsers. This test helps greatly reduce duplication of code, at the expense of some flexibility. The passed-in command_function is mocked and set-up to return a success exit code, since the main point is to ensure the CLI supports specific commands and parameters. To add a new valid invocation of CLI, update the VALID_CLI_INVOCATIONS variable above. """ # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() with mock.patch(command_function) as mock_command_function, mock.patch('sys.argv', cli_invocation): mock_command_function.return_value = gimmecert.commands.ExitCode.SUCCESS gimmecert.cli.main() # Should not raise @pytest.mark.parametrize("command", ["help", "init", "server", "client", "renew"]) @pytest.mark.parametrize("help_option", ["--help", "-h"]) def test_command_exists_and_accepts_help_flag(tmpdir, command, help_option): """ Tests availability of commands and whether they accept the help flag. Test is parametrised in order to avoid code duplication. The only thing necessary to add a new command is to extend the command parameter. Both short and long form of help flag is tested. """ # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() with mock.patch('sys.argv', ['gimmecert', command, help_option]): with pytest.raises(SystemExit) as e_info: gimmecert.cli.main() assert e_info.value.code == 0 @mock.patch('sys.argv', ['gimmecert', 'init']) @mock.patch('gimmecert.cli.init') def test_init_command_invoked_with_correct_parameters_no_options(mock_init, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_init.return_value = gimmecert.commands.ExitCode.SUCCESS default_depth = 1 gimmecert.cli.main() mock_init.assert_called_once_with(sys.stdout, sys.stderr, tmpdir.strpath, tmpdir.basename, default_depth) @mock.patch('sys.argv', ['gimmecert', 'init', '-b', 'My Project']) @mock.patch('gimmecert.cli.init') def test_init_command_invoked_with_correct_parameters_with_options(mock_init, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_init.return_value = gimmecert.commands.ExitCode.SUCCESS default_depth = 1 gimmecert.cli.main() mock_init.assert_called_once_with(sys.stdout, sys.stderr, tmpdir.strpath, 'My Project', default_depth) def test_setup_server_subcommand_parser_registered(): registered_functions = gimmecert.decorators.get_subcommand_parser_setup_functions() assert gimmecert.cli.setup_server_subcommand_parser in registered_functions def test_setup_server_subcommand_parser_returns_parser(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() subparser = gimmecert.cli.setup_server_subcommand_parser(parser, subparsers) assert isinstance(subparser, argparse.ArgumentParser) @mock.patch('sys.argv', ['gimmecert', 'server']) def test_setup_server_subcommand_fails_without_arguments(tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() with pytest.raises(SystemExit) as e_info: gimmecert.cli.main() assert e_info.value.code != 0 def test_setup_server_subcommand_sets_function_callback(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() subparser = gimmecert.cli.setup_server_subcommand_parser(parser, subparsers) assert callable(subparser.get_default('func')) @mock.patch('sys.argv', ['gimmecert', 'server', 'myserver']) @mock.patch('gimmecert.cli.server') def test_server_command_invoked_with_correct_parameters_without_extra_dns_names(mock_server, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_server.return_value = gimmecert.commands.ExitCode.SUCCESS gimmecert.cli.main() mock_server.assert_called_once_with(sys.stdout, sys.stderr, tmpdir.strpath, 'myserver', [], False) @mock.patch('sys.argv', ['gimmecert', 'server', 'myserver', 'service.local', 'service.example.com']) @mock.patch('gimmecert.cli.server') def test_server_command_invoked_with_correct_parameters_with_extra_dns_names(mock_server, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_server.return_value = gimmecert.commands.ExitCode.SUCCESS gimmecert.cli.main() mock_server.assert_called_once_with(sys.stdout, sys.stderr, tmpdir.strpath, 'myserver', ['service.local', 'service.example.com'], False) @mock.patch('sys.argv', ['gimmecert', 'help']) @mock.patch('gimmecert.cli.help_') def test_help_command_invoked_with_correct_parameters(mock_help_, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_help_.return_value = gimmecert.commands.ExitCode.SUCCESS gimmecert.cli.main() assert mock_help_.called assert mock_help_.call_count == 1 @mock.patch('sys.argv', ['gimmecert']) @mock.patch('gimmecert.cli.usage') def test_usage_command_invoked_with_correct_parameters(mock_usage, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_usage.return_value = gimmecert.commands.ExitCode.SUCCESS gimmecert.cli.main() # Can't test calling arguments, since we'd need to mask # get_parser, and if we do that we mask the set_defaults and # what-not. assert mock_usage.called assert mock_usage.call_count == 1 @mock.patch('sys.argv', ['gimmecert', 'testcommand']) def test_main_does_not_exit_if_it_calls_function_that_returns_success(tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() @gimmecert.decorators.subcommand_parser def setup_testcommand_parser(parser, subparsers): subparser = subparsers.add_parser('testcommand', description='test command') def testcommand_wrapper(args): return gimmecert.commands.ExitCode.SUCCESS subparser.set_defaults(func=testcommand_wrapper) return subparser gimmecert.cli.main() # Should not raise @mock.patch('sys.argv', ['gimmecert', 'testcommand']) def test_main_exits_if_it_calls_function_that_returns_success(tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() @gimmecert.decorators.subcommand_parser def setup_testcommand_parser(parser, subparsers): subparser = subparsers.add_parser('testcommand', description='test command') def testcommand_wrapper(args): return gimmecert.commands.ExitCode.ERROR_ALREADY_INITIALISED subparser.set_defaults(func=testcommand_wrapper) return subparser with pytest.raises(SystemExit) as e_info: gimmecert.cli.main() assert e_info.value.code == gimmecert.commands.ExitCode.ERROR_ALREADY_INITIALISED def test_setup_client_subcommand_parser_registered(): registered_functions = gimmecert.decorators.get_subcommand_parser_setup_functions() assert gimmecert.cli.setup_client_subcommand_parser in registered_functions def test_setup_client_subcommand_parser_returns_parser(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() subparser = gimmecert.cli.setup_client_subcommand_parser(parser, subparsers) assert isinstance(subparser, argparse.ArgumentParser) @mock.patch('sys.argv', ['gimmecert', 'client']) def test_setup_client_subcommand_fails_without_arguments(tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() with pytest.raises(SystemExit) as e_info: gimmecert.cli.main() assert e_info.value.code != 0 def test_setup_client_subcommand_sets_function_callback(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() subparser = gimmecert.cli.setup_client_subcommand_parser(parser, subparsers) assert callable(subparser.get_default('func')) @mock.patch('sys.argv', ['gimmecert', 'client', 'myclient']) @mock.patch('gimmecert.cli.client') def test_client_command_invoked_with_correct_parameters(mock_client, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_client.return_value = gimmecert.commands.ExitCode.SUCCESS gimmecert.cli.main() mock_client.assert_called_once_with(sys.stdout, sys.stderr, tmpdir.strpath, 'myclient') @mock.patch('sys.argv', ['gimmecert', 'server', '--update-dns-names', 'myserver', 'service.local']) @mock.patch('gimmecert.cli.server') def test_server_command_invoked_with_correct_parameters_with_update_option(mock_server, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_server.return_value = gimmecert.commands.ExitCode.SUCCESS gimmecert.cli.main() mock_server.assert_called_once_with(sys.stdout, sys.stderr, tmpdir.strpath, 'myserver', ['service.local'], True) def test_setup_renew_subcommand_parser_registered(): registered_functions = gimmecert.decorators.get_subcommand_parser_setup_functions() assert gimmecert.cli.setup_renew_subcommand_parser in registered_functions def test_setup_renew_subcommand_parser_returns_parser(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() subparser = gimmecert.cli.setup_renew_subcommand_parser(parser, subparsers) assert isinstance(subparser, argparse.ArgumentParser) @mock.patch('sys.argv', ['gimmecert', 'renew']) def test_renew_command_fails_without_arguments(tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() with pytest.raises(SystemExit) as e_info: gimmecert.cli.main() assert e_info.value.code != 0 def test_setup_renew_subcommand_sets_function_callback(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() subparser = gimmecert.cli.setup_renew_subcommand_parser(parser, subparsers) assert callable(subparser.get_default('func')) @mock.patch('sys.argv', ['gimmecert', 'renew', 'server', 'myserver']) @mock.patch('gimmecert.cli.renew') def test_renew_command_invoked_with_correct_parameters_for_server(mock_renew, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_renew.return_value = gimmecert.commands.ExitCode.SUCCESS gimmecert.cli.main() mock_renew.assert_called_once_with(sys.stdout, sys.stderr, tmpdir.strpath, 'server', 'myserver', False) @mock.patch('sys.argv', ['gimmecert', 'renew', 'client', 'myclient']) @mock.patch('gimmecert.cli.renew') def test_renew_command_invoked_with_correct_parameters_for_client(mock_renew, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_renew.return_value = gimmecert.commands.ExitCode.SUCCESS gimmecert.cli.main() mock_renew.assert_called_once_with(sys.stdout, sys.stderr, tmpdir.strpath, 'client', 'myclient', False) @mock.patch('sys.argv', ['gimmecert', 'renew', '--new-private-key', 'server', 'myserver']) @mock.patch('gimmecert.cli.renew') def test_renew_command_invoked_with_correct_parameters_for_server_with_new_private_key_option(mock_renew, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_renew.return_value = gimmecert.commands.ExitCode.SUCCESS gimmecert.cli.main() mock_renew.assert_called_once_with(sys.stdout, sys.stderr, tmpdir.strpath, 'server', 'myserver', True) @mock.patch('sys.argv', ['gimmecert', 'renew', '--new-private-key', 'client', 'myclient']) @mock.patch('gimmecert.cli.renew') def test_renew_command_invoked_with_correct_parameters_for_client_with_new_private_key_option(mock_renew, tmpdir): # This should ensure we don't accidentally create artifacts # outside of test directory. tmpdir.chdir() mock_renew.return_value = gimmecert.commands.ExitCode.SUCCESS gimmecert.cli.main() mock_renew.assert_called_once_with(sys.stdout, sys.stderr, tmpdir.strpath, 'client', 'myclient', True)