Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the cache store and cache key configurable #6

Merged
merged 1 commit into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
coverage.xml
composer.lock
/reports

/tests/cache
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ This helper takes care of all the headaches and boilerplate code with a simple a
- Access token key
- ~~Improve data validation and error messages~~
- Write/update readme
- Make the cache store configurable
- ~~Make the cache store configurable~~
- Maybe: add more tests
- Maybe: Add support for authorization_code and implicit grants

Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
cacheDirectory="tests/cache/.phpunit.cache"
executionOrder="depends,defects"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
Expand Down
20 changes: 17 additions & 3 deletions src/AccessToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ final class AccessToken

public const TYPE_CUSTOM = 'custom';

protected string $tokenName = 'token';

public function __construct(
protected string $accessToken,
protected Carbon $expiresAt,
protected string $tokenType = self::TYPE_BEARER,
protected ?Closure $customCallback = null
protected string $tokenName = 'token',
protected ?Closure $customCallback = null,
) {
if ($tokenType === self::TYPE_CUSTOM && is_null($customCallback)) {
throw new InvalidArgumentException('customCallback must be set when using AUTH_TYPE_CUSTOM');
Expand All @@ -43,6 +42,21 @@ public function getExpiresIn(): int
return (int) round(Carbon::now()->diffInSeconds($this->expiresAt));
}

public function getTokenType(): string
{
return $this->tokenType;
}

public function getTokenName(): string
{
return $this->tokenName;
}

public function getCustomCallback(): ?Closure
{
return $this->customCallback;
}

public function getHttpClient(PendingRequest $httpClient): PendingRequest
{
return match ($this->tokenType) {
Expand Down
1 change: 1 addition & 0 deletions src/Credentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public function addAuthToRequest(PendingRequest $httpClient, Options $options):
$options->tokenName => $this->token,
]);
}

return $httpClient->withToken($this->token, $options->authType);
}
if ($options->authType === self::AUTH_TYPE_BASIC) {
Expand Down
2 changes: 2 additions & 0 deletions src/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ final public function __construct(
public int|string|Closure $expires = 3600,
public string|Closure $accessToken = 'access_token',
public ?Closure $tokenTypeCustomCallback = null,
public ?string $cacheKey = null,
public ?string $cacheDriver = null,
) {
$this->validateOptions();
}
Expand Down
1 change: 1 addition & 0 deletions src/RefreshToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public function __invoke(
//tokenType: $options['auth_type'],
tokenType: $options->tokenType,
customCallback: $options->tokenTypeCustomCallback,
tokenName: $options->tokenName,
);
}

Expand Down
9 changes: 5 additions & 4 deletions src/TokenStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ protected static function generateCacheKey(string $refreshUrl): string
}

/**
* @throws Exception
* @throws Exception|\Psr\SimpleCache\InvalidArgumentException
*/
public static function get(
string $refreshUrl,
Credentials $credentials,
Options $options,
): AccessToken {
$cacheKey = static::generateCacheKey($refreshUrl);
$accessToken = Cache::get($cacheKey);
$cacheKey = $options->cacheKey ?? static::generateCacheKey($refreshUrl);

$accessToken = Cache::store($options->cacheDriver)->get($cacheKey);

if ($accessToken) {
return $accessToken;
Expand All @@ -31,7 +32,7 @@ public static function get(
$accessToken = app(RefreshToken::class)(...func_get_args());
$ttl = $accessToken->getExpiresIn();

Cache::put($cacheKey, $accessToken, $ttl);
Cache::store($options->cacheDriver)->put($cacheKey, $accessToken, $ttl);

return $accessToken;
}
Expand Down
57 changes: 54 additions & 3 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

namespace Pelmered\LaravelHttpOAuthHelper\Tests;

use Illuminate\Contracts\Config\Repository;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Pelmered\LaravelHttpOAuthHelper\LaravelHttpOAuthHelperServiceProvider;

class TestCase extends \Orchestra\Testbench\TestCase
Expand Down Expand Up @@ -36,17 +38,66 @@ static function (Request $request) {
}
}

if ($request->url() === 'https://example.com/api') {
if (
$request->url() === 'https://example.com/api'
&& $request->hasHeader('Authorization')
) {
return Http::response([
'data' => 'some data',
'data' => 'some data with bearer token',
'token' => $request->header('Authorization')[0],
], 200);
}

if (Str::of($request->url())->startsWith('https://example.com/api?token=')) {

$url = parse_url($request->url(), PHP_URL_QUERY);
parse_str($url, $output);
$token = $output['token'];

return Http::response([
'data' => 'some data with query string token',
'token' => $token,
], 200);
}

return Http::response([], 200);
}
);

Cache::spy();
//Cache::spy();
}

protected function defineEnvironment($app)
{
// Setup default database to use sqlite :memory:
tap($app['config'], function (Repository $config) {

//dd($config->get('cache.stores'));

// Setup queue database connections.
$configData = [
'database.default' => 'testbench',
'database.connections.testbench' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
],
'queue.batching.database' => 'testbench',
'queue.failed.database' => 'testbench',

//'cache.stores.array'

];

foreach ($configData as $key => $value) {
$config->set($key, $value);
}
});
}

protected function usesMySqlConnection($app)
{
$app['config']->set('database.default', 'mysql');
}

protected function getPackageProviders($app): array
Expand Down
1 change: 0 additions & 1 deletion tests/Unit/CredentialsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

uses(\Pelmered\LaravelHttpOAuthHelper\Tests\TestCase::class);

use Illuminate\Validation\ValidationException;
use Pelmered\LaravelHttpOAuthHelper\Credentials;

it('validates credentials array when creating a credential object', function () {
Expand Down
46 changes: 40 additions & 6 deletions tests/Unit/MacroTest.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
<?php

uses(\Pelmered\LaravelHttpOAuthHelper\Tests\TestCase::class);

use Illuminate\Cache\FileStore;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Pelmered\LaravelHttpOAuthHelper\AccessToken;
use Pelmered\LaravelHttpOAuthHelper\Credentials;

beforeEach(function () {});

test('macro with shorthand refresh token', function () {
$response = Http::withRefreshToken(
'https://example.com/oauth/token',
Expand All @@ -22,7 +26,7 @@
return $request->hasHeader('Authorization', 'Bearer this_is_my_access_token_from_body_refresh_token') && $request->url() === 'https://example.com/api';
});
});
test('macro with shorthand client credentials', function () {
test('macro with shorthand client credentials', function () {
$response = Http::withRefreshToken(
'https://example.com/oauth/token',
[
Expand All @@ -31,7 +35,7 @@
['scopes' => ['scope1', 'scope2']],
)->get('https://example.com/api');

expect($response->json()['data'])->toBe('some data');
expect($response->json()['data'])->toBe('some data with bearer token');

Http::assertSentInOrder([
function (Request $request) {
Expand All @@ -44,19 +48,19 @@ function (Request $request) {
},
]);
});
test('macro with refresh token in credentials object', function () {
test('macro with refresh token in credentials object', function () {
$response = Http::withRefreshToken(
'https://example.com/oauth/token',
new Credentials(
token: 'this_is_my_refresh_token',
),
[
'scopes' => ['scope1', 'scope2'],
'scopes' => ['scope1', 'scope2'],
'authType' => Credentials::AUTH_TYPE_BEARER,
]
)->get('https://example.com/api');

expect($response->json()['data'])->toBe('some data');
expect($response->json()['data'])->toBe('some data with bearer token');

Http::assertSentInOrder([
function (Request $request) {
Expand All @@ -68,7 +72,7 @@ function (Request $request) {
]);
});

test('macro with client credentials in credentials object', function () {
test('macro with client credentials in credentials object', function () {
$response = Http::withRefreshToken(
'https://example.com/oauth/token',
new Credentials(
Expand All @@ -92,3 +96,33 @@ function (Request $request) {
},
]);
});

test('macro with custom cache store', function () {

Cache::clear();
Cache::spy();

Cache::shouldReceive('store')
->with('file')
->andReturn(new FileStore(app()['files'], 'tests/cache'));

$response = Http::withRefreshToken(
'https://example.com/oauth/token',
new Credentials(
clientId: 'this_is_my_client_id',
clientSecret: 'this_is_my_client_secret',
),
[
'scopes' => ['scope1', 'scope2'],
'tokenType' => AccessToken::TYPE_BEARER,
'authType' => Credentials::AUTH_TYPE_BASIC,
'cacheDriver' => 'file',
'cacheKey' => 'my_cache_key',
],
)->get('https://example.com/api');

$data = $response->json();

expect($data['data'])->toBe('some data with bearer token')
->and($data['token'])->toBe('Bearer this_is_my_access_token_from_body_refresh_token');
});
2 changes: 1 addition & 1 deletion tests/Unit/OptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
$this->expectExceptionMessage('The scopes.2 field must be a string.');

new Options(
scopes: ['valid', 'also_valid', new stdClass()],
scopes: ['valid', 'also_valid', new stdClass],
);
});

Expand Down
46 changes: 46 additions & 0 deletions tests/Unit/TokenStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@

uses(\Pelmered\LaravelHttpOAuthHelper\Tests\TestCase::class);

use Illuminate\Cache\FileStore;
use Illuminate\Support\Facades\Cache;
use Orchestra\Testbench\Attributes\DefineEnvironment;
use Pelmered\LaravelHttpOAuthHelper\Credentials;
use Pelmered\LaravelHttpOAuthHelper\Options;
use Pelmered\LaravelHttpOAuthHelper\TokenStore;

beforeEach(function () {
//DefineEnvironment::

});

/*
it('reads and stores a token in cache', function () {
Cache::clear();
Cache::spy();
Expand All @@ -28,3 +36,41 @@
// Does not work with composer update --prefer-lowest
//Cache::shouldHaveReceived('put')->once()->with('oauth_token_example.comoauthtoken', $accessToken, 3540);
});
*/

it('reads and stores a token in cache with custom cache driver', function () {

Cache::clear();
//Cache::spy();

//FileStore::

//Cache::shouldReceive('get')->once()->with('oauth_token_example.comoauthtoken')->andReturn(null);

$cacheStore = 'file';
$cacheKey = 'custom_cache_key';
$cacheStoreNotUsed = 'array';

$accessToken = TokenStore::get(
'https://example.com/oauth/token',
new Credentials(
clientId: 'this_is_my_client_id',
clientSecret: 'this_is_my_client_secret',
),
new Options(
scopes: ['scope1', 'scope2'],
authType: Credentials::AUTH_TYPE_BASIC,
cacheKey: $cacheKey,
cacheDriver: $cacheStore
),
);

$accessToken2 = Cache::store($cacheStore)->get($cacheKey);
$accessToken3 = Cache::store($cacheStoreNotUsed)->get($cacheKey);

expect($accessToken->getAccessToken())->toBe($accessToken2->getAccessToken())
->and($accessToken->getExpiresIn())->toBe($accessToken2->getExpiresIn())
->and($accessToken->getTokenType())->toBe($accessToken2->getTokenType())
->and($accessToken->getTokenName())->toBe($accessToken2->getTokenName())
->and($accessToken3)->toBeNull();
});
Loading