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