diff --git a/src/firetower/integrations/services/notion.py b/src/firetower/integrations/services/notion.py index d37f0f2d..b2a35dfa 100644 --- a/src/firetower/integrations/services/notion.py +++ b/src/firetower/integrations/services/notion.py @@ -245,9 +245,11 @@ def apply_template( page_id: str, messages: list[dict[str, Any]], update_slack: bool = False, + incident: Any | None = None, ) -> None: if not update_slack and self.template_markdown: - if not self._send_markdown(page_id, self.template_markdown): + content = self._render_template(self.template_markdown, incident) + if not self._send_markdown(page_id, content): raise RuntimeError( f"Failed to apply markdown template to Notion page {page_id}" ) @@ -330,6 +332,13 @@ def apply_template( notion_index += 1 index = stopping_index + @staticmethod + def _render_template(template: str, incident: Any | None) -> str: + if incident is None: + return template + linear_url = incident.external_links_dict.get("linear", "") + return template.replace("{linear_url}", linear_url) + def _send_markdown(self, page_id: str, content: str, max_retries: int = 3) -> bool: # notion-client v3 does not wrap the Markdown API endpoint, so we call it directly. for attempt in range(max_retries): diff --git a/src/firetower/integrations/tests/test_notion.py b/src/firetower/integrations/tests/test_notion.py index f3c9eae3..90508ddc 100644 --- a/src/firetower/integrations/tests/test_notion.py +++ b/src/firetower/integrations/tests/test_notion.py @@ -356,6 +356,38 @@ def test_omits_caption_when_source_url_empty(self, notion): assert "caption" not in block["image"] +class TestRenderTemplate: + def test_returns_template_unchanged_when_no_incident(self): + result = NotionService._render_template("# Title\n{linear_url}", None) + assert result == "# Title\n{linear_url}" + + def test_replaces_linear_url_placeholder(self): + incident = MagicMock() + incident.external_links_dict = { + "linear": "https://linear.app/team/issue/INC-100" + } + result = NotionService._render_template( + "[Action Items]({linear_url})", incident + ) + assert result == "[Action Items](https://linear.app/team/issue/INC-100)" + + def test_replaces_with_empty_string_when_no_linear_link(self): + incident = MagicMock() + incident.external_links_dict = {} + result = NotionService._render_template( + "[Action Items]({linear_url})", incident + ) + assert result == "[Action Items]()" + + def test_no_placeholder_passes_through(self): + incident = MagicMock() + incident.external_links_dict = { + "linear": "https://linear.app/team/issue/INC-100" + } + result = NotionService._render_template("# No placeholders here", incident) + assert result == "# No placeholders here" + + class TestApplyTemplate: def _make_append_response(self, block_id: str) -> dict: return {"results": [{"id": block_id}]} @@ -394,6 +426,30 @@ def test_raises_when_toggle_creation_fails(self, notion): ): notion.apply_template("page-id", messages=[], update_slack=False) + def test_interpolates_incident_into_template(self): + svc = NotionService( + integration_token="test-key", + database_id="db-id", + template_markdown="# PM\n[Action Items]({linear_url})", + ) + svc.client = MagicMock() + svc.client.blocks.children.append.return_value = self._make_append_response( + "toggle-id" + ) + incident = MagicMock() + incident.external_links_dict = { + "linear": "https://linear.app/team/issue/INC-42" + } + + with patch.object(svc, "_send_markdown", return_value=True) as mock_md: + svc.apply_template( + "page-id", messages=[], update_slack=False, incident=incident + ) + + mock_md.assert_called_once_with( + "page-id", "# PM\n[Action Items](https://linear.app/team/issue/INC-42)" + ) + def test_update_slack_skips_markdown_template(self, notion): notion.client.blocks.children.append.return_value = self._make_append_response( "toggle-id" diff --git a/src/firetower/slack_app/handlers/dumpslack.py b/src/firetower/slack_app/handlers/dumpslack.py index 0e2282b8..f2918b4f 100644 --- a/src/firetower/slack_app/handlers/dumpslack.py +++ b/src/firetower/slack_app/handlers/dumpslack.py @@ -133,7 +133,9 @@ def _trigger_slack_dump(client: Any, channel_id: str, incident: Any) -> None: messages = _get_channel_messages(slack_service, channel_id) try: - notion.apply_template(page_id, messages, update_slack=update_slack) + notion.apply_template( + page_id, messages, update_slack=update_slack, incident=incident + ) except Exception: logger.exception("Failed to populate Notion page %s", page_id) try: