"""Unit tests for the Jira REST utilities and ADF conversion.""" from __future__ import annotations from typing import Any import pytest from agent.utils import adf, jira # --- ADF conversion -------------------------------------------------------- def test_adf_to_markdown_handles_none_and_empty() -> None: assert adf.adf_to_markdown(None) == "" assert adf.adf_to_markdown({}) == "" assert adf.adf_to_markdown("not a dict") == "" def test_adf_to_markdown_paragraphs_marks_and_links() -> None: doc = { "type": "doc", "version": 1, "content": [ { "type": "paragraph", "content": [ {"type": "text", "text": "Hello "}, {"type": "text", "text": "world", "marks": [{"type": "strong"}]}, ], }, { "type": "paragraph", "content": [ { "type": "text", "text": "a link", "marks": [{"type": "link", "attrs": {"href": "https://x.com"}}], } ], }, ], } md = adf.adf_to_markdown(doc) assert "Hello **world**" in md assert "[a link](https://x.com)" in md def test_adf_to_markdown_bullet_and_code() -> None: doc = { "type": "doc", "content": [ { "type": "bulletList", "content": [ { "type": "listItem", "content": [ {"type": "paragraph", "content": [{"type": "text", "text": "one"}]} ], }, { "type": "listItem", "content": [ {"type": "paragraph", "content": [{"type": "text", "text": "two"}]} ], }, ], }, { "type": "codeBlock", "attrs": {"language": "python"}, "content": [{"type": "text", "text": "print(1)"}], }, ], } md = adf.adf_to_markdown(doc) assert "- one" in md assert "- two" in md assert "```python" in md assert "print(1)" in md def test_markdown_to_adf_structure() -> None: doc = adf.markdown_to_adf("first block\n\nsecond block") assert doc["type"] == "doc" assert doc["version"] == 1 assert len(doc["content"]) == 2 assert doc["content"][0]["content"][0]["text"] == "first block" def test_markdown_to_adf_empty_is_valid_doc() -> None: doc = adf.markdown_to_adf("") assert doc["type"] == "doc" assert doc["content"] == [{"type": "paragraph", "content": []}] def test_markdown_to_adf_multiline_block_uses_hardbreaks() -> None: doc = adf.markdown_to_adf("line one\nline two") para = doc["content"][0]["content"] assert {"type": "hardBreak"} in para # --- Jira REST utilities (mocked transport) -------------------------------- @pytest.fixture def _jira_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(jira, "JIRA_BASE_URL", "https://seahaven.atlassian.net") monkeypatch.setattr(jira, "JIRA_EMAIL", "bot@seahavenind.com") monkeypatch.setattr(jira, "JIRA_API_TOKEN", "token") def _mock_request( monkeypatch: pytest.MonkeyPatch, response: dict[str, Any] ) -> list[dict[str, Any]]: calls: list[dict[str, Any]] = [] async def fake_request(method: str, path: str, *, json=None, params=None): calls.append({"method": method, "path": path, "json": json, "params": params}) return response monkeypatch.setattr(jira, "_request", fake_request) return calls async def test_get_issue_normalizes_fields( _jira_env: None, monkeypatch: pytest.MonkeyPatch ) -> None: raw = { "key": "PROJ-123", "id": "10001", "fields": { "summary": "Fix the bug", "description": { "type": "doc", "content": [ {"type": "paragraph", "content": [{"type": "text", "text": "details"}]} ], }, "status": {"name": "In Progress"}, "assignee": {"displayName": "Ada", "emailAddress": "ada@x.com", "accountId": "acc1"}, "priority": {"name": "High"}, "labels": ["backend"], "project": {"key": "PROJ", "name": "Project"}, "issuetype": {"name": "Bug"}, }, } _mock_request(monkeypatch, raw) result = await jira.get_issue("PROJ-123") issue = result["issue"] assert issue["key"] == "PROJ-123" assert issue["title"] == "Fix the bug" assert issue["description"] == "details" assert issue["assignee"]["email"] == "ada@x.com" assert issue["project_key"] == "PROJ" assert issue["url"] == "https://seahaven.atlassian.net/browse/PROJ-123" async def test_comment_on_issue_success(_jira_env: None, monkeypatch: pytest.MonkeyPatch) -> None: calls = _mock_request(monkeypatch, {"id": "5001"}) ok = await jira.comment_on_issue("PROJ-1", "done, see PR") assert ok is True assert calls[0]["method"] == "POST" assert calls[0]["path"] == "/issue/PROJ-1/comment" # Body must be ADF, not raw markdown. assert calls[0]["json"]["body"]["type"] == "doc" async def test_comment_on_issue_error_returns_false( _jira_env: None, monkeypatch: pytest.MonkeyPatch ) -> None: _mock_request(monkeypatch, {"error": "boom"}) assert await jira.comment_on_issue("PROJ-1", "x") is False async def test_get_issue_comments_normalizes( _jira_env: None, monkeypatch: pytest.MonkeyPatch ) -> None: _mock_request( monkeypatch, { "comments": [ { "id": "1", "author": {"displayName": "Ada", "emailAddress": "ada@x.com", "accountId": "a"}, "body": { "type": "doc", "content": [ {"type": "paragraph", "content": [{"type": "text", "text": "hi"}]} ], }, } ] }, ) result = await jira.get_issue_comments("PROJ-1") assert result["comments"][0]["body"] == "hi" assert result["comments"][0]["author"]["email"] == "ada@x.com" async def test_create_issue_builds_fields(_jira_env: None, monkeypatch: pytest.MonkeyPatch) -> None: calls = _mock_request(monkeypatch, {"key": "PROJ-9", "id": "999"}) result = await jira.create_issue("PROJ", "New thing", description="body", priority="High") assert result["success"] is True assert result["issue"]["key"] == "PROJ-9" sent = calls[0]["json"]["fields"] assert sent["project"] == {"key": "PROJ"} assert sent["summary"] == "New thing" assert sent["description"]["type"] == "doc" assert sent["priority"] == {"name": "High"} async def test_update_issue_no_fields_errors(_jira_env: None) -> None: result = await jira.update_issue("PROJ-1") assert result["error"] == "No fields to update" async def test_request_without_env_returns_error(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(jira, "JIRA_BASE_URL", "") monkeypatch.setattr(jira, "JIRA_API_TOKEN", "") result = await jira._request("GET", "/issue/PROJ-1") assert "error" in result # --- issue_key validation + path-segment encoding (INJ hardening) ---------- def test_is_valid_issue_key() -> None: assert jira.is_valid_issue_key("PROJ-123") assert jira.is_valid_issue_key("OS-1") assert not jira.is_valid_issue_key("") assert not jira.is_valid_issue_key("../../../../rest/api/2/permissions") assert not jira.is_valid_issue_key("PROJ-1?expand=x") assert not jira.is_valid_issue_key("PROJ-1/comment") assert not jira.is_valid_issue_key("1-PROJ") async def test_get_issue_percent_encodes_path( _jira_env: None, monkeypatch: pytest.MonkeyPatch ) -> None: # Even if a traversal key reaches the util, the path segment is encoded so it # cannot climb out of /issue/ or inject a query. calls = _mock_request(monkeypatch, {"key": "x", "fields": {}}) await jira.get_issue("../../../../rest/api/2/permissions") assert calls[0]["path"] == "/issue/..%2F..%2F..%2F..%2Frest%2Fapi%2F2%2Fpermissions" async def test_comment_on_issue_encodes_path( _jira_env: None, monkeypatch: pytest.MonkeyPatch ) -> None: calls = _mock_request(monkeypatch, {"id": "1"}) await jira.comment_on_issue("ABC-1?expand=evil", "hi") assert calls[0]["path"] == "/issue/ABC-1%3Fexpand%3Devil/comment" async def test_get_comment_fetches_single_comment( _jira_env: None, monkeypatch: pytest.MonkeyPatch ) -> None: calls = _mock_request( monkeypatch, { "id": "10050", "author": {"displayName": "Ada", "emailAddress": "ada@x.com", "accountId": "acc"}, "body": { "type": "doc", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "hi"}]}], }, }, ) result = await jira.get_comment("PROJ-1", "10050") assert calls[0]["path"] == "/issue/PROJ-1/comment/10050" assert result["comment"]["author"]["account_id"] == "acc" assert result["comment"]["body"] == "hi"