From 5d2e88b34439547050c518d0ef75b28f8749569c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Jul 2026 21:29:33 +0000 Subject: [PATCH] feat(cli): require confirmation before delete-repo Prompt before deleting unless --yes/-y is passed. Add tests for the skip and abort paths. Closes #76. Co-authored-by: akae --- README.md | 3 ++ src/github_rest_cli/main.py | 22 ++++++++++ tests/test_delete_repo.py | 84 +++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 tests/test_delete_repo.py diff --git a/README.md b/README.md index 5540acd..e3bd4df 100644 --- a/README.md +++ b/README.md @@ -94,9 +94,12 @@ github-rest-cli create-repo --name my-new-repo --empty ### Delete a repository +Prompts for confirmation unless `--yes` / `-y` is passed: + ```shell github-rest-cli delete-repo --name my-repo github-rest-cli delete-repo --name my-repo --org my-org +github-rest-cli delete-repo --name my-repo --yes ``` ### Dependabot security updates diff --git a/src/github_rest_cli/main.py b/src/github_rest_cli/main.py index 04ee1b9..a19e153 100644 --- a/src/github_rest_cli/main.py +++ b/src/github_rest_cli/main.py @@ -150,6 +150,13 @@ def build_parser() -> argparse.ArgumentParser: dest="org", help="The organization name", ) + delete_repo_parser.add_argument( + "-y", + "--yes", + action="store_true", + dest="yes", + help="Skip the confirmation prompt and delete immediately", + ) delete_repo_parser.set_defaults(func=delete_repository) # Subparser for "dependabot" function @@ -218,6 +225,18 @@ def build_parser() -> argparse.ArgumentParser: return parser +def confirm_delete_repository( + name: str, org: str | None = None, *, yes: bool = False +) -> bool: + """Return True if deletion should proceed.""" + if yes: + return True + + target = f"{org}/{name}" if org else name + answer = input(f"Delete repository '{target}'? This cannot be undone. [y/N] ") + return answer.strip().lower() in {"y", "yes"} + + def cli(): parser = build_parser() args = parser.parse_args() @@ -235,6 +254,9 @@ def cli(): elif command == "create-repo": args.func(args.name, args.visibility, args.org, args.empty) elif command == "delete-repo": + if not confirm_delete_repository(args.name, args.org, yes=args.yes): + print("Aborted.") # noqa: T201 + return args.func(args.name, args.org) elif command == "dependabot": args.func(args.name, args.control, args.org) diff --git a/tests/test_delete_repo.py b/tests/test_delete_repo.py new file mode 100644 index 0000000..b4b5972 --- /dev/null +++ b/tests/test_delete_repo.py @@ -0,0 +1,84 @@ +from github_rest_cli.main import build_parser, confirm_delete_repository, cli +from github_rest_cli import api + + +GET_HEADERS_FUNCTION = "github_rest_cli.api.get_headers" +FETCH_USER_FUNCTION = "github_rest_cli.api.fetch_user" +REQUEST_HANDLER_FUNCTION = "github_rest_cli.api.request_with_handling" + + +def test_delete_repo_yes_flag_parses(): + parser = build_parser() + args = parser.parse_args(["delete-repo", "--name", "my-repo", "--yes"]) + + assert args.command == "delete-repo" + assert args.name == "my-repo" + assert args.yes is True + + +def test_delete_repo_short_yes_flag_parses(): + parser = build_parser() + args = parser.parse_args(["delete-repo", "-n", "my-repo", "-y"]) + + assert args.yes is True + + +def test_confirm_delete_skips_prompt_with_yes(mocker): + prompt = mocker.patch("github_rest_cli.main.input") + + assert confirm_delete_repository("my-repo", yes=True) is True + prompt.assert_not_called() + + +def test_confirm_delete_accepts_yes(mocker): + mocker.patch("github_rest_cli.main.input", return_value="y") + + assert confirm_delete_repository("my-repo", org="my-org") is True + + +def test_confirm_delete_rejects_other_answers(mocker): + mocker.patch("github_rest_cli.main.input", return_value="n") + + assert confirm_delete_repository("my-repo") is False + + +def test_cli_delete_repo_aborts_without_confirmation(mocker, capsys): + mocker.patch( + "github_rest_cli.main.confirm_delete_repository", + return_value=False, + ) + delete_mock = mocker.patch("github_rest_cli.main.delete_repository") + mocker.patch( + "sys.argv", + ["github-rest-cli", "delete-repo", "--name", "my-repo"], + ) + + cli() + + delete_mock.assert_not_called() + assert "Aborted." in capsys.readouterr().out + + +def test_cli_delete_repo_proceeds_with_yes(mocker): + delete_mock = mocker.patch("github_rest_cli.main.delete_repository") + prompt = mocker.patch("github_rest_cli.main.input") + mocker.patch( + "sys.argv", + ["github-rest-cli", "delete-repo", "--name", "my-repo", "--yes"], + ) + + cli() + + prompt.assert_not_called() + delete_mock.assert_called_once_with("my-repo", None) + + +def test_delete_repository_api(mocker): + mocker.patch(GET_HEADERS_FUNCTION, return_value={"Authorization": "token fake"}) + mocker.patch(FETCH_USER_FUNCTION, return_value="test-user") + request_mock = mocker.patch(REQUEST_HANDLER_FUNCTION, return_value=None) + + api.delete_repository("my-repo") + + request_mock.assert_called_once() + assert request_mock.call_args.args[0] == "DELETE"