config_preview_deploy-1.0.0-alpha3/tests/src/Kernel/ConfigRebaserTest.php
tests/src/Kernel/ConfigRebaserTest.php
<?php
declare(strict_types=1);
namespace Drupal\Tests\config_preview_deploy\Kernel;
use Drupal\Core\Archiver\ArchiveTar;
use Drupal\config_preview_deploy\ConfigRebaser;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the ConfigRebaser service.
*
* @group config_preview_deploy
*/
class ConfigRebaserTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['config_preview_deploy', 'system', 'config'];
/**
* The config rebaser service.
*
* @var \Drupal\config_preview_deploy\ConfigRebaser
*/
protected ConfigRebaser $configRebaser;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['system']);
$this->configRebaser = $this->container->get('config_preview_deploy.config_rebase');
$this->fileSystem = $this->container->get('file_system');
}
/**
* Tests error handling for invalid tarball paths.
*/
public function testRebaseWithInvalidTarball(): void {
// Test 1: Non-existent tarball should be handled gracefully.
$nonExistentPath = '/path/that/does/not/exist.tar.gz';
try {
$result = $this->configRebaser->rebase($nonExistentPath);
// This should throw an exception before reaching here.
$this->fail('Expected exception was not thrown for non-existent tarball');
}
catch (\Exception $e) {
// Expected - verify error message is informative.
$this->assertIsString($e->getMessage());
$this->assertNotEmpty($e->getMessage());
}
}
/**
* Tests simple rebase without any preview changes.
*/
public function testRebaseWithoutChanges(): void {
// Enable additional modules for more services.
$this->enableModules(['user', 'node']);
// Get required services.
$configDiff = $this->container->get('config_preview_deploy.config_diff');
// Step 1: Start with config state.
$this->config('system.site')
->set('name', 'Production Site')
->set('mail', 'admin@example.com')
->set('slogan', 'Production Slogan')
->save();
// Step 2: Export current config to tarball (this represents production).
$configExporter = $this->container->get('config_preview_deploy.config_export');
$tarballContent = $configExporter->exportConfigTarball();
// Save to a temporary file.
$exportDir = $this->fileSystem->getTempDirectory() . '/export-' . uniqid();
$this->fileSystem->mkdir($exportDir);
$tarballPath = $exportDir . '/production-config.tar.gz';
file_put_contents($tarballPath, $tarballContent);
$this->assertFileExists($tarballPath, 'Export tarball should be created');
// Step 3: Change config to simulate preview environment.
$this->config('system.site')
->set('name', 'Preview Site')
->save();
// Step 4: Initialize preview environment checkpoint.
$configDiff->createBaseCheckpoint();
// Step 5: Verify no changes from checkpoint (since we just created it).
$this->assertFalse($configDiff->hasChanges(), 'Should have no changes from checkpoint');
// Step 6: Perform rebase with production tarball.
$result = $this->configRebaser->rebase($tarballPath);
// Step 7: Verify rebase was successful.
$this->assertTrue($result['success'], 'Rebase should succeed');
$this->assertEmpty($result['conflicts'], 'Should have no conflicts');
// Clear caches to ensure config is loaded fresh.
\Drupal::configFactory()->reset();
drupal_flush_all_caches();
// Get fresh config instance.
$siteConfig = \Drupal::configFactory()->get('system.site');
// Verify production values were imported.
$this->assertEquals('Production Site', $siteConfig->get('name'),
'Production site name should be imported');
$this->assertEquals('Production Slogan', $siteConfig->get('slogan'),
'Production slogan should be imported');
// Clean up.
$this->fileSystem->deleteRecursive($exportDir);
}
/**
* Tests complete rebase workflow.
*
* This integration test verifies that:
* 1. Production config is imported during rebase
* 2. Preview changes that can apply cleanly are preserved.
*
* Note: Due to the patch-based implementation, only changes that don't
* conflict with the imported production config will be preserved.
*/
public function testRebaseIntegrationWorkflow(): void {
// Enable additional modules for more services.
$this->enableModules(['user', 'node']);
// Get required services.
$configDiff = $this->container->get('config_preview_deploy.config_diff');
// Step 1: Start with base config state (common base).
$this->config('system.site')
->set('name', 'Common Base Site')
->set('mail', 'admin@example.com')
->set('slogan', 'Welcome')
->set('page.front', '/node')
->save();
// Step 2: Simulate production environment changes.
// Production updates the site name.
$this->config('system.site')
->set('name', 'Production Site')
->save();
// Export production config to tarball.
$configExporter = $this->container->get('config_preview_deploy.config_export');
$tarballContent = $configExporter->exportConfigTarball();
// Save to a temporary file.
$exportDir = $this->fileSystem->getTempDirectory() . '/export-' . uniqid();
$this->fileSystem->mkdir($exportDir);
$tarballPath = $exportDir . '/production-config.tar.gz';
file_put_contents($tarballPath, $tarballContent);
$this->assertFileExists($tarballPath, 'Export tarball should be created');
// Step 3: Reset back to common base state to simulate preview environment.
// This should be reverted back to the production state after rebase.
$this->config('system.site')
->set('name', 'Common Base Site')
->save();
// Initialize preview environment checkpoint with common base.
$configDiff->createBaseCheckpoint();
// Step 4: Make preview environment changes.
// Also change a different config file to avoid conflicts.
$this->config('system.performance')
->set('cache.page.max_age', 3600)
->save();
// Verify preview has changes.
$this->assertTrue($configDiff->hasChanges(), 'Preview should have changes');
$diff = $configDiff->generateDiff();
$this->assertStringNotContainsString('system.site', $diff, 'Diff should contain system.site');
$this->assertStringContainsString('system.performance', $diff, 'Diff should contain system.performance');
// Step 5: Perform rebase with production tarball.
$result = $this->configRebaser->rebase($tarballPath);
// Step 6: Verify rebase was successful.
if (!$result['success']) {
$this->fail('Rebase failed: ' . $result['message'] . ' - Conflicts: ' . json_encode($result['conflicts']));
}
$this->assertTrue($result['success'], 'Rebase should succeed');
$this->assertEmpty($result['conflicts'], 'Should have no conflicts');
// Clear caches to ensure config is loaded fresh.
\Drupal::configFactory()->reset();
drupal_flush_all_caches();
// Get fresh config instance.
$siteConfig = \Drupal::configFactory()->get('system.site');
// Verify site name was updated to production value.
// The preview change from 'Common Base Site' to 'Preview Site' is lost,
// and production value 'Production Site' is applied.
$this->assertEquals('Production Site', $siteConfig->get('name'),
'Site name should be updated to production value');
// Verify other unchanged fields remain the same.
$this->assertEquals('admin@example.com', $siteConfig->get('mail'),
'Site mail should remain unchanged');
$this->assertEquals('Welcome', $siteConfig->get('slogan'),
'Site slogan should remain unchanged');
// Verify non-conflicting change was preserved.
// (preview-specific change in different config file).
$performanceConfig = \Drupal::configFactory()->get('system.performance');
$this->assertEquals(3600, $performanceConfig->get('cache.page.max_age'),
'Preview performance config change should be preserved');
// Clean up.
$this->fileSystem->deleteRecursive($exportDir);
}
/**
* Tests rebase with conflicts.
*
* This test covers:
* 1. Create conflicting changes in production and preview
* 2. Attempt rebase
* 3. Verify conflicts are detected and reported.
*/
public function testRebaseWithConflicts(): void {
// Enable additional modules.
$this->enableModules(['user', 'node']);
// Get required services.
$configDiff = $this->container->get('config_preview_deploy.config_diff');
// Step 1: Create production state and export.
$this->config('system.site')
->set('name', 'Production Updated Site')
->set('slogan', 'Production New Slogan')
->set('page.403', '/production-403')
->set('page.404', '/production-404')
->set('page.front', '/production-home')
->set('mail', 'production@example.com')
->save();
// Export complete configuration.
$configExporter = $this->container->get('config_preview_deploy.config_export');
$tarballContent = $configExporter->exportConfigTarball();
// Save to a temporary file.
$exportDir = $this->fileSystem->getTempDirectory() . '/export-conflict-' . uniqid();
$this->fileSystem->mkdir($exportDir);
$tarballPath = $exportDir . '/production-conflict.tar.gz';
file_put_contents($tarballPath, $tarballContent);
$this->assertFileExists($tarballPath);
// Assert the production tarball contains the expected values.
$extractDir = $exportDir . '/verify';
$this->fileSystem->mkdir($extractDir);
$archiver = new ArchiveTar($tarballPath, 'gz');
$archiver->extract($extractDir);
$siteConfigYaml = file_get_contents($extractDir . '/system.site.yml');
$this->assertStringContainsString("name: 'Production Updated Site'", $siteConfigYaml,
'Production tarball should contain Production Updated Site name');
// Step 2: Reset to base state.
$this->config('system.site')
->set('name', 'Base Site')
->set('slogan', '')
->set('mail', 'admin@example.com')
->set('page.403', '')
->set('page.404', '')
->set('page.front', '/node')
->save();
// Step 3: Initialize base checkpoint AFTER setting base state.
$configDiff->createBaseCheckpoint();
// Step 4: Make conflicting changes in preview.
// Change the same fields that production changed to different values.
// Also change additional fields to ensure patch context conflicts.
$this->config('system.site')
->set('name', 'Preview Different Site')
->set('slogan', 'Preview Different Slogan')
->set('mail', 'preview@example.com')
->set('page.403', '/preview-403')
->set('page.404', '/preview-404')
->set('page.front', '/preview-home')
->save();
// Verify preview has changes.
$this->assertTrue($configDiff->hasChanges(), 'Config diff should detect changes');
$diff = $configDiff->generateDiff();
$this->assertNotEmpty($diff, 'Generated diff should not be empty');
// Assert the diff is correctly between base state and preview.
// It should show changes FROM base TO preview.
$this->assertStringContainsString('-name: \'Base Site\'', $diff);
$this->assertStringContainsString('+name: \'Preview Different Site\'', $diff);
$this->assertStringContainsString('-slogan: \'\'', $diff);
$this->assertStringContainsString('+slogan: \'Preview Different Slogan\'', $diff);
$this->assertStringContainsString('-mail: admin@example.com', $diff);
$this->assertStringContainsString('+mail: preview@example.com', $diff);
// Flush all caches before rebase.
\Drupal::configFactory()->reset();
drupal_flush_all_caches();
// Step 5: Attempt rebase.
// Get a fresh instance of the rebaser service after cache clear.
$freshRebaser = $this->container->get('config_preview_deploy.config_rebase');
$result = $freshRebaser->rebase($tarballPath);
// Step 6: Verify conflicts were detected.
// When both production and preview change the same fields,
// the patch should fail to apply.
$this->assertFalse($result['success'], 'Rebase should fail due to conflicts');
$this->assertNotEmpty($result['conflicts'], 'Should have conflicts');
$this->assertStringContainsString('conflicts', (string) $result['message']);
// Step 7: Verify conflicts file is created.
$this->assertNotEmpty($result['conflicts_file'], 'Conflicts file path should be provided');
$this->assertFileExists($result['conflicts_file'], 'Conflicts file should be created');
// Read the conflicts file.
$conflictsDiff = file_get_contents($result['conflicts_file']);
// Verify it contains our preview changes.
$this->assertStringContainsString('Preview Different Site', $conflictsDiff);
$this->assertStringContainsString('Preview Different Slogan', $conflictsDiff);
// Step 8: Verify production config was imported.
// The rebase imports production config but fails to re-apply
// preview changes.
// Clear caches.
\Drupal::configFactory()->reset();
drupal_flush_all_caches();
$siteConfig = \Drupal::configFactory()->get('system.site');
$this->assertEquals('Production Updated Site', $siteConfig->get('name'),
'Production config should be imported');
$this->assertEquals('Production New Slogan', $siteConfig->get('slogan'),
'Production slogan should be imported');
// Preview-only change should be lost due to conflict.
$this->assertNotEquals('/preview-home', $siteConfig->get('page.front'),
'Preview-only changes may be lost during conflict');
// Clean up.
$this->fileSystem->deleteRecursive($exportDir);
}
}
