"""Tests for ticket_export service — Notion and Jira exporters.""" from __future__ import annotations from unittest.mock import MagicMock, patch import pytest INCIDENT = { "id": "inc-1", "label": "Redis OOM — key eviction flood", "issue_type": "memory", "started_at": "2026-06-01T03:00:00Z", "ended_at": "2026-06-01T03:45:00Z", "notes": "Triggered by batch job at 03:00", "severity": "high", } ENTRIES = [ {"entry_id": "e1", "source_id": "host:redis", "severity": "ERROR", "text": "maxmemory reached, evicting keys"}, {"entry_id": "e2", "source_id": "host:app", "severity": "WARN", "text": "Redis NOEVICTION response"}, ] def _mock_response(status_code: int, body: dict): resp = MagicMock() resp.is_success = (status_code < 400) resp.status_code = status_code resp.json.return_value = body resp.text = str(body) return resp # --------------------------------------------------------------------------- # available_targets # --------------------------------------------------------------------------- def test_available_targets_lists_known_integrations(): from app.services.ticket_export import available_targets targets = available_targets() assert "notion" in targets assert "jira" in targets # --------------------------------------------------------------------------- # export_incident dispatch # --------------------------------------------------------------------------- def test_export_incident_raises_for_unknown_target(): from app.services.ticket_export import export_incident with pytest.raises(ValueError, match="Unknown ticket target"): export_incident("linear", INCIDENT, ENTRIES, {}) # --------------------------------------------------------------------------- # Notion exporter # --------------------------------------------------------------------------- class TestNotionExport: def test_successful_export_returns_url_and_id(self): from app.services.ticket_export import export_incident page_id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" mock_resp = _mock_response(200, {"id": page_id, "url": f"https://notion.so/{page_id}"}) with patch("app.services.ticket_export.httpx.post", return_value=mock_resp) as mock_post: result = export_incident("notion", INCIDENT, ENTRIES, { "notion_token": "secret_abc123", "notion_database_id": "db-id-xyz", }) assert result["ticket_id"] == page_id assert "notion.so" in result["url"] mock_post.assert_called_once() def test_raises_value_error_when_not_configured(self): from app.services.ticket_export import export_incident with pytest.raises(ValueError, match="Notion not configured"): export_incident("notion", INCIDENT, ENTRIES, { "notion_token": "", "notion_database_id": "db-id", }) def test_raises_value_error_when_database_id_missing(self): from app.services.ticket_export import export_incident with pytest.raises(ValueError, match="Notion not configured"): export_incident("notion", INCIDENT, ENTRIES, { "notion_token": "secret_abc", "notion_database_id": "", }) def test_raises_runtime_error_on_api_error(self): from app.services.ticket_export import export_incident mock_resp = _mock_response(401, {"message": "Unauthorized"}) with patch("app.services.ticket_export.httpx.post", return_value=mock_resp): with pytest.raises(RuntimeError, match="Notion API error 401"): export_incident("notion", INCIDENT, ENTRIES, { "notion_token": "bad-token", "notion_database_id": "db-id", }) def test_sends_correct_database_id(self): from app.services.ticket_export import export_incident db_id = "my-database-uuid" mock_resp = _mock_response(200, {"id": "page-id", "url": "https://notion.so/page-id"}) with patch("app.services.ticket_export.httpx.post", return_value=mock_resp) as mock_post: export_incident("notion", INCIDENT, ENTRIES, { "notion_token": "secret_abc123", "notion_database_id": db_id, }) call_kwargs = mock_post.call_args payload = call_kwargs.kwargs.get("json") or call_kwargs.args[1] if len(call_kwargs.args) > 1 else call_kwargs.kwargs["json"] assert payload["parent"]["database_id"] == db_id def test_incident_label_becomes_page_title(self): from app.services.ticket_export import export_incident mock_resp = _mock_response(200, {"id": "pid", "url": "https://notion.so/pid"}) with patch("app.services.ticket_export.httpx.post", return_value=mock_resp) as mock_post: export_incident("notion", INCIDENT, ENTRIES, { "notion_token": "tok", "notion_database_id": "dbid", }) payload = mock_post.call_args.kwargs["json"] title_text = payload["properties"]["title"]["title"][0]["text"]["content"] assert INCIDENT["label"] in title_text def test_url_falls_back_to_constructed_url(self): from app.services.ticket_export import export_incident page_id = "abc123" mock_resp = _mock_response(200, {"id": page_id}) # no 'url' in response with patch("app.services.ticket_export.httpx.post", return_value=mock_resp): result = export_incident("notion", INCIDENT, ENTRIES, { "notion_token": "tok", "notion_database_id": "dbid", }) assert "notion.so" in result["url"] or page_id in result["url"] def test_long_text_truncated_to_notion_limit(self): from app.services.ticket_export import export_incident mock_resp = _mock_response(200, {"id": "pid", "url": "https://notion.so/pid"}) long_entries = [{"entry_id": f"e{i}", "source_id": "host:svc", "severity": "ERROR", "text": "x" * 300} for i in range(60)] with patch("app.services.ticket_export.httpx.post", return_value=mock_resp) as mock_post: export_incident("notion", INCIDENT, long_entries, { "notion_token": "tok", "notion_database_id": "dbid", }) payload = mock_post.call_args.kwargs["json"] for block in payload.get("children", []): for rt in block.get("bulleted_list_item", {}).get("rich_text", []): assert len(rt["text"]["content"]) <= 2000 # --------------------------------------------------------------------------- # Jira exporter # --------------------------------------------------------------------------- class TestJiraExport: _config = { "jira_url": "https://myorg.atlassian.net", "jira_email": "ops@example.com", "jira_api_token": "ATATT3xFfGF0abc123", "jira_project_key": "OPS", "jira_issue_type": "Bug", } def test_successful_export_returns_url_and_key(self): from app.services.ticket_export import export_incident mock_resp = _mock_response(201, {"id": "10042", "key": "OPS-42", "self": "https://myorg.atlassian.net/rest/api/3/issue/10042"}) with patch("app.services.ticket_export.httpx.post", return_value=mock_resp): result = export_incident("jira", INCIDENT, ENTRIES, self._config) assert result["ticket_id"] == "OPS-42" assert "OPS-42" in result["url"] assert "myorg.atlassian.net" in result["url"] def test_raises_value_error_when_not_configured(self): from app.services.ticket_export import export_incident with pytest.raises(ValueError, match="Jira not configured"): export_incident("jira", INCIDENT, ENTRIES, { "jira_url": "", "jira_email": "a@b.com", "jira_api_token": "tok", "jira_project_key": "OPS", }) def test_raises_runtime_error_on_api_error(self): from app.services.ticket_export import export_incident mock_resp = _mock_response(403, {"errorMessages": ["Forbidden"]}) with patch("app.services.ticket_export.httpx.post", return_value=mock_resp): with pytest.raises(RuntimeError, match="Jira API error 403"): export_incident("jira", INCIDENT, ENTRIES, self._config) def test_sends_basic_auth_header(self): from app.services.ticket_export import export_incident import base64 mock_resp = _mock_response(201, {"key": "OPS-1", "id": "1"}) with patch("app.services.ticket_export.httpx.post", return_value=mock_resp) as mock_post: export_incident("jira", INCIDENT, ENTRIES, self._config) call_kwargs = mock_post.call_args.kwargs auth_header = call_kwargs["headers"]["Authorization"] assert auth_header.startswith("Basic ") decoded = base64.b64decode(auth_header[6:]).decode() assert "ops@example.com" in decoded def test_uses_correct_project_key(self): from app.services.ticket_export import export_incident mock_resp = _mock_response(201, {"key": "OPS-7", "id": "7"}) with patch("app.services.ticket_export.httpx.post", return_value=mock_resp) as mock_post: export_incident("jira", INCIDENT, ENTRIES, self._config) payload = mock_post.call_args.kwargs["json"] assert payload["fields"]["project"]["key"] == "OPS" def test_incident_label_becomes_summary(self): from app.services.ticket_export import export_incident mock_resp = _mock_response(201, {"key": "OPS-8", "id": "8"}) with patch("app.services.ticket_export.httpx.post", return_value=mock_resp) as mock_post: export_incident("jira", INCIDENT, ENTRIES, self._config) payload = mock_post.call_args.kwargs["json"] assert payload["fields"]["summary"] == INCIDENT["label"] def test_default_issue_type_is_bug(self): from app.services.ticket_export import export_incident config = {k: v for k, v in self._config.items() if k != "jira_issue_type"} mock_resp = _mock_response(201, {"key": "OPS-9", "id": "9"}) with patch("app.services.ticket_export.httpx.post", return_value=mock_resp) as mock_post: export_incident("jira", INCIDENT, ENTRIES, config) payload = mock_post.call_args.kwargs["json"] assert payload["fields"]["issuetype"]["name"] == "Bug"