diff --git a/cli/scheduled_task.py b/cli/scheduled_task.py index d1471a2..3b794a1 100644 --- a/cli/scheduled_task.py +++ b/cli/scheduled_task.py @@ -1,6 +1,9 @@ +from typing import List + import typer -from pythonanywhere.task import Task +from pythonanywhere.scripts_commons import get_task_from_id +from pythonanywhere.task import Task, TaskList app = typer.Typer() @@ -56,9 +59,40 @@ def create( task.create_schedule() -@app.command() -def delete(): - raise NotImplementedError +delete_app = typer.Typer() +app.add_typer( + delete_app, name="delete", help="Delete scheduled task(s) by id or nuke'em all." +) + + +@delete_app.command("nuke", help="Delete all scheduled tasks.") +def delete_all_tasks( + force: bool = typer.Option( + False, "-f", "--force", help="Turns off user confirmation before deleting tasks" + ), +): + if not force: + user_response = typer.confirm( + "This will irrevocably delete all your tasks, proceed?" + ) + if not user_response: + return None + + for task in TaskList().tasks: + task.delete_schedule() + + +@delete_app.command( + "id", + help="""\b + Delete one or more scheduled tasks by id. + ID_NUMBERS may be acquired with `pa scheduled-task list` + """, +) +def delete_task_by_id(id_numbers: List[int] = typer.Argument(...)): + for task_id in id_numbers: + task = get_task_from_id(task_id, no_exit=True) + task.delete_schedule() @app.command() diff --git a/tests/test_cli_scheduled_task.py b/tests/test_cli_scheduled_task.py index 56afd9b..13cf06b 100644 --- a/tests/test_cli_scheduled_task.py +++ b/tests/test_cli_scheduled_task.py @@ -1,41 +1,114 @@ -from unittest.mock import call +from unittest.mock import call, Mock +import pytest from typer.testing import CliRunner -from cli.scheduled_task import app +from cli.scheduled_task import app, delete_app runner = CliRunner() -def test_create_calls_all_stuff_in_right_order(mocker): - mock_task_to_be_created = mocker.patch("cli.scheduled_task.Task.to_be_created") +@pytest.fixture +def task_list(mocker): + mock_task_list = mocker.patch("cli.scheduled_task.TaskList") + mock_task_list.return_value.tasks = [Mock(task_id=1), Mock(task_id=2)] + return mock_task_list - runner.invoke( - app, - [ - "create", - "--command", - "echo foo", - "--hour", - 8, - "--minute", - 10, - ], - ) - assert mock_task_to_be_created.call_args == call( - command="echo foo", hour=8, minute=10, disabled=False - ) - assert mock_task_to_be_created.return_value.method_calls == [call.create_schedule()] +@pytest.fixture +def mock_confirm(mocker): + return mocker.patch("cli.scheduled_task.typer.confirm") -def test_create_validates_minutes(): - result = runner.invoke(app, ["create", "-c", "echo foo", "-h", 8, "-m", 66]) - assert "Invalid value" in result.stdout - assert "66 is not in the valid range of 0 to 59" in result.stdout +class TestCreate: + def test_calls_all_stuff_in_right_order(self, mocker): + mock_task_to_be_created = mocker.patch("cli.scheduled_task.Task.to_be_created") + runner.invoke( + app, + [ + "create", + "--command", + "echo foo", + "--hour", + "8", + "--minute", + "10", + ], + ) -def test_create_validates_hours(): - result = runner.invoke(app, ["create", "-c", "echo foo", "-h", 66, "-m", 1]) - assert "Invalid value" in result.stdout - assert "66 is not in the valid range of 0 to 23" in result.stdout + assert mock_task_to_be_created.call_args == call( + command="echo foo", hour=8, minute=10, disabled=False + ) + assert mock_task_to_be_created.return_value.method_calls == [ + call.create_schedule() + ] + + def test_validates_minutes(self): + result = runner.invoke(app, ["create", "-c", "echo foo", "-h", "8", "-m", "66"]) + + assert "Invalid value" in result.stdout + assert "66 is not in the valid range of 0 to 59" in result.stdout + + def test_validates_hours(self): + result = runner.invoke(app, ["create", "-c", "echo foo", "-h", "66", "-m", "1"]) + assert "Invalid value" in result.stdout + assert "66 is not in the valid range of 0 to 23" in result.stdout + + +class TestDeleteAllTasks: + def test_deletes_all_tasks_with_user_permission(self, task_list, mock_confirm): + mock_confirm.return_value = True + + runner.invoke(delete_app, ["nuke"]) + + assert mock_confirm.call_args == call( + "This will irrevocably delete all your tasks, proceed?" + ) + assert task_list.call_count == 1 + for task in task_list.return_value.tasks: + assert task.method_calls == [call.delete_schedule()] + + def test_exits_when_user_changes_mind(self, task_list, mock_confirm): + mock_confirm.return_value = False + + runner.invoke( + delete_app, + [ + "nuke", + ], + ) + + assert task_list.call_count == 0 + + def test_deletes_all_tasks_when_forced(self, task_list, mock_confirm): + runner.invoke(delete_app, ["nuke", "--force"]) + + assert mock_confirm.call_count == 0 + assert task_list.call_count == 1 + for task in task_list.return_value.tasks: + assert task.method_calls == [call.delete_schedule()] + + +class TestDeleteTaskById: + def test_deletes_one_task(self, mocker): + mock_task_from_id = mocker.patch("cli.scheduled_task.get_task_from_id") + + runner.invoke(delete_app, ["id", "42"]) + + assert mock_task_from_id.call_args == call(42, no_exit=True) + assert mock_task_from_id.return_value.method_calls == [call.delete_schedule()] + + def test_deletes_some_tasks(self, mocker): + mock_task_from_id = mocker.patch("cli.scheduled_task.get_task_from_id") + + runner.invoke(delete_app, ["id", "24", "42"]) + + assert mock_task_from_id.call_args_list == [ + call(24, no_exit=True), + call(42, no_exit=True), + ] + assert mock_task_from_id.return_value.method_calls == [ + call.delete_schedule(), + call.delete_schedule(), + ]