"""合作方通讯录驱动:飞书 wire 形状 → 名册记录。 这个文件全部是纯解析,不碰 DB 也不碰网络 —— 但它守的是这套东西最容易 悄悄坏掉的那一层。飞书在权限不足时返回的是 ``code: 0`` 加**字段缺席**, 所以「解析出空值」和「对方没填」在响应里长得一模一样;判断逻辑写错不会 抛异常,只会让名册静静地少一列。 """ from __future__ import annotations from app.integrations.drivers.feishu_partner_directory import ( FeishuPartnerDirectoryDriver, _attr_text, _clean_github_login, _dept_path, _first_i18n, ) GITHUB_ATTR_ID = "C-7356820809171140610" # CBG 租户里「GitHub ID」的真实 id def _driver(**config): return FeishuPartnerDirectoryDriver( integration_id="test", config={ "app_id": "cli_x", "app_secret": "s", "partner_email_domain": "pingcap.cn", "local_email_domain": "pingcap.com", **config, }, ) def test_first_i18n_prefers_default_over_blank_localised(): """🔴 「取第一个非空值」会在 CBG 的云学堂字段上挑到空串。 实测 CBG 租户返回的形状:``default`` 有值,而 ``en_us`` / ``ja_jp`` 是空字符串。按 locale 顺序取才对。 """ assert _first_i18n({ "i18n_name": [ {"locale": "default", "value": "云学堂-禁用标记"}, {"locale": "en_us", "value": ""}, {"locale": "ja_jp", "value": ""}, {"locale": "zh_cn", "value": ""}, ], }) == "云学堂-禁用标记" assert _first_i18n({"i18n_name": [ {"locale": "default", "value": "GitHub ID"}, ]}) == "GitHub ID" assert _first_i18n({}) == "" def test_attr_text_skips_non_scalar_values(): """GENERIC_USER 之类没有标量读法的字段必须返回空串。 返回 ``str(value)`` 会把一个 dict 的 repr 塞进 ``github_login``, 而那一列是要拿去和 GitHub handle 比对的。 """ assert _attr_text({"type": "TEXT", "value": {"text": " octocat "}}) == "octocat" assert _attr_text({"type": "GENERIC_USER", "value": { "generic_user": {"id": "ou_x", "type": 1}, }}) == "" assert _attr_text({"type": "TEXT", "value": {}}) == "" assert _attr_text({"type": "TEXT"}) == "" def test_to_record_maps_the_real_feishu_shape(): driver = _driver() raw = { "open_id": "ou_abc", "union_id": "on_abc", "user_id": "ff11ee22", "employee_no": "P12345", "name": "张三", "en_name": "San Zhang", "enterprise_email": "zhang.san@pingcap.cn", "email": "zhangsan@personal.example", "job_title": "后端工程师", "department_ids": ["od-eng", "od-infra"], "status": { "is_frozen": False, "is_resigned": False, "is_activated": False, "is_exited": False, "is_unjoin": False, }, "custom_attrs": [ {"id": GITHUB_ATTR_ID, "type": "TEXT", "value": {"text": "sanzhang"}}, {"id": "C-base", "type": "TEXT", "value": {"text": "北京"}}, ], } record = driver._to_record( raw, GITHUB_ATTR_ID, {"od-eng": "研发部", "od-infra": "基础架构"}, {"od-eng": "", "od-infra": "od-eng"}, ) assert record is not None assert record.external_id == "ou_abc" assert record.email == "zhang.san@pingcap.cn" # 飞书的 ``email`` 常常是私人邮箱,不能当成企业邮箱用。 assert record.secondary_email == "zhangsan@personal.example" assert record.github_login == "sanzhang" assert record.employee_no == "P12345" assert record.department_path == ["研发部"] assert record.custom_attrs["C-base"] == "北京" def test_is_activated_false_is_not_a_leaver(): """🔴 ``is_activated`` 是「登录过没有」,不是「在不在职」。 把它当在职判据,会让对方每一个还没打开过飞书的新人在我们这边显示成 已离职 —— 而离职的人在打标里权重是不一样的。 """ driver = _driver() never_logged_in = driver._to_record( {"open_id": "ou_new", "status": {"is_activated": False}}, None, {}, {}, ) assert never_logged_in is not None assert never_logged_in.is_active is True resigned = driver._to_record( {"open_id": "ou_gone", "status": {"is_activated": True, "is_resigned": True}}, None, {}, {}, ) assert resigned is not None assert resigned.is_active is False def test_missing_fields_yield_none_not_empty_strings(): """权限不足时飞书把字段整个拿掉 —— 解析结果必须是 None。 空字符串会被下游当成「对方登记了一个空邮箱」,然后 ``email_name`` 算出空串,再被匹配器当成一个可用的键。 """ driver = _driver() record = driver._to_record({"open_id": "ou_x"}, GITHUB_ATTR_ID, {}, {}) assert record is not None assert record.email is None assert record.name is None assert record.github_login is None assert record.department_path == [] def test_dept_path_survives_a_parent_cycle(): """对方组织调整期间出现过父子互指;走链不能死循环。""" assert _dept_path( ["a"], {"a": "A", "b": "B"}, {"a": "b", "b": "a"}, ) == ["B", "A"] # ── GitHub ID 字段是手填的自由文本 ──────────────────────────────── # # 下面每一个输入都取自 2026-09-01 对 CBG 租户的第一次全量拉取(381 人), # 不是想象出来的边界。357 个人在这个字段里填了东西,其中只有 230 个是 # handle。 def test_clean_github_login_keeps_the_seven_real_accounts_in_wrong_shape(): """🔴 直接按语法丢弃会白丢 7 个真账号。 6 个人粘了整条个人主页 URL(其中一个把域名写成了 ``github.cn``), 1 个人带了 ``@`` 前缀 —— 它们背后都是真实存在的 GitHub 账号。 """ assert _clean_github_login("https://github.com/sselyp") == "sselyp" assert _clean_github_login("https://github.com/AceDarkknight") == "AceDarkknight" # 真实数据里的域名 typo。 assert _clean_github_login("https://github.cn/zuowei593") == "zuowei593" assert _clean_github_login("@f9z2r6cy6w-alt") == "f9z2r6cy6w-alt" def test_clean_github_login_drops_the_118_placeholders(): """🔴 118 个人在这个字段里写的是「无」。 留着的话,索引里 118 个人共用键「无」,而且界面上会给这 118 个人 渲染一个假的 GitHub 标签。 """ assert _clean_github_login("无") == "" assert _clean_github_login("") == "" assert _clean_github_login(" ") == "" # 真实数据里还有人填了 QQ 邮箱。 assert _clean_github_login("1269372661@qq.cn") == "" # 别家的 URL 不是 GitHub handle。 assert _clean_github_login("https://gitlab.com/x") == "" def test_clean_github_login_enforces_github_syntax(): assert _clean_github_login("octocat") == "octocat" assert _clean_github_login(" OctoCat ") == "OctoCat" assert _clean_github_login("wuchao-pingcap") == "wuchao-pingcap" # 全数字是合法 handle 形状,不能因为「看起来像工号」就丢。 assert _clean_github_login("113084617") == "113084617" assert _clean_github_login("-leading-hyphen") == "" assert _clean_github_login("double--hyphen") == "" assert _clean_github_login("a" * 40) == "" def test_raw_placeholder_is_still_kept_in_custom_attrs(): """清洗掉的原值仍然留在 ``custom_attrs`` 里。 「没人填」和「填了但我们没解析出来」是两件事,而管理员要能分辨。 """ driver = _driver() record = driver._to_record( { "open_id": "ou_x", "custom_attrs": [ {"id": GITHUB_ATTR_ID, "type": "TEXT", "value": {"text": "无"}}, ], }, GITHUB_ATTR_ID, {}, {}, ) assert record is not None assert record.github_login is None assert record.custom_attrs[GITHUB_ATTR_ID] == "无"