202 lines
8.1 KiB
PHP
202 lines
8.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Weerscherm\Tests;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use Weerscherm\NetatmoAuthException;
|
|
use Weerscherm\NetatmoClient;
|
|
use Weerscherm\TokenStore;
|
|
use Weerscherm\UpstreamException;
|
|
|
|
final class NetatmoClientTest extends TestCase
|
|
{
|
|
private const NOW = 1_800_000_000;
|
|
|
|
private string $dir;
|
|
private TokenStore $store;
|
|
private FakeHttp $http;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->dir = sys_get_temp_dir() . '/weerscherm-netatmo-' . uniqid();
|
|
$this->store = new TokenStore($this->dir . '/netatmo-token.json');
|
|
$this->http = new FakeHttp();
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
array_map('unlink', glob($this->dir . '/*') ?: []);
|
|
@rmdir($this->dir);
|
|
}
|
|
|
|
private function client(): NetatmoClient
|
|
{
|
|
return new NetatmoClient($this->http, $this->store, 'cid', 'csecret', static fn(): int => self::NOW);
|
|
}
|
|
|
|
private function stationsBody(): array
|
|
{
|
|
return json_decode((string) file_get_contents(__DIR__ . '/fixtures/getstationsdata.json'), true);
|
|
}
|
|
|
|
public function testValidTokenIsUsedWithoutRefresh(): void
|
|
{
|
|
$this->store->write(['access_token' => 'A1', 'refresh_token' => 'R1', 'expires_at' => self::NOW + 3600]);
|
|
$this->http->queue(200, $this->stationsBody());
|
|
|
|
$body = $this->client()->getStationsData();
|
|
|
|
self::assertSame('70:ee:50:00:00:01', $body['devices'][0]['_id']);
|
|
self::assertCount(1, $this->http->requests);
|
|
self::assertSame('https://api.netatmo.com/api/getstationsdata', $this->http->requests[0]['url']);
|
|
self::assertContains('Authorization: Bearer A1', $this->http->requests[0]['headers']);
|
|
}
|
|
|
|
public function testExpiringTokenIsRefreshedAndRotatedRefreshTokenStored(): void
|
|
{
|
|
$this->store->write(['access_token' => 'A1', 'refresh_token' => 'R1', 'expires_at' => self::NOW + 200]);
|
|
$this->http
|
|
->queue(200, ['access_token' => 'A2', 'refresh_token' => 'R2', 'expires_in' => 10800])
|
|
->queue(200, $this->stationsBody());
|
|
|
|
$this->client()->getStationsData();
|
|
|
|
[$post, $get] = $this->http->requests;
|
|
self::assertSame('POST', $post['method']);
|
|
self::assertSame('https://api.netatmo.com/oauth2/token', $post['url']);
|
|
self::assertSame(['grant_type' => 'refresh_token', 'refresh_token' => 'R1', 'client_id' => 'cid', 'client_secret' => 'csecret'], $post['params']);
|
|
self::assertContains('Authorization: Bearer A2', $get['headers']);
|
|
self::assertSame(['access_token' => 'A2', 'refresh_token' => 'R2', 'expires_at' => self::NOW + 10800], $this->store->read());
|
|
}
|
|
|
|
public function testFreshlyInitialisedStoreWithoutAccessTokenRefreshesFirst(): void
|
|
{
|
|
$this->store->write(['access_token' => '', 'refresh_token' => 'R1', 'expires_at' => 0]);
|
|
$this->http
|
|
->queue(200, ['access_token' => 'A2', 'refresh_token' => 'R2', 'expires_in' => 10800])
|
|
->queue(200, $this->stationsBody());
|
|
|
|
$this->client()->getStationsData();
|
|
|
|
self::assertCount(1, $this->http->requestsOf('POST'));
|
|
}
|
|
|
|
public function testUnauthorizedResponseTriggersOneRefreshAndRetry(): void
|
|
{
|
|
$this->store->write(['access_token' => 'A1', 'refresh_token' => 'R1', 'expires_at' => self::NOW + 3600]);
|
|
$this->http
|
|
->queue(403, ['error' => ['code' => 3, 'message' => 'Access token expired']])
|
|
->queue(200, ['access_token' => 'A2', 'refresh_token' => 'R2', 'expires_in' => 10800])
|
|
->queue(200, $this->stationsBody());
|
|
|
|
$this->client()->getStationsData();
|
|
|
|
self::assertSame(['GET', 'POST', 'GET'], array_column($this->http->requests, 'method'));
|
|
self::assertContains('Authorization: Bearer A2', $this->http->requests[2]['headers']);
|
|
}
|
|
|
|
public function testSecondUnauthorizedResponseIsAuthError(): void
|
|
{
|
|
$this->store->write(['access_token' => 'A1', 'refresh_token' => 'R1', 'expires_at' => self::NOW + 3600]);
|
|
$this->http
|
|
->queue(401, '')
|
|
->queue(200, ['access_token' => 'A2', 'refresh_token' => 'R2', 'expires_in' => 10800])
|
|
->queue(401, '');
|
|
|
|
$this->expectException(NetatmoAuthException::class);
|
|
$this->client()->getStationsData();
|
|
}
|
|
|
|
public function testInvalidGrantIsAuthError(): void
|
|
{
|
|
$this->store->write(['access_token' => '', 'refresh_token' => 'R-oud', 'expires_at' => 0]);
|
|
$this->http->queue(400, ['error' => 'invalid_grant']);
|
|
|
|
$this->expectException(NetatmoAuthException::class);
|
|
$this->client()->getStationsData();
|
|
}
|
|
|
|
public function testMissingTokenFileIsAuthError(): void
|
|
{
|
|
$this->expectException(NetatmoAuthException::class);
|
|
$this->client()->getStationsData();
|
|
}
|
|
|
|
public function testServerErrorIsUpstreamError(): void
|
|
{
|
|
$this->store->write(['access_token' => 'A1', 'refresh_token' => 'R1', 'expires_at' => self::NOW + 3600]);
|
|
$this->http->queue(500, 'kapot');
|
|
|
|
$this->expectException(UpstreamException::class);
|
|
$this->client()->getStationsData();
|
|
}
|
|
|
|
public function testRefreshServerErrorIsUpstreamNotAuth(): void
|
|
{
|
|
$this->store->write(['access_token' => '', 'refresh_token' => 'R1', 'expires_at' => 0]);
|
|
$this->http->queue(503, '');
|
|
|
|
$this->expectException(UpstreamException::class);
|
|
$this->expectExceptionMessage('HTTP 503');
|
|
$this->client()->getStationsData();
|
|
}
|
|
|
|
public function testConcurrentClientsShareOneRefresh(): void
|
|
{
|
|
$this->store->write(['access_token' => 'A1', 'refresh_token' => 'R1', 'expires_at' => self::NOW + 100]);
|
|
$this->http
|
|
->queue(200, ['access_token' => 'A2', 'refresh_token' => 'R2', 'expires_in' => 10800])
|
|
->queue(200, $this->stationsBody())
|
|
->queue(200, $this->stationsBody());
|
|
|
|
$first = $this->client();
|
|
$second = $this->client();
|
|
$first->getStationsData();
|
|
$second->getStationsData();
|
|
|
|
self::assertCount(1, $this->http->requestsOf('POST'), 'de tweede client leest het verse token en ververst niet');
|
|
self::assertSame('R2', $this->store->read()['refresh_token']);
|
|
}
|
|
|
|
public function testRejectedTokenAlreadyReplacedByOtherProcessIsReusedWithoutRefresh(): void
|
|
{
|
|
$this->store->write(['access_token' => 'A1', 'refresh_token' => 'R1', 'expires_at' => self::NOW + 3600]);
|
|
$this->http->queue(403, '')->queue(200, $this->stationsBody());
|
|
|
|
// Nabootsen dat een ander proces al ververst heeft zodra ons token geweigerd wordt.
|
|
$http = new class($this->http, $this->store) extends FakeHttpDecorator {
|
|
public function get(string $url, array $query = [], array $headers = []): \Weerscherm\HttpResponse
|
|
{
|
|
$res = parent::get($url, $query, $headers);
|
|
if ($res->status === 403) {
|
|
$this->store->write(['access_token' => 'A9', 'refresh_token' => 'R9', 'expires_at' => 1_800_000_000 + 3600]);
|
|
}
|
|
return $res;
|
|
}
|
|
};
|
|
$client = new NetatmoClient($http, $this->store, 'cid', 'csecret', static fn(): int => self::NOW);
|
|
|
|
$client->getStationsData();
|
|
|
|
self::assertSame([], $this->http->requestsOf('POST'), 'geen eigen refresh nodig');
|
|
self::assertContains('Authorization: Bearer A9', $this->http->requests[1]['headers']);
|
|
}
|
|
|
|
public function testGetMeasureQuery(): void
|
|
{
|
|
$this->store->write(['access_token' => 'A1', 'refresh_token' => 'R1', 'expires_at' => self::NOW + 3600]);
|
|
$this->http->queue(200, json_decode((string) file_get_contents(__DIR__ . '/fixtures/getmeasure.json'), true));
|
|
|
|
$body = $this->client()->getMeasure('dev', 'mod', 1000, 2000);
|
|
|
|
self::assertCount(11, $body);
|
|
self::assertSame('https://api.netatmo.com/api/getmeasure', $this->http->requests[0]['url']);
|
|
self::assertSame([
|
|
'device_id' => 'dev', 'module_id' => 'mod', 'scale' => '1day', 'type' => 'sum_rain',
|
|
'date_begin' => 1000, 'date_end' => 2000, 'optimize' => 'false', 'real_time' => 'false',
|
|
], $this->http->requests[0]['params']);
|
|
}
|
|
}
|