mini_sms_sender_test.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. <?php
  2. declare(strict_types=1);
  3. /** Exercise the provider adapter with an in-memory HTTP transport. No SMS is sent. */
  4. namespace app\miniapi\service {
  5. final class SmsTransportFixture
  6. {
  7. public static string $url = '';
  8. public static array $options = [];
  9. public static string|false $response = '0';
  10. public static int $status = 200;
  11. public static int $errorNumber = 0;
  12. public static string $error = '';
  13. }
  14. function curl_init(string $url): \stdClass
  15. {
  16. SmsTransportFixture::$url = $url;
  17. return new \stdClass();
  18. }
  19. function curl_setopt_array(\stdClass $handle, array $options): bool
  20. {
  21. SmsTransportFixture::$options = $options;
  22. return true;
  23. }
  24. function curl_exec(\stdClass $handle): string|false
  25. {
  26. return SmsTransportFixture::$response;
  27. }
  28. function curl_getinfo(\stdClass $handle, int $option): int
  29. {
  30. return SmsTransportFixture::$status;
  31. }
  32. function curl_errno(\stdClass $handle): int
  33. {
  34. return SmsTransportFixture::$errorNumber;
  35. }
  36. function curl_error(\stdClass $handle): string
  37. {
  38. return SmsTransportFixture::$error;
  39. }
  40. function curl_close(\stdClass $handle): void
  41. {
  42. }
  43. }
  44. namespace {
  45. require dirname(__DIR__) . '/vendor/autoload.php';
  46. \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
  47. \support\App::loadAllConfig(['route']);
  48. use app\miniapi\exception\ApiException;
  49. use app\miniapi\service\SmsbaoSender;
  50. use app\miniapi\service\SmsTransportFixture;
  51. use Monolog\Handler\TestHandler;
  52. use support\Log;
  53. $checks = 0;
  54. function check(bool $condition, string $label): void
  55. {
  56. global $checks;
  57. $checks++;
  58. if (!$condition) {
  59. throw new RuntimeException($label);
  60. }
  61. }
  62. function settings(array $changes): void
  63. {
  64. $property = new ReflectionProperty(\Webman\Config::class, 'config');
  65. $all = $property->getValue();
  66. $all['mini_api'] = array_replace($all['mini_api'], $changes);
  67. $property->setValue(null, $all);
  68. (new ReflectionProperty(\Webman\Config::class, 'flatCache'))->setValue(null, []);
  69. }
  70. function rejects(callable $action, int $expectedCode): void
  71. {
  72. try {
  73. $action();
  74. } catch (ApiException $exception) {
  75. check($exception->getCode() === $expectedCode, 'Expected API error ' . $expectedCode);
  76. return;
  77. }
  78. throw new RuntimeException('Expected API rejection');
  79. }
  80. $original = config('mini_api');
  81. $handler = new TestHandler(\Monolog\Logger::DEBUG, false);
  82. $logger = Log::channel();
  83. $logger->pushHandler($handler);
  84. try {
  85. $sender = new SmsbaoSender();
  86. $password = md5('fixture-password');
  87. settings([
  88. 'sms_endpoint' => 'https://api.smsbao.com/sms',
  89. 'sms_user' => 'fixture-user',
  90. 'sms_password' => $password,
  91. 'sms_signature' => '',
  92. ]);
  93. rejects(fn () => $sender->send('13800138000', '验证码123456'), 50301);
  94. check(SmsTransportFixture::$url === '', 'Missing signature prevents the provider request');
  95. settings(['sms_signature' => '云泉智配']);
  96. $sender->send('13800138000', '验证码123456');
  97. parse_str((string) parse_url(SmsTransportFixture::$url, PHP_URL_QUERY), $query);
  98. check($query['p'] === $password, 'Configured MD5 password is not hashed twice');
  99. check($query['c'] === '【云泉智配】验证码123456', 'Content includes the configured company signature');
  100. check($query['m'] === '13800138000' && $query['u'] === 'fixture-user', 'Provider receives the intended account and recipient');
  101. check(SmsTransportFixture::$options[CURLOPT_SSL_VERIFYPEER] === true, 'TLS peer verification stays enabled');
  102. check(SmsTransportFixture::$options[CURLOPT_SSL_VERIFYHOST] === 2, 'TLS hostname verification stays enabled');
  103. settings(['sms_signature' => '【云泉智配】', 'sms_password' => 'fixture-api-key']);
  104. $sender->send('13800138000', '验证码123456');
  105. parse_str((string) parse_url(SmsTransportFixture::$url, PHP_URL_QUERY), $query);
  106. check($query['c'] === '【云泉智配】验证码123456', 'Bracketed signatures are not duplicated');
  107. check($query['p'] === 'fixture-api-key', 'API keys pass through unchanged');
  108. foreach ([
  109. '30' => '账户密码错误',
  110. '40' => '账户不存在',
  111. '41' => '账户余额不足',
  112. '43' => '来源IP受限',
  113. '50' => '内容包含敏感词',
  114. '51' => '手机号码不正确',
  115. ] as $status => $reason) {
  116. $handler->clear();
  117. SmsTransportFixture::$response = (string) $status;
  118. rejects(fn () => $sender->send('13800138000', '验证码123456'), 50302);
  119. $record = $handler->getRecords()[0];
  120. check($record['context']['reason'] === $reason, 'Official provider status is interpreted correctly: ' . $status);
  121. check(array_key_exists('request_id', $record['context']), 'Provider rejection supports request correlation');
  122. }
  123. $handler->clear();
  124. SmsTransportFixture::$response = false;
  125. SmsTransportFixture::$status = 0;
  126. SmsTransportFixture::$errorNumber = 28;
  127. SmsTransportFixture::$error = 'Timeout accessing ' . SmsTransportFixture::$url;
  128. rejects(fn () => $sender->send('13800138000', '验证码123456'), 50302);
  129. $record = $handler->getRecords()[0];
  130. check($record['context']['curl_errno'] === 28, 'Transport errno is available for diagnosis');
  131. check(!str_contains(json_encode($record), 'fixture-api-key'), 'Transport logs redact provider credentials');
  132. check(!str_contains(json_encode($record), '13800138000'), 'Transport logs do not contain a request URL or recipient');
  133. $handler->clear();
  134. SmsTransportFixture::$response = 'Unexpected HTML response containing fixture-api-key';
  135. SmsTransportFixture::$status = 200;
  136. rejects(fn () => $sender->send('13800138000', '验证码123456'), 50302);
  137. $record = $handler->getRecords()[0];
  138. check($record['context']['status'] === 'unexpected_response', 'Unexpected provider response is not logged verbatim');
  139. check(!str_contains(json_encode($record), 'fixture-api-key'), 'Unexpected response cannot expose credentials');
  140. } finally {
  141. settings($original);
  142. $logger->popHandler();
  143. }
  144. echo "PASS: $checks SMS provider checks; no network request or SMS sent.\n";
  145. }