"""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)