|
|
@@ -0,0 +1,170 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+declare(strict_types=1);
|
|
|
+
|
|
|
+/** Exercise the provider adapter with an in-memory HTTP transport. No SMS is sent. */
|
|
|
+
|
|
|
+namespace app\miniapi\service {
|
|
|
+ final class SmsTransportFixture
|
|
|
+ {
|
|
|
+ public static string $url = '';
|
|
|
+ public static array $options = [];
|
|
|
+ public static string|false $response = '0';
|
|
|
+ public static int $status = 200;
|
|
|
+ public static int $errorNumber = 0;
|
|
|
+ public static string $error = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ function curl_init(string $url): \stdClass
|
|
|
+ {
|
|
|
+ SmsTransportFixture::$url = $url;
|
|
|
+
|
|
|
+ return new \stdClass();
|
|
|
+ }
|
|
|
+
|
|
|
+ function curl_setopt_array(\stdClass $handle, array $options): bool
|
|
|
+ {
|
|
|
+ SmsTransportFixture::$options = $options;
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ function curl_exec(\stdClass $handle): string|false
|
|
|
+ {
|
|
|
+ return SmsTransportFixture::$response;
|
|
|
+ }
|
|
|
+
|
|
|
+ function curl_getinfo(\stdClass $handle, int $option): int
|
|
|
+ {
|
|
|
+ return SmsTransportFixture::$status;
|
|
|
+ }
|
|
|
+
|
|
|
+ function curl_errno(\stdClass $handle): int
|
|
|
+ {
|
|
|
+ return SmsTransportFixture::$errorNumber;
|
|
|
+ }
|
|
|
+
|
|
|
+ function curl_error(\stdClass $handle): string
|
|
|
+ {
|
|
|
+ return SmsTransportFixture::$error;
|
|
|
+ }
|
|
|
+
|
|
|
+ function curl_close(\stdClass $handle): void
|
|
|
+ {
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+namespace {
|
|
|
+ require dirname(__DIR__) . '/vendor/autoload.php';
|
|
|
+ \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
|
|
|
+ \support\App::loadAllConfig(['route']);
|
|
|
+
|
|
|
+ use app\miniapi\exception\ApiException;
|
|
|
+ use app\miniapi\service\SmsbaoSender;
|
|
|
+ use app\miniapi\service\SmsTransportFixture;
|
|
|
+ use Monolog\Handler\TestHandler;
|
|
|
+ use support\Log;
|
|
|
+
|
|
|
+ $checks = 0;
|
|
|
+ function check(bool $condition, string $label): void
|
|
|
+ {
|
|
|
+ global $checks;
|
|
|
+ $checks++;
|
|
|
+ if (!$condition) {
|
|
|
+ throw new RuntimeException($label);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function settings(array $changes): void
|
|
|
+ {
|
|
|
+ $property = new ReflectionProperty(\Webman\Config::class, 'config');
|
|
|
+ $all = $property->getValue();
|
|
|
+ $all['mini_api'] = array_replace($all['mini_api'], $changes);
|
|
|
+ $property->setValue(null, $all);
|
|
|
+ (new ReflectionProperty(\Webman\Config::class, 'flatCache'))->setValue(null, []);
|
|
|
+ }
|
|
|
+
|
|
|
+ function rejects(callable $action, int $expectedCode): void
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $action();
|
|
|
+ } catch (ApiException $exception) {
|
|
|
+ check($exception->getCode() === $expectedCode, 'Expected API error ' . $expectedCode);
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ throw new RuntimeException('Expected API rejection');
|
|
|
+ }
|
|
|
+
|
|
|
+ $original = config('mini_api');
|
|
|
+ $handler = new TestHandler(\Monolog\Logger::DEBUG, false);
|
|
|
+ $logger = Log::channel();
|
|
|
+ $logger->pushHandler($handler);
|
|
|
+ try {
|
|
|
+ $sender = new SmsbaoSender();
|
|
|
+ $password = md5('fixture-password');
|
|
|
+ settings([
|
|
|
+ 'sms_endpoint' => 'https://api.smsbao.com/sms',
|
|
|
+ 'sms_user' => 'fixture-user',
|
|
|
+ 'sms_password' => $password,
|
|
|
+ 'sms_signature' => '',
|
|
|
+ ]);
|
|
|
+ rejects(fn () => $sender->send('13800138000', '验证码123456'), 50301);
|
|
|
+ check(SmsTransportFixture::$url === '', 'Missing signature prevents the provider request');
|
|
|
+
|
|
|
+ settings(['sms_signature' => '云泉智配']);
|
|
|
+ $sender->send('13800138000', '验证码123456');
|
|
|
+ parse_str((string) parse_url(SmsTransportFixture::$url, PHP_URL_QUERY), $query);
|
|
|
+ check($query['p'] === $password, 'Configured MD5 password is not hashed twice');
|
|
|
+ check($query['c'] === '【云泉智配】验证码123456', 'Content includes the configured company signature');
|
|
|
+ check($query['m'] === '13800138000' && $query['u'] === 'fixture-user', 'Provider receives the intended account and recipient');
|
|
|
+ check(SmsTransportFixture::$options[CURLOPT_SSL_VERIFYPEER] === true, 'TLS peer verification stays enabled');
|
|
|
+ check(SmsTransportFixture::$options[CURLOPT_SSL_VERIFYHOST] === 2, 'TLS hostname verification stays enabled');
|
|
|
+
|
|
|
+ settings(['sms_signature' => '【云泉智配】', 'sms_password' => 'fixture-api-key']);
|
|
|
+ $sender->send('13800138000', '验证码123456');
|
|
|
+ parse_str((string) parse_url(SmsTransportFixture::$url, PHP_URL_QUERY), $query);
|
|
|
+ check($query['c'] === '【云泉智配】验证码123456', 'Bracketed signatures are not duplicated');
|
|
|
+ check($query['p'] === 'fixture-api-key', 'API keys pass through unchanged');
|
|
|
+
|
|
|
+ foreach ([
|
|
|
+ '30' => '账户密码错误',
|
|
|
+ '40' => '账户不存在',
|
|
|
+ '41' => '账户余额不足',
|
|
|
+ '43' => '来源IP受限',
|
|
|
+ '50' => '内容包含敏感词',
|
|
|
+ '51' => '手机号码不正确',
|
|
|
+ ] as $status => $reason) {
|
|
|
+ $handler->clear();
|
|
|
+ SmsTransportFixture::$response = (string) $status;
|
|
|
+ rejects(fn () => $sender->send('13800138000', '验证码123456'), 50302);
|
|
|
+ $record = $handler->getRecords()[0];
|
|
|
+ check($record['context']['reason'] === $reason, 'Official provider status is interpreted correctly: ' . $status);
|
|
|
+ check(array_key_exists('request_id', $record['context']), 'Provider rejection supports request correlation');
|
|
|
+ }
|
|
|
+
|
|
|
+ $handler->clear();
|
|
|
+ SmsTransportFixture::$response = false;
|
|
|
+ SmsTransportFixture::$status = 0;
|
|
|
+ SmsTransportFixture::$errorNumber = 28;
|
|
|
+ SmsTransportFixture::$error = 'Timeout accessing ' . SmsTransportFixture::$url;
|
|
|
+ rejects(fn () => $sender->send('13800138000', '验证码123456'), 50302);
|
|
|
+ $record = $handler->getRecords()[0];
|
|
|
+ check($record['context']['curl_errno'] === 28, 'Transport errno is available for diagnosis');
|
|
|
+ check(!str_contains(json_encode($record), 'fixture-api-key'), 'Transport logs redact provider credentials');
|
|
|
+ check(!str_contains(json_encode($record), '13800138000'), 'Transport logs do not contain a request URL or recipient');
|
|
|
+
|
|
|
+ $handler->clear();
|
|
|
+ SmsTransportFixture::$response = 'Unexpected HTML response containing fixture-api-key';
|
|
|
+ SmsTransportFixture::$status = 200;
|
|
|
+ rejects(fn () => $sender->send('13800138000', '验证码123456'), 50302);
|
|
|
+ $record = $handler->getRecords()[0];
|
|
|
+ check($record['context']['status'] === 'unexpected_response', 'Unexpected provider response is not logged verbatim');
|
|
|
+ check(!str_contains(json_encode($record), 'fixture-api-key'), 'Unexpected response cannot expose credentials');
|
|
|
+ } finally {
|
|
|
+ settings($original);
|
|
|
+ $logger->popHandler();
|
|
|
+ }
|
|
|
+
|
|
|
+ echo "PASS: $checks SMS provider checks; no network request or SMS sent.\n";
|
|
|
+}
|