Fix RSS health check

This commit is contained in:
DariusIII
2026-06-17 13:34:13 +02:00
parent 995c215be9
commit 2f96d921df
7 changed files with 199 additions and 2 deletions
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
class RssHealthController extends Controller
{
public function __invoke(): JsonResponse
{
return response()->json([
'status' => 'ok',
'service' => 'rss',
]);
}
}
@@ -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) {
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (! Schema::hasTable('service_statuses') || ! Schema::hasColumn('service_statuses', 'endpoint_url')) {
return;
}
DB::table('service_statuses')
->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');
}
};
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (! $this->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');
}
};
+3
View File
@@ -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']);
+11 -1
View File
@@ -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');
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Services;
use App\Enums\IncidentImpactEnum;
use App\Models\ServiceStatus;
use App\Services\SiteStatusService;
use App\Services\StatusProbes\ServiceProbeRegistry;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class SiteStatusServiceHttpHealthCheckTest extends TestCase
{
public function test_authenticated_rss_feed_url_400_is_reported_as_unexpected_response(): void
{
config(['app.url' => '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));
}
}