Skip to content

Commit

Permalink
Add tests for login controller (#7)
Browse files Browse the repository at this point in the history
* Update LoginResponse to implement LoginResponseInterface
* Updated test namespace
* Overwritten OpenIDConnectClient redirect function to use Laravel abort method
* Login tests
* Add response test
  • Loading branch information
ricklambrechts authored Mar 31, 2023
1 parent 7fdff54 commit cbb4e9f
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 3 deletions.
3 changes: 1 addition & 2 deletions src/Http/Responses/LoginResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@

namespace MinVWS\OpenIDConnectLaravel\Http\Responses;

use Illuminate\Contracts\Support\Responsable;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class LoginResponse implements Responsable
class LoginResponse implements LoginResponseInterface
{
public function __construct(
protected object $userInfo
Expand Down
13 changes: 13 additions & 0 deletions src/OpenIDConnectClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace MinVWS\OpenIDConnectLaravel;

use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Jumbojett\OpenIDConnectClient as BaseOpenIDConnectClient;
Expand Down Expand Up @@ -108,4 +109,16 @@ protected function getWellKnownConfigValue($param, $default = null): string|arra

return $config->{$param};
}

/**
* Overwrite the redirect method to use Laravel's abort method.
* Sometimes the error 'Cannot modify header information - headers already sent' was thrown.
* By using Laravel's abort method, this error is prevented.
* @param string $url
* @return void
*/
public function redirect($url): void
{
App::abort(302, '', ['Location' => $url]);
}
}
104 changes: 104 additions & 0 deletions tests/Feature/Http/Controllers/LoginControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

declare(strict_types=1);

namespace MinVWS\OpenIDConnectLaravel\Tests\Feature\Http\Controllers;

use Illuminate\Support\Facades\Http;
use MinVWS\OpenIDConnectLaravel\OpenIDConfiguration\OpenIDConfiguration;
use MinVWS\OpenIDConnectLaravel\OpenIDConfiguration\OpenIDConfigurationLoader;
use MinVWS\OpenIDConnectLaravel\OpenIDConnectClient;
use MinVWS\OpenIDConnectLaravel\Tests\TestCase;
use Mockery;

class LoginControllerTest extends TestCase
{
public function setUp(): void
{
parent::setUp();

// Support for running tests with Laravel 8
if (method_exists(Http::class, 'preventStrayRequests')) {
Http::preventStrayRequests();
}
}

public function testLoginRouteRedirectsToAuthorizeUrlOfProvider(): void
{
$this->mockOpenIDConfigurationLoader();

config()->set('oidc.client_id', 'test-client-id');

$response = $this->get(route('oidc.login'));
$response
->assertStatus(302)
->assertRedirectContains("https://provider.rdobeheer.nl/authorize")
->assertRedirectContains('test-client-id');
}

public function testLoginRouteReturnsUserInfoWitchMockedClient(): void
{
$mockClient = Mockery::mock(OpenIDConnectClient::class);
$mockClient
->shouldReceive('authenticate')
->once();

$mockClient
->shouldReceive('requestUserInfo')
->andReturn((object) [
'sub' => 'test-sub',
'name' => 'test-name',
'email' => 'test-email',
]);

$this->app->instance(OpenIDConnectClient::class, $mockClient);

$response = $this->get(route('oidc.login'));
$response
->assertJson([
'userInfo' => [
'sub' => 'test-sub',
'name' => 'test-name',
'email' => 'test-email',
],
]);
}

protected function mockOpenIDConfigurationLoader(): void
{
$mock = Mockery::mock(OpenIDConfigurationLoader::class);
$mock
->shouldReceive('getConfiguration')
->andReturn($this->exampleOpenIDConfiguration());

$this->app->instance(OpenIDConfigurationLoader::class, $mock);
}

protected function exampleOpenIDConfiguration(): OpenIDConfiguration
{
return new OpenIDConfiguration(
version: "3.0",
tokenEndpointAuthMethodsSupported: ["none"],
claimsParameterSupported: true,
requestParameterSupported: false,
requestUriParameterSupported: true,
requireRequestUriRegistration: false,
grantTypesSupported: ["authorization_code"],
frontchannelLogoutSupported: false,
frontchannelLogoutSessionSupported: false,
backchannelLogoutSupported: false,
backchannelLogoutSessionSupported: false,
issuer: "https://provider.rdobeheer.nl",
authorizationEndpoint: "https://provider.rdobeheer.nl/authorize",
jwksUri: "https://provider.rdobeheer.nl/jwks",
tokenEndpoint: "https://provider.rdobeheer.nl/token",
scopesSupported: ["openid"],
responseTypesSupported: ["code"],
responseModesSupported: ["query"],
subjectTypesSupported: ["pairwise"],
idTokenSigningAlgValuesSupported: ["RS256"],
userinfoEndpoint: "https://provider.rdobeheer.nl/userinfo",
codeChallengeMethodsSupported: ["S256"],
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace MinVWS\OpenIDConnectLaravel\Tests\Feature;
namespace MinVWS\OpenIDConnectLaravel\Tests\Feature\OpenIDConfiguration;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
Expand Down
204 changes: 204 additions & 0 deletions tests/Unit/Http/Controllers/LoginControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<?php

declare(strict_types=1);

namespace MinVWS\OpenIDConnectLaravel\Tests\Unit\Http\Controllers;

use Exception;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Http\Request;
use Jumbojett\OpenIDConnectClientException;
use MinVWS\OpenIDConnectLaravel\Http\Controllers\LoginController;
use MinVWS\OpenIDConnectLaravel\Http\Responses\LoginResponse;
use MinVWS\OpenIDConnectLaravel\Http\Responses\LoginResponseInterface;
use MinVWS\OpenIDConnectLaravel\OpenIDConnectClient;
use MinVWS\OpenIDConnectLaravel\Services\JWE\JweDecryptException;
use MinVWS\OpenIDConnectLaravel\Services\OpenIDConnectExceptionHandler;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use PHPUnit\Framework\TestCase;

class LoginControllerTest extends TestCase
{
use MockeryPHPUnitIntegration;

protected function setUp(): void
{
parent::setUp();

// Bind the LoginResponseInterface to the LoginResponse class
app()->bind(LoginResponseInterface::class, LoginResponse::class);
}

protected function tearDown(): void
{
// Flush so the LoginResponseInterface binding is removed
app()->flush();

parent::tearDown();
}

public function testLoginControllerCanBeCreated(): void
{
$loginController = new LoginController(
new OpenIDConnectClient(),
new OpenIDConnectExceptionHandler(),
);
$this->assertInstanceOf(LoginController::class, $loginController);
}

public function testExceptionHandlerIsCalledWhenAuthenticateThrowsException(): void
{
$mockClient = Mockery::mock(OpenIDConnectClient::class);
$mockClient
->shouldReceive('authenticate')
->andThrow(OpenIDConnectClientException::class);

$mockExceptionHandler = Mockery::mock(OpenIDConnectExceptionHandler::class);
$mockExceptionHandler
->shouldReceive('handleExceptionWhileAuthenticate')
->once();

$loginController = new LoginController(
$mockClient,
$mockExceptionHandler,
);

$loginController->__invoke();
}

public function testExceptionHandlerIsCalledWhenRequestUserInfoDoesNotReturnAnObject(): void
{
$mockClient = Mockery::mock(OpenIDConnectClient::class);
$mockClient->shouldReceive('authenticate')->once();
$mockClient
->shouldReceive('requestUserInfo')
->andReturn('not an object')
->once();

$mockExceptionHandler = Mockery::mock(OpenIDConnectExceptionHandler::class);
$mockExceptionHandler
->shouldReceive('handleExceptionWhileRequestUserInfo')
->withArgs(function (OpenIDConnectClientException $e) {
return $e->getMessage() === 'Received user info is not an object';
})
->once();

$loginController = new LoginController(
$mockClient,
$mockExceptionHandler,
);

$loginController->__invoke();
}

public function testExceptionHandlerIsCalledWhenRequestUserInfoThrowsAnException(): void
{
$mockClient = Mockery::mock(OpenIDConnectClient::class);
$mockClient->shouldReceive('authenticate')->once();
$mockClient
->shouldReceive('requestUserInfo')
->andThrow(OpenIDConnectClientException::class, 'Something went wrong')
->once();

$mockExceptionHandler = Mockery::mock(OpenIDConnectExceptionHandler::class);
$mockExceptionHandler
->shouldReceive('handleExceptionWhileRequestUserInfo')
->withArgs(function (OpenIDConnectClientException $e) {
return $e->getMessage() === 'Something went wrong';
})
->once();

$loginController = new LoginController(
$mockClient,
$mockExceptionHandler,
);

$loginController->__invoke();
}

public function testExceptionHandlerIsCalledWhenRequestUserInfoThrowsAnJweDecryptException(): void
{
$mockClient = Mockery::mock(OpenIDConnectClient::class);
$mockClient->shouldReceive('authenticate')->once();
$mockClient
->shouldReceive('requestUserInfo')
->andThrow(JweDecryptException::class, 'Something went wrong')
->once();

$mockExceptionHandler = Mockery::mock(OpenIDConnectExceptionHandler::class);
$mockExceptionHandler
->shouldReceive('handleException')
->withArgs(function (Exception $e) {
return $e->getMessage() === 'Something went wrong';
})
->once();

$loginController = new LoginController(
$mockClient,
$mockExceptionHandler,
);

$loginController->__invoke();
}

public function testLoginResponseIsReturnedWithUserInfo(): void
{
$mockClient = Mockery::mock(OpenIDConnectClient::class);
$mockClient->shouldReceive('authenticate')->once();
$mockClient
->shouldReceive('requestUserInfo')
->andReturn($this->exampleUserInfo())
->once();

$mockExceptionHandler = Mockery::mock(OpenIDConnectExceptionHandler::class);

$loginController = new LoginController(
$mockClient,
$mockExceptionHandler,
);

$response = $loginController->__invoke();

$this->assertInstanceOf(LoginResponseInterface::class, $response);
$this->assertInstanceOf(Responsable::class, $response);
}

public function testUserInfoIsReturned(): void
{
$mockClient = Mockery::mock(OpenIDConnectClient::class);
$mockClient->shouldReceive('authenticate')->once();
$mockClient
->shouldReceive('requestUserInfo')
->andReturn($this->exampleUserInfo())
->once();

$mockExceptionHandler = Mockery::mock(OpenIDConnectExceptionHandler::class);

$loginController = new LoginController(
$mockClient,
$mockExceptionHandler,
);

$loginResponse = $loginController->__invoke();
$response = $loginResponse->toResponse(Mockery::mock(Request::class));

$this->assertSame(json_encode([
'userInfo' => $this->exampleUserInfo(),
]), $response->getContent());
}

protected function exampleUserInfo(): object
{
return (object) [
'sub' => '1234567890',
'name' => 'John Doe',
'given_name' => 'John',
'family_name' => 'Doe',
'middle_name' => 'Middle',
'nickname' => 'JD',
'preferred_username' => 'johndoe',
'email' => '',
];
}
}

0 comments on commit cbb4e9f

Please sign in to comment.