dab47f0cb3
- 添加 .env.local 支持,app.sh 和 dev-migrate.sh 自动覆盖 - Docker Compose 使用 profiles 区分 dev/prod 环境 - 改进认证 dev session 判断逻辑,使用 test account 配置 - 修复 CoinPackageCard 重复代码问题 - 清理 opencode 配置,移除敏感信息 - 新增 infra/docker/README.md 文档 - 修复 ruff/pyright/flutter lint 错误 - 更新测试用例移除已删除的 country 字段
628 lines
23 KiB
Python
628 lines
23 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from uuid import UUID, uuid4
|
|
|
|
import pytest
|
|
|
|
from core.http.errors import ApiProblemError
|
|
from models.apple_iap_transaction import AppleIapTransaction
|
|
from models.register_bonus_claims import RegisterBonusClaims
|
|
from schemas.domain.points import ApplyPointsChangeCommand
|
|
from v1.payments.apple_verifier import VerificationError, VerifiedTransaction
|
|
from v1.payments.schemas import VerifyTransactionRequest
|
|
from v1.payments.service import PaymentService
|
|
|
|
|
|
@dataclass
|
|
class _FakeAccount:
|
|
balance: int = 0
|
|
frozen_balance: int = 0
|
|
lifetime_earned: int = 0
|
|
lifetime_spent: int = 0
|
|
version: int = 0
|
|
|
|
|
|
class _FakePaymentRepository:
|
|
def __init__(self, *, existing_transaction: AppleIapTransaction | None = None) -> None:
|
|
self.account = _FakeAccount()
|
|
self.existing_transaction = existing_transaction
|
|
self.inserted_transactions: list[AppleIapTransaction] = []
|
|
self.claim: RegisterBonusClaims | None = None
|
|
self.claim_starter_pack_called: bool = False
|
|
self.commit_count = 0
|
|
|
|
async def get_or_create_user_points_for_update(self, *, user_id: UUID) -> _FakeAccount:
|
|
return self.account
|
|
|
|
async def get_transaction_by_transaction_id(self, *, transaction_id: str) -> AppleIapTransaction | None:
|
|
return self.existing_transaction
|
|
|
|
async def insert_transaction(self, *, transaction: AppleIapTransaction) -> None:
|
|
self.inserted_transactions.append(transaction)
|
|
|
|
async def get_register_bonus_claim(self, *, email_hash: str) -> RegisterBonusClaims | None:
|
|
return self.claim
|
|
|
|
async def upsert_register_bonus_claim_for_starter_pack(
|
|
self, *, email_hash: str, user_email_snapshot: str, first_user_id_snapshot: UUID
|
|
) -> RegisterBonusClaims:
|
|
self.claim_starter_pack_called = True
|
|
if self.claim is None:
|
|
self.claim = RegisterBonusClaims(
|
|
email_hash=email_hash,
|
|
user_email_snapshot=user_email_snapshot,
|
|
first_user_id_snapshot=first_user_id_snapshot,
|
|
grant_event_id="starter_pack_purchase:test",
|
|
has_purchased_starter_pack=True,
|
|
)
|
|
else:
|
|
self.claim.has_purchased_starter_pack = True
|
|
return self.claim
|
|
|
|
async def commit(self) -> None:
|
|
self.commit_count += 1
|
|
|
|
|
|
class _FakePointsRepository:
|
|
def __init__(self) -> None:
|
|
self.appended_ledger: list[ApplyPointsChangeCommand] = []
|
|
|
|
async def append_ledger(self, *, command: ApplyPointsChangeCommand, balance_after: int) -> None:
|
|
self.appended_ledger.append(command)
|
|
|
|
|
|
class _FakeVerifier:
|
|
def __init__(self, *, result: VerifiedTransaction | VerificationError) -> None:
|
|
self._result = result
|
|
|
|
def verify_signed_transaction(
|
|
self,
|
|
signed_transaction_info: str,
|
|
*,
|
|
expected_bundle_id: str,
|
|
expected_product_id: str,
|
|
expected_environment: str,
|
|
) -> VerifiedTransaction | VerificationError:
|
|
del signed_transaction_info, expected_bundle_id, expected_product_id
|
|
del expected_environment
|
|
return self._result
|
|
|
|
|
|
def _make_verified_transaction(
|
|
*,
|
|
transaction_id: str = "2000000123456789",
|
|
product_id: str = "com.meeyao.qianwen.starter_pack",
|
|
environment: str = "Sandbox",
|
|
) -> VerifiedTransaction:
|
|
return VerifiedTransaction(
|
|
transaction_id=transaction_id,
|
|
original_transaction_id=transaction_id,
|
|
web_order_line_item_id=None,
|
|
bundle_id="com.meeyao.qianwen",
|
|
product_id=product_id,
|
|
purchase_date=1700000000000,
|
|
revocation_date=None,
|
|
environment=environment,
|
|
app_account_token=None,
|
|
raw_payload={},
|
|
)
|
|
|
|
|
|
class TestPaymentServiceProductNotFound:
|
|
@pytest.mark.asyncio
|
|
async def test_raises_product_not_found(self) -> None:
|
|
service = PaymentService(
|
|
payment_repo=_FakePaymentRepository(),
|
|
points_repo=_FakePointsRepository(),
|
|
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
|
)
|
|
request = VerifyTransactionRequest(
|
|
productCode="nonexistent_pack",
|
|
appStoreProductId="com.meeyao.qianwen.nonexistent",
|
|
transactionId="2000000123456789",
|
|
signedTransactionInfo="fake_jws",
|
|
)
|
|
with pytest.raises(ApiProblemError) as exc_info:
|
|
await service.verify_and_grant(
|
|
user_id=uuid4(),
|
|
user_email="test@example.com",
|
|
request=request,
|
|
)
|
|
assert exc_info.value.code == "PAYMENT_PRODUCT_NOT_FOUND"
|
|
|
|
|
|
class TestPaymentServiceProductMismatch:
|
|
@pytest.mark.asyncio
|
|
async def test_raises_product_mismatch_when_ids_differ(self) -> None:
|
|
service = PaymentService(
|
|
payment_repo=_FakePaymentRepository(),
|
|
points_repo=_FakePointsRepository(),
|
|
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
|
)
|
|
request = VerifyTransactionRequest(
|
|
productCode="starter_pack",
|
|
appStoreProductId="com.meeyao.qianwen.wrong_pack",
|
|
transactionId="2000000123456789",
|
|
signedTransactionInfo="fake_jws",
|
|
)
|
|
with pytest.raises(ApiProblemError) as exc_info:
|
|
await service.verify_and_grant(
|
|
user_id=uuid4(),
|
|
user_email="test@example.com",
|
|
request=request,
|
|
)
|
|
assert exc_info.value.code == "PAYMENT_PRODUCT_MISMATCH"
|
|
|
|
|
|
class TestPaymentServiceVerificationFailed:
|
|
@pytest.mark.asyncio
|
|
async def test_raises_when_verifier_returns_error(self) -> None:
|
|
service = PaymentService(
|
|
payment_repo=_FakePaymentRepository(),
|
|
points_repo=_FakePointsRepository(),
|
|
verifier=_FakeVerifier(
|
|
result=VerificationError(
|
|
code="PAYMENT_TRANSACTION_INVALID",
|
|
detail="bad signature",
|
|
)
|
|
),
|
|
)
|
|
request = VerifyTransactionRequest(
|
|
productCode="starter_pack",
|
|
appStoreProductId="com.meeyao.qianwen.starter_pack",
|
|
transactionId="2000000123456789",
|
|
signedTransactionInfo="fake_jws",
|
|
)
|
|
with pytest.raises(ApiProblemError) as exc_info:
|
|
await service.verify_and_grant(
|
|
user_id=uuid4(),
|
|
user_email="test@example.com",
|
|
request=request,
|
|
)
|
|
assert exc_info.value.code == "PAYMENT_TRANSACTION_INVALID"
|
|
|
|
|
|
class TestPaymentServiceAlreadyGranted:
|
|
@pytest.mark.asyncio
|
|
async def test_returns_already_granted_for_same_user(self) -> None:
|
|
user_id = uuid4()
|
|
existing = AppleIapTransaction(
|
|
id=uuid4(),
|
|
user_id=user_id,
|
|
product_code="starter_pack",
|
|
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
|
transaction_id="2000000123456789",
|
|
original_transaction_id="2000000123456789",
|
|
environment="Sandbox",
|
|
bundle_id="com.meeyao.qianwen",
|
|
purchase_date="1700000000000",
|
|
status="granted",
|
|
credits=100,
|
|
signed_transaction_info="fake",
|
|
apple_payload_json={},
|
|
ledger_event_id="payment.apple_iap:2000000123456789",
|
|
)
|
|
service = PaymentService(
|
|
payment_repo=_FakePaymentRepository(existing_transaction=existing),
|
|
points_repo=_FakePointsRepository(),
|
|
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
|
)
|
|
request = VerifyTransactionRequest(
|
|
productCode="starter_pack",
|
|
appStoreProductId="com.meeyao.qianwen.starter_pack",
|
|
transactionId="2000000123456789",
|
|
signedTransactionInfo="fake_jws",
|
|
)
|
|
result = await service.verify_and_grant(
|
|
user_id=user_id,
|
|
user_email="test@example.com",
|
|
request=request,
|
|
)
|
|
assert result.status == "already_granted"
|
|
assert result.credits_added == 0
|
|
|
|
|
|
class TestPaymentServiceTransactionConflict:
|
|
@pytest.mark.asyncio
|
|
async def test_raises_conflict_for_different_user(self) -> None:
|
|
existing = AppleIapTransaction(
|
|
id=uuid4(),
|
|
user_id=uuid4(),
|
|
product_code="starter_pack",
|
|
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
|
transaction_id="2000000123456789",
|
|
original_transaction_id="2000000123456789",
|
|
environment="Sandbox",
|
|
bundle_id="com.meeyao.qianwen",
|
|
purchase_date="1700000000000",
|
|
status="granted",
|
|
credits=100,
|
|
signed_transaction_info="fake",
|
|
apple_payload_json={},
|
|
ledger_event_id="payment.apple_iap:2000000123456789",
|
|
)
|
|
service = PaymentService(
|
|
payment_repo=_FakePaymentRepository(existing_transaction=existing),
|
|
points_repo=_FakePointsRepository(),
|
|
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
|
)
|
|
request = VerifyTransactionRequest(
|
|
productCode="starter_pack",
|
|
appStoreProductId="com.meeyao.qianwen.starter_pack",
|
|
transactionId="2000000123456789",
|
|
signedTransactionInfo="fake_jws",
|
|
)
|
|
with pytest.raises(ApiProblemError) as exc_info:
|
|
await service.verify_and_grant(
|
|
user_id=uuid4(),
|
|
user_email="test@example.com",
|
|
request=request,
|
|
)
|
|
assert exc_info.value.code == "PAYMENT_TRANSACTION_CONFLICT"
|
|
|
|
|
|
class TestPaymentServiceSuccessfulGrant:
|
|
@pytest.mark.asyncio
|
|
async def test_grants_credits_for_new_transaction(self) -> None:
|
|
payment_repo = _FakePaymentRepository()
|
|
points_repo = _FakePointsRepository()
|
|
service = PaymentService(
|
|
payment_repo=payment_repo,
|
|
points_repo=points_repo,
|
|
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
|
)
|
|
request = VerifyTransactionRequest(
|
|
productCode="starter_pack",
|
|
appStoreProductId="com.meeyao.qianwen.starter_pack",
|
|
transactionId="2000000123456789",
|
|
signedTransactionInfo="fake_jws",
|
|
)
|
|
result = await service.verify_and_grant(
|
|
user_id=uuid4(),
|
|
user_email="test@example.com",
|
|
request=request,
|
|
)
|
|
assert result.status == "granted"
|
|
assert result.credits_added == 100
|
|
assert result.new_balance == 100
|
|
assert result.ledger_event_id == "payment.apple_iap:2000000123456789"
|
|
assert len(points_repo.appended_ledger) == 1
|
|
assert len(payment_repo.inserted_transactions) == 1
|
|
|
|
|
|
class TestPaymentServiceStarterPackIneligible:
|
|
@pytest.mark.asyncio
|
|
async def test_raises_when_starter_pack_already_purchased(self) -> None:
|
|
claim = RegisterBonusClaims(
|
|
email_hash="fake_hash",
|
|
user_email_snapshot="test@example.com",
|
|
first_user_id_snapshot=uuid4(),
|
|
grant_event_id="register.bonus:test",
|
|
has_purchased_starter_pack=True,
|
|
)
|
|
payment_repo = _FakePaymentRepository()
|
|
payment_repo.claim = claim
|
|
service = PaymentService(
|
|
payment_repo=payment_repo,
|
|
points_repo=_FakePointsRepository(),
|
|
verifier=_FakeVerifier(
|
|
result=_make_verified_transaction(
|
|
product_id="com.meeyao.qianwen.new_user_pack"
|
|
)
|
|
),
|
|
)
|
|
request = VerifyTransactionRequest(
|
|
productCode="new_user_pack",
|
|
appStoreProductId="com.meeyao.qianwen.new_user_pack",
|
|
transactionId="2000000123456789",
|
|
signedTransactionInfo="fake_jws",
|
|
)
|
|
with pytest.raises(ApiProblemError) as exc_info:
|
|
await service.verify_and_grant(
|
|
user_id=uuid4(),
|
|
user_email="test@example.com",
|
|
request=request,
|
|
)
|
|
assert exc_info.value.code == "PAYMENT_STARTER_PACK_INELIGIBLE"
|
|
|
|
|
|
class TestPaymentServiceStarterPackSuccess:
|
|
@pytest.mark.asyncio
|
|
async def test_grants_starter_pack_and_updates_claim(self) -> None:
|
|
payment_repo = _FakePaymentRepository()
|
|
service = PaymentService(
|
|
payment_repo=payment_repo,
|
|
points_repo=_FakePointsRepository(),
|
|
verifier=_FakeVerifier(
|
|
result=_make_verified_transaction(
|
|
product_id="com.meeyao.qianwen.new_user_pack"
|
|
)
|
|
),
|
|
)
|
|
request = VerifyTransactionRequest(
|
|
productCode="new_user_pack",
|
|
appStoreProductId="com.meeyao.qianwen.new_user_pack",
|
|
transactionId="2000000123456789",
|
|
signedTransactionInfo="fake_jws",
|
|
)
|
|
result = await service.verify_and_grant(
|
|
user_id=uuid4(),
|
|
user_email="test@example.com",
|
|
request=request,
|
|
)
|
|
assert result.status == "granted"
|
|
assert result.credits_added == 60
|
|
assert payment_repo.claim_starter_pack_called
|
|
|
|
|
|
class _FakeAccountForRefund:
|
|
def __init__(self, balance: int = 100, lifetime_earned: int = 100) -> None:
|
|
self.balance: int = balance
|
|
self.frozen_balance: int = 0
|
|
self.lifetime_earned: int = lifetime_earned
|
|
self.lifetime_spent: int = 0
|
|
self.version: int = 1
|
|
|
|
|
|
class _FakePaymentRepoForRefund:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
transaction: AppleIapTransaction | None = None,
|
|
account: _FakeAccountForRefund | None = None,
|
|
) -> None:
|
|
self._transaction = transaction
|
|
self.account = account or _FakeAccountForRefund()
|
|
self.inserted_transactions: list[AppleIapTransaction] = []
|
|
self.commit_count = 0
|
|
|
|
async def get_transaction_by_transaction_id(self, *, transaction_id: str) -> AppleIapTransaction | None:
|
|
return self._transaction
|
|
|
|
async def get_user_points_for_update(self, *, user_id: UUID) -> _FakeAccountForRefund:
|
|
return self.account
|
|
|
|
async def get_or_create_user_points_for_update(self, *, user_id: UUID) -> _FakeAccountForRefund:
|
|
return self.account
|
|
|
|
async def insert_transaction(self, *, transaction: AppleIapTransaction) -> None:
|
|
self.inserted_transactions.append(transaction)
|
|
|
|
async def get_register_bonus_claim(self, *, email_hash: str) -> None:
|
|
return None
|
|
|
|
async def upsert_register_bonus_claim_for_starter_pack(
|
|
self, *, email_hash: str, user_email_snapshot: str, first_user_id_snapshot: UUID
|
|
) -> None:
|
|
pass
|
|
|
|
async def commit(self) -> None:
|
|
self.commit_count += 1
|
|
|
|
|
|
class TestProcessRefundUnknownTransaction:
|
|
@pytest.mark.asyncio
|
|
async def test_skips_silently_for_unknown_transaction(self) -> None:
|
|
service = PaymentService(
|
|
payment_repo=_FakePaymentRepoForRefund(transaction=None),
|
|
points_repo=_FakePointsRepository(),
|
|
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
|
)
|
|
await service.process_refund(transaction_id="nonexistent")
|
|
|
|
|
|
class TestProcessRefundNotGranted:
|
|
@pytest.mark.asyncio
|
|
async def test_skips_for_non_granted_transaction(self) -> None:
|
|
txn = AppleIapTransaction(
|
|
id=uuid4(),
|
|
user_id=uuid4(),
|
|
product_code="starter_pack",
|
|
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
|
transaction_id="2000000999999999",
|
|
original_transaction_id="2000000999999999",
|
|
environment="Sandbox",
|
|
bundle_id="com.meeyao.qianwen",
|
|
purchase_date="1700000000000",
|
|
status="verified",
|
|
credits=100,
|
|
signed_transaction_info="fake",
|
|
apple_payload_json={},
|
|
)
|
|
service = PaymentService(
|
|
payment_repo=_FakePaymentRepoForRefund(transaction=txn),
|
|
points_repo=_FakePointsRepository(),
|
|
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
|
)
|
|
await service.process_refund(transaction_id="2000000999999999")
|
|
assert txn.status == "verified"
|
|
|
|
|
|
class TestProcessRefundSufficientBalance:
|
|
@pytest.mark.asyncio
|
|
async def test_deducts_credits_and_writes_refund_ledger(self) -> None:
|
|
user_id = uuid4()
|
|
txn = AppleIapTransaction(
|
|
id=uuid4(),
|
|
user_id=user_id,
|
|
product_code="starter_pack",
|
|
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
|
transaction_id="2000000999999999",
|
|
original_transaction_id="2000000999999999",
|
|
environment="Sandbox",
|
|
bundle_id="com.meeyao.qianwen",
|
|
purchase_date="1700000000000",
|
|
status="granted",
|
|
credits=100,
|
|
signed_transaction_info="fake",
|
|
apple_payload_json={},
|
|
ledger_event_id="payment.apple_iap:2000000999999999",
|
|
)
|
|
account = _FakeAccountForRefund(balance=150, lifetime_earned=200)
|
|
points_repo = _FakePointsRepository()
|
|
service = PaymentService(
|
|
payment_repo=_FakePaymentRepoForRefund(transaction=txn, account=account),
|
|
points_repo=points_repo,
|
|
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
|
)
|
|
await service.process_refund(transaction_id="2000000999999999")
|
|
assert txn.status == "refunded"
|
|
assert account.balance == 50
|
|
assert account.lifetime_earned == 100
|
|
assert len(points_repo.appended_ledger) == 1
|
|
ledger = points_repo.appended_ledger[0]
|
|
assert ledger.change_type.value == "refund"
|
|
assert ledger.direction == -1
|
|
assert ledger.amount == 100
|
|
|
|
|
|
class TestProcessRefundInsufficientBalance:
|
|
@pytest.mark.asyncio
|
|
async def test_deducts_to_zero_and_sets_insufficient_status(self) -> None:
|
|
user_id = uuid4()
|
|
txn = AppleIapTransaction(
|
|
id=uuid4(),
|
|
user_id=user_id,
|
|
product_code="starter_pack",
|
|
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
|
transaction_id="2000000999999998",
|
|
original_transaction_id="2000000999999998",
|
|
environment="Sandbox",
|
|
bundle_id="com.meeyao.qianwen",
|
|
purchase_date="1700000000000",
|
|
status="granted",
|
|
credits=100,
|
|
signed_transaction_info="fake",
|
|
apple_payload_json={},
|
|
ledger_event_id="payment.apple_iap:2000000999999998",
|
|
)
|
|
account = _FakeAccountForRefund(balance=30, lifetime_earned=100)
|
|
points_repo = _FakePointsRepository()
|
|
service = PaymentService(
|
|
payment_repo=_FakePaymentRepoForRefund(transaction=txn, account=account),
|
|
points_repo=points_repo,
|
|
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
|
)
|
|
await service.process_refund(transaction_id="2000000999999998")
|
|
assert txn.status == "refunded_insufficient"
|
|
assert txn.failure_code == "INSUFFICIENT_BALANCE"
|
|
assert account.balance == 0
|
|
assert len(points_repo.appended_ledger) == 1
|
|
ledger = points_repo.appended_ledger[0]
|
|
assert ledger.amount == 30
|
|
|
|
|
|
class TestProcessRefundIdempotency:
|
|
@pytest.mark.asyncio
|
|
async def test_second_refund_is_noop(self) -> None:
|
|
user_id = uuid4()
|
|
txn = AppleIapTransaction(
|
|
id=uuid4(),
|
|
user_id=user_id,
|
|
product_code="starter_pack",
|
|
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
|
transaction_id="2000000999999997",
|
|
original_transaction_id="2000000999999997",
|
|
environment="Sandbox",
|
|
bundle_id="com.meeyao.qianwen",
|
|
purchase_date="1700000000000",
|
|
status="refunded",
|
|
credits=100,
|
|
signed_transaction_info="fake",
|
|
apple_payload_json={},
|
|
)
|
|
points_repo = _FakePointsRepository()
|
|
service = PaymentService(
|
|
payment_repo=_FakePaymentRepoForRefund(transaction=txn, account=_FakeAccountForRefund(balance=50)),
|
|
points_repo=points_repo,
|
|
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
|
)
|
|
await service.process_refund(transaction_id="2000000999999997")
|
|
assert len(points_repo.appended_ledger) == 0
|
|
assert txn.status == "refunded"
|
|
|
|
|
|
class TestHandleServerNotificationRefund:
|
|
@pytest.mark.asyncio
|
|
async def test_processes_refund_notification(self) -> None:
|
|
user_id = uuid4()
|
|
txn = AppleIapTransaction(
|
|
id=uuid4(),
|
|
user_id=user_id,
|
|
product_code="starter_pack",
|
|
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
|
transaction_id="2000000999999001",
|
|
original_transaction_id="2000000999999001",
|
|
environment="Sandbox",
|
|
bundle_id="com.meeyao.qianwen",
|
|
purchase_date="1700000000000",
|
|
status="granted",
|
|
credits=100,
|
|
signed_transaction_info="fake",
|
|
apple_payload_json={},
|
|
ledger_event_id="payment.apple_iap:2000000999999001",
|
|
)
|
|
account = _FakeAccountForRefund(balance=200, lifetime_earned=200)
|
|
points_repo = _FakePointsRepository()
|
|
service = PaymentService(
|
|
payment_repo=_FakePaymentRepoForRefund(transaction=txn, account=account),
|
|
points_repo=points_repo,
|
|
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
|
)
|
|
|
|
import json
|
|
|
|
signed_txn = _make_fake_signed_transaction(transaction_id="2000000999999001")
|
|
notification_payload = json.dumps({
|
|
"notificationType": "REFUND",
|
|
"data": {"signedTransactionInfo": signed_txn},
|
|
})
|
|
signed_payload = _make_fake_jws(notification_payload)
|
|
|
|
await service.handle_server_notification(signed_payload=signed_payload)
|
|
assert txn.status == "refunded"
|
|
assert account.balance == 100
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ignores_empty_payload(self) -> None:
|
|
service = PaymentService(
|
|
payment_repo=_FakePaymentRepoForRefund(),
|
|
points_repo=_FakePointsRepository(),
|
|
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
|
)
|
|
await service.handle_server_notification(signed_payload="")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ignores_non_refund_notification(self) -> None:
|
|
import json
|
|
|
|
notification_payload = json.dumps({
|
|
"notificationType": "DID_RENEW",
|
|
"data": {},
|
|
})
|
|
signed_payload = _make_fake_jws(notification_payload)
|
|
service = PaymentService(
|
|
payment_repo=_FakePaymentRepoForRefund(),
|
|
points_repo=_FakePointsRepository(),
|
|
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
|
)
|
|
await service.handle_server_notification(signed_payload=signed_payload)
|
|
|
|
|
|
def _make_fake_jws(payload_str: str) -> str:
|
|
import base64
|
|
h = base64.urlsafe_b64encode(b'{"alg":"ES256"}').rstrip(b"=").decode()
|
|
p = base64.urlsafe_b64encode(payload_str.encode()).rstrip(b"=").decode()
|
|
return f"{h}.{p}.fake_signature"
|
|
|
|
|
|
def _make_fake_signed_transaction(transaction_id: str) -> str:
|
|
import base64
|
|
import json
|
|
|
|
txn_payload = json.dumps({"transactionId": transaction_id})
|
|
h = base64.urlsafe_b64encode(b'{"alg":"ES256"}').rstrip(b"=").decode()
|
|
p = base64.urlsafe_b64encode(txn_payload.encode()).rstrip(b"=").decode()
|
|
return f"{h}.{p}.fake_signature"
|