PaymentProviderConfigImportTest.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Console\Commands\ImportPaymentProviderConfig;
  4. use App\Services\Payment\JdPayService;
  5. use App\Services\Payment\PaymentProviderConfig;
  6. use Illuminate\Encryption\Encrypter;
  7. use Illuminate\Support\Facades\DB;
  8. use Illuminate\Support\Facades\Facade;
  9. use Symfony\Component\Console\Tester\CommandTester;
  10. use Tests\Support\PaymentProviderTestCase;
  11. class PaymentProviderConfigImportTest extends PaymentProviderTestCase
  12. {
  13. private function command(array $options = []): CommandTester
  14. {
  15. $command = new ImportPaymentProviderConfig();
  16. $command->setLaravel($this->app);
  17. $tester = new CommandTester($command);
  18. $tester->execute($options);
  19. return $tester;
  20. }
  21. private function configs(): array
  22. {
  23. return DB::table('payment_providers')->orderBy('code')->pluck('config', 'code')->all();
  24. }
  25. public function test_default_preview_lists_only_field_names_and_never_writes_or_discloses_values(): void
  26. {
  27. $before = $this->configs();
  28. $result = $this->command();
  29. $this->assertSame(0, $result->getStatusCode());
  30. $output = $result->getDisplay();
  31. $this->assertStringContainsString('未写入数据库', $output);
  32. $this->assertStringContainsString('mch_id', $output);
  33. $this->assertStringContainsString('config:cache', $output);
  34. foreach (['sanjin', 'qianbao', 'jd', 'no', 'zimu'] as $code) $this->assertStringContainsString($code, $output);
  35. foreach (['env-jd-mch', 'env-jd-secret-1234', 'https://env.example.com/pay'] as $value) $this->assertStringNotContainsString($value, $output);
  36. $this->assertSame($before, $this->configs());
  37. $this->assertSame(0, DB::table('operation_audits')->count());
  38. }
  39. public function test_selected_company_import_preserves_exact_active_credentials_and_pending_callbacks(): void
  40. {
  41. config(['app.jd_pay_key' => ' fixture-key-with-spaces ', 'app.jd_pay_recharge_user_ids' => ' 7, 8 ']);
  42. $before = PaymentProviderConfig::snapshot('jd');
  43. DB::table('payment_orders')->insert(['type' => 1, 'channel' => 'JDpay', 'status' => 1]);
  44. $callback = ['userCode' => JdPayService::getMerchantId(), 'orderCode' => 'pending-order', 'amount' => '100.00', 'status' => '3'];
  45. $callback['sign'] = JdPayService::signature([$callback['orderCode'], $callback['amount'], $callback['userCode'], $callback['status']]);
  46. $result = $this->command(['--company' => ['jd'], '--write' => true]);
  47. $this->assertSame(0, $result->getStatusCode());
  48. $this->assertSame($before, PaymentProviderConfig::snapshot('jd'));
  49. $this->assertTrue(JdPayService::verifyPayNotify($callback));
  50. $this->assertSame(' fixture-key-with-spaces ', JdPayService::getSecret());
  51. $raw = $this->configs();
  52. $this->assertNotNull($raw['jd']);
  53. foreach (['no', 'sanjin', 'qianbao', 'zimu'] as $code) $this->assertNull($raw[$code]);
  54. $this->assertStringNotContainsString('fixture-key-with-spaces', $raw['jd']);
  55. $this->assertStringNotContainsString('fixture-key-with-spaces', $result->getDisplay());
  56. $audit = DB::table('operation_audits')->first();
  57. $this->assertSame('import_env', $audit->action);
  58. $this->assertSame('console:payment:config-import-env', $audit->operator_name);
  59. $changes = json_decode($audit->changes, true, 512, JSON_THROW_ON_ERROR);
  60. $this->assertSame('******', $changes['after']['key']);
  61. }
  62. public function test_existing_admin_values_including_empty_allowlist_win_and_repeated_import_is_idempotent(): void
  63. {
  64. (new PaymentProviderConfig())->save('jd', ['mch_id' => 'admin-merchant', 'key' => 'admin-secret', 'recharge_user_ids' => '']);
  65. config(['app.jd_pay_recharge_user_ids' => '99']);
  66. $before = PaymentProviderConfig::snapshot('jd');
  67. $result = $this->command(['--company' => ['jd'], '--write' => true]);
  68. $this->assertSame(0, $result->getStatusCode());
  69. $this->assertSame($before, PaymentProviderConfig::snapshot('jd'));
  70. $this->assertSame('admin-merchant', JdPayService::getMerchantId());
  71. $this->assertSame('admin-secret', JdPayService::getSecret());
  72. $this->assertSame('', PaymentProviderConfig::get('jd', 'recharge_user_ids'));
  73. $raw = $this->configs();
  74. $audits = DB::table('operation_audits')->count();
  75. $this->assertSame(0, $this->command(['--company' => ['jd', 'jd'], '--write' => true])->getStatusCode());
  76. $this->assertSame($raw, $this->configs());
  77. $this->assertSame($audits, DB::table('operation_audits')->count());
  78. }
  79. public function test_bulk_import_copies_only_nonempty_defaults_and_changes_no_effective_values(): void
  80. {
  81. $before = [];
  82. foreach (array_keys($this->configs()) as $code) $before[$code] = PaymentProviderConfig::snapshot($code);
  83. $this->assertSame(0, $this->command(['--write' => true])->getStatusCode());
  84. foreach ($before as $code => $snapshot) $this->assertSame($snapshot, PaymentProviderConfig::snapshot($code));
  85. $fields = array_column((new PaymentProviderConfig())->show('jd')['fields'], null, 'field');
  86. $this->assertSame('admin', $fields['mch_id']['source']);
  87. $this->assertSame('admin', $fields['key']['source']);
  88. $this->assertSame('empty', $fields['pay_query_gateway']['source']);
  89. $this->assertNull($this->configs()['zimu']);
  90. $this->assertSame('7,8', PaymentProviderConfig::get('no', 'recharge_user_ids'));
  91. }
  92. public function test_invalid_company_is_rejected_without_writing(): void
  93. {
  94. $before = $this->configs();
  95. $result = $this->command(['--company' => ['unknown'], '--write' => true]);
  96. $this->assertSame(1, $result->getStatusCode());
  97. $this->assertStringContainsString('代码无效', $result->getDisplay());
  98. $this->assertSame($before, $this->configs());
  99. $this->assertSame(0, DB::table('operation_audits')->count());
  100. }
  101. public function test_unreadable_later_company_rolls_back_the_whole_batch(): void
  102. {
  103. (new PaymentProviderConfig())->save('jd', ['mch_id' => 'admin-merchant', 'key' => 'admin-secret']);
  104. $before = $this->configs();
  105. $audits = DB::table('operation_audits')->count();
  106. $this->app->instance('encrypter', new Encrypter(str_repeat('x', 32), 'AES-256-CBC'));
  107. Facade::clearResolvedInstance('encrypter');
  108. $result = $this->command(['--write' => true]);
  109. $this->assertSame(1, $result->getStatusCode());
  110. $this->assertSame($before, $this->configs());
  111. $this->assertSame($audits, DB::table('operation_audits')->count());
  112. $this->assertStringNotContainsString('MAC', $result->getDisplay());
  113. $this->assertStringNotContainsString('admin-secret', $result->getDisplay());
  114. }
  115. public function test_missing_schema_returns_a_controlled_failure(): void
  116. {
  117. DB::statement('DROP TABLE bot_payment_providers');
  118. $result = $this->command(['--write' => true]);
  119. $this->assertSame(1, $result->getStatusCode());
  120. $this->assertStringContainsString('导入失败', $result->getDisplay());
  121. $this->assertStringNotContainsString('SQLSTATE', $result->getDisplay());
  122. }
  123. public function test_audit_failure_rolls_back_imported_secrets(): void
  124. {
  125. $before = $this->configs();
  126. DB::statement('DROP TABLE bot_operation_audits');
  127. $result = $this->command(['--company' => ['jd'], '--write' => true]);
  128. $this->assertSame(1, $result->getStatusCode());
  129. $this->assertSame($before, $this->configs());
  130. $this->assertStringNotContainsString('env-jd-secret-1234', $result->getDisplay());
  131. }
  132. }