o2o-negosium-original/negodata/backend/tests/test_card.py
Mina Choi 1b9ca062f3 [feat] negodata: 협상카드 회사 스코프·공용 읽기전용 + 견적 목표가 산정내역 수정
협상카드
- 목록/조회를 등록자 개인 → 회사(company.users 조인) 단위로 확장: 같은 회사 유저 카드 + 공용(user_id NULL)
- 공용 카드는 기본 제공 자산 → 수정·삭제 불가(OWNER 포함), is_shared=True API 등록 거부
- 개인 카드 변경은 본인∪최고관리자만, 프론트 버튼 disable+툴팁
- 공개범위 셀렉터·엑셀 공개범위 컬럼·CardTable 전체 배지 제거(항상 개인 등록)
- test_card.py 5케이스 재작성

견적 목표가 산정내역
- get_setting_rates: 단일 컬럼 select 결과를 이중 인덱싱해 항상 예외 → 마진율이 늘 0으로 적용되던 버그 수정
- get_target_breakdown: 채택 후보 판정을 '현재 최소값' → '저장 목표가와 값 일치'로 변경(산정 후 상품가 변동에도 초록 체크 유지)
- 견적상세 협상카드 탭 세션ID 컬럼 제거

견적생성 모달
- 목표가 미리보기·마감기한 기본값(+1h)·미래 검증, combobox 팝오버 scrollIntoView, maskPrices 비율(%) 마스킹

Little-Helped: Stupid Claude
2026-07-09 17:04:40 +09:00

123 lines
6.0 KiB
Python

"""card 도메인 스코프 e2e — 회사 카드는 회사 전체 공유, 전체(공용) 카드는 읽기 전용.
스코프 규칙: user_id 있으면 회사 카드(같은 회사 유저는 조회, 수정·삭제는 본인∪최고관리자) /
NULL 이면 전체(공용) 카드(모든 회사 조회 가능, 수정·삭제·API 등록 불가 — DB 시드 전용). 로그인은 auth_headers."""
import uuid
from sqlalchemy import text
from common.enums import UserRole
async def _create_card(client, headers, *, number, name="카드", is_wildcard=False):
r = await client.post(
"/v1/card/create",
json={
"is_wildcard": is_wildcard,
"name": name,
"number": number,
"script": "안녕하세요",
},
headers=headers,
)
body = r.json()
assert body["result"]["success"] is True, body
return body["card"]["nego_card_id"]
async def _seed_shared_card(db_engine, *, number, name="공용카드"):
"""전체(공용, user_id NULL) 카드는 API 로 못 만들므로 DB 에 직접 시드한다."""
cid = uuid.uuid4()
async with db_engine.begin() as conn:
# usage_type 은 ORM 파이썬 default 뿐(server_default 없음) → raw INSERT 엔 명시.
await conn.execute(
text(
"INSERT INTO nego_cards (nego_card_id, user_id, name, number, script, usage_type) "
"VALUES (:cid, NULL, :name, :number, :script, 1)"
),
{"cid": cid, "name": name, "number": number, "script": "공용 스크립트"},
)
return str(cid)
async def _list_numbers(client, headers):
r = await client.get("/v1/card/list", headers=headers)
return {c["number"] for c in r.json().get("cards", [])}
async def test_company_card_visible_to_colleague_not_other_company(client, auth_headers, other_company_id):
"""검증: 개인(회사) 카드는 같은 회사 동료의 목록·단건조회에 노출되고 타 회사 유저에겐 숨는다.
기대결과: 등록자 A·동료 B 목록엔 있고 타 회사 C 목록엔 없음, C 의 단건 조회는 success=False(CARD_NOT_FOUND)."""
ha = await auth_headers("cardA")
hb = await auth_headers("cardB")
hc = await auth_headers("cardC", other_company_id)
cid = await _create_card(client, ha, number="P-1")
assert "P-1" in await _list_numbers(client, ha)
assert "P-1" in await _list_numbers(client, hb)
assert "P-1" not in await _list_numbers(client, hc)
assert (await client.get(f"/v1/card/{cid}", headers=hb)).json()["result"]["success"] is True
assert (await client.get(f"/v1/card/{cid}", headers=hc)).json()["result"]["success"] is False
async def test_shared_card_visible_to_all_companies(client, auth_headers, other_company_id, db_engine):
"""검증: 전체(공용, user_id NULL) 카드는 회사와 무관하게 모든 유저 목록·단건조회에 노출된다.
기대결과: 서로 다른 회사 A·C 목록 모두에 존재, 단건 조회 success=True + is_shared=True."""
ha = await auth_headers("cardSA")
hc = await auth_headers("cardSC", other_company_id)
cid = await _seed_shared_card(db_engine, number="S-1")
assert "S-1" in await _list_numbers(client, ha)
assert "S-1" in await _list_numbers(client, hc)
got = await client.get(f"/v1/card/{cid}", headers=hc)
assert got.json()["result"]["success"] is True
assert got.json()["card"]["is_shared"] is True
async def test_shared_card_immutable(client, auth_headers, db_engine):
"""검증: 전체(공용) 카드는 기본 제공 자산이라 일반 유저는 물론 최고관리자도 수정·삭제할 수 없다.
기대결과: USER 의 수정, OWNER 의 수정·삭제 모두 success=False, 카드는 목록에 남는다."""
hu = await auth_headers("cardRU")
ho = await auth_headers("cardRO", role=UserRole.OWNER.value)
cid = await _seed_shared_card(db_engine, number="S-RO")
upd = await client.patch(f"/v1/card/update/{cid}", json={"name": "수정 시도"}, headers=hu)
assert upd.json()["result"]["success"] is False
upd_owner = await client.patch(f"/v1/card/update/{cid}", json={"name": "수정 시도"}, headers=ho)
assert upd_owner.json()["result"]["success"] is False
dele = await client.delete(f"/v1/card/delete/{cid}", headers=ho)
assert dele.json()["result"]["success"] is False
assert "S-RO" in await _list_numbers(client, hu)
async def test_colleague_card_mutation_gating(client, auth_headers):
"""검증: 같은 회사 동료의 카드는 조회는 되지만 수정·삭제는 본인 또는 최고관리자(OWNER)만 가능하다.
기대결과: 동료 USER 의 수정 success=False, OWNER 의 수정·삭제는 success=True 후 목록에서 제거."""
ha = await auth_headers("cardGA")
hb = await auth_headers("cardGB")
ho = await auth_headers("cardGO", role=UserRole.OWNER.value)
cid = await _create_card(client, ha, number="P-G")
upd = await client.patch(f"/v1/card/update/{cid}", json={"name": "동료가 수정"}, headers=hb)
assert upd.json()["result"]["success"] is False
upd_owner = await client.patch(f"/v1/card/update/{cid}", json={"name": "관리자가 수정"}, headers=ho)
assert upd_owner.json()["result"]["success"] is True
dele = await client.delete(f"/v1/card/delete/{cid}", headers=ho)
assert dele.json()["result"]["success"] is True
assert "P-G" not in await _list_numbers(client, ha)
async def test_create_shared_card_rejected(client, auth_headers):
"""검증: is_shared=True 등록은 거부된다 — 공용 카드는 DB 시드로만 관리(만들면 수정·삭제 불가라 되돌릴 수 없음).
기대결과: success=False, 카드는 목록에 생기지 않는다."""
ha = await auth_headers("cardXA")
r = await client.post(
"/v1/card/create",
json={"is_shared": True, "name": "공용 시도", "number": "S-X", "script": "안녕하세요"},
headers=ha,
)
assert r.json()["result"]["success"] is False
assert "S-X" not in await _list_numbers(client, ha)