"""对齐 Muse P10 支付单元测试:凭证保险库、一次性卡号、 购买详情摘要、限额、审计、沙箱钱包。 只用标准库与假时钟,绝不启动真实 VM、不联网、不产生真实 扣款。覆盖: - 凭证隔离(PAN 拒绝入库、未知提供方拒绝、按次释放绑定、 单次兑现、过期、忘记作废、repr 无凭证值); - 一次性卡号(换商家/超额/换货币/过期/复用一律拒); - 购买详情(自洽校验、摘要防篡改); - 限额(三档、边界、限额不可用时不编造上限); - 审计(秘密字段名丢弃 + redacted); - 沙箱钱包(铸卡、记账、失败 void、账本无卡号)。 """ import sys import unittest from pathlib import Path try: from cloud.chv.payments import ( OneTimeCard, PaymentAudit, PaymentCredentialVault, PaymentRefusal, PurchaseDetails, SimulatedWalletBackend, SpendLimits, luhn_valid, ) except ImportError: sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from cloud.chv.payments import ( OneTimeCard, PaymentAudit, PaymentCredentialVault, PaymentRefusal, PurchaseDetails, SimulatedWalletBackend, SpendLimits, luhn_valid, ) _NOW = 1_700_000_000.0 _TEST_PAN = "4242424242424242" # Luhn 有效的测试卡号,永不真扣款 class _Clock: """可推进的假时钟(测过期用)。""" def __init__(self, now: float = _NOW) -> None: self.now = now def __call__(self) -> float: return self.now def advance(self, seconds: float) -> None: self.now += seconds def _details(amount: int = 5000) -> PurchaseDetails: return PurchaseDetails( merchant="Example Store", merchant_host="shop.example.com", items=(("Widget", 2, 2500),), amount_minor=amount, currency="USD", wallet_provider="stripe-link") class LuhnTest(unittest.TestCase): def test_valid_test_pan_passes_luhn(self) -> None: self.assertTrue(luhn_valid(_TEST_PAN)) def test_short_or_bad_number_fails(self) -> None: self.assertFalse(luhn_valid("123")) self.assertFalse(luhn_valid("4242424242424241")) class VaultTest(unittest.TestCase): def setUp(self) -> None: self.clock = _Clock() self.vault = PaymentCredentialVault(clock=self.clock) def test_store_release_consume_roundtrip(self) -> None: self.vault.store("stripe-link", "pm_abc", "Visa ....4242", "shop.example.com") self.assertTrue(self.vault.has("stripe-link", "pm_abc")) rel = self.vault.release("stripe-link", "pm_abc", "shop.example.com", 5000, "USD") method = self.vault.consume(rel.release_id) self.assertEqual(method.method_id, "pm_abc") self.assertEqual(method.masked_label, "Visa ....4242") def test_raw_pan_as_method_id_refused(self) -> None: with self.assertRaises(PaymentRefusal) as ctx: self.vault.store("stripe-link", _TEST_PAN, "Visa ....4242") self.assertEqual(ctx.exception.kind, "raw_pan_refused") def test_raw_pan_as_label_refused(self) -> None: with self.assertRaises(PaymentRefusal): self.vault.store("stripe-link", "pm_abc", _TEST_PAN) def test_unknown_provider_refused(self) -> None: with self.assertRaises(PaymentRefusal) as ctx: self.vault.store("sketchy-pay", "pm_abc", "Visa ....4242") self.assertEqual(ctx.exception.kind, "unknown_provider") def test_empty_method_id_refused(self) -> None: with self.assertRaises(PaymentRefusal): self.vault.store("stripe-link", " ", "Visa ....4242") def test_release_scope_mismatch_refused(self) -> None: self.vault.store("stripe-link", "pm_abc", "Visa ....4242", "shop.example.com") with self.assertRaises(PaymentRefusal) as ctx: self.vault.release("stripe-link", "pm_abc", "evil.example.com", 5000, "USD") self.assertEqual(ctx.exception.kind, "scope_mismatch") def test_release_unknown_method_refused(self) -> None: with self.assertRaises(PaymentRefusal): self.vault.release("stripe-link", "pm_nope", "shop.example.com", 5000, "USD") def test_release_bad_amount_refused(self) -> None: self.vault.store("stripe-link", "pm_abc", "Visa ....4242") with self.assertRaises(PaymentRefusal): self.vault.release("stripe-link", "pm_abc", "shop.example.com", 0, "USD") def test_consume_twice_second_fails(self) -> None: self.vault.store("stripe-link", "pm_abc", "Visa ....4242") rel = self.vault.release("stripe-link", "pm_abc", "shop.example.com", 5000, "USD") self.vault.consume(rel.release_id) with self.assertRaises(PaymentRefusal) as ctx: self.vault.consume(rel.release_id) self.assertEqual(ctx.exception.kind, "release_unknown") def test_consume_expired_release_fails(self) -> None: self.vault.store("stripe-link", "pm_abc", "Visa ....4242") rel = self.vault.release("stripe-link", "pm_abc", "shop.example.com", 5000, "USD", ttl_seconds=300) self.clock.advance(301) with self.assertRaises(PaymentRefusal) as ctx: self.vault.consume(rel.release_id) self.assertEqual(ctx.exception.kind, "release_expired") def test_forget_removes_and_voids_releases(self) -> None: self.vault.store("stripe-link", "pm_abc", "Visa ....4242") rel = self.vault.release("stripe-link", "pm_abc", "shop.example.com", 5000, "USD") self.assertTrue(self.vault.forget("stripe-link", "pm_abc")) self.assertFalse(self.vault.has("stripe-link", "pm_abc")) with self.assertRaises(PaymentRefusal): self.vault.consume(rel.release_id) self.assertFalse(self.vault.forget("stripe-link", "pm_abc")) def test_repr_contains_no_credential_value(self) -> None: self.vault.store("stripe-link", "pm_abc", "Visa ....4242") self.assertNotIn(_TEST_PAN, repr(self.vault)) self.assertNotIn("pm_abc", repr(self.vault)) def test_methods_for_merchant_scoped(self) -> None: self.vault.store("stripe-link", "pm_a", "Visa ....4242", "shop.example.com") self.vault.store("shop-pay", "pm_b", "MC ....5555", "other.example.com") found = self.vault.methods_for_merchant("shop.example.com") self.assertEqual([m.method_id for m in found], ["pm_a"]) class OneTimeCardTest(unittest.TestCase): def _card(self) -> OneTimeCard: return OneTimeCard( card_id="otc_test", merchant="Example Store", amount_minor=5000, currency="USD", expires_at=_NOW + 900, masked_label="Visa ....4242") def test_authorize_ok_consumes_once(self) -> None: card = self._card() ok, reason = card.authorize("Example Store", 5000, "USD", _NOW) self.assertTrue(ok) self.assertEqual(reason, "ok") ok2, reason2 = card.authorize("Example Store", 5000, "USD", _NOW) self.assertFalse(ok2) self.assertEqual(reason2, "card_exhausted") def test_partial_amount_ok(self) -> None: card = self._card() ok, _ = card.authorize("Example Store", 4999, "USD", _NOW) self.assertTrue(ok) def test_wrong_merchant_refused(self) -> None: card = self._card() ok, reason = card.authorize("Evil Store", 5000, "USD", _NOW) self.assertFalse(ok) self.assertEqual(reason, "wrong_merchant") def test_amount_exceeded_refused(self) -> None: card = self._card() ok, reason = card.authorize("Example Store", 5001, "USD", _NOW) self.assertFalse(ok) self.assertEqual(reason, "amount_exceeded") def test_wrong_currency_refused(self) -> None: card = self._card() ok, reason = card.authorize("Example Store", 5000, "EUR", _NOW) self.assertFalse(ok) self.assertEqual(reason, "wrong_currency") def test_expired_refused(self) -> None: card = self._card() ok, reason = card.authorize("Example Store", 5000, "USD", _NOW + 901) self.assertFalse(ok) self.assertEqual(reason, "card_expired") def test_describe_and_repr_have_no_pan(self) -> None: card = self._card() self.assertNotIn(_TEST_PAN, str(card.describe())) self.assertNotIn(_TEST_PAN, repr(card)) class PurchaseDetailsTest(unittest.TestCase): def test_validate_ok_and_total(self) -> None: details = _details() details.validate() self.assertEqual(details.total_minor(), 5000) def test_total_mismatch_rejected(self) -> None: details = _details(amount=5001) with self.assertRaises(ValueError): details.validate() def test_bad_currency_rejected(self) -> None: details = PurchaseDetails( "Example Store", "shop.example.com", (("Widget", 1, 100),), 100, "US", "stripe-link") with self.assertRaises(ValueError): details.validate() def test_empty_items_rejected(self) -> None: details = PurchaseDetails( "Example Store", "shop.example.com", (), 100, "USD", "stripe-link") with self.assertRaises(ValueError): details.validate() def test_unknown_provider_rejected(self) -> None: details = PurchaseDetails( "Example Store", "shop.example.com", (("Widget", 1, 100),), 100, "USD", "sketchy-pay") with self.assertRaises(ValueError): details.validate() def test_digest_stable_and_tamper_sensitive(self) -> None: first = _details().digest() self.assertEqual(first, _details().digest()) tampered_price = PurchaseDetails( "Example Store", "shop.example.com", (("Widget", 2, 2501),), 5002, "USD", "stripe-link") self.assertNotEqual(first, tampered_price.digest()) other_merchant = PurchaseDetails( "Other Store", "shop.example.com", (("Widget", 2, 2500),), 5000, "USD", "stripe-link") self.assertNotEqual(first, other_merchant.digest()) class SpendLimitsTest(unittest.TestCase): def test_within_limits_ok(self) -> None: limits = SpendLimits(per_purchase_minor=10000, daily_minor=20000, thirty_day_minor=100000, daily_spent_minor=5000) ok, reason = limits.check(5000) self.assertTrue(ok) self.assertEqual(reason, "ok") def test_over_per_purchase_limit(self) -> None: limits = SpendLimits(per_purchase_minor=4999) ok, reason = limits.check(5000) self.assertFalse(ok) self.assertEqual(reason, "over_per_purchase_limit") def test_over_daily_limit(self) -> None: limits = SpendLimits(daily_minor=9000, daily_spent_minor=5000) ok, reason = limits.check(5000) self.assertFalse(ok) self.assertEqual(reason, "over_daily_limit") def test_over_thirty_day_limit(self) -> None: limits = SpendLimits(thirty_day_minor=9000, thirty_day_spent_minor=5000) ok, reason = limits.check(5000) self.assertFalse(ok) self.assertEqual(reason, "over_thirty_day_limit") def test_exact_limit_boundary_ok(self) -> None: limits = SpendLimits(per_purchase_minor=5000) ok, _ = limits.check(5000) self.assertTrue(ok) def test_limits_unavailable_never_invents_cap(self) -> None: limits = SpendLimits() ok, reason = limits.check(10_000_000) self.assertTrue(ok) self.assertEqual(reason, "limits_unavailable") class PaymentAuditTest(unittest.TestCase): def test_record_keeps_allowed_fields(self) -> None: audit = PaymentAudit(clock=_Clock()) rec = audit.record("payment_completed", {"merchant": "Example Store", "amount_minor": 5000, "masked_label": "Visa ....4242", "pending_id": "abc123"}) self.assertEqual(rec["event"], "payment_completed") self.assertEqual(rec["amount_minor"], 5000) self.assertEqual(rec["masked_label"], "Visa ....4242") def test_secret_field_names_dropped_and_flagged(self) -> None: audit = PaymentAudit(clock=_Clock()) rec = audit.record("payment_completed", {"merchant": "Example Store", "card_number": _TEST_PAN, "token": "tok_secret", "amount_minor": 5000}) self.assertNotIn("card_number", rec) self.assertNotIn("token", rec) self.assertTrue(rec["redacted"]) self.assertNotIn(_TEST_PAN, str(rec)) def test_unknown_fields_dropped_silently(self) -> None: audit = PaymentAudit(clock=_Clock()) rec = audit.record("payment_completed", {"merchant": "Example Store", "buyer_email": "a@example.com"}) self.assertNotIn("buyer_email", rec) self.assertEqual(len(audit.list_records()), 1) class SimulatedWalletTest(unittest.TestCase): def setUp(self) -> None: self.clock = _Clock() self.wallet = SimulatedWalletBackend(clock=self.clock) def test_supports_known_providers_only(self) -> None: self.assertTrue(self.wallet.supports("stripe-link")) self.assertTrue(self.wallet.supports("shop-pay")) self.assertFalse(self.wallet.supports("sketchy-pay")) def test_mint_and_charge_roundtrip(self) -> None: card = self.wallet.mint_card(_details(), now=_NOW) self.assertTrue(card.masked_label.startswith("Visa ....")) result = self.wallet.charge(card, _details(), now=_NOW) self.assertEqual(result["status"], "simulated_paid") self.assertEqual(result["amount_minor"], 5000) self.assertEqual(len(self.wallet.ledger()), 1) def test_no_pan_leaks_into_ledger_or_repr(self) -> None: card = self.wallet.mint_card(_details(), now=_NOW) self.wallet.charge(card, _details(), now=_NOW) blob = str(self.wallet.ledger()) + repr(self.wallet) self.assertNotIn("400000000000", blob) def test_charge_wrong_merchant_refused(self) -> None: card = self.wallet.mint_card(_details(), now=_NOW) other = PurchaseDetails( "Other Store", "shop.example.com", (("Widget", 2, 2500),), 5000, "USD", "stripe-link") result = self.wallet.charge(card, other, now=_NOW) self.assertEqual(result["status"], "refused") self.assertEqual(result["reason"], "wrong_merchant") def test_fail_next_charge_voids_card(self) -> None: card = self.wallet.mint_card(_details(), now=_NOW) self.wallet.fail_next_charge = True result = self.wallet.charge(card, _details(), now=_NOW) self.assertEqual(result["status"], "simulated_failed") self.assertEqual(result["reason"], "wallet_declined") ok, reason = card.authorize("Example Store", 5000, "USD", _NOW) self.assertFalse(ok) self.assertEqual(reason, "card_exhausted") def test_void_card_unknown_and_twice(self) -> None: self.assertFalse(self.wallet.void_card("otc_nope")) card = self.wallet.mint_card(_details(), now=_NOW) self.assertTrue(self.wallet.void_card(card.card_id)) self.assertFalse(self.wallet.void_card(card.card_id)) def test_add_and_list_methods(self) -> None: self.wallet.add_method("stripe-link", "pm_abc", "Visa ....4242", "shop.example.com") methods = self.wallet.list_methods("shop.example.com") self.assertEqual(len(methods), 1) self.assertEqual(methods[0]["method_id"], "pm_abc") self.assertNotIn(_TEST_PAN, str(methods)) if __name__ == "__main__": unittest.main()