diff --git a/deploy/playbooks/03_app.yml b/deploy/playbooks/03_app.yml index eea96b3..2a90d57 100644 --- a/deploy/playbooks/03_app.yml +++ b/deploy/playbooks/03_app.yml @@ -24,17 +24,16 @@ src: ../templates/app/docker-compose.app.yml.j2 dest: ./docker-compose.yml + - name: Copy env file example + ansible.builtin.copy: + src: ../templates/app/intbot.env.example + dest: intbot.env.example + - name: Check if the env file exists ansible.builtin.stat: path: intbot.env register: env_file - - name: If env file doesn't exist - copy the example - ansible.builtin.copy: - src: ../templates/app/intbot.env.example - dest: intbot.env.example - when: not env_file.stat.exists - - name: If the env file doesn't exist - fail with error message ansible.builtin.fail: msg: "The env file doesn't exist. Please ssh, copy the example and adjust" diff --git a/intbot/core/integrations/github.py b/intbot/core/integrations/github.py index 09532be..44db66d 100644 --- a/intbot/core/integrations/github.py +++ b/intbot/core/integrations/github.py @@ -89,14 +89,18 @@ class GithubDraftIssue(BaseModel): def as_discord_message(self): return self.title + JsonType = dict[str, Any] + class GithubWebhook: """ Base class for all the other specific types of webhooks. """ - def __init__(self, action: str, headers: JsonType, content: JsonType, extra: JsonType): + def __init__( + self, action: str, headers: JsonType, content: JsonType, extra: JsonType + ): self.action = action self.headers = headers self.content = content @@ -154,7 +158,6 @@ def get_repository(self) -> GithubRepository: return GithubRepository(name="Placeholder", id="placeholder-repo") def changes(self) -> dict: - # Early return! \o/ if "changes" not in self.content: # Fallback because some webhooks just don't have changes. @@ -218,7 +221,7 @@ def prep_github_webhook(wh: Webhook): if event == "projects_v2_item": node_id = wh.content["projects_v2_item"]["node_id"] project_item = fetch_github_project_item(node_id) - wh.event = f"{event}.{wh.content['projects_v2_item']['action']}" + wh.event = f"{event}.{wh.content['action']}" wh.extra = project_item wh.save() return wh @@ -228,6 +231,7 @@ def prep_github_webhook(wh: Webhook): class GithubAPIError(Exception): """Custom exception for GithubAPI Errors""" + pass @@ -244,7 +248,9 @@ def fetch_github_project_item(item_id: str) -> dict[str, Any]: if response.status_code == 200: return response.json()["data"]["node"] else: - raise GithubAPIError(f"GitHub API error: {response.status_code} - {response.text}") + raise GithubAPIError( + f"GitHub API error: {response.status_code} - {response.text}" + ) def parse_github_webhook(wh: Webhook): diff --git a/intbot/core/migrations/0003_added_extra_field_to_webhook.py b/intbot/core/migrations/0003_added_extra_field_to_webhook.py index 5897cc2..77691fc 100644 --- a/intbot/core/migrations/0003_added_extra_field_to_webhook.py +++ b/intbot/core/migrations/0003_added_extra_field_to_webhook.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('core', '0002_test'), + ("core", "0002_test"), ] operations = [ migrations.AddField( - model_name='webhook', - name='extra', + model_name="webhook", + name="extra", field=models.JSONField(default={}), preserve_default=False, ), diff --git a/intbot/core/tasks.py b/intbot/core/tasks.py index 90487bb..619643d 100644 --- a/intbot/core/tasks.py +++ b/intbot/core/tasks.py @@ -9,7 +9,6 @@ logger = logging.getLogger() - @task def process_webhook(wh_uuid: str): wh = Webhook.objects.get(uuid=wh_uuid) @@ -65,10 +64,9 @@ def process_github_webhook(wh: Webhook): DiscordMessage.objects.create( channel_id=channel.channel_id, channel_name=channel.channel_name, - content=f"GitHub: {parsed.message}", + content=f"GitHub: {parsed.as_discord_message()}", # Mark as unsend - to be sent with the next batch sent_at=None, ) - wh.event = parsed.event_action wh.processed_at = timezone.now() wh.save() diff --git a/intbot/intbot/settings.py b/intbot/intbot/settings.py index 3d71806..90e0d5e 100644 --- a/intbot/intbot/settings.py +++ b/intbot/intbot/settings.py @@ -136,6 +136,7 @@ def get(name) -> str: return value or "" + # Discord # This is only needed if you end up running the bot locally, hence it # doesn't fail explicilty – however it does emit a warning. @@ -192,7 +193,6 @@ def get(name) -> str: WEBHOOK_INTERNAL_TOKEN = "dev-token" - elif DJANGO_ENV == "test": DEBUG = True ALLOWED_HOSTS = ["127.0.0.1", "localhost"] @@ -343,7 +343,6 @@ def get(name) -> str: ] - elif DJANGO_ENV == "build": # Currently used only for collecting staticfiles in docker DEBUG = False diff --git a/intbot/tests/test_bot/test_main.py b/intbot/tests/test_bot/test_main.py index 141f16a..108edeb 100644 --- a/intbot/tests/test_bot/test_main.py +++ b/intbot/tests/test_bot/test_main.py @@ -16,6 +16,7 @@ # it seems to fix the issue and also speed up the test from ~6s down to 1s. # Thanks to (@gbdlin) for help with debugging. + @pytest.fixture(autouse=True) def fix_async_db(request): """ diff --git a/intbot/tests/test_integrations/test_github.py b/intbot/tests/test_integrations/test_github.py index 645a4bd..da65d6f 100644 --- a/intbot/tests/test_integrations/test_github.py +++ b/intbot/tests/test_integrations/test_github.py @@ -179,7 +179,6 @@ def test_github_project_item_edited_event_no_changes(): class TestGithubProjectV2Item: - def test_changes_for_single_select(self): parser = GithubProjectV2Item( action="changed", @@ -249,7 +248,6 @@ def test_changes_for_unsupported_format(self): "field": "Randomfield", } - def test_get_project_parses_project_correctly(self, github_data): wh = Webhook( meta={"X-Github-Event": "projects_v2_item"}, @@ -285,7 +283,6 @@ def test_sender_formats_sender_correctly(self, github_data): ) gwh = parse_github_webhook(wh) - assert isinstance(gwh.sender, str) assert ( gwh.sender == "[@github-project-automation[bot]](" @@ -307,10 +304,10 @@ def test_prep_github_webhook_fetches_extra_data_for_project_v2_item(): wh = Webhook( meta={"X-Github-Event": "projects_v2_item"}, content={ + "action": "random", "projects_v2_item": { "node_id": "PVTI_random_projectItemV2ID", - "action": "random", - } + }, }, ) node = { @@ -354,7 +351,9 @@ def test_prep_github_webhook_reraises_exception_in_case_of_API_error(): respx.post(GITHUB_API_URL).mock(return_value=Response(500, json={"lol": "failed"})) - with pytest.raises(GithubAPIError, match='GitHub API error: 500 - {"lol":"failed"}'): + with pytest.raises( + GithubAPIError, match='GitHub API error: 500 - {"lol":"failed"}' + ): wh = prep_github_webhook(wh) diff --git a/intbot/tests/test_tasks.py b/intbot/tests/test_tasks.py index 689bf8d..abc4a97 100644 --- a/intbot/tests/test_tasks.py +++ b/intbot/tests/test_tasks.py @@ -1,9 +1,14 @@ import logging +from django.conf import settings import pytest +import respx +from core.integrations.github import GITHUB_API_URL from core.models import DiscordMessage, Webhook from core.tasks import process_github_webhook, process_internal_webhook, process_webhook +from django.utils import timezone from django_tasks.task import ResultStatus +from httpx import Response @pytest.mark.django_db @@ -69,3 +74,96 @@ def test_process_github_webhook_logs_unsupported_event(caplog): caplog.records[0].message == f"Not processing Github Webhook {wh.uuid}: Event `testrandom` not supported" ) + + +@pytest.mark.django_db +@respx.mock +def test_process_github_webhook_skips_a_message_when_unsupported_project( + github_data, +): + wh = Webhook.objects.create( + source="github", + event="", + meta={"X-Github-Event": "projects_v2_item"}, + content=github_data["project_v2_item.edited"], + extra={}, + ) + node = { + "project": { + "id": "PVT_Random_Project", + "title": "Random Project", + "url": "https://github.com/europython", + }, + "content": { + "__typename": "Issue", + "id": "I_randomIssueID", + "title": "Test Issue", + "url": "https://github.com/test-issue", + }, + } + + mocked_response = { + "data": { + "node": node, + } + } + + respx.post(GITHUB_API_URL).mock(return_value=Response(200, json=mocked_response)) + process_github_webhook(wh) + + # Skip the message but mark as processed + assert DiscordMessage.objects.count() == 0 + assert wh.processed_at is not None + assert wh.processed_at < timezone.now() + assert wh.event == "projects_v2_item.edited" + + +@pytest.mark.django_db +@respx.mock +def test_process_github_webhook_creates_a_message_from_supported( + github_data, +): + wh = Webhook.objects.create( + source="github", + event="", + meta={"X-Github-Event": "projects_v2_item"}, + content=github_data["project_v2_item.edited"], + extra={}, + ) + node = { + "project": { + "id": "PVT_Test_Board_Project", + "title": "Test Board Project", + "url": "https://github.com/europython", + }, + "content": { + "__typename": "Issue", + "id": "I_randomIssueID", + "title": "Test Issue", + "url": "https://github.com/test-issue", + }, + } + + mocked_response = { + "data": { + "node": node, + } + } + + respx.post(GITHUB_API_URL).mock(return_value=Response(200, json=mocked_response)) + process_github_webhook(wh) + + dm = DiscordMessage.objects.get() + assert wh.processed_at is not None + assert wh.processed_at < timezone.now() + assert wh.event == "projects_v2_item.edited" + assert dm.channel_id == settings.DISCORD_BOARD_CHANNEL_ID + assert dm.channel_name == settings.DISCORD_BOARD_CHANNEL_NAME + assert dm.content == ( + "GitHub: [@github-project-automation[bot]]" + "(https://github.com/apps/github-project-automation)" + " projects_v2_item.edited **Status** of " + "**[Test Issue](https://github.com/test-issue)**" + " from **Done** to **In progress**" + ) + assert dm.sent_at is None