PaymentProviderConfigTest.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Services\Payment\JdPayService;
  4. use App\Services\Payment\NoPayService;
  5. use App\Services\Payment\PaymentProviderConfig;
  6. use App\Services\Payment\SanJinService;
  7. use App\Services\Payment\ZimuPayService;
  8. use Illuminate\Contracts\Encryption\DecryptException;
  9. use Illuminate\Encryption\Encrypter;
  10. use Illuminate\Support\Facades\DB;
  11. use Illuminate\Support\Facades\Facade;
  12. use RuntimeException;
  13. use Tests\Support\PaymentProviderTestCase;
  14. class PaymentProviderConfigTest extends PaymentProviderTestCase
  15. {
  16. public function test_unsaved_fields_fall_back_to_env_and_secrets_are_masked(): void
  17. {
  18. $result = $this->call('config', '/admin/paymentCompany/config', ['payment_company' => 'jd']);
  19. $this->assertSame(0, $result['code']);
  20. $fields = array_column($result['data']['fields'], null, 'field');
  21. $this->assertSame('env-jd-mch', $fields['mch_id']['value']);
  22. $this->assertSame('env', $fields['mch_id']['source']);
  23. $this->assertSame('', $fields['key']['value']);
  24. $this->assertTrue($fields['key']['has_value']);
  25. $this->assertSame('******1234', $fields['key']['masked']);
  26. $this->assertSame('empty', $fields['recharge_user_ids']['source']);
  27. $this->assertSame('env-jd-mch', JdPayService::getMerchantId());
  28. }
  29. public function test_saved_values_take_effect_immediately_and_blank_secret_keeps_existing(): void
  30. {
  31. $result = $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'jd', 'config' => [
  32. 'mch_id' => ' db-mch ', 'key' => 'db-secret-9999', 'pay_gateway' => 'https://db.example.com/pay',
  33. ]]);
  34. $this->assertSame(0, $result['code']);
  35. $this->assertSame('db-mch', JdPayService::getMerchantId());
  36. $this->assertSame('db-secret-9999', JdPayService::getSecret());
  37. $this->assertSame('https://db.example.com/pay', PaymentProviderConfig::get('jd', 'pay_gateway'));
  38. // 未提交字段继续回退 .env
  39. $fields = array_column((new PaymentProviderConfig())->show('jd')['fields'], null, 'field');
  40. $this->assertSame('admin', $fields['mch_id']['source']);
  41. $this->assertNotSame('admin', $fields['pay_query_gateway']['source']);
  42. $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'jd', 'config' => ['key' => '', 'mch_id' => 'db-mch-2']]);
  43. $this->assertSame('db-secret-9999', JdPayService::getSecret());
  44. $this->assertSame('db-mch-2', JdPayService::getMerchantId());
  45. $raw = DB::table('payment_providers')->where('code', 'jd')->value('config');
  46. $this->assertStringNotContainsString('db-secret-9999', $raw);
  47. $audit = DB::table('operation_audits')->where('resource_type', 'payment_company_config')->orderByDesc('id')->value('changes');
  48. $this->assertStringNotContainsString('db-secret-9999', $audit);
  49. $this->assertSame(2, DB::table('operation_audits')->where('resource_type', 'payment_company_config')->count());
  50. }
  51. public function test_recharge_whitelist_can_be_cleared_from_admin_to_allow_everyone(): void
  52. {
  53. $this->assertFalse(NoPayService::canUserRecharge(9));
  54. // 全角逗号不合法
  55. $this->assertNotSame(0, $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'no', 'config' => ['recharge_user_ids' => '9,10']])['code']);
  56. $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'no', 'config' => ['recharge_user_ids' => ' 9 , ,10']]);
  57. $this->assertSame(['9', '10'], PaymentProviderConfig::userIds('no'));
  58. $this->assertTrue(NoPayService::canUserRecharge(9));
  59. $this->assertFalse(NoPayService::canUserRecharge(7));
  60. $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'no', 'config' => ['recharge_user_ids' => '']]);
  61. $this->assertTrue(NoPayService::canUserRecharge(7));
  62. }
  63. public function test_invalid_input_is_rejected_without_writing(): void
  64. {
  65. $cases = [
  66. ['payment_company' => 'zimu', 'config' => ['pay_gateway' => 'not a url']],
  67. ['payment_company' => 'zimu', 'config' => ['recharge_user_ids' => 'abc']],
  68. ['payment_company' => 'zimu', 'config' => ['unknown' => 'x']],
  69. ['payment_company' => 'zimu', 'config' => ['app_id' => ['nested']]],
  70. ['payment_company' => 'nope', 'config' => ['app_id' => 'x']],
  71. ];
  72. foreach ($cases as $params) {
  73. $this->assertNotSame(0, $this->call('saveConfig', '/admin/paymentCompany/config/save', $params)['code']);
  74. }
  75. $this->assertNull(DB::table('payment_providers')->where('code', 'zimu')->value('config'));
  76. $this->assertSame('', ZimuPayService::getAppId());
  77. }
  78. public function test_sanjin_gateway_is_editable_with_builtin_default(): void
  79. {
  80. $this->assertSame('https://jkapi-sanjin.jkcbb.com/', (string)SanJinService::getClient()->getConfig('base_uri'));
  81. $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'sanjin', 'config' => ['gateway' => 'https://new.example.com/v1']]);
  82. $this->assertSame('https://new.example.com/v1/', (string)SanJinService::getClient()->getConfig('base_uri'));
  83. }
  84. public function test_unreadable_config_never_falls_back_to_env_or_opens_the_allowlist(): void
  85. {
  86. $config = new PaymentProviderConfig();
  87. $config->save('jd', ['mch_id' => 'active-merchant', 'key' => 'active-secret']);
  88. $config->save('no', ['recharge_user_ids' => '9']);
  89. $this->app->instance('encrypter', new Encrypter(str_repeat('x', 32), 'AES-256-CBC'));
  90. Facade::clearResolvedInstance('encrypter');
  91. foreach ([fn () => JdPayService::getMerchantId(), fn () => NoPayService::canUserRecharge(7)] as $read) {
  92. try {
  93. $read();
  94. $this->fail('An unreadable override must stop the operation.');
  95. } catch (RuntimeException $e) {
  96. $this->assertSame('支付商户配置读取失败,请联系管理员', $e->getMessage());
  97. $this->assertInstanceOf(DecryptException::class, $e->getPrevious());
  98. }
  99. }
  100. $response = $this->call('config', '/admin/paymentCompany/config', ['payment_company' => 'jd']);
  101. $this->assertSame(-3, $response['code']);
  102. $this->assertStringNotContainsString('MAC', $response['msg']);
  103. }
  104. public function test_database_read_failure_is_not_treated_as_unsaved_config(): void
  105. {
  106. DB::statement('DROP TABLE bot_payment_providers');
  107. $this->expectException(RuntimeException::class);
  108. $this->expectExceptionMessage('支付商户配置读取失败');
  109. PaymentProviderConfig::snapshot('jd');
  110. }
  111. public function test_pending_orders_block_credential_changes_for_all_providers_and_directions(): void
  112. {
  113. foreach ([['jd', 'mch_id', 'key', 'JDpay', 1], ['jd', 'mch_id', 'key', 'JD钱包', 2],
  114. ['no', 'deposit_mch_id', 'deposit_key', 'NOpay12', 1], ['no', 'withdraw_mch_id', 'withdraw_key', 'NOwithdraw', 2],
  115. ['zimu', 'app_id', 'key', '808pay', 1], ['zimu', 'app_id', 'key', 'ZIMUcash', 2],
  116. ['sanjin', 'mch_id', 'key', 'ylsm', 1], ['qianbao', 'merchant_id', 'secret', 'DF001', 2]] as [$code, $merchant, $secret, $channel, $type]) {
  117. DB::table('payment_orders')->delete();
  118. (new PaymentProviderConfig())->save($code, [$merchant => 'original', $secret => 'original-secret']);
  119. $audits = DB::table('operation_audits')->count();
  120. foreach ([0, 1, 4, 5] as $status) {
  121. DB::table('payment_orders')->delete();
  122. DB::table('payment_orders')->insert(compact('type', 'channel', 'status'));
  123. foreach ([$merchant, $secret] as $field) {
  124. $result = $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => $code, 'config' => [$field => 'changed']]);
  125. $this->assertSame(-3, $result['code']);
  126. $this->assertStringContainsString('存在未完成订单', $result['msg']);
  127. $this->assertSame('original', PaymentProviderConfig::get($code, $merchant));
  128. $this->assertSame('original-secret', PaymentProviderConfig::get($code, $secret));
  129. $this->assertSame($audits, DB::table('operation_audits')->count());
  130. }
  131. }
  132. }
  133. }
  134. public function test_rejected_merchant_change_preserves_a_valid_pending_order_callback(): void
  135. {
  136. $config = new PaymentProviderConfig();
  137. $config->save('jd', ['mch_id' => 'merchant-A', 'key' => 'secret-A']);
  138. $callback = ['userCode' => 'merchant-A', 'orderCode' => 'pending-order', 'amount' => '100.00', 'status' => '3'];
  139. $callback['sign'] = JdPayService::signature([$callback['orderCode'], $callback['amount'], $callback['userCode'], $callback['status']]);
  140. DB::table('payment_orders')->insert(['type' => 1, 'channel' => 'JDpay', 'status' => 1]);
  141. $result = $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'jd', 'config' => ['mch_id' => 'merchant-B', 'key' => 'secret-B']]);
  142. $this->assertSame(-3, $result['code']);
  143. $this->assertTrue(JdPayService::verifyPayNotify($callback));
  144. // 白名单、修正网关及原值保存不会中断已有订单。
  145. $config->save('jd', ['mch_id' => 'merchant-A', 'key' => '', 'recharge_user_ids' => '9', 'pay_gateway' => 'https://new.example.test/pay']);
  146. $this->assertTrue(JdPayService::verifyPayNotify($callback));
  147. DB::table('payment_orders')->update(['status' => 2]);
  148. $config->save('jd', ['mch_id' => 'merchant-B', 'key' => 'secret-B']);
  149. $this->assertSame('merchant-B', JdPayService::getMerchantId());
  150. }
  151. public function test_terminal_manual_and_other_provider_orders_do_not_block_updates(): void
  152. {
  153. foreach ([2, 3, 6] as $status) DB::table('payment_orders')->insert(['type' => 1, 'channel' => 'JDpay', 'status' => $status]);
  154. foreach ([3, 4] as $type) DB::table('payment_orders')->insert(['type' => $type, 'channel' => 'JDpay', 'status' => 0]);
  155. DB::table('payment_orders')->insert(['type' => 2, 'channel' => 'NOwithdraw', 'status' => 1]);
  156. (new PaymentProviderConfig())->save('jd', ['mch_id' => 'new-merchant', 'key' => 'new-secret']);
  157. $this->assertSame('new-merchant', JdPayService::getMerchantId());
  158. }
  159. }