"""/api/v1/me/requests/detail — unified rich detail for any tracker row. Asserts: * Anonymous → 401 * employee_request row → detail with has_feishu_flow=True + lifecycle * non-Feishu source (VPN) → content_fields + synthesised timeline * a stranger's row → 404 (NotFound, not 403 — no existence oracle) * an unknown source → 404 """ from __future__ import annotations import uuid from datetime import UTC, datetime import pytest from app.domains.asset.models import EmployeeRequest from app.domains.vpn_application.models import VpnApplication from app.identity.person_models import IdentityPerson @pytest.mark.asyncio async def test_employee_request_detail_has_feishu_flow( client, employee_user, db_session, ): me, token = employee_user req = EmployeeRequest( id=uuid.uuid4(), request_number=f"REQ-{uuid.uuid4().hex[:8]}", request_type="software_seat", status="approved", requester_id=me.id, reason="need a Claude seat", created_at=datetime.now(UTC), ) db_session.add(req) await db_session.commit() db_session.expunge_all() resp = await client.get( f"/api/v1/me/requests/detail?id=employee_request:{req.id}", headers={"Authorization": f"Bearer {token}"}, ) assert resp.status_code == 200 d = resp.json()["data"] assert d["source"] == "employee_request" assert d["has_feishu_flow"] is True # approved seat normalises to completed (the grant-terminal fix). assert d["status"] == "completed" assert d["raw_status"] == "approved" assert d["requester_id"] == str(me.id) # lifecycle timeline always opens with a "submitted" event. assert d["timeline"][0]["key"] == "submitted" # reason surfaces as a content field. assert any(f["key"] == "reason" for f in d["content_fields"]) @pytest.mark.asyncio async def test_vpn_detail_synthesised_timeline(client, employee_user, db_session): me, token = employee_user vpn = VpnApplication( person_id=str(me.id), vpn_node="tokyo-gw", usage_description="dev access", resource_checklist="git, ci", status="pending", ) db_session.add(vpn) await db_session.commit() db_session.expunge_all() resp = await client.get( f"/api/v1/me/requests/detail?id=vpn:{vpn.id}", headers={"Authorization": f"Bearer {token}"}, ) assert resp.status_code == 200 d = resp.json()["data"] assert d["source"] == "vpn" assert d["has_feishu_flow"] is False assert d["status"] == "pending" fields = {f["key"]: f["value"] for f in d["content_fields"]} assert fields["vpn_node"] == "tokyo-gw" assert fields["usage_description"] == "dev access" # pending → submitted + current marker. states = [e["state"] for e in d["timeline"]] assert states == ["done", "current"] @pytest.mark.asyncio async def test_feishu_source_overlays_live_approval_flow( client, employee_user, db_session, monkeypatch, ): """A non-employee_request source carrying a Feishu instance gets the live approval flow (progress + current approver + timeline) overlaid so the requester can see WHO is currently approving.""" from app.approval.schemas import ( AdminApprovalTimelineEntry, ApprovalProgress, ApprovalProgressNode, ) from app.approval.service import ApprovalService me, token = employee_user vpn = VpnApplication( person_id=str(me.id), vpn_node="tokyo-gw", usage_description="dev access", resource_checklist="git, ci", status="pending", feishu_instance_code="INST-LIVE-1", ) db_session.add(vpn) await db_session.commit() db_session.expunge_all() async def _fake_flow(self, instance_code): assert instance_code == "INST-LIVE-1" progress = ApprovalProgress( total_nodes=2, completed_nodes=1, current_node_index=2, current_node_name="IT Review", current_approvers=["Alice Approver"], is_finished=False, nodes=[ ApprovalProgressNode(node_id="n1", name="Manager", status="APPROVED"), ApprovalProgressNode( node_id="n2", name="IT Review", status="PENDING", approvers=["Alice Approver"], ), ], ) timeline = [ AdminApprovalTimelineEntry(kind="task", status="APPROVED", node_id="n1"), ] return progress, timeline, None monkeypatch.setattr(ApprovalService, "build_live_feishu_flow", _fake_flow) resp = await client.get( f"/api/v1/me/requests/detail?id=vpn:{vpn.id}", headers={"Authorization": f"Bearer {token}"}, ) assert resp.status_code == 200 d = resp.json()["data"] assert d["approval_progress"]["current_approvers"] == ["Alice Approver"] assert d["approval_progress"]["current_node_name"] == "IT Review" assert len(d["approval_timeline"]) == 1 assert d["approval_timeline_error"] is None # the synthesised timeline is still present as the frontend fallback. assert d["timeline"][0]["key"] == "submitted" @pytest.mark.asyncio async def test_feishu_fetch_failure_degrades_gracefully( client, employee_user, db_session, monkeypatch, ): """If the live Feishu fetch fails the detail still returns 200 with the error surfaced and no progress — the frontend falls back to the synthesised timeline rather than the request 500-ing.""" from app.approval.service import ApprovalService me, token = employee_user vpn = VpnApplication( person_id=str(me.id), vpn_node="osaka-gw", usage_description="x", resource_checklist="y", status="pending", feishu_instance_code="INST-DOWN", ) db_session.add(vpn) await db_session.commit() db_session.expunge_all() async def _fail_flow(self, instance_code): return None, [], "Feishu timeout" monkeypatch.setattr(ApprovalService, "build_live_feishu_flow", _fail_flow) resp = await client.get( f"/api/v1/me/requests/detail?id=vpn:{vpn.id}", headers={"Authorization": f"Bearer {token}"}, ) assert resp.status_code == 200 d = resp.json()["data"] assert d["approval_progress"] is None assert d["approval_timeline"] == [] assert d["approval_timeline_error"] == "Feishu timeout" states = [e["state"] for e in d["timeline"]] assert states == ["done", "current"] @pytest.mark.asyncio async def test_detail_of_other_persons_row_is_404(client, employee_user, db_session): _, token = employee_user other = IdentityPerson( id=uuid.uuid4(), work_email=f"{uuid.uuid4().hex[:8]}@test.local", display_name="Other", employment_status="active", source_system="test", hris_person_id=f"sf-{uuid.uuid4().hex[:6]}", ) db_session.add(other) await db_session.flush() vpn = VpnApplication( person_id=str(other.id), vpn_node="osaka-gw", usage_description="x", resource_checklist="y", status="pending", ) db_session.add(vpn) await db_session.commit() db_session.expunge_all() resp = await client.get( f"/api/v1/me/requests/detail?id=vpn:{vpn.id}", headers={"Authorization": f"Bearer {token}"}, ) assert resp.status_code == 404 @pytest.mark.asyncio async def test_detail_unknown_source_is_404(client, employee_user): _, token = employee_user resp = await client.get( "/api/v1/me/requests/detail?id=bogus:123", headers={"Authorization": f"Bearer {token}"}, ) assert resp.status_code == 404