cache_path = sys_get_temp_dir() . '/currency_rate_test_' . uniqid() . '.json'; } protected function tearDown(): void { if (is_file($this->cache_path)) { unlink($this->cache_path); } if (is_file($this->cache_path . '.failed')) { unlink($this->cache_path . '.failed'); } } public function testRubAlwaysReturnsOne(): void { $client = new Client(['url' => 'https://unreachable.invalid/', 'timeout' => 1, 'cache_path' => $this->cache_path]); $this->assertSame(1.0, $client->rate('rub')); $this->assertSame(1.0, $client->rate('RUB')); } public function testUnreachableSourceReturnsNullWithoutThrowing(): void { $client = new Client(['url' => 'https://unreachable.invalid/', 'timeout' => 1, 'cache_path' => $this->cache_path]); $this->assertNull($client->rate('EUR')); } public function testReadsFromFreshCacheWithoutNetwork(): void { file_put_contents($this->cache_path, json_encode(['EUR' => 104.5])); // url заведомо невалиден — если бы клиент реально пошёл в сеть, тест бы не прошёл (null). $client = new Client(['url' => 'not-a-url', 'timeout' => 1, 'cache_path' => $this->cache_path, 'ttl' => 86400]); $this->assertSame(104.5, $client->rate('EUR')); } public function testUnknownCodeReturnsNull(): void { file_put_contents($this->cache_path, json_encode(['EUR' => 104.5])); $client = new Client(['url' => 'not-a-url', 'timeout' => 1, 'cache_path' => $this->cache_path, 'ttl' => 86400]); $this->assertNull($client->rate('XXX')); } /** * Неудачный fetch() (сеть/ЦБ недоступны) должен оставить .failed-маркер — см. Client::rates(). * fetch() замокан, а не реально бьёт в сеть: тайминг на "недоступном" хосте ненадёжен для * теста (DNS-отказ мгновенный, а реальный таймаут — секунды, в зависимости от окружения). */ public function testFailedFetchWritesFailureMarker(): void { $client = $this->getMockBuilder(Client::class) ->setConstructorArgs([['cache_path' => $this->cache_path]]) ->onlyMethods(['fetch']) ->getMock(); $client->method('fetch')->willReturn(null); $client->rate('EUR'); $this->assertFileExists($this->cache_path . '.failed'); } /** * Без этого кулдауна каждый следующий запрос (например, каждая загрузка дашборда, пока ЦБ * недоступен) заново ждал бы полный timeout — см. Client::rates(). Проверяем не временем * (ненадёжно), а тем, что fetch() вообще не вызывается повторно, пока действует недавний * .failed-маркер. */ public function testFailureCooldownSkipsFetch(): void { touch($this->cache_path . '.failed'); $client = $this->getMockBuilder(Client::class) ->setConstructorArgs([['cache_path' => $this->cache_path, 'fail_cooldown' => 300]]) ->onlyMethods(['fetch']) ->getMock(); $client->expects($this->never())->method('fetch'); $this->assertNull($client->rate('EUR')); } public function testFetchIsRetriedAfterCooldownExpires(): void { touch($this->cache_path . '.failed', time() - 10); $client = $this->getMockBuilder(Client::class) ->setConstructorArgs([['cache_path' => $this->cache_path, 'fail_cooldown' => 5]]) ->onlyMethods(['fetch']) ->getMock(); $client->expects($this->once())->method('fetch')->willReturn(null); $this->assertNull($client->rate('EUR')); } }