agnes-the-ai-analyst/connectors/jira/tests/test_sla_poll.py
Petr 86edd27655 Extract Jira into connectors/jira module
Move all Jira-specific code into a self-contained connector module:
- 22 files moved via git mv (transform, service, webhook, scripts,
  systemd units, tests, docs, bin helper)
- All imports updated to use connectors.jira.* paths
- Jira is now conditional: auto-detected via JIRA_DOMAIN env var
- Webapp registers Jira blueprint only when available
- Health service monitors Jira timers only when enabled
- Profiler loads Jira tables dynamically from filesystem
- Sync settings uses config-driven dependency validation
- Renamed keboola_platform_url -> custom_url in transform
- Updated deploy.sh, sudoers-deploy, backfill_gap.sh paths
- Fixed pytest.ini to skip live tests by default
2026-03-09 11:17:50 +01:00

303 lines
11 KiB
Python

"""
Tests for connectors/jira/scripts/poll_sla.py - SLA polling and self-healing logic.
Covers:
- fetch_sla_and_status: API response parsing for SLA + status fields
- update_issue_sla: self-healing, skip logic, and missing JSON handling
"""
import json
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
# Ensure project root is importable
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent.parent))
from connectors.jira.scripts.poll_sla import (
SLA_FIELDS,
STATUS_FIELDS,
fetch_sla_and_status,
update_issue_sla,
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture()
def fake_issue_json_in_progress(tmp_path: Path) -> Path:
"""
Create a temporary issues directory with a single issue JSON file
whose status is "In Progress" (statusCategory "In Progress").
Returns the raw_dir (parent of issues/).
"""
issues_dir = tmp_path / "issues"
issues_dir.mkdir()
issue_data = {
"key": "TEST-1",
"fields": {
"summary": "Test issue for SLA poll",
"status": {
"name": "In Progress",
"statusCategory": {
"name": "In Progress",
},
},
"resolution": None,
"resolutiondate": None,
"updated": "2026-01-15T10:00:00.000+0000",
"customfield_10328": None,
"customfield_10161": None,
},
}
json_path = issues_dir / "TEST-1.json"
json_path.write_text(json.dumps(issue_data, indent=2))
return tmp_path
# ---------------------------------------------------------------------------
# Test 1: fetch_sla_and_status returns all 6 field types
# ---------------------------------------------------------------------------
class TestFetchSlaAndStatus:
"""Tests for the fetch_sla_and_status function."""
@patch("connectors.jira.scripts.poll_sla.httpx.Client")
def test_returns_all_sla_and_status_fields(self, mock_client_cls: MagicMock) -> None:
"""
When the Jira API returns 200 with all requested fields,
fetch_sla_and_status should return a dict containing every
SLA_FIELD and STATUS_FIELD.
"""
api_fields = {
# SLA fields
"customfield_10328": {
"name": "Time to first response",
"ongoingCycle": {
"elapsedTime": {"millis": 120000},
"remainingTime": {"millis": 600000},
"breached": False,
},
},
"customfield_10161": {
"name": "Time to resolution",
"completedCycles": [],
"ongoingCycle": {
"elapsedTime": {"millis": 360000},
"remainingTime": {"millis": 1440000},
"breached": False,
},
},
# Status fields
"status": {
"name": "In Progress",
"statusCategory": {"name": "In Progress"},
},
"resolution": None,
"resolutiondate": None,
"updated": "2026-02-18T14:30:00.000+0000",
}
# Build mock response
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"fields": api_fields}
mock_client_instance = MagicMock()
mock_client_instance.get.return_value = mock_response
mock_client_instance.__enter__ = MagicMock(return_value=mock_client_instance)
mock_client_instance.__exit__ = MagicMock(return_value=False)
mock_client_cls.return_value = mock_client_instance
result = fetch_sla_and_status(
base_url="https://api.atlassian.com/ex/jira/fake-cloud-id/rest/api/3",
auth=("user@example.com", "fake-token"),
issue_key="TEST-1",
)
assert result is not None
# All SLA fields must be present
for field in SLA_FIELDS:
assert field in result, f"SLA field {field} missing from result"
# All STATUS fields must be present
for field in STATUS_FIELDS:
assert field in result, f"Status field {field} missing from result"
# Verify specific values
assert result["customfield_10328"]["name"] == "Time to first response"
assert result["status"]["name"] == "In Progress"
assert result["resolution"] is None
assert result["updated"] == "2026-02-18T14:30:00.000+0000"
# ---------------------------------------------------------------------------
# Test 2: update_issue_sla self-healing
# ---------------------------------------------------------------------------
class TestUpdateIssueSlaHealing:
"""Tests for self-healing when API reports an issue as resolved."""
@patch("connectors.jira.scripts.poll_sla.transform_single_issue")
@patch("connectors.jira.scripts.poll_sla.fetch_sla_and_status")
def test_self_healing_returns_healed_and_updates_json(
self,
mock_fetch: MagicMock,
mock_transform: MagicMock,
fake_issue_json_in_progress: Path,
) -> None:
"""
Given a local JSON with status "In Progress",
when the API says the issue is "Done" with resolution "Fixed",
update_issue_sla should return "healed" and the JSON file
should be updated with the new status fields.
"""
raw_dir = fake_issue_json_in_progress
# API returns resolved status
mock_fetch.return_value = {
"customfield_10328": {
"name": "Time to first response",
"completedCycles": [
{"elapsedTime": {"millis": 60000}, "breached": False}
],
},
"customfield_10161": {
"name": "Time to resolution",
"completedCycles": [
{"elapsedTime": {"millis": 300000}, "breached": False}
],
},
"status": {
"name": "Done",
"statusCategory": {"name": "Done"},
},
"resolution": {"name": "Fixed"},
"resolutiondate": "2026-02-19T16:00:00.000+0000",
"updated": "2026-02-19T16:00:01.000+0000",
}
mock_transform.return_value = True
result = update_issue_sla(
issue_key="TEST-1",
raw_dir=raw_dir,
base_url="https://api.atlassian.com/ex/jira/fake-cloud-id/rest/api/3",
auth=("user@example.com", "fake-token"),
)
assert result == "healed"
# Verify JSON was updated on disk
updated_json_path = raw_dir / "issues" / "TEST-1.json"
with open(updated_json_path) as f:
updated_data = json.load(f)
fields = updated_data["fields"]
# Status should now reflect "Done"
assert fields["status"]["statusCategory"]["name"] == "Done"
assert fields["status"]["name"] == "Done"
# Resolution should be set
assert fields["resolution"]["name"] == "Fixed"
assert fields["resolutiondate"] == "2026-02-19T16:00:00.000+0000"
# SLA fields should be updated
assert fields["customfield_10328"]["name"] == "Time to first response"
assert fields["customfield_10161"]["name"] == "Time to resolution"
# transform_single_issue should have been called once
mock_transform.assert_called_once_with(issue_key="TEST-1")
# ---------------------------------------------------------------------------
# Test 3: update_issue_sla skips when no useful data
# ---------------------------------------------------------------------------
class TestUpdateIssueSlaSkip:
"""Tests for the skip logic when SLA data is empty and status is not Done."""
@patch("connectors.jira.scripts.poll_sla.transform_single_issue")
@patch("connectors.jira.scripts.poll_sla.fetch_sla_and_status")
def test_skips_when_no_sla_data_and_not_resolved(
self,
mock_fetch: MagicMock,
mock_transform: MagicMock,
fake_issue_json_in_progress: Path,
) -> None:
"""
When fetch_sla_and_status returns fields where SLA data is absent
(null) and status is still not "Done", update_issue_sla should
return "skipped" and NOT modify the JSON or call transform.
"""
raw_dir = fake_issue_json_in_progress
# API returns empty/null SLA data, status still In Progress
mock_fetch.return_value = {
"customfield_10328": None,
"customfield_10161": None,
"status": {
"name": "In Progress",
"statusCategory": {"name": "In Progress"},
},
"resolution": None,
"resolutiondate": None,
"updated": "2026-02-18T10:00:00.000+0000",
}
result = update_issue_sla(
issue_key="TEST-1",
raw_dir=raw_dir,
base_url="https://api.atlassian.com/ex/jira/fake-cloud-id/rest/api/3",
auth=("user@example.com", "fake-token"),
)
assert result == "skipped"
# transform_single_issue should NOT have been called
mock_transform.assert_not_called()
# ---------------------------------------------------------------------------
# Test 4: update_issue_sla returns "skipped" when JSON is missing
# ---------------------------------------------------------------------------
class TestUpdateIssueSlaJsonMissing:
"""Tests for missing JSON file handling."""
@patch("connectors.jira.scripts.poll_sla.transform_single_issue")
@patch("connectors.jira.scripts.poll_sla.fetch_sla_and_status")
def test_returns_skipped_when_json_file_missing(
self,
mock_fetch: MagicMock,
mock_transform: MagicMock,
tmp_path: Path,
) -> None:
"""
When the raw JSON file for the issue does not exist,
update_issue_sla should return "skipped" immediately
without calling the API or transform.
"""
# Create the issues directory but no JSON file inside
issues_dir = tmp_path / "issues"
issues_dir.mkdir()
result = update_issue_sla(
issue_key="NONEXISTENT-999",
raw_dir=tmp_path,
base_url="https://api.atlassian.com/ex/jira/fake-cloud-id/rest/api/3",
auth=("user@example.com", "fake-token"),
)
assert result == "skipped"
# Should not have attempted to fetch or transform
mock_fetch.assert_not_called()
mock_transform.assert_not_called()