diff --git a/.gitignore b/.gitignore index 4061960bb..414fc83b4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ data/infra/matomo docs/mercure.html .phpunit.result.cache docs/swagger/openapi-inlined.json +config/test/dynamic_test_env.php diff --git a/composer.json b/composer.json index f04124d4d..3152018dd 100644 --- a/composer.json +++ b/composer.json @@ -74,7 +74,8 @@ "shlinkio/php-coding-standard": "~2.5.0", "shlinkio/shlink-test-utils": "^4.3.1", "symfony/var-dumper": "^7.3", - "veewee/composer-run-parallel": "^1.4" + "veewee/composer-run-parallel": "^1.4", + "webimpress/safe-writer": "^2.2" }, "conflict": { "symfony/var-exporter": ">=6.3.9,<=6.4.0" diff --git a/config/container.php b/config/container.php index 31e406a3d..842aa9932 100644 --- a/config/container.php +++ b/config/container.php @@ -16,6 +16,8 @@ require 'vendor/autoload.php'; +// Promote env vars from dynamic test config +loadEnvVarsFromConfig('config/test/dynamic_test_env.php', enumValues(EnvVars::class)); // Promote env vars from installer, dev config or test config loadEnvVarsFromConfig( EnvVars::isTestEnv() ? 'config/test/shlink_test_env.php' : 'config/params/*.php', diff --git a/config/test/constants.php b/config/test/constants.php index f2ad124d6..dee3d5950 100644 --- a/config/test/constants.php +++ b/config/test/constants.php @@ -19,3 +19,5 @@ . 'Version/18.4 Safari/605.1.15'; const CHROMEOS_USER_AGENT = 'Mozilla/5.0 (X11; CrOS x86_64 16181.61.0) AppleWebKit/537.36 (KHTML, like Gecko) ' . 'Chrome/134.0.6998.198 Safari/537.36'; + +const DYNAMIC_ENV_VARS_FILE = __DIR__ . '/../../config/test/dynamic_test_env.php'; diff --git a/module/Rest/test-api/Action/MercureInfoTest.php b/module/Rest/test-api/Action/MercureInfoTest.php new file mode 100644 index 000000000..6bb1c0bb4 --- /dev/null +++ b/module/Rest/test-api/Action/MercureInfoTest.php @@ -0,0 +1,34 @@ +callApiWithKey('GET', '/mercure-info'); + self::assertEquals(501, $resp->getStatusCode()); + } + + #[Test, WithEnvVars([ + EnvVars::MERCURE_ENABLED->value => true, + EnvVars::MERCURE_PUBLIC_HUB_URL->value => 'https://mercure.example.com', + EnvVars::MERCURE_JWT_SECRET->value => 'mercure_jwt_key_long_enough_to_avoid_error', + ])] + public function errorIsReturnedIfMercureServerIsNotConfigured(): void + { + $resp = $this->callApiWithKey('GET', '/mercure-info'); + $payload = $this->getJsonResponsePayload($resp); + + self::assertEquals(200, $resp->getStatusCode()); + self::assertEquals('https://mercure.example.com/.well-known/mercure', $payload['mercureHubUrl']); + } +} diff --git a/module/Rest/test-api/Middleware/CorsTest.php b/module/Rest/test-api/Middleware/CorsTest.php index 4ed3f2cbc..e5ba8e172 100644 --- a/module/Rest/test-api/Middleware/CorsTest.php +++ b/module/Rest/test-api/Middleware/CorsTest.php @@ -7,7 +7,9 @@ use GuzzleHttp\RequestOptions; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; +use Shlinkio\Shlink\Core\Config\EnvVars; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; +use ShlinkioApiTest\Shlink\Rest\Utils\WithEnvVars; class CorsTest extends ApiTestCase { @@ -61,8 +63,8 @@ public function preflightRequestsIncludeExtraCorsHeaders(string $endpoint, strin ]); self::assertEquals(204, $resp->getStatusCode()); - self::assertTrue($resp->hasHeader('Access-Control-Allow-Origin')); - self::assertTrue($resp->hasHeader('Access-Control-Max-Age')); + self::assertEquals('*', $resp->getHeaderLine('Access-Control-Allow-Origin')); + self::assertEquals('3600', $resp->getHeaderLine('Access-Control-Max-Age')); self::assertEquals($expectedAllowedMethods, $resp->getHeaderLine('Access-Control-Allow-Methods')); self::assertEquals($allowedHeaders, $resp->getHeaderLine('Access-Control-Allow-Headers')); } @@ -74,4 +76,20 @@ public static function providePreflightEndpoints(): iterable yield 'tags route' => ['/tags', 'GET,DELETE,PUT']; yield 'health route' => ['/health', 'GET']; } + + #[Test, WithEnvVars([ + EnvVars::CORS_MAX_AGE->value => 5000, + EnvVars::CORS_ALLOW_CREDENTIALS->value => true, + EnvVars::CORS_ALLOW_ORIGIN->value => 'example.com,localhost:8000', + ])] + public function parametersCanBeCustomized(): void + { + $resp = $this->callApiWithKey(self::METHOD_OPTIONS, '/short-urls', [ + RequestOptions::HEADERS => ['Origin' => 'example.com'], + ]); + + self::assertEquals('5000', $resp->getHeaderLine('Access-Control-Max-Age')); + self::assertEquals('example.com', $resp->getHeaderLine('Access-Control-Allow-Origin')); + self::assertEquals('true', $resp->getHeaderLine('Access-Control-Allow-Credentials')); + } } diff --git a/module/Rest/test-api/Utils/ApiTestsExtension.php b/module/Rest/test-api/Utils/ApiTestsExtension.php new file mode 100644 index 000000000..e1dbd9c0f --- /dev/null +++ b/module/Rest/test-api/Utils/ApiTestsExtension.php @@ -0,0 +1,25 @@ +registerSubscriber(new EnvSpecificTestListener()); + $facade->registerSubscriber(new CleanDynamicEnvVarsTestListener()); + } + + public static function restartRRServer(): void + { + (new Process(['bin/rr', 'reset', '-c=config/roadrunner/.rr.test.yml']))->mustRun(); + } +} diff --git a/module/Rest/test-api/Utils/CleanDynamicEnvVarsTestListener.php b/module/Rest/test-api/Utils/CleanDynamicEnvVarsTestListener.php new file mode 100644 index 000000000..08436ca91 --- /dev/null +++ b/module/Rest/test-api/Utils/CleanDynamicEnvVarsTestListener.php @@ -0,0 +1,28 @@ +test(); + if (! ($test instanceof TestMethod)) { + return; + } + + $className = $test->className(); + $methodName = $test->methodName(); + + $reflection = new ReflectionMethod($className, $methodName); + $attributes = $reflection->getAttributes(WithEnvVars::class); + + // If the method has the attribute, generate a temporary env file, and restart the RoadRunner server + if (! empty($attributes)) { + /** @var WithEnvVars $withEnvVars */ + $withEnvVars = $attributes[0]->newInstance(); + $this->createDynamicEnvVarsFile($withEnvVars->envVars); + ApiTestsExtension::restartRRServer(); + } + } + + private function createDynamicEnvVarsFile(array $envVars): void + { + $template = <<