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.
182 lines
7 KiB
Python
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}"
|