Files
galaxy/test/integration/test_notifications.py
T
mvdbeek a1ecabdfbc Adjust API tests
Most of the time we just want an OK status code. 202, 204, 200 should
all be OK and some of this depends on the Galaxy instance config,
so this should be more robust to future changes.
2025-09-23 22:16:20 +02:00

454 lines
20 KiB
Python

from datetime import (
datetime,
timedelta,
)
from typing import (
Any,
Optional,
)
from uuid import uuid4
from galaxy_test.base.populators import (
DatasetPopulator,
WorkflowPopulator,
)
from galaxy_test.driver.integration_util import IntegrationTestCase
def notification_test_data(subject: Optional[str] = None, message: Optional[str] = None):
return {
"source": "integration_tests",
"variant": "info",
"category": "message",
"content": {
"category": "message",
"subject": subject or "Testing Subject",
"message": message or "Testing Message",
},
}
def notification_broadcast_test_data(subject: Optional[str] = None, message: Optional[str] = None):
return {
"source": "integration_tests",
"variant": "info",
"category": "broadcast",
"content": {
"category": "broadcast",
"subject": subject or "Testing Broadcast Subject",
"message": message or "Testing Broadcast Message",
},
}
class NotificationsIntegrationBase(IntegrationTestCase):
dataset_populator: DatasetPopulator
task_based = False
framework_tool_and_types = False
@classmethod
def handle_galaxy_config_kwds(cls, config):
super().handle_galaxy_config_kwds(config)
config["enable_celery_tasks"] = False
config["enable_notification_system"] = True
def setUp(self):
super().setUp()
self.dataset_populator = DatasetPopulator(self.galaxy_interactor)
self.workflow_populator = WorkflowPopulator(self.galaxy_interactor)
def test_notification_status(self):
user1 = self._create_test_user()
user2 = self._create_test_user()
before_creating_notifications = datetime.utcnow()
# Only user1 will receive this notification
subject1 = f"notification_{uuid4()}"
created_response_1 = self._send_test_notification_to(
[user1["id"]], subject=subject1, message="test_notification_status 1"
)
self._assert_notifications_sent(created_response_1, expected_count=1)
# Both user1 and user2 will receive this notification
subject2 = f"notification_{uuid4()}"
created_response_2 = self._send_test_notification_to(
[user1["id"], user2["id"]], subject=subject2, message="test_notification_status 2"
)
self._assert_notifications_sent(created_response_2, expected_count=2)
# All users will receive this broadcasted notification
created_response_3 = self._send_broadcast_notification("test_notification_status 3")
assert created_response_3["total_notifications_sent"] == 1
after_creating_notifications = datetime.utcnow()
# The default user should have received only the broadcasted notifications
status = self._get_notifications_status_since(before_creating_notifications)
assert status["total_unread_count"] == 0
assert len(status["notifications"]) == 0
assert len(status["broadcasts"]) == 1
with self._different_user(user1["email"]):
status = self._get_notifications_status_since(before_creating_notifications)
assert status["total_unread_count"] == 2
assert len(status["notifications"]) == 2
assert len(status["broadcasts"]) == 1
# Mark one unseen notification as seen
unseen_notification = status["notifications"][0]["id"]
self._update_notification(unseen_notification, update_state={"seen": True})
status = self._get_notifications_status_since(before_creating_notifications)
# The total unread count should be updated
assert status["total_unread_count"] == 1
assert len(status["notifications"]) == 2
assert len(status["broadcasts"]) == 1
with self._different_user(user2["email"]):
status = self._get_notifications_status_since(before_creating_notifications)
assert status["total_unread_count"] == 1
assert len(status["notifications"]) == 1
assert len(status["broadcasts"]) == 1
# Getting the notifications status since a posterior date does not return notifications
# but the total number of unread notifications is maintained
status = self._get_notifications_status_since(after_creating_notifications)
assert status["total_unread_count"] == 1
assert len(status["notifications"]) == 0
assert len(status["broadcasts"]) == 0
# Updating a notification association value should return that notification in the next request
notification_id = self._get_notification_id_by_subject(subject2)
assert notification_id is not None
self._update_notification(notification_id, update_state={"seen": True})
status = self._get_notifications_status_since(after_creating_notifications)
assert status["total_unread_count"] == 0
assert len(status["notifications"]) == 1
assert len(status["broadcasts"]) == 0
with self._different_user(anon=True):
# Anonymous users can access broadcasted notifications
status = self._get_notifications_status_since(before_creating_notifications)
assert status["total_unread_count"] == 0
assert len(status["notifications"]) == 0
assert len(status["broadcasts"]) == 1
def test_user_cannot_access_other_users_notifications(self):
user1 = self._create_test_user()
user2 = self._create_test_user()
subject = f"notification_{uuid4()}"
created_response = self._send_test_notification_to(
[user1["id"]], subject=subject, message="test_user_cannot_access_other_users_notifications"
)
self._assert_notifications_sent(created_response, expected_count=1)
notification_id = None
with self._different_user(user1["email"]):
notification_id = self._get_notification_id_by_subject(subject)
assert notification_id is not None
response = self._get(f"notifications/{notification_id}")
self._assert_status_code_is_ok(response)
with self._different_user(user2["email"]):
assert notification_id is not None
response = self._get(f"notifications/{notification_id}")
self._assert_status_code_is(response, 404)
def test_delete_notification_by_user(self):
user1 = self._create_test_user()
user2 = self._create_test_user()
before_creating_notifications = datetime.utcnow()
subject = f"notification_{uuid4()}"
created_response = self._send_test_notification_to(
[user1["id"], user2["id"]], subject=subject, message="test_delete_notification_by_user"
)
self._assert_notifications_sent(created_response, expected_count=2)
with self._different_user(user1["email"]):
notification_id = self._get_notification_id_by_subject(subject)
assert notification_id is not None
response = self._get(f"notifications/{notification_id}")
self._assert_status_code_is_ok(response)
self._delete(f"notifications/{notification_id}")
# After delete, it is no longer available for this user
response = self._get(f"notifications/{notification_id}")
self._assert_status_code_is(response, 404)
status = self._get_notifications_status_since(before_creating_notifications)
assert status["total_unread_count"] == 0
assert len(status["notifications"]) == 0
assert len(status["broadcasts"]) == 0
with self._different_user(user2["email"]):
assert notification_id is not None
response = self._get(f"notifications/{notification_id}")
self._assert_status_code_is_ok(response)
def test_non_admin_users_cannot_create_notifications(self):
recipient_user = self._create_test_user()
notification_request = {
"recipients": {"user_ids": [recipient_user["id"]]},
"notification": notification_test_data(),
}
response = self._post("notifications", data=notification_request, json=True)
self._assert_status_code_is(response, 403)
broadcast_notification_data = notification_broadcast_test_data()
response = self._post("notifications/broadcast", data=broadcast_notification_data, json=True)
self._assert_status_code_is(response, 403)
with self._different_user(anon=True):
response = self._post("notifications", data=notification_request, json=True)
self._assert_status_code_is(response, 403)
response = self._post("notifications/broadcast", data=broadcast_notification_data, json=True)
self._assert_status_code_is(response, 403)
def test_update_notifications(self):
recipient_user = self._create_test_user()
subject = f"User Notification to update {uuid4()}"
created_user_notification_response = self._send_test_notification_to([recipient_user["id"]], subject=subject)
self._assert_notifications_sent(created_user_notification_response, expected_count=1)
with self._different_user(recipient_user["email"]):
user_notification_id = self._get_notification_id_by_subject(subject)
assert user_notification_id is not None
created_broadcast_notification_response = self._send_broadcast_notification(
subject="Broadcasted Notification to update"
)
assert created_broadcast_notification_response["total_notifications_sent"] == 1
broadcasted_notification_id = created_broadcast_notification_response["notification"]["id"]
update_core_value_payload = {"source": "updated_source"} # Core values are part of the notification itself
update_user_value_payload = {"seen": True} # User values are from the association (seen, deleted, etc)
# Regular users cannot update core notification values
update_response = self._put(f"notifications/{user_notification_id}", data=update_core_value_payload, json=True)
self._assert_status_code_is(update_response, 400)
update_response = self._put(
f"notifications/broadcast/{broadcasted_notification_id}", data=update_core_value_payload, json=True
)
self._assert_status_code_is(update_response, 403)
# Regular users cannot update associated values from other users (they don't exists for them)
update_response = self._put(f"notifications/{user_notification_id}", data=update_user_value_payload, json=True)
self._assert_status_code_is(update_response, 404)
# Recipient users can only update their associated values
with self._different_user(recipient_user["email"]):
update_response = self._put(
f"notifications/{user_notification_id}", data=update_core_value_payload, json=True
)
self._assert_status_code_is(update_response, 400)
update_response = self._put(
f"notifications/{user_notification_id}", data=update_user_value_payload, json=True
)
self._assert_status_code_is_ok(update_response)
updated_response = self._get(f"notifications/{user_notification_id}").json()
assert updated_response["seen_time"] is not None
# Admin users can update core values of broadcasted notifications
update_response = self._put(
f"notifications/broadcast/{broadcasted_notification_id}",
data=update_core_value_payload,
json=True,
admin=True,
)
self._assert_status_code_is_ok(update_response)
updated_response = self._get(f"notifications/broadcast/{broadcasted_notification_id}").json()
assert updated_response["source"] == "updated_source"
def test_admins_get_all_broadcasted_even_inactive(self):
tomorrow = datetime.utcnow() + timedelta(days=1)
yesterday = datetime.utcnow() - timedelta(days=1)
self._send_broadcast_notification(subject="Active")
self._send_broadcast_notification(subject="Scheduled", publication_time=tomorrow)
self._send_broadcast_notification(subject="Expired", expiration_time=yesterday)
# Regular users will only get active (published and non-expired) broadcasted notifications
response_from_user = self._get("notifications/broadcast").json()
subjects = [notification["content"]["subject"] for notification in response_from_user]
assert "Active" in subjects
assert "Scheduled" not in subjects
assert "Expired" not in subjects
# Admins will get all broadcasted notifications even inactive ones
response_from_admin = self._get("notifications/broadcast", admin=True).json()
subjects = [notification["content"]["subject"] for notification in response_from_admin]
assert "Active" in subjects
assert "Scheduled" in subjects
assert "Expired" in subjects
def test_notification_input_dates_consider_timezone(self):
payload = notification_broadcast_test_data(subject="Test", message="Test")
payload["publication_time"] = "2021-01-01T12:00:00+02:00"
payload["expiration_time"] = "2021-01-01T12:00:00Z"
response = self._post("notifications/broadcast", data=payload, admin=True, json=True)
self._assert_status_code_is_ok(response)
notification = response.json()["notification"]
assert notification["publication_time"] == "2021-01-01T10:00:00"
assert notification["expiration_time"] == "2021-01-01T12:00:00"
def test_broadcast_notification_action_links(self):
# Broadcast notifications can have relative and absolute links
response = self._send_broadcast_notification(
action_links=[("View Workflows", "/workflows/list"), ("Go to GTN", "https://training.galaxyproject.org")]
)
notification_id = response["notification"]["id"]
response = self._get(f"notifications/broadcast/{notification_id}")
self._assert_status_code_is_ok(response)
notification = response.json()
assert "content" in notification
assert "action_links" in notification["content"]
assert len(notification["content"]["action_links"]) == 2
action_link = notification["content"]["action_links"][0]
assert action_link["action_name"] == "View Workflows"
assert action_link["link"] == "/workflows/list"
action_link = notification["content"]["action_links"][1]
assert action_link["action_name"] == "Go to GTN"
assert action_link["link"] == "https://training.galaxyproject.org"
def test_sharing_items_creates_notifications_when_expected(self):
user1 = self._create_test_user()
user2 = self._create_test_user()
user_ids = [user1["id"], user2["id"]]
# User2 doesn't want to receive shared item notifications
with self._different_user(user2["email"]):
update_request = {
"preferences": {"new_shared_item": {"enabled": False, "channels": {"push": False}}},
}
update_response = self._put("notifications/preferences", data=update_request, json=True)
self._assert_status_code_is_ok(update_response)
# Share History with both users
history_id = self.dataset_populator.new_history("Notification test history")
self.dataset_populator.new_dataset(history_id)
payload = {"user_ids": user_ids, "share_option": "make_accessible_to_shared"}
sharing_response = self._put(f"histories/{history_id}/share_with_users", data=payload, json=True)
self._assert_status_code_is_ok(sharing_response)
# Share Workflow with both users
workflow_id = self.workflow_populator.simple_workflow("Notification test workflow")
payload = {"user_ids": user_ids}
sharing_response = self._put(f"workflows/{workflow_id}/share_with_users", data=payload, json=True)
self._assert_status_code_is_ok(sharing_response)
# Share Page with both users
page_id = self.dataset_populator.new_page(slug="notification-test-page")["id"]
payload = {"user_ids": user_ids}
sharing_response = self._put(f"pages/{page_id}/share_with_users", data=payload, json=True)
self._assert_status_code_is_ok(sharing_response)
# Share Visualization with both users
create_payload = {
"title": "Notification test visualization",
"slug": "notification-test-viz",
"type": "example",
"dbkey": "hg17",
}
response = self._post("visualizations", data=create_payload, json=True).json()
visualization_id = response["id"]
payload = {"user_ids": user_ids}
sharing_response = self._put(f"visualizations/{visualization_id}/share_with_users", data=payload, json=True)
self._assert_status_code_is_ok(sharing_response)
with self._different_user(user1["email"]):
notifications = self._get("notifications").json()
assert len(notifications) == 4
with self._different_user(user2["email"]):
notifications = self._get("notifications").json()
assert len(notifications) == 0
def _create_test_user(self):
user = self._setup_user(f"{uuid4()}@galaxy.test")
return user
def _get_notifications_status_since(self, since: datetime):
status_response = self._get(f"notifications/status?since={since}")
self._assert_status_code_is_ok(status_response)
status = status_response.json()
return status
def _send_test_notification_to(
self, user_ids: list[str], subject: Optional[str] = None, message: Optional[str] = None
):
request = {
"recipients": {"user_ids": user_ids},
"notification": notification_test_data(subject, message),
}
response = self._post("notifications", data=request, admin=True, json=True)
self._assert_status_code_is_ok(response)
created_response = response.json()
return created_response
def _send_broadcast_notification(
self,
subject: Optional[str] = None,
message: Optional[str] = None,
publication_time: Optional[datetime] = None,
expiration_time: Optional[datetime] = None,
action_links: Optional[list[tuple[str, str]]] = None,
):
payload = notification_broadcast_test_data()
if subject is not None:
payload["content"]["subject"] = subject
if message is not None:
payload["content"]["message"] = message
if action_links is not None:
payload["content"]["action_links"] = [
{"action_name": action_name, "link": link} for action_name, link in action_links
]
if publication_time is not None:
payload["publication_time"] = publication_time.isoformat()
if expiration_time is not None:
payload["expiration_time"] = expiration_time.isoformat()
response = self._post("notifications/broadcast", data=payload, admin=True, json=True)
self._assert_status_code_is_ok(response)
notifications_status = response.json()
return notifications_status
def _update_notification(self, notification_id: str, update_state: dict[str, Any]):
update_response = self._put(f"notifications/{notification_id}", data=update_state, json=True)
self._assert_status_code_is_ok(update_response)
def _assert_notifications_sent(self, response, expected_count: int = 0):
if self.task_based:
task_id = response["id"]
assert task_id is not None
self.dataset_populator.wait_on_task_id(task_id)
else:
assert response["total_notifications_sent"] == expected_count
def _get_notification_id_by_subject(self, subject: str) -> Optional[str]:
notifications = self._get("notifications").json()
for notification in notifications:
if notification["content"]["subject"] == subject:
return notification["id"]
return None
class TestNotificationsIntegration(NotificationsIntegrationBase):
task_based = False
@classmethod
def handle_galaxy_config_kwds(cls, config):
super().handle_galaxy_config_kwds(config)
config["enable_celery_tasks"] = False
class TestNotificationsIntegrationTaskBased(NotificationsIntegrationBase):
task_based = True
@classmethod
def handle_galaxy_config_kwds(cls, config):
super().handle_galaxy_config_kwds(config)
config["enable_celery_tasks"] = True