diff --git a/littlepay/commands/groups.py b/littlepay/commands/groups.py new file mode 100644 index 0000000..214841d --- /dev/null +++ b/littlepay/commands/groups.py @@ -0,0 +1,27 @@ +from littlepay.api.client import Client +from littlepay.commands import RESULT_SUCCESS +from littlepay.config import Config + + +def groups(group_term: str = None, group_command: str = None, **kwargs) -> int: + config = Config() + client = Client.from_active_config(config) + client.oauth.ensure_active_token(client.token) + config.active_token = client.token + + groups = client.get_concession_groups() + + if group_term is not None: + term = group_term.lower() + groups = filter( + lambda g: term in g.id.lower() or term in g.label.lower(), + groups, + ) + + groups = list(groups) + print(f"👥 Matching groups ({len(groups)})") + + for group in groups: + print(group) + + return RESULT_SUCCESS diff --git a/littlepay/main.py b/littlepay/main.py index 5f3fdac..f081330 100644 --- a/littlepay/main.py +++ b/littlepay/main.py @@ -1,55 +1,73 @@ -from argparse import ArgumentParser +from argparse import _SubParsersAction, ArgumentParser import sys from littlepay import __version__ as version from littlepay.commands.configure import configure +from littlepay.commands.groups import groups from littlepay.commands.switch import switch from littlepay.config import CONFIG_TYPES, Config +def _subcmd(subparsers: _SubParsersAction, name: str, help: str) -> ArgumentParser: + """Helper creates a new subcommand parser in the collection.""" + parser = subparsers.add_parser(name, help=help) + return parser + + def main(argv=None): argv = argv if argv is not None else sys.argv[1:] - parser = ArgumentParser(prog="littlepay") + main_parser = ArgumentParser(prog="littlepay") # https://stackoverflow.com/a/8521644/812183 - parser.add_argument( + # littlepay -v + # littlepay --version + main_parser.add_argument( "-v", "--version", action="version", version=f"%(prog)s {version}", ) - parser.add_argument( + # littlepay -c CONFIG_PATH + # littlepay --config CONFIG_PATH + main_parser.add_argument( "-c", "--config", dest="config_path", help="Path to a readable and writeable config file to use. File will be created if it does not exist.", ) - subparsers = parser.add_subparsers(dest="command") + main_commands = main_parser.add_subparsers(dest="command") - def _subcmd(name, help) -> ArgumentParser: - """Helper creates a new subcommand parser.""" - parser = subparsers.add_parser(name, help=help) - return parser + def _maincmd(name, help): + return _subcmd(main_commands, name, help) - config_parser = _subcmd("config", help="Get or set configuration.") - config_parser.add_argument( - "config_path", - nargs="?", - default=Config.current_path(), - ) + # littlepay config [CONFIG_PATH] + config_parser = _maincmd("config", help="Get or set configuration.") + config_parser.add_argument("config_path", nargs="?", default=Config.current_path()) + + # littlepay groups [-f GROUP] + groups_parser = _maincmd("groups", help="Get information about groups in the active environment.") + groups_parser.add_argument("-f", "--filter", help="Filter for groups with matching group ID or label", dest="group") + _ = groups_parser.add_subparsers(dest="group_command", required=False) - switch_parser = _subcmd("switch", help="Switch the active environment or participant.") + # littlepay switch {env, participant} VALUE + switch_parser = _maincmd("switch", help="Switch the active environment or participant.") switch_parser.add_argument("switch_type", choices=CONFIG_TYPES, help="The type of object to switch", metavar="TYPE") switch_parser.add_argument("switch_arg", help="The new object value", metavar="VALUE") if len(argv) == 0: argv = ["config"] - args = parser.parse_args(argv) + args = main_parser.parse_args(argv) if args.command == "config" or args.config_path: return configure(args.config_path) + elif args.command == "groups": + return groups( + getattr(args, "group", None), + getattr(args, "group_command", None), + funding_source_id=getattr(args, "funding_source_id", None), + ) elif args.command == "switch": return switch(args.switch_type, args.switch_arg) diff --git a/tests/commands/test_groups.py b/tests/commands/test_groups.py new file mode 100644 index 0000000..94ab3cf --- /dev/null +++ b/tests/commands/test_groups.py @@ -0,0 +1,74 @@ +from argparse import Namespace +import pytest + +from littlepay.api.groups import GroupResponse +from littlepay.commands import RESULT_SUCCESS +from littlepay.commands.groups import groups + +GROUP_RESPONSES = [ + GroupResponse("id0", "zero", "participant123"), + GroupResponse("id1", "one", "participant123"), + GroupResponse("id2", "two", "participant123"), +] + + +@pytest.fixture(autouse=True) +def mock_config(mocker): + mocker.patch("littlepay.commands.groups.config") + + +@pytest.fixture +def mock_client(mocker): + client = mocker.Mock() + mocker.patch("littlepay.commands.groups.Client.from_active_config", return_value=client) + return client + + +@pytest.fixture(autouse=True) +def mock_get_groups(mock_client): + mock_client.get_concession_groups.return_value = GROUP_RESPONSES + + +def test_groups_default(mock_client, capfd): + res = groups() + capture = capfd.readouterr() + + assert res == RESULT_SUCCESS + + mock_client.oauth.ensure_active_token.assert_called_once() + + assert "Matching groups (3)" in capture.out + for response in GROUP_RESPONSES: + assert str(response) in capture.out + + +@pytest.mark.parametrize("group_response", GROUP_RESPONSES) +def test_groups_group_term__group_id(group_response, capfd): + args = Namespace(group_term=group_response.id) + res = groups(args) + capture = capfd.readouterr() + + assert res == RESULT_SUCCESS + + assert "Matching groups (1)" in capture.out + for response in GROUP_RESPONSES: + if response == group_response: + assert str(response) in capture.out + else: + assert str(response) not in capture.out + + +@pytest.mark.parametrize("group_response", GROUP_RESPONSES) +def test_groups_group_term__group_label(group_response, capfd): + args = Namespace(group_term=group_response.label) + res = groups(args) + capture = capfd.readouterr() + + assert res == RESULT_SUCCESS + + assert "Matching groups (1)" in capture.out + for response in GROUP_RESPONSES: + if response == group_response: + assert str(response) in capture.out + else: + assert str(response) not in capture.out diff --git a/tests/conftest.py b/tests/conftest.py index 4c73507..d26ad2d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -111,6 +111,12 @@ def mock_commands_config(mock_module_name): return mock_module_name("configure") +@pytest.fixture +def mock_commands_groups(mock_module_name): + """Fixture returns a function that patches commands.groups in a given module.""" + return mock_module_name("groups") + + @pytest.fixture def mock_commands_switch(mock_module_name): """Fixture returns a function that patches commands.switch in a given module.""" diff --git a/tests/test_main.py b/tests/test_main.py index 9246ba8..835b1cf 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -11,6 +11,11 @@ def mock_commands_config(mock_commands_config): return mock_commands_config(MODULE) +@pytest.fixture +def mock_commands_groups(mock_commands_groups): + return mock_commands_groups(MODULE) + + @pytest.fixture def mock_commands_switch(mock_commands_switch): return mock_commands_switch(MODULE) @@ -54,6 +59,22 @@ def test_main_config_config_path(custom_config_file: Path, mock_commands_config) mock_commands_config.assert_called_once_with(new_config_path) +def test_main_groups(mock_commands_groups): + result = main(argv=["groups"]) + + assert result == RESULT_SUCCESS + mock_commands_groups.assert_called_once() + + +@pytest.mark.parametrize("filter_flag", ["-f", "--filter"]) +def test_main_groups_filter(mock_commands_groups, filter_flag): + result = main(argv=["groups", filter_flag, "term"]) + + assert result == RESULT_SUCCESS + mock_commands_groups.assert_called_once() + assert "term" in mock_commands_groups.call_args.args + + @pytest.mark.parametrize("switch_type", CONFIG_TYPES) def test_main_switch_recognized_type(mock_commands_switch, switch_type): result = main(argv=["switch", switch_type, "new_value"])