Files
superset2/tests/integration_tests/tasks/commands/test_cancel.py
2026-02-11 10:40:07 -08:00

495 lines
15 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from unittest.mock import patch
from uuid import UUID, uuid4
import pytest
from superset_core.api.tasks import TaskScope, TaskStatus
from superset import db
from superset.commands.tasks.cancel import CancelTaskCommand
from superset.commands.tasks.exceptions import (
TaskAbortFailedError,
TaskNotAbortableError,
TaskNotFoundError,
TaskPermissionDeniedError,
)
from superset.daos.tasks import TaskDAO
from superset.utils.core import override_user
from tests.integration_tests.test_app import app
def test_cancel_pending_task_aborts(app_context, get_user) -> None:
"""Test canceling a pending task directly aborts it"""
admin = get_user("admin")
# Create a pending private task
task = TaskDAO.create_task(
task_type="test_type",
task_key="cancel_pending_test",
scope=TaskScope.PRIVATE,
user_id=admin.id,
)
task.created_by = admin
db.session.commit()
try:
# Cancel the pending task with admin user context
with override_user(admin):
command = CancelTaskCommand(task_uuid=task.uuid)
result = command.run()
# Verify task is aborted (pending goes directly to ABORTED)
assert result.uuid == task.uuid
assert result.status == TaskStatus.ABORTED.value
assert command.action_taken == "aborted"
# Verify in database
db.session.refresh(task)
assert task.status == TaskStatus.ABORTED.value
finally:
# Cleanup
db.session.delete(task)
db.session.commit()
def test_cancel_in_progress_abortable_task_sets_aborting(app_context, get_user) -> None:
"""Test canceling an in-progress task with abort handler sets ABORTING"""
admin = get_user("admin")
# Create an in-progress abortable task
task = TaskDAO.create_task(
task_type="test_type",
task_key="cancel_in_progress_test",
scope=TaskScope.PRIVATE,
user_id=admin.id,
properties={"is_abortable": True},
)
task.created_by = admin
task.set_status(TaskStatus.IN_PROGRESS)
db.session.commit()
try:
# Cancel the in-progress task - mock publish_abort to avoid Redis dependency
with (
override_user(admin),
patch("superset.tasks.manager.TaskManager.publish_abort"),
):
command = CancelTaskCommand(task_uuid=task.uuid)
result = command.run()
# In-progress tasks go to ABORTING (not ABORTED)
assert result.uuid == task.uuid
assert result.status == TaskStatus.ABORTING.value
assert command.action_taken == "aborted"
# Verify in database
db.session.refresh(task)
assert task.status == TaskStatus.ABORTING.value
finally:
# Cleanup
db.session.delete(task)
db.session.commit()
def test_cancel_in_progress_not_abortable_raises_error(app_context, get_user) -> None:
"""Test canceling an in-progress task without abort handler raises error"""
admin = get_user("admin")
unique_key = f"cancel_not_abortable_test_{uuid4().hex[:8]}"
# Create an in-progress non-abortable task
task = TaskDAO.create_task(
task_type="test_type",
task_key=unique_key,
scope=TaskScope.PRIVATE,
user_id=admin.id,
properties={"is_abortable": False},
)
task.created_by = admin
task.set_status(TaskStatus.IN_PROGRESS)
db.session.commit()
try:
with override_user(admin):
command = CancelTaskCommand(task_uuid=task.uuid)
with pytest.raises(TaskNotAbortableError):
command.run()
# Verify task status unchanged
db.session.refresh(task)
assert task.status == TaskStatus.IN_PROGRESS.value
finally:
# Cleanup
db.session.delete(task)
db.session.commit()
def test_cancel_task_not_found(app_context, get_user) -> None:
"""Test canceling non-existent task raises error"""
admin = get_user("admin")
with override_user(admin):
command = CancelTaskCommand(
task_uuid=UUID("00000000-0000-0000-0000-000000000000")
)
with pytest.raises(TaskNotFoundError):
command.run()
def test_cancel_finished_task_raises_error(app_context, get_user) -> None:
"""Test canceling an already finished task raises error"""
admin = get_user("admin")
unique_key = f"cancel_finished_test_{uuid4().hex[:8]}"
# Create a finished task
task = TaskDAO.create_task(
task_type="test_type",
task_key=unique_key,
scope=TaskScope.PRIVATE,
user_id=admin.id,
)
task.created_by = admin
task.set_status(TaskStatus.SUCCESS)
db.session.commit()
try:
with override_user(admin):
command = CancelTaskCommand(task_uuid=task.uuid)
with pytest.raises(TaskAbortFailedError):
command.run()
# Verify task status unchanged
db.session.refresh(task)
assert task.status == TaskStatus.SUCCESS.value
finally:
# Cleanup
db.session.delete(task)
db.session.commit()
def test_cancel_shared_task_with_multiple_subscribers_unsubscribes(
app_context, get_user
) -> None:
"""Test canceling a shared task with multiple subscribers unsubscribes user"""
admin = get_user("admin")
gamma = get_user("gamma")
# Create a shared task with admin as creator
task = TaskDAO.create_task(
task_type="test_type",
task_key="cancel_shared_test",
scope=TaskScope.SHARED,
user_id=admin.id,
)
task.created_by = admin
db.session.commit()
# Add gamma as subscriber
TaskDAO.add_subscriber(task.id, user_id=gamma.id)
db.session.commit()
try:
# Verify we have 2 subscribers
db.session.refresh(task)
assert task.subscriber_count == 2
# Cancel as gamma (non-admin subscriber)
with override_user(gamma):
command = CancelTaskCommand(task_uuid=task.uuid)
result = command.run()
# Should unsubscribe, not abort
assert command.action_taken == "unsubscribed"
assert result.status == TaskStatus.PENDING.value # Status unchanged
# Verify gamma was unsubscribed
db.session.refresh(task)
assert task.subscriber_count == 1
assert not task.has_subscriber(gamma.id)
assert task.has_subscriber(admin.id)
finally:
# Cleanup
db.session.delete(task)
db.session.commit()
def test_cancel_shared_task_last_subscriber_aborts(app_context, get_user) -> None:
"""Test canceling a shared task as last subscriber aborts it"""
admin = get_user("admin")
# Create a shared task with only admin as subscriber
task = TaskDAO.create_task(
task_type="test_type",
task_key="cancel_last_subscriber_test",
scope=TaskScope.SHARED,
user_id=admin.id,
)
task.created_by = admin
db.session.commit()
try:
# Verify only 1 subscriber
db.session.refresh(task)
assert task.subscriber_count == 1
# Cancel as the only subscriber
with override_user(admin):
command = CancelTaskCommand(task_uuid=task.uuid)
result = command.run()
# Should abort since last subscriber
assert command.action_taken == "aborted"
assert result.status == TaskStatus.ABORTED.value
finally:
# Cleanup
db.session.delete(task)
db.session.commit()
def test_cancel_with_force_aborts_for_all_subscribers(app_context, get_user) -> None:
"""Test force cancel aborts shared task even with multiple subscribers"""
admin = get_user("admin")
gamma = get_user("gamma")
# Create a shared task with multiple subscribers
task = TaskDAO.create_task(
task_type="test_type",
task_key="force_cancel_test",
scope=TaskScope.SHARED,
user_id=admin.id,
)
task.created_by = admin
db.session.commit()
# Add gamma as subscriber
TaskDAO.add_subscriber(task.id, user_id=gamma.id)
db.session.commit()
try:
# Verify 2 subscribers
db.session.refresh(task)
assert task.subscriber_count == 2
# Force cancel as admin
with override_user(admin):
command = CancelTaskCommand(task_uuid=task.uuid, force=True)
result = command.run()
# Should abort despite multiple subscribers
assert command.action_taken == "aborted"
assert result.status == TaskStatus.ABORTED.value
finally:
# Cleanup
db.session.delete(task)
db.session.commit()
def test_cancel_with_force_requires_admin(app_context, get_user) -> None:
"""Test force cancel requires admin privileges"""
admin = get_user("admin")
gamma = get_user("gamma")
# Create a shared task
task = TaskDAO.create_task(
task_type="test_type",
task_key="force_requires_admin_test",
scope=TaskScope.SHARED,
user_id=admin.id,
)
task.created_by = admin
db.session.commit()
# Add gamma as subscriber
TaskDAO.add_subscriber(task.id, user_id=gamma.id)
db.session.commit()
try:
# Try to force cancel as gamma (non-admin)
with override_user(gamma):
command = CancelTaskCommand(task_uuid=task.uuid, force=True)
with pytest.raises(TaskPermissionDeniedError):
command.run()
# Verify task unchanged
db.session.refresh(task)
assert task.status == TaskStatus.PENDING.value
finally:
# Cleanup
db.session.delete(task)
db.session.commit()
def test_cancel_private_task_permission_denied(app_context, get_user) -> None:
"""Test non-owner cannot cancel private task"""
admin = get_user("admin")
gamma = get_user("gamma")
unique_key = f"private_permission_test_{uuid4().hex[:8]}"
# Use test_request_context to ensure has_request_context() returns True
# so that TaskFilter properly applies permission filtering
with app.test_request_context():
# Create a private task owned by admin
task = TaskDAO.create_task(
task_type="test_type",
task_key=unique_key,
scope=TaskScope.PRIVATE,
user_id=admin.id,
)
task.created_by = admin
db.session.commit()
try:
# Try to cancel admin's private task as gamma (non-owner)
with override_user(gamma):
command = CancelTaskCommand(task_uuid=task.uuid)
# Should fail because gamma can't see admin's private task (base filter)
with pytest.raises(TaskNotFoundError):
command.run()
# Verify task unchanged
db.session.refresh(task)
assert task.status == TaskStatus.PENDING.value
finally:
# Cleanup
db.session.delete(task)
db.session.commit()
def test_cancel_system_task_requires_admin(app_context, get_user) -> None:
"""Test system tasks can only be canceled by admin"""
admin = get_user("admin")
gamma = get_user("gamma")
unique_key = f"system_task_test_{uuid4().hex[:8]}"
# Use test_request_context to ensure has_request_context() returns True
# so that TaskFilter properly applies permission filtering
with app.test_request_context():
# Create a system task
task = TaskDAO.create_task(
task_type="test_type",
task_key=unique_key,
scope=TaskScope.SYSTEM,
user_id=None,
)
task.created_by = admin
db.session.commit()
try:
# Try to cancel as gamma (non-admin)
with override_user(gamma):
command = CancelTaskCommand(task_uuid=task.uuid)
# System tasks are not visible to non-admins via base filter
with pytest.raises(TaskNotFoundError):
command.run()
# Verify task unchanged
db.session.refresh(task)
assert task.status == TaskStatus.PENDING.value
# But admin can cancel it
with override_user(admin):
command = CancelTaskCommand(task_uuid=task.uuid)
result = command.run()
assert result.status == TaskStatus.ABORTED.value
finally:
# Cleanup
db.session.delete(task)
db.session.commit()
def test_cancel_already_aborting_is_idempotent(app_context, get_user) -> None:
"""Test canceling an already aborting task is idempotent"""
admin = get_user("admin")
# Create a task already in ABORTING state
task = TaskDAO.create_task(
task_type="test_type",
task_key="idempotent_cancel_test",
scope=TaskScope.PRIVATE,
user_id=admin.id,
)
task.created_by = admin
task.set_status(TaskStatus.ABORTING)
db.session.commit()
try:
# Cancel the already aborting task
with override_user(admin):
command = CancelTaskCommand(task_uuid=task.uuid)
result = command.run()
# Should succeed without error
assert result.uuid == task.uuid
assert result.status == TaskStatus.ABORTING.value
finally:
# Cleanup
db.session.delete(task)
db.session.commit()
def test_cancel_shared_task_not_subscribed_raises_not_found(
app_context, get_user
) -> None:
"""Test non-subscriber cannot see or cancel shared task.
With subscription-only filtering, users who aren't subscribed to a
shared task can't see it at all, so canceling returns "not found"
rather than "permission denied".
"""
admin = get_user("admin")
gamma = get_user("gamma")
unique_key = f"not_subscribed_test_{uuid4().hex[:8]}"
# Use test_request_context to ensure TaskFilter is applied
with app.test_request_context():
# Create a shared task with only admin as subscriber
with override_user(admin):
task = TaskDAO.create_task(
task_type="test_type",
task_key=unique_key,
scope=TaskScope.SHARED,
user_id=admin.id,
)
db.session.commit()
try:
# Try to cancel as gamma (not subscribed) - they can't see the task
with override_user(gamma):
command = CancelTaskCommand(task_uuid=task.uuid)
# TaskNotFoundError because gamma isn't subscribed and can't see
# the task
with pytest.raises(TaskNotFoundError):
command.run()
# Verify task unchanged
db.session.refresh(task)
assert task.status == TaskStatus.PENDING.value
finally:
# Cleanup
db.session.delete(task)
db.session.commit()