"""Tests for the close-task owner self-service surface (/me/close-tasks + MCP).""" from __future__ import annotations import uuid import pytest from app.core.exceptions import ForbiddenError from app.domains.finance import close_self_service as selfsvc from app.domains.finance import close_task_service as svc @pytest.mark.asyncio async def test_my_close_tasks_scoped_to_owner(db_session): from app.identity.person_models import IdentityPerson pid = uuid.uuid4() db_session.add(IdentityPerson( id=pid, hris_person_id="sf-cici", work_email="chenchao@pingcap.com", )) await db_session.flush() await svc.instantiate_from_templates(db_session, "2026-06") mine = await selfsvc.my_close_tasks(db_session, person_id=str(pid)) # Cici owns exactly 4 tasks in the captured checklist. assert mine["period"] == "2026-06" assert mine["total"] == 4 assert all(t.owner_person_id == str(pid) for t in mine["tasks"]) assert mine["close_date"] is not None @pytest.mark.asyncio async def test_my_close_tasks_empty_is_warm(db_session): await svc.instantiate_from_templates(db_session, "2026-06") out = await selfsvc.my_close_tasks(db_session, person_id=str(uuid.uuid4())) assert out == {"period": None, "label": None, "close_date": None, "tasks": [], "counts": {}} @pytest.mark.asyncio async def test_owner_can_set_status_nonowner_cannot(db_session): from app.identity.person_models import IdentityPerson pid = uuid.uuid4() other = uuid.uuid4() db_session.add(IdentityPerson(id=pid, hris_person_id="sf-cici", work_email="chenchao@pingcap.com")) await db_session.flush() await svc.instantiate_from_templates(db_session, "2026-06") mine = await selfsvc.my_close_tasks(db_session, person_id=str(pid)) task = mine["tasks"][0] updated = await selfsvc.set_my_task_status( db_session, person_id=str(pid), task_id=task.id, status="completed") assert updated.status == "completed" with pytest.raises(ForbiddenError): await selfsvc.set_my_task_status( db_session, person_id=str(other), task_id=task.id, status="na") @pytest.mark.asyncio async def test_my_tasks_prefers_open_period_over_closed(db_session): """If a later period was rolled forward then closed, the owner's default period is still the latest OPEN one (their live work), not the closed one.""" from app.domains.finance import close_service from app.identity.person_models import IdentityPerson pid = uuid.uuid4() db_session.add(IdentityPerson(id=pid, hris_person_id="sf-cici", work_email="chenchao@pingcap.com")) await db_session.flush() await svc.instantiate_from_templates(db_session, "2026-06") await svc.roll_forward(db_session, from_period="2026-06", to_period="2026-07") # Close the later period 2026-07. run = await close_service.ensure_run(db_session, "2026-07") await close_service.set_status(db_session, run, new_status="closed", actor_id=None) mine = await selfsvc.my_close_tasks(db_session, person_id=str(pid)) assert mine["period"] == "2026-06" # the open one, not the closed 2026-07 @pytest.mark.asyncio async def test_http_me_close_tasks(client, finance_headers): # finance_user owns no close tasks → warm empty payload, 200. await client.post("/api/v1/finance/close/runs/2026-06/instantiate", headers=finance_headers) r = await client.get("/api/v1/me/close-tasks", headers=finance_headers) assert r.status_code == 200, r.text data = r.json()["data"] assert data["tasks"] == [] @pytest.mark.asyncio async def test_http_me_status_forbidden_on_unowned(client, finance_headers): await client.post("/api/v1/finance/close/runs/2026-06/instantiate", headers=finance_headers) r = await client.get("/api/v1/finance/close/runs/2026-06/tasks", headers=finance_headers) tid = r.json()["data"]["tasks"][0]["id"] # finance_user is not the owner of any task → 403 on the self endpoint. r = await client.post(f"/api/v1/me/close-tasks/{tid}/status", headers=finance_headers, json={"status": "completed"}) assert r.status_code == 403