o2o-negosium-original/negodata/backend/tests/test_features.py
hbyang 7e0f88ca03 [fix] 견적 자동마감 동시성·정합성 + 프론트 안정화 (코드리뷰 후속)
백엔드 close_and_decide 경로:
- 동시 이중 마감 가드: 마감 판정 전 원자적 CLOSED 선점(claim)으로 두 크론 잡·수동마감 경합 직렬화
- 재생성 실패 표면화: regenerate_next_round 결과 검사 → 실패 시 REGEN_FAILED 반환(체인 끊김 은폐 방지)
- 차수 충돌 방지: 다음 라운드 = 체인 최신 round+1(chain_max_round 기준)
- 재생성 사유 집계 정밀화: 미참여/동가를 양성 표식으로 구분(단독낙찰·거부 오집계 제거)
- 재생성 라운드 최소 협상기간 하한(즉시 재마감 캐스케이드 방지)
- 잡 루프 per-item 예외 격리(한 건 실패가 배치 전체를 멈추지 않음)

프론트:
- useChatController: 무권한 가드를 sessionId 별로 추적해 세션 변경 시 자연 해제
- useScrollLock: 마지막 해제를 rAF 로 지연해 재마운트 사이 일시적 잠금 해제 방지
- quotation 상세 쿼리 placeholderData 로 라운드 전환 중 시트 유지

테스트:
- 신규 test_close_and_decide_fixes.py(동시성·차수·집계·기간 하한 검증)
- conftest 결함 수정(존재하지 않는 tbl_account TRUNCATE 제거, companies.status 명시)
- stale 테스트 갱신(test_quotation_create 를 타입드 Req/신규 응답 형식에 맞게 재작성)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:22:21 +09:00

76 lines
2.8 KiB
Python

"""supplier / quotation_setting / quotation 슬라이스 런타임 스모크.
create(재조회로 created_at 적재) + list + get 경로를 라이브 DB 로 확인한다.
"""
import uuid
from common.enums import QuotationStatus, QuotationType
async def _headers(client, company_id, login_id):
await client.post(
"/v1/auth/create",
json={"id": login_id, "password": "pw1234", "company_id": company_id, "name": "n"},
)
r = await client.post("/v1/auth/login", json={"id": login_id, "password": "pw1234"})
return {"Authorization": f"Bearer {r.json()['access_token']}"}
async def test_supplier_crud(client, company_id):
h = await _headers(client, company_id, "supuser")
r = await client.post("/v1/supplier/create", json={"name": "공급사A", "code": "S1"}, headers=h)
body = r.json()
assert body["result"]["success"] is True
sup = body["supplier"]
assert sup["name"] == "공급사A"
assert sup["created_at"] # 재조회 픽스: 서버 기본값 적재 확인
sid = sup["supplier_id"]
r = await client.get("/v1/supplier/list", headers=h)
assert r.json()["total"] == 1
r = await client.get(f"/v1/supplier/{sid}", headers=h)
assert r.json()["supplier"]["supplier_id"] == sid
async def test_quotation_setting_crud(client, company_id):
h = await _headers(client, company_id, "qsuser")
r = await client.post("/v1/quotation-setting/create", json={"target_margin_rate": 0.15}, headers=h)
body = r.json()
assert body["result"]["success"] is True
st = body["setting"]
assert st["target_margin_rate"] == 0.15
assert st["card_count"] == 3 # 기본값
assert st["created_at"]
r = await client.get("/v1/quotation-setting/list", headers=h)
assert r.json()["total"] >= 1
async def test_quotation_create(client, company_id):
h = await _headers(client, company_id, "qtuser")
# type/status 는 int 코드(QuotationType/QuotationStatus). number 는 서버가 생성하므로 미전송.
body = {
"qt_setting_id": str(uuid.uuid4()),
"version_id": str(uuid.uuid4()),
"name": "견적A",
"type": QuotationType.REQUOTE.value,
"status": QuotationStatus.ACTIVE.value,
"start_time": "2026-06-16T00:00:00",
"end_time": "2026-06-17T00:00:00",
}
r = await client.post("/v1/quotation/create", json=body, headers=h)
res = r.json()
# 생성 응답은 본문(quotation)을 안 주고 qt_id/session_count 만 반환 → qt_id 로 재조회한다.
assert res["result"]["success"] is True
qt_id = res["qt_id"]
assert qt_id
r = await client.get(f"/v1/quotation/{qt_id}", headers=h)
q = r.json()["quotation"]
assert q["name"] == "견적A"
assert q["created_at"] # 재조회로 created_at 적재 확인
r = await client.get("/v1/quotation/list", headers=h)
assert r.json()["total"] >= 1