From 6e48e75de084c396d1f0fefe66b375a6548eddb6 Mon Sep 17 00:00:00 2001 From: Matthew Radcliffe Date: Wed, 19 Jun 2024 11:49:52 -0400 Subject: [PATCH] 22: Resolves middleware not working after hours of troubleshooting guzzle's opaque error handling --- src/XeroClient.php | 45 +++++++++++++++++++++--------- tests/src/XeroClientOAuth2Test.php | 37 +++++++++--------------- tests/src/XeroClientTest.php | 29 +++++++++++++------ 3 files changed, 66 insertions(+), 45 deletions(-) diff --git a/src/XeroClient.php b/src/XeroClient.php index f523c61..c8ed946 100644 --- a/src/XeroClient.php +++ b/src/XeroClient.php @@ -89,22 +89,24 @@ public function getConnections(): array /** * {@inheritdoc} - * - * @throws \Radcliffe\Xero\Exception\InvalidOptionsException */ public static function createFromConfig(array $config, array $options = []): static { - if (isset($options['tenant'])) { - $config['headers']['xero-tenant-id'] = $options['tenant']; + if (!isset($config['Accept'])) { + $config['Accept'] = 'application/json'; + } + + if (!isset($config['base_uri'])) { + $config['base_uri'] = 'https://api.xero.com/api.xro/2.0/'; } if (isset($config['handler']) && is_a($config['handler'], '\GuzzleHttp\HandlerStack')) { - $stack = $config['handler']; + $stack = HandlerStack::create($config['handler']); } else { $stack = HandlerStack::create(); } - $stack->push(Middleware::mapRequest(function (RequestInterface $request) use ($options) { + $stack->before('prepare_body', Middleware::mapRequest(function (RequestInterface $request) use ($options) { $validUrls = array_filter(self::getValidUrls(), fn ($url) => str_starts_with($request->getUri(), $url)); if (empty($validUrls)) { throw new XeroRequestException('API URL is not valid', $request); @@ -113,8 +115,13 @@ public static function createFromConfig(array $config, array $options = []): sta if (!isset($options['auth_token'])) { throw new XeroRequestException('Missing required parameter auth_token', $request); } - return $request->withHeader('Authorization', 'Bearer ' . $options['auth_token']); - })); + $new = $request->withHeader('Authorization', 'Bearer ' . $options['auth_token']); + + if (isset($options['tenant'])) { + $new = $new->withHeader('Xero-tenant-id', $options['tenant']); + } + return $new; + }), 'xero'); $client = new Client($config + [ 'handler' => $stack, @@ -126,7 +133,6 @@ public static function createFromConfig(array $config, array $options = []): sta * {@inheritdoc} * * @throws \League\OAuth2\Client\Provider\Exception\IdentityProviderException - * @throws \Radcliffe\Xero\Exception\InvalidOptionsException */ public static function createFromToken( string $id, @@ -138,6 +144,8 @@ public static function createFromToken( array $collaborators = [], string $redirectUri = '' ): static { + $guzzle_options = []; + $xero_options = []; if ($grant !== null) { // Fetch a new access token from a refresh token. $provider = new XeroProvider([ @@ -157,14 +165,20 @@ public static function createFromToken( $token = $refreshedToken->getToken(); } + $guzzle_options['Accept'] = 'application/json'; + if (isset($options['handler'])) { + $guzzle_options['handler'] = $options['handler']; + } if (!isset($options['base_uri'])) { - $options['base_uri'] = 'https://api.xero.com/api.xro/2.0/'; + $guzzle_options['base_uri'] = 'https://api.xero.com/api.xro/2.0/'; + } + if (isset($options['tenant'])) { + $xero_options['tenant'] = $options['tenant']; } + $xero_options['auth_token'] = $token; // Create a new static instance. - $instance = self::createFromConfig($options, ['auth_token' => $token]); - - $instance->tenantIds = $instance->getConnections(); + $instance = self::createFromConfig($guzzle_options, $xero_options); if (isset($refreshedToken)) { $instance->refreshedToken = $refreshedToken; @@ -226,10 +240,15 @@ public function getRefreshedToken(): ?AccessTokenInterface /** * The tenant guids accessible by this client. * + * This will make a request if tenant ids is empty. + * * @return string[] */ public function getTenantIds(): array { + if (!$this->tenantIds) { + $this->tenantIds = $this->getConnections(); + } return $this->tenantIds; } } diff --git a/tests/src/XeroClientOAuth2Test.php b/tests/src/XeroClientOAuth2Test.php index 1b16a32..51a9294 100644 --- a/tests/src/XeroClientOAuth2Test.php +++ b/tests/src/XeroClientOAuth2Test.php @@ -41,7 +41,7 @@ public function testCreateFromTokenError(): void 'instance' => $this->createGuid(), ])), ]); - $options = ['handler' => new HandlerStack($mock)]; + $options = ['handler' => HandlerStack::create($mock)]; $httpClient = new Client($options); $this->expectException(IdentityProviderException::class); @@ -60,23 +60,23 @@ public function testCreateFromTokenError(): void * Tests creating from a refresh token. * * @throws \League\OAuth2\Client\Provider\Exception\IdentityProviderException - * @throws \Radcliffe\Xero\Exception\InvalidOptionsException */ public function testCreateFromRefreshToken(): void { + $expected = [ + [ + 'id' => $this->createGuid(), + 'tenantId' => $this->createGuid(), + 'tenantType' => 'ORGANISATION', + ], + ]; $token = $this->createRandomString(30); $refresh_token = $this->createRandomString(30); - $tenantIdResponse = json_encode([ - [ - 'id' => $this->createGuid(), - 'tenantId' => $this->createGuid(), - 'tenantType' => 'ORGANISATION', - ], - ]); + $tenantIdResponse = json_encode($expected); $mock = new MockHandler([ new Response(200, ['Content-Type' => 'application/json'], $tenantIdResponse), ]); - $options = ['handler' => new HandlerStack($mock)]; + $options = ['handler' => HandlerStack::create($mock)]; // Mocks the OAuth2 Client request factory and requests. $refreshTokenResponse = json_encode([ @@ -88,7 +88,7 @@ public function testCreateFromRefreshToken(): void $providerMock = new MockHandler([ new Response(200, ['Content-Type' => 'application/json'], $refreshTokenResponse), ]); - $providerOptions = ['handler' => new HandlerStack($providerMock)]; + $providerOptions = ['handler' => HandlerStack::create($providerMock)]; $httpClient = new Client($providerOptions); @@ -103,29 +103,18 @@ public function testCreateFromRefreshToken(): void 'https://example.com/authorize' ); - $this->assertInstanceOf('\Radcliffe\Xero\XeroClient', $client); + $this->assertEquals($expected, $client->getTenantIds()); } /** * Tests creating from an access token. * * @throws \League\OAuth2\Client\Provider\Exception\IdentityProviderException - * @throws \Radcliffe\Xero\Exception\InvalidOptionsException */ public function testCreateFromAccessToken(): void { $token = $this->createRandomString(30); - $tenantIdResponse = json_encode([ - [ - 'id' => $this->createGuid(), - 'tenantId' => $this->createGuid(), - 'tenantType' => 'ORGANISATION', - ], - ]); - $mock = new MockHandler([ - new Response(200, ['Content-Type' => 'application/json'], $tenantIdResponse), - ]); - $options = ['handler' => new HandlerStack($mock)]; + $options = []; $client = XeroClient::createFromToken( $this->clientId, diff --git a/tests/src/XeroClientTest.php b/tests/src/XeroClientTest.php index 5e73354..8eebe29 100644 --- a/tests/src/XeroClientTest.php +++ b/tests/src/XeroClientTest.php @@ -32,13 +32,14 @@ public function testPublicApplication(): void * @param array $headers * @param string $body * - * @dataProvider providerGetTest + * @throws \GuzzleHttp\Exception\GuzzleException * - * @throws \Radcliffe\Xero\Exception\InvalidOptionsException|\GuzzleHttp\Exception\GuzzleException + * @dataProvider providerGetTest */ public function testGet(int $statusCode, array $headers, string $body): void { $options = $this->createConfiguration(); + $mock = new MockHandler( [ new Response($statusCode, $headers, $body) @@ -48,6 +49,7 @@ public function testGet(int $statusCode, array $headers, string $body): void $client = XeroClient::createFromConfig($options, [ 'auth_token' => self::createRandomString(), + 'tenant' => '46bda23d-0659-47d6-bcaf-d1419aca0e7f', ]); $response = $client->request('GET', 'BrandingThemes'); @@ -65,7 +67,6 @@ public function testGet(int $statusCode, array $headers, string $body): void * The expected number of connections. * * @dataProvider connectionsResponseProvider - * @throws \Radcliffe\Xero\Exception\InvalidOptionsException */ public function testGetConnections(int $statusCode, array $response, int $expectedCount): void { @@ -111,14 +112,26 @@ public function testWithoutAuthToken(): void */ public static function providerGetTest(): array { + $json = [ + 'Id' => '3cc4210d-bf86-4cf5-aa5c-4a7c308dbfe1"', + 'Status' => 'OK', + 'ProviderName' => 'mradcliffe/xeroclient', + 'DateTimeUTC' => '\/Date(1718810712561)', + 'BrandingThemes' => [ + [ + 'BrandingThemeID' => self::createGuid(), + 'Name' => 'Standard', + 'SortOrder' => 0, + 'CreatedDateUTC' => '2010-06-29T18:16:36.27', + ], + ] + ]; + return [ [ 200, - ['Content-Type' => 'text/xml'], - '' . - self::createGuid() . - 'Standard0' . - '2010-06-29T18:16:36.27', + ['Content-Type' => 'application/json'], + json_encode($json), ] ]; }