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'));
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc