utilikit-1.0.0/tests/src/Functional/UtilikitAjaxControllerTest.php

tests/src/Functional/UtilikitAjaxControllerTest.php
<?php

declare(strict_types=1);

namespace Drupal\Tests\utilikit\Functional;

use GuzzleHttp\Promise\Utils;
use Drupal\Core\Url;
use Drupal\Tests\utilikit\Traits\UtilikitTestHelpers;
use Drupal\utilikit\Service\UtilikitConstants;

/**
 * Tests the UtilikitAjaxController functionality.
 *
 * This test class covers AJAX endpoints for CSS updates and button rendering,
 * including security features like rate limiting, CSRF protection, access
 * control, and error handling. Tests cover both inline and static rendering
 * modes with comprehensive validation of JSON responses.
 *
 * @group utilikit
 * @group utilikit_functional
 *
 * @coversDefaultClass \Drupal\utilikit\Controller\UtilikitAjaxController
 */
class UtilikitAjaxControllerTest extends UtilikitFunctionalTestBase {

  use UtilikitTestHelpers;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'utilikit',
    'node',
    'field',
    'text',
    'user',
    'system',
  ];

  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'stark';

  /**
   * Test user with appropriate permissions.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $webUser;

  /**
   * Test user without required permissions.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $unauthorizedUser;

  /**
   * The update CSS endpoint URL.
   *
   * @var \Drupal\Core\Url
   */
  protected $updateCssUrl;

  /**
   * The render button endpoint URL.
   *
   * @var \Drupal\Core\Url
   */
  protected $renderButtonUrl;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    // Create content type for testing.
    $this->drupalCreateContentType(['type' => 'article']);

    // Create test users with different permissions.
    $this->webUser = $this->drupalCreateUser([
      'access content',
      'create article content',
      'edit own article content',
    ]);

    $this->unauthorizedUser = $this->drupalCreateUser([
      'access content',
    ]);

    // Set up endpoint URLs.
    $this->updateCssUrl = Url::fromRoute('utilikit.update_css');
    $this->renderButtonUrl = Url::fromRoute('utilikit.render_button');

    // Configure UtiliKit for testing.
    $this->config('utilikit.settings')
      ->set('rendering_mode', 'static')
      ->set('scope', 'all')
      ->set('cache_enabled', TRUE)
      ->save();
  }

  /**
   * Tests updateCss endpoint with valid AJAX request in static mode.
   */
  public function testUpdateCssValidRequestStaticMode(): void {
    $this->drupalLogin($this->webUser);

    // Create test classes.
    $testClasses = [
      'uk-pd--20',
      'uk-mg--10',
      'uk-bg--ff0000',
    ];

    // Get CSRF token.
    $csrfToken = $this->getSession()->getPage()->find('css', 'meta[name="csrf-token"]');

    // Make AJAX request.
    $response = $this->drupalPostJson($this->updateCssUrl->toString(), [
      'classes' => $testClasses,
    ], [
      'X-CSRF-Token' => $csrfToken ? $csrfToken->getAttribute('content') : '',
    ]);

    $this->assertNotNull($response);
    $data = json_decode($response, TRUE);

    // Verify response structure.
    $this->assertArrayHasKey('status', $data);
    $this->assertArrayHasKey('message', $data);
    $this->assertArrayHasKey('mode', $data);
    $this->assertArrayHasKey('classes_processed', $data);

    // Verify response data.
    $this->assertEquals('success', $data['status']);
    $this->assertEquals('static', $data['mode']);
    $this->assertEquals(3, $data['classes_processed']);
    $this->assertStringContainsString('processed', $data['message']);

    // Verify CSS file URL is provided in static mode.
    $this->assertArrayHasKey('css_url', $data);
    $this->assertNotEmpty($data['css_url']);
  }

  /**
   * Tests updateCss endpoint with valid AJAX request in inline mode.
   */
  public function testUpdateCssValidRequestInlineMode(): void {
    // Switch to inline mode.
    $this->config('utilikit.settings')
      ->set('rendering_mode', 'inline')
      ->save();

    $this->drupalLogin($this->webUser);

    $testClasses = ['uk-pd--30', 'uk-mg--20'];

    // Get CSRF token.
    $csrfToken = $this->getSession()->getPage()->find('css', 'meta[name="csrf-token"]');

    $response = $this->drupalPostJson($this->updateCssUrl->toString(), [
      'classes' => $testClasses,
    ], [
      'X-CSRF-Token' => $csrfToken ? $csrfToken->getAttribute('content') : '',
    ]);

    $data = json_decode($response, TRUE);

    $this->assertEquals('success', $data['status']);
    $this->assertEquals('inline', $data['mode']);
    $this->assertEquals(2, $data['classes_processed']);

    // In inline mode, CSS URL should not be provided.
    $this->assertArrayNotHasKey('css_url', $data);
  }

  /**
   * Tests updateCss endpoint with empty classes array.
   */
  public function testUpdateCssEmptyClasses(): void {
    $this->drupalLogin($this->webUser);

    $response = $this->drupalPostJson($this->updateCssUrl->toString(), [
      'classes' => [],
    ]);

    $data = json_decode($response, TRUE);

    $this->assertEquals('error', $data['status']);
    $this->assertStringContainsString('No classes provided', $data['message']);
  }

  /**
   * Tests updateCss endpoint with invalid classes.
   */
  public function testUpdateCssInvalidClasses(): void {
    $this->drupalLogin($this->webUser);

    $invalidClasses = [
      'not-a-utility-class',
      'uk-invalid',
      'random-class',
    ];

    $response = $this->drupalPostJson($this->updateCssUrl->toString(), [
      'classes' => $invalidClasses,
    ]);

    $data = json_decode($response, TRUE);

    // Should still succeed but process 0 valid classes.
    $this->assertEquals('success', $data['status']);
    $this->assertEquals(0, $data['classes_processed']);
  }

  /**
   * Tests updateCss endpoint rate limiting.
   */
  public function testUpdateCssRateLimit(): void {
    $this->drupalLogin($this->webUser);

    $testClasses = ['uk-pd--20'];
    $maxRequests = UtilikitConstants::RATE_LIMIT_REQUESTS_PER_MINUTE;

    // Make requests up to the limit.
    for ($i = 0; $i < $maxRequests; $i++) {
      $response = $this->drupalPostJson($this->updateCssUrl->toString(), [
        'classes' => $testClasses,
      ]);

      $data = json_decode($response, TRUE);
      $this->assertEquals('success', $data['status']);
    }

    // Next request should be rate limited.
    $response = $this->drupalPostJson($this->updateCssUrl->toString(), [
      'classes' => $testClasses,
    ]);

    $data = json_decode($response, TRUE);
    $this->assertEquals('locked', $data['status']);
    $this->assertArrayHasKey('retry_after', $data);
    $this->assertStringContainsString('Rate limit', $data['message']);
  }

  /**
   * Tests updateCss endpoint with too many classes.
   */
  public function testUpdateCssTooManyClasses(): void {
    $this->drupalLogin($this->webUser);

    // Create array exceeding max classes limit.
    $tooManyClasses = [];
    for ($i = 0; $i <= UtilikitConstants::MAX_CLASSES_PER_REQUEST; $i++) {
      $tooManyClasses[] = 'uk-pd--' . $i;
    }

    $response = $this->drupalPostJson($this->updateCssUrl->toString(), [
      'classes' => $tooManyClasses,
    ]);

    $data = json_decode($response, TRUE);
    $this->assertEquals('error', $data['status']);
    $this->assertStringContainsString('Too many classes', $data['message']);
  }

  /**
   * Tests updateCss endpoint without CSRF token.
   */
  public function testUpdateCssNoCsrfToken(): void {
    $this->drupalLogin($this->webUser);

    // Make request without CSRF token header.
    $client = $this->getHttpClient();
    $response = $client->post($this->updateCssUrl->toString(), [
      'json' => ['classes' => ['uk-pd--20']],
      'http_errors' => FALSE,
    ]);

    $this->assertEquals(403, $response->getStatusCode());
  }

  /**
   * Tests updateCss endpoint with unauthorized user.
   */
  public function testUpdateCssUnauthorized(): void {
    $this->drupalLogin($this->unauthorizedUser);

    $client = $this->getHttpClient();
    $response = $client->post($this->updateCssUrl->toString(), [
      'json' => ['classes' => ['uk-pd--20']],
      'http_errors' => FALSE,
    ]);

    $this->assertEquals(403, $response->getStatusCode());
  }

  /**
   * Tests updateCss endpoint as anonymous user.
   */
  public function testUpdateCssAnonymous(): void {
    $this->drupalLogout();

    $client = $this->getHttpClient();
    $response = $client->post($this->updateCssUrl->toString(), [
      'json' => ['classes' => ['uk-pd--20']],
      'http_errors' => FALSE,
    ]);

    $this->assertEquals(403, $response->getStatusCode());
  }

  /**
   * Tests renderButton endpoint success.
   */
  public function testRenderButtonSuccess(): void {
    $this->drupalLogin($this->webUser);

    $response = $this->drupalGet($this->renderButtonUrl->toString(), [
      'query' => ['ajax_form' => 1],
    ]);

    $data = json_decode($response, TRUE);

    $this->assertArrayHasKey('status', $data);
    $this->assertArrayHasKey('html', $data);
    $this->assertEquals('success', $data['status']);
    $this->assertStringContainsString('utilikit-update-button', $data['html']);
    $this->assertStringContainsString('button', $data['html']);
  }

  /**
   * Tests renderButton endpoint caching.
   */
  public function testRenderButtonCaching(): void {
    $this->drupalLogin($this->webUser);

    // First request.
    $response1 = $this->drupalGet($this->renderButtonUrl->toString(), [
      'query' => ['ajax_form' => 1],
    ]);
    $data1 = json_decode($response1, TRUE);

    // Second request should be cached.
    $response2 = $this->drupalGet($this->renderButtonUrl->toString(), [
      'query' => ['ajax_form' => 1],
    ]);
    $data2 = json_decode($response2, TRUE);

    // HTML should be identical.
    $this->assertEquals($data1['html'], $data2['html']);

    // Check cache headers.
    $this->assertSession()->responseHeaderContains('X-Drupal-Cache', 'HIT');
  }

  /**
   * Tests AJAX response handling for malformed requests.
   */
  public function testMalformedRequest(): void {
    $this->drupalLogin($this->webUser);

    // Send malformed JSON.
    $client = $this->getHttpClient();
    $response = $client->post($this->updateCssUrl->toString(), [
      'body' => 'not-json-data',
      'headers' => [
        'Content-Type' => 'application/json',
        'X-CSRF-Token' => $this->getCsrfToken(),
      ],
      'http_errors' => FALSE,
    ]);

    $this->assertEquals(400, $response->getStatusCode());
  }

  /**
   * Tests concurrent request handling.
   */
  public function testConcurrentRequests(): void {
    $this->drupalLogin($this->webUser);

    // Simulate multiple concurrent requests.
    $promises = [];
    $client = $this->getHttpClient();

    for ($i = 0; $i < 5; $i++) {
      $promises[] = $client->postAsync($this->updateCssUrl->toString(), [
        'json' => ['classes' => ['uk-pd--' . $i]],
        'headers' => ['X-CSRF-Token' => $this->getCsrfToken()],
      ]);
    }

    // Wait for all requests to complete.
    $responses = Utils::settle($promises)->wait();

    // Verify all succeeded.
    foreach ($responses as $response) {
      $this->assertEquals('fulfilled', $response['state']);
      $data = json_decode($response['value']->getBody()->getContents(), TRUE);
      $this->assertEquals('success', $data['status']);
    }
  }

  /**
   * Tests error logging on CSS generation failure.
   */
  public function testCssGenerationFailure(): void {
    $this->drupalLogin($this->webUser);

    // Mock a service to throw exception.
    $this->container->set('utilikit.service_provider', new class() {

      /**
       * Processes new classes and throws an exception for testing.
       *
       * @param array $classes
       *   The classes to process.
       *
       * @throws \Exception
       *   Always throws an exception for testing purposes.
       */
      public function processNewClasses($classes) {
        throw new \Exception('Test exception');
      }

    });

    $response = $this->drupalPostJson($this->updateCssUrl->toString(), [
      'classes' => ['uk-pd--20'],
    ]);

    $data = json_decode($response, TRUE);
    $this->assertEquals('error', $data['status']);
    $this->assertStringContainsString('Failed to process', $data['message']);
  }

  /**
   * Helper to get CSRF token.
   *
   * @return string
   *   The CSRF token.
   */
  protected function getCsrfToken(): string {
    $token = \Drupal::csrfToken()->get('utilikit-update-css');
    return $token;
  }

}

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

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