PaymentProviderSnapshotTest.php 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  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\QianBaoService;
  7. use App\Services\Payment\SanJinService;
  8. use App\Services\Payment\ZimuPayService;
  9. use Illuminate\Events\Dispatcher;
  10. use Illuminate\Support\Facades\DB;
  11. use Illuminate\Support\Facades\Facade;
  12. use PHPUnit\Framework\Attributes\DataProvider;
  13. use PHPUnit\Framework\Attributes\PreserveGlobalState;
  14. use PHPUnit\Framework\Attributes\RunInSeparateProcess;
  15. use Tests\Support\RecordingProviderClient;
  16. class PaymentProviderSnapshotTest extends \Tests\Support\PaymentProviderTestCase
  17. {
  18. public static function providers(): array
  19. {
  20. return array_combine(['jd', 'no', 'sanjin', 'qianbao', 'zimu'], array_map(fn ($code) => [$code], ['jd', 'no', 'sanjin', 'qianbao', 'zimu']));
  21. }
  22. #[DataProvider('providers')]
  23. #[RunInSeparateProcess]
  24. #[PreserveGlobalState(false)]
  25. public function test_each_payment_request_uses_one_snapshot_and_the_next_request_sees_new_config(string $code): void
  26. {
  27. // Replace only the transport; execute the real provider payload and signing code.
  28. $this->assertFalse(class_exists(\GuzzleHttp\Client::class, false));
  29. class_alias(RecordingProviderClient::class, \GuzzleHttp\Client::class);
  30. $old = $new = [];
  31. foreach (PaymentProviderConfig::fields($code) as $field => $definition) {
  32. $old[$field] = match ($definition['type']) {
  33. 'url' => 'https://merchant-a.example.test/', 'user_ids' => '', 'secret' => 'secret-A', default => 'merchant-A',
  34. };
  35. $new[$field] = match ($definition['type']) {
  36. 'url' => 'https://merchant-b.example.test/', 'user_ids' => '', 'secret' => 'secret-B', default => 'merchant-B',
  37. };
  38. }
  39. $config = new PaymentProviderConfig();
  40. $config->save($code, $old);
  41. config(['app.url' => 'https://local.example.test']);
  42. DB::connection()->setEventDispatcher(new Dispatcher(Facade::getFacadeApplication()));
  43. $switched = false;
  44. DB::listen(function ($event) use (&$switched, $config, $code, $new) {
  45. if (!$switched && str_starts_with($event->sql, 'select') && str_contains($event->sql, 'payment_providers')) {
  46. $switched = true;
  47. $config->save($code, $new);
  48. }
  49. });
  50. $pay = match ($code) {
  51. 'jd' => fn () => JdPayService::pay('100', 'order'),
  52. 'no' => fn () => NoPayService::pay('100', 'order', 'member', 'NOpay12'),
  53. 'sanjin' => fn () => SanJinService::pay('100', 'order'),
  54. 'qianbao' => fn () => QianBaoService::payout('100', 'order', 'bank', 'account', 'card'),
  55. 'zimu' => fn () => ZimuPayService::pay('100', 'order', 'member'),
  56. };
  57. $pay();
  58. $this->assertTrue($switched);
  59. $this->assertRequestUses(RecordingProviderClient::$requests[0], $code, 'A');
  60. $pay();
  61. $this->assertRequestUses(RecordingProviderClient::$requests[1], $code, 'B');
  62. }
  63. private function assertRequestUses(array $request, string $code, string $version): void
  64. {
  65. $this->assertSame('https://merchant-' . strtolower($version) . '.example.test/', $request['base_uri'] ?? $request['url']);
  66. $data = $request['options']['form_params'] ?? $request['options']['json'];
  67. $merchantField = match ($code) { 'jd' => 'userCode', 'sanjin' => 'mchId', 'qianbao' => 'merchantNum', default => 'appId' };
  68. $this->assertSame('merchant-' . $version, $data[$merchantField]);
  69. $key = 'secret-' . $version;
  70. $expected = match ($code) {
  71. 'jd' => JdPayService::signature([$data['orderCode'], $data['amount'], $data['userCode']], $key),
  72. 'no' => NoPayService::signature($data, $key),
  73. 'sanjin' => SanJinService::signature(array_diff_key($data, ['sign' => true]), [], $key),
  74. 'qianbao' => md5($data['merchantNum'] . $data['orderNo'] . $data['amount'] . $key),
  75. 'zimu' => ZimuPayService::signature($data, $key),
  76. };
  77. $this->assertSame($expected, $data['sign']);
  78. }
  79. }