From e8b5e824d5e6308c6fecf8ccbce2e9a0189f9aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Baconnier?= Date: Wed, 4 May 2022 16:34:50 +0200 Subject: [PATCH 01/10] Refactor to macro --- composer.json | 11 +- src/Clients/ZohoHttpFactory.php | 100 -------- src/Clients/ZohoUrlFactory.php | 99 ++++---- src/Facades/ZohoHttp.php | 13 - src/LaravelZohoServiceProvider.php | 41 +++- src/Middleware/RefreshZohoAuthToken.php | 50 ++++ .../VerifyZohoCredentialsDoesntExists.php | 9 +- src/Repositories/AccessTokenRepository.php | 2 +- .../DefaultAccessTokenRepository.php | 2 +- src/ZohoModules.php | 8 +- src/ZohoPendingRequest.php | 35 --- src/ZohoResponse.php | 38 --- tests/Clients/ZohoHttpFactoryTest.php | 226 ------------------ tests/Clients/ZohoUrlFactoryTest.php | 32 +-- tests/Zoho/ZohoHeaderAuthTest.php | 43 ++++ tests/Zoho/ZohoHttpTest.php | 84 +++++++ tests/{ => Zoho}/ZohoResponseTest.php | 24 +- 17 files changed, 306 insertions(+), 511 deletions(-) delete mode 100644 src/Clients/ZohoHttpFactory.php delete mode 100644 src/Facades/ZohoHttp.php create mode 100644 src/Middleware/RefreshZohoAuthToken.php delete mode 100644 src/ZohoPendingRequest.php delete mode 100644 src/ZohoResponse.php delete mode 100644 tests/Clients/ZohoHttpFactoryTest.php create mode 100644 tests/Zoho/ZohoHeaderAuthTest.php create mode 100644 tests/Zoho/ZohoHttpTest.php rename tests/{ => Zoho}/ZohoResponseTest.php (80%) diff --git a/composer.json b/composer.json index 339ae5f..66cacb7 100644 --- a/composer.json +++ b/composer.json @@ -30,16 +30,17 @@ } }, "require": { - "php": "^8.0", - "illuminate/contracts": "^8.37|^9.0", - "illuminate/support": "^8.49|^9.0", + "php": "^8.1", + "illuminate/contracts": "^9.0", + "illuminate/support": "^9.0", "league/oauth2-client": "^2.6" }, "require-dev": { "brianium/paratest": "^6.2", - "orchestra/testbench": "^6.22|^7.0", + "orchestra/testbench": "^7.0", "phpunit/phpunit": "^9.3", - "nunomaduro/collision": "^6.1" + "nunomaduro/collision": "^6.1", + "spatie/invade": "^1.0" }, "scripts": { "test": "./vendor/bin/testbench package:test --parallel --no-coverage", diff --git a/src/Clients/ZohoHttpFactory.php b/src/Clients/ZohoHttpFactory.php deleted file mode 100644 index 7c36a28..0000000 --- a/src/Clients/ZohoHttpFactory.php +++ /dev/null @@ -1,100 +0,0 @@ -macroCall($method, $parameters); - } - - if ($this->isFaking === false) { - $this->refreshAccessToken(); - } - - $response = tap($this->newPendingRequest(), function (PendingRequest $request) - { - $request - ->withHeaders($this->headers()) - ->stub($this->stubCallbacks); - })->{$method}(...$parameters); - - return $this->transformResponse($response); - } - - private function transformResponse( - Response|PendingRequest|array $response - ): ZohoResponse|ZohoPendingRequest|array { - if ($response instanceof Response) { - return ZohoResponse::fromResponse($response); - } - if ($response instanceof PendingRequest) { - return ZohoPendingRequest::fromPendingRequest($response); - } - - if (is_array($response)) { - return array_map(fn($res) => $this->transformResponse($res), $response); - } - } - - public function fake($callback = null) - { - $this->isFaking = true; - return parent::fake($callback); - } - - public function headers(): array - { - $headers = []; - if ($token = app(AccessTokenRepository::class)->get()) { - $headers['Authorization'] = "Zoho-oauthtoken {$token->getToken()}"; - } - return $headers; - } - - public function hasAccessToken(): bool - { - return app(AccessTokenRepository::class)->exists(); - } - - private function refreshAccessToken(): void - { - $provider = app(ZohoAuthProvider::class); - $accessTokenRepository = app(AccessTokenRepository::class); - - $accessToken = $accessTokenRepository->get(); - - if (! $accessToken) { - return; - } - - $refreshToken = $accessToken->getRefreshToken(); - - if ($accessToken->hasExpired()) { - $accessToken = $provider->getAccessToken('refresh_token', [ - 'refresh_token' => $refreshToken, - ]); - - // Zoho doesn't return the refreshToken in the response. We have to re-set it afterward - // https://help.zoho.com/portal/community/topic/refresh-token-missing - $accessToken->setRefreshToken($refreshToken); - - $accessTokenRepository->store($accessToken); - } - } -} \ No newline at end of file diff --git a/src/Clients/ZohoUrlFactory.php b/src/Clients/ZohoUrlFactory.php index d876a6a..cceb95d 100644 --- a/src/Clients/ZohoUrlFactory.php +++ b/src/Clients/ZohoUrlFactory.php @@ -16,21 +16,16 @@ public function __construct( $this->config = $configRepository; } - public function api(string $module, string $url, array $parameters = []) - { - return $this->make($module, $url, $parameters); - } - - public function make(string $module, string $url, array $parameters = []): string + public function api(ZohoModules $module, string $url, array $parameters = []) { if (Str::startsWith($url, '/')) { $url = Str::replaceFirst('/', '', $url); } - if ($module === ZohoModules::Books) { + if ($module === ZohoModules::BOOKS) { $url = $this->books($url); } else { - $url = $this->default($module, $url); + $url = Str::finish($this->baseApiUrl($module), '/') . $url; } foreach ($parameters as $parameter => $value) { @@ -40,7 +35,7 @@ public function make(string $module, string $url, array $parameters = []): strin return $url; } - public function web(string $module, string $url, array $parameters = []) + public function web(ZohoModules $module, string $url, array $parameters = []) { if (Str::startsWith($url, '/')) { $url = Str::replaceFirst('/', '', $url); @@ -55,15 +50,43 @@ public function web(string $module, string $url, array $parameters = []) return $url; } - - protected function default(string $module, string $url) + /** + * @internal + * @param string $type + * @return string + */ + public function oauthApiUrl(string $type): string { - return Str::finish($this->baseApiUrl($module), '/') . $url; + $region = $this->config->region() ?? 'US'; + + return [ + 'authorization_url' => [ + 'EU' => 'https://accounts.zoho.eu/oauth/v2/auth', + 'US' => 'https://accounts.zoho.com/oauth/v2/auth', + 'IN' => 'https://accounts.zoho.in/oauth/v2/auth', + 'AU' => 'https://accounts.zoho.com.au/oauth/v2/auth', + 'CN' => 'https://accounts.zoho.com.cn/oauth/v2/auth', + ], + 'access_token_url' => [ + 'EU' => 'https://accounts.zoho.eu/oauth/v2/token', + 'US' => 'https://accounts.zoho.com/oauth/v2/token', + 'IN' => 'https://accounts.zoho.in/oauth/v2/token', + 'AU' => 'https://accounts.zoho.com.au/oauth/v2/token', + 'CN' => 'https://accounts.zoho.com.cn/oauth/v2/token', + ], + 'revoke_access_token_url' => [ + 'EU' => 'https://accounts.zoho.eu/oauth/v2/token/revoke', + 'US' => 'https://accounts.zoho.com/oauth/v2/token/revoke', + 'IN' => 'https://accounts.zoho.in/oauth/v2/token/revoke', + 'AU' => 'https://accounts.zoho.com.au/oauth/v2/token/revoke', + 'CN' => 'https://accounts.zoho.com.cn/oauth/v2/token/revoke', + ], + ][$type][$region]; } protected function books(string $url) { - $url = Str::finish($this->baseApiUrl(ZohoModules::Books), '/') . $url; + $url = Str::finish($this->baseApiUrl(ZohoModules::BOOKS), '/') . $url; return $this->addParameterToUrlQuery($url, 'organization_id', $this->config->currentOrganizationId()); } @@ -83,92 +106,62 @@ protected function addParameterToUrlQuery(string $url, string $parameter, $value return $urlParts['scheme'] . '://' . $urlParts['host'] . $urlParts['path'] . '?' . $urlParts['query']; } - public function baseApiUrl(string $module): string + protected function baseApiUrl(ZohoModules $module): string { $region = $this->config->region() ?? 'US'; return [ - ZohoModules::Books => [ + ZohoModules::BOOKS->value => [ 'EU' => 'https://books.zoho.eu/api/v3', 'US' => 'https://books.zoho.com/api/v3', 'IN' => 'https://books.zoho.in/api/v3', 'AU' => 'https://books.zoho.com.au/api/v3', 'CN' => 'https://books.zoho.com.cn/api/v3', ], - ZohoModules::Crm => [ + ZohoModules::CRM->value => [ 'EU' => 'https://www.zohoapis.eu/crm/v2', 'US' => 'https://www.zohoapis.com/crm/v2', 'IN' => 'https://www.zohoapis.in/crm/v2', 'AU' => 'https://www.zohoapis.com.au/crm/v2', 'CN' => 'https://www.zohoapis.com.cn/crm/v2', ], - ZohoModules::Recruit => [ + ZohoModules::RECRUIT->value => [ 'EU' => 'https://recruit.zoho.eu/recruit/v2', 'US' => 'https://recruit.zoho.com/recruit/v2', 'IN' => 'https://recruit.zoho.in/recruit/v2', 'AU' => 'https://recruit.zoho.com.au/recruit/v2', 'CN' => 'https://recruit.zoho.com.cn/recruit/v2', ], - ][$module][$region]; + ][$module->value][$region]; } - public function oauthApiUrl(string $type): string - { - $region = $this->config->region() ?? 'US'; - - return [ - 'authorization_url' => [ - 'EU' => 'https://accounts.zoho.eu/oauth/v2/auth', - 'US' => 'https://accounts.zoho.com/oauth/v2/auth', - 'IN' => 'https://accounts.zoho.in/oauth/v2/auth', - 'AU' => 'https://accounts.zoho.com.au/oauth/v2/auth', - 'CN' => 'https://accounts.zoho.com.cn/oauth/v2/auth', - ], - 'access_token_url' => [ - 'EU' => 'https://accounts.zoho.eu/oauth/v2/token', - 'US' => 'https://accounts.zoho.com/oauth/v2/token', - 'IN' => 'https://accounts.zoho.in/oauth/v2/token', - 'AU' => 'https://accounts.zoho.com.au/oauth/v2/token', - 'CN' => 'https://accounts.zoho.com.cn/oauth/v2/token', - ], - 'revoke_access_token_url' => [ - 'EU' => 'https://accounts.zoho.eu/oauth/v2/token/revoke', - 'US' => 'https://accounts.zoho.com/oauth/v2/token/revoke', - 'IN' => 'https://accounts.zoho.in/oauth/v2/token/revoke', - 'AU' => 'https://accounts.zoho.com.au/oauth/v2/token/revoke', - 'CN' => 'https://accounts.zoho.com.cn/oauth/v2/token/revoke', - ], - ][$type][$region]; - } - - public function baseWebUrl(string $module): string + protected function baseWebUrl(ZohoModules $module): string { $region = $this->config->region() ?? 'US'; $organization = $this->config->currentOrganizationId(); return [ - ZohoModules::Books => [ + ZohoModules::BOOKS->value => [ 'EU' => 'https://books.zoho.eu/app#', 'US' => 'https://books.zoho.com/app#', 'IN' => 'https://books.zoho.in/app#', 'AU' => 'https://books.zoho.com.au/app#', 'CN' => 'https://books.zoho.com.cn/app#', ], - ZohoModules::Crm => [ + ZohoModules::CRM->value => [ 'EU' => "https://crm.zoho.eu/crm/{$organization}", 'US' => "https://crm.zoho.com/crm/{$organization}", 'IN' => "https://crm.zoho.in/crm/{$organization}", 'AU' => "https://crm.zoho.com.eu/crm/{$organization}", 'CN' => "https://crm.zoho.com.cn/crm/{$organization}", ], - ZohoModules::Recruit => [ + ZohoModules::RECRUIT->value => [ 'EU' => "https://recruit.zoho.eu/recruit/{$organization}", 'US' => "https://recruit.zoho.com/recruit/{$organization}", 'IN' => "https://recruit.zoho.in/recruit/{$organization}", 'AU' => "https://recruit.zoho.com.eu/recruit/{$organization}", 'CN' => "https://recruit.zoho.com.cn/recruit/{$organization}", ], - ][$module][$region]; + ][$module->value][$region]; } - } \ No newline at end of file diff --git a/src/Facades/ZohoHttp.php b/src/Facades/ZohoHttp.php deleted file mode 100644 index 313487e..0000000 --- a/src/Facades/ZohoHttp.php +++ /dev/null @@ -1,13 +0,0 @@ - base_path('config/zoho.php'), ], 'laravel-zoho-config'); - if (! class_exists('CreateOauthTokensTable')) { + if (!class_exists('CreateOauthTokensTable')) { $this->publishes([ - __DIR__.'/../database/migrations/create_oauth_tokens_table.php.stub' => database_path('migrations/'.date('Y_m_d_His', time()).'_create_oauth_tokens_table.php'), + __DIR__ . '/../database/migrations/create_oauth_tokens_table.php.stub' => database_path('migrations/' . date('Y_m_d_His', time()) . '_create_oauth_tokens_table.php'), ], 'laravel-zoho-migrations'); } + Http::macro('withZohoHeader', function () { + $accessTokenRepository = app(AccessTokenRepository::class); + $headers = []; + if ($token = $accessTokenRepository->get()) { + $headers['Authorization'] = "Zoho-oauthtoken {$token->getToken()}"; + } + return Http::withHeaders($headers); + }); + + Response::macro('hasErrorsFromZoho', function () { + return $this->status() >= 400 || count($this->errorsFromZoho()); + }); + + Response::macro('errorsFromZoho', function () { + if ($this->status() >= 400) { + return $this->json() ?? []; + } + + return collect($this->json()) + ->flatten(1) + ->filter(function ($value) { + if (is_array($value) && array_key_exists('status', $value)) { + return $value['status'] === 'error'; + } + return null; + }) + ->values() + ->toArray(); + }); + Route::get(config('zoho.url', '/oauth2/zoho'), [ZohoAuthController::class, 'requestToken']); } @@ -42,8 +72,7 @@ public function register() config('zoho.access_token_repository', DefaultAccessTokenRepository::class) ); - $this->app->bind(ZohoAuthProvider::class, function () - { + $this->app->bind(ZohoAuthProvider::class, function () { $config = app(ConfigRepository::class); return new ZohoAuthProvider([ 'clientSecret' => $config->secret(), diff --git a/src/Middleware/RefreshZohoAuthToken.php b/src/Middleware/RefreshZohoAuthToken.php new file mode 100644 index 0000000..cfbc2dd --- /dev/null +++ b/src/Middleware/RefreshZohoAuthToken.php @@ -0,0 +1,50 @@ +accessTokenRepository->get(); + + if (!$accessToken) { + return $next($request); + } + + $refreshToken = $accessToken->getRefreshToken(); + + if ($accessToken->hasExpired()) { + $accessToken = $this->provider->getAccessToken('refresh_token', [ + 'refresh_token' => $refreshToken, + ]); + + // Zoho doesn't return the refreshToken in the response. We have to re-set it afterward + // https://help.zoho.com/portal/community/topic/refresh-token-missing + $accessToken->setRefreshToken($refreshToken); + + $this->accessTokenRepository->store($accessToken); + } + + return $next($request); + } +} \ No newline at end of file diff --git a/src/Middleware/VerifyZohoCredentialsDoesntExists.php b/src/Middleware/VerifyZohoCredentialsDoesntExists.php index 658ac98..763c1d4 100644 --- a/src/Middleware/VerifyZohoCredentialsDoesntExists.php +++ b/src/Middleware/VerifyZohoCredentialsDoesntExists.php @@ -3,10 +3,15 @@ namespace MelbaCh\LaravelZoho\Middleware; use Closure; -use MelbaCh\LaravelZoho\Facades\ZohoHttp; +use MelbaCh\LaravelZoho\Repositories\AccessTokenRepository; class VerifyZohoCredentialsDoesntExists { + public function __construct( + private readonly AccessTokenRepository $accessTokenRepository + ) { + } + /** * Handle an incoming request. * @@ -18,7 +23,7 @@ class VerifyZohoCredentialsDoesntExists */ public function handle($request, Closure $next) { - if (ZohoHttp::hasAccessToken()) { + if ($this->accessTokenRepository->exists()) { abort(403); } return $next($request); diff --git a/src/Repositories/AccessTokenRepository.php b/src/Repositories/AccessTokenRepository.php index 7e4150a..34368a1 100644 --- a/src/Repositories/AccessTokenRepository.php +++ b/src/Repositories/AccessTokenRepository.php @@ -8,7 +8,7 @@ interface AccessTokenRepository { public function store(AccessTokenInterface $accessToken): self; - public function get(): ?AccessTokenInterface; + public function get(): AccessTokenInterface|null; public function delete(): void; diff --git a/src/Repositories/DefaultAccessTokenRepository.php b/src/Repositories/DefaultAccessTokenRepository.php index 7771955..5cd6679 100644 --- a/src/Repositories/DefaultAccessTokenRepository.php +++ b/src/Repositories/DefaultAccessTokenRepository.php @@ -19,7 +19,7 @@ public function store(AccessTokenInterface $accessToken): AccessTokenRepository return $this; } - public function get(): ?AccessTokenInterface + public function get(): AccessTokenInterface|null { try { $hash = Storage::disk(config('zoho.access_token_disk', null)) diff --git a/src/ZohoModules.php b/src/ZohoModules.php index ee27e03..1d68d16 100644 --- a/src/ZohoModules.php +++ b/src/ZohoModules.php @@ -2,9 +2,9 @@ namespace MelbaCh\LaravelZoho; -class ZohoModules +enum ZohoModules: string { - public const Crm = 'crm'; - public const Books = 'books'; - public const Recruit = 'recruit'; + case CRM = 'crm'; + case BOOKS = 'books'; + case RECRUIT = 'recruit'; } \ No newline at end of file diff --git a/src/ZohoPendingRequest.php b/src/ZohoPendingRequest.php deleted file mode 100644 index 35bbd2e..0000000 --- a/src/ZohoPendingRequest.php +++ /dev/null @@ -1,35 +0,0 @@ -loadFromPendingRequest($pendingRequest); - return $instance; - } - - public function loadFromPendingRequest(PendingRequest $pendingRequest) - { - $attributes = get_object_vars($pendingRequest); - foreach ($attributes as $attribute => $value) { - $this->$attribute = $value; - } - } - - public function send(string $method, string $url, array $options = []) - { - $response = parent::send($method, $url, $options); - if ($response instanceof Response) { - return ZohoResponse::fromResponse($response); - } - return $response; - } - -} \ No newline at end of file diff --git a/src/ZohoResponse.php b/src/ZohoResponse.php deleted file mode 100644 index b70a7e9..0000000 --- a/src/ZohoResponse.php +++ /dev/null @@ -1,38 +0,0 @@ -response); - } - - public function hasErrors(): bool - { - return $this->status() >= 400 || count($this->errors()); - } - - public function errors(): array - { - if ($this->status() >= 400) { - return $this->json() ?? []; - } - - return collect($this->json()) - ->flatten(1) - ->filter(function ($value) - { - if (is_array($value) && array_key_exists('status', $value)) { - return $value['status'] === 'error'; - } - return null; - }) - ->values() - ->toArray(); - } - -} \ No newline at end of file diff --git a/tests/Clients/ZohoHttpFactoryTest.php b/tests/Clients/ZohoHttpFactoryTest.php deleted file mode 100644 index be23701..0000000 --- a/tests/Clients/ZohoHttpFactoryTest.php +++ /dev/null @@ -1,226 +0,0 @@ - 'mock_access_token', 'expires' => now()->addHour()->timestamp]); - - $this->mock(AccessTokenRepository::class, static function (MockInterface $repository) use ($accessToken) - { - $repository->shouldReceive('exists')->andReturn(true); - $repository->shouldReceive('get')->andReturn($accessToken); - $repository->shouldReceive('store')->with($accessToken); - }); - - $this->mock(ZohoAuthProvider::class, static function (MockInterface $repository) use ($accessToken) - { - $repository->shouldReceive('getAccessToken')->andReturn($accessToken); - }); - - $this->zohoClientHttp = new ZohoHttpFactory; - } - - protected function tearDown(): void - { - \Mockery::close(); - } - - /** @test */ - public function it_can_make_a_request(): void - { - $fakeResponse = [ - "users" => [ - [ - "country" => "US", - "street" => null, - "id" => "4150868000000225013", - "first_name" => "Patricia", - "last_name" => "Boyle", - ], - ], - "info" => [ - "per_page" => 200, - "count" => 3, - "page" => 1, - "more_records" => false, - ], - ]; - - $this->zohoClientHttp->fake([ - 'users' => Http::response($fakeResponse), - '*' => Http::response(['error' => 'error']), - ]); - - $this->assertEquals( - $fakeResponse, - $this->zohoClientHttp - ->get('users')->json() - - ); - } - - - /** @test */ - public function it_returns_a_zoho_response_class(): void - { - $this->zohoClientHttp->fake(); - - $response = $this->zohoClientHttp->get('/'); - - /** @var Request $request */ - $request = $this->zohoClientHttp->recorded()[0][0]; - $this->assertTrue($request->hasHeader('Authorization')); - $this->assertEquals('Zoho-oauthtoken mock_access_token', $request->headers()['Authorization'][0]); - $this->assertInstanceOf(ZohoResponse::class, $response); - } - - /** @test */ - public function it_returns_an_array_of_zoho_responses_class_when_using_pool(): void - { - $this->zohoClientHttp->fake(); - - $responses = $this->zohoClientHttp->pool(fn(Pool $pool) => [ - $pool->get('/1'), - $pool->get('/2'), - ]); - - /** @var Request $request */ - $request = $this->zohoClientHttp->recorded()[0][0]; - $this->assertTrue($request->hasHeader('Authorization')); - $this->assertEquals('Zoho-oauthtoken mock_access_token', $request->headers()['Authorization'][0]); - - $this->assertInstanceOf(ZohoResponse::class, $responses[0]); - } - - /** @test */ - public function it_returns_a_zoho_pending_request(): void - { - $this->zohoClientHttp->fake(); - - $pendingRequest = $this->zohoClientHttp->asForm(); - - $reflection = new \ReflectionClass($pendingRequest); - $reflectionProperty = $reflection->getProperty('options'); - $reflectionProperty->setAccessible(true); - - $options = $reflectionProperty->getValue($pendingRequest); - - $this->assertInstanceOf(ZohoPendingRequest::class, $pendingRequest); - $this->assertEquals('Zoho-oauthtoken mock_access_token', $options['headers']['Authorization']); - } - - /** @test */ - public function it_uses_the_zoho_bearer_header_when_performing_a_request(): void - { - $this->zohoClientHttp->fake(); - - $this->assertInstanceOf(ZohoResponse::class, $this->zohoClientHttp->get('url')); - } - - /** @test */ - public function it_prevent_refresh_token_call_when_using_fake(): void - { - $accessToken = $this->mock(ZohoAccessToken::class, static function (MockInterface $mock): void - { - $mock->shouldReceive('hasExpired')->never(); - $mock->shouldReceive('getRefreshToken')->never(); - $mock->shouldReceive('getToken')->once(); - }); - - $this->mock(AccessTokenRepository::class, static function (MockInterface $repository) use ($accessToken) - { - $repository->shouldReceive('exists')->andReturn(true); - $repository->shouldReceive('get')->andReturn($accessToken); - $repository->shouldReceive('store'); - }); - - $this->zohoClientHttp->fake(); - - $this->zohoClientHttp->get('/'); - - $this->assertInstanceOf(MockInterface::class, $accessToken); - } - - /** @test */ - public function it_call_refresh_token_when_performing_a_request(): void - { - $accessToken = $this->mock(ZohoAccessToken::class, static function (MockInterface $mock): void - { - $mock->shouldReceive('hasExpired')->once()->andReturn(true); - $mock->shouldReceive('getRefreshToken')->once()->andReturnSelf(); - $mock->shouldReceive('getToken')->once(); - }); - - $this->mock(AccessTokenRepository::class, static function (MockInterface $repository) use ($accessToken) - { - $repository->shouldReceive('exists')->andReturn(true); - $repository->shouldReceive('get')->andReturn($accessToken); - $repository->shouldReceive('store'); - }); - - $this->zohoClientHttp->fake(); - - $reflection = new \ReflectionClass($this->zohoClientHttp); - $reflectionProperty = $reflection->getProperty('isFaking'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($this->zohoClientHttp, false); - - $this->zohoClientHttp->get('/'); - - $this->assertInstanceOf(MockInterface::class, $accessToken); - } - - /** @test */ - public function it_call_only_once_refresh_token_when_performing_multiple_operations(): void - { - $accessToken = $this->mock(ZohoAccessToken::class, static function (MockInterface $mock): void - { - $mock->shouldReceive('hasExpired')->once()->andReturn(true); - $mock->shouldReceive('getRefreshToken')->once()->andReturnSelf(); - $mock->shouldReceive('getToken')->once(); - }); - - $this->mock(AccessTokenRepository::class, static function (MockInterface $repository) use ($accessToken) - { - $repository->shouldReceive('exists')->andReturn(true); - $repository->shouldReceive('get')->andReturn($accessToken); - $repository->shouldReceive('store'); - }); - - $this->zohoClientHttp->fake(); - - $reflection = new \ReflectionClass($this->zohoClientHttp); - $reflectionProperty = $reflection->getProperty('isFaking'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($this->zohoClientHttp, false); - - $this->zohoClientHttp - ->asForm() - ->attach('file') - ->get('/'); - - $this->assertInstanceOf(MockInterface::class, $accessToken); - } - -} \ No newline at end of file diff --git a/tests/Clients/ZohoUrlFactoryTest.php b/tests/Clients/ZohoUrlFactoryTest.php index 9984818..c8d59e7 100644 --- a/tests/Clients/ZohoUrlFactoryTest.php +++ b/tests/Clients/ZohoUrlFactoryTest.php @@ -29,17 +29,17 @@ public function it_build_url_for_a_module(): void $this->assertEquals( 'https://www.zohoapis.eu/crm/v2/users/4', - $urlFactory->make(ZohoModules::Crm, '/users/4') + $urlFactory->api(ZohoModules::CRM, '/users/4') ); $this->assertEquals( 'https://books.zoho.eu/api/v3/invoices?organization_id=1234', - $urlFactory->make(ZohoModules::Books, '/invoices') + $urlFactory->api(ZohoModules::BOOKS, '/invoices') ); $this->assertEquals( 'https://recruit.zoho.eu/recruit/v2/users', - $urlFactory->make(ZohoModules::Recruit, '/users') + $urlFactory->api(ZohoModules::RECRUIT, '/users') ); } @@ -50,17 +50,17 @@ public function it_returns_the_base_urls_for_the_api(): void $this->assertEquals( 'https://www.zohoapis.eu/crm/v2', - $urlFactory->baseApiUrl(ZohoModules::Crm) + invade($urlFactory)->baseApiUrl(ZohoModules::CRM) ); $this->assertEquals( 'https://books.zoho.eu/api/v3', - $urlFactory->baseApiUrl(ZohoModules::Books) + invade($urlFactory)->baseApiUrl(ZohoModules::BOOKS) ); $this->assertEquals( 'https://recruit.zoho.eu/recruit/v2', - $urlFactory->baseApiUrl(ZohoModules::Recruit) + invade($urlFactory)->baseApiUrl(ZohoModules::RECRUIT) ); } @@ -92,12 +92,12 @@ public function it_add_the_current_organization_id_to_url_when_using_books(): vo $this->assertEquals( 'https://books.zoho.eu/api/v3/invoices?organization_id=1234', - $urlFactory->make(ZohoModules::Books, '/invoices') + $urlFactory->api(ZohoModules::BOOKS, '/invoices') ); $this->assertEquals( 'https://books.zoho.eu/api/v3/invoices?param_1=param&organization_id=1234', - $urlFactory->make(ZohoModules::Books, '/invoices?param_1=param') + $urlFactory->api(ZohoModules::BOOKS, '/invoices?param_1=param') ); } @@ -111,7 +111,7 @@ public function it_uses_the_region(): void }); $this->assertEquals( 'https://books.zoho.eu/api/v3', - app(ZohoUrlFactory::class)->baseApiUrl(ZohoModules::Books) + invade(app(ZohoUrlFactory::class))->baseApiUrl(ZohoModules::BOOKS) ); $this->mock(DefaultConfigRepository::class, static function (MockInterface $repository) @@ -120,7 +120,7 @@ public function it_uses_the_region(): void }); $this->assertEquals( 'https://books.zoho.com/api/v3', - app(ZohoUrlFactory::class)->baseApiUrl(ZohoModules::Books) + invade(app(ZohoUrlFactory::class))->baseApiUrl(ZohoModules::BOOKS) ); } @@ -131,17 +131,17 @@ public function it_build_using_api_method(): void $this->assertEquals( 'https://www.zohoapis.eu/crm/v2/users/4', - $urlFactory->api(ZohoModules::Crm, '/users/4') + $urlFactory->api(ZohoModules::CRM, '/users/4') ); $this->assertEquals( 'https://books.zoho.eu/api/v3/invoices?organization_id=1234', - $urlFactory->api(ZohoModules::Books, '/invoices') + $urlFactory->api(ZohoModules::BOOKS, '/invoices') ); $this->assertEquals( 'https://recruit.zoho.eu/recruit/v2/users', - $urlFactory->api(ZohoModules::Recruit, '/users') + $urlFactory->api(ZohoModules::RECRUIT, '/users') ); } @@ -153,17 +153,17 @@ public function it_build_using_web_method(): void $this->assertEquals( 'https://crm.zoho.eu/crm/1234/tab/Potentials/292528000000000000', - $urlFactory->web(ZohoModules::Crm, '/tab/Potentials/292528000000000000') + $urlFactory->web(ZohoModules::CRM, '/tab/Potentials/292528000000000000') ); $this->assertEquals( 'https://books.zoho.eu/app#/contacts/139996000000000000', - $urlFactory->web(ZohoModules::Books, '/contacts/139996000000000000') + $urlFactory->web(ZohoModules::BOOKS, '/contacts/139996000000000000') ); $this->assertEquals( 'https://recruit.zoho.eu/recruit/1234/EntityInfo.do?module=Candidates&id=31529000000000000&submodule=Candidates', - $urlFactory->web(ZohoModules::Recruit, '/EntityInfo.do?module=Candidates&id=31529000000000000&submodule=Candidates') + $urlFactory->web(ZohoModules::RECRUIT, '/EntityInfo.do?module=Candidates&id=31529000000000000&submodule=Candidates') ); } } \ No newline at end of file diff --git a/tests/Zoho/ZohoHeaderAuthTest.php b/tests/Zoho/ZohoHeaderAuthTest.php new file mode 100644 index 0000000..f54e717 --- /dev/null +++ b/tests/Zoho/ZohoHeaderAuthTest.php @@ -0,0 +1,43 @@ + 'mock_access_token', 'expires' => now()->addHour()->timestamp]); + $this->mock(AccessTokenRepository::class, static function (MockInterface $repository) use ($accessToken) { + $repository->shouldReceive('get')->andReturn($accessToken); + }); + + /** @var PendingRequest $http */ + $http = Http::withZohoHeader(); + $this->assertEquals(['Authorization' => 'Zoho-oauthtoken mock_access_token'], $http->getOptions()['headers']); + } + + /** @test */ + public function it_doesnt_add_header_when_token_is_not_found(): void + { + $this->mock(AccessTokenRepository::class, static function (MockInterface $repository) { + $repository->shouldReceive('get')->andReturn(null); + }); + + Http::fake(); + + /** @var PendingRequest $http */ + $http = Http::withZohoHeader(); + $this->assertEquals([], $http->getOptions()['headers']); + } + + +} \ No newline at end of file diff --git a/tests/Zoho/ZohoHttpTest.php b/tests/Zoho/ZohoHttpTest.php new file mode 100644 index 0000000..7510f91 --- /dev/null +++ b/tests/Zoho/ZohoHttpTest.php @@ -0,0 +1,84 @@ + 'mock_access_token', 'expires' => now()->addHour()->timestamp]); + + $this->mock(AccessTokenRepository::class, static function (MockInterface $repository) use ($accessToken) { + $repository->shouldReceive('get')->andReturn($accessToken); + }); + + $this->mock(ZohoAuthProvider::class, static function (MockInterface $repository) use ($accessToken) { + $repository->shouldReceive('getAccessToken')->andReturn($accessToken); + }); + } + + protected function tearDown(): void + { + \Mockery::close(); + } + + protected array $fakeResponse = [ + "users" => [ + [ + "country" => "US", + "street" => null, + "id" => "4150868000000225013", + "first_name" => "Patricia", + "last_name" => "Boyle", + ], + ], + "info" => [ + "per_page" => 200, + "count" => 3, + "page" => 1, + "more_records" => false, + ], + ]; + + /** @test */ + public function it_can_make_a_request_using_header(): void + { + Http::fake([ + 'users' => Http::response($this->fakeResponse), + ]); + + $this->assertEquals( + $this->fakeResponse, + Http::withZohoHeader()->get('users')->json() + ); + } + + /** @test */ + public function it_can_make_a_request_zoho_url_facade(): void + { + Http::fake([ + 'https://www.zohoapis.com/crm/v2/users' => Http::response($this->fakeResponse), + ]); + + $this->assertEquals( + $this->fakeResponse, + Http::withZohoHeader() + ->get(ZohoUrl::api(ZohoModules::CRM, 'users')) + ->json() + ); + } + + +} \ No newline at end of file diff --git a/tests/ZohoResponseTest.php b/tests/Zoho/ZohoResponseTest.php similarity index 80% rename from tests/ZohoResponseTest.php rename to tests/Zoho/ZohoResponseTest.php index 89f3d7e..4077ac5 100644 --- a/tests/ZohoResponseTest.php +++ b/tests/Zoho/ZohoResponseTest.php @@ -1,18 +1,19 @@ assertTrue($response->hasErrors()); + $response = new Response(new Psr7Response(400, [], null)); + $this->assertTrue($response->hasErrorsFromZoho()); } /** @test */ @@ -44,13 +45,13 @@ public function it_see_errors_in_response_body(): void ], ]; - $response = new ZohoResponse(new Psr7Response(200, [], json_encode($body))); + $response = new Response(new Psr7Response(200, [], json_encode($body))); - $this->assertTrue($response->hasErrors()); + $this->assertTrue($response->hasErrorsFromZoho()); } /** @test */ - public function it_returns_the_errors(): void + public function it_returns_the_errorsFromZoho(): void { $body = [ "users" => [ @@ -78,9 +79,9 @@ public function it_returns_the_errors(): void ], ]; - $response = new ZohoResponse(new Psr7Response(200, [], json_encode($body))); + $response = new Response(new Psr7Response(200, [], json_encode($body))); - $this->assertTrue($response->hasErrors()); + $this->assertTrue($response->hasErrorsFromZoho()); $this->assertEquals( [ [ @@ -98,8 +99,9 @@ public function it_returns_the_errors(): void "status" => "error", ], ], - $response->errors(), + $response->errorsFromZoho(), ); } + } \ No newline at end of file From 72c7351dbd1d34a21d610de3a64fc450ac18a8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Baconnier?= Date: Wed, 4 May 2022 17:30:24 +0200 Subject: [PATCH 02/10] Add tests, rename repository and allow null to be returned from config --- config/zoho.php | 4 +- src/LaravelZohoServiceProvider.php | 8 +- src/Repositories/ConfigRepository.php | 6 +- src/Repositories/DatabaseConfigRepository.php | 8 +- ...y.php => StorageAccessTokenRepository.php} | 2 +- ...sitory.php => StorageConfigRepository.php} | 8 +- tests/Clients/ZohoUrlFactoryTest.php | 8 +- tests/Controllers/ZohoAuthControllerTest.php | 4 +- .../RefreshZohoAuthTokenMiddlewareTest.php | 116 ++++++++++++++++++ .../VerifyZohoCredentialsDoesntExistsTest.php | 59 +++++++++ ...p => StorageAccessTokenRepositoryTest.php} | 12 +- ...st.php => StorageConfigRepositoryTest.php} | 16 +-- 12 files changed, 213 insertions(+), 38 deletions(-) rename src/Repositories/{DefaultAccessTokenRepository.php => StorageAccessTokenRepository.php} (95%) rename src/Repositories/{DefaultConfigRepository.php => StorageConfigRepository.php} (88%) create mode 100644 tests/Middleware/RefreshZohoAuthTokenMiddlewareTest.php create mode 100644 tests/Middleware/VerifyZohoCredentialsDoesntExistsTest.php rename tests/Repositories/{DefaultAccessTokenRepositoryTest.php => StorageAccessTokenRepositoryTest.php} (87%) rename tests/Repositories/{DefaultConfigRepositoryTest.php => StorageConfigRepositoryTest.php} (79%) diff --git a/config/zoho.php b/config/zoho.php index 027234a..892f772 100644 --- a/config/zoho.php +++ b/config/zoho.php @@ -24,7 +24,7 @@ \MelbaCh\LaravelZoho\Middleware\VerifyZohoCredentialsDoesntExists::class, ], - 'config_repository' => \MelbaCh\LaravelZoho\Repositories\DefaultConfigRepository::class, + 'config_repository' => \MelbaCh\LaravelZoho\Repositories\StorageConfigRepository::class, /** * Specific to the Default config Repository */ @@ -36,7 +36,7 @@ 'ZohoBooks.settings.READ', ], - 'access_token_repository' => \MelbaCh\LaravelZoho\Repositories\DefaultAccessTokenRepository::class, + 'access_token_repository' => \MelbaCh\LaravelZoho\Repositories\StorageAccessTokenRepository::class, /** * Specific to the Default Access Token Repository */ diff --git a/src/LaravelZohoServiceProvider.php b/src/LaravelZohoServiceProvider.php index 4bdd8d7..7bbeeff 100644 --- a/src/LaravelZohoServiceProvider.php +++ b/src/LaravelZohoServiceProvider.php @@ -10,8 +10,8 @@ use MelbaCh\LaravelZoho\Controllers\ZohoAuthController; use MelbaCh\LaravelZoho\Repositories\AccessTokenRepository; use MelbaCh\LaravelZoho\Repositories\ConfigRepository; -use MelbaCh\LaravelZoho\Repositories\DefaultAccessTokenRepository; -use MelbaCh\LaravelZoho\Repositories\DefaultConfigRepository; +use MelbaCh\LaravelZoho\Repositories\StorageAccessTokenRepository; +use MelbaCh\LaravelZoho\Repositories\StorageConfigRepository; class LaravelZohoServiceProvider extends ServiceProvider { @@ -64,12 +64,12 @@ public function register() { $this->app->bind( ConfigRepository::class, - config('zoho.config_repository', DefaultConfigRepository::class) + config('zoho.config_repository', StorageConfigRepository::class) ); $this->app->bind( AccessTokenRepository::class, - config('zoho.access_token_repository', DefaultAccessTokenRepository::class) + config('zoho.access_token_repository', StorageAccessTokenRepository::class) ); $this->app->bind(ZohoAuthProvider::class, function () { diff --git a/src/Repositories/ConfigRepository.php b/src/Repositories/ConfigRepository.php index e8a36fb..9f5a603 100644 --- a/src/Repositories/ConfigRepository.php +++ b/src/Repositories/ConfigRepository.php @@ -16,9 +16,9 @@ public function scopes(): array; public function setScopes(array $scopes): self; - public function secret(): string; + public function secret(): string|null; - public function clientId(): string; + public function clientId(): string|null; - public function currentOrganizationId(): ?string; + public function currentOrganizationId(): string|null; } \ No newline at end of file diff --git a/src/Repositories/DatabaseConfigRepository.php b/src/Repositories/DatabaseConfigRepository.php index dc4a85e..394a8cf 100644 --- a/src/Repositories/DatabaseConfigRepository.php +++ b/src/Repositories/DatabaseConfigRepository.php @@ -49,14 +49,14 @@ public function setScopes(array $scopes): self return $this; } - public function secret(): string + public function secret(): string|null { - return $this->get()['secrets']['secret'] ?? ''; + return $this->get()['secrets']['secret'] ?? null; } - public function clientId(): string + public function clientId(): string|null { - return $this->get()['secrets']['client_id'] ?? ''; + return $this->get()['secrets']['client_id'] ?? null; } public function currentOrganizationId(): string|null diff --git a/src/Repositories/DefaultAccessTokenRepository.php b/src/Repositories/StorageAccessTokenRepository.php similarity index 95% rename from src/Repositories/DefaultAccessTokenRepository.php rename to src/Repositories/StorageAccessTokenRepository.php index 5cd6679..888bc5d 100644 --- a/src/Repositories/DefaultAccessTokenRepository.php +++ b/src/Repositories/StorageAccessTokenRepository.php @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Storage; use League\OAuth2\Client\Token\AccessTokenInterface; -class DefaultAccessTokenRepository implements AccessTokenRepository +class StorageAccessTokenRepository implements AccessTokenRepository { public function store(AccessTokenInterface $accessToken): AccessTokenRepository diff --git a/src/Repositories/DefaultConfigRepository.php b/src/Repositories/StorageConfigRepository.php similarity index 88% rename from src/Repositories/DefaultConfigRepository.php rename to src/Repositories/StorageConfigRepository.php index 03209ef..ab67522 100644 --- a/src/Repositories/DefaultConfigRepository.php +++ b/src/Repositories/StorageConfigRepository.php @@ -2,7 +2,7 @@ namespace MelbaCh\LaravelZoho\Repositories; -class DefaultConfigRepository implements ConfigRepository +class StorageConfigRepository implements ConfigRepository { protected array $config = []; @@ -42,17 +42,17 @@ public function setScopes(array $scopes): ConfigRepository return $this; } - public function secret(): string + public function secret(): string|null { return $this->config['secrets']['secret']; } - public function clientId(): string + public function clientId(): string|null { return $this->config['secrets']['client_id']; } - public function currentOrganizationId(): ?string + public function currentOrganizationId(): string|null { return $this->config['parameters']['current_organization_id']; } diff --git a/tests/Clients/ZohoUrlFactoryTest.php b/tests/Clients/ZohoUrlFactoryTest.php index c8d59e7..567dc4e 100644 --- a/tests/Clients/ZohoUrlFactoryTest.php +++ b/tests/Clients/ZohoUrlFactoryTest.php @@ -3,7 +3,7 @@ namespace MelbaCh\LaravelZoho\Tests\Clients; use MelbaCh\LaravelZoho\Clients\ZohoUrlFactory; -use MelbaCh\LaravelZoho\Repositories\DefaultConfigRepository; +use MelbaCh\LaravelZoho\Repositories\StorageConfigRepository; use MelbaCh\LaravelZoho\Tests\TestCase; use MelbaCh\LaravelZoho\ZohoModules; use Mockery\MockInterface; @@ -15,7 +15,7 @@ protected function setUp(): void { parent::setUp(); - $this->mock(DefaultConfigRepository::class, static function (MockInterface $repository) + $this->mock(StorageConfigRepository::class, static function (MockInterface $repository) { $repository->shouldReceive('region')->andReturn('EU'); $repository->shouldReceive('currentOrganizationId')->andReturn(1234); @@ -105,7 +105,7 @@ public function it_add_the_current_organization_id_to_url_when_using_books(): vo /** @test */ public function it_uses_the_region(): void { - $this->mock(DefaultConfigRepository::class, static function (MockInterface $repository) + $this->mock(StorageConfigRepository::class, static function (MockInterface $repository) { $repository->shouldReceive('region')->andReturn('EU'); }); @@ -114,7 +114,7 @@ public function it_uses_the_region(): void invade(app(ZohoUrlFactory::class))->baseApiUrl(ZohoModules::BOOKS) ); - $this->mock(DefaultConfigRepository::class, static function (MockInterface $repository) + $this->mock(StorageConfigRepository::class, static function (MockInterface $repository) { $repository->shouldReceive('region')->andReturn('US'); }); diff --git a/tests/Controllers/ZohoAuthControllerTest.php b/tests/Controllers/ZohoAuthControllerTest.php index 01dd2f7..12ccfbb 100644 --- a/tests/Controllers/ZohoAuthControllerTest.php +++ b/tests/Controllers/ZohoAuthControllerTest.php @@ -6,7 +6,7 @@ use MelbaCh\LaravelZoho\Auth\ZohoAccessToken; use MelbaCh\LaravelZoho\Auth\ZohoAuthProvider; use MelbaCh\LaravelZoho\Repositories\AccessTokenRepository; -use MelbaCh\LaravelZoho\Repositories\DefaultConfigRepository; +use MelbaCh\LaravelZoho\Repositories\StorageConfigRepository; use MelbaCh\LaravelZoho\Tests\TestCase; use Mockery\MockInterface; @@ -20,7 +20,7 @@ protected function setUp(): void { parent::setUp(); - $this->mock(DefaultConfigRepository::class, static function (MockInterface $repository) + $this->mock(StorageConfigRepository::class, static function (MockInterface $repository) { $repository->shouldReceive('clientId')->andReturn('abc-xyz'); $repository->shouldReceive('secret')->andReturn('123-789'); diff --git a/tests/Middleware/RefreshZohoAuthTokenMiddlewareTest.php b/tests/Middleware/RefreshZohoAuthTokenMiddlewareTest.php new file mode 100644 index 0000000..3eb27bb --- /dev/null +++ b/tests/Middleware/RefreshZohoAuthTokenMiddlewareTest.php @@ -0,0 +1,116 @@ + $token, 'expires_in_sec' => - 1000]); + + $repository->store($accessToken); + + // Expectations + $expectedToken = uniqid('', true); + $expectedAccessToken = new ZohoAccessToken(['access_token' => $expectedToken]); + $this->mock(ZohoAuthProvider::class, static function (MockInterface $provider) use ($expectedAccessToken) { + $provider->shouldReceive('getAccessToken')->once()->andReturn($expectedAccessToken); + }); + + // Run middleware + $response = app(RefreshZohoAuthToken::class) + ->handle( + $this->createRequest('get', '/'), + fn() => new Response(), + ); + + // Assert + $this->assertEquals($expectedToken, $repository->get()->getToken()); + $this->assertNotEquals($token, $repository->get()->getToken()); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertFalse( $response->isRedirect()); + } + + /** @test */ + public function it_skips_when_there_is_no_token() + { + // Configuration + Storage::fake(config('zoho.access_token_disk')); + + // Expectations + $this->mock(AccessTokenRepository::class, static function (MockInterface $repository) { + $repository->shouldReceive('get')->once()->andReturn(null); + $repository->shouldNotReceive('store'); + }); + $this->mock(ZohoAuthProvider::class, static function (MockInterface $provider) { + $provider->shouldNotReceive('getAccessToken'); + }); + + // Run middleware + $response = app(RefreshZohoAuthToken::class) + ->handle( + $this->createRequest('get', '/'), + fn() => new Response(), + ); + + // Assert + $this->assertEquals(200, $response->getStatusCode()); + $this->assertFalse( $response->isRedirect()); + } + + /** @test */ + public function it_skips_when_token_is_not_expired() + { + // Configuration + Storage::fake(config('zoho.access_token_disk')); + $repository = app(StorageAccessTokenRepository::class); + + $token = uniqid('', true); + $accessToken = new ZohoAccessToken(['access_token' => $token, 'expires_in_sec' => + 1000]); + + $repository->store($accessToken); + + // Expectations + $this->mock(ZohoAuthProvider::class, static function (MockInterface $repository) { + $repository->shouldNotReceive('getAccessToken'); + }); + + // Run middleware + $response = app(RefreshZohoAuthToken::class) + ->handle( + $this->createRequest('get', '/'), + fn() => new Response(), + ); + + // Assert + $this->assertEquals($token, $repository->get()->getToken()); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertFalse( $response->isRedirect()); + } + +} \ No newline at end of file diff --git a/tests/Middleware/VerifyZohoCredentialsDoesntExistsTest.php b/tests/Middleware/VerifyZohoCredentialsDoesntExistsTest.php new file mode 100644 index 0000000..933f074 --- /dev/null +++ b/tests/Middleware/VerifyZohoCredentialsDoesntExistsTest.php @@ -0,0 +1,59 @@ +mock(AccessTokenRepository::class, static function (MockInterface $repository) { + $repository->shouldReceive('exists')->once()->andReturn(true); + }); + + $this->expectException(HttpException::class); + + // Run middleware + app(VerifyZohoCredentialsDoesntExists::class) + ->handle( + $this->createRequest('get', '/'), + fn() => new Response(), + ); + } + + /** @test */ + public function it_allow_when_credentials_doesnt_exists() + { + $this->mock(AccessTokenRepository::class, static function (MockInterface $repository) { + $repository->shouldReceive('exists')->once()->andReturn(false); + }); + + // Run middleware + $response = app(VerifyZohoCredentialsDoesntExists::class) + ->handle( + $this->createRequest('get', '/'), + fn() => new Response(), + ); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertFalse($response->isRedirect()); + } + + +} \ No newline at end of file diff --git a/tests/Repositories/DefaultAccessTokenRepositoryTest.php b/tests/Repositories/StorageAccessTokenRepositoryTest.php similarity index 87% rename from tests/Repositories/DefaultAccessTokenRepositoryTest.php rename to tests/Repositories/StorageAccessTokenRepositoryTest.php index ca28e56..b74edb5 100644 --- a/tests/Repositories/DefaultAccessTokenRepositoryTest.php +++ b/tests/Repositories/StorageAccessTokenRepositoryTest.php @@ -5,17 +5,17 @@ use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Storage; use MelbaCh\LaravelZoho\Auth\ZohoAccessToken; -use MelbaCh\LaravelZoho\Repositories\DefaultAccessTokenRepository; +use MelbaCh\LaravelZoho\Repositories\StorageAccessTokenRepository; use MelbaCh\LaravelZoho\Tests\TestCase; -class DefaultAccessTokenRepositoryTest extends TestCase +class StorageAccessTokenRepositoryTest extends TestCase { /** @test */ public function it_store_the_token(): void { Storage::fake(config('zoho.access_token_disk')); - $repository = app(DefaultAccessTokenRepository::class); + $repository = app(StorageAccessTokenRepository::class); $token = uniqid('', true); $accessToken = new ZohoAccessToken(['access_token' => $token]); @@ -34,7 +34,7 @@ public function it_store_the_token(): void public function it_get_the_token(): void { Storage::fake(config('zoho.access_token_disk')); - $repository = app(DefaultAccessTokenRepository::class); + $repository = app(StorageAccessTokenRepository::class); $token = uniqid('', true); $accessToken = new ZohoAccessToken(['access_token' => $token]); @@ -53,7 +53,7 @@ public function it_get_the_token(): void public function it_can_delete_the_token(): void { Storage::fake(config('zoho.access_token_disk')); - $repository = app(DefaultAccessTokenRepository::class); + $repository = app(StorageAccessTokenRepository::class); $token = uniqid('', true); $accessToken = new ZohoAccessToken(['access_token' => $token]); @@ -77,7 +77,7 @@ public function it_can_delete_the_token(): void public function it_can_verify_the_token_exists(): void { Storage::fake(config('zoho.access_token_disk')); - $repository = app(DefaultAccessTokenRepository::class); + $repository = app(StorageAccessTokenRepository::class); $token = uniqid('', true); $accessToken = new ZohoAccessToken(['access_token' => $token]); diff --git a/tests/Repositories/DefaultConfigRepositoryTest.php b/tests/Repositories/StorageConfigRepositoryTest.php similarity index 79% rename from tests/Repositories/DefaultConfigRepositoryTest.php rename to tests/Repositories/StorageConfigRepositoryTest.php index 6854898..a53635c 100644 --- a/tests/Repositories/DefaultConfigRepositoryTest.php +++ b/tests/Repositories/StorageConfigRepositoryTest.php @@ -2,10 +2,10 @@ namespace MelbaCh\LaravelZoho\Tests\Repositories; -use MelbaCh\LaravelZoho\Repositories\DefaultConfigRepository; +use MelbaCh\LaravelZoho\Repositories\StorageConfigRepository; use MelbaCh\LaravelZoho\Tests\TestCase; -class DefaultConfigRepositoryTest extends TestCase +class StorageConfigRepositoryTest extends TestCase { protected function setUp(): void { @@ -21,7 +21,7 @@ protected function setUp(): void /** @test */ public function it_returns_the_config(): void { - $repository = app(DefaultConfigRepository::class); + $repository = app(StorageConfigRepository::class); $this->assertEquals( [ @@ -43,35 +43,35 @@ public function it_returns_the_config(): void /** @test */ public function it_returns_the_region(): void { - $repository = app(DefaultConfigRepository::class); + $repository = app(StorageConfigRepository::class); $this->assertEquals('EU', $repository->region()); } /** @test */ public function it_returns_the_scopes(): void { - $repository = app(DefaultConfigRepository::class); + $repository = app(StorageConfigRepository::class); $this->assertEquals(['my-scope', 'my-another-scope'], $repository->scopes()); } /** @test */ public function it_returns_the_secret(): void { - $repository = app(DefaultConfigRepository::class); + $repository = app(StorageConfigRepository::class); $this->assertEquals('123-789', $repository->secret()); } /** @test */ public function it_returns_the_client_id(): void { - $repository = app(DefaultConfigRepository::class); + $repository = app(StorageConfigRepository::class); $this->assertEquals('abc-xyz', $repository->clientId()); } /** @test */ public function it_returns_the_current_organization_id(): void { - $repository = app(DefaultConfigRepository::class); + $repository = app(StorageConfigRepository::class); $this->assertEquals(1234, $repository->currentOrganizationId()); } From be7e8f833186e89f9bb2c23f44b6cc33752b483a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Baconnier?= Date: Wed, 4 May 2022 17:55:08 +0200 Subject: [PATCH 03/10] Add tests --- tests/Clients/ZohoUrlFactoryTest.php | 46 +++++++++++++++++++ .../DatabaseAccessTokenRepositoryTest.php | 26 +++++++++++ .../DatabaseConfigRepositoryTest.php | 12 +++++ .../StorageConfigRepositoryTest.php | 31 +++++++++++++ 4 files changed, 115 insertions(+) diff --git a/tests/Clients/ZohoUrlFactoryTest.php b/tests/Clients/ZohoUrlFactoryTest.php index 567dc4e..9a820ae 100644 --- a/tests/Clients/ZohoUrlFactoryTest.php +++ b/tests/Clients/ZohoUrlFactoryTest.php @@ -166,4 +166,50 @@ public function it_build_using_web_method(): void $urlFactory->web(ZohoModules::RECRUIT, '/EntityInfo.do?module=Candidates&id=31529000000000000&submodule=Candidates') ); } + + /** @test */ + public function it_adds_parameter_to_api_url() + { + $urlFactory = app(ZohoUrlFactory::class); + + $this->assertEquals( + 'https://www.zohoapis.eu/crm/v2/users/4?foo=1&bar=2&baz=3', + $urlFactory->api(ZohoModules::CRM, '/users/4', [ + 'foo' => 1, + 'bar' => 2, + 'baz' => 3, + ]) + ); + + $this->assertEquals( + 'https://www.zohoapis.eu/crm/v2/users/4?foo=1&bar=2&baz=3', + $urlFactory->api(ZohoModules::CRM, '/users/4?foo=1', [ + 'bar' => 2, + 'baz' => 3, + ]) + ); + } + + /** @test */ + public function it_adds_parameter_to_web_url() + { + $urlFactory = app(ZohoUrlFactory::class); + + $this->assertEquals( + 'https://crm.zoho.eu/crm/1234/users/4?foo=1&bar=2&baz=3', + $urlFactory->web(ZohoModules::CRM, '/users/4', [ + 'foo' => 1, + 'bar' => 2, + 'baz' => 3, + ]) + ); + + $this->assertEquals( + 'https://crm.zoho.eu/crm/1234/users/4?foo=1&bar=2&baz=3', + $urlFactory->web(ZohoModules::CRM, '/users/4?foo=1', [ + 'bar' => 2, + 'baz' => 3, + ]) + ); + } } \ No newline at end of file diff --git a/tests/Repositories/DatabaseAccessTokenRepositoryTest.php b/tests/Repositories/DatabaseAccessTokenRepositoryTest.php index beb91b1..5624833 100644 --- a/tests/Repositories/DatabaseAccessTokenRepositoryTest.php +++ b/tests/Repositories/DatabaseAccessTokenRepositoryTest.php @@ -22,6 +22,32 @@ protected function setUp(): void Auth::shouldReceive('user')->andReturn(new User); } + /** @test */ + public function it_know_the_access_token_exists() + { + $token = uniqid('', true); + $accessToken = new ZohoAccessToken(['access_token' => $token]); + + DB::table('oauth_tokens')->insert([ + 'provider' => 'zoho', + 'owner_id' => 1, + 'owner_type' => (new User)->getMorphClass(), + 'access_token' => Crypt::encrypt($accessToken), + 'config' => null, + ]); + + $repository = app(DatabaseAccessTokenRepository::class); + + $this->assertTrue($repository->exists()); + } + + /** @test */ + public function it_know_the_access_token_does_not_exists() + { + $repository = app(DatabaseAccessTokenRepository::class); + $this->assertFalse($repository->exists()); + } + /** @test */ public function it_returns_the_access_token_for_the_authenticated_user(): void { diff --git a/tests/Repositories/DatabaseConfigRepositoryTest.php b/tests/Repositories/DatabaseConfigRepositoryTest.php index 981be25..c675406 100644 --- a/tests/Repositories/DatabaseConfigRepositoryTest.php +++ b/tests/Repositories/DatabaseConfigRepositoryTest.php @@ -95,6 +95,18 @@ public function it_returns_the_region(): void $this->assertEquals('EU', $repository->region()); } + /** @test */ + public function it_set_the_scopes(): void + { + $repository = app(DatabaseConfigRepository::class); + + $this->storeDefaultConfig(); + + $repository->setScopes(['my-new-scope', 'another-new-scope']); + + $this->assertEquals(['my-new-scope', 'another-new-scope'], $repository->scopes()); + } + /** @test */ public function it_returns_the_scopes(): void { diff --git a/tests/Repositories/StorageConfigRepositoryTest.php b/tests/Repositories/StorageConfigRepositoryTest.php index a53635c..0f7eeb5 100644 --- a/tests/Repositories/StorageConfigRepositoryTest.php +++ b/tests/Repositories/StorageConfigRepositoryTest.php @@ -47,6 +47,16 @@ public function it_returns_the_region(): void $this->assertEquals('EU', $repository->region()); } + /** @test */ + public function it_cannot_set_the_scopes(): void + { + $repository = app(StorageConfigRepository::class); + + // Not supported + $this->expectException(\Exception::class); + $repository->setScopes(['my-new-scope', 'another-new-scope']); + } + /** @test */ public function it_returns_the_scopes(): void { @@ -75,4 +85,25 @@ public function it_returns_the_current_organization_id(): void $this->assertEquals(1234, $repository->currentOrganizationId()); } + /** @test */ + public function it_cannot_store(): void + { + $repository = app(StorageConfigRepository::class); + + // Not supported + $this->expectException(\Exception::class); + $repository->store([]); + } + + /** @test */ + public function it_cannot_be_deleted(): void + { + $repository = app(StorageConfigRepository::class); + + // Not supported + $this->expectException(\Exception::class); + $repository->delete(); + } + + } \ No newline at end of file From a4962c5f886506b82f0d5d7313d10843c774226a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Baconnier?= Date: Wed, 4 May 2022 17:55:30 +0200 Subject: [PATCH 04/10] Remove deprecated Exception --- .../StorageAccessTokenRepository.php | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Repositories/StorageAccessTokenRepository.php b/src/Repositories/StorageAccessTokenRepository.php index 888bc5d..905dc83 100644 --- a/src/Repositories/StorageAccessTokenRepository.php +++ b/src/Repositories/StorageAccessTokenRepository.php @@ -3,7 +3,6 @@ namespace MelbaCh\LaravelZoho\Repositories; use Crypt; -use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Support\Facades\Storage; use League\OAuth2\Client\Token\AccessTokenInterface; @@ -21,19 +20,14 @@ public function store(AccessTokenInterface $accessToken): AccessTokenRepository public function get(): AccessTokenInterface|null { - try { - $hash = Storage::disk(config('zoho.access_token_disk', null)) - ->get(config('zoho.access_token_path')); + $hash = Storage::disk(config('zoho.access_token_disk', null)) + ->get(config('zoho.access_token_path')); - if ($hash === null) { - return null; - } - - return Crypt::decrypt($hash); - - } catch (FileNotFoundException $exception) { + if ($hash === null) { return null; } + + return Crypt::decrypt($hash); } public function delete(): void From b28c9b66a018600321cfc728fccb47d6792d8f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Baconnier?= Date: Wed, 4 May 2022 17:56:05 +0200 Subject: [PATCH 05/10] Add exception when setScopes is used on Storage configuration --- src/Repositories/StorageConfigRepository.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Repositories/StorageConfigRepository.php b/src/Repositories/StorageConfigRepository.php index ab67522..e2896f6 100644 --- a/src/Repositories/StorageConfigRepository.php +++ b/src/Repositories/StorageConfigRepository.php @@ -38,8 +38,7 @@ public function scopes(): array public function setScopes(array $scopes): ConfigRepository { - // Not supported - return $this; + throw new \Exception('`setScopes` is not available when using DefaultConfigRepository'); } public function secret(): string|null From 1444df20226197200630803463fce4c8ad970cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Baconnier?= Date: Wed, 4 May 2022 17:56:30 +0200 Subject: [PATCH 06/10] Add error status from zoho --- src/Auth/ZohoAuthProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Auth/ZohoAuthProvider.php b/src/Auth/ZohoAuthProvider.php index 7479433..2858912 100644 --- a/src/Auth/ZohoAuthProvider.php +++ b/src/Auth/ZohoAuthProvider.php @@ -104,7 +104,7 @@ protected function checkResponse(ResponseInterface $response, $data): void throw new IdentityProviderException( sprintf('There was an error on response: %s', $data['error']), match ($data['error']) { - 'invalid_client_secret', 'invalid_code' => 403, + 'invalid_client_secret', 'invalid_code', 'invalid_client' => 403, default => 500 }, $data['error'] From d5f4ceeaaa81ebcfd36e8e2102325526873a7704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Baconnier?= Date: Wed, 20 Jul 2022 13:47:03 +0200 Subject: [PATCH 07/10] Rename config parameter name --- config/zoho.php | 2 +- src/Controllers/ZohoAuthController.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/zoho.php b/config/zoho.php index 892f772..9828842 100644 --- a/config/zoho.php +++ b/config/zoho.php @@ -14,7 +14,7 @@ // - 403: invalid_client_secret // - 403: invalid_code // - 500: fallback on unknown error - 'on_error_url' => '/', + 'on_error_redirect_to' => '/', /** * Middleware to generate a Token diff --git a/src/Controllers/ZohoAuthController.php b/src/Controllers/ZohoAuthController.php index 5aeb913..50530bb 100644 --- a/src/Controllers/ZohoAuthController.php +++ b/src/Controllers/ZohoAuthController.php @@ -90,7 +90,7 @@ private function getAccessToken( 'message' => $e->getMessage(), ]); - return redirect(config('zoho.on_error_url', '/')); + return redirect(config('zoho.on_error_redirect_to', '/')); } $accessTokenRepository->store($accessToken); From dd9b383545ff4fcfd15689522f9581cf8a7c7a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Baconnier?= Date: Wed, 20 Jul 2022 13:48:14 +0200 Subject: [PATCH 08/10] Fix tests --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 66cacb7..ec72d76 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ "orchestra/testbench": "^7.0", "phpunit/phpunit": "^9.3", "nunomaduro/collision": "^6.1", - "spatie/invade": "^1.0" + "spatie/invade": "^1.1" }, "scripts": { "test": "./vendor/bin/testbench package:test --parallel --no-coverage", From a0071003b4d86f55c4dc9881c66aae81d7a16e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Baconnier?= Date: Wed, 20 Jul 2022 14:14:56 +0200 Subject: [PATCH 09/10] Upgrade URLs to Zoho V3 and add sandbox urls to web --- config/zoho.php | 3 +- src/Clients/ZohoUrlFactory.php | 41 +++++++++++++++++++++++----- tests/Clients/ZohoUrlFactoryTest.php | 24 ++++++++++++---- tests/Zoho/ZohoHttpTest.php | 2 +- 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/config/zoho.php b/config/zoho.php index 9828842..02ac40f 100644 --- a/config/zoho.php +++ b/config/zoho.php @@ -6,9 +6,10 @@ */ 'url' => '/oauth2/zoho', 'redirect_url' => '/', + 'sandbox' => env('ZOHO_SANDBOX', false), // When requesting an accessToken, the API may return an error, - // The controller will redirect the user to `on_error_url` + // The controller will redirect the user to `on_error_redirect_to` // with the error flashed in the session with the key `zoho.access_token_error` // known error code: // - 403: invalid_client_secret diff --git a/src/Clients/ZohoUrlFactory.php b/src/Clients/ZohoUrlFactory.php index cceb95d..3724ecd 100644 --- a/src/Clients/ZohoUrlFactory.php +++ b/src/Clients/ZohoUrlFactory.php @@ -41,7 +41,12 @@ public function web(ZohoModules $module, string $url, array $parameters = []) $url = Str::replaceFirst('/', '', $url); } - $url = Str::finish($this->baseWebUrl($module), '/') . $url; + if (config('zoho.sandbox', false)) { + $url = Str::finish($this->baseWebUrlSandbox($module), '/') . $url; + } else { + $url = Str::finish($this->baseWebUrl($module), '/') . $url; + } + foreach ($parameters as $parameter => $value) { $url = $this->addParameterToUrlQuery($url, $parameter, $value); @@ -51,9 +56,9 @@ public function web(ZohoModules $module, string $url, array $parameters = []) } /** - * @internal * @param string $type * @return string + * @internal */ public function oauthApiUrl(string $type): string { @@ -119,11 +124,11 @@ protected function baseApiUrl(ZohoModules $module): string 'CN' => 'https://books.zoho.com.cn/api/v3', ], ZohoModules::CRM->value => [ - 'EU' => 'https://www.zohoapis.eu/crm/v2', - 'US' => 'https://www.zohoapis.com/crm/v2', - 'IN' => 'https://www.zohoapis.in/crm/v2', - 'AU' => 'https://www.zohoapis.com.au/crm/v2', - 'CN' => 'https://www.zohoapis.com.cn/crm/v2', + 'EU' => 'https://www.zohoapis.eu/crm/v3', + 'US' => 'https://www.zohoapis.com/crm/v3', + 'IN' => 'https://www.zohoapis.in/crm/v3', + 'AU' => 'https://www.zohoapis.com.au/crm/v3', + 'CN' => 'https://www.zohoapis.com.cn/crm/v3', ], ZohoModules::RECRUIT->value => [ 'EU' => 'https://recruit.zoho.eu/recruit/v2', @@ -135,6 +140,28 @@ protected function baseApiUrl(ZohoModules $module): string ][$module->value][$region]; } + protected function baseWebUrlSandbox(ZohoModules $module): string + { + $region = $this->config->region() ?? 'US'; + $organization = $this->config->currentOrganizationId(); + + return [ + ZohoModules::BOOKS->value => [ + // Not implemented yet + ], + ZohoModules::CRM->value => [ + 'EU' => "https://crmsandbox.zoho.eu/crm/{$organization}", + 'US' => "https://crmsandbox.zoho.com/crm/{$organization}", + 'IN' => "https://crmsandbox.zoho.in/crm/{$organization}", + 'AU' => "https://crmsandbox.zoho.com.eu/crm/{$organization}", + 'CN' => "https://crmsandbox.zoho.com.cn/crm/{$organization}", + ], + ZohoModules::RECRUIT->value => [ + // Not implemented yet + ], + ][$module->value][$region]; + } + protected function baseWebUrl(ZohoModules $module): string { $region = $this->config->region() ?? 'US'; diff --git a/tests/Clients/ZohoUrlFactoryTest.php b/tests/Clients/ZohoUrlFactoryTest.php index 9a820ae..f3ae62b 100644 --- a/tests/Clients/ZohoUrlFactoryTest.php +++ b/tests/Clients/ZohoUrlFactoryTest.php @@ -28,7 +28,7 @@ public function it_build_url_for_a_module(): void $urlFactory = app(ZohoUrlFactory::class); $this->assertEquals( - 'https://www.zohoapis.eu/crm/v2/users/4', + 'https://www.zohoapis.eu/crm/v3/users/4', $urlFactory->api(ZohoModules::CRM, '/users/4') ); @@ -49,7 +49,7 @@ public function it_returns_the_base_urls_for_the_api(): void $urlFactory = app(ZohoUrlFactory::class); $this->assertEquals( - 'https://www.zohoapis.eu/crm/v2', + 'https://www.zohoapis.eu/crm/v3', invade($urlFactory)->baseApiUrl(ZohoModules::CRM) ); @@ -130,7 +130,7 @@ public function it_build_using_api_method(): void $urlFactory = app(ZohoUrlFactory::class); $this->assertEquals( - 'https://www.zohoapis.eu/crm/v2/users/4', + 'https://www.zohoapis.eu/crm/v3/users/4', $urlFactory->api(ZohoModules::CRM, '/users/4') ); @@ -173,7 +173,7 @@ public function it_adds_parameter_to_api_url() $urlFactory = app(ZohoUrlFactory::class); $this->assertEquals( - 'https://www.zohoapis.eu/crm/v2/users/4?foo=1&bar=2&baz=3', + 'https://www.zohoapis.eu/crm/v3/users/4?foo=1&bar=2&baz=3', $urlFactory->api(ZohoModules::CRM, '/users/4', [ 'foo' => 1, 'bar' => 2, @@ -182,7 +182,7 @@ public function it_adds_parameter_to_api_url() ); $this->assertEquals( - 'https://www.zohoapis.eu/crm/v2/users/4?foo=1&bar=2&baz=3', + 'https://www.zohoapis.eu/crm/v3/users/4?foo=1&bar=2&baz=3', $urlFactory->api(ZohoModules::CRM, '/users/4?foo=1', [ 'bar' => 2, 'baz' => 3, @@ -212,4 +212,18 @@ public function it_adds_parameter_to_web_url() ]) ); } + + /** @test */ + public function it_uses_the_sandbox_for_web() + { + config(['zoho.sandbox' => true]); + + $urlFactory = app(ZohoUrlFactory::class); + + $this->assertEquals( + 'https://crmsandbox.zoho.eu/crm/1234/tab/Potentials/292528000000000000', + $urlFactory->web(ZohoModules::CRM, '/tab/Potentials/292528000000000000') + ); + + } } \ No newline at end of file diff --git a/tests/Zoho/ZohoHttpTest.php b/tests/Zoho/ZohoHttpTest.php index 7510f91..a4bd232 100644 --- a/tests/Zoho/ZohoHttpTest.php +++ b/tests/Zoho/ZohoHttpTest.php @@ -69,7 +69,7 @@ public function it_can_make_a_request_using_header(): void public function it_can_make_a_request_zoho_url_facade(): void { Http::fake([ - 'https://www.zohoapis.com/crm/v2/users' => Http::response($this->fakeResponse), + 'https://www.zohoapis.com/crm/v3/users' => Http::response($this->fakeResponse), ]); $this->assertEquals( From 1400b710dc5ff28809043133bddded2448d4d9d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Baconnier?= Date: Wed, 20 Jul 2022 14:50:08 +0200 Subject: [PATCH 10/10] Refactor Macros and silent the exception from setScopes --- src/LaravelZohoServiceProvider.php | 35 ++++--------------- src/Macros/ErrorsFromZoho.php | 33 +++++++++++++++++ src/Macros/HasErrorsFromZoho.php | 20 +++++++++++ src/Macros/WithZohoHeader.php | 28 +++++++++++++++ src/Repositories/StorageConfigRepository.php | 3 +- .../StorageConfigRepositoryTest.php | 6 ++-- 6 files changed, 93 insertions(+), 32 deletions(-) create mode 100644 src/Macros/ErrorsFromZoho.php create mode 100644 src/Macros/HasErrorsFromZoho.php create mode 100644 src/Macros/WithZohoHeader.php diff --git a/src/LaravelZohoServiceProvider.php b/src/LaravelZohoServiceProvider.php index 7bbeeff..5fd3847 100644 --- a/src/LaravelZohoServiceProvider.php +++ b/src/LaravelZohoServiceProvider.php @@ -8,6 +8,9 @@ use Illuminate\Support\ServiceProvider; use MelbaCh\LaravelZoho\Auth\ZohoAuthProvider; use MelbaCh\LaravelZoho\Controllers\ZohoAuthController; +use MelbaCh\LaravelZoho\Macros\ErrorsFromZoho; +use MelbaCh\LaravelZoho\Macros\HasErrorsFromZoho; +use MelbaCh\LaravelZoho\Macros\WithZohoHeader; use MelbaCh\LaravelZoho\Repositories\AccessTokenRepository; use MelbaCh\LaravelZoho\Repositories\ConfigRepository; use MelbaCh\LaravelZoho\Repositories\StorageAccessTokenRepository; @@ -27,35 +30,9 @@ public function boot() ], 'laravel-zoho-migrations'); } - Http::macro('withZohoHeader', function () { - $accessTokenRepository = app(AccessTokenRepository::class); - $headers = []; - if ($token = $accessTokenRepository->get()) { - $headers['Authorization'] = "Zoho-oauthtoken {$token->getToken()}"; - } - return Http::withHeaders($headers); - }); - - Response::macro('hasErrorsFromZoho', function () { - return $this->status() >= 400 || count($this->errorsFromZoho()); - }); - - Response::macro('errorsFromZoho', function () { - if ($this->status() >= 400) { - return $this->json() ?? []; - } - - return collect($this->json()) - ->flatten(1) - ->filter(function ($value) { - if (is_array($value) && array_key_exists('status', $value)) { - return $value['status'] === 'error'; - } - return null; - }) - ->values() - ->toArray(); - }); + Http::macro('withZohoHeader', app(WithZohoHeader::class)()); + Response::macro('hasErrorsFromZoho', app(HasErrorsFromZoho::class)()); + Response::macro('errorsFromZoho', app(ErrorsFromZoho::class)()); Route::get(config('zoho.url', '/oauth2/zoho'), [ZohoAuthController::class, 'requestToken']); } diff --git a/src/Macros/ErrorsFromZoho.php b/src/Macros/ErrorsFromZoho.php new file mode 100644 index 0000000..d6751ea --- /dev/null +++ b/src/Macros/ErrorsFromZoho.php @@ -0,0 +1,33 @@ +status() >= 400) { + return $this->json() ?? []; + } + + return collect($this->json()) + ->flatten(1) + ->filter(function ($value) { + if (is_array($value) && array_key_exists('status', $value)) { + return $value['status'] === 'error'; + } + return null; + }) + ->values() + ->toArray(); + }; + } +} \ No newline at end of file diff --git a/src/Macros/HasErrorsFromZoho.php b/src/Macros/HasErrorsFromZoho.php new file mode 100644 index 0000000..a7313ae --- /dev/null +++ b/src/Macros/HasErrorsFromZoho.php @@ -0,0 +1,20 @@ +status() >= 400 || count($this->errorsFromZoho()) > 0; + }; + } +} \ No newline at end of file diff --git a/src/Macros/WithZohoHeader.php b/src/Macros/WithZohoHeader.php new file mode 100644 index 0000000..1c2e0a8 --- /dev/null +++ b/src/Macros/WithZohoHeader.php @@ -0,0 +1,28 @@ +get()) { + $headers['Authorization'] = "Zoho-oauthtoken {$token->getToken()}"; + } + + return Http::withHeaders($headers); + }; + } +} \ No newline at end of file diff --git a/src/Repositories/StorageConfigRepository.php b/src/Repositories/StorageConfigRepository.php index e2896f6..e39a467 100644 --- a/src/Repositories/StorageConfigRepository.php +++ b/src/Repositories/StorageConfigRepository.php @@ -38,7 +38,8 @@ public function scopes(): array public function setScopes(array $scopes): ConfigRepository { - throw new \Exception('`setScopes` is not available when using DefaultConfigRepository'); + // `setScopes` is not available when using DefaultConfigRepository + return $this; } public function secret(): string|null diff --git a/tests/Repositories/StorageConfigRepositoryTest.php b/tests/Repositories/StorageConfigRepositoryTest.php index 0f7eeb5..932f510 100644 --- a/tests/Repositories/StorageConfigRepositoryTest.php +++ b/tests/Repositories/StorageConfigRepositoryTest.php @@ -52,9 +52,11 @@ public function it_cannot_set_the_scopes(): void { $repository = app(StorageConfigRepository::class); - // Not supported - $this->expectException(\Exception::class); + $scopes = $repository->scopes(); + $repository->setScopes(['my-new-scope', 'another-new-scope']); + + $this->assertEquals($scopes, $repository->scopes()); } /** @test */