diff --git a/composer.json b/composer.json index 4631cb0..b603d54 100644 --- a/composer.json +++ b/composer.json @@ -70,7 +70,8 @@ }, "autoload-dev": { "psr-4": { - "App\\Tests\\": "tests/" + "App\\Tests\\": "tests/", + "Codeception\\Module\\Symfony\\": "tests/Support/Codeception/Module/Symfony/" } }, "replace": { diff --git a/composer.lock b/composer.lock index 7cc8603..b82eaa6 100644 --- a/composer.lock +++ b/composer.lock @@ -5682,12 +5682,12 @@ "source": { "type": "git", "url": "https://github.com/Codeception/module-symfony.git", - "reference": "784f1b034ba70686b1ac766c2664f1b1f2b1d638" + "reference": "a516dc6fc04db11d9d65f8de36297babf8fc16de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-symfony/zipball/784f1b034ba70686b1ac766c2664f1b1f2b1d638", - "reference": "784f1b034ba70686b1ac766c2664f1b1f2b1d638", + "url": "https://api.github.com/repos/Codeception/module-symfony/zipball/a516dc6fc04db11d9d65f8de36297babf8fc16de", + "reference": "a516dc6fc04db11d9d65f8de36297babf8fc16de", "shasum": "" }, "require": { @@ -5699,35 +5699,37 @@ "require-dev": { "codeception/module-asserts": "^3.0", "codeception/module-doctrine": "^3.1", - "doctrine/orm": "^2.20", - "symfony/browser-kit": "^5.4 | ^6.4 | ^7.2", - "symfony/cache": "^5.4 | ^6.4 | ^7.2", - "symfony/config": "^5.4 | ^6.4 | ^7.2", - "symfony/dependency-injection": "^5.4 | ^6.4 | ^7.2", - "symfony/dom-crawler": "^5.4 | ^6.4 | ^7.2", - "symfony/dotenv": "^5.4 | ^6.4 | ^7.2", - "symfony/error-handler": "^5.4 | ^6.4 | ^7.2", - "symfony/filesystem": "^5.4 | ^6.4 | ^7.2", - "symfony/form": "^5.4 | ^6.4 | ^7.2", - "symfony/framework-bundle": "^5.4 | ^6.4 | ^7.2", - "symfony/http-client": "^5.4 | ^6.4 | ^7.2", - "symfony/http-foundation": "^5.4 | ^6.4 | ^7.2", - "symfony/http-kernel": "^5.4 | ^6.4 | ^7.2", - "symfony/mailer": "^5.4 | ^6.4 | ^7.2", - "symfony/mime": "^5.4 | ^6.4 | ^7.2", - "symfony/notifier": "^5.4 | ^6.4 | ^7.2", - "symfony/options-resolver": "^5.4 | ^6.4 | ^7.2", - "symfony/property-access": "^5.4 | ^6.4 | ^7.2", - "symfony/property-info": "^5.4 | ^6.4 | ^7.2", - "symfony/routing": "^5.4 | ^6.4 | ^7.2", - "symfony/security-bundle": "^5.4 | ^6.4 | ^7.2", - "symfony/security-core": "^5.4 | ^6.4 | ^7.2", - "symfony/security-csrf": "^5.4 | ^6.4 | ^7.2", - "symfony/security-http": "^5.4 | ^6.4 | ^7.2", - "symfony/translation": "^5.4 | ^6.4 | ^7.2", - "symfony/twig-bundle": "^5.4 | ^6.4 | ^7.2", - "symfony/validator": "^5.4 | ^6.4 | ^7.2", - "symfony/var-exporter": "^5.4 | ^6.4 | ^7.2", + "doctrine/orm": "^3.5", + "friendsofphp/php-cs-fixer": "^3.85", + "phpstan/phpstan": "^2.1", + "symfony/browser-kit": "^5.4 | ^6.4 | ^7.3", + "symfony/cache": "^5.4 | ^6.4 | ^7.3", + "symfony/config": "^5.4 | ^6.4 | ^7.3", + "symfony/dependency-injection": "^5.4 | ^6.4 | ^7.3", + "symfony/dom-crawler": "^5.4 | ^6.4 | ^7.3", + "symfony/dotenv": "^5.4 | ^6.4 | ^7.3", + "symfony/error-handler": "^5.4 | ^6.4 | ^7.3", + "symfony/filesystem": "^5.4 | ^6.4 | ^7.3", + "symfony/form": "^5.4 | ^6.4 | ^7.3", + "symfony/framework-bundle": "^5.4 | ^6.4 | ^7.3", + "symfony/http-client": "^5.4 | ^6.4 | ^7.3", + "symfony/http-foundation": "^5.4 | ^6.4 | ^7.3", + "symfony/http-kernel": "^5.4 | ^6.4 | ^7.3", + "symfony/mailer": "^5.4 | ^6.4 | ^7.3", + "symfony/mime": "^5.4 | ^6.4 | ^7.3", + "symfony/notifier": "^5.4 | ^6.4 | ^7.3", + "symfony/options-resolver": "^5.4 | ^6.4 | ^7.3", + "symfony/property-access": "^5.4 | ^6.4 | ^7.3", + "symfony/property-info": "^5.4 | ^6.4 | ^7.3", + "symfony/routing": "^5.4 | ^6.4 | ^7.3", + "symfony/security-bundle": "^5.4 | ^6.4 | ^7.3", + "symfony/security-core": "^5.4 | ^6.4 | ^7.3", + "symfony/security-csrf": "^5.4 | ^6.4 | ^7.3", + "symfony/security-http": "^5.4 | ^6.4 | ^7.3", + "symfony/translation": "^5.4 | ^6.4 | ^7.3", + "symfony/twig-bundle": "^5.4 | ^6.4 | ^7.3", + "symfony/validator": "^5.4 | ^6.4 | ^7.3", + "symfony/var-exporter": "^5.4 | ^6.4 | ^7.3", "vlucas/phpdotenv": "^4.2 | ^5.4" }, "suggest": { @@ -5737,9 +5739,9 @@ "default-branch": true, "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Codeception\\": "src/Codeception/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5766,7 +5768,7 @@ "issues": "https://github.com/Codeception/module-symfony/issues", "source": "https://github.com/Codeception/module-symfony/tree/main" }, - "time": "2025-08-03T21:05:47+00:00" + "time": "2025-08-09T19:27:12+00:00" }, { "name": "codeception/stub", @@ -6453,16 +6455,16 @@ }, { "name": "doctrine/orm", - "version": "3.5.1", + "version": "3.5.2", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "64444dcfd511089d526cd2c7f74b9d7ed583bdfc" + "reference": "5a541b8b3a327ab1ea5f93b1615b4ff67a34e109" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/64444dcfd511089d526cd2c7f74b9d7ed583bdfc", - "reference": "64444dcfd511089d526cd2c7f74b9d7ed583bdfc", + "url": "https://api.github.com/repos/doctrine/orm/zipball/5a541b8b3a327ab1ea5f93b1615b4ff67a34e109", + "reference": "5a541b8b3a327ab1ea5f93b1615b4ff67a34e109", "shasum": "" }, "require": { @@ -6537,9 +6539,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/3.5.1" + "source": "https://github.com/doctrine/orm/tree/3.5.2" }, - "time": "2025-08-05T06:05:51+00:00" + "time": "2025-08-08T17:00:40+00:00" }, { "name": "evenement/evenement", @@ -8093,16 +8095,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.48", + "version": "10.5.49", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "6e0a2bc39f6fae7617989d690d76c48e6d2eb541" + "reference": "e2571b0e6e372de90f0b147b0cb9379bb105c124" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6e0a2bc39f6fae7617989d690d76c48e6d2eb541", - "reference": "6e0a2bc39f6fae7617989d690d76c48e6d2eb541", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e2571b0e6e372de90f0b147b0cb9379bb105c124", + "reference": "e2571b0e6e372de90f0b147b0cb9379bb105c124", "shasum": "" }, "require": { @@ -8112,7 +8114,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.3", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.1", @@ -8174,7 +8176,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.48" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.49" }, "funding": [ { @@ -8198,7 +8200,7 @@ "type": "tidelift" } ], - "time": "2025-07-11T04:07:17+00:00" + "time": "2025-08-09T07:08:00+00:00" }, { "name": "psr/http-client", diff --git a/tests/Functional.suite.yml b/tests/Functional.suite.yml index 9867a88..6632bec 100644 --- a/tests/Functional.suite.yml +++ b/tests/Functional.suite.yml @@ -9,3 +9,4 @@ modules: - Doctrine: depends: Symfony cleanup: true + - App\Tests\Support\Helper\Environment diff --git a/tests/Functional/EnvironmentAssertionsCest.php b/tests/Functional/EnvironmentAssertionsCest.php new file mode 100644 index 0000000..d2992e1 --- /dev/null +++ b/tests/Functional/EnvironmentAssertionsCest.php @@ -0,0 +1,47 @@ +assertKernelEnvironment('test'); + $I->assertDebugModeIsEnabled(); + $I->assertSymfonyVersion('>=', '7.3'); + $I->assertAppEnvAndDebugMatchKernel(); + $I->assertAppCacheIsWritable(); + $I->assertProjectStructureIsSane(); + } + + public function serviceAndBundleAssertions(FunctionalTester $I): void + { + $I->assertBundleIsEnabled(\Symfony\Bundle\FrameworkBundle\FrameworkBundle::class); + } + + public function securityAssertions(FunctionalTester $I): void + { + $I->assertFirewallIsActive('main'); + } + + public function doctrineAssertions(FunctionalTester $I): void + { + $I->assertDoctrineDatabaseIsUp(); + } + + public function otherComponentAssertions(FunctionalTester $I): void + { + $I->assertSessionSavePathIsWritable(); + $I->assertKernelCharsetIs('UTF-8'); + } +} diff --git a/tests/Support/Codeception/Module/Symfony/EnvironmentAssertionsTrait.php b/tests/Support/Codeception/Module/Symfony/EnvironmentAssertionsTrait.php new file mode 100644 index 0000000..cdb88e0 --- /dev/null +++ b/tests/Support/Codeception/Module/Symfony/EnvironmentAssertionsTrait.php @@ -0,0 +1,536 @@ +getKernel()->getEnvironment(); + Assert::assertSame( + $expectedEnv, + $currentEnv, + sprintf('Kernel is running in environment "%s" but expected "%s".', $currentEnv, $expectedEnv) + ); + } + + /** + * Asserts that the application's debug mode is enabled. + */ + public function assertDebugModeIsEnabled(): void + { + $isDebug = $this->getKernel()->isDebug(); + Assert::assertTrue($isDebug, 'Debug mode is expected to be enabled, but it is not.'); + } + + /** + * Asserts that the application's debug mode is disabled (production-like). + */ + public function assertDebugModeIsDisabled(): void + { + $isDebug = $this->getKernel()->isDebug(); + Assert::assertFalse($isDebug, 'Debug mode is expected to be disabled, but it is enabled.'); + } + + /** + * Asserts that the current Symfony version satisfies the given comparison. + * Example: `$I->assertSymfonyVersion('>=', '6.4');` + */ + public function assertSymfonyVersion(string $operator, string $version, string $message = ''): void + { + Assert::assertTrue( + version_compare(Kernel::VERSION, $version, $operator), + $message ?: sprintf('Symfony version %s does not satisfy the constraint: %s %s', Kernel::VERSION, $operator, $version) + ); + } + + /** + * Asserts that `APP_ENV` and `APP_DEBUG` env vars match the Kernel state. + */ + public function assertAppEnvAndDebugMatchKernel(): void + { + $kernel = $this->getKernel(); + $appEnv = getenv('APP_ENV'); + $appDebug = getenv('APP_DEBUG'); + + if ($appEnv !== false) { + Assert::assertSame( + $kernel->getEnvironment(), + (string) $appEnv, + sprintf('APP_ENV (%s) differs from Kernel environment (%s).', $appEnv, $kernel->getEnvironment()) + ); + } + + if ($appDebug !== false) { + $expected = $kernel->isDebug(); + $normalized = in_array(strtolower((string) $appDebug), ['1', 'true', 'yes', 'on'], true); + Assert::assertSame( + $expected, + $normalized, + sprintf('APP_DEBUG (%s) differs from Kernel debug (%s).', $appDebug, $expected ? 'true' : 'false') + ); + } + } + + /** + * Asserts that the application's cache directory is writable. + */ + public function assertAppCacheIsWritable(): void + { + $cacheDir = $this->getKernel()->getCacheDir(); + Assert::assertTrue( + is_writable($cacheDir), + sprintf('Symfony cache directory is not writable: %s', $cacheDir) + ); + } + + /** + * Asserts that the application's log directory is writable (parameter-first). + */ + public function assertAppLogIsWritable(): void + { + $container = $this->getSymfonyModule()->_getContainer(); + $logDir = null; + if ($container->hasParameter('kernel.logs_dir')) { + $logDir = (string) $container->getParameter('kernel.logs_dir'); + } + + if (!$logDir) { + $logDir = method_exists($this->getKernel(), 'getLogDir') + ? $this->getKernel()->getLogDir() + : $this->getProjectDir() . 'var/log'; + } + + Assert::assertTrue( + is_writable($logDir), + sprintf('Symfony log directory is not writable: %s', $logDir) + ); + } + + /** + * Asserts that the minimal Symfony project structure exists and is usable. + */ + public function assertProjectStructureIsSane(): void + { + $root = $this->getProjectDir(); + foreach (['config', 'src', 'public', 'var'] as $dir) { + Assert::assertTrue(is_dir($root . $dir), sprintf('Directory "%s" is missing.', $dir)); + } + + foreach (['var/cache', 'var/log'] as $dir) { + Assert::assertTrue(is_dir($root . $dir), sprintf('Directory "%s" is missing.', $dir)); + Assert::assertTrue(is_writable($root . $dir), sprintf('Directory "%s" is not writable.', $dir)); + } + + Assert::assertFileExists($root . 'config/bundles.php', 'Missing config/bundles.php file.'); + + $bin = $root . 'bin/console'; + Assert::assertTrue(is_file($bin), 'bin/console is missing.'); + if (strncasecmp(PHP_OS, 'WIN', 3) !== 0) { + Assert::assertTrue(is_executable($bin) || is_file($bin), 'bin/console is not executable.'); + } + } + + /** + * Asserts that all keys in example env file(s) exist either in the provided env file(s) OR as OS env vars. + * This validates presence only, not values. It also considers common local/test files if present. + * + * @param non-empty-string $envPath + * @param non-empty-string $examplePath + * @param list $additionalEnvPaths + */ + public function assertEnvFileIsSynchronized(string $envPath = '.env', string $examplePath = '.env.example', array $additionalEnvPaths = []): void + { + $projectDir = $this->getProjectDir(); + + $candidateExtras = ['.env.local', '.env.test', '.env.test.local']; + foreach ($candidateExtras as $extra) { + if (file_exists($projectDir . $extra)) { + $additionalEnvPaths[] = $extra; + } + } + + $exampleContent = @file_get_contents($projectDir . $examplePath) ?: ''; + $envContent = @file_get_contents($projectDir . $envPath) ?: ''; + + foreach ($additionalEnvPaths as $extra) { + $envContent .= "\n" . (@file_get_contents($projectDir . $extra) ?: ''); + } + + $exampleKeys = $this->extractEnvKeys($exampleContent); + $envKeys = $this->extractEnvKeys($envContent); + + $osKeys = array_keys($_ENV + $_SERVER); + $present = array_flip(array_merge($envKeys, $osKeys)); + + $missing = []; + foreach ($exampleKeys as $key) { + if (!isset($present[$key])) { + $missing[] = $key; + } + } + + Assert::assertEmpty( + $missing, + sprintf('Missing variables from %s (not found across %s nor as OS envs): %s', $examplePath, implode(', ', array_merge([$envPath], $additionalEnvPaths)), implode(', ', $missing)) + ); + } + + // ========================================================================= + // Symfony Components & Services Assertions + // ========================================================================= + + /** + * Asserts that a specific bundle is enabled in the Kernel. + * @param class-string $bundleClass The Fully Qualified Class Name of the bundle. + */ + public function assertBundleIsEnabled(string $bundleClass): void + { + $bundles = $this->getKernel()->getBundles(); + $found = false; + foreach ($bundles as $bundle) { + if ($bundle instanceof $bundleClass || get_class($bundle) === $bundleClass) { + $found = true; + break; + } + } + + Assert::assertTrue( + $found, + sprintf('Bundle "%s" is not enabled in the Kernel. Check config/bundles.php.', $bundleClass) + ); + } + + // ========================================================================= + // Security Assertions + // ========================================================================= + + /** + * Asserts that a security firewall is active (configured). + */ + public function assertFirewallIsActive(string $firewallName): void + { + $container = $this->getSymfonyModule()->_getContainer(); + + if ($container->hasParameter('security.firewalls')) { + /** @var list $firewalls */ + $firewalls = $container->getParameter('security.firewalls'); + Assert::assertContains($firewallName, $firewalls, sprintf('Firewall "%s" is not configured. Check your security.yaml.', $firewallName)); + return; + } + + $contextId = 'security.firewall.map.context.' . $firewallName; + Assert::assertTrue( + $container->has($contextId), + sprintf('Firewall "%s" context was not found (checked "%s").', $firewallName, $contextId) + ); + } + + /** + * Asserts that a role is present either as a key of the role hierarchy or among any inherited roles. + * Skips when role hierarchy is not configured. + */ + public function assertRoleInHierarchy(string $role): void + { + $container = $this->getSymfonyModule()->_getContainer(); + if (!$container->hasParameter('security.role_hierarchy.roles')) { + Assert::markTestSkipped('Role hierarchy is not configured; skipping role hierarchy assertion.'); + } + + /** @var array> $hierarchy */ + $hierarchy = $container->getParameter('security.role_hierarchy.roles'); + + $all = array_keys($hierarchy); + foreach ($hierarchy as $children) { + foreach ($children as $child) { + $all[] = $child; + } + } + $all = array_values(array_unique($all)); + Assert::assertContains( + $role, + $all, + sprintf('Role "%s" was not found in the role hierarchy. Check security.yaml.', $role) + ); + } + + /** + * Asserts that a secret from the Symfony vault can be resolved. + * @param non-empty-string $secretName The name of the secret (e.g., 'DATABASE_PASSWORD'). + */ + public function assertCanResolveSecret(string $secretName): void + { + try { + /** @var ContainerBagInterface $params */ + $params = $this->grabService('parameter_bag'); + $value = $params->get(sprintf('env(resolve:%s)', $secretName)); + + Assert::assertIsString($value, sprintf('Secret "%s" could be resolved but did not return a string.', $secretName)); + } catch (Throwable $e) { + Assert::fail(sprintf('Failed to resolve secret "%s". Check your vault and decryption keys. Error: %s', $secretName, $e->getMessage())); + } + } + + // ========================================================================= + // Doctrine Assertions + // ========================================================================= + + /** + * Asserts that the application can connect to a Doctrine database. + * @param non-empty-string $connectionName The name of the Doctrine connection to check. + */ + public function assertDoctrineDatabaseIsUp(string $connectionName = 'default'): void + { + try { + /** @var ManagerRegistry $doctrine */ + $doctrine = $this->grabService('doctrine'); + $connection = $doctrine->getConnection($connectionName); + $connection->executeQuery($connection->getDatabasePlatform()->getDummySelectSQL()); + Assert::assertTrue(true, sprintf('Doctrine connection "%s" is up and responsive.', $connectionName)); + } catch (Throwable $e) { + Assert::fail(sprintf('Doctrine connection "%s" failed: %s', $connectionName, $e->getMessage())); + } + } + + /** + * Asserts that the Doctrine mapping is valid and the DB schema is in sync for one EM. + * Programmatic equivalent of `bin/console doctrine:schema:validate`. + * @param non-empty-string $entityManagerName + */ + public function assertDoctrineSchemaIsValid(string $entityManagerName = 'default'): void + { + try { + /** @var ManagerRegistry $doctrine */ + $doctrine = $this->grabService('doctrine'); + $em = $doctrine->getManager($entityManagerName); + $validator = new SchemaValidator($em); + $errors = $validator->validateMapping(); + $errorMessages = []; + foreach ($errors as $className => $classErrors) { + $errorMessages[] = sprintf(' - %s: %s', $className, implode('; ', $classErrors)); + } + Assert::assertEmpty( + $errors, + sprintf( + "The Doctrine mapping is invalid for the '%s' entity manager:\n%s", + $entityManagerName, + implode("\n", $errorMessages) + ) + ); + + if (!$validator->schemaInSyncWithMetadata()) { + Assert::fail(sprintf( + 'The database schema is not in sync with the current mapping for the "%s" entity manager. Generate and run a new migration.', + $entityManagerName + )); + } + Assert::assertTrue(true, sprintf('Doctrine schema for "%s" EM is valid and synchronized.', $entityManagerName)); + } catch (Throwable $e) { + Assert::fail(sprintf('Could not validate Doctrine schema for the "%s" entity manager: %s', $entityManagerName, $e->getMessage())); + } + } + + /** + * Asserts that Doctrine proxy directory is writable for a given EM. + */ + public function assertDoctrineProxyDirIsWritable(string $entityManagerName = 'default'): void + { + /** @var ManagerRegistry $doctrine */ + $doctrine = $this->grabService('doctrine'); + $em = $doctrine->getManager($entityManagerName); + $proxyDir = $em->getConfiguration()->getProxyDir(); + Assert::assertTrue($proxyDir !== null && $proxyDir !== '', sprintf('Doctrine proxy dir is not configured for EM "%s".', $entityManagerName)); + Assert::assertTrue(is_dir($proxyDir), sprintf('Doctrine proxy dir does not exist: %s', $proxyDir)); + Assert::assertTrue(is_writable($proxyDir), sprintf('Doctrine proxy dir is not writable: %s', $proxyDir)); + } + + // ========================================================================= + // Other Component Assertions + // ========================================================================= + + /** + * Asserts that an asset manifest file exists, checking for Webpack Encore or AssetMapper. + * A common CI/CD failure point is missing frontend assets. + */ + public function assertAssetManifestExists(): void + { + $projectDir = $this->getProjectDir(); + $encoreManifest = $projectDir . 'public/build/manifest.json'; + $mapperManifest = $projectDir . 'public/assets/manifest.json'; + $encoreEntrypoints = $projectDir . 'public/build/entrypoints.json'; + + if (is_readable($encoreManifest) && is_readable($encoreEntrypoints)) { + Assert::assertJson((string) file_get_contents($encoreManifest), 'Webpack Encore manifest.json is not valid JSON.'); + Assert::assertJson((string) file_get_contents($encoreEntrypoints), 'Webpack Encore entrypoints.json is not valid JSON.'); + Assert::assertTrue(true, 'Webpack Encore manifest files found and are valid.'); + return; + } + + if (is_readable($mapperManifest)) { + Assert::assertJson((string) file_get_contents($mapperManifest), 'AssetMapper manifest.json is not valid JSON.'); + Assert::assertTrue(true, 'AssetMapper manifest file found and is valid.'); + return; + } + + Assert::fail('No asset manifest file found. Checked for Webpack Encore (public/build/manifest.json) and AssetMapper (public/assets/manifest.json).'); + } + + /** + * Asserts that the session save path is writable when using file-based sessions. + * Skips when session storage is not file-based. + */ + public function assertSessionSavePathIsWritable(): void + { + $container = $this->getSymfonyModule()->_getContainer(); + + $isFileBased = false; + if ($container->has('session.storage.factory.native_file') || $container->has('session.handler.native_file')) { + $isFileBased = true; + } + $iniHandler = (string) (ini_get('session.save_handler') ?: ''); + if ($iniHandler === 'files') { + $isFileBased = true; + } + + if (!$isFileBased) { + Assert::markTestSkipped('Session storage is not file-based; skipping save path writability check.'); + } + + $savePath = null; + + if ($container->hasParameter('session.storage.options')) { + $options = $container->getParameter('session.storage.options'); + if (is_array($options) && isset($options['save_path']) && is_string($options['save_path']) && $options['save_path'] !== '') { + $savePath = $options['save_path']; + } + } + + if (!$savePath) { + $ini = (string) (ini_get('session.save_path') ?: ''); + if ($ini !== '') { + $savePath = $ini; + } + } + + if (!$savePath) { + $env = $this->getKernel()->getEnvironment(); + $savePath = $this->getProjectDir() . 'var/sessions/' . $env; + } + + Assert::assertTrue(is_dir($savePath), sprintf('Session save path is not a directory: %s', $savePath)); + Assert::assertTrue(is_writable($savePath), sprintf('Session save path is not writable: %s', $savePath)); + } + + /** + * Asserts the Kernel charset matches the expected value. + */ + public function assertKernelCharsetIs(string $expected = 'UTF-8'): void + { + $charset = $this->getKernel()->getCharset(); + Assert::assertSame($expected, $charset, sprintf('Kernel charset is "%s" but expected "%s".', $charset, $expected)); + } + + // ========================================================================= + // Trait Internals + // ========================================================================= + + /** + * Helper to get the Symfony module. + */ + private function getSymfonyModule(): SymfonyModule + { + $symfonyModule = $this->getModule('Symfony'); + if (!$symfonyModule instanceof SymfonyModule) { + throw new LogicException('This trait can only be used in a class that uses the Codeception Symfony module.'); + } + return $symfonyModule; + } + + /** + * Helper to get a service from the container. + */ + private function grabService(string $serviceId): object + { + return $this->getSymfonyModule()->_getContainer()->get($serviceId); + } + + /** + * Helper to get the Kernel instance. + */ + private function getKernel(): Kernel + { + /** @var Kernel $kernel */ + $kernel = $this->getSymfonyModule()->grabService('kernel'); + return $kernel; + } + + /** + * Helper to get the project's root directory. + */ + private function getProjectDir(): string + { + return $this->getKernel()->getProjectDir() . '/'; + } + + /** + * Extracts variable keys from the content of a .env file. + * @return list + */ + private function extractEnvKeys(string $content): array + { + $keys = []; + if (preg_match_all('/^(?!#)\s*([a-zA-Z_][a-zA-Z0-9_]*)=/m', $content, $matches)) { + $keys = $matches[1]; + } + return $keys; + } +} diff --git a/tests/Support/Helper/Environment.php b/tests/Support/Helper/Environment.php new file mode 100644 index 0000000..a803895 --- /dev/null +++ b/tests/Support/Helper/Environment.php @@ -0,0 +1,13 @@ +