"""Unit tests for Confluence client and dispatcher.""" import json import pytest import httpx from backend.confluence import ConfluenceClient, ConfluenceDispatcher, escape_cql_literal from backend.errors import ( ConfluenceAuthFailedError, DestinationDeniedError, InvalidInputError, UpstreamResponseTooLargeError, ) def test_escape_cql_literal(): assert escape_cql_literal('deploy "service" X') == 'deploy \\"service\\" X' assert escape_cql_literal("path\\to\\file") == "path\\\\to\\\\file" assert escape_cql_literal("line1\nline2") == "line1 line2" @pytest.mark.asyncio async def test_confluence_auth_verify_success(): async def handler(request: httpx.Request) -> httpx.Response: assert request.headers.get("Authorization") == "Bearer test-pat" assert request.url.path == "/rest/api/space" return httpx.Response(200, json={"results": [{"key": "ENG"}]}, headers={"content-type": "application/json"}) transport = httpx.MockTransport(handler) client = ConfluenceClient( base_url="https://approved.example.com", pat="test-pat", approved_origins=["https://approved.example.com"], transport=transport, ) assert await client.verify_auth() is True await client.close() @pytest.mark.asyncio async def test_confluence_auth_verify_fail(): async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(401, json={"message": "Unauthorized"}, headers={"content-type": "application/json"}) transport = httpx.MockTransport(handler) client = ConfluenceClient( base_url="https://approved.example.com", pat="invalid-pat", approved_origins=["https://approved.example.com"], transport=transport, ) with pytest.raises(ConfluenceAuthFailedError): await client.verify_auth() await client.close() @pytest.mark.asyncio async def test_confluence_redirect_denied(): async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(302, headers={"Location": "https://other.example.com/login"}) transport = httpx.MockTransport(handler) client = ConfluenceClient( base_url="https://approved.example.com", pat="test-pat", approved_origins=["https://approved.example.com"], transport=transport, ) with pytest.raises(DestinationDeniedError): await client.verify_auth() await client.close() @pytest.mark.asyncio async def test_confluence_search_and_dispatch(): async def handler(request: httpx.Request) -> httpx.Response: if request.url.path == "/rest/api/content/search": return httpx.Response( 200, json={ "results": [ { "id": "847291", "title": "Deployment Guide", "space": {"key": "OPS"}, "excerpt": "Deployment steps", } ], "totalSize": 1, }, headers={"content-type": "application/json"}, ) elif request.url.path == "/rest/api/content/847291": return httpx.Response( 200, json={ "id": "847291", "title": "Deployment Guide", "space": {"key": "OPS"}, "body": {"storage": {"value": "

Deploy service X

"}}, }, headers={"content-type": "application/json"}, ) return httpx.Response(404) transport = httpx.MockTransport(handler) client = ConfluenceClient( base_url="https://approved.example.com", pat="test-pat", approved_origins=["https://approved.example.com"], transport=transport, ) dispatcher = ConfluenceDispatcher(client, max_calls=5) # 1. Search call result, error, cache_hit = await dispatcher.dispatch( "confluence_search", {"query": "deploy service X", "limit": 10} ) assert error is None assert cache_hit is False assert len(result["pages"]) == 1 assert result["pages"][0]["page_id"] == "847291" assert result["pages"][0]["url"] == "https://approved.example.com/pages/viewpage.action?pageId=847291" assert result["pagination"]["has_more"] is False # 2. Repeated search call -> cache hit result2, error2, cache_hit2 = await dispatcher.dispatch( "confluence_search", {"query": "deploy service X", "limit": 10} ) assert cache_hit2 is True assert result2 == result # 3. View page call v_res, v_err, v_hit = await dispatcher.dispatch("confluence_view", {"page_id": "847291"}) assert v_err is None assert v_hit is False assert v_res["page_id"] == "847291" assert "Deploy service X" in v_res["markdown"] assert v_res["truncated"] is False # 4. Repeated view -> cache hit v_res2, v_err2, v_hit2 = await dispatcher.dispatch("confluence_view", {"page_id": "847291"}) assert v_hit2 is True assert v_res2 == v_res # 5. Invalid page id -> error inv_res, inv_err, _ = await dispatcher.dispatch("confluence_view", {"page_id": "invalid-id"}) assert inv_err is not None assert inv_err["code"] == "invalid_input" await client.close() @pytest.mark.asyncio async def test_confluence_call_budget(): async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(200, json={"results": []}, headers={"content-type": "application/json"}) transport = httpx.MockTransport(handler) client = ConfluenceClient( base_url="https://approved.example.com", pat="test-pat", approved_origins=["https://approved.example.com"], transport=transport, ) dispatcher = ConfluenceDispatcher(client, max_calls=2) await dispatcher.dispatch("confluence_search", {"query": "q1"}) await dispatcher.dispatch("confluence_search", {"query": "q2"}) # 3rd call should fail budget _, err, _ = await dispatcher.dispatch("confluence_search", {"query": "q3"}) assert err is not None assert "limit" in err["message"] await client.close()