agnes-the-ai-analyst/tests/test_jira_service_full.py
ZdenekSrotyr 3c653b6dc2 test: add connector test suite (Block D) — 5 files, 58 tests
Tests cover Keboola extractor (extension + legacy fallback, _remote_attach),
BigQuery extractor (remote views, contract validation), Jira service
(webhook processing, HMAC verification, HTTP mocking), Jira incremental
transform (upsert/delete, monthly parquet partitioning), and LLM providers
(factory, AnthropicExtractor retry/auth, OpenAICompatExtractor strategy
cascade, JSON extraction helpers). Also adds tests/helpers/factories.py
with WebhookEventFactory.
2026-04-12 11:12:50 +02:00

182 lines
7 KiB
Python

"""Full tests for the Jira service (JiraService.process_webhook_event and friends)."""
import json
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from tests.helpers.factories import WebhookEventFactory
@pytest.fixture
def jira_env(tmp_path, monkeypatch):
"""Set up a Jira environment with required dirs and env vars."""
data_dir = tmp_path / "jira_data"
data_dir.mkdir()
(data_dir / "issues").mkdir()
monkeypatch.setenv("JIRA_DOMAIN", "mycompany.atlassian.net")
monkeypatch.setenv("JIRA_EMAIL", "bot@mycompany.com")
monkeypatch.setenv("JIRA_API_TOKEN", "test-token-xyz")
monkeypatch.setenv("JIRA_DATA_DIR", str(data_dir))
monkeypatch.setenv("JIRA_WEBHOOK_SECRET", "webhook-secret-123")
return data_dir
def _make_jira_service(jira_env):
"""Create a fresh JiraService with test configuration."""
from connectors.jira import service as svc
svc.Config.JIRA_DOMAIN = "mycompany.atlassian.net"
svc.Config.JIRA_EMAIL = "bot@mycompany.com"
svc.Config.JIRA_API_TOKEN = "test-token-xyz"
svc.Config.JIRA_DATA_DIR = jira_env
svc.Config.JIRA_WEBHOOK_SECRET = "webhook-secret-123"
svc._jira_service = None
return svc.JiraService()
def _fake_issue_data(issue_key: str = "TEST-1") -> dict:
return {
"key": issue_key,
"id": "10001",
"fields": {
"summary": "Test issue summary",
"status": {"name": "Open"},
"issuetype": {"name": "Bug"},
"attachment": [],
"comment": {"comments": []},
},
}
class TestJiraServiceWebhookProcessing:
def test_process_issue_updated_calls_fetch_and_save(self, jira_env):
"""process_webhook_event for issue_updated fetches fresh data from API."""
service = _make_jira_service(jira_env)
event_data, _, _ = WebhookEventFactory.issue_updated("PROJ-100")
issue_data = _fake_issue_data("PROJ-100")
with patch.object(service, "fetch_issue", return_value=issue_data), \
patch.object(service, "fetch_remote_links", return_value=[]), \
patch.object(service, "fetch_sla_fields", return_value=None), \
patch.object(service, "download_all_attachments", return_value=[]), \
patch("connectors.jira.service.trigger_incremental_transform", return_value=True):
result = service.process_webhook_event(event_data)
assert result is True
saved_file = jira_env / "issues" / "PROJ-100.json"
assert saved_file.exists()
with open(saved_file) as f:
saved = json.load(f)
assert saved["key"] == "PROJ-100"
def test_process_issue_deleted_marks_file(self, jira_env):
"""process_webhook_event for issue_deleted marks existing JSON with _deleted_at."""
service = _make_jira_service(jira_env)
# Pre-create the issue JSON
issue_file = jira_env / "issues" / "PROJ-200.json"
issue_file.write_text(json.dumps({"key": "PROJ-200", "fields": {}}))
event_data, _, _ = WebhookEventFactory.issue_deleted("PROJ-200")
with patch("connectors.jira.service.trigger_incremental_transform", return_value=True):
result = service.process_webhook_event(event_data)
assert result is True
with open(issue_file) as f:
saved = json.load(f)
assert "_deleted_at" in saved
def test_process_missing_issue_key_returns_false(self, jira_env):
"""Webhook event without issue key returns False."""
service = _make_jira_service(jira_env)
result = service.process_webhook_event({"webhookEvent": "jira:issue_updated"})
assert result is False
def test_process_uses_embedded_data_when_fetch_fails(self, jira_env):
"""Falls back to embedded issue data in webhook payload when API fetch fails."""
service = _make_jira_service(jira_env)
event_data = {
"webhookEvent": "jira:issue_updated",
"issue": {
"key": "PROJ-300",
"id": "10003",
"fields": {
"summary": "Embedded issue",
"attachment": [],
"comment": {"comments": []},
},
},
}
with patch.object(service, "fetch_issue", return_value=None), \
patch.object(service, "fetch_remote_links", return_value=[]), \
patch.object(service, "fetch_sla_fields", return_value=None), \
patch.object(service, "download_all_attachments", return_value=[]), \
patch("connectors.jira.service.trigger_incremental_transform", return_value=True):
result = service.process_webhook_event(event_data)
assert result is True
saved_file = jira_env / "issues" / "PROJ-300.json"
assert saved_file.exists()
def test_deletion_of_nonexistent_issue_returns_true(self, jira_env):
"""Deleting an issue that has no local file returns True (idempotent)."""
service = _make_jira_service(jira_env)
event_data, _, _ = WebhookEventFactory.issue_deleted("PROJ-NOEXIST")
result = service.process_webhook_event(event_data)
assert result is True
def test_fetch_issue_returns_none_on_404(self, jira_env):
"""fetch_issue returns None when Jira returns 404."""
import httpx
service = _make_jira_service(jira_env)
mock_response = MagicMock()
mock_response.status_code = 404
mock_client = MagicMock()
mock_client.get.return_value = mock_response
mock_client.__enter__ = lambda s: mock_client
mock_client.__exit__ = MagicMock(return_value=False)
with patch("connectors.jira.service.httpx.Client", return_value=mock_client):
result = service.fetch_issue("PROJ-MISSING")
assert result is None
def test_fetch_issue_returns_data_on_200(self, jira_env):
"""fetch_issue returns parsed JSON on HTTP 200."""
service = _make_jira_service(jira_env)
issue_data = _fake_issue_data("PROJ-42")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = issue_data
mock_client = MagicMock()
mock_client.get.return_value = mock_response
mock_client.__enter__ = lambda s: mock_client
mock_client.__exit__ = MagicMock(return_value=False)
with patch("connectors.jira.service.httpx.Client", return_value=mock_client):
result = service.fetch_issue("PROJ-42")
assert result is not None
assert result["key"] == "PROJ-42"
def test_webhook_event_factory_signature_verification(self):
"""WebhookEventFactory produces correct HMAC-SHA256 signatures."""
import hashlib
import hmac
secret = "test-secret"
event_data, payload, sig = WebhookEventFactory.issue_updated("TEST-1", secret)
expected_mac = hmac.new(
secret.encode("utf-8"), payload, hashlib.sha256
).hexdigest()
assert sig == f"sha256={expected_mac}"