"""Tests for GET /api/departments, /api/staff, /api/accounts. Seeds a small org (3 departments, 3 staff, 2 wechat + 1 whatsapp accounts) and asserts shape + filters + pagination across all three endpoints. """ from __future__ import annotations from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import Department, Staff, WechatAccount, WhatsappAccount async def _seed(session: AsyncSession) -> None: session.add_all( [ Department(dept_id="root", parent_id=None, name="Root", path="root"), Department( dept_id="d1", parent_id="root", name="Sales", path="root/d1" ), Department( dept_id="d2", parent_id="root", name="Support", path="root/d2" ), Staff( user_id="u1", name="Alice", dept_id="d1", is_on_job=True, phone="111", # Verbatim upstream staff record — the all-fields drawer reads # these camelCase keys off raw_json. raw_json={ "userId": "u1", "userName": "Alice", "sale_phone": "111", "deptId": "d1", }, ), Staff( user_id="u2", name="Bob", dept_id="d2", is_on_job=False, raw_json=None, ), Staff(user_id="u3", name="Carol", dept_id="d1", is_on_job=True), WechatAccount( wx_id="wx_a", nick_name="WxOne", user_id="u1", user_name="Alice", phone="111", contacts_num=10, group_num=2, is_on_job=True, raw_json={ "wechatId": "wx_a", "nickName": "WxOne", "alias": "alice_wx", "sendNum": 120, "receiveNum": 80, "dbSize": 2048, "onlineStatus": "online", }, ), WechatAccount( wx_id="wx_b", nick_name="WxTwo", user_id="u2", contacts_num=5, is_on_job=False, raw_json=None, ), WhatsappAccount( uid="wa_c@s.whatsapp.net", nick_name="WaOne", user_id="u3", user_name="Carol", phone="333", contacts_num=7, group_num=1, is_on_job=True, raw_json={ "uid": "wa_c@s.whatsapp.net", "nickName": "WaOne", "talkerCount": 7, "friendAddNum": 3, }, ), ] ) await session.commit() async def test_departments_returns_all_ordered_by_path(make_client): """``GET /api/departments`` -> plain list, ordered by ``path``.""" client = await make_client(_seed) r = await client.get("/api/departments") assert r.status_code == 200 items = r.json() assert isinstance(items, list) assert len(items) == 3 # Path order: root, root/d1, root/d2 assert [it["dept_id"] for it in items] == ["root", "d1", "d2"] # Shape for key in ("dept_id", "parent_id", "name", "path"): assert key in items[0] async def test_staff_default_page_returns_all(make_client): """``GET /api/staff`` -> 3 rows, page 1 / page_size 20.""" client = await make_client(_seed) r = await client.get("/api/staff") assert r.status_code == 200 body = r.json() assert body["page"] == 1 assert body["page_size"] == 20 assert body["total"] == 3 assert len(body["items"]) == 3 for key in ( "user_id", "name", "phone", "sale_phone", "dept_id", "is_on_job", "raw_json", ): assert key in body["items"][0] async def test_staff_raw_json_serializes_verbatim(make_client): """``raw_json`` round-trips the verbatim staff record unmolested. Seeded ``u1`` carries a populated blob; ``u2`` has NULL raw_json; ``u3`` omits it entirely. All three must serialize (populated dict vs null). """ client = await make_client(_seed) r = await client.get("/api/staff") assert r.status_code == 200 by_id = {it["user_id"]: it for it in r.json()["items"]} assert by_id["u1"]["raw_json"] == { "userId": "u1", "userName": "Alice", "sale_phone": "111", "deptId": "d1", } assert by_id["u2"]["raw_json"] is None assert by_id["u3"]["raw_json"] is None async def test_staff_filter_by_department_id(make_client): """``?department_id=d1`` -> 2 staff (Alice + Carol).""" client = await make_client(_seed) r = await client.get("/api/staff", params={"department_id": "d1"}) assert r.status_code == 200 body = r.json() assert body["total"] == 2 assert {it["user_id"] for it in body["items"]} == {"u1", "u3"} async def test_staff_filter_by_is_on_job(make_client): """``?is_on_job=true`` -> 2 on-job staff (Alice + Carol).""" client = await make_client(_seed) r = await client.get("/api/staff", params={"is_on_job": "true"}) assert r.status_code == 200 body = r.json() assert body["total"] == 2 assert all(it["is_on_job"] is True for it in body["items"]) async def test_staff_filters_compose(make_client): """``department_id=d1 & is_on_job=true`` AND together -> still 2.""" client = await make_client(_seed) r = await client.get( "/api/staff", params={"department_id": "d1", "is_on_job": "true"} ) assert r.status_code == 200 assert r.json()["total"] == 2 async def test_staff_pagination(make_client): """``page_size=1&page=2`` -> 1 item; total still 3.""" client = await make_client(_seed) r = await client.get("/api/staff", params={"page": 2, "page_size": 1}) assert r.status_code == 200 body = r.json() assert body["total"] == 3 assert len(body["items"]) == 1 async def test_accounts_default_returns_both_channels(make_client): """``GET /api/accounts`` with no channel -> 3 rows across both tables.""" client = await make_client(_seed) r = await client.get("/api/accounts") assert r.status_code == 200 body = r.json() assert body["total"] == 3 # Sorted by (channel, account_id): wechat first, then whatsapp. ids = [(it["channel"], it["account_id"]) for it in body["items"]] assert ids == [ ("wechat", "wx_a"), ("wechat", "wx_b"), ("whatsapp", "wa_c@s.whatsapp.net"), ] # Unified shape exposes channel + key fields. item = body["items"][0] for key in ( "channel", "account_id", "nickname", "user_id", "user_name", "phone", "contacts_num", "group_num", "is_on_job", "raw_json", ): assert key in item assert item["channel"] == "wechat" assert item["nickname"] == "WxOne" assert item["user_name"] == "Alice" async def test_accounts_raw_json_serializes_verbatim(make_client): """``raw_json`` round-trips the verbatim account record for both channels. Seeded ``wx_a`` and the whatsapp account carry populated blobs; ``wx_b`` has NULL raw_json. All three must serialize (populated dict vs null). """ client = await make_client(_seed) r = await client.get("/api/accounts") assert r.status_code == 200 by_id = {(it["channel"], it["account_id"]): it for it in r.json()["items"]} wx_a = by_id[("wechat", "wx_a")] assert wx_a["raw_json"] == { "wechatId": "wx_a", "nickName": "WxOne", "alias": "alice_wx", "sendNum": 120, "receiveNum": 80, "dbSize": 2048, "onlineStatus": "online", } wa = by_id[("whatsapp", "wa_c@s.whatsapp.net")] assert wa["raw_json"] == { "uid": "wa_c@s.whatsapp.net", "nickName": "WaOne", "talkerCount": 7, "friendAddNum": 3, } # NULL raw_json serializes as JSON null. assert by_id[("wechat", "wx_b")]["raw_json"] is None async def test_accounts_filter_channel_wechat(make_client): """``?channel=wechat`` -> only the 2 wechat accounts.""" client = await make_client(_seed) r = await client.get("/api/accounts", params={"channel": "wechat"}) assert r.status_code == 200 body = r.json() assert body["total"] == 2 assert all(it["channel"] == "wechat" for it in body["items"]) async def test_accounts_filter_channel_whatsapp(make_client): """``?channel=whatsapp`` -> the single whatsapp account.""" client = await make_client(_seed) r = await client.get("/api/accounts", params={"channel": "whatsapp"}) assert r.status_code == 200 body = r.json() assert body["total"] == 1 assert body["items"][0]["account_id"] == "wa_c@s.whatsapp.net" assert body["items"][0]["user_name"] == "Carol" async def test_accounts_pagination(make_client): """``page_size=2&page=1`` -> 2 items, total 3; page 2 -> 1 item.""" client = await make_client(_seed) r1 = await client.get("/api/accounts", params={"page": 1, "page_size": 2}) assert r1.status_code == 200 b1 = r1.json() assert b1["total"] == 3 assert len(b1["items"]) == 2 r2 = await client.get("/api/accounts", params={"page": 2, "page_size": 2}) assert r2.status_code == 200 b2 = r2.json() assert len(b2["items"]) == 1 assert b2["items"][0]["account_id"] == "wa_c@s.whatsapp.net" async def test_accounts_avatar_from_raw_json(make_client): """``avatar`` 从 raw_json.headImageUrl 提取(对齐云客员工聊天窗口左侧头像)。 WeChat wxUser/list 原始记录带 ``headImageUrl``;WhatsApp businessSalesList 原始记录无该键(抓包确认,见 wa_accounts_fixture) -> None。NULL raw_json 同样 -> None。 """ async def seed(s: AsyncSession) -> None: s.add_all( [ WechatAccount( wx_id="wx_av", nick_name="WxAv", raw_json={ "wechatId": "wx_av", "headImageUrl": "https://wx.qlogo.cn/a/0", }, ), WechatAccount(wx_id="wx_noav", nick_name="WxNoAv", raw_json=None), WhatsappAccount( uid="wa_av@s.whatsapp.net", nick_name="WaAv", raw_json={"uid": "wa_av@s.whatsapp.net", "nickName": "WaAv"}, ), ] ) await s.commit() client = await make_client(seed) r = await client.get("/api/accounts", params={"channel": "wechat"}) assert r.status_code == 200 by_id = {it["account_id"]: it for it in r.json()["items"]} assert by_id["wx_av"]["avatar"] == "https://wx.qlogo.cn/a/0" assert by_id["wx_noav"]["avatar"] is None # NULL raw_json r2 = await client.get("/api/accounts", params={"channel": "whatsapp"}) assert r2.status_code == 200 assert r2.json()["items"][0]["avatar"] is None # WA raw 无头像键