Files
handbook/docker/packages/jira/tests/test_workflow_collaboration.py
2026-06-29 10:08:59 -07:00

205 lines
7.5 KiB
Python

from __future__ import annotations
import base64
import pytest
from jira_mock.server import (
add_attachment,
add_comment,
add_watcher,
create_issue,
create_status,
create_user,
export_state,
get_attachments,
get_issue,
get_transitions,
get_watchers,
import_state,
remove_watcher,
transition_issue,
upsert_workflow_transition,
)
@pytest.mark.asyncio
async def test_transitions_follow_default_workflow() -> None:
await create_issue("MOCK", "Workflow", "Task")
transitions = (await get_transitions("MOCK-1"))["transitions"]
assert transitions[0]["id"] == "1"
issue = await transition_issue("MOCK-1", "1")
assert issue["fields"]["status"]["name"] == "In Progress"
with pytest.raises(ValueError, match="not available"):
await transition_issue("MOCK-1", "1")
@pytest.mark.asyncio
async def test_empty_statuses_and_workflow_inherit_defaults() -> None:
state = await export_state()
state["statuses"] = {}
state["workflow"] = {}
state.pop("defaultStatusValue", None)
await import_state(state)
exported = await export_state()
assert exported["defaultStatusValue"] == "To Do"
assert exported["statuses"]["10001"]["name"] == "To Do"
assert "To Do" in exported["workflow"]
created = await create_issue("MOCK", "Default status after empty maps", "Task")
assert (await get_issue(created["key"], fields="status"))["fields"]["status"]["name"] == "To Do"
@pytest.mark.asyncio
async def test_empty_default_status_value_inherits_default() -> None:
state = await export_state()
state["defaultStatusValue"] = ""
await import_state(state)
assert (await export_state())["defaultStatusValue"] == "To Do"
@pytest.mark.asyncio
async def test_create_issue_uses_configured_default_status_value() -> None:
state = await export_state()
state["defaultStatusValue"] = "Backlog"
state["statuses"] = {
"20001": {
"id": "20001",
"name": "Backlog",
"description": "Ready for triage",
"statusCategory": {"id": 2, "key": "new", "name": "To Do", "colorName": "blue-gray"},
},
"20002": {
"id": "20002",
"name": "Selected",
"description": "Ready to start",
"statusCategory": {"id": 4, "key": "indeterminate", "name": "In Progress", "colorName": "yellow"},
},
}
state["workflow"] = {"Backlog": [{"id": "21", "name": "Select for Work", "to": "Selected"}], "Selected": []}
await import_state(state)
created = await create_issue("MOCK", "Starts in custom default", "Task")
assert (await get_issue(created["key"], fields="status"))["fields"]["status"]["name"] == "Backlog"
transitions = (await get_transitions(created["key"]))["transitions"]
assert transitions == [
{
"id": "21",
"name": "Select for Work",
"to": {
"id": "20002",
"name": "Selected",
"description": "Ready to start",
"iconUrl": None,
"statusCategory": {
"id": 4,
"key": "indeterminate",
"name": "In Progress",
"colorName": "yellow",
},
},
"hasScreen": False,
"isGlobal": False,
"isInitial": False,
"isAvailable": True,
"isConditional": False,
"isLooped": False,
}
]
@pytest.mark.asyncio
async def test_default_status_value_must_reference_status_and_workflow() -> None:
state = await export_state()
state["defaultStatusValue"] = "Backlog"
with pytest.raises(ValueError, match="defaultStatusValue 'Backlog' does not reference a configured status"):
await import_state(state)
state["statuses"]["20001"] = {
"id": "20001",
"name": "Backlog",
"description": "Ready for triage",
"statusCategory": {"id": 2, "key": "new", "name": "To Do", "colorName": "blue-gray"},
}
with pytest.raises(ValueError, match="defaultStatusValue 'Backlog' does not have a workflow entry"):
await import_state(state)
@pytest.mark.asyncio
async def test_admin_can_create_custom_status_and_workflow_transition() -> None:
state = await export_state()
state["is_admin"] = True
await import_state(state)
await create_issue("MOCK", "Blocked work", "Task")
status = await create_status("10005", "Blocked", "indeterminate", description="Work is blocked")
transition = await upsert_workflow_transition("To Do", "8", "Mark Blocked", "Blocked")
assert status["name"] == "Blocked"
assert transition == {"id": "8", "name": "Mark Blocked", "to": "Blocked"}
assert any(item["id"] == "8" for item in (await get_transitions("MOCK-1"))["transitions"])
assert (await transition_issue("MOCK-1", "8"))["fields"]["status"]["name"] == "Blocked"
@pytest.mark.asyncio
async def test_workflow_transition_upsert_canonicalizes_status_names() -> None:
state = await export_state()
state["is_admin"] = True
await import_state(state)
await create_issue("MOCK", "Blocked work", "Task")
await create_status("10005", "Blocked", "indeterminate", description="Work is blocked")
transition = await upsert_workflow_transition("to do", "8", "Mark Blocked", "blocked")
assert transition == {"id": "8", "name": "Mark Blocked", "to": "Blocked"}
exported = await export_state()
assert "to do" not in exported["workflow"]
assert exported["workflow"]["To Do"][-1] == {"id": "8", "name": "Mark Blocked", "to": "Blocked"}
assert any(item["id"] == "8" for item in (await get_transitions("MOCK-1"))["transitions"])
assert (await transition_issue("MOCK-1", "8"))["fields"]["status"]["name"] == "Blocked"
@pytest.mark.asyncio
async def test_non_admin_cannot_configure_statuses_or_workflows() -> None:
with pytest.raises(PermissionError, match="Admin privileges"):
await create_status("10005", "Blocked", "indeterminate")
with pytest.raises(PermissionError, match="Admin privileges"):
await upsert_workflow_transition("To Do", "8", "Mark Blocked", "Blocked")
@pytest.mark.asyncio
async def test_comments_watchers_and_attachments() -> None:
await create_issue("MOCK", "Collaboration", "Task")
comment = await add_comment("MOCK-1", "Looks good")
assert comment["body"]["content"][0]["content"][0]["text"] == "Looks good"
assert (await get_issue("MOCK-1", fields="comment"))["fields"]["comment"]["total"] == 1
await add_comment("MOCK-1", "Second")
assert "comment" not in (await export_state())["issues"]["MOCK-1"]["fields"]
assert (await get_issue("MOCK-1", fields="comment"))["fields"]["comment"]["total"] == 2
state = await export_state()
state["is_admin"] = True
await import_state(state)
await create_user("alice", "Alice")
assert (await add_watcher("MOCK-1", "alice"))["message"] == "Added watcher alice to MOCK-1"
assert (await add_watcher("MOCK-1", "alice"))["message"] == "User alice is already watching MOCK-1"
assert (await get_watchers("MOCK-1"))["watchCount"] == 1
assert (await remove_watcher("MOCK-1", "alice"))["message"] == "Removed watcher alice from MOCK-1"
payload = base64.b64encode(b"hello").decode()
attachment = await add_attachment("MOCK-1", "note.txt", payload)
assert attachment["mimeType"] == "text/plain"
attachments = await get_attachments("MOCK-1")
assert attachments["total"] == 1
assert "content" not in attachments["attachments"][0]