import json import shutil import tempfile from pathlib import Path from unittest import mock from galaxy import model from galaxy.app_unittest_utils import galaxy_mock from galaxy.app_unittest_utils.tools_support import UsesApp from galaxy.tools.errors import EmailErrorReporter from galaxy.util.unittest import TestCase from galaxy.workflow.errors import WorkflowEmailErrorReporter # The email the user created their account with. TEST_USER_EMAIL = "mockgalaxyuser@galaxyproject.org" # The email the user supplied when submitting the error TEST_USER_SUPPLIED_EMAIL = "fake@example.org" TEST_SERVER_EMAIL_FROM = "email_from@galaxyproject.org" TEST_SERVER_ERROR_EMAIL_TO = "admin@email.to" # setup in mock config class TestErrorReporter(TestCase, UsesApp): def setUp(self): self.setup_app() self.app.config.email_from = TEST_SERVER_EMAIL_FROM self.tmp_path = Path(tempfile.mkdtemp()) self.email_path = self.tmp_path / "email.json" smtp_server = f"mock_emails_to_path://{self.email_path}" self.app.config.smtp_server = smtp_server # type: ignore[attr-defined] self.app.workflow_manager = mock.MagicMock() def tearDown(self): shutil.rmtree(self.tmp_path) def test_basic(self): user, hda = self._setup_model_objects() email_path = self.email_path assert not email_path.exists() error_report = EmailErrorReporter(hda, self.app) error_report.send_report(user, email=TEST_USER_SUPPLIED_EMAIL, message="My custom message") assert email_path.exists() text = email_path.read_text() email_json = json.loads(text) assert email_json["from"] == TEST_SERVER_EMAIL_FROM assert email_json["to"] == f"{TEST_SERVER_ERROR_EMAIL_TO}, {TEST_USER_SUPPLIED_EMAIL}" assert f"Galaxy tool error report from {TEST_USER_SUPPLIED_EMAIL}" == email_json["subject"] assert "cat1" in email_json["body"] assert "cat1" in email_json["html"] assert TEST_USER_EMAIL == email_json["reply_to"] def test_workflow_error_reporting(self): user, invocation, trans = self._setup_invocation_model_objects() email_path = self.email_path assert not email_path.exists() error_report = WorkflowEmailErrorReporter(invocation, self.app) error_report.send_report(user, email=TEST_USER_SUPPLIED_EMAIL, message="My custom message", trans=trans) assert email_path.exists() text = email_path.read_text() email_json = json.loads(text) assert email_json["from"] == TEST_SERVER_EMAIL_FROM assert email_json["to"] == f"{TEST_SERVER_ERROR_EMAIL_TO}, {TEST_USER_SUPPLIED_EMAIL}" assert f"Galaxy workflow run error report from {TEST_USER_SUPPLIED_EMAIL}" == email_json["subject"] assert "Test Workflow" in email_json["body"] assert "Test Workflow" in email_json["html"] assert TEST_USER_EMAIL == email_json["reply_to"] def test_workflow_error_reporting_inaccessible_history(self): _, invocation, trans = self._setup_invocation_model_objects() other_user = model.User(email="otheruser@galaxyproject.org", password="mockpass2") self._commit_objects([other_user]) email_path = self.email_path assert not email_path.exists() error_report = WorkflowEmailErrorReporter(invocation, self.app) self.app.workflow_manager.check_security.side_effect = Exception("Invocation is not accessible") # type: ignore[attr-defined] error_report.send_report(other_user, email=TEST_USER_SUPPLIED_EMAIL, message="My custom message", trans=trans) # Same comment as in `test_hda_security`, but for accessibility of the invocation: # Without accessibility, the email still gets sent but the supplied email is ignored assert email_path.exists() text = email_path.read_text() email_json = json.loads(text) assert email_json["from"] == TEST_SERVER_EMAIL_FROM assert email_json["to"] == f"{TEST_SERVER_ERROR_EMAIL_TO}" def test_hda_security(self, tmp_path): user, hda = self._setup_model_objects() error_report = EmailErrorReporter(hda, self.app) security_agent = self.app.security_agent private_role = security_agent.create_private_user_role(user) access_action = security_agent.permitted_actions.DATASET_ACCESS.action manage_action = security_agent.permitted_actions.DATASET_MANAGE_PERMISSIONS.action permissions = {access_action: [private_role], manage_action: [private_role]} security_agent.set_all_dataset_permissions(hda.dataset, permissions) other_user = model.User(email="otheruser@galaxyproject.org", password="mockpass2") self._commit_objects([other_user]) security_agent = self.app.security_agent email_path = self.email_path assert not email_path.exists() error_report.send_report(other_user, email=TEST_USER_SUPPLIED_EMAIL, message="My custom message") # Without permissions, the email still gets sent but the supplied email is ignored # I'm not saying this is the right behavior but it is what the code does at the time of test # writing -John assert email_path.exists() text = email_path.read_text() email_json = json.loads(text) assert "otheruser@galaxyproject.org" not in email_json["to"] def test_html_sanitization(self, tmp_path): user, hda = self._setup_model_objects() email_path = self.email_path assert not email_path.exists() error_report = EmailErrorReporter(hda, self.app) error_report.send_report( user, email=TEST_USER_SUPPLIED_EMAIL, message='My custom message' ) assert email_path.exists() text = email_path.read_text() email_json = json.loads(text) html = email_json["html"] assert "<a href="http://sneaky.com/">message</a>" in html def test_redact_user_details_in_bugreport(self, tmp_path): user, hda = self._setup_model_objects() email_path = self.email_path assert not email_path.exists() error_report = EmailErrorReporter(hda, self.app) error_report.send_report( user, email=TEST_USER_SUPPLIED_EMAIL, message="My custom message", redact_user_details_in_bugreport=True ) assert email_path.exists() text = email_path.read_text() email_json = json.loads(text) assert "The user redacted (user: 1) provided the following information:" in email_json["body"] assert ( """The user redacted (user: 1) provided the following information:""" in email_json["html"] ) def test_no_redact_user_details_in_bugreport(self, tmp_path): user, hda = self._setup_model_objects() email_path = self.email_path assert not email_path.exists() error_report = EmailErrorReporter(hda, self.app) error_report.send_report( user, email=TEST_USER_SUPPLIED_EMAIL, message="My custom message", redact_user_details_in_bugreport=False ) assert email_path.exists() text = email_path.read_text() email_json = json.loads(text) assert ( f"The user '{TEST_USER_EMAIL}' (providing preferred contact email '{TEST_USER_SUPPLIED_EMAIL}') provided the following information:" in email_json["body"] ) assert ( f"""The user '{TEST_USER_EMAIL}' (providing preferred contact email '{TEST_USER_SUPPLIED_EMAIL}') provided the following information:""" in email_json["html"] ) def _setup_model_objects(self): user = model.User(email=TEST_USER_EMAIL, password="mockpass") job = model.Job() job.tool_id = "cat1" job.history = model.History() job.user = user job.history.user = user hda = model.HistoryDatasetAssociation(history=job.history) hda.dataset = model.Dataset() hda.dataset.state = "ok" job.add_output_dataset("out1", hda) self._commit_objects([job, hda, user]) return user, hda def _setup_invocation_model_objects(self): user = model.User(email=TEST_USER_EMAIL, password="mockpass") invocation = model.WorkflowInvocation() invocation.workflow = model.Workflow() invocation.workflow.name = "Test Workflow" invocation.workflow.stored_workflow = model.StoredWorkflow() invocation.workflow.stored_workflow.user = user invocation.history = model.History() invocation.history.user = user trans = galaxy_mock.MockTrans(app=self.app, history=invocation.history, user=user) self._commit_objects([user, invocation]) return user, invocation, trans def _commit_objects(self, objects): session = self.app.model.context session.add_all(objects) session.commit()