LangGraph-based release automation agent with: - PR discovery (webhook + polling) - AI code review via Claude Code CLI (subscription-based) - Auto-create Jira tickets for PRs without ticket ID - Jira ticket lifecycle management (code review -> staging -> done) - CI/CD pipeline trigger, polling, and approval gates - Slack interactive messages with approval buttons - Per-repo semantic versioning - PostgreSQL persistence (threads, staging, releases) - FastAPI API (webhooks, approvals, status, manual triggers) - Docker Compose deployment 1069 tests, 95%+ coverage.
756 lines
25 KiB
Python
756 lines
25 KiB
Python
"""Tests for SlackClient and Block Kit builders. Written FIRST (TDD RED phase)."""
|
|
|
|
import json
|
|
from datetime import date
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from release_agent.exceptions import ServiceError
|
|
from release_agent.models.ticket import TicketEntry
|
|
from release_agent.tools.slack import (
|
|
SlackClient,
|
|
_build_approval_blocks,
|
|
_build_ci_status_blocks,
|
|
_build_interactive_approval_blocks,
|
|
_build_release_blocks,
|
|
_build_resolved_approval_blocks,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixture helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _make_ticket(ticket_id: str = "ALLPOST-100", summary: str = "Fix bug") -> TicketEntry:
|
|
return TicketEntry(
|
|
id=ticket_id,
|
|
summary=summary,
|
|
pr_id="PR-42",
|
|
pr_url="https://dev.azure.com/org/project/_git/repo/pullrequest/42",
|
|
pr_title="Fix bug PR",
|
|
branch=f"bug/{ticket_id}_fix-bug",
|
|
merged_at=date(2024, 1, 15),
|
|
)
|
|
|
|
|
|
def _make_transport(status: int = 200, body: bytes = b'{"ok": true}') -> httpx.MockTransport:
|
|
return httpx.MockTransport(lambda r: httpx.Response(status_code=status, content=body))
|
|
|
|
|
|
def _make_client(status: int = 200) -> SlackClient:
|
|
transport = _make_transport(status)
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
return SlackClient(
|
|
webhook_url="https://hooks.slack.com/services/T000/B000/xxxx",
|
|
http_client=http_client,
|
|
)
|
|
|
|
|
|
def _make_web_api_client(
|
|
status: int = 200,
|
|
body: bytes = b'{"ok": true, "ts": "1234567890.123456"}',
|
|
) -> SlackClient:
|
|
transport = httpx.MockTransport(lambda r: httpx.Response(status_code=status, content=body))
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
return SlackClient(
|
|
bot_token="xoxb-test-token",
|
|
channel_id="C12345678",
|
|
http_client=http_client,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _build_release_blocks tests (pure function)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestBuildReleaseBlocks:
|
|
"""Tests for the _build_release_blocks pure function."""
|
|
|
|
def test_returns_list(self) -> None:
|
|
blocks = _build_release_blocks(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=[],
|
|
)
|
|
assert isinstance(blocks, list)
|
|
|
|
def test_has_at_least_one_block(self) -> None:
|
|
blocks = _build_release_blocks(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=[],
|
|
)
|
|
assert len(blocks) >= 1
|
|
|
|
def test_repo_name_present_in_blocks(self) -> None:
|
|
blocks = _build_release_blocks(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=[],
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "my-repo" in text
|
|
|
|
def test_version_present_in_blocks(self) -> None:
|
|
blocks = _build_release_blocks(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=[],
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "v1.2.0" in text
|
|
|
|
def test_release_date_present_in_blocks(self) -> None:
|
|
blocks = _build_release_blocks(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=[],
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "2024" in text
|
|
|
|
def test_ticket_ids_present_in_blocks(self) -> None:
|
|
tickets = [_make_ticket("ALLPOST-100"), _make_ticket("ALLPOST-200")]
|
|
blocks = _build_release_blocks(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=tickets,
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "ALLPOST-100" in text
|
|
assert "ALLPOST-200" in text
|
|
|
|
def test_empty_tickets_still_valid(self) -> None:
|
|
blocks = _build_release_blocks(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=[],
|
|
)
|
|
assert len(blocks) >= 1
|
|
|
|
def test_blocks_are_dicts(self) -> None:
|
|
blocks = _build_release_blocks(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=[_make_ticket()],
|
|
)
|
|
assert all(isinstance(b, dict) for b in blocks)
|
|
|
|
def test_each_block_has_type_key(self) -> None:
|
|
blocks = _build_release_blocks(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=[],
|
|
)
|
|
for block in blocks:
|
|
assert "type" in block
|
|
|
|
def test_ticket_summaries_included(self) -> None:
|
|
tickets = [_make_ticket("ALLPOST-100", "Fix the auth bug")]
|
|
blocks = _build_release_blocks(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=tickets,
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "Fix the auth bug" in text
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _build_approval_blocks tests (pure function)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestBuildApprovalBlocks:
|
|
"""Tests for the _build_approval_blocks pure function."""
|
|
|
|
def test_returns_list(self) -> None:
|
|
blocks = _build_approval_blocks(
|
|
action="Deploy to Production",
|
|
details="v1.2.0 for my-repo",
|
|
approval_url="https://dev.azure.com/approve/123",
|
|
)
|
|
assert isinstance(blocks, list)
|
|
|
|
def test_has_at_least_one_block(self) -> None:
|
|
blocks = _build_approval_blocks(
|
|
action="Deploy",
|
|
details="v1.0.0",
|
|
approval_url="https://example.com",
|
|
)
|
|
assert len(blocks) >= 1
|
|
|
|
def test_action_present_in_blocks(self) -> None:
|
|
blocks = _build_approval_blocks(
|
|
action="Deploy to Production",
|
|
details="v1.2.0",
|
|
approval_url="https://example.com",
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "Deploy to Production" in text
|
|
|
|
def test_details_present_in_blocks(self) -> None:
|
|
blocks = _build_approval_blocks(
|
|
action="Deploy",
|
|
details="version v1.2.0 of my-repo",
|
|
approval_url="https://example.com",
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "version v1.2.0 of my-repo" in text
|
|
|
|
def test_approval_url_present_in_blocks(self) -> None:
|
|
blocks = _build_approval_blocks(
|
|
action="Deploy",
|
|
details="details",
|
|
approval_url="https://dev.azure.com/approve/abc",
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "https://dev.azure.com/approve/abc" in text
|
|
|
|
def test_blocks_are_dicts(self) -> None:
|
|
blocks = _build_approval_blocks(
|
|
action="Deploy",
|
|
details="details",
|
|
approval_url="https://example.com",
|
|
)
|
|
assert all(isinstance(b, dict) for b in blocks)
|
|
|
|
def test_each_block_has_type_key(self) -> None:
|
|
blocks = _build_approval_blocks(
|
|
action="Deploy",
|
|
details="details",
|
|
approval_url="https://example.com",
|
|
)
|
|
for block in blocks:
|
|
assert "type" in block
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SlackClient.send_release_notification tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSendReleaseNotification:
|
|
"""Tests for SlackClient.send_release_notification."""
|
|
|
|
async def test_returns_true_on_success(self) -> None:
|
|
client = _make_client(status=200)
|
|
|
|
result = await client.send_release_notification(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=[_make_ticket()],
|
|
)
|
|
assert result is True
|
|
|
|
async def test_returns_true_with_empty_tickets(self) -> None:
|
|
client = _make_client(status=200)
|
|
|
|
result = await client.send_release_notification(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=[],
|
|
)
|
|
assert result is True
|
|
|
|
async def test_500_raises_service_error(self) -> None:
|
|
client = _make_client(status=500)
|
|
|
|
with pytest.raises(ServiceError):
|
|
await client.send_release_notification(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=[],
|
|
)
|
|
|
|
async def test_sends_post_request(self) -> None:
|
|
requests_captured: list[httpx.Request] = []
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
requests_captured.append(request)
|
|
return httpx.Response(200, content=b'{"ok": true}')
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
client = SlackClient(
|
|
webhook_url="https://hooks.slack.com/services/T000/B000/xxxx",
|
|
http_client=http_client,
|
|
)
|
|
|
|
await client.send_release_notification(
|
|
repo="my-repo",
|
|
version="v1.2.0",
|
|
release_date=date(2024, 1, 15),
|
|
tickets=[],
|
|
)
|
|
|
|
assert len(requests_captured) == 1
|
|
assert requests_captured[0].method == "POST"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SlackClient.send_approval_request tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSendApprovalRequest:
|
|
"""Tests for SlackClient.send_approval_request."""
|
|
|
|
async def test_returns_true_on_success(self) -> None:
|
|
client = _make_client(status=200)
|
|
|
|
result = await client.send_approval_request(
|
|
action="Deploy to Production",
|
|
details="v1.2.0 for my-repo",
|
|
approval_url="https://dev.azure.com/approve/123",
|
|
)
|
|
assert result is True
|
|
|
|
async def test_500_raises_service_error(self) -> None:
|
|
client = _make_client(status=500)
|
|
|
|
with pytest.raises(ServiceError):
|
|
await client.send_approval_request(
|
|
action="Deploy",
|
|
details="v1.0.0",
|
|
approval_url="https://example.com",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SlackClient lifecycle tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSlackClientLifecycle:
|
|
"""Tests for SlackClient close() and context manager."""
|
|
|
|
async def test_close_closes_http_client(self) -> None:
|
|
transport = _make_transport()
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
client = SlackClient(
|
|
webhook_url="https://hooks.slack.com/services/T000/B000/xxxx",
|
|
http_client=http_client,
|
|
)
|
|
|
|
await client.close()
|
|
|
|
assert http_client.is_closed
|
|
|
|
async def test_context_manager_closes_client(self) -> None:
|
|
transport = _make_transport()
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
async with SlackClient(
|
|
webhook_url="https://hooks.slack.com/services/T000/B000/xxxx",
|
|
http_client=http_client,
|
|
) as client:
|
|
assert client is not None
|
|
assert http_client.is_closed
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SlackClient dual-mode construction tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSlackClientDualMode:
|
|
"""Tests for dual-mode SlackClient (webhook vs Web API)."""
|
|
|
|
def test_can_be_created_with_webhook_only(self) -> None:
|
|
transport = _make_transport()
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
client = SlackClient(
|
|
webhook_url="https://hooks.slack.com/services/T000/B000/xxxx",
|
|
http_client=http_client,
|
|
)
|
|
assert client is not None
|
|
|
|
def test_can_be_created_with_bot_token_and_channel(self) -> None:
|
|
transport = _make_transport()
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
client = SlackClient(
|
|
bot_token="xoxb-test",
|
|
channel_id="C12345",
|
|
http_client=http_client,
|
|
)
|
|
assert client is not None
|
|
|
|
def test_can_be_created_with_all_params(self) -> None:
|
|
transport = _make_transport()
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
client = SlackClient(
|
|
webhook_url="https://hooks.slack.com/services/T000/B000/xxxx",
|
|
bot_token="xoxb-test",
|
|
channel_id="C12345",
|
|
http_client=http_client,
|
|
)
|
|
assert client is not None
|
|
|
|
def test_can_be_created_with_no_url_params(self) -> None:
|
|
transport = _make_transport()
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
client = SlackClient(http_client=http_client)
|
|
assert client is not None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SlackClient.send_interactive_approval tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSendInteractiveApproval:
|
|
"""Tests for SlackClient.send_interactive_approval."""
|
|
|
|
async def test_returns_message_ts_on_success(self) -> None:
|
|
client = _make_web_api_client()
|
|
|
|
result = await client.send_interactive_approval(
|
|
thread_id="thread-abc",
|
|
action="Deploy to Sandbox",
|
|
details="Release v1.0.0 of my-repo",
|
|
buttons=[{"text": "Approve", "value": "approve"}, {"text": "Reject", "value": "reject"}],
|
|
)
|
|
assert isinstance(result, str)
|
|
assert result == "1234567890.123456"
|
|
|
|
async def test_returns_empty_string_on_api_error(self) -> None:
|
|
client = _make_web_api_client(status=200, body=b'{"ok": false, "error": "channel_not_found"}')
|
|
|
|
result = await client.send_interactive_approval(
|
|
thread_id="thread-abc",
|
|
action="Deploy",
|
|
details="v1.0.0",
|
|
buttons=[],
|
|
)
|
|
assert result == ""
|
|
|
|
async def test_posts_to_chat_postmessage(self) -> None:
|
|
captured_urls: list[str] = []
|
|
captured_bodies: list[dict] = []
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
captured_urls.append(str(request.url))
|
|
captured_bodies.append(json.loads(request.content))
|
|
return httpx.Response(200, content=b'{"ok": true, "ts": "111.222"}')
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
client = SlackClient(
|
|
bot_token="xoxb-test",
|
|
channel_id="C99999",
|
|
http_client=http_client,
|
|
)
|
|
|
|
await client.send_interactive_approval(
|
|
thread_id="thread-xyz",
|
|
action="Deploy",
|
|
details="details",
|
|
buttons=[],
|
|
)
|
|
|
|
assert any("chat.postMessage" in url for url in captured_urls)
|
|
assert captured_bodies[0]["channel"] == "C99999"
|
|
|
|
async def test_includes_thread_id_in_blocks(self) -> None:
|
|
captured_bodies: list[dict] = []
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
captured_bodies.append(json.loads(request.content))
|
|
return httpx.Response(200, content=b'{"ok": true, "ts": "111.222"}')
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
client = SlackClient(bot_token="xoxb-test", channel_id="C1", http_client=http_client)
|
|
|
|
await client.send_interactive_approval(
|
|
thread_id="my-thread-id",
|
|
action="Deploy",
|
|
details="v1.0.0",
|
|
buttons=[{"text": "Approve", "value": "approve"}],
|
|
)
|
|
|
|
body_str = json.dumps(captured_bodies)
|
|
assert "my-thread-id" in body_str
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SlackClient.update_message tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestUpdateMessage:
|
|
"""Tests for SlackClient.update_message."""
|
|
|
|
async def test_returns_true_on_success(self) -> None:
|
|
client = _make_web_api_client(body=b'{"ok": true}')
|
|
|
|
result = await client.update_message(
|
|
message_ts="1234567890.123456",
|
|
text="Updated message",
|
|
blocks=[],
|
|
)
|
|
assert result is True
|
|
|
|
async def test_returns_false_on_api_error(self) -> None:
|
|
client = _make_web_api_client(body=b'{"ok": false, "error": "message_not_found"}')
|
|
|
|
result = await client.update_message(
|
|
message_ts="bad-ts",
|
|
text="Update",
|
|
blocks=[],
|
|
)
|
|
assert result is False
|
|
|
|
async def test_posts_to_chat_update(self) -> None:
|
|
captured_urls: list[str] = []
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
captured_urls.append(str(request.url))
|
|
return httpx.Response(200, content=b'{"ok": true}')
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
http_client = httpx.AsyncClient(transport=transport)
|
|
client = SlackClient(bot_token="xoxb-test", channel_id="C1", http_client=http_client)
|
|
|
|
await client.update_message(message_ts="ts-abc", text="Hello", blocks=[])
|
|
|
|
assert any("chat.update" in url for url in captured_urls)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SlackClient.send_notification tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSendNotification:
|
|
"""Tests for SlackClient.send_notification."""
|
|
|
|
async def test_returns_true_via_web_api(self) -> None:
|
|
client = _make_web_api_client(body=b'{"ok": true, "ts": "111.222"}')
|
|
|
|
result = await client.send_notification(text="Build passed", blocks=[])
|
|
assert result is True
|
|
|
|
async def test_returns_true_via_webhook(self) -> None:
|
|
client = _make_client(status=200)
|
|
|
|
result = await client.send_notification(text="Build passed", blocks=[])
|
|
assert result is True
|
|
|
|
async def test_returns_false_on_web_api_error(self) -> None:
|
|
client = _make_web_api_client(body=b'{"ok": false, "error": "invalid_auth"}')
|
|
|
|
result = await client.send_notification(text="Build passed", blocks=[])
|
|
assert result is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _build_interactive_approval_blocks tests (pure function)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestBuildInteractiveApprovalBlocks:
|
|
"""Tests for _build_interactive_approval_blocks pure function."""
|
|
|
|
def test_returns_list(self) -> None:
|
|
blocks = _build_interactive_approval_blocks(
|
|
thread_id="t1",
|
|
action="Deploy to Sandbox",
|
|
details="v1.0.0",
|
|
buttons=[{"text": "Approve", "value": "approve"}],
|
|
)
|
|
assert isinstance(blocks, list)
|
|
|
|
def test_has_at_least_one_block(self) -> None:
|
|
blocks = _build_interactive_approval_blocks(
|
|
thread_id="t1",
|
|
action="Deploy",
|
|
details="details",
|
|
buttons=[],
|
|
)
|
|
assert len(blocks) >= 1
|
|
|
|
def test_action_present_in_blocks(self) -> None:
|
|
blocks = _build_interactive_approval_blocks(
|
|
thread_id="t1",
|
|
action="Deploy to Production",
|
|
details="v1.2.0",
|
|
buttons=[],
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "Deploy to Production" in text
|
|
|
|
def test_thread_id_in_button_value(self) -> None:
|
|
blocks = _build_interactive_approval_blocks(
|
|
thread_id="my-unique-thread",
|
|
action="Deploy",
|
|
details="details",
|
|
buttons=[{"text": "Approve", "value": "approve"}],
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "my-unique-thread" in text
|
|
|
|
def test_buttons_render_as_actions_block(self) -> None:
|
|
blocks = _build_interactive_approval_blocks(
|
|
thread_id="t1",
|
|
action="Deploy",
|
|
details="details",
|
|
buttons=[
|
|
{"text": "Approve", "value": "approve"},
|
|
{"text": "Reject", "value": "reject"},
|
|
],
|
|
)
|
|
block_types = [b["type"] for b in blocks]
|
|
assert "actions" in block_types
|
|
|
|
def test_empty_buttons_still_valid(self) -> None:
|
|
blocks = _build_interactive_approval_blocks(
|
|
thread_id="t1",
|
|
action="Deploy",
|
|
details="details",
|
|
buttons=[],
|
|
)
|
|
assert isinstance(blocks, list)
|
|
|
|
def test_details_present_in_blocks(self) -> None:
|
|
blocks = _build_interactive_approval_blocks(
|
|
thread_id="t1",
|
|
action="Deploy",
|
|
details="Release v2.0.0 of my-service",
|
|
buttons=[],
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "Release v2.0.0 of my-service" in text
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _build_ci_status_blocks tests (pure function)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestBuildCiStatusBlocks:
|
|
"""Tests for _build_ci_status_blocks pure function."""
|
|
|
|
def test_returns_list(self) -> None:
|
|
blocks = _build_ci_status_blocks(
|
|
repo="my-repo",
|
|
branch="main",
|
|
status="succeeded",
|
|
build_url="https://dev.azure.com/org/proj/_build/results?buildId=42",
|
|
)
|
|
assert isinstance(blocks, list)
|
|
|
|
def test_repo_present(self) -> None:
|
|
blocks = _build_ci_status_blocks(
|
|
repo="my-service",
|
|
branch="main",
|
|
status="succeeded",
|
|
build_url=None,
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "my-service" in text
|
|
|
|
def test_branch_present(self) -> None:
|
|
blocks = _build_ci_status_blocks(
|
|
repo="my-repo",
|
|
branch="release/v1.0.0",
|
|
status="succeeded",
|
|
build_url=None,
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "release/v1.0.0" in text
|
|
|
|
def test_status_present(self) -> None:
|
|
blocks = _build_ci_status_blocks(
|
|
repo="my-repo",
|
|
branch="main",
|
|
status="failed",
|
|
build_url=None,
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "failed" in text
|
|
|
|
def test_build_url_present_when_provided(self) -> None:
|
|
url = "https://dev.azure.com/org/proj/_build/results?buildId=99"
|
|
blocks = _build_ci_status_blocks(
|
|
repo="my-repo",
|
|
branch="main",
|
|
status="succeeded",
|
|
build_url=url,
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert url in text
|
|
|
|
def test_build_url_none_does_not_crash(self) -> None:
|
|
blocks = _build_ci_status_blocks(
|
|
repo="my-repo",
|
|
branch="main",
|
|
status="succeeded",
|
|
build_url=None,
|
|
)
|
|
assert isinstance(blocks, list)
|
|
|
|
def test_all_blocks_are_dicts(self) -> None:
|
|
blocks = _build_ci_status_blocks(
|
|
repo="my-repo",
|
|
branch="main",
|
|
status="succeeded",
|
|
build_url=None,
|
|
)
|
|
assert all(isinstance(b, dict) for b in blocks)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _build_resolved_approval_blocks tests (pure function)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestBuildResolvedApprovalBlocks:
|
|
"""Tests for _build_resolved_approval_blocks pure function."""
|
|
|
|
def test_returns_list(self) -> None:
|
|
blocks = _build_resolved_approval_blocks(
|
|
action="Deploy to Sandbox",
|
|
outcome="approved",
|
|
user="alice",
|
|
)
|
|
assert isinstance(blocks, list)
|
|
|
|
def test_action_present(self) -> None:
|
|
blocks = _build_resolved_approval_blocks(
|
|
action="Deploy to Production",
|
|
outcome="approved",
|
|
user="alice",
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "Deploy to Production" in text
|
|
|
|
def test_outcome_present(self) -> None:
|
|
blocks = _build_resolved_approval_blocks(
|
|
action="Deploy",
|
|
outcome="rejected",
|
|
user="bob",
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "rejected" in text
|
|
|
|
def test_user_present(self) -> None:
|
|
blocks = _build_resolved_approval_blocks(
|
|
action="Deploy",
|
|
outcome="approved",
|
|
user="charlie",
|
|
)
|
|
text = json.dumps(blocks)
|
|
assert "charlie" in text
|
|
|
|
def test_all_blocks_are_dicts(self) -> None:
|
|
blocks = _build_resolved_approval_blocks(
|
|
action="Deploy",
|
|
outcome="approved",
|
|
user="dave",
|
|
)
|
|
assert all(isinstance(b, dict) for b in blocks)
|