"""LPS API 라우터 테스트 — 검색 적재/중복/상태조회/큐 통계 (ASGI 클라이언트 + 실 lps_db).""" import pytest_asyncio from sqlalchemy import text @pytest_asyncio.fixture async def clean_jobs(db_engine): async with db_engine.begin() as conn: await conn.execute(text("TRUNCATE job")) async def test_search_enqueues_jobs(client, clean_jobs): body = {"data": [ {"product_code": "P1", "product_name": "커피", "job_type": "new_product"}, {"product_code": "P2", "product_name": "무선마우스", "specification": "M170"}, ]} r = await client.post("/v1/lps/search", json=body) assert r.status_code == 200 j = r.json() assert j["accepted"] == 2 assert {i["product_code"] for i in j["items"]} == {"P1", "P2"} assert all(i.get("job_id") for i in j["items"]) async def test_search_dedupes_active_product(client, clean_jobs): body = {"data": [{"product_code": "P1", "product_name": "커피", "job_type": "new_product"}]} await client.post("/v1/lps/search", json=body) r2 = await client.post("/v1/lps/search", json=body) # 같은 product_code 재요청 item = r2.json()["items"][0] assert item["duplicated"] is True assert "job_id" not in item # None → RemoveNoneResponse 로 제거됨 assert r2.json()["accepted"] == 0 async def test_job_status_flow(client, clean_jobs): jid = (await client.post("/v1/lps/search", json={"data": [{"product_code": "P9", "product_name": "커피"}]})).json()["items"][0]["job_id"] r = await client.get(f"/v1/lps/jobs/{jid}") body = r.json() assert body["status"] == "PENDING" and body["attempts"] == 0 assert body["result"]["success"] is True async def test_job_status_not_found(client, clean_jobs): # 존재하지 않는(유효 UUID) 잡 r = await client.get("/v1/lps/jobs/00000000-0000-0000-0000-000000000000") assert r.json()["result"]["success"] is False assert r.json()["result"]["desc"] == "LPS_JOB_NOT_FOUND" # 잘못된 형식의 id 도 not-found 처리 r2 = await client.get("/v1/lps/jobs/not-a-uuid") assert r2.json()["result"]["desc"] == "LPS_JOB_NOT_FOUND" async def test_price_history_endpoint(client, db_engine): from crud.price_history import PriceHistory async with db_engine.begin() as conn: await conn.execute(text("TRUNCATE price_history")) ph = PriceHistory() await ph.record({"product_code": "GRAPH1", "outcome": "found", "final_lowest": 2500, "final_source": "naver", "naver_lowest": 2500, "coupang_lowest": 2700, "matched_count": 2}) r = await client.get("/v1/lps/products/GRAPH1/history") assert r.status_code == 200 body = r.json() assert body["product_code"] == "GRAPH1" assert len(body["points"]) == 1 pt = body["points"][0] assert pt["final"] == 2500 and pt["naver"] == 2500 and pt["coupang"] == 2700 and pt["final_source"] == "naver" assert "triggered_at" in pt async def test_queue_stats(client, clean_jobs): await client.post("/v1/lps/search", json={"data": [ {"product_code": "A", "product_name": "x"}, {"product_code": "B", "product_name": "y"}, ]}) r = await client.get("/v1/lps/queue/stats") counts = r.json()["counts"] assert counts["PENDING"] == 2 and counts["DONE"] == 0 and counts["DEAD"] == 0 async def test_readyz(client): r = await client.get("/readyz") # DB 도달 → ready assert r.status_code == 200 and r.json()["ready"] is True async def test_ops_snapshot(client, clean_jobs): await client.post("/v1/lps/search", json={"data": [{"product_code": "A", "product_name": "x"}]}) r = await client.get("/v1/lps/ops") assert r.status_code == 200 j = r.json() for k in ("pending", "running", "done", "dead", "dead_1h", "stuck_running", "oldest_pending_sec", "blocks_1h", "deadline_1h", "pool_pct"): assert k in j and isinstance(j[k], int) assert isinstance(j["cost_1h_usd"], (int, float)) assert j["pending"] == 1