"""Tests for GET /api/voice-clips + PATCH /voice-clips/{id}/match. Seeds 3 clips (2 wechat for user u1 at end_time 1000/3000, 1 whatsapp for u2 at end_time 2000) and asserts GET filters + ordering (``end_time desc``) + pagination + the ``user_id``-beats-``user_real_name`` precedence rule, plus the 手动匹配 PATCH (set + clear + name resolution + 404 path). """ from __future__ import annotations from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import Customer, VoiceClip async def _seed(session: AsyncSession) -> None: session.add_all( [ VoiceClip( voice_clip_id=1, user_id="u1", channel="wechat", user_real_name="Alice", company_code="8m0atd", start_time=900, end_time=1000, oss_file_name="https://oss/clip1.mp3", mp3_duration=100, duration_format="00:01:40", ), VoiceClip( voice_clip_id=2, user_id="u2", channel="whatsapp", user_real_name="Bob", company_code="8m0atd", start_time=1900, end_time=2000, oss_file_name="https://oss/clip2.mp3", mp3_duration=60, duration_format="00:01:00", ), VoiceClip( voice_clip_id=3, user_id="u1", channel="wechat", user_real_name="Alice", company_code="other", start_time=2900, end_time=3000, oss_file_name=None, mp3_duration=0, duration_format="00:00:00", ), ] ) await session.commit() async def test_default_list_returns_all_ordered_desc(make_client): """No filters -> 3 rows, ordered by ``end_time desc``.""" client = await make_client(_seed) r = await client.get("/api/voice-clips") assert r.status_code == 200 body = r.json() assert body["total"] == 3 assert [it["voice_clip_id"] for it in body["items"]] == [3, 2, 1] for key in ( "voice_clip_id", "user_id", "channel", "user_real_name", "wx_id", "talker", "status", "is_send", "company_code", "start_time", "end_time", "oss_file_name", "mp3_duration", "duration_format", "raw_json", ): assert key in body["items"][0] async def test_filter_by_user_id(make_client): """``?user_id=u1`` -> 2 clips for Alice.""" client = await make_client(_seed) r = await client.get("/api/voice-clips", params={"user_id": "u1"}) assert r.status_code == 200 body = r.json() assert body["total"] == 2 assert {it["voice_clip_id"] for it in body["items"]} == {1, 3} async def test_user_id_takes_precedence_over_user_real_name(make_client): """When both ``user_id`` and ``user_real_name`` are given, user_id wins. The 云客 modal switches between the two based on whether the user edited the staff-name box; the id is the more precise filter and wins on conflict. Here ``user_id=u1`` (Alice) + ``user_real_name=Bob`` should still return Alice's clips (user_id path), NOT Bob's. """ client = await make_client(_seed) r = await client.get( "/api/voice-clips", params={"user_id": "u1", "user_real_name": "Bob"} ) assert r.status_code == 200 body = r.json() assert body["total"] == 2 assert {it["voice_clip_id"] for it in body["items"]} == {1, 3} async def test_filter_by_user_real_name_fuzzy(make_client): """``?user_real_name=ob`` -> Bob's clip only (ILIKE contains).""" client = await make_client(_seed) r = await client.get("/api/voice-clips", params={"user_real_name": "ob"}) assert r.status_code == 200 body = r.json() assert body["total"] == 1 assert body["items"][0]["voice_clip_id"] == 2 async def test_filter_by_channel(make_client): """``?channel=whatsapp`` -> the single WA clip.""" client = await make_client(_seed) r = await client.get("/api/voice-clips", params={"channel": "whatsapp"}) assert r.status_code == 200 body = r.json() assert body["total"] == 1 assert body["items"][0]["voice_clip_id"] == 2 async def test_filter_by_company_code(make_client): """``?company_code=8m0atd`` -> 2 clips (clip 3 has 'other').""" client = await make_client(_seed) r = await client.get("/api/voice-clips", params={"company_code": "8m0atd"}) assert r.status_code == 200 body = r.json() assert body["total"] == 2 assert {it["voice_clip_id"] for it in body["items"]} == {1, 2} async def test_time_window_filter(make_client): """``?start=1500&end=2500`` -> only clip 2 (end_time 2000).""" client = await make_client(_seed) r = await client.get("/api/voice-clips", params={"start": 1500, "end": 2500}) assert r.status_code == 200 body = r.json() assert body["total"] == 1 assert body["items"][0]["voice_clip_id"] == 2 async def test_filters_compose(make_client): """``user_id=u1 & start=1500`` -> only clip 3 (u1's later clip).""" client = await make_client(_seed) r = await client.get( "/api/voice-clips", params={"user_id": "u1", "start": 1500} ) assert r.status_code == 200 body = r.json() assert body["total"] == 1 assert body["items"][0]["voice_clip_id"] == 3 async def test_pagination(make_client): """``page_size=1`` -> 1 item, total 3.""" client = await make_client(_seed) r = await client.get("/api/voice-clips", params={"page": 1, "page_size": 1}) assert r.status_code == 200 body = r.json() assert body["total"] == 3 assert len(body["items"]) == 1 assert body["items"][0]["voice_clip_id"] == 3 async def test_oss_file_name_serializes(make_client): """``oss_file_name`` (OSS recording URL) round-trips so the modal can render an inline ``