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

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

declare(strict_types=1);

namespace Drupal\Tests\config_preview_deploy\Kernel;

use Drupal\node\Entity\NodeType;
use Drupal\config_preview_deploy\ConfigDiff;
use Drupal\config_preview_deploy\ProductionConfigDeployer;
use Drupal\KernelTests\KernelTestBase;

/**
 * Integration tests for configuration operations using real module services.
 *
 * This test class uses the actual ConfigDiff and ProductionConfigDeployer
 * services to ensure we catch issues like missing line numbers in diffs.
 * Tests the complete workflow: generate diff -> undo changes -> apply diff.
 *
 * @group config_preview_deploy
 */
class ConfigDeployerIntegrationTest extends KernelTestBase {

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

  /**
   * The config diff service.
   */
  protected ConfigDiff $configDiff;

  /**
   * The production config deployer service.
   */
  protected ProductionConfigDeployer $productionConfigDeployer;

  /**
   * Active config storage.
   *
   * @var \Drupal\Core\Config\StorageInterface
   */
  protected $activeStorage;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->installEntitySchema('user');
    $this->installEntitySchema('node');
    $this->installConfig(['system', 'user', 'field', 'node']);

    // Mock HTTP_HOST for hash verification.
    $_SERVER['HTTP_HOST'] = 'localhost';

    $this->configDiff = $this->container->get('config_preview_deploy.config_diff');
    $this->productionConfigDeployer = $this->container->get('config_preview_deploy.production_config_deployer');
    $this->activeStorage = $this->container->get('config.storage');

    // 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 integration tests. Install with: apt install patch');
    }
  }

  /**
   * Tests adding configuration values with complete workflow.
   */
  public function testAddConfigurationValuesWorkflow(): void {
    // Step 1: Create base checkpoint.
    $checkpointId = $this->configDiff->createBaseCheckpoint();
    $this->assertNotEmpty($checkpointId, 'Base checkpoint should be created');

    // Step 2: Make configuration changes using config API.
    $originalConfig = $this->activeStorage->read('system.site');
    $siteConfig = $this->config('system.site');
    $siteConfig->set('name', 'Integration Test Site - Modified');
    $siteConfig->set('slogan', 'Added via integration test');
    $siteConfig->save();

    // Step 3: Generate diff using our real service.
    $diff = $this->configDiff->generateDiff();
    $this->assertNotEmpty($diff, 'Diff should be generated');

    // Step 4: Undo changes (revert to original state).
    $this->activeStorage->write('system.site', $originalConfig);
    $verifyOriginal = $this->activeStorage->read('system.site');
    $this->assertEquals($originalConfig, $verifyOriginal, 'Configuration should be reverted');

    // Step 5: Verify diff format is correct (this would catch missing line
    // numbers).
    $this->assertStringContainsString('@@ -2,9 +2,9 @@', $diff, 'Diff should have proper line numbers in hunk header');
    $this->assertMatchesRegularExpression('/^--- a\/system\.site\.yml$/m', $diff, 'Should have proper source file header');
    $this->assertMatchesRegularExpression('/^\+\+\+ b\/system\.site\.yml$/m', $diff, 'Should have proper target file header');
    $this->assertMatchesRegularExpression('/^@@ -\d+,\d+ \+\d+,\d+ @@$/m', $diff, 'Should have proper hunk header with line numbers');

    // Step 6: Test that the diff can be applied (this is fundamental).
    // Use validateDiff to check validation separately.
    $validation = $this->configDiff->validateDiff($diff);

    // Clean up temp storage from validation since we're not using applyDiff.
    if (!empty($validation['temp_dir']) && is_dir($validation['temp_dir'])) {
      $this->container->get('file_system')->deleteRecursive($validation['temp_dir']);
    }

    if (!$validation['valid']) {
      // If validation fails, let's get specific error details.
      $errorDetails = [];
      foreach ($validation['errors'] as $error) {
        $errorDetails[] = $error;
      }

      // Also dump the diff for debugging.
      $diffLines = explode("\n", $diff);
      $this->fail("Diff validation failed. Errors: " . implode('; ', $errorDetails) .
                  "\nDiff content (first 10 lines):\n" . implode("\n", array_slice($diffLines, 0, 10)));
    }

    $this->assertTrue($validation['valid'], 'Diff validation should succeed');
    $this->assertGreaterThan(0, $validation['config_count'], 'Should detect configuration changes');

    // Step 7: Apply the diff and verify it works.
    $results = $this->configDiff->validateAndApplyDiff($diff);
    $this->assertNotEmpty($results, 'Deployment should return results');
    $this->assertArrayHasKey('system.site', $results, 'system.site should be in results');
    $this->assertEquals('update', $results['system.site'], 'Operation should be update');

    // Step 8: Verify the diff was applied correctly.
    $finalConfig = $this->activeStorage->read('system.site');
    $this->assertEquals('Integration Test Site - Modified', $finalConfig['name'], 'Modified name should be applied');
    $this->assertEquals('Added via integration test', $finalConfig['slogan'], 'Added slogan should be applied');

    // Step 9: Verify consistency across service methods.
    $changedConfigs = $this->configDiff->getChangedConfigs();
    $this->assertContains('system.site', $changedConfigs, 'Should detect system.site as changed');
    $this->assertTrue($this->configDiff->hasChanges(), 'Should detect changes exist');
  }

  /**
   * Tests changing configuration values with complete workflow.
   */
  public function testChangeConfigurationWorkflow(): void {
    // Step 1: Create base checkpoint.
    $originalConfig = $this->activeStorage->read('system.site');
    $checkpointId = $this->configDiff->createBaseCheckpoint();
    $this->assertNotEmpty($checkpointId, 'Base checkpoint should be created');

    // Step 2: Make configuration changes using config API.
    $siteConfig = $this->config('system.site');
    $siteConfig->set('name', 'Integration Test Site Name - Modified');
    $siteConfig->set('slogan', 'Modified slogan via integration test');
    $siteConfig->save();

    // Step 3: Generate diff using our real service and verify format.
    $diff = $this->configDiff->generateDiff();
    $this->assertNotEmpty($diff, 'Diff should be generated');

    // Verify proper diff format that would catch missing line numbers.
    $this->assertMatchesRegularExpression('/@@ -\d+,\d+ \+\d+,\d+ @@/', $diff, 'Diff should have proper hunk headers with line numbers');
    $this->assertStringContainsString('Integration Test Site Name - Modified', $diff, 'Diff should contain modified name');
    $this->assertStringContainsString('Modified slogan via integration test', $diff, 'Diff should contain modified slogan');

    // Step 4: Revert changes.
    $this->activeStorage->write('system.site', $originalConfig);

    // Step 5: Apply diff and verify.
    $results = $this->configDiff->validateAndApplyDiff($diff);
    $this->assertArrayHasKey('system.site', $results, 'system.site should be in results');

    $finalConfig = $this->activeStorage->read('system.site');
    $this->assertEquals('Integration Test Site Name - Modified', $finalConfig['name'], 'Modified name should be applied');
    $this->assertEquals('Modified slogan via integration test', $finalConfig['slogan'], 'Modified slogan should be applied');
  }

  /**
   * Tests deleting configuration values with complete workflow.
   */
  public function testDeleteConfigurationValuesWorkflow(): void {
    // Step 1: Add temporary fields to the configuration first.
    $originalConfig = $this->activeStorage->read('system.site');
    $configWithTempFields = $originalConfig;
    $configWithTempFields['temp_field_to_delete'] = 'will be removed';
    $configWithTempFields['another_temp_field'] = 'also removed';
    $this->activeStorage->write('system.site', $configWithTempFields);

    // Step 2: Create base checkpoint (with the temp fields).
    $checkpointId = $this->configDiff->createBaseCheckpoint();
    $this->assertNotEmpty($checkpointId, 'Base checkpoint should be created');

    // Step 3: Make configuration changes (remove the temp fields) using
    // config API.
    $siteConfig = $this->config('system.site');
    $siteConfig->clear('temp_field_to_delete');
    $siteConfig->clear('another_temp_field');
    $siteConfig->save();

    // Step 4: Generate diff using our real service.
    $diff = $this->configDiff->generateDiff();
    $this->assertNotEmpty($diff, 'Diff should be generated');

    // Verify proper diff format.
    $this->assertMatchesRegularExpression('/@@ -\d+,\d+ \+\d+,\d+ @@/', $diff, 'Diff should have proper hunk headers with line numbers');
    $this->assertStringContainsString('-temp_field_to_delete:', $diff, 'Diff should show deleted field');
    $this->assertStringContainsString('-another_temp_field:', $diff, 'Diff should show other deleted field');

    // Step 5: Undo changes (restore the temp fields).
    $this->activeStorage->write('system.site', $configWithTempFields);
    $verifyWithTempFields = $this->activeStorage->read('system.site');
    $this->assertEquals($configWithTempFields, $verifyWithTempFields, 'Configuration should be reverted with temp fields');

    // Step 6: Apply the diff.
    $validation = $this->configDiff->validateDiff($diff);
    $this->assertTrue($validation['valid'], 'Diff validation should succeed');
    $this->assertGreaterThan(0, $validation['config_count'], 'Should detect configuration changes');

    $results = $this->configDiff->validateAndApplyDiff($diff);
    $this->assertArrayHasKey('system.site', $results, 'system.site should be in results');
    $this->assertEquals('update', $results['system.site'], 'Operation should be update');

    // Step 7: Verify the diff was applied correctly (temp fields removed).
    $finalConfig = $this->activeStorage->read('system.site');
    $this->assertArrayNotHasKey('temp_field_to_delete', $finalConfig, 'Deleted field should be removed');
    $this->assertArrayNotHasKey('another_temp_field', $finalConfig, 'Other deleted field should be removed');

    // Verify original fields remained unchanged.
    $this->assertEquals($originalConfig['name'], $finalConfig['name'], 'Original name should be preserved');
    $this->assertEquals($originalConfig['uuid'], $finalConfig['uuid'], 'Original UUID should be preserved');
  }

  /**
   * Tests multiple configuration files with complete workflow.
   */
  public function testMultipleConfigurationFilesWorkflow(): void {
    // Step 1: Create base checkpoint.
    $checkpointId = $this->configDiff->createBaseCheckpoint();
    $this->assertNotEmpty($checkpointId, 'Base checkpoint should be created');

    // Step 2: Make changes to multiple configuration files using config API.
    $originalSiteConfig = $this->activeStorage->read('system.site');
    $originalMaintenanceConfig = $this->activeStorage->read('system.maintenance');

    $siteConfig = $this->config('system.site');
    $siteConfig->set('name', 'Multi-config test site');
    $siteConfig->save();

    $maintenanceConfig = $this->config('system.maintenance');
    $maintenanceConfig->set('message', 'Multi-config test maintenance message');
    $maintenanceConfig->save();

    // Step 3: Generate diff using our real service.
    $diff = $this->configDiff->generateDiff();
    $this->assertNotEmpty($diff, 'Diff should be generated');

    // Verify proper diff format for multiple files.
    $this->assertMatchesRegularExpression('/@@ -\d+,\d+ \+\d+,\d+ @@/', $diff, 'Diff should have proper hunk headers with line numbers');
    $this->assertStringContainsString('--- a/system.site', $diff, 'Diff should contain system.site');
    $this->assertStringContainsString('--- a/system.maintenance', $diff, 'Diff should contain system.maintenance');
    $this->assertStringContainsString('Multi-config test site', $diff, 'Diff should contain site name change');
    $this->assertStringContainsString('Multi-config test maintenance message', $diff, 'Diff should contain maintenance message change');

    // Verify changed config detection.
    $changedConfigs = $this->configDiff->getChangedConfigs();
    $this->assertContains('system.site', $changedConfigs, 'Should detect system.site as changed');
    $this->assertContains('system.maintenance', $changedConfigs, 'Should detect system.maintenance as changed');
    $this->assertEquals(2, count($changedConfigs), 'Should detect exactly 2 changed configs');

    // Step 4: Undo changes.
    $this->activeStorage->write('system.site', $originalSiteConfig);
    $this->activeStorage->write('system.maintenance', $originalMaintenanceConfig);

    // Step 5: Apply the diff.
    $validation = $this->configDiff->validateDiff($diff);
    $this->assertTrue($validation['valid'], 'Diff validation should succeed');
    $this->assertEquals(2, $validation['config_count'], 'Should detect 2 configuration changes');

    $results = $this->configDiff->validateAndApplyDiff($diff);
    $this->assertArrayHasKey('system.site', $results, 'system.site should be in results');
    $this->assertArrayHasKey('system.maintenance', $results, 'system.maintenance should be in results');
    $this->assertEquals('update', $results['system.site'], 'site operation should be update');
    $this->assertEquals('update', $results['system.maintenance'], 'maintenance operation should be update');

    // Step 6: Verify both changes were applied correctly.
    $finalSiteConfig = $this->activeStorage->read('system.site');
    $finalMaintenanceConfig = $this->activeStorage->read('system.maintenance');

    $this->assertEquals('Multi-config test site', $finalSiteConfig['name'], 'Site name should be updated');
    $this->assertEquals('Multi-config test maintenance message', $finalMaintenanceConfig['message'], 'Maintenance message should be updated');
  }

  /**
   * Tests creating and deleting a node type workflow.
   *
   * Uses proper Drupal APIs to create config objects, then tests both
   * adding and deleting operations in the same test.
   */
  public function testCreateAndDeleteNodeTypeWorkflow(): void {
    // Step 1: Create base checkpoint.
    $checkpointId = $this->configDiff->createBaseCheckpoint();
    $this->assertNotEmpty($checkpointId, 'Base checkpoint should be created');

    // Step 2: Create a new node type using proper Drupal APIs.
    $nodeTypeName = 'test_article';

    // Create node type using NodeType entity.
    $nodeType = NodeType::create([
      'type' => $nodeTypeName,
      'name' => 'Test Article',
      'description' => 'A test article type',
      'help' => '',
      'new_revision' => TRUE,
      'preview_mode' => 1,
      'display_submitted' => TRUE,
    ]);
    $nodeType->save();

    // Step 3: Verify changes are detected for creation.
    $hasChanges = $this->configDiff->hasChanges();
    $changedConfigs = $this->configDiff->getChangedConfigs();

    $this->assertTrue($hasChanges, 'Should detect changes for new configs');
    $this->assertContains("node.type.$nodeTypeName", $changedConfigs, 'Should detect new node type');

    // Step 4: Generate diff for creation.
    $createDiff = $this->configDiff->generateDiff();
    $this->assertNotEmpty($createDiff, 'Diff should be generated for new configs');

    // Step 5: Verify diff format for new files.
    // For new files, we expect headers like:
    // --- /dev/null
    // +++ b/node.type.test_article.yml.
    $this->assertStringContainsString('--- /dev/null', $createDiff, 'Should have /dev/null as source for new files');
    $this->assertStringContainsString("+++ b/node.type.$nodeTypeName.yml", $createDiff, 'Should have proper target header for new node type');

    // The diff should contain the actual config content being added.
    $this->assertStringContainsString('+type: test_article', $createDiff, 'Should show added node type');
    $this->assertStringContainsString('+name: \'Test Article\'', $createDiff, 'Should show added node type name');

    // Step 6: Apply the creation diff to test it works.
    // First delete the config to simulate clean state.
    $nodeType->delete();

    $validation = $this->configDiff->validateDiff($createDiff);

    $this->assertTrue($validation['valid'], 'Creation diff validation should succeed');
    $this->assertEquals(1, $validation['config_count'], 'Should detect 1 configuration change');

    $results = $this->configDiff->validateAndApplyDiff($createDiff);
    $this->assertArrayHasKey("node.type.$nodeTypeName", $results, 'Node type should be in results');
    $this->assertEquals('create', $results["node.type.$nodeTypeName"], 'Node type operation should be create');

    // Step 7: Verify the config was recreated correctly.
    $this->assertTrue($this->activeStorage->exists("node.type.$nodeTypeName"), 'Node type should exist after diff application');

    // Step 8: Now test deletion - create new checkpoint and delete the config.
    // First capture the exact config that exists for later restoration.
    $configBeforeDeletion = $this->activeStorage->read("node.type.$nodeTypeName");

    $deleteCheckpointId = $this->configDiff->createBaseCheckpoint();
    $this->assertNotEmpty($deleteCheckpointId, 'Delete checkpoint should be created');

    // Delete the config using proper API.
    $recreatedNodeType = NodeType::load($nodeTypeName);
    $recreatedNodeType->delete();

    // Step 9: Verify deletion changes are detected.
    $hasDeleteChanges = $this->configDiff->hasChanges();
    $deleteChangedConfigs = $this->configDiff->getChangedConfigs();

    $this->assertTrue($hasDeleteChanges, 'Should detect changes for deleted configs');
    $this->assertContains("node.type.$nodeTypeName", $deleteChangedConfigs, 'Should detect deleted node type');

    // Step 10: Generate diff for deletion.
    $deleteDiff = $this->configDiff->generateDiff();
    $this->assertNotEmpty($deleteDiff, 'Diff should be generated for deleted configs');

    // Step 11: Verify diff format for deleted files.
    // For deleted files, we expect headers like:
    // --- a/node.type.test_article.yml
    // +++ /dev/null.
    $this->assertStringContainsString("--- a/node.type.$nodeTypeName.yml", $deleteDiff, 'Should have proper source header for deleted file');
    $this->assertStringContainsString('+++ /dev/null', $deleteDiff, 'Should have /dev/null as target for deleted file');

    // The diff should show the config content being removed.
    $this->assertStringContainsString('-type: test_article', $deleteDiff, 'Should show removed node type');
    $this->assertStringContainsString('-name: \'Test Article\'', $deleteDiff, 'Should show removed node type name');

    // Step 12: Test deploying the deletion diff.
    // First restore the config to simulate having it in production.
    // Use the exact config that was captured before deletion.
    $this->activeStorage->write("node.type.$nodeTypeName", $configBeforeDeletion);

    // Verify it exists before applying deletion diff.
    $this->assertTrue($this->activeStorage->exists("node.type.$nodeTypeName"), 'Node type should exist before deletion diff');

    // Step 13: Apply the deletion diff.
    $deleteValidation = $this->configDiff->validateDiff($deleteDiff);
    $this->assertTrue($deleteValidation['valid'], 'Deletion diff validation should succeed');
    $this->assertEquals(1, $deleteValidation['config_count'], 'Should detect 1 configuration deletion');

    $deleteResults = $this->configDiff->validateAndApplyDiff($deleteDiff);
    $this->assertArrayHasKey("node.type.$nodeTypeName", $deleteResults, 'Node type should be in deletion results');
    $this->assertEquals('delete', $deleteResults["node.type.$nodeTypeName"], 'Node type operation should be delete');

    // Step 14: Verify the config was actually deleted.
    $this->assertFalse($this->activeStorage->exists("node.type.$nodeTypeName"), 'Node type should not exist after deletion diff application');
  }

  /**
   * Tests edge case with no changes.
   */
  public function testNoChangesWorkflow(): void {
    // Step 1: Create base checkpoint.
    $checkpointId = $this->configDiff->createBaseCheckpoint();
    $this->assertNotEmpty($checkpointId, 'Base checkpoint should be created');

    // Step 2: Don't make any changes.
    // Step 3: Generate diff (should be empty).
    $diff = $this->configDiff->generateDiff();
    $this->assertEmpty($diff, 'Diff should be empty when no changes');

    // Step 4: Verify no changes detected.
    $this->assertFalse($this->configDiff->hasChanges(), 'Should report no changes');
    $changedConfigs = $this->configDiff->getChangedConfigs();
    $this->assertEmpty($changedConfigs, 'Should detect no changed configs');
  }

}

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

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