From 2f96d921df690b0a2f25e033e6841e7ac3041264 Mon Sep 17 00:00:00 2001 From: DariusIII Date: Wed, 17 Jun 2026 13:34:13 +0200 Subject: [PATCH] Fix RSS health check --- app/Http/Controllers/RssHealthController.php | 18 ++++++ ...0_add_endpoint_url_to_service_statuses.php | 2 +- ..._rss_service_status_endpoint_to_health.php | 46 ++++++++++++++ ..._rss_http_400_false_positive_incidents.php | 58 +++++++++++++++++ routes/rss.php | 3 + tests/Feature/NzbAndRssAccessTest.php | 12 +++- .../SiteStatusServiceHttpHealthCheckTest.php | 62 +++++++++++++++++++ 7 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 app/Http/Controllers/RssHealthController.php create mode 100644 database/migrations/2026_06_17_000000_update_rss_service_status_endpoint_to_health.php create mode 100644 database/migrations/2026_06_17_000001_neutralize_rss_http_400_false_positive_incidents.php create mode 100644 tests/Unit/Services/SiteStatusServiceHttpHealthCheckTest.php diff --git a/app/Http/Controllers/RssHealthController.php b/app/Http/Controllers/RssHealthController.php new file mode 100644 index 000000000..4d921b8e3 --- /dev/null +++ b/app/Http/Controllers/RssHealthController.php @@ -0,0 +1,18 @@ +json([ + 'status' => 'ok', + 'service' => 'rss', + ]); + } +} diff --git a/database/migrations/2026_04_01_130000_add_endpoint_url_to_service_statuses.php b/database/migrations/2026_04_01_130000_add_endpoint_url_to_service_statuses.php index 9de1e211b..0a36cf7d8 100644 --- a/database/migrations/2026_04_01_130000_add_endpoint_url_to_service_statuses.php +++ b/database/migrations/2026_04_01_130000_add_endpoint_url_to_service_statuses.php @@ -18,7 +18,7 @@ return new class extends Migration $defaults = [ 'api' => '/api/v2/capabilities,/api/v1/api', 'http' => '/up', - 'rss' => '/rss/full-feed', + 'rss' => '/rss/health', ]; foreach ($defaults as $slug => $path) { diff --git a/database/migrations/2026_06_17_000000_update_rss_service_status_endpoint_to_health.php b/database/migrations/2026_06_17_000000_update_rss_service_status_endpoint_to_health.php new file mode 100644 index 000000000..70e846334 --- /dev/null +++ b/database/migrations/2026_06_17_000000_update_rss_service_status_endpoint_to_health.php @@ -0,0 +1,46 @@ +where('slug', 'rss') + ->where('endpoint_url', '/rss/full-feed') + ->update(['endpoint_url' => '/rss/health']); + + $this->forgetSiteStatusCaches(); + } + + public function down(): void + { + if (! Schema::hasTable('service_statuses') || ! Schema::hasColumn('service_statuses', 'endpoint_url')) { + return; + } + + DB::table('service_statuses') + ->where('slug', 'rss') + ->where('endpoint_url', '/rss/health') + ->update(['endpoint_url' => '/rss/full-feed']); + + $this->forgetSiteStatusCaches(); + } + + private function forgetSiteStatusCaches(): void + { + Cache::forget('admin:site-status:enabled-services'); + Cache::forget('admin:site-status:active-incidents'); + Cache::forget('admin:dashboard:snapshot'); + } +}; diff --git a/database/migrations/2026_06_17_000001_neutralize_rss_http_400_false_positive_incidents.php b/database/migrations/2026_06_17_000001_neutralize_rss_http_400_false_positive_incidents.php new file mode 100644 index 000000000..f0544e33c --- /dev/null +++ b/database/migrations/2026_06_17_000001_neutralize_rss_http_400_false_positive_incidents.php @@ -0,0 +1,58 @@ +hasRequiredTablesAndColumns()) { + return; + } + + DB::table('service_incidents') + ->where('service_incidents.is_auto', true) + ->where('service_incidents.title', '[Auto] RSS — Unexpected response (HTTP 400)') + ->whereExists(static function ($query): void { + $query->select(DB::raw(1)) + ->from('service_incident_service_status') + ->join('service_statuses', 'service_statuses.id', '=', 'service_incident_service_status.service_status_id') + ->whereColumn('service_incident_service_status.service_incident_id', 'service_incidents.id') + ->where('service_statuses.slug', 'rss'); + }) + ->update([ + 'status' => 'resolved', + 'impact' => 'none', + 'resolved_at' => DB::raw('COALESCE(resolved_at, started_at)'), + 'updated_at' => now(), + ]); + + $this->forgetSiteStatusCaches(); + } + + public function down(): void + { + // Intentionally not reversible: these rows are auto-created false positives + // from probing an authenticated RSS feed without an API token. + } + + private function hasRequiredTablesAndColumns(): bool + { + return Schema::hasTable('service_statuses') + && Schema::hasTable('service_incidents') + && Schema::hasTable('service_incident_service_status') + && Schema::hasColumn('service_incidents', 'is_auto'); + } + + private function forgetSiteStatusCaches(): void + { + Cache::forget('admin:site-status:enabled-services'); + Cache::forget('admin:site-status:active-incidents'); + Cache::forget('admin:dashboard:snapshot'); + } +}; diff --git a/routes/rss.php b/routes/rss.php index 5fb48d690..36f571530 100644 --- a/routes/rss.php +++ b/routes/rss.php @@ -12,6 +12,9 @@ */ use App\Http\Controllers\RssController; +use App\Http\Controllers\RssHealthController; + +Route::get('health', RssHealthController::class); Route::middleware('apiRateLimit')->group(function (): void { Route::get('mymovies', [RssController::class, 'myMoviesRss']); diff --git a/tests/Feature/NzbAndRssAccessTest.php b/tests/Feature/NzbAndRssAccessTest.php index be6196276..ad0902096 100644 --- a/tests/Feature/NzbAndRssAccessTest.php +++ b/tests/Feature/NzbAndRssAccessTest.php @@ -241,7 +241,17 @@ class NzbAndRssAccessTest extends TestCase ->assertJsonPath('error', 'Incorrect user credentials'); } - public function test_rss_feed_without_api_token_returns_403_error_instead_of_login_redirect(): void + public function test_rss_health_endpoint_is_public_for_status_monitoring(): void + { + $this->getJson('/rss/health') + ->assertOk() + ->assertJson([ + 'status' => 'ok', + 'service' => 'rss', + ]); + } + + public function test_rss_feed_without_api_token_returns_api_error_instead_of_login_redirect(): void { $response = $this->get('/rss/full-feed'); diff --git a/tests/Unit/Services/SiteStatusServiceHttpHealthCheckTest.php b/tests/Unit/Services/SiteStatusServiceHttpHealthCheckTest.php new file mode 100644 index 000000000..57f6b0646 --- /dev/null +++ b/tests/Unit/Services/SiteStatusServiceHttpHealthCheckTest.php @@ -0,0 +1,62 @@ + 'https://example.test']); + + Http::fake([ + 'https://example.test/rss/full-feed' => Http::response(['error' => 'Missing parameter (api_token)'], 400), + ]); + + $result = $this->statusService()->checkServiceHealth(new ServiceStatus([ + 'name' => 'RSS', + 'slug' => 'rss', + 'endpoint_url' => '/rss/full-feed', + 'check_type' => 'http', + ])); + + $this->assertFalse($result['ok']); + $this->assertSame(400, $result['status_code']); + $this->assertSame(IncidentImpactEnum::Major, $result['impact']); + $this->assertSame('Unexpected response (HTTP 400)', $result['reason']); + } + + public function test_public_rss_health_endpoint_is_reported_as_healthy(): void + { + config(['app.url' => 'https://example.test']); + + Http::fake([ + 'https://example.test/rss/health' => Http::response(['status' => 'ok', 'service' => 'rss'], 200), + ]); + + $result = $this->statusService()->checkServiceHealth(new ServiceStatus([ + 'name' => 'RSS', + 'slug' => 'rss', + 'endpoint_url' => '/rss/health', + 'check_type' => 'http', + ])); + + $this->assertTrue($result['ok']); + $this->assertSame(200, $result['status_code']); + $this->assertNull($result['impact']); + $this->assertSame('OK', $result['reason']); + } + + private function statusService(): SiteStatusService + { + return new SiteStatusService($this->createMock(ServiceProbeRegistry::class)); + } +}