"""The argument coercion the MCP tools need. An HTTP caller sends a parsed UUID and a validated date; an AI client sends whatever the user said. These helpers absorb that difference — and the lookup is scoped to the caller's own assignments, so a report code belonging to somebody else answers "not found" rather than confirming the report exists. """ from __future__ import annotations import uuid from datetime import date import pytest from app.core.exceptions import NotFoundError, ValidationError from app.domains.bounty import service as svc from app.domains.bounty.mcp_support import ( parse_optional_date, resolve_my_report_id, ) from app.identity.person_models import IdentityPerson async def _person(db_session, email: str): person = IdentityPerson( id=uuid.uuid4(), work_email=email, display_name=email.split("@")[0], employment_status="active", source_system="successfactors", hris_person_id=f"sf-{email.split('@')[0]}", ) db_session.add(person) await db_session.flush() return person async def _assigned(db_session, owner, **kw): report = await svc.create_report( db_session, title=kw.pop("title", "XSS"), researcher_email=kw.pop("researcher_email", "wh@example.com"), actor=None, **kw, ) loaded = await svc.load_assignable_person(db_session, owner.id) await svc.assign_report( db_session, report, person=loaded, note=None, actor=None, ) return report @pytest.mark.asyncio async def test_resolves_a_uuid_unchanged(db_session): owner = await _person(db_session, "mcp-a@test.com") report = await _assigned(db_session, owner) assert await resolve_my_report_id( db_session, str(report.id), person_id=owner.id, ) == report.id @pytest.mark.asyncio async def test_resolves_a_report_code_case_and_padding_insensitively( db_session, ): owner = await _person(db_session, "mcp-b@test.com") report = await _assigned(db_session, owner) # "BB-2026-001" spoken back as "bb-2026-1" must still land. year = report.code.split("-")[1] for spoken in (report.code, report.code.lower(), f"bb-{year}-1"): assert await resolve_my_report_id( db_session, spoken, person_id=owner.id, ) == report.id @pytest.mark.asyncio async def test_a_code_owned_by_someone_else_is_not_found(db_session): owner = await _person(db_session, "mcp-c@test.com") stranger = await _person(db_session, "mcp-d@test.com") report = await _assigned(db_session, owner) # Not 403 — answering "you may not see BB-2026-001" would confirm the # report exists to somebody who has no business knowing that. with pytest.raises(NotFoundError): await resolve_my_report_id( db_session, report.code, person_id=stranger.id, ) @pytest.mark.asyncio async def test_garbage_is_a_validation_error_not_a_lookup(db_session): owner = await _person(db_session, "mcp-e@test.com") for bad in ("", " ", "the xss one", "BB-26-1", "1234"): with pytest.raises(ValidationError): await resolve_my_report_id(db_session, bad, person_id=owner.id) def test_parse_optional_date(): assert parse_optional_date(None, field="fix_eta") is None assert parse_optional_date(" ", field="fix_eta") is None assert parse_optional_date("2026-09-30", field="fix_eta") == date(2026, 9, 30) with pytest.raises(ValidationError) as exc: parse_optional_date("30/09/2026", field="fix_eta") # The message has to name the field and the expected shape, or the # model retries with the same wrong format. assert "fix_eta" in exc.value.message assert "YYYY-MM-DD" in exc.value.message