call('config', '/admin/paymentCompany/config', ['payment_company' => 'jd']); $this->assertSame(0, $result['code']); $fields = array_column($result['data']['fields'], null, 'field'); $this->assertSame('env-jd-mch', $fields['mch_id']['value']); $this->assertSame('env', $fields['mch_id']['source']); $this->assertSame('', $fields['key']['value']); $this->assertTrue($fields['key']['has_value']); $this->assertSame('******1234', $fields['key']['masked']); $this->assertSame('empty', $fields['recharge_user_ids']['source']); $this->assertSame('env-jd-mch', JdPayService::getMerchantId()); } public function test_saved_values_take_effect_immediately_and_blank_secret_keeps_existing(): void { $result = $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'jd', 'config' => [ 'mch_id' => ' db-mch ', 'key' => 'db-secret-9999', 'pay_gateway' => 'https://db.example.com/pay', ]]); $this->assertSame(0, $result['code']); $this->assertSame('db-mch', JdPayService::getMerchantId()); $this->assertSame('db-secret-9999', JdPayService::getSecret()); $this->assertSame('https://db.example.com/pay', PaymentProviderConfig::get('jd', 'pay_gateway')); // 未提交字段继续回退 .env $fields = array_column((new PaymentProviderConfig())->show('jd')['fields'], null, 'field'); $this->assertSame('admin', $fields['mch_id']['source']); $this->assertNotSame('admin', $fields['pay_query_gateway']['source']); $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'jd', 'config' => ['key' => '', 'mch_id' => 'db-mch-2']]); $this->assertSame('db-secret-9999', JdPayService::getSecret()); $this->assertSame('db-mch-2', JdPayService::getMerchantId()); $raw = DB::table('payment_providers')->where('code', 'jd')->value('config'); $this->assertStringNotContainsString('db-secret-9999', $raw); $audit = DB::table('operation_audits')->where('resource_type', 'payment_company_config')->orderByDesc('id')->value('changes'); $this->assertStringNotContainsString('db-secret-9999', $audit); $this->assertSame(2, DB::table('operation_audits')->where('resource_type', 'payment_company_config')->count()); } public function test_recharge_whitelist_can_be_cleared_from_admin_to_allow_everyone(): void { $this->assertFalse(NoPayService::canUserRecharge(9)); // 全角逗号不合法 $this->assertNotSame(0, $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'no', 'config' => ['recharge_user_ids' => '9,10']])['code']); $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'no', 'config' => ['recharge_user_ids' => ' 9 , ,10']]); $this->assertSame(['9', '10'], PaymentProviderConfig::userIds('no')); $this->assertTrue(NoPayService::canUserRecharge(9)); $this->assertFalse(NoPayService::canUserRecharge(7)); $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'no', 'config' => ['recharge_user_ids' => '']]); $this->assertTrue(NoPayService::canUserRecharge(7)); } public function test_invalid_input_is_rejected_without_writing(): void { $cases = [ ['payment_company' => 'zimu', 'config' => ['pay_gateway' => 'not a url']], ['payment_company' => 'zimu', 'config' => ['recharge_user_ids' => 'abc']], ['payment_company' => 'zimu', 'config' => ['unknown' => 'x']], ['payment_company' => 'zimu', 'config' => ['app_id' => ['nested']]], ['payment_company' => 'nope', 'config' => ['app_id' => 'x']], ]; foreach ($cases as $params) { $this->assertNotSame(0, $this->call('saveConfig', '/admin/paymentCompany/config/save', $params)['code']); } $this->assertNull(DB::table('payment_providers')->where('code', 'zimu')->value('config')); $this->assertSame('', ZimuPayService::getAppId()); } public function test_sanjin_gateway_is_editable_with_builtin_default(): void { $this->assertSame('https://jkapi-sanjin.jkcbb.com/', (string)SanJinService::getClient()->getConfig('base_uri')); $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'sanjin', 'config' => ['gateway' => 'https://new.example.com/v1']]); $this->assertSame('https://new.example.com/v1/', (string)SanJinService::getClient()->getConfig('base_uri')); } public function test_unreadable_config_never_falls_back_to_env_or_opens_the_allowlist(): void { $config = new PaymentProviderConfig(); $config->save('jd', ['mch_id' => 'active-merchant', 'key' => 'active-secret']); $config->save('no', ['recharge_user_ids' => '9']); $this->app->instance('encrypter', new Encrypter(str_repeat('x', 32), 'AES-256-CBC')); Facade::clearResolvedInstance('encrypter'); foreach ([fn () => JdPayService::getMerchantId(), fn () => NoPayService::canUserRecharge(7)] as $read) { try { $read(); $this->fail('An unreadable override must stop the operation.'); } catch (RuntimeException $e) { $this->assertSame('支付商户配置读取失败,请联系管理员', $e->getMessage()); $this->assertInstanceOf(DecryptException::class, $e->getPrevious()); } } $response = $this->call('config', '/admin/paymentCompany/config', ['payment_company' => 'jd']); $this->assertSame(-3, $response['code']); $this->assertStringNotContainsString('MAC', $response['msg']); } public function test_database_read_failure_is_not_treated_as_unsaved_config(): void { DB::statement('DROP TABLE bot_payment_providers'); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('支付商户配置读取失败'); PaymentProviderConfig::snapshot('jd'); } public function test_pending_orders_block_credential_changes_for_all_providers_and_directions(): void { foreach ([['jd', 'mch_id', 'key', 'JDpay', 1], ['jd', 'mch_id', 'key', 'JD钱包', 2], ['no', 'deposit_mch_id', 'deposit_key', 'NOpay12', 1], ['no', 'withdraw_mch_id', 'withdraw_key', 'NOwithdraw', 2], ['zimu', 'app_id', 'key', '808pay', 1], ['zimu', 'app_id', 'key', 'ZIMUcash', 2], ['sanjin', 'mch_id', 'key', 'ylsm', 1], ['qianbao', 'merchant_id', 'secret', 'DF001', 2]] as [$code, $merchant, $secret, $channel, $type]) { DB::table('payment_orders')->delete(); (new PaymentProviderConfig())->save($code, [$merchant => 'original', $secret => 'original-secret']); $audits = DB::table('operation_audits')->count(); foreach ([0, 1, 4, 5] as $status) { DB::table('payment_orders')->delete(); DB::table('payment_orders')->insert(compact('type', 'channel', 'status')); foreach ([$merchant, $secret] as $field) { $result = $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => $code, 'config' => [$field => 'changed']]); $this->assertSame(-3, $result['code']); $this->assertStringContainsString('存在未完成订单', $result['msg']); $this->assertSame('original', PaymentProviderConfig::get($code, $merchant)); $this->assertSame('original-secret', PaymentProviderConfig::get($code, $secret)); $this->assertSame($audits, DB::table('operation_audits')->count()); } } } } public function test_rejected_merchant_change_preserves_a_valid_pending_order_callback(): void { $config = new PaymentProviderConfig(); $config->save('jd', ['mch_id' => 'merchant-A', 'key' => 'secret-A']); $callback = ['userCode' => 'merchant-A', 'orderCode' => 'pending-order', 'amount' => '100.00', 'status' => '3']; $callback['sign'] = JdPayService::signature([$callback['orderCode'], $callback['amount'], $callback['userCode'], $callback['status']]); DB::table('payment_orders')->insert(['type' => 1, 'channel' => 'JDpay', 'status' => 1]); $result = $this->call('saveConfig', '/admin/paymentCompany/config/save', ['payment_company' => 'jd', 'config' => ['mch_id' => 'merchant-B', 'key' => 'secret-B']]); $this->assertSame(-3, $result['code']); $this->assertTrue(JdPayService::verifyPayNotify($callback)); // 白名单、修正网关及原值保存不会中断已有订单。 $config->save('jd', ['mch_id' => 'merchant-A', 'key' => '', 'recharge_user_ids' => '9', 'pay_gateway' => 'https://new.example.test/pay']); $this->assertTrue(JdPayService::verifyPayNotify($callback)); DB::table('payment_orders')->update(['status' => 2]); $config->save('jd', ['mch_id' => 'merchant-B', 'key' => 'secret-B']); $this->assertSame('merchant-B', JdPayService::getMerchantId()); } public function test_terminal_manual_and_other_provider_orders_do_not_block_updates(): void { foreach ([2, 3, 6] as $status) DB::table('payment_orders')->insert(['type' => 1, 'channel' => 'JDpay', 'status' => $status]); foreach ([3, 4] as $type) DB::table('payment_orders')->insert(['type' => $type, 'channel' => 'JDpay', 'status' => 0]); DB::table('payment_orders')->insert(['type' => 2, 'channel' => 'NOwithdraw', 'status' => 1]); (new PaymentProviderConfig())->save('jd', ['mch_id' => 'new-merchant', 'key' => 'new-secret']); $this->assertSame('new-merchant', JdPayService::getMerchantId()); } }