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