config_preview_deploy-1.0.0-alpha3/tests/src/Kernel/OAuthControllerTest.php
tests/src/Kernel/OAuthControllerTest.php
<?php
declare(strict_types=1);
namespace Drupal\Tests\config_preview_deploy\Kernel;
use Drupal\config_preview_deploy\Controller\OAuthController;
use Drupal\consumers\Entity\Consumer;
use Drupal\key\Entity\Key;
use Drupal\KernelTests\KernelTestBase;
use Drupal\simple_oauth\Entity\Oauth2Scope;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
/**
* Tests the OAuth controller.
*
* @group config_preview_deploy
*/
class OAuthControllerTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'field',
'file',
'image',
'text',
'options',
'serialization',
'key',
'consumers',
'simple_oauth',
'config_preview_deploy',
];
/**
* The OAuth controller.
*/
protected OAuthController $controller;
/**
* The OAuth consumer entity.
*/
protected Consumer $consumer;
/**
* The deployment key entity.
*/
protected Key $deploymentKey;
/**
* Mock HTTP client.
*/
protected Client $mockHttpClient;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Install entity schemas.
$this->installEntitySchema('user');
$this->installEntitySchema('consumer');
$this->installEntitySchema('oauth2_scope');
$this->installEntitySchema('key');
// Install module configuration.
$this->installConfig(['system', 'user', 'config_preview_deploy']);
// Create OAuth2 scope entity.
$scope = Oauth2Scope::create([
'name' => 'config_preview_deploy',
'description' => 'Test scope for config deployment',
'grant_types' => [
'authorization_code' => [
'status' => TRUE,
'description' => 'Authorization code flow',
],
],
'umbrella' => FALSE,
'granularity_id' => 'permission',
'granularity_configuration' => [
'permission' => 'accept config deployments',
],
]);
$scope->save();
// Create OAuth consumer for testing.
$this->consumer = Consumer::create([
'label' => 'Config Preview Deploy Test',
'client_id' => 'config_preview_deploy',
'secret' => 'test-client-secret-12345',
'redirect' => [
'https://preview.example.com/admin/config/development/config-preview-deploy/oauth/callback',
],
'scopes' => ['config_preview_deploy'],
'is_default' => FALSE,
'third_party' => FALSE,
'confidential' => TRUE,
'automatic_authorization' => FALSE,
'remember_approval' => FALSE,
'grant_types' => ['authorization_code'],
'description' => 'OAuth consumer for testing.',
]);
$this->consumer->save();
// Create deployment key.
$this->deploymentKey = Key::create([
'id' => 'config_deploy_secret',
'label' => 'Configuration Deployment Secret',
'description' => 'Secret for testing',
'key_type' => 'authentication',
'key_provider' => 'config',
'key_provider_settings' => [],
]);
$this->deploymentKey->save();
$this->deploymentKey->setKeyValue('test-client-secret-12345');
$this->deploymentKey->save();
// Set production URL configuration.
$this->config('config_preview_deploy.settings')
->set('production_url', 'https://production.example.com')
->save();
// Create controller instance with container.
$this->controller = OAuthController::create($this->container);
// Setup default mock HTTP client.
$this->setupMockHttpClient([]);
}
/**
* Sets up a mock HTTP client with predefined responses.
*/
protected function setupMockHttpClient(array $responses): void {
$mock = new MockHandler($responses);
$handlerStack = HandlerStack::create($mock);
$this->mockHttpClient = new Client(['handler' => $handlerStack]);
// Replace the HTTP client service.
$this->container->set('http_client', $this->mockHttpClient);
// Recreate controller to use new HTTP client.
$this->controller = OAuthController::create($this->container);
}
/**
* Tests successful OAuth callback with token exchange.
*/
public function testSuccessfulCallback(): void {
// Mock successful token exchange response.
$this->setupMockHttpClient([
new Response(200, ['Content-Type' => 'application/json'], json_encode([
'access_token' => 'test-access-token-12345',
'token_type' => 'Bearer',
'expires_in' => 3600,
'scope' => 'config_preview_deploy',
])),
]);
// Store expected state in tempstore.
$expected_state = 'test-state-12345';
\Drupal::service('tempstore.private')->get('config_preview_deploy')
->set('oauth_state', $expected_state);
// Create request with callback parameters.
$request = SymfonyRequest::create('/oauth/callback', 'GET', [
'code' => 'test-authorization-code',
'state' => $expected_state,
]);
// Execute callback.
$response = $this->controller->callback($request);
// Verify redirect to deployment form.
$this->assertEquals(302, $response->getStatusCode());
$this->assertStringContainsString('/admin/config/development/config-preview-deploy', $response->getTargetUrl());
// Verify access token stored in tempstore.
$tempstore = \Drupal::service('tempstore.private')->get('config_preview_deploy');
$this->assertEquals('test-access-token-12345', $tempstore->get('access_token'));
$this->assertEquals('Bearer', $tempstore->get('token_type'));
$this->assertEquals(3600, $tempstore->get('expires_in'));
$this->assertIsInt($tempstore->get('token_received_at'));
}
/**
* Tests various OAuth callback error scenarios.
*/
public function testCallbackErrorScenarios(): void {
// Test 1: OAuth error parameters.
$request = SymfonyRequest::create('/oauth/callback', 'GET', [
'error' => 'access_denied',
'error_description' => 'The resource owner denied the request',
'state' => 'test-state-12345',
]);
$response = $this->controller->callback($request);
$this->assertEquals(302, $response->getStatusCode());
$this->assertStringContainsString('/admin/config/development/config-preview-deploy', $response->getTargetUrl());
$messages = \Drupal::messenger()->messagesByType('error');
$this->assertCount(1, $messages);
$this->assertStringContainsString('OAuth authorization failed: access_denied', (string) $messages[0]);
// Clear messages for next test.
\Drupal::messenger()->deleteAll();
// Test 2: Missing required parameters.
$request = SymfonyRequest::create('/oauth/callback', 'GET', [
'state' => 'test-state-12345',
]);
$response = $this->controller->callback($request);
$this->assertEquals(302, $response->getStatusCode());
$this->assertStringContainsString('/admin/config/development/config-preview-deploy', $response->getTargetUrl());
$messages = \Drupal::messenger()->messagesByType('error');
$this->assertCount(1, $messages);
$this->assertStringContainsString('Invalid OAuth callback - missing authorization code', (string) $messages[0]);
// Clear messages for next test.
\Drupal::messenger()->deleteAll();
// Test 3: Invalid state parameter (CSRF protection).
\Drupal::service('tempstore.private')->get('config_preview_deploy')
->set('oauth_state', 'expected-state-12345');
$request = SymfonyRequest::create('/oauth/callback', 'GET', [
'code' => 'test-authorization-code',
'state' => 'invalid-state-67890',
]);
$response = $this->controller->callback($request);
$this->assertEquals(302, $response->getStatusCode());
$this->assertStringContainsString('/admin/config/development/config-preview-deploy', $response->getTargetUrl());
$messages = \Drupal::messenger()->messagesByType('error');
$this->assertCount(1, $messages);
$this->assertStringContainsString('Invalid OAuth state parameter - possible CSRF attack', (string) $messages[0]);
// Clear messages for next test.
\Drupal::messenger()->deleteAll();
// Test 4: Missing client secret.
// Remove the deployment key to simulate missing client secret.
$this->deploymentKey->delete();
$request = SymfonyRequest::create('/oauth/callback', 'GET', [
'code' => 'test-authorization-code',
'state' => 'expected-state-12345',
]);
$response = $this->controller->callback($request);
$this->assertEquals(302, $response->getStatusCode());
$this->assertStringContainsString('/admin/config/development/config-preview-deploy', $response->getTargetUrl());
$messages = \Drupal::messenger()->messagesByType('error');
$this->assertCount(1, $messages);
$this->assertStringContainsString('OAuth authorization failed due to technical error', (string) $messages[0]);
}
/**
* Tests token exchange error scenarios.
*/
public function testTokenExchangeErrors(): void {
// Store expected state in tempstore.
$expected_state = 'test-state-12345';
\Drupal::service('tempstore.private')->get('config_preview_deploy')
->set('oauth_state', $expected_state);
// Test 1: HTTP error during token exchange.
$this->setupMockHttpClient([
new RequestException('Connection timeout', new Request('POST', 'test')),
]);
$request = SymfonyRequest::create('/oauth/callback', 'GET', [
'code' => 'test-authorization-code',
'state' => $expected_state,
]);
$response = $this->controller->callback($request);
$this->assertEquals(302, $response->getStatusCode());
$this->assertStringContainsString('/admin/config/development/config-preview-deploy', $response->getTargetUrl());
$messages = \Drupal::messenger()->messagesByType('error');
$this->assertCount(1, $messages);
$this->assertStringContainsString('OAuth authorization failed due to technical error', (string) $messages[0]);
// Clear messages for next test.
\Drupal::messenger()->deleteAll();
// Test 2: Invalid token response format.
$this->setupMockHttpClient([
new Response(200, ['Content-Type' => 'application/json'], json_encode([
'error' => 'invalid_grant',
'error_description' => 'The authorization code is invalid',
])),
]);
$response = $this->controller->callback($request);
$this->assertEquals(302, $response->getStatusCode());
$this->assertStringContainsString('/admin/config/development/config-preview-deploy', $response->getTargetUrl());
$messages = \Drupal::messenger()->messagesByType('error');
$this->assertCount(1, $messages);
$this->assertStringContainsString('Failed to exchange authorization code for access token', (string) $messages[0]);
}
/**
* Tests successful authorization redirect.
*/
public function testSuccessfulAuthorize(): void {
// Create a request object.
$request = SymfonyRequest::create('/api/config-preview-deploy/oauth/authorize');
// Execute authorize method.
$response = $this->controller->authorize($request);
// Verify it's a redirect response.
$this->assertEquals(302, $response->getStatusCode());
// Verify redirect URL structure.
$target_url = $response->getTargetUrl();
$this->assertStringStartsWith('https://production.example.com/oauth/authorize?', $target_url);
// Parse query parameters.
$parsed_url = parse_url($target_url);
parse_str($parsed_url['query'], $params);
// Verify required OAuth parameters.
$this->assertEquals('code', $params['response_type']);
$this->assertEquals('config_preview_deploy', $params['client_id']);
$this->assertEquals('config_preview_deploy', $params['scope']);
$this->assertArrayHasKey('state', $params);
$this->assertArrayHasKey('redirect_uri', $params);
// Verify state was stored in tempstore.
$stored_state = \Drupal::service('tempstore.private')->get('config_preview_deploy')
->get('oauth_state');
$this->assertEquals($params['state'], $stored_state);
}
/**
* Tests authorization failure scenarios.
*/
public function testAuthorizationFailures(): void {
// Test 1: Authorization without production URL.
$this->config('config_preview_deploy.settings')
->clear('production_url')
->save();
// Create a request object.
$request = SymfonyRequest::create('/api/config-preview-deploy/oauth/authorize');
$response = $this->controller->authorize($request);
$this->assertEquals(302, $response->getStatusCode());
$this->assertStringContainsString('/admin/config/development/config-preview-deploy', $response->getTargetUrl());
$messages = \Drupal::messenger()->messagesByType('error');
$this->assertCount(1, $messages);
$this->assertStringContainsString('Production URL not configured', (string) $messages[0]);
// Clear messages and restore config for next test.
\Drupal::messenger()->deleteAll();
$this->config('config_preview_deploy.settings')
->set('production_url', 'https://production.example.com')
->save();
// Test 2: Authorization without OAuth consumer.
$this->consumer->delete();
// Create another request object.
$request2 = SymfonyRequest::create('/api/config-preview-deploy/oauth/authorize');
$response = $this->controller->authorize($request2);
$this->assertEquals(302, $response->getStatusCode());
$this->assertStringContainsString('/admin/config/development/config-preview-deploy', $response->getTargetUrl());
$messages = \Drupal::messenger()->messagesByType('error');
$this->assertCount(1, $messages);
$this->assertStringContainsString('OAuth consumer not found', (string) $messages[0]);
}
/**
* Tests token exchange request format validation.
*/
public function testTokenExchangeRequestFormat(): void {
// Mock successful token exchange to capture the request.
$this->setupMockHttpClient([
new Response(200, ['Content-Type' => 'application/json'], json_encode([
'access_token' => 'test-access-token-12345',
'token_type' => 'Bearer',
'expires_in' => 3600,
'scope' => 'config_preview_deploy',
])),
]);
// Store expected state in tempstore.
$expected_state = 'test-state-12345';
\Drupal::service('tempstore.private')->get('config_preview_deploy')
->set('oauth_state', $expected_state);
// Create request with callback parameters.
$request = SymfonyRequest::create('/oauth/callback', 'GET', [
'code' => 'test-authorization-code',
'state' => $expected_state,
]);
// Execute callback.
$response = $this->controller->callback($request);
// Verify successful completion.
$this->assertEquals(302, $response->getStatusCode());
$this->assertStringContainsString('/admin/config/development/config-preview-deploy', $response->getTargetUrl());
// Verify access token stored correctly.
$tempstore = \Drupal::service('tempstore.private')->get('config_preview_deploy');
$this->assertEquals('test-access-token-12345', $tempstore->get('access_token'));
$this->assertEquals('Bearer', $tempstore->get('token_type'));
$this->assertEquals(3600, $tempstore->get('expires_in'));
}
}
