This commit is contained in:
gaofeng
2026-09-11 09:34:15 +08:00
parent 8a02b7fd11
commit 5be2b76d88
3 changed files with 218 additions and 9 deletions

View File

@@ -5,8 +5,12 @@ const MINIAPI_TOKEN_KEY = 'test';
function authcode($value, $operation = 'DECODE', $key = '', $expiry = 0) { return $value; }
require dirname(__DIR__) . '/app/miniapi/service/VisaApplication/VisaApplicationService.php';
class VisaApplicationTestBase {}
class_alias(VisaApplicationTestBase::class, 'app\\miniapi\\controller\\Base');
require dirname(__DIR__) . '/app/miniapi/controller/VisaApplication.php';
use app\miniapi\service\VisaApplication\VisaApplicationService;
use app\miniapi\controller\VisaApplication;
$service = new VisaApplicationService();
if ($service->endpoint('https://user.example.com/users/list', 'start') !== 'https://user.example.com/visa_service/start') throw new RuntimeException('endpoint construction failed');
@@ -16,6 +20,8 @@ if ($service->endpoint('https://user.example.com/users/list', 'invalid') !== '')
$payload = $service->payload(9, ['order_source' => 'jnwxmini']);
if (isset($payload['user_id']) || (json_decode((string)($payload['user_token'] ?? ''), true) !== ['purpose' => 'visa_api', 'user_id' => 9, 'order_source' => 'jnwxmini'])) throw new RuntimeException('trusted token payload failed');
if (str_contains((new ReflectionMethod($service, 'start'))->getFileName() ? file_get_contents((new ReflectionMethod($service, 'start'))->getFileName()) : '', '$data + [\'order_source\'')) throw new RuntimeException('start source override failed');
if (VisaApplication::sourceForCode('evus_jn_app') !== 'jnandroid') throw new RuntimeException('android visa source mapping failed');
if (VisaApplication::sourceForCode('evus_jn_mp') !== 'jnwxmini' || VisaApplication::sourceForCode('evus_bj_mp') !== 'qlwxmini' || VisaApplication::sourceForCode('unknown') !== '') throw new RuntimeException('visa source mapping regression');
if (!str_contains((string)file_get_contents(dirname(__DIR__) . '/app/miniapi/controller/VisaApplication.php'), "sourceForCode(string \$code)") || !str_contains((string)file_get_contents(dirname(__DIR__) . '/app/miniapi/controller/VisaApplication.php'), "\$code === 'evus_bj_mp'") || !str_contains((string)file_get_contents(dirname(__DIR__) . '/app/miniapi/controller/VisaApplication.php'), "\$filters['source_code'] = \$this->orderSource()")) throw new RuntimeException('visa source allowlist failed');
$neutral = VisaApplicationService::decorateSubmitForCheck(['code' => 1, 'data' => ['order_sn' => 'V1', 'requires_payment' => false]]);
$payable = VisaApplicationService::decorateSubmitForCheck(['code' => 1, 'data' => ['order_sn' => 'V 1', 'requires_payment' => true]]);

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace {
const MINIAPI_TOKEN_KEY = 'test';
const PAY_MARK = 'evuscn';
function authcode($value, ...$args) { return $value; }
function config($key) { return 'https://example.test/home/users/list'; }
}
namespace think\facade { class Log { static function warning($message) {} } }
namespace api {
class Httpcurl {
public static array $result = [];
public static array $lastPayload = [];
static function request($url, $method, $data) { parse_str($data, self::$lastPayload); return [json_encode(self::$result), null, null, '']; }
}
}
namespace app\miniapi\service\Payment {
class MiniPayService {
public static int $calls = 0;
public static bool $throws = false;
public static array $result = ['code'=>0,'msg'=>'temporary failure'];
function requestForTarget(...$args) { self::$calls++; if (self::$throws) throw new \RuntimeException('offline'); return self::$result; }
}
}
namespace app\miniapi\controller {
class Base {
public array $input = ['openid'=>'test-openid'];
public string $appCode = 'evus_jn_mp';
protected function requireLogin() { return 7; }
protected function postData() { return $this->input; }
protected function currentMiniAppCode() { return $this->appCode; }
protected function currentMiniAppContext($business) { return new \stdClass(); }
protected function ok($data, $msg = '') { return ['code'=>1,'data'=>$data,'msg'=>$msg]; }
protected function fail($msg, $code = 0, $data = []) { return ['code'=>$code,'msg'=>$msg,'data'=>$data]; }
}
}
namespace {
require __DIR__ . '/../app/miniapi/service/VisaApplication/VisaApplicationService.php';
require __DIR__ . '/../app/miniapi/controller/VisaApplication.php';
function checkSubmit($condition, $message) { if (!$condition) throw new RuntimeException($message); }
$controller = new app\miniapi\controller\VisaApplication();
$data = ['order_sn'=>'TEST1','requires_payment'=>true,'total_price'=>7900,'payment_ready'=>true,'payment_business'=>'arrival_card','target_orders'=>[['target_table'=>'singapore','target_order_sn'=>'TEST1','applicant_index'=>0]]];
api\Httpcurl::$result = ['code'=>1,'data'=>$data];
$result = $controller->submit();
checkSubmit($result['code'] === 1 && $result['data']['submission_status'] === 'submitted' && !isset($result['data']['pay']), 'parameter failure must not undo submission');
app\miniapi\service\Payment\MiniPayService::$throws = true;
checkSubmit($controller->submit()['code'] === 1, 'transport exception must not undo submission');
app\miniapi\service\Payment\MiniPayService::$throws = false;
$data['target_orders'][] = ['target_table'=>'singapore','target_order_sn'=>'TEST2','applicant_index'=>1];
api\Httpcurl::$result['data'] = $data;
$before = app\miniapi\service\Payment\MiniPayService::$calls;
checkSubmit($controller->submit()['code'] === 1 && app\miniapi\service\Payment\MiniPayService::$calls === $before, 'batch must not auto-charge first applicant');
api\Httpcurl::$result = ['code'=>0,'msg'=>'validation failed'];
checkSubmit($controller->submit()['code'] === 0, 'actual submission failure remains a failure');
api\Httpcurl::$result = ['code'=>0,'msg'=>'validation failed','data'=>['field_errors'=>[['module_key'=>'traveler','field'=>'passport_number','path'=>'data.applicants.0.traveler.passport_number','message'=>'护照号格式不正确','value'=>'SECRET','money'=>'0.01','mark'=>'forged','target_table'=>'secret']],'target_orders'=>[['target_table'=>'secret']],'pay_url'=>'https://secret.test']];
checkSubmit(($controller->submit()['data'] ?? []) === [], 'legacy submit rejection must not expose direct field metadata');
$controller->appCode = 'evus_jn_app';
$controller->input = ['request_id'=>str_repeat('d',64),'service_code'=>'TEST'];
api\Httpcurl::$result = ['code'=>1,'data'=>['order_sn'=>'TEST1','requires_payment'=>true,'total_price'=>7900,'payment_ready'=>true,'payment_business'=>'arrival_card','target_orders'=>[['target_table'=>'singapore','target_order_sn'=>'TEST1','applicant_index'=>0]],'submission_status'=>'submitted', 'pay_url'=>'https://secret.test']];
$androidStatus = $controller->directStatus();
checkSubmit(($androidStatus['data']['app_payment'] ?? null) === ['order_sn'=>'TEST1','money'=>'79.00','mark'=>'evuscnsingapore','business'=>'arrival_card'] && !isset($androidStatus['data']['target_orders'], $androidStatus['data']['pay_url']), 'android direct status must sanitize internal targets and expose trusted payment business');
$androidForm = $controller->directForm();
checkSubmit(($androidForm['data']['app_payment']['business'] ?? '') === 'arrival_card' && !isset($androidForm['data']['target_orders'], $androidForm['data']['pay_url']), 'android existing direct form result uses the same safe payment shape');
api\Httpcurl::$result = ['code'=>1,'data'=>['order_sn'=>'TEST1','requires_payment'=>true,'total_price'=>7900,'payment_ready'=>false,'directory_synced'=>true,'payment_business'=>'arrival_card','target_orders'=>[['target_table'=>'singapore','target_order_sn'=>'TEST1','applicant_index'=>0]]]];
checkSubmit(!isset($controller->directStatus()['data']['app_payment']), 'android status must not create payment before directory payment readiness');
api\Httpcurl::$result = ['code'=>0,'msg'=>'validation failed','data'=>['field_errors'=>[['module_key'=>'traveler','field'=>'passport_number','path'=>'data.applicants.0.traveler.passport_number','message'=>'护照号格式不正确','value'=>'SECRET','money'=>'0.01','mark'=>'forged','target_table'=>'secret']],'target_orders'=>[['target_table'=>'secret']],'pay_url'=>'https://secret.test']];
$controller->input = ['request_id'=>str_repeat('f',64),'service_code'=>'TEST','nationality_iso2'=>'CN','destination_iso2'=>'SG'];
$safeFailure = $controller->directSubmit();
checkSubmit($safeFailure['code'] === 0 && ($safeFailure['data']['field_errors'] ?? null) === [['module_key'=>'traveler','field_id'=>'passport_number','path'=>'data.applicants.0.traveler.passport_number','message'=>'护照号格式不正确']], 'direct rejection forwards only bounded field location metadata');
checkSubmit(!str_contains(json_encode($safeFailure['data']), 'SECRET') && !str_contains(json_encode($safeFailure['data']), 'forged') && !str_contains(json_encode($safeFailure['data']), 'secret'), 'direct rejection must drop sensitive upstream keys and values');
$controller->appCode = 'evus_jn_mp';
$controller->input = ['openid'=>'test-openid'];
api\Httpcurl::$result = ['code'=>1,'data'=>$data];
$controller->input += ['business'=>'singapore','order_sn'=>'TEST1','total_price'=>1,'target_table'=>'evus'];
api\Httpcurl::$result = ['code'=>1,'data'=>array_merge($data, ['target_orders'=>[$data['target_orders'][0]]])];
app\miniapi\service\Payment\MiniPayService::$result = ['code'=>1,'data'=>['package'=>'prepay_id=test']];
checkSubmit($controller->orderPayment()['code'] === 1, 'per-person payment parameters are forwarded');
checkSubmit(!isset(api\Httpcurl::$lastPayload['total_price'], api\Httpcurl::$lastPayload['target_table']), 'client cannot choose amount or target table');
$controller->input = ['openid'=>'test-openid','request_id'=>str_repeat('a',64),'service_code'=>'TEST','nationality_iso2'=>'CN','destination_iso2'=>'SG'];
checkSubmit($controller->directSubmit()['code'] === 1, 'new direct submit still obtains original native payment parameters');
checkSubmit(api\Httpcurl::$lastPayload['request_id'] === str_repeat('a',64) && !isset(api\Httpcurl::$lastPayload['application_token'],api\Httpcurl::$lastPayload['submission_token']), 'direct submit needs no application credentials');
$claims=json_decode(api\Httpcurl::$lastPayload['user_token'],true);
checkSubmit($claims['order_source']==='jnwxmini' && $claims['user_id']===7, 'new endpoint forwards verified account and source');
$controller->appCode = 'evus_jn_app';
$controller->input = ['request_id'=>str_repeat('b',64),'service_code'=>'TEST','nationality_iso2'=>'CN','destination_iso2'=>'SG','mark'=>'forged','money'=>'0.01','target_table'=>'evus'];
$androidSubmit = $controller->directSubmit();
checkSubmit(($androidSubmit['data']['app_payment'] ?? null) === ['order_sn'=>'TEST1','money'=>'79.00','mark'=>'evuscnsingapore','business'=>'arrival_card'] && !isset($androidSubmit['data']['pay']), 'android direct submit must expose only the server-derived app payment target and business');
$controller->input = ['business'=>'singapore','order_sn'=>'TEST1','mark'=>'forged','money'=>'0.01','target_table'=>'evus'];
$androidRepay = $controller->orderPayment();
checkSubmit(($androidRepay['data']['app_payment'] ?? null) === ['order_sn'=>'TEST1','money'=>'79.00','mark'=>'evuscnsingapore','business'=>'singapore'] && !isset($androidRepay['data']['pay']), 'android repeat payment must only echo the upstream-verified public business');
echo "PASS visa_submission_payment_self_check\n";
}