협상카드 - 목록/조회를 등록자 개인 → 회사(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
123 lines
6.0 KiB
Python
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)
|