diff --git a/services/resend/text/issues_disabled_email.py b/services/resend/text/issues_disabled_email.py new file mode 100644 index 000000000..6aeec2f00 --- /dev/null +++ b/services/resend/text/issues_disabled_email.py @@ -0,0 +1,18 @@ +from config import PRODUCT_NAME +from constants.urls import SETTINGS_TRIGGERS_URL +from services.resend.constants import EMAIL_SIGNATURE + + +def get_issues_disabled_email_text(user_name: str | None, owner: str, repo: str): + subject = "Enable Issues to use GitAuto" + + text = f"""Hi {user_name}, + +{PRODUCT_NAME} couldn't run on {owner}/{repo} because GitHub Issues are disabled. To continue: + +1. Enable GitHub Issues: https://github.com/{owner}/{repo}/settings +2. Re-enable {PRODUCT_NAME} schedule: {SETTINGS_TRIGGERS_URL} + +{EMAIL_SIGNATURE}""" + + return subject, text diff --git a/services/resend/text/test_issues_disabled_email.py b/services/resend/text/test_issues_disabled_email.py new file mode 100644 index 000000000..5cd917db7 --- /dev/null +++ b/services/resend/text/test_issues_disabled_email.py @@ -0,0 +1,121 @@ +from unittest.mock import patch + +import pytest + +from services.resend.text.issues_disabled_email import get_issues_disabled_email_text + + +def test_get_issues_disabled_email_text_basic(): + subject, text = get_issues_disabled_email_text("John", "owner", "repo") + + assert subject == "Enable Issues to use GitAuto" + assert "Hi John," in text + assert "GitHub Issues are disabled" in text + assert "https://github.com/owner/repo/settings" in text + assert "Re-enable" in text + assert "Wes\nGitAuto" in text + + +def test_get_issues_disabled_email_text_with_full_name(): + subject, text = get_issues_disabled_email_text("John Doe", "myorg", "myrepo") + + assert subject == "Enable Issues to use GitAuto" + assert "Hi John Doe," in text + assert "myorg/myrepo" in text + assert "https://github.com/myorg/myrepo/settings" in text + + +def test_get_issues_disabled_email_text_with_special_characters(): + subject, text = get_issues_disabled_email_text( + "José María", "org-name", "repo_name" + ) + + assert subject == "Enable Issues to use GitAuto" + assert "Hi José María," in text + assert "org-name/repo_name" in text + assert "https://github.com/org-name/repo_name/settings" in text + + +def test_get_issues_disabled_email_text_with_empty_string(): + subject, text = get_issues_disabled_email_text("", "owner", "repo") + + assert subject == "Enable Issues to use GitAuto" + assert "Hi ," in text + assert "owner/repo" in text + + +def test_get_issues_disabled_email_text_with_none(): + subject, text = get_issues_disabled_email_text(None, "owner", "repo") + + assert subject == "Enable Issues to use GitAuto" + assert "Hi None," in text + assert "owner/repo" in text + + +def test_get_issues_disabled_email_text_includes_instructions(): + _, text = get_issues_disabled_email_text("Alice", "owner", "repo") + + assert "https://github.com/owner/repo/settings" in text + assert "Re-enable" in text + assert "schedule" in text + + +def test_get_issues_disabled_email_text_includes_settings_url(): + _, text = get_issues_disabled_email_text("Bob", "owner", "repo") + + assert "gitauto.ai/settings/triggers" in text + + +def test_get_issues_disabled_email_text_includes_email_signature(): + with patch( + "services.resend.text.issues_disabled_email.EMAIL_SIGNATURE", + "Custom Signature", + ): + _, text = get_issues_disabled_email_text("Bob", "owner", "repo") + + assert "Custom Signature" in text + assert text.endswith("Custom Signature") + + +def test_get_issues_disabled_email_text_return_type(): + result = get_issues_disabled_email_text("Test User", "owner", "repo") + + assert isinstance(result, tuple) + assert len(result) == 2 + assert isinstance(result[0], str) + assert isinstance(result[1], str) + + +@pytest.mark.parametrize( + "user_name,owner,repo,expected_greeting,expected_repo", + [ + ("Alice", "owner1", "repo1", "Hi Alice,", "owner1/repo1"), + ("Bob Smith", "myorg", "myrepo", "Hi Bob Smith,", "myorg/myrepo"), + ("李小明", "chinese-org", "test-repo", "Hi 李小明,", "chinese-org/test-repo"), + ("O'Connor", "org", "app", "Hi O'Connor,", "org/app"), + ( + "user@example.com", + "company", + "project", + "Hi user@example.com,", + "company/project", + ), + ("123", "num-org", "num-repo", "Hi 123,", "num-org/num-repo"), + ( + "user-name_test", + "test-org", + "test_repo", + "Hi user-name_test,", + "test-org/test_repo", + ), + ], +) +def test_get_issues_disabled_email_text_parametrized( + user_name, owner, repo, expected_greeting, expected_repo +): + subject, text = get_issues_disabled_email_text(user_name, owner, repo) + + assert subject == "Enable Issues to use GitAuto" + assert expected_greeting in text + assert expected_repo in text + assert f"https://github.com/{owner}/{repo}/settings" in text diff --git a/services/supabase/users/get_user.py b/services/supabase/users/get_user.py index ccc49f619..966fafef8 100644 --- a/services/supabase/users/get_user.py +++ b/services/supabase/users/get_user.py @@ -1,4 +1,6 @@ -from typing import Any +from typing import cast + +from schemas.supabase.types import Users from services.supabase.client import supabase from utils.error.handle_exceptions import handle_exceptions @@ -12,6 +14,7 @@ def get_user(user_id: int): .execute() ) if len(data[1]) > 0: - user: dict[str, Any] = data[1][0] + user = cast(Users, data[1][0]) return user + return None diff --git a/services/webhook/schedule_handler.py b/services/webhook/schedule_handler.py index 7a9e26f6e..b90ddde5e 100644 --- a/services/webhook/schedule_handler.py +++ b/services/webhook/schedule_handler.py @@ -19,6 +19,8 @@ from services.github.trees.get_file_tree import get_file_tree # Local imports (Notifications) +from services.resend.send_email import send_email +from services.resend.text.issues_disabled_email import get_issues_disabled_email_text from services.slack.slack_notify import slack_notify # Local imports (Supabase) @@ -27,6 +29,7 @@ from services.supabase.coverages.update_issue_url import update_issue_url from services.supabase.repositories.get_repository import get_repository from services.supabase.repositories.update_repository import update_repository +from services.supabase.users.get_user import get_user from services.stripe.check_availability import check_availability # Local imports (Utils) @@ -279,6 +282,16 @@ def schedule_handler(event: EventBridgeSchedulerEvent): schedule_name = f"gitauto-repo-{owner_id}-{repo_id}" delete_scheduler(schedule_name) + # Send email notification to user + if user_id: + user = get_user(user_id=user_id) + email = user.get("email") if user else None + if email: + subject, text = get_issues_disabled_email_text( + user_name, owner_name, repo_name + ) + send_email(to=email, subject=subject, text=text) + msg = f"Issues are disabled for {owner_name}/{repo_name}. Disabled schedule trigger." logging.warning(msg) slack_notify(msg) diff --git a/services/webhook/test_schedule_handler.py b/services/webhook/test_schedule_handler.py index d41fa2f74..55f3744c5 100644 --- a/services/webhook/test_schedule_handler.py +++ b/services/webhook/test_schedule_handler.py @@ -341,6 +341,8 @@ def mock_empty_content_side_effect(file_path=None, **_): assert "src/app.ts" in call_kwargs["title"] assert result["status"] == "success" + @patch("services.webhook.schedule_handler.send_email") + @patch("services.webhook.schedule_handler.get_user") @patch("services.webhook.schedule_handler.slack_notify") @patch("services.webhook.schedule_handler.delete_scheduler") @patch("services.webhook.schedule_handler.update_repository") @@ -383,6 +385,8 @@ def test_schedule_handler_410_issues_disabled( mock_update_repository, mock_delete_scheduler, mock_slack_notify, + mock_get_user, + mock_send_email, mock_event, ): """Test that schedule_handler handles 410 (issues disabled) correctly.""" @@ -434,6 +438,9 @@ def test_schedule_handler_410_issues_disabled( # Mock create_issue to return 410 (issues disabled) mock_create_issue.return_value = (410, None) + # Mock get_user to return user with email + mock_get_user.return_value = {"email": "test@example.com", "user_id": 789} + result = schedule_handler(cast(EventBridgeSchedulerEvent, mock_event)) # Verify the function handled 410 correctly @@ -448,6 +455,19 @@ def test_schedule_handler_410_issues_disabled( # Verify that it deleted the AWS scheduler mock_delete_scheduler.assert_called_once_with("gitauto-repo-123-456") + # Verify that it sent email notification to user + mock_get_user.assert_called_once_with(user_id=789) + mock_send_email.assert_called_once() + email_call = mock_send_email.call_args + assert email_call.kwargs["to"] == "test@example.com" + assert email_call.kwargs["subject"] == "Enable Issues to use GitAuto" + assert "Hi test-user," in email_call.kwargs["text"] + assert "test-org/test-repo" in email_call.kwargs["text"] + assert ( + "https://github.com/test-org/test-repo/settings" + in email_call.kwargs["text"] + ) + # Verify that it sent a Slack notification mock_slack_notify.assert_called_once() slack_msg = mock_slack_notify.call_args[0][0]