"""飞书审批回调 —— 两个来源共用一个执行分支。 外来单子那一半最容易被漏掉,而漏掉它的后果是**静默的**:过渡期里 从飞书表单提交的人拿不到自动化,而 Helix 这边什么都不会说。所以这个 文件里有一半的用例在打那条路。 """ from __future__ import annotations import json import uuid import pytest from app.core.exceptions import AppException from app.domains.github_org import callback as cb from app.domains.github_org import constants as const from app.domains.github_org import execution from app.domains.github_org import service as svc from app.domains.github_org.models import GithubOrgJoinRequest pytestmark = pytest.mark.asyncio _SCHEMA = { "github_login": {"id": "w_login", "type": "input"}, "org": {"id": "w_org", "type": "radioV2"}, "is_community_member": {"id": "w_comm", "type": "radioV2"}, "reason": {"id": "w_reason", "type": "textarea"}, } class _Req: """最小的 approval_request 替身 —— 回调只读 source_payload。""" def __init__(self, source_payload: dict | None = None) -> None: self.id = uuid.uuid4() self.source_payload = source_payload or {} def _detail(**kw): form = kw.pop("form", None) base = {"instance_code": "INST-1", "status": "APPROVED"} if form is not None: # ⚠️ 飞书把 form 作为 **JSON 字符串** 返回,不是 list。 # 平台为此吃过一次 P0,所以测试也照真实形状造。 base["form"] = json.dumps(form, ensure_ascii=False) base.update(kw) return base async def _org(db, **kw): payload = { "slug": "pingcap-inc", "display_name": "PingCAP Inc", "feishu_option_value": "opt-inc", } payload.update(kw) org = await svc.create_org(db, **payload) await db.commit() return org def _stub_execution(monkeypatch, outcome=execution.OUTCOME_INVITED): calls: list[dict] = [] async def _fake(db, **kwargs): calls.append(kwargs) membership = await svc.upsert_membership( db, org=kwargs["org"], github_login=kwargs["github_login"], person_id=( str(kwargs["person"].id) if kwargs.get("person") else None ), state=const.MEMBERSHIP_STATE_PENDING_INVITE, source=kwargs["source"], ) return execution.ExecutionOutcome(outcome, membership=membership) monkeypatch.setattr(cb.execution, "execute_approved", _fake) return calls def _stub_flow(monkeypatch, schema=_SCHEMA): class _Flow: payload_schema = schema external_code = "CODE" async def _get(key, *, session=None): return _Flow() import app.integrations.service as isvc monkeypatch.setattr(isvc, "get_approval_flow", _get) # ── 非通过状态 ─────────────────────────────────────────────────── async def test_rejected_touches_no_github(db_session, monkeypatch, employee_user): person, _ = employee_user org = await _org(db_session) row = GithubOrgJoinRequest( id=str(uuid.uuid4()), person_id=str(person.id), org_id=org.id, github_login="octocat", status=const.JOIN_REQUEST_STATUS_PENDING, ) db_session.add(row) await db_session.flush() calls = _stub_execution(monkeypatch) await cb.handle_feishu_callback( db_session, approval_request=_Req({"github_org_join_request_id": row.id}), instance_detail=_detail(status="REJECTED"), approval_status="rejected", ) assert calls == [], "被拒的单子不该碰 GitHub" assert row.status == const.JOIN_REQUEST_STATUS_REJECTED @pytest.mark.parametrize("feishu_status", ["PENDING", "RUNNING", "SOMETHING_NEW"]) async def test_non_terminal_callback_leaves_request_pending( db_session, monkeypatch, employee_user, feishu_status, ): """非终态回调**不许**把在审的单子翻成「已取消」。 分发器对每一次飞书实例事件都调这里 —— 提交那一刻就有一次 ``status=PENDING``。回归自 2026-09-01 生产事故:当时的写法是 「不是 approved 就是 rejected 或 cancelled」,于是提交后 2 秒单子 自己变成 cancelled,员工既看不到真实状态也撤销不了,而飞书上那张单 子还在审批中。不认得的状态同样只能是「什么都不做」。 """ person, _ = employee_user org = await _org(db_session) row = GithubOrgJoinRequest( id=str(uuid.uuid4()), person_id=str(person.id), org_id=org.id, github_login="octocat", status=const.JOIN_REQUEST_STATUS_PENDING, ) db_session.add(row) await db_session.flush() calls = _stub_execution(monkeypatch) await cb.handle_feishu_callback( db_session, approval_request=_Req({"github_org_join_request_id": row.id}), instance_detail=_detail(status=feishu_status), approval_status=( "pending" if feishu_status != "SOMETHING_NEW" else "weird_status" ), ) assert calls == [], "还没批的单子不该碰 GitHub" assert row.status == const.JOIN_REQUEST_STATUS_PENDING assert row.decided_at is None async def test_cancelled_callback_flips_to_cancelled( db_session, monkeypatch, employee_user, ): """真的撤销了,才落 cancelled。""" person, _ = employee_user org = await _org(db_session) row = GithubOrgJoinRequest( id=str(uuid.uuid4()), person_id=str(person.id), org_id=org.id, github_login="octocat", status=const.JOIN_REQUEST_STATUS_PENDING, ) db_session.add(row) await db_session.flush() calls = _stub_execution(monkeypatch) await cb.handle_feishu_callback( db_session, approval_request=_Req({"github_org_join_request_id": row.id}), instance_detail=_detail(status="CANCELED"), approval_status="cancelled", ) assert calls == [] assert row.status == const.JOIN_REQUEST_STATUS_CANCELLED assert row.decided_at is not None # ── Helix 发起 ─────────────────────────────────────────────────── async def test_local_request_executes_and_flips_status( db_session, monkeypatch, employee_user, ): person, _ = employee_user org = await _org(db_session) row = GithubOrgJoinRequest( id=str(uuid.uuid4()), person_id=str(person.id), org_id=org.id, github_login="octocat", status=const.JOIN_REQUEST_STATUS_PENDING, ) db_session.add(row) await db_session.flush() calls = _stub_execution(monkeypatch) await cb.handle_feishu_callback( db_session, approval_request=_Req({"github_org_join_request_id": row.id}), instance_detail=_detail(), approval_status="approved", ) assert len(calls) == 1 assert calls[0]["github_login"] == "octocat" assert calls[0]["org"].id == org.id assert row.status == const.JOIN_REQUEST_STATUS_APPROVED assert row.executed_at is not None async def test_execution_failure_marks_request_failed_and_raises( db_session, monkeypatch, employee_user, ): """卡住的单子必须在审批台账上看得见,不能悄悄躺成 approved。""" person, _ = employee_user org = await _org(db_session) row = GithubOrgJoinRequest( id=str(uuid.uuid4()), person_id=str(person.id), org_id=org.id, github_login="ghost", status=const.JOIN_REQUEST_STATUS_PENDING, ) db_session.add(row) await db_session.flush() async def _fail(db, **kwargs): return execution.ExecutionOutcome( execution.OUTCOME_FAILED, error="no such user", failure_kind="unknown_handle", ) monkeypatch.setattr(cb.execution, "execute_approved", _fail) with pytest.raises(AppException) as exc: await cb.handle_feishu_callback( db_session, approval_request=_Req({"github_org_join_request_id": row.id}), instance_detail=_detail(), approval_status="approved", ) assert exc.value.code == "GITHUB_INVITE_FAILED" assert row.status == const.JOIN_REQUEST_STATUS_FAILED assert "no such user" in (row.last_error or "") # ── 飞书直接提交(external origin) ─────────────────────────────── async def test_external_origin_parses_the_form(db_session, monkeypatch): """🔴 过渡期里从飞书表单提交的人,必须也能拿到自动化。 没有本地申请单,一切从表单里现场解析。少了这条路,那批人的单子会 静默地什么都不发生 —— 而那正是本来要修的那个问题。 """ org = await _org(db_session) _stub_flow(monkeypatch) calls = _stub_execution(monkeypatch) monkeypatch.setattr( cb, "_resolve_applicant_from_feishu", lambda db, detail: _none_coro(), ) await cb.handle_feishu_callback( db_session, approval_request=_Req({}), instance_detail=_detail( form=[ {"id": "w_login", "type": "input", "value": "octocat"}, {"id": "w_org", "type": "radioV2", "value": "github.com/pingcap-inc"}, {"id": "w_comm", "type": "radioV2", "value": "No"}, ], ), approval_status="approved", ) assert len(calls) == 1 assert calls[0]["github_login"] == "octocat" assert calls[0]["org"].id == org.id async def _none_coro(): return None async def test_external_origin_resolves_org_by_option_value_too( db_session, monkeypatch, ): """两种形态都要认:实例详情给的是显示文本,定义里是选项内部标识。""" org = await _org(db_session, feishu_option_value="l9i7-xyz") _stub_flow(monkeypatch) calls = _stub_execution(monkeypatch) monkeypatch.setattr( cb, "_resolve_applicant_from_feishu", lambda db, d: _none_coro(), ) await cb.handle_feishu_callback( db_session, approval_request=_Req({}), instance_detail=_detail( form=[ {"id": "w_login", "value": "octocat"}, {"id": "w_org", "value": "l9i7-xyz"}, ], ), approval_status="approved", ) assert calls[0]["org"].id == org.id async def test_unresolvable_form_raises_loudly(db_session, monkeypatch): """🔴 一张已批准却无处落地的单子必须响亮。 静默丢弃正是这套东西要消灭的那种失败。 """ await _org(db_session) _stub_flow(monkeypatch) _stub_execution(monkeypatch) monkeypatch.setattr( cb, "_resolve_applicant_from_feishu", lambda db, d: _none_coro(), ) with pytest.raises(AppException) as exc: await cb.handle_feishu_callback( db_session, approval_request=_Req({}), instance_detail=_detail( form=[{"id": "w_login", "value": "octocat"}, {"id": "w_org", "value": "github.com/unknown-org"}], ), approval_status="approved", ) assert exc.value.code == "GITHUB_CALLBACK_UNRESOLVABLE" async def test_form_is_a_json_string_not_a_list(db_session, monkeypatch): """⚠️ ``instance_detail["form"]`` 是 JSON **字符串**。 平台为此吃过一次 P0(每个 APPROVED 回调 500,飞书重投耗尽后单子卡在 pending)。这条钉住解码本身。 """ from app.domains.github_org.request_service import extract_form_values detail = {"form": json.dumps([{"id": "w_login", "value": "octocat"}])} assert extract_form_values(detail, _SCHEMA)["github_login"] == "octocat" # 坏 JSON 也不能炸 —— 它只该产出一个空解析结果,由上层的 # "unresolvable" 分支响亮地失败。 assert extract_form_values({"form": "not json"}, _SCHEMA) == {} # ── 离职闸 ─────────────────────────────────────────────────────── async def test_leaver_is_refused_even_though_approved( db_session, monkeypatch, employee_user, ): """审批可能跑了几天,批下来的时候人已经走了。""" person, _ = employee_user person.employment_status = "leaver_pending" await db_session.flush() org = await _org(db_session) row = GithubOrgJoinRequest( id=str(uuid.uuid4()), person_id=str(person.id), org_id=org.id, github_login="octocat", status=const.JOIN_REQUEST_STATUS_PENDING, ) db_session.add(row) await db_session.flush() calls = _stub_execution(monkeypatch) with pytest.raises(AppException) as exc: await cb.handle_feishu_callback( db_session, approval_request=_Req({"github_org_join_request_id": row.id}), instance_detail=_detail(), approval_status="approved", ) assert exc.value.code == "GITHUB_APPLICANT_LEFT" assert calls == [], "离职者绝不能被加进组织" assert row.status == const.JOIN_REQUEST_STATUS_FAILED