Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e53c471
commit c9b642c
Showing
3 changed files
with
163 additions
and
128 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace AlexMasterov\OAuth2\Client\Provider\Tests; | ||
|
||
use League\OAuth2\Client\Token\AccessToken; | ||
|
||
trait CanAccessTokenStub | ||
{ | ||
protected function accessToken(...$args): AccessToken | ||
{ | ||
$default = [ | ||
'access_token' => bin2hex(random_bytes(128)), | ||
'expires_in' => 3600, | ||
]; | ||
|
||
$values = array_replace($default, ...$args); | ||
|
||
return new AccessToken($values); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace AlexMasterov\OAuth2\Client\Provider\Tests; | ||
|
||
use Psr\Http\Message\ResponseInterface; | ||
|
||
trait CanMockHttp | ||
{ | ||
protected function mockResponse( | ||
string $body = '', | ||
string $type = 'application/json', | ||
int $code = 200 | ||
): ResponseInterface { | ||
$response = self::createMock(ResponseInterface::class); | ||
$response->expects(self::any()) | ||
->method('getHeader') | ||
->with(self::stringContains('content-type')) | ||
->willReturn($type); | ||
$response->expects(self::any()) | ||
->method('getBody') | ||
->willReturn($body); | ||
$response->expects(self::any()) | ||
->method('getStatusCode') | ||
->willReturn($code); | ||
|
||
return $response; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,187 +1,172 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace AlexMasterov\OAuth2\Client\Tests\Provider; | ||
namespace AlexMasterov\OAuth2\Client\Provider\Tests; | ||
|
||
use AlexMasterov\OAuth2\Client\Provider\Exception\StackExchangeException; | ||
use AlexMasterov\OAuth2\Client\Provider\StackExchange; | ||
use Eloquent\Phony\Phpunit\Phony; | ||
use GuzzleHttp\ClientInterface; | ||
use League\OAuth2\Client\Token\AccessToken; | ||
use AlexMasterov\OAuth2\Client\Provider\{ | ||
StackExchange, | ||
StackExchangeException, | ||
StackExchangeResourceOwner, | ||
Tests\CanAccessTokenStub, | ||
Tests\CanMockHttp | ||
}; | ||
use PHPUnit\Framework\TestCase; | ||
use Psr\Http\Message\ResponseInterface; | ||
|
||
class StackExchangeTest extends TestCase | ||
{ | ||
/** | ||
* @var StackExchange | ||
*/ | ||
private $provider; | ||
use CanAccessTokenStub; | ||
use CanMockHttp; | ||
|
||
protected function setUp() | ||
{ | ||
$this->provider = new StackExchange([ | ||
'clientId' => 'mock_client_id', | ||
'clientSecret' => 'mock_secret', | ||
'redirectUri' => 'mock_redirect_uri', | ||
]); | ||
} | ||
|
||
protected function tearDown() | ||
{ | ||
parent::tearDown(); | ||
} | ||
|
||
protected function mockResponse($body) | ||
{ | ||
$response = Phony::mock(ResponseInterface::class); | ||
$response->getHeader->with('content-type')->returns('application/json'); | ||
$response->getBody->returns(json_encode($body)); | ||
|
||
return $response; | ||
} | ||
|
||
protected function mockClient(ResponseInterface $response) | ||
public function testAuthorizationUrl() | ||
{ | ||
$client = Phony::mock(ClientInterface::class); | ||
$client->send->returns($response); | ||
// Execute | ||
$url = $this->provider() | ||
->getAuthorizationUrl(); | ||
|
||
return $client; | ||
// Verify | ||
self::assertSame('/oauth', path($url)); | ||
} | ||
|
||
protected function getMethod($class, $name) | ||
public function testBaseAccessTokenUrl() | ||
{ | ||
$class = new \ReflectionClass($class); | ||
$method = $class->getMethod($name); | ||
$method->setAccessible(true); | ||
static $params = []; | ||
|
||
return $method; | ||
} | ||
|
||
public function testAuthorizationUrl() | ||
{ | ||
// Run | ||
$url = $this->provider->getAuthorizationUrl(); | ||
$path = \parse_url($url, PHP_URL_PATH); | ||
// Execute | ||
$url = $this->provider() | ||
->getBaseAccessTokenUrl($params); | ||
|
||
// Verify | ||
$this->assertSame('/oauth', $path); | ||
self::assertSame('/oauth/access_token', path($url)); | ||
} | ||
|
||
public function testBaseAccessTokenUrl() | ||
public function testResourceOwnerDetailsUrl() | ||
{ | ||
$params = []; | ||
// Stub | ||
$apiUrl = $this->apiUrl(); | ||
$tokenParams = [ | ||
'access_token' => 'mock_access_token', | ||
'site' => 'stackoverflow', | ||
]; | ||
|
||
// Run | ||
$url = $this->provider->getBaseAccessTokenUrl($params); | ||
$path = \parse_url($url, PHP_URL_PATH); | ||
list($accessToken, $site) = array_values($tokenParams); | ||
|
||
// Execute | ||
$detailUrl = $this->provider() | ||
->getResourceOwnerDetailsUrl($this->accessToken($tokenParams)); | ||
|
||
// Verify | ||
$this->assertSame('/oauth/access_token', $path); | ||
self::assertSame( | ||
"{$apiUrl}me?access_token={$accessToken}&site={$site}", | ||
$detailUrl | ||
); | ||
} | ||
|
||
public function testDefaultScopes() | ||
{ | ||
// Run | ||
$method = $this->getMethod(get_class($this->provider), 'getDefaultScopes'); | ||
$result = $method->invoke($this->provider); | ||
$getDefaultScopes = function () { | ||
return $this->getDefaultScopes(); | ||
}; | ||
|
||
// Execute | ||
$defaultScopes = $getDefaultScopes->call($this->provider()); | ||
|
||
// Verify | ||
$this->assertEquals([], $result); | ||
self::assertSame([], $defaultScopes); | ||
} | ||
|
||
public function testGetAccessToken() | ||
public function testParseResponse() | ||
{ | ||
// https://api.stackexchange.com/docs/authentication | ||
$body = [ | ||
'access_token' => 'mock_access_token', | ||
'token_type' => 'bearer', | ||
'expires_in' => \time() * 3600, | ||
'refresh_token' => 'mock_refresh_token', | ||
]; | ||
$getParseResponse = function ($response) { | ||
return $this->parseResponse($response); | ||
}; | ||
|
||
$response = $this->mockResponse($body); | ||
$client = $this->mockClient($response->get()); | ||
// Mock | ||
$plain = $this->mockResponse('mock_body=test', 'text/plain'); | ||
$default = $this->mockResponse(json_encode(['mock_body' => 'test'])); | ||
|
||
// Run | ||
$this->provider->setHttpClient($client->get()); | ||
$token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); | ||
// Execute | ||
$parsedPlain = $getParseResponse->call($this->provider(), $plain); | ||
$parsedDefault = $getParseResponse->call($this->provider(), $default); | ||
|
||
// Verify | ||
$this->assertNull($token->getResourceOwnerId()); | ||
$this->assertEquals($body['access_token'], $token->getToken()); | ||
$this->assertEquals($body['refresh_token'], $token->getRefreshToken()); | ||
$this->assertGreaterThanOrEqual($body['expires_in'], $token->getExpires()); | ||
self::assertSame(['mock_body' => 'test'], $parsedPlain); | ||
self::assertSame(['mock_body' => 'test'], $parsedDefault); | ||
} | ||
|
||
public function testUserProperty() | ||
public function testCheckResponse() | ||
{ | ||
$body = [ | ||
'items' => [ | ||
0 => [ | ||
'user_id' => 12345678, | ||
], | ||
], | ||
]; | ||
$getParseResponse = function () use (&$response, &$data) { | ||
return $this->checkResponse($response, $data); | ||
}; | ||
|
||
$tokenOptions = [ | ||
'access_token' => 'mock_access_token', | ||
'expires_in' => 3600, | ||
]; | ||
|
||
$token = new AccessToken($tokenOptions); | ||
$response = $this->mockResponse($body); | ||
$client = $this->mockClient($response->get()); | ||
// Stub | ||
$code = 400; | ||
$data = ['error' => [ | ||
'type' => 'Foo error', | ||
'message' => 'Error message', | ||
]]; | ||
|
||
// Run | ||
$this->provider->setHttpClient($client->get()); | ||
$user = $this->provider->getResourceOwner($token); | ||
// Mock | ||
$response = $this->mockResponse('', '', $code); | ||
|
||
// Verify | ||
$this->assertSame([$body['items'][0]['user_id']], $user->getId()); | ||
self::expectException(StackExchangeException::class); | ||
self::expectExceptionCode($code); | ||
self::expectExceptionMessage(implode(': ', $data['error'])); | ||
|
||
foreach ($user->toArray() as $user) { | ||
$this->assertArrayHasKey('user_id', $user); | ||
} | ||
// Execute | ||
$getParseResponse->call($this->provider()); | ||
} | ||
|
||
public function testParseResponse() | ||
public function testCreateResourceOwner() | ||
{ | ||
$body = 'access_token=mock_access_token&expires=3600'; | ||
parse_str($body, $parsed); | ||
$getCreateResourceOwner = function () use (&$response, &$token) { | ||
return $this->createResourceOwner($response, $token); | ||
}; | ||
|
||
$response = Phony::mock(ResponseInterface::class); | ||
$response->getHeader->with('content-type')->returns('text/plain'); | ||
$response->getBody->returns($body); | ||
$client = $this->mockClient($response->get()); | ||
// Stub | ||
$token = $this->accessToken(); | ||
$response = ['items' => [ | ||
0 => ['user_id' => random_int(1, 1000)], | ||
]]; | ||
|
||
// Run | ||
$method = $this->getMethod(get_class($this->provider), 'parseResponse'); | ||
$result = $method->invoke($this->provider, $response->get()); | ||
// Execute | ||
$resourceOwner = $getCreateResourceOwner->call($this->provider()); | ||
|
||
// Verify | ||
$this->assertEquals($parsed, $result); | ||
self::assertInstanceOf(StackExchangeResourceOwner::class, $resourceOwner); | ||
|
||
$items = $response['items']; | ||
$ids = array_values($items[0]); | ||
|
||
self::assertEquals($ids, $resourceOwner->getId()); | ||
self::assertSame($items, $resourceOwner->toArray()); | ||
} | ||
|
||
public function testErrorResponses() | ||
private function provider(...$args): StackExchange | ||
{ | ||
$code = 400; | ||
$body = [ | ||
'error' => [ | ||
'type' => 'Foo error', | ||
'message' => 'Error message', | ||
], | ||
static $default = [ | ||
'clientId' => 'mock_client_id', | ||
'clientSecret' => 'mock_secret', | ||
'redirectUri' => 'mock_redirect_uri', | ||
]; | ||
|
||
$response = $this->mockResponse($body); | ||
$response->getStatusCode->returns($code); | ||
$client = $this->mockClient($response->get()); | ||
$values = array_replace($default, ...$args); | ||
|
||
$this->expectException(StackExchangeException::class); | ||
$this->expectExceptionCode($code); | ||
$this->expectExceptionMessage(implode(': ', $body['error'])); | ||
return new StackExchange($values); | ||
} | ||
|
||
// Run | ||
$this->provider->setHttpClient($client->get()); | ||
$this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); | ||
private function apiUrl(): string | ||
{ | ||
$getApiUrl = function () { | ||
return $this->urlApi; | ||
}; | ||
|
||
return $getApiUrl->call($this->provider()); | ||
} | ||
} | ||
|
||
function path(string $url): string | ||
{ | ||
return parse_url($url, PHP_URL_PATH); | ||
} |