181 lines
6.1 KiB
PHP
181 lines
6.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Grav\Plugin\Api\Tests\Unit\Controllers;
|
|
|
|
use Grav\Common\Config\Config;
|
|
use Grav\Common\Grav;
|
|
use Grav\Plugin\Api\Controllers\ConfigController;
|
|
use Grav\Plugin\Api\Exceptions\ValidationException;
|
|
use Grav\Plugin\Api\Tests\Unit\TestHelper;
|
|
use PHPUnit\Framework\Attributes\CoversClass;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* resolveTargetEnv() decides where a config write lands. The interesting
|
|
* cases are the three header states, so we drive it directly via reflection
|
|
* rather than wiring a full update() round-trip.
|
|
*/
|
|
#[CoversClass(ConfigController::class)]
|
|
class ConfigControllerResolveTargetEnvTest extends TestCase
|
|
{
|
|
private ?string $tmp = null;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->tmp = sys_get_temp_dir() . '/grav-cfgctl-' . bin2hex(random_bytes(4));
|
|
mkdir($this->tmp . '/user/config', 0777, true);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
if ($this->tmp !== null) {
|
|
$this->rrmdir($this->tmp);
|
|
$this->tmp = null;
|
|
}
|
|
Grav::resetInstance();
|
|
}
|
|
|
|
#[Test]
|
|
public function missing_header_falls_back_to_active_environment(): void
|
|
{
|
|
// Simulates the bug we're fixing: hostname-derived env folder exists
|
|
// and shadows base config, but admin2 doesn't pass X-Config-Environment.
|
|
mkdir($this->tmp . '/user/localhost/config', 0777, true);
|
|
$controller = $this->buildController(activeEnv: 'localhost');
|
|
|
|
$request = TestHelper::createMockRequest('PATCH', '/config/system');
|
|
|
|
$this->assertSame('localhost', $this->invokeResolveTargetEnv($controller, $request));
|
|
}
|
|
|
|
#[Test]
|
|
public function missing_header_returns_null_when_no_active_env_overlay(): void
|
|
{
|
|
// Active env name is set but has no config dir — base writes are correct.
|
|
$controller = $this->buildController(activeEnv: 'production.example.com');
|
|
|
|
$request = TestHelper::createMockRequest('PATCH', '/config/system');
|
|
|
|
$this->assertNull($this->invokeResolveTargetEnv($controller, $request));
|
|
}
|
|
|
|
#[Test]
|
|
public function empty_header_is_explicit_base_write_and_skips_auto_detect(): void
|
|
{
|
|
// Caller wants to bypass auto-detection — they MUST be able to target
|
|
// base even when a Grav env is active. An explicitly-empty header is
|
|
// the opt-out lever.
|
|
mkdir($this->tmp . '/user/localhost/config', 0777, true);
|
|
$controller = $this->buildController(activeEnv: 'localhost');
|
|
|
|
$request = TestHelper::createMockRequest(
|
|
'PATCH',
|
|
'/config/system',
|
|
['X-Config-Environment' => ''],
|
|
);
|
|
|
|
$this->assertNull($this->invokeResolveTargetEnv($controller, $request));
|
|
}
|
|
|
|
#[Test]
|
|
public function default_sentinel_header_is_explicit_base_write(): void
|
|
{
|
|
// admin-next sends the reserved `default` sentinel for its base
|
|
// ("Default") selection — non-empty so proxies/FPM can't strip it.
|
|
// It must resolve to a base write even when a Grav env is active.
|
|
mkdir($this->tmp . '/user/localhost/config', 0777, true);
|
|
$controller = $this->buildController(activeEnv: 'localhost');
|
|
|
|
$request = TestHelper::createMockRequest(
|
|
'PATCH',
|
|
'/config/system',
|
|
['X-Config-Environment' => 'default'],
|
|
);
|
|
|
|
$this->assertNull($this->invokeResolveTargetEnv($controller, $request));
|
|
}
|
|
|
|
#[Test]
|
|
public function explicit_header_value_wins_over_active_env(): void
|
|
{
|
|
mkdir($this->tmp . '/user/localhost/config', 0777, true);
|
|
mkdir($this->tmp . '/user/env/staging/config', 0777, true);
|
|
$controller = $this->buildController(activeEnv: 'localhost');
|
|
|
|
$request = TestHelper::createMockRequest(
|
|
'PATCH',
|
|
'/config/system',
|
|
['X-Config-Environment' => 'staging'],
|
|
);
|
|
|
|
$this->assertSame('staging', $this->invokeResolveTargetEnv($controller, $request));
|
|
}
|
|
|
|
#[Test]
|
|
public function invalid_header_value_throws(): void
|
|
{
|
|
$controller = $this->buildController(activeEnv: null);
|
|
|
|
$request = TestHelper::createMockRequest(
|
|
'PATCH',
|
|
'/config/system',
|
|
['X-Config-Environment' => '../etc'],
|
|
);
|
|
|
|
$this->expectException(ValidationException::class);
|
|
$this->invokeResolveTargetEnv($controller, $request);
|
|
}
|
|
|
|
private function buildController(?string $activeEnv): ConfigController
|
|
{
|
|
Grav::resetInstance();
|
|
$grav = Grav::instance();
|
|
$grav['locator'] = new CfgCtlFakeLocator($this->tmp);
|
|
if ($activeEnv !== null) {
|
|
$grav['uri'] = new class ($activeEnv) {
|
|
public function __construct(private readonly string $env) {}
|
|
public function environment(): string { return $this->env; }
|
|
};
|
|
}
|
|
return new ConfigController($grav, new Config());
|
|
}
|
|
|
|
private function invokeResolveTargetEnv(ConfigController $controller, object $request): ?string
|
|
{
|
|
$ref = new \ReflectionMethod($controller, 'resolveTargetEnv');
|
|
return $ref->invoke($controller, $request);
|
|
}
|
|
|
|
private function rrmdir(string $path): void
|
|
{
|
|
if (!is_dir($path)) return;
|
|
foreach (new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS),
|
|
\RecursiveIteratorIterator::CHILD_FIRST,
|
|
) as $file) {
|
|
$file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname());
|
|
}
|
|
rmdir($path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* EnvironmentService only ever resolves user://, mirrored from the
|
|
* EnvironmentServiceTest fixture so this test is self-contained.
|
|
*/
|
|
class CfgCtlFakeLocator
|
|
{
|
|
public function __construct(private readonly string $root) {}
|
|
|
|
public function findResource(string $uri, bool $absolute = true, bool $first = false): string|false
|
|
{
|
|
if ($uri === 'user://') {
|
|
return is_dir($this->root . '/user') ? $this->root . '/user' : false;
|
|
}
|
|
return false;
|
|
}
|
|
}
|