Skip to content

Commit 67c9eae

Browse files
committed
Validate price in purchase endpoint
1 parent 1daf5ad commit 67c9eae

3 files changed

Lines changed: 332 additions & 1 deletion

File tree

apps/api/app/api/purchase.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,15 @@ def request_product(
476476
# Decimal을 직접 센트 단위로 변환 (정밀도 문제 방지)
477477
expected_amount_cents = int(price.price * 100)
478478

479+
# 가격이 0원 이하인 경우 차단
480+
if expected_amount_cents <= 0:
481+
receipt.status = ReceiptStatus.INVALID
482+
raise_error(
483+
sess,
484+
receipt,
485+
ValueError(f"Price must be greater than 0. Current price: {price.price}"),
486+
)
487+
479488
# Stripe 검증
480489
success, msg, purchase = validate_web(
481490
stripe_secret_key=stripe_key,

tests/api/test_price_validation.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""가격 검증 로직 테스트"""
2+
import pytest
3+
from decimal import Decimal
4+
from unittest.mock import Mock, patch
5+
6+
7+
class TestPriceValidation:
8+
"""가격 검증 로직 단위 테스트"""
9+
10+
def test_zero_price_validation_logic(self):
11+
"""가격이 0원인 경우 검증 로직 테스트"""
12+
# 가격이 0원인 경우
13+
price_value = Decimal("0.0")
14+
expected_amount_cents = int(price_value * 100)
15+
16+
# 가격이 0원 이하인지 확인
17+
assert expected_amount_cents <= 0, "가격이 0원 이하여야 함"
18+
assert expected_amount_cents == 0, "가격이 정확히 0원이어야 함"
19+
20+
# 검증 로직 시뮬레이션
21+
if expected_amount_cents <= 0:
22+
should_reject = True
23+
else:
24+
should_reject = False
25+
26+
assert should_reject is True, "0원 가격은 거부되어야 함"
27+
28+
def test_negative_price_validation_logic(self):
29+
"""가격이 음수인 경우 검증 로직 테스트"""
30+
# 가격이 음수인 경우
31+
price_value = Decimal("-1.00")
32+
expected_amount_cents = int(price_value * 100)
33+
34+
# 가격이 0원 이하인지 확인
35+
assert expected_amount_cents <= 0, "가격이 0원 이하여야 함"
36+
assert expected_amount_cents < 0, "가격이 음수여야 함"
37+
38+
# 검증 로직 시뮬레이션
39+
if expected_amount_cents <= 0:
40+
should_reject = True
41+
else:
42+
should_reject = False
43+
44+
assert should_reject is True, "음수 가격은 거부되어야 함"
45+
46+
def test_positive_price_validation_logic(self):
47+
"""가격이 양수인 경우 검증 로직 테스트"""
48+
# 가격이 양수인 경우
49+
price_value = Decimal("12.99")
50+
expected_amount_cents = int(price_value * 100)
51+
52+
# 가격이 0원 초과인지 확인
53+
assert expected_amount_cents > 0, "가격이 0원 초과여야 함"
54+
assert expected_amount_cents == 1299, "가격이 1299센트(12.99달러)여야 함"
55+
56+
# 검증 로직 시뮬레이션
57+
if expected_amount_cents <= 0:
58+
should_reject = True
59+
else:
60+
should_reject = False
61+
62+
assert should_reject is False, "양수 가격은 허용되어야 함"
63+
64+
def test_small_positive_price_validation_logic(self):
65+
"""작은 양수 가격 검증 로직 테스트"""
66+
# 가격이 매우 작은 양수인 경우 (예: $0.01)
67+
price_value = Decimal("0.01")
68+
expected_amount_cents = int(price_value * 100)
69+
70+
# 가격이 0원 초과인지 확인
71+
assert expected_amount_cents > 0, "가격이 0원 초과여야 함"
72+
assert expected_amount_cents == 1, "가격이 1센트(0.01달러)여야 함"
73+
74+
# 검증 로직 시뮬레이션
75+
if expected_amount_cents <= 0:
76+
should_reject = True
77+
else:
78+
should_reject = False
79+
80+
assert should_reject is False, "작은 양수 가격도 허용되어야 함"
81+
82+
def test_price_validation_error_message(self):
83+
"""가격 검증 에러 메시지 테스트"""
84+
# 가격이 0원인 경우
85+
price_value = Decimal("0.0")
86+
expected_amount_cents = int(price_value * 100)
87+
88+
if expected_amount_cents <= 0:
89+
error_message = f"Price must be greater than 0. Current price: {price_value}"
90+
assert "Price must be greater than 0" in error_message
91+
assert "Current price: 0.0" in error_message
92+
93+
def test_price_conversion_to_cents(self):
94+
"""가격을 센트 단위로 변환하는 로직 테스트"""
95+
test_cases = [
96+
(Decimal("0.0"), 0),
97+
(Decimal("-1.00"), -100),
98+
(Decimal("0.01"), 1),
99+
(Decimal("1.00"), 100),
100+
(Decimal("12.99"), 1299),
101+
(Decimal("99.99"), 9999),
102+
]
103+
104+
for price, expected_cents in test_cases:
105+
actual_cents = int(price * 100)
106+
assert actual_cents == expected_cents, f"{price} -> {actual_cents}센트 (예상: {expected_cents}센트)"
107+
108+
# 검증 로직
109+
should_reject = actual_cents <= 0
110+
if price <= 0:
111+
assert should_reject is True, f"{price}는 거부되어야 함"
112+
else:
113+
assert should_reject is False, f"{price}는 허용되어야 함"

tests/api/test_web_payment_with_db.py

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
2-
from sqlalchemy import create_engine
2+
from unittest.mock import patch, Mock
3+
from sqlalchemy import create_engine, select
34
from sqlalchemy.orm import sessionmaker
45
from sqlalchemy.pool import StaticPool
56

@@ -137,5 +138,213 @@ def test_web_payment_request_product_integration(self, db_session, test_product,
137138
# Stripe API 호출을 모킹하고 request_product 함수를 테스트할 수 있습니다
138139
pass
139140

141+
def test_zero_price_validation(self, db_session):
142+
"""가격이 0원인 경우 검증 로직 테스트"""
143+
from sqlalchemy import select
144+
145+
# 가격이 0원인 상품 생성
146+
product = Product(
147+
name="Zero Price Product",
148+
google_sku="zero_price_sku",
149+
apple_sku="zero_price_apple_sku",
150+
apple_sku_k="zero_price_apple_sku_k",
151+
product_type=ProductType.IAP,
152+
active=True
153+
)
154+
db_session.add(product)
155+
db_session.flush()
156+
157+
# 가격이 0원인 Price 생성
158+
price = Price(
159+
product_id=product.id,
160+
price=0.0, # 0원
161+
currency="USD",
162+
store=Store.WEB,
163+
regular_price=0.0
164+
)
165+
db_session.add(price)
166+
db_session.commit()
167+
168+
# 가격 조회 및 검증 로직 테스트
169+
price = db_session.scalar(
170+
select(Price)
171+
.where(Price.product_id == product.id)
172+
.limit(1)
173+
)
174+
175+
assert price is not None
176+
expected_amount_cents = int(price.price * 100)
177+
178+
# 가격이 0원 이하인지 확인
179+
assert expected_amount_cents <= 0, "가격이 0원 이하여야 함"
180+
assert expected_amount_cents == 0, "가격이 정확히 0원이어야 함"
181+
182+
def test_negative_price_validation(self, db_session):
183+
"""가격이 음수인 경우 검증 로직 테스트"""
184+
from sqlalchemy import select
185+
from decimal import Decimal
186+
187+
# 가격이 음수인 상품 생성
188+
product = Product(
189+
name="Negative Price Product",
190+
google_sku="negative_price_sku",
191+
apple_sku="negative_price_apple_sku",
192+
apple_sku_k="negative_price_apple_sku_k",
193+
product_type=ProductType.IAP,
194+
active=True
195+
)
196+
db_session.add(product)
197+
db_session.flush()
198+
199+
# 가격이 음수인 Price 생성
200+
price = Price(
201+
product_id=product.id,
202+
price=Decimal("-1.00"), # 음수 가격
203+
currency="USD",
204+
store=Store.WEB,
205+
regular_price=Decimal("-1.00")
206+
)
207+
db_session.add(price)
208+
db_session.commit()
209+
210+
# 가격 조회 및 검증 로직 테스트
211+
price = db_session.scalar(
212+
select(Price)
213+
.where(Price.product_id == product.id)
214+
.limit(1)
215+
)
216+
217+
assert price is not None
218+
expected_amount_cents = int(price.price * 100)
219+
220+
# 가격이 0원 이하인지 확인
221+
assert expected_amount_cents <= 0, "가격이 0원 이하여야 함"
222+
assert expected_amount_cents < 0, "가격이 음수여야 함"
223+
224+
@patch('app.api.purchase.validate_web')
225+
@patch('app.api.purchase.send_to_worker')
226+
def test_request_endpoint_zero_price_rejection(self, mock_send_to_worker, mock_validate_web, db_session):
227+
"""/request 엔드포인트에서 가격이 0원인 경우 거부 테스트"""
228+
from app.api.purchase import request_product
229+
from shared.schemas.receipt import ReceiptSchema
230+
231+
# 가격이 0원인 상품 생성
232+
product = Product(
233+
name="Zero Price Product",
234+
google_sku="zero_price_sku",
235+
apple_sku="zero_price_apple_sku",
236+
apple_sku_k="zero_price_apple_sku_k",
237+
product_type=ProductType.IAP,
238+
active=True
239+
)
240+
db_session.add(product)
241+
db_session.flush()
242+
243+
# 가격이 0원인 Price 생성
244+
price = Price(
245+
product_id=product.id,
246+
price=0.0, # 0원
247+
currency="USD",
248+
store=Store.WEB,
249+
regular_price=0.0
250+
)
251+
db_session.add(price)
252+
db_session.commit()
253+
254+
# 구매 요청 데이터 생성
255+
receipt_data = ReceiptSchema(
256+
store=Store.WEB,
257+
agentAddress="0x1234567890abcdef1234567890abcdef12345678",
258+
avatarAddress="0xabcdef1234567890abcdef1234567890abcdef12",
259+
data={
260+
"Store": "WebPayment",
261+
"orderId": "pi_zero_price_test",
262+
"productId": product.id,
263+
"purchaseTime": 1640995200,
264+
},
265+
planetId=PlanetID.ODIN.value.decode("utf-8")
266+
)
267+
268+
# 가격이 0원이므로 ValueError가 발생해야 함
269+
with pytest.raises(ValueError, match="Price must be greater than 0"):
270+
request_product(
271+
receipt_data=receipt_data,
272+
x_iap_packagename=PackageName.NINE_CHRONICLES_WEB,
273+
sess=db_session
274+
)
275+
276+
# validate_web이 호출되지 않았는지 확인 (가격 검증에서 먼저 실패해야 함)
277+
mock_validate_web.assert_not_called()
278+
279+
# Receipt가 INVALID 상태로 저장되었는지 확인
280+
receipt = db_session.scalar(
281+
select(Receipt).where(Receipt.order_id == "pi_zero_price_test")
282+
)
283+
assert receipt is not None
284+
assert receipt.status == ReceiptStatus.INVALID
285+
286+
@patch('app.api.purchase.validate_web')
287+
@patch('app.api.purchase.send_to_worker')
288+
def test_request_endpoint_negative_price_rejection(self, mock_send_to_worker, mock_validate_web, db_session):
289+
"""/request 엔드포인트에서 가격이 음수인 경우 거부 테스트"""
290+
from app.api.purchase import request_product
291+
from shared.schemas.receipt import ReceiptSchema
292+
from decimal import Decimal
293+
294+
# 가격이 음수인 상품 생성
295+
product = Product(
296+
name="Negative Price Product",
297+
google_sku="negative_price_sku",
298+
apple_sku="negative_price_apple_sku",
299+
apple_sku_k="negative_price_apple_sku_k",
300+
product_type=ProductType.IAP,
301+
active=True
302+
)
303+
db_session.add(product)
304+
db_session.flush()
305+
306+
# 가격이 음수인 Price 생성
307+
price = Price(
308+
product_id=product.id,
309+
price=Decimal("-1.00"), # 음수 가격
310+
currency="USD",
311+
store=Store.WEB,
312+
regular_price=Decimal("-1.00")
313+
)
314+
db_session.add(price)
315+
db_session.commit()
316+
317+
# 구매 요청 데이터 생성
318+
receipt_data = ReceiptSchema(
319+
store=Store.WEB,
320+
agentAddress="0x1234567890abcdef1234567890abcdef12345678",
321+
avatarAddress="0xabcdef1234567890abcdef1234567890abcdef12",
322+
data={
323+
"Store": "WebPayment",
324+
"orderId": "pi_negative_price_test",
325+
"productId": product.id,
326+
"purchaseTime": 1640995200,
327+
},
328+
planetId=PlanetID.ODIN.value.decode("utf-8")
329+
)
330+
331+
# 가격이 음수이므로 ValueError가 발생해야 함
332+
with pytest.raises(ValueError, match="Price must be greater than 0"):
333+
request_product(
334+
receipt_data=receipt_data,
335+
x_iap_packagename=PackageName.NINE_CHRONICLES_WEB,
336+
sess=db_session
337+
)
338+
339+
# validate_web이 호출되지 않았는지 확인 (가격 검증에서 먼저 실패해야 함)
340+
mock_validate_web.assert_not_called()
341+
342+
# Receipt가 INVALID 상태로 저장되었는지 확인
343+
receipt = db_session.scalar(
344+
select(Receipt).where(Receipt.order_id == "pi_negative_price_test")
345+
)
346+
assert receipt is not None
347+
assert receipt.status == ReceiptStatus.INVALID
348+
140349
if __name__ == "__main__":
141350
pytest.main([__file__, "-v"])

0 commit comments

Comments
 (0)