Skip to content

Commit

Permalink
feat: some OAuthcontroller tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
victore13 committed Sep 17, 2024
1 parent c52b7c2 commit 6ca9531
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 21 deletions.
2 changes: 1 addition & 1 deletion database/migrations/modify_user_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function up(): void
$table->string('oauth_id')->nullable();
$table->longText('oauth_token')->nullable();
$table->string('oauth_refresh_token')->nullable();
$table->integer('oauth_token_expires_at')->nullable();
$table->timestamp('oauth_token_expires_at')->nullable();
});
}

Expand Down
9 changes: 9 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,13 @@
<directory suffix=".php">./src</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="MAIL_MAILER" value="array"/>
</php>
</phpunit>
1 change: 1 addition & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
use Raiolanetworks\OAuth\Controllers\OAuthController;

Route::prefix('oauth')->group(function () {
Route::get('/request', [OAuthController::class, 'request'])->name('oauth.request');
Route::get('/callback', [OAuthController::class, 'callback'])->name('oauth.callback');
});
11 changes: 5 additions & 6 deletions src/Controllers/OAuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ class OAuthController extends Controller
{
private OAuthService $provider;

public function __construct()
public function __construct(OAuthService $provider = null)
{
$this->provider = new OAuthService();
$this->provider = $provider ?? app(OAuthService::class);
}

public function request(): RedirectResponse|HttpFoundationRedirectResponse|Redirector
Expand Down Expand Up @@ -76,7 +76,6 @@ public function callback(): RedirectResponse
]);

$callback = $this->provider->getResourceOwner($accessToken)->toArray();

$user = $this->updateOrCreateUser($callback, $accessToken);

EventsOAuthTokenUpdated::dispatch($user, $callback['groups']);
Expand All @@ -100,7 +99,7 @@ public function renew(): null|\Illuminate\Routing\Redirector|RedirectResponse
$user = Auth::guard(config()->string('oauth.guard_name'))->user();

// @phpstan-ignore-next-line
if ($user->oauth_token !== null && $user->oauth_token_expires_at < Carbon::now()->timestamp) {
if ($user->oauth_token !== null && $user->oauth_token_expires_at->timestamp < Carbon::now()->timestamp) {
try {
/** @var \League\OAuth2\Client\Token\AccessToken $accessToken */
$accessToken = $this->provider->getAccessToken('refresh_token', [
Expand Down Expand Up @@ -143,7 +142,7 @@ public function renew(): null|\Illuminate\Routing\Redirector|RedirectResponse
protected function updateOrCreateUser(array $callback, AccessTokenInterface $accessToken): Model
{
/** @var array<string,string> $groups */
$groups = $callback['groups'];
$groups = $callback['groups'] ?? [];

/** @var Model $model */
$model = config('oauth.user_model_name');
Expand All @@ -155,7 +154,7 @@ protected function updateOrCreateUser(array $callback, AccessTokenInterface $acc
],
[
'name' => $callback['name'],
'type' => in_array(config('services.oauth.admin_group'), $groups) ? 'admin' : 'user',
'type' => in_array(config('oauth.admin_group'), $groups) ? 'admin' : 'user',
'oauth_token' => $accessToken->getToken(),
'oauth_refresh_token' => $accessToken->getRefreshToken(),
'oauth_token_expires_at' => $accessToken->getExpires(),
Expand Down
File renamed without changes.
9 changes: 0 additions & 9 deletions src/Models/User.php

This file was deleted.

196 changes: 196 additions & 0 deletions tests/Feature/ControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<?php

declare(strict_types=1);

use Carbon\Carbon;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Session;
use League\OAuth2\Client\Token\AccessToken;
use Raiolanetworks\OAuth\Controllers\OAuthController;
use Raiolanetworks\OAuth\Services\OAuthService;
use Raiolanetworks\OAuth\Tests\Models\TestUser;

it('redirect authenticated users to the homepage', function () {
$mockGuard = Mockery::mock(Guard::class);
$mockGuard->shouldReceive('check')->andReturnTrue();

Auth::shouldReceive('guard')
->with(config('oauth.guard_name'))
->andReturn($mockGuard);

$response = $this->get(route('oauth.request'));
$response->assertRedirect('/');
});

it('redirects unauthenticated users to the OAuth provider', function () {
$mockGuard = Mockery::mock(Guard::class);
$mockGuard->shouldReceive('check')->andReturnFalse();

Auth::shouldReceive('guard')
->with(config('oauth.guard_name'))
->andReturn($mockGuard);

$mockProvider = Mockery::mock(OAuthService::class);
$mockProvider->shouldReceive('getAuthorizationUrl')
->with(['prompt' => 'consent'])
->andReturn('https://example.com/oauth/authorize');

$mockProvider->shouldReceive('getState')
->andReturn('fake_state');

$mockProvider->shouldReceive('getPkceCode')
->andReturn('fake_pkce_code');

$this->instance(OAuthService::class, $mockProvider);

$response = $this->get(route('oauth.request'));

$this->assertEquals('fake_state', Session::get('oauth2-state'));
$this->assertEquals('fake_pkce_code', Session::get('oauth2-pkceCode'));

$response->assertRedirect('https://example.com/oauth/authorize');
});

it('redirect where the user intends to go if authenticated in the callback', function () {
$mockGuard = Mockery::mock(Guard::class);
$mockGuard->shouldReceive('check')->andReturnTrue();

Auth::shouldReceive('guard')
->with(config('oauth.guard_name'))
->andReturn($mockGuard);

$response = $this->get(route('oauth.callback'));
$response->assertRedirect('/');
});

it('handles invalid or missing code in callback', function () {
// Temporal route
Route::get('/login', function () {
return 'Login Page';
})->name(config('oauth.login_route'));

Session::put('oauth2-state', 'correct_state');

$response = $this->get(route('oauth.callback'), [
'state' => 'correct_state',
]);

$response->assertRedirect(route(config('oauth.login_route')))
->assertSessionHas('message', 'Authentication failed. Please try again.');
});

it('handles invalid state in callback', function () {
// Temporal route
Route::get('/login', function () {
return 'Login Page';
})->name(config('oauth.login_route'));

$response = $this->get(route('oauth.callback', [
'code' => 'valid_code',
]));

$response->assertRedirect(route(config('oauth.login_route')))
->assertSessionHas('message', 'Authentication failed. Please try again.');
});

it('logs in the user after a successful OAuth callback', function () {
// Temporal route
Route::get('/login', function () {
return 'Login Page';
})->name(config('oauth.login_route'));

$mockOAuthService = Mockery::mock(OAuthService::class);

$pkceCode = 'valid_pkce_code';
$stateCode = 'valid_state';
$validCode = 'valid_code';

Session::put('oauth2-state', $stateCode);
Session::put('oauth2-pkceCode', $pkceCode);

Config::set('oauth.user_model_name', TestUser::class);

$mockOAuthService->shouldReceive('setPkceCode')
->with($pkceCode)
->once();

$mockAccessToken = Mockery::mock(AccessToken::class);
$mockAccessToken->shouldReceive('getToken')->andReturn('mock_token');
$mockAccessToken->shouldReceive('getRefreshToken')->andReturn('mock_refresh_token');
$mockAccessToken->shouldReceive('getExpires')->andReturn(time() + 3600);

$mockOAuthService->shouldReceive('getAccessToken')
->with('authorization_code', ['code' => $validCode])
->andReturn($mockAccessToken);

$mockOAuthService->shouldReceive('getResourceOwner')
->andReturn(Mockery::mock([
'toArray' => [
'id' => 1,
'name' => 'user',
'email' => '[email protected]',
'groups' => ['admin'],
'sub' => '123456abc',
],
]));

$this->instance(OAuthService::class, $mockOAuthService);

Auth::shouldReceive('guard')
->with(config('oauth.guard_name'))
->andReturn(Mockery::mock(Guard::class, ['login' => null, 'check' => false]));

$response = $this->get(route('oauth.callback', [
'code' => $validCode,
'state' => $stateCode,
]));

$response->assertRedirect(config('oauth.redirect_route_callback_ok'));
});

it('renews the OAuth token if the user is authenticated and the token is expired', function () {
$mockOAuthService = Mockery::mock(OAuthService::class);

// $mockUser = Mockery::mock(\Illuminate\Contracts\Auth\Authenticatable::class);
// $mockUser->oauth_token = 'expired_token';
// $mockUser->oauth_refresh_token = 'valid_refresh_token';
// $mockUser->oauth_token_expires_at = Carbon::now()->subHour()->timestamp;
$mockUser = TestUser::factory(state: [
'oauth_token_expires_at' => Carbon::now()->subHour()
])->create();

Auth::shouldReceive('guard')
->with(config('oauth.guard_name'))
->andReturn(Mockery::mock(Guard::class, ['check' => true, 'user' => $mockUser]));

$newOAuthToken = 'new_oauth_token';
$newRefreshToken = 'new_refresh_token';
$newExpiredDate = Carbon::now()->addHour()->timestamp;

$mockAccessToken = Mockery::mock(AccessToken::class);
$mockAccessToken->shouldReceive('getToken')->andReturn($newOAuthToken);
$mockAccessToken->shouldReceive('getRefreshToken')->andReturn($newRefreshToken);
$mockAccessToken->shouldReceive('getExpires')->andReturn($newExpiredDate);

$mockOAuthService->shouldReceive('getAccessToken')
->with('refresh_token', ['refresh_token' => 'oauth_refresh_token'])
->andReturn($mockAccessToken);

$mockOAuthService->shouldReceive('getResourceOwner')
->andReturn(Mockery::mock(['toArray' => [
'groups' => ['admin'],
]]));

$this->instance(OAuthService::class, $mockOAuthService);

$response = $this->app->make(OAuthController::class)->renew();

expect($mockUser->oauth_token)->toBe($newOAuthToken);
expect($mockUser->oauth_refresh_token)->toBe($newRefreshToken);
expect($mockUser->oauth_token_expires_at)->toBe($newExpiredDate);

expect($response)->toBeNull();
});
27 changes: 26 additions & 1 deletion tests/Models/TestUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,34 @@
namespace Raiolanetworks\OAuth\Tests\Models;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Raiolanetworks\OAuth\Tests\Database\Factories\TestUserFactory;

class TestUser implements Authenticatable
class TestUser extends Model implements Authenticatable
{
use HasFactory;

protected $fillable = [
'email',
'oauth_id',
'name',
'type',
'oauth_token',
'oauth_refresh_token',
'oauth_token_expires_at',
];

/**
* Indicar el path de la factoría personalizada.
*
* @return \Illuminate\Database\Eloquent\Factories\Factory
*/
protected static function newFactory()
{
return TestUserFactory::new();
}

public function getAuthIdentifierName(): string
{
return 'id';
Expand Down
15 changes: 12 additions & 3 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Config;
use Orchestra\Testbench\TestCase as Orchestra;
use Raiolanetworks\OAuth\OAuthServiceProvider;
use Raiolanetworks\OAuth\Tests\Models\TestUser;

class TestCase extends Orchestra
{
Expand All @@ -17,7 +19,15 @@ protected function setUp(): void
{
parent::setUp();

$this->loadMigrationsFrom(__DIR__ . '/database/migrations');
Config::set('oauth.user_model_name', TestUser::class);

// Test migrations
$this->loadMigrationsFrom(realpath(__DIR__ . '/database/migrations'));

// Package migrations
$this->loadMigrationsFrom(realpath(__DIR__ . '/../database/migrations'));

$this->artisan('migrate')->run();
}

protected function getPackageProviders($app)
Expand All @@ -32,8 +42,7 @@ public function getEnvironmentSetUp($app)
config()->set('database.default', 'testing');
config()->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
'database' => ':memory:'
]);
}
}
Loading

0 comments on commit 6ca9531

Please sign in to comment.