root = sys_get_temp_dir().'/kant-environment-'.bin2hex(random_bytes(6)); mkdir($this->root); file_put_contents($this->root.'/.env.example', "APP_ENV=prod\nDATABASE_URL=sqlite:///default.db\nMAILER_DSN=null://null\n"); } protected function tearDown(): void { $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->root, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST); foreach ($files as $file) { $file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname()); } rmdir($this->root); } public function testNewInstallationUsesOnlyEnvAndKeepsSecretAcrossRequests(): void { Environment::prepare($this->root); $secret = Environment::ensureSecret($this->root); Environment::write($this->root, ['DATABASE_URL' => 'sqlite:///customer.db']); Environment::prepare($this->root); self::assertSame($secret, Environment::ensureSecret($this->root)); self::assertSame('sqlite:///customer.db', $this->values()['DATABASE_URL']); self::assertFileDoesNotExist($this->root.'/.env.local'); } public function testLegacySettingsAreConsolidatedOnceWithBothOriginalsBackedUp(): void { Environment::prepare($this->root); $legacy = "DATABASE_URL=sqlite:///live.db\nAPP_SECRET=existing-secret\nCUSTOM_OPTION=kept\n"; file_put_contents($this->root.'/.env.local', $legacy); Environment::prepare($this->root); self::assertSame('sqlite:///live.db', $this->values()['DATABASE_URL']); self::assertSame('existing-secret', Environment::ensureSecret($this->root)); self::assertSame('kept', $this->values()['CUSTOM_OPTION']); self::assertFileDoesNotExist($this->root.'/.env.local'); $backups = glob($this->root.'/var/backups/environment-*/.env.local'); self::assertCount(1, $backups); self::assertSame($legacy, file_get_contents($backups[0])); Environment::write($this->root, ['DATABASE_URL' => 'sqlite:///chosen.db']); Environment::prepare($this->root); self::assertSame('sqlite:///chosen.db', $this->values()['DATABASE_URL'], 'No legacy override after editing .env.'); self::assertCount(1, glob($this->root.'/var/backups/environment-*')); } public function testSecretsAndCustomSettingsRoundTripWithoutInterpolation(): void { Environment::prepare($this->root); $value = 'a$NOT_AN_ENV_VALUE"b\\c'; Environment::write($this->root, ['APP_SECRET' => $value, 'CUSTOM_OPTION' => "one\ntwo"]); Environment::write($this->root, ['SITE_ORIGIN' => 'https://example.org']); self::assertSame($value, $this->values()['APP_SECRET']); self::assertSame("one\ntwo", $this->values()['CUSTOM_OPTION']); self::assertSame('null://null', $this->values()['MAILER_DSN']); } public function testFailedMigrationPreservesBothOriginalFiles(): void { Environment::prepare($this->root); $before = file_get_contents($this->root.'/.env'); $legacy = "APP_SECRET=preserved\nDATABASE_URL=sqlite:///live.db\n"; file_put_contents($this->root.'/.env.local', $legacy); unlink($this->root.'/.env.lock'); mkdir($this->root.'/.env.lock'); // An unwritable lock target, reproducible on Windows/Linux. try { Environment::prepare($this->root); self::fail('Migration must report failure.'); } catch (\RuntimeException $error) { self::assertStringContainsString('Schreibrechte', $error->getMessage()); } self::assertSame($before, file_get_contents($this->root.'/.env')); self::assertSame($legacy, file_get_contents($this->root.'/.env.local')); } public function testBootstrapIgnoresOtherOverlaysButHonorsExplicitServerVariables(): void { mkdir($this->root.'/config'); mkdir($this->root.'/vendor'); copy(dirname(__DIR__).'/config/bootstrap.php', $this->root.'/config/bootstrap.php'); file_put_contents($this->root.'/vendor/autoload.php', 'root.'/.env', "APP_ENV=prod\nAPP_DEBUG=0\nAPP_SECRET=fixture-secret\nDATABASE_URL=sqlite:///chosen.db\n"); file_put_contents($this->root.'/.env.prod', "DATABASE_URL=sqlite:///wrong.db\n"); file_put_contents($this->root.'/.env.prod.local', "DATABASE_URL=sqlite:///also-wrong.db\n"); file_put_contents($this->root.'/.env.local.php', ' "sqlite:///cached-wrong.db"];'); file_put_contents($this->root.'/boot.php', 'root.'/boot.php'], [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $this->root, $environment); self::assertIsResource($process); $output = stream_get_contents($pipes[1]); $error = stream_get_contents($pipes[2]); fclose($pipes[1]); fclose($pipes[2]); self::assertSame(0, proc_close($process), $error); self::assertSame($override ?? 'sqlite:///chosen.db', $output); self::assertFileDoesNotExist($this->root.'/.env.local'); } } private function values(): array { return (new Dotenv())->parse((string) file_get_contents($this->root.'/.env')); } }