CurrencyExchangeTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. <?php
  2. namespace Tests\Unit\Utils;
  3. use App\Utils\CurrencyExchange;
  4. use Exception;
  5. use Illuminate\Support\Facades\Cache;
  6. use Illuminate\Support\Facades\Http;
  7. use ReflectionClass;
  8. use Tests\TestCase;
  9. class CurrencyExchangeTest extends TestCase
  10. {
  11. public static function providerExchangeServices(): array
  12. {
  13. return [
  14. 'exchangerateApi_free' => [[
  15. 'name' => 'exchangerateApi',
  16. 'endpoint' => 'https://open.er-api.com/v6/latest/*',
  17. 'response' => '{"result":"success","provider":"https://www.exchangerate-api.com","documentation":"https://www.exchangerate-api.com/docs/free","terms_of_use":"https://www.exchangerate-api.com/terms","time_last_update_unix":1757462551,"time_last_update_utc":"Wed, 10 Sep 2025 00:02:31 +0000","time_next_update_unix":1757550141,"time_next_update_utc":"Thu, 11 Sep 2025 00:22:21 +0000","time_eol_unix":0,"base_code":"USD","rates":{"USD":1,"BYN":3.233971,"BZD":2,"CAD":1.383268,"CDF":2878.431948,"CHF":0.79644,"CLP":971.373897,"CNY":7.124079}}',
  18. 'expected' => 7.12,
  19. ]],
  20. 'exchangerateApi_paid' => [[
  21. 'name' => 'exchangerateApi',
  22. 'config' => ['services.currency.exchangerate-api_key' => 'fake_key'],
  23. 'endpoint' => 'https://v6.exchangerate-api.com/v6/*',
  24. 'response' => '{"result":"success","documentation":"https://www.exchangerate-api.com/docs","terms_of_use":"https://www.exchangerate-api.com/terms","time_last_update_unix":1757462401,"time_last_update_utc":"Wed, 10 Sep 2025 00:00:01 +0000","time_next_update_unix":1757548801,"time_next_update_utc":"Thu, 11 Sep 2025 00:00:01 +0000","base_code":"USD","target_code":"CNY","conversion_rate":7.1241}',
  25. 'expected' => 7.12,
  26. ]],
  27. 'k780' => [[
  28. 'name' => 'k780',
  29. 'endpoint' => 'https://sapi.k780.com/?app=finance.rate&scur=USD&tcur=CNY&appkey=10003&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json',
  30. 'response' => '{"success":"1","result":{"status":"ALREADY","scur":"USD","tcur":"CNY","ratenm":"美元/人民币","rate":"7.1206","update":"2025-09-10 22:47:01"}}',
  31. 'expected' => 7.12,
  32. ]],
  33. 'it120' => [[
  34. 'name' => 'it120',
  35. 'config' => ['services.currency.it120_key' => 'fake_key'],
  36. 'endpoint' => 'https://api.it120.cc/*',
  37. 'response' => '{"code":0,"data":{"rate":6.5749,"toCode":657.49,"fromCode":100},"msg":"success"}',
  38. 'expected' => 6.57,
  39. ]],
  40. 'fixer' => [[
  41. 'name' => 'fixer',
  42. 'config' => ['services.currency.apiLayer_key' => 'fake_key'],
  43. 'endpoint' => 'https://api.apilayer.com/fixer/latest*',
  44. 'response' => '{"success":true,"timestamp":1757522110,"base":"USD","date":"2025-09-10","rates":{"CNY":7.121499}}',
  45. 'expected' => 7.12,
  46. ]],
  47. 'currencyData' => [[
  48. 'name' => 'currencyData',
  49. 'config' => ['services.currency.apiLayer_key' => 'fake_key'],
  50. 'endpoint' => 'https://api.apilayer.com/currency_data/live*',
  51. 'response' => '{"success":true,"timestamp":1757522348,"source":"USD","quotes":{"USDCNY":7.121498}}',
  52. 'expected' => 7.12,
  53. ]],
  54. 'exchangeRatesData' => [[
  55. 'name' => 'exchangeRatesData',
  56. 'config' => ['services.currency.apiLayer_key' => 'fake_key'],
  57. 'endpoint' => 'https://api.apilayer.com/exchangerates_data/latest*',
  58. 'response' => '{"success":true,"timestamp":1757564885,"base":"USD","date":"2025-09-11","rates":{"CNY":7.12125}}',
  59. 'expected' => 7.12,
  60. ]],
  61. 'jsdelivrFile' => [[
  62. 'name' => 'jsdelivrFile',
  63. 'endpoint' => 'https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/*',
  64. 'response' => '{"date":"2025-09-10","usd":{"cfx":5.80551999,"chf":0.79810103,"chz":23.57267514,"clp":967.40244215,"cnh":7.1267651,"cny":7.12926824,"comp":0.022938216,"cop":3922.09820874,"crc":505.17600184,"cro":3.94111354,"crv":1.28387416,"cspr":103.9529098,"cuc":1,"cup":24.06001705,"cve":94.26691294,"cvx":0.28371305,"cyp":0.5003353,"czk":20.81016181,"dai":0.9997095,"dash":0.040430194}}',
  65. 'expected' => 7.12,
  66. ]],
  67. 'duckduckgo' => [[
  68. 'name' => 'duckduckgo',
  69. 'endpoint' => 'https://duckduckgo.com/js/spice/currency_convert/1/*',
  70. 'response' => '{"terms":"https://www.xe.com/legal/","privacy":"http://www.xe.com/privacy.php","from":"USD","amount":1.0,"timestamp":"2025-09-11T04:44:00Z","to":[{"quotecurrency":"CNY","mid":7.1209786645}]}',
  71. 'expected' => 7.12,
  72. ]],
  73. 'wise' => [[
  74. 'name' => 'wise',
  75. 'endpoint' => 'https://api.wise.com/v1/rates*',
  76. 'response' => '[{"rate":7.12105,"source":"USD","target":"CNY","time":"2025-09-11T04:47:28+0000"}]',
  77. 'expected' => 7.12,
  78. ]],
  79. 'xRates' => [[
  80. 'name' => 'xRates',
  81. 'endpoint' => 'https://www.x-rates.com/calculator/*',
  82. 'response' => '<div class="ccOutputBx"><span class="ccOutputTxt">1.00 USD =</span><span class="ccOutputRslt">7.121<span class="ccOutputTrail">208</span><span class="ccOutputCode"> CNY</span></span></div><span class="calOutputTS">Sep 11, 2025 05:21 UTC</span>',
  83. 'expected' => 7.12,
  84. ]],
  85. 'valutafx' => [[
  86. 'name' => 'valutafx',
  87. 'endpoint' => 'https://www.valutafx.com/api/v2/rates/lookup*',
  88. 'response' => '{"Amount":1,"Rate":7.1218,"UpdatedDateTimeUTC":"2025-09-11T06:15:00","FormattedResult":"= <span class=\"converter-result-to\">7.1218 CNY</span>","FormattedRates":"<span class=\"converter-rate-from\">1 USD</span> = <span class=\"converter-rate-to\">7.1218 CNY</span>","FormattedIndirectRates":"<span class=\"converter-rate-to\">1 CNY</span> = <span class=\"converter-rate-from\">0.14041 USD</span>","FormattedDateTime":"Last update <span class=\"converter-last-updated-value\">2025-09-11 6:15 AM UTC</span>","ErrorMessage":null}',
  89. 'expected' => 7.12,
  90. ]],
  91. 'unionpay' => [[
  92. 'name' => 'unionpay',
  93. 'endpoint' => 'https://www.unionpayintl.com/upload/jfimg/*',
  94. 'response' => '{"exchangeRateJson":[{"transCur":"AED","baseCur":"AUD","rateData":0.41397204},{"transCur":"TJS","baseCur":"CNY","rateData":0.75934168},{"transCur":"TMT","baseCur":"CNY","rateData":2.0464021},{"transCur":"TND","baseCur":"CNY","rateData":2.45719918},{"transCur":"TOP","baseCur":"CNY","rateData":2.99489085},{"transCur":"UGX","baseCur":"CNY","rateData":0.00204028},{"transCur":"USD","baseCur":"CNY","rateData":7.1399},{"transCur":"UYU","baseCur":"CNY","rateData":0.17892278},{"transCur":"UZS","baseCur":"CNY","rateData":0.00058941}],"curDate":"2025-09-11"}',
  95. 'expected' => 7.13,
  96. ]],
  97. 'baidu' => [[
  98. 'name' => 'baidu',
  99. 'endpoint' => 'https://finance.pae.baidu.com/vapi/async/v1*',
  100. 'response' => '{"DispExt":null,"QueryDispInfo":null,"ResultCode":0,"ResultNum":2,"QueryID":"162386106721317024","Result":[{"ClickNeed":"1","Degree":"0","DisplayData":{"StdCls":"2","StdStg":"5293","StdStl":"2","resultData":{"extData":{"OriginQuery":"","resourceid":"5293","tplt":"exrate"},"tplData":{"StdCls":"2","StdStg":"5293","StdStl":"2","card_order":"1","content1":"1美元=7.12270000人民币","content2":"1人民币=0.14039700美元","money1":"美元","money1_num":"1","money1_rev":"1","money2":"人民币","money2_num":"7.12270000","money2_rev":"0.140397","pk":[],"sigma_use":"1","strong_use":"1","templateName":"exrate","template_type":"1","text":"更新时间:2025-09-11 15:23 数据仅供参考"}},"strategy":{"ctplOrPhp":"1","hilightWord":"","precharge":"0","tempName":"unitstatic"}},"RecoverCacheTime":"0","Sort":"1","SrcID":"5293","SubResNum":"0","SubResult":[],"SuppInfo":"汇率换算","Title":"汇率换算","Weight":"3"}]}',
  101. 'expected' => 7.13,
  102. ]],
  103. ];
  104. }
  105. /**
  106. * @dataProvider providerExchangeServices
  107. */
  108. public function test_currency_exchange_services(array $case): void
  109. {
  110. // 设置配置
  111. if (isset($case['config'])) {
  112. foreach ($case['config'] as $key => $value) {
  113. config([$key => $value]);
  114. }
  115. }
  116. // 模拟HTTP响应
  117. if (isset($case['response'])) {
  118. $fakeResponses[$case['endpoint']] = Http::response($case['response']);
  119. }
  120. $fakeResponses['*'] = Http::response([], 500);
  121. Http::fake($fakeResponses);
  122. $result = CurrencyExchange::getCurrencyRate($case['target'] ?? 'CNY', $case['base'] ?? 'USD', $case['name']);
  123. $this->assertEqualsWithDelta(
  124. $case['expected'],
  125. $result,
  126. 0.01,
  127. "Currency exchange service {$case['name']} failed"
  128. );
  129. }
  130. public function test_currency_exchange_with_cache(): void
  131. {
  132. Cache::put('Currency_USD_CNY_ExRate', 6.5, 3600);
  133. $result = CurrencyExchange::getCurrencyRate('CNY', 'USD');
  134. $this->assertEquals(6.5, $result);
  135. // 验证没有进行HTTP请求
  136. Http::assertNothingSent();
  137. }
  138. public function test_currency_exchange_fallback(): void
  139. {
  140. // 模拟所有API都失败
  141. Http::fake([
  142. '*' => Http::response('Not Found', 404),
  143. ]);
  144. $result = CurrencyExchange::getCurrencyRate('CNY', 'USD');
  145. $this->assertNull($result);
  146. }
  147. public function test_convert_method(): void
  148. {
  149. Cache::put('Currency_USD_CNY_ExRate', 6.5, 3600);
  150. $result = CurrencyExchange::convert('CNY', 100, 'USD');
  151. $this->assertEquals(650.0, $result);
  152. }
  153. public function test_real_api_requests(): void
  154. {
  155. $target = 'CNY';
  156. $base = 'USD';
  157. $services = ['exchangerateApi', 'k780', 'it120', 'fixer', 'currencyData', 'exchangeRatesData',
  158. 'jsdelivrFile', 'duckduckgo', 'wise', 'xRates', 'valutafx', 'unionpay', 'baidu'];
  159. $successfulRequests = 0;
  160. $failedRequests = 0;
  161. $results = [];
  162. foreach ($services as $service) {
  163. try {
  164. $result = CurrencyExchange::getCurrencyRate($target, $base, $service);
  165. if (is_numeric($result)) {
  166. $successfulRequests++;
  167. } else {
  168. $failedRequests++;
  169. }
  170. $results[$service] = $result;
  171. } catch (Exception $e) {
  172. $failedRequests++;
  173. echo "Service {$service} failed with exception: ".$e->getMessage()."\n";
  174. }
  175. }
  176. // 输出测试结果摘要
  177. echo "实际汇率API请求测试结果:\n";
  178. echo "成功请求: {$successfulRequests}\n";
  179. echo "失败请求: {$failedRequests}\n";
  180. echo '总请求数: '.($successfulRequests + $failedRequests)."\n\n";
  181. // 输出详细结果
  182. foreach ($results as $service => $result) {
  183. echo "[{$service}] - ".json_encode($result, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE)."\n";
  184. }
  185. $this->assertGreaterThan(0, $successfulRequests, '至少应有一个汇率API请求成功');
  186. }
  187. protected function setUp(): void
  188. {
  189. parent::setUp();
  190. // 清理 HTTP 假造与缓存
  191. Http::fake([]);
  192. Cache::flush();
  193. // 重置 basicRequest
  194. $ref = new ReflectionClass(CurrencyExchange::class);
  195. if ($ref->hasProperty('basicRequest')) {
  196. $prop = $ref->getProperty('basicRequest');
  197. $prop->setAccessible(true);
  198. $prop->setValue(null, null);
  199. }
  200. }
  201. }