| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113 |
- <?php
- // Opt-in isolated MySQL only. Never loads .env or connects to the application's database.
- $socket = getenv('PAYMENT_TEST_MYSQL_SOCKET');
- if (!$socket) { echo "SKIP: set PAYMENT_TEST_MYSQL_SOCKET to an isolated test server\n"; exit(0); }
- if (!preg_match('#^/tmp/b28paycfg-[a-z0-9]+\.sock$#D', $socket)) throw new RuntimeException('Use an isolated /tmp/b28paycfg-*.sock server.');
- require dirname(__DIR__, 2) . '/vendor/autoload.php';
- use App\Services\Payment\PaymentProviderConfig;
- use Illuminate\Config\Repository;
- use Illuminate\Database\Capsule\Manager;
- use Illuminate\Encryption\Encrypter;
- use Illuminate\Foundation\Application;
- use Illuminate\Http\Request;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Facade;
- use Illuminate\Translation\ArrayLoader;
- use Illuminate\Translation\Translator;
- use Illuminate\Validation\Factory;
- use Illuminate\Validation\ValidationException;
- $mode = $argv[1] ?? 'parent';
- $dir = $argv[2] ?? sys_get_temp_dir() . '/payment-config-barriers-' . bin2hex(random_bytes(5));
- $case = $argv[3] ?? 'early';
- if ($mode === 'parent') {
- mkdir($dir, 0700, true);
- $pdo = new PDO('mysql:unix_socket=' . $socket, 'root', '');
- $pdo->exec('CREATE DATABASE IF NOT EXISTS bot28_payment_config_test');
- }
- $app = new Application(dirname(__DIR__, 2));
- $app->instance('config', new Repository(['app' => ['locale' => 'zh']]));
- $app->instance('request', Request::create('/'));
- $app->instance('encrypter', new Encrypter(str_repeat('k', 32), 'AES-256-CBC'));
- $app->instance('log', new Psr\Log\NullLogger());
- $translator = new Translator(new ArrayLoader(), 'zh');
- $app->instance('translator', $translator);
- $app->instance('validator', new Factory($translator, $app));
- Facade::setFacadeApplication($app);
- $db = new Manager($app);
- $db->addConnection(['driver' => 'mysql', 'unix_socket' => $socket, 'database' => 'bot28_payment_config_test', 'username' => 'root', 'password' => '', 'prefix' => 'bot_']);
- $db->setAsGlobal(); $db->bootEloquent();
- $app->instance('db', $db->getDatabaseManager());
- DB::statement('SET SESSION innodb_lock_wait_timeout = 8');
- DB::statement('SET SESSION TRANSACTION ISOLATION LEVEL ' . ($case === 'late' ? 'READ COMMITTED' : 'REPEATABLE READ'));
- function awaitFile(string $file): void
- {
- $deadline = microtime(true) + 10;
- while (!file_exists($file)) {
- if (microtime(true) > $deadline) throw new RuntimeException('Timed out waiting for ' . basename($file));
- usleep(10000);
- }
- }
- function check($actual, $expected, string $label): void
- {
- if ($actual !== $expected) throw new RuntimeException($label . ': ' . json_encode($actual));
- }
- if ($mode === 'reader') {
- touch($dir . '/reader-started');
- if ($case === 'early') DB::beginTransaction();
- DB::table('payment_orders')->insert(['type' => 1, 'channel' => 'JDpay', 'status' => 1]);
- $snapshot = PaymentProviderConfig::snapshot('jd');
- touch($dir . '/reader-ready');
- if ($case === 'early') { awaitFile($dir . '/release'); DB::commit(); }
- file_put_contents($dir . '/reader-result', $snapshot['mch_id']);
- exit(0);
- }
- if ($mode === 'writer') {
- touch($dir . '/writer-started');
- try {
- if ($case === 'late') DB::beginTransaction();
- (new PaymentProviderConfig())->save('jd', ['mch_id' => 'merchant-B', 'key' => 'secret-B']);
- if ($case === 'late') { touch($dir . '/writer-ready'); awaitFile($dir . '/release'); DB::commit(); }
- file_put_contents($dir . '/writer-result', 'saved');
- } catch (ValidationException $e) {
- file_put_contents($dir . '/writer-result', 'blocked');
- }
- exit(0);
- }
- foreach (['early', 'late'] as $case) {
- $barrier = $dir . '/' . $case;
- mkdir($barrier, 0700);
- foreach (['operation_audits', 'payment_orders', 'payment_providers'] as $table) $db->schema()->dropIfExists($table);
- DB::statement('CREATE TABLE bot_payment_providers (id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,code VARCHAR(32) UNIQUE,status TINYINT DEFAULT 1,config TEXT NULL,created_at DATETIME NULL,updated_at DATETIME NULL) ENGINE=InnoDB');
- DB::statement('CREATE TABLE bot_payment_orders (id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,type INT,channel VARCHAR(32),status INT) ENGINE=InnoDB');
- DB::statement('CREATE TABLE bot_operation_audits (id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,resource_type VARCHAR(64),resource_id BIGINT NULL,action VARCHAR(32),changes JSON,operator_id BIGINT NULL,operator_name VARCHAR(64),created_at DATETIME NULL,updated_at DATETIME NULL) ENGINE=InnoDB');
- DB::table('payment_providers')->insert(['code' => 'jd']);
- (new PaymentProviderConfig())->save('jd', ['mch_id' => 'merchant-A', 'key' => 'secret-A']);
- $start = function (string $worker) use ($barrier, $case) {
- $process = proc_open([PHP_BINARY, __FILE__, $worker, $barrier, $case], [0 => ['pipe', 'r'], 1 => ['file', $barrier . '/' . $worker . '.log', 'a'], 2 => ['file', $barrier . '/' . $worker . '.log', 'a']], $pipes);
- if (!is_resource($process)) throw new RuntimeException('Cannot start test worker.');
- fclose($pipes[0]);
- return $process;
- };
- $workers = [];
- try {
- $first = $case === 'early' ? 'reader' : 'writer';
- $second = $case === 'early' ? 'writer' : 'reader';
- $workers[] = $start($first);
- awaitFile($barrier . '/' . $first . '-ready');
- $workers[] = $start($second);
- awaitFile($barrier . '/' . $second . '-started');
- usleep(200000);
- check(file_exists($barrier . '/' . $second . '-result'), false, 'Concurrent operation waits for configuration lock');
- } finally {
- touch($barrier . '/release');
- foreach ($workers as $worker) check(proc_close($worker), 0, 'Worker exits successfully; inspect ' . $barrier . ' on failure');
- }
- check(file_get_contents($barrier . '/reader-result'), $case === 'early' ? 'merchant-A' : 'merchant-B', 'Payment uses a consistent committed configuration');
- check(file_get_contents($barrier . '/writer-result'), $case === 'early' ? 'blocked' : 'saved', 'Pending-order protection survives concurrent transactions');
- echo 'PASS ' . $case . " configuration/transaction race\n";
- }
|