From 93cdb62dfe3b7df0fd0135fccebbd4d2d48b0540 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 21:56:10 +0000 Subject: [PATCH] feat(api): allow deprecation without sunset date Co-Authored-By: Virgil --- .../src/Front/Api/Middleware/ApiSunset.php | 32 ++++++++++++------- src/php/src/Front/Api/VersionedRoutes.php | 14 +++++--- src/php/tests/Feature/ApiSunsetTest.php | 31 ++++++++++++++++++ src/php/tests/Feature/VersionedRoutesTest.php | 28 ++++++++++++++++ 4 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 src/php/tests/Feature/ApiSunsetTest.php diff --git a/src/php/src/Front/Api/Middleware/ApiSunset.php b/src/php/src/Front/Api/Middleware/ApiSunset.php index c853f9a..20f3535 100644 --- a/src/php/src/Front/Api/Middleware/ApiSunset.php +++ b/src/php/src/Front/Api/Middleware/ApiSunset.php @@ -40,6 +40,14 @@ use Symfony\Component\HttpFoundation\Response; * Route::middleware('api.sunset:2025-06-01,/api/v2/new-endpoint')->group(function () { * Route::get('/old-endpoint', OldController::class); * }); + * + * You can also mark a route as deprecated without a fixed removal date: + * + * ```php + * Route::middleware('api.sunset')->group(function () { + * Route::get('/old-endpoint', OldController::class); + * }); + * ``` * ``` * * ## Response Headers @@ -56,29 +64,31 @@ class ApiSunset /** * Handle an incoming request. * - * @param string $sunsetDate The sunset date (YYYY-MM-DD or RFC7231 format) + * @param string $sunsetDate The sunset date (YYYY-MM-DD or RFC7231 format), or empty for deprecation-only * @param string|null $replacement Optional replacement endpoint URL */ - public function handle(Request $request, Closure $next, string $sunsetDate, ?string $replacement = null): Response + public function handle(Request $request, Closure $next, string $sunsetDate = '', ?string $replacement = null): Response { /** @var Response $response */ $response = $next($request); - // Convert date to RFC7231 format if needed - $formattedDate = $this->formatSunsetDate($sunsetDate); + if ($sunsetDate !== '') { + // Convert date to RFC7231 format if needed + $formattedDate = $this->formatSunsetDate($sunsetDate); - // Add Sunset header - $response->headers->set('Sunset', $formattedDate); + // Add Sunset header + $response->headers->set('Sunset', $formattedDate); + } // Add Deprecation header $response->headers->set('Deprecation', 'true'); // Add warning header - $version = $request->attributes->get('api_version', 'unknown'); - $response->headers->set( - 'X-API-Warn', - "This endpoint is deprecated and will be removed on {$sunsetDate}." - ); + $warning = 'This endpoint is deprecated.'; + if ($sunsetDate !== '') { + $warning = "This endpoint is deprecated and will be removed on {$sunsetDate}."; + } + $response->headers->set('X-API-Warn', $warning); // Add Link header for replacement if provided if ($replacement !== null) { diff --git a/src/php/src/Front/Api/VersionedRoutes.php b/src/php/src/Front/Api/VersionedRoutes.php index 9ee15a3..4239435 100644 --- a/src/php/src/Front/Api/VersionedRoutes.php +++ b/src/php/src/Front/Api/VersionedRoutes.php @@ -246,11 +246,17 @@ class VersionedRoutes { $middleware = ["api.version:{$this->version}"]; - if ($this->isDeprecated && $this->sunsetDate) { - if ($this->replacement !== null && $this->replacement !== '') { - $middleware[] = "api.sunset:{$this->sunsetDate},{$this->replacement}"; + if ($this->isDeprecated) { + if ($this->sunsetDate !== null && $this->sunsetDate !== '') { + if ($this->replacement !== null && $this->replacement !== '') { + $middleware[] = "api.sunset:{$this->sunsetDate},{$this->replacement}"; + } else { + $middleware[] = "api.sunset:{$this->sunsetDate}"; + } + } elseif ($this->replacement !== null && $this->replacement !== '') { + $middleware[] = "api.sunset:,$this->replacement"; } else { - $middleware[] = "api.sunset:{$this->sunsetDate}"; + $middleware[] = 'api.sunset'; } } diff --git a/src/php/tests/Feature/ApiSunsetTest.php b/src/php/tests/Feature/ApiSunsetTest.php new file mode 100644 index 0000000..1c0069a --- /dev/null +++ b/src/php/tests/Feature/ApiSunsetTest.php @@ -0,0 +1,31 @@ +handle($request, fn () => new Response('OK')); + + expect($response->headers->get('Deprecation'))->toBe('true'); + expect($response->headers->has('Sunset'))->toBeFalse(); + expect($response->headers->has('Link'))->toBeFalse(); + expect($response->headers->get('X-API-Warn'))->toBe('This endpoint is deprecated.'); +}); + +it('adds a replacement link without a sunset date', function () { + $middleware = new ApiSunset(); + $request = Request::create('/old-endpoint', 'GET'); + + $response = $middleware->handle($request, fn () => new Response('OK'), '', '/api/v4/users'); + + expect($response->headers->get('Deprecation'))->toBe('true'); + expect($response->headers->has('Sunset'))->toBeFalse(); + expect($response->headers->get('Link'))->toBe('; rel="successor-version"'); + expect($response->headers->get('X-API-Warn'))->toBe('This endpoint is deprecated.'); +}); diff --git a/src/php/tests/Feature/VersionedRoutesTest.php b/src/php/tests/Feature/VersionedRoutesTest.php index 089e232..1aef855 100644 --- a/src/php/tests/Feature/VersionedRoutesTest.php +++ b/src/php/tests/Feature/VersionedRoutesTest.php @@ -32,3 +32,31 @@ it('preserves the existing deprecated signature without a replacement url', func expect($attributes['middleware'])->toContain('api.sunset:2025-06-01'); expect($attributes['middleware'])->not->toContain('api.sunset:2025-06-01,/api/v3/users'); }); + +it('keeps deprecated routes active without a sunset date', function () { + $routes = new class (3) extends VersionedRoutes { + public function attributes(): array + { + return $this->buildRouteAttributes(); + } + }; + + $attributes = $routes->deprecated()->attributes(); + + expect($attributes['middleware'])->toContain('api.version:3'); + expect($attributes['middleware'])->toContain('api.sunset'); +}); + +it('passes a replacement url through deprecated versioned routes without a sunset date', function () { + $routes = new class (4) extends VersionedRoutes { + public function attributes(): array + { + return $this->buildRouteAttributes(); + } + }; + + $attributes = $routes->deprecated(null, '/api/v4/users')->attributes(); + + expect($attributes['middleware'])->toContain('api.version:4'); + expect($attributes['middleware'])->toContain('api.sunset:,/api/v4/users'); +});