205 lines
7.5 KiB
Python
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]
|