config_preview_deploy-1.0.0-alpha3/tests/src/Kernel/ProductionControllerTest.php

tests/src/Kernel/ProductionControllerTest.php
<?php

declare(strict_types=1);

namespace Drupal\Tests\config_preview_deploy\Kernel;

use Drupal\Component\Serialization\Yaml;
use Drupal\config_preview_deploy\ProductionConfigDeployer;
use Drupal\config_preview_deploy\ConfigVerifier;
use Drupal\Core\Url;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use SebastianBergmann\Diff\Differ;
use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder;
use Symfony\Component\HttpFoundation\Request;

/**
 * Tests the ProductionController endpoints.
 *
 * @group config_preview_deploy
 */
class ProductionControllerTest extends KernelTestBase {

  use UserCreationTrait;

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

  /**
   * The config verifier service.
   */
  protected ConfigVerifier $configVerifier;

  /**
   * The config deployer service.
   */
  protected ProductionConfigDeployer $configDeployer;


  /**
   * The production deployment endpoint path.
   */
  protected string $deployPath = '/admin/config/development/config-preview-deploy/deploy-endpoint';

  /**
   * The production status endpoint path.
   */
  protected string $statusPath = '/api/config-preview-deploy/status';

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

    // Set HTTP_HOST for all tests to match our test configuration.
    $_SERVER['HTTP_HOST'] = 'example.com';

    $this->installConfig(['system', 'config_preview_deploy']);
    $this->installEntitySchema('user');
    $this->installEntitySchema('user_role');

    $this->configVerifier = $this->container->get('config_preview_deploy.config_verifier');
    $this->configDeployer = $this->container->get('config_preview_deploy.production_config_deployer');

    // Fail tests if patch tool is not available.
    $patchTool = $this->container->get('config_preview_deploy.patch_tool');
    if (!$patchTool->isAvailable()) {
      $this->fail('GNU patch tool is required for tests. Install with: apt install patch');
    }

    // Create a user with the required permissions.
    $admin_user = $this->setUpCurrentUser([], ['accept config deployments']);

    // Ensure current user is logged in.
    $this->container->get('current_user')->setAccount($admin_user);

    // Set up basic configuration for testing.
    $this->config('config_preview_deploy.settings')
      ->set('production_url', 'https://example.com')
      ->save();
  }

  /**
   * Creates an authenticated request with session.
   *
   * @param string $path
   *   The request path.
   * @param string $method
   *   The HTTP method.
   * @param mixed $data
   *   The request data to JSON encode.
   *
   * @return \Symfony\Component\HttpFoundation\Request
   *   The configured request.
   */
  protected function createAuthenticatedRequest(string $path, string $method, $data = NULL): Request {
    $session = $this->container->get('session');
    $session->start();

    $content = $data ? json_encode($data) : '';
    $request = Request::create($path, $method, [], [], [], [], $content);

    if ($data) {
      $request->headers->set('Content-Type', 'application/json');
    }

    $request->setSession($session);
    return $request;
  }

  /**
   * Tests the deployment endpoint with valid requests.
   */
  public function testDeployEndpointValid(): void {
    $environment = 'test-env';
    $timestamp = time();
    $serverBasedHash = $this->generateValidAuthHash($timestamp);

    $validDiff = $this->generateValidConfigDiff();

    $requestData = [
      'diff' => $validDiff,
      'environment' => $environment,
      'auth_hash' => $serverBasedHash,
      'timestamp' => $timestamp,
    ];

    $request = $this->createAuthenticatedRequest($this->deployPath, 'POST', $requestData);
    $response = $this->container->get('http_kernel')->handle($request);

    $this->assertEquals(200, $response->getStatusCode(), 'Valid deployment request is successful.');

    $responseData = json_decode($response->getContent(), TRUE);
    $this->assertIsArray($responseData, 'Response is valid JSON array.');
  }

  /**
   * Tests the deployment endpoint with invalid JSON.
   */
  public function testDeployEndpointInvalidJson(): void {
    $session = $this->container->get('session');
    $session->start();

    $request = Request::create($this->deployPath, 'POST', [], [], [], [], 'invalid json');
    $request->headers->set('Content-Type', 'application/json');
    $request->setSession($session);

    $response = $this->container->get('http_kernel')->handle($request);

    $this->assertEquals(400, $response->getStatusCode(), 'Invalid JSON returns 400 status.');

    $responseData = json_decode($response->getContent(), TRUE);
    $this->assertArrayHasKey('error', $responseData, 'Response contains error message.');
    $this->assertStringContainsString('Invalid request format', $responseData['error']);
  }

  /**
   * Tests the deployment endpoint with missing required fields.
   */
  public function testDeployEndpointMissingFields(): void {
    $requestData = [
      'diff' => $this->generateValidConfigDiff(),
      // Missing environment, auth_hash, and timestamp.
    ];

    $request = $this->createAuthenticatedRequest($this->deployPath, 'POST', $requestData);
    $response = $this->container->get('http_kernel')->handle($request);

    $this->assertEquals(400, $response->getStatusCode(), 'Missing fields return 400 status.');

    $responseData = json_decode($response->getContent(), TRUE);
    $this->assertArrayHasKey('error', $responseData, 'Response contains error message.');
    $this->assertStringContainsString('Missing required field', $responseData['error']);
  }

  /**
   * Tests the deployment endpoint with invalid configuration diff.
   */
  public function testDeployEndpointInvalidDiff(): void {
    $environment = 'test-env';
    $timestamp = time();
    $authHash = $this->generateValidAuthHash($timestamp);
    $invalidDiff = 'invalid diff content';

    $requestData = [
      'diff' => $invalidDiff,
      'environment' => $environment,
      'auth_hash' => $authHash,
      'timestamp' => $timestamp,
    ];

    $request = $this->createAuthenticatedRequest($this->deployPath, 'POST', $requestData);
    $response = $this->container->get('http_kernel')->handle($request);

    $this->assertEquals(400, $response->getStatusCode(), 'Invalid diff returns 400 status.');

    $responseData = json_decode($response->getContent(), TRUE);
    $this->assertArrayHasKey('error', $responseData, 'Response contains error message.');
    // The exact error message will depend on what ConfigDiff validation finds
    // wrong. Just verify we get an error message about configuration
    // deployment/validation.
    $this->assertMatchesRegularExpression(
      '/Configuration (deployment|validation) failed/',
      $responseData['error']
    );
  }

  /**
   * Tests the deployment endpoint with invalid authentication hash.
   */
  public function testDeployEndpointInvalidAuth(): void {
    $environment = 'test-env';
    $timestamp = time();
    $invalidAuthHash = 'invalid_hash';
    $validDiff = $this->generateValidConfigDiff();

    $requestData = [
      'diff' => $validDiff,
      'environment' => $environment,
      'auth_hash' => $invalidAuthHash,
      'timestamp' => $timestamp,
    ];

    $request = $this->createAuthenticatedRequest($this->deployPath, 'POST', $requestData);
    $response = $this->container->get('http_kernel')->handle($request);

    $this->assertEquals(403, $response->getStatusCode(), 'Invalid authentication returns 403 status.');

    $responseData = json_decode($response->getContent(), TRUE);
    if (is_array($responseData)) {
      $this->assertArrayHasKey('error', $responseData, 'Response contains error message.');
      $this->assertArrayHasKey('success', $responseData, 'Response contains success field.');
      $this->assertFalse($responseData['success'], 'Success is false for failed authentication.');
    }
  }

  /**
   * Tests deployment endpoint with wrong timestamp in auth hash.
   */
  public function testDeployEndpointWrongTimestamp(): void {
    $environment = 'test-env';
    $correctTimestamp = time();
    // 1 hour ago
    $wrongTimestamp = time() - 3600;

    // Create hash with wrong timestamp but send correct timestamp in request.
    $wrongAuthHash = $this->generateValidAuthHash($wrongTimestamp);
    $validDiff = $this->generateValidConfigDiff();

    $requestData = [
      'diff' => $validDiff,
      'environment' => $environment,
      'auth_hash' => $wrongAuthHash,
    // Correct timestamp but hash was made with wrong one.
      'timestamp' => $correctTimestamp,
    ];

    $request = $this->createAuthenticatedRequest($this->deployPath, 'POST', $requestData);
    $response = $this->container->get('http_kernel')->handle($request);

    $this->assertEquals(403, $response->getStatusCode(), 'Wrong timestamp in auth hash returns 403 status.');

    $responseData = json_decode($response->getContent(), TRUE);
    if (is_array($responseData)) {
      $this->assertArrayHasKey('error', $responseData, 'Response contains error message.');
      $this->assertArrayHasKey('success', $responseData, 'Response contains success field.');
      $this->assertFalse($responseData['success'], 'Success is false for wrong timestamp.');
    }
  }

  /**
   * Tests that deployment actually applies configuration changes.
   *
   * Also creates checkpoint.
   */
  public function testDeploymentActuallyAppliesChanges(): void {
    $environment = 'test-env';
    $timestamp = time();
    $authHash = $this->generateValidAuthHash($timestamp);

    // Get original site name.
    $originalSiteName = $this->config('system.site')->get('name');
    $newSiteName = 'Deployed Test Site ' . rand(1000, 9999);

    // Create a proper diff that changes the site name using sebastian/diff
    // format.
    $activeStorage = $this->container->get('config.storage');
    $systemSiteConfig = $activeStorage->read('system.site');
    $originalYaml = Yaml::encode($systemSiteConfig);

    // Create modified version.
    $modifiedConfig = $systemSiteConfig;
    $modifiedConfig['name'] = $newSiteName;
    $modifiedConfig['slogan'] = 'Deployed via timestamp test';
    $modifiedYaml = Yaml::encode($modifiedConfig);

    // Generate proper unified diff using sebastian/diff.
    $outputBuilder = new UnifiedDiffOutputBuilder(
      "--- a/system.site.yml\n+++ b/system.site.yml\n",
      TRUE
    );
    $differ = new Differ($outputBuilder);
    $configDiff = $differ->diff($originalYaml, $modifiedYaml);

    $requestData = [
      'diff' => $configDiff,
      'environment' => $environment,
      'auth_hash' => $authHash,
      'timestamp' => $timestamp,
    ];

    $request = $this->createAuthenticatedRequest($this->deployPath, 'POST', $requestData);
    $response = $this->container->get('http_kernel')->handle($request);

    $this->assertEquals(200, $response->getStatusCode(), 'Deployment request is successful.');

    $responseData = json_decode($response->getContent(), TRUE);
    $this->assertIsArray($responseData, 'Response is valid JSON array.');
    $this->assertArrayHasKey('success', $responseData, 'Response contains success field.');
    $this->assertTrue($responseData['success'], 'Deployment was successful.');
    $this->assertArrayHasKey('checkpoint_id', $responseData, 'Response contains checkpoint ID.');
    $this->assertArrayHasKey('changes', $responseData, 'Response contains changes information.');

    // Verify the configuration was actually changed.
    $updatedSiteName = $this->config('system.site')->get('name');
    $this->assertEquals($newSiteName, $updatedSiteName, 'Site name was updated correctly.');
    $this->assertNotEquals($originalSiteName, $updatedSiteName, 'Site name changed from original.');

    // Verify a checkpoint ID was returned (we can't easily verify the
    // checkpoint itself in this test)
    $checkpointId = $responseData['checkpoint_id'];
    $this->assertNotEmpty($checkpointId, 'Checkpoint ID is not empty.');
    $this->assertIsString($checkpointId, 'Checkpoint ID is a string.');

    // Verify changes contain our config.
    $this->assertArrayHasKey('system.site', $responseData['changes'], 'Changes include system.site configuration.');
    $this->assertEquals('update', $responseData['changes']['system.site'], 'system.site was updated successfully.');
  }

  /**
   * Generates a valid authentication hash for testing.
   *
   * @param int $timestamp
   *   The timestamp to include in the hash.
   *
   * @return string
   *   The generated hash.
   */
  protected function generateValidAuthHash(int $timestamp): string {
    // Get the host from Drupal's base URL to match verification logic.
    $currentUrl = Url::fromRoute('system.admin', [], ['absolute' => TRUE])->toString();
    $productionHost = parse_url($currentUrl, PHP_URL_HOST);
    $hashVerification = $this->container->get('config_preview_deploy.hash_verification');
    return $hashVerification->generateVerificationHash($productionHost, $timestamp);
  }

  /**
   * Generates a valid configuration diff for testing.
   */
  protected function generateValidConfigDiff(): string {
    $activeStorage = $this->container->get('config.storage');

    // Get current system.site configuration.
    $systemSiteConfig = $activeStorage->read('system.site');
    $originalYaml = Yaml::encode($systemSiteConfig);

    // Create modified version.
    $modifiedConfig = $systemSiteConfig;
    $modifiedConfig['name'] = 'Test Deployed Site';
    $modifiedConfig['slogan'] = 'Deployed via config diff';
    $modifiedYaml = Yaml::encode($modifiedConfig);

    // Generate diff using sebastian/diff like our working integration tests.
    $outputBuilder = new UnifiedDiffOutputBuilder(
      "--- a/system.site.yml\n+++ b/system.site.yml\n",
      TRUE
    );
    $differ = new Differ($outputBuilder);
    return $differ->diff($originalYaml, $modifiedYaml);
  }

  /**
   * Tests the status endpoint with missing parameters.
   */
  public function testStatusEndpointMissingParameters(): void {
    // Test with no parameters at all.
    $request = Request::create($this->statusPath, 'GET');
    $response = $this->container->get('http_kernel')->handle($request);

    $this->assertEquals(403, $response->getStatusCode(), 'Missing all parameters returns 403 status.');

    $responseData = json_decode($response->getContent(), TRUE);
    $this->assertIsArray($responseData, 'Response is valid JSON array.');
    $this->assertArrayHasKey('error', $responseData, 'Response contains error message.');
    $this->assertEquals('Missing required parameters', $responseData['error'], 'Correct error message for missing parameters.');

    // Test with only timestamp parameter.
    $request = Request::create($this->statusPath . '?timestamp=' . time(), 'GET');
    $response = $this->container->get('http_kernel')->handle($request);

    $this->assertEquals(403, $response->getStatusCode(), 'Missing token parameter returns 403 status.');

    $responseData = json_decode($response->getContent(), TRUE);
    $this->assertIsArray($responseData, 'Response is valid JSON array.');
    $this->assertArrayHasKey('error', $responseData, 'Response contains error message.');
    $this->assertEquals('Missing required parameters', $responseData['error'], 'Correct error message for missing token.');

    // Test with only token parameter.
    $request = Request::create($this->statusPath . '?token=test-token', 'GET');
    $response = $this->container->get('http_kernel')->handle($request);

    $this->assertEquals(403, $response->getStatusCode(), 'Missing timestamp parameter returns 403 status.');

    $responseData = json_decode($response->getContent(), TRUE);
    $this->assertIsArray($responseData, 'Response is valid JSON array.');
    $this->assertArrayHasKey('error', $responseData, 'Response contains error message.');
    $this->assertEquals('Missing required parameters', $responseData['error'], 'Correct error message for missing timestamp.');
  }

}

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

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