93 - Add feed staleness detection with configurable threshold and alerts
This commit is contained in:
parent
4feab96765
commit
062b00d01c
10 changed files with 407 additions and 0 deletions
51
app/Jobs/CheckFeedStalenessJob.php
Normal file
51
app/Jobs/CheckFeedStalenessJob.php
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Enums\NotificationSeverityEnum;
|
||||||
|
use App\Enums\NotificationTypeEnum;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use App\Models\Notification;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Services\Notification\NotificationService;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
|
|
||||||
|
class CheckFeedStalenessJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public function handle(NotificationService $notificationService): void
|
||||||
|
{
|
||||||
|
$thresholdHours = Setting::getFeedStalenessThreshold();
|
||||||
|
|
||||||
|
if ($thresholdHours === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$staleFeeds = Feed::stale($thresholdHours)->get();
|
||||||
|
|
||||||
|
foreach ($staleFeeds as $feed) {
|
||||||
|
$alreadyNotified = Notification::query()
|
||||||
|
->where('type', NotificationTypeEnum::FEED_STALE)
|
||||||
|
->where('notifiable_type', $feed->getMorphClass())
|
||||||
|
->where('notifiable_id', $feed->getKey())
|
||||||
|
->unread()
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($alreadyNotified) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$notificationService->send(
|
||||||
|
type: NotificationTypeEnum::FEED_STALE,
|
||||||
|
severity: NotificationSeverityEnum::WARNING,
|
||||||
|
title: "Feed \"{$feed->name}\" is stale",
|
||||||
|
message: $feed->last_fetched_at
|
||||||
|
? "Last fetched {$feed->last_fetched_at->diffForHumans()}. Threshold is {$thresholdHours} hours."
|
||||||
|
: "This feed has never been fetched. Threshold is {$thresholdHours} hours.",
|
||||||
|
notifiable: $feed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,8 @@ class Settings extends Component
|
||||||
|
|
||||||
public int $articlePublishingInterval = 5;
|
public int $articlePublishingInterval = 5;
|
||||||
|
|
||||||
|
public int $feedStalenessThreshold = 48;
|
||||||
|
|
||||||
public ?string $successMessage = null;
|
public ?string $successMessage = null;
|
||||||
|
|
||||||
public ?string $errorMessage = null;
|
public ?string $errorMessage = null;
|
||||||
|
|
@ -22,6 +24,7 @@ public function mount(): void
|
||||||
$this->articleProcessingEnabled = Setting::isArticleProcessingEnabled();
|
$this->articleProcessingEnabled = Setting::isArticleProcessingEnabled();
|
||||||
$this->publishingApprovalsEnabled = Setting::isPublishingApprovalsEnabled();
|
$this->publishingApprovalsEnabled = Setting::isPublishingApprovalsEnabled();
|
||||||
$this->articlePublishingInterval = Setting::getArticlePublishingInterval();
|
$this->articlePublishingInterval = Setting::getArticlePublishingInterval();
|
||||||
|
$this->feedStalenessThreshold = Setting::getFeedStalenessThreshold();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleArticleProcessing(): void
|
public function toggleArticleProcessing(): void
|
||||||
|
|
@ -48,6 +51,16 @@ public function updateArticlePublishingInterval(): void
|
||||||
$this->showSuccess();
|
$this->showSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updateFeedStalenessThreshold(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'feedStalenessThreshold' => 'required|integer|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Setting::setFeedStalenessThreshold($this->feedStalenessThreshold);
|
||||||
|
$this->showSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
protected function showSuccess(): void
|
protected function showSuccess(): void
|
||||||
{
|
{
|
||||||
$this->successMessage = 'Settings updated successfully!';
|
$this->successMessage = 'Settings updated successfully!';
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Database\Factories\FeedFactory;
|
use Database\Factories\FeedFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
@ -86,6 +87,19 @@ public function getStatusAttribute(): string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Builder<Feed> $query
|
||||||
|
* @return Builder<Feed>
|
||||||
|
*/
|
||||||
|
public function scopeStale(Builder $query, int $thresholdHours): Builder
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true)
|
||||||
|
->where(function (Builder $query) use ($thresholdHours) {
|
||||||
|
$query->whereNull('last_fetched_at')
|
||||||
|
->orWhere('last_fetched_at', '<', now()->subHours($thresholdHours));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsToMany<PlatformChannel, $this>
|
* @return BelongsToMany<PlatformChannel, $this>
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -71,4 +71,14 @@ public static function setArticlePublishingInterval(int $minutes): void
|
||||||
{
|
{
|
||||||
static::set('article_publishing_interval', (string) $minutes);
|
static::set('article_publishing_interval', (string) $minutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getFeedStalenessThreshold(): int
|
||||||
|
{
|
||||||
|
return (int) static::get('feed_staleness_threshold', 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function setFeedStalenessThreshold(int $hours): void
|
||||||
|
{
|
||||||
|
static::set('feed_staleness_threshold', (string) $hours);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,51 @@ class="flex-shrink-0"
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Feed Monitoring Settings -->
|
||||||
|
<div class="bg-white rounded-lg shadow">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h2 class="text-lg font-medium text-gray-900 flex items-center">
|
||||||
|
<svg class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
|
||||||
|
</svg>
|
||||||
|
Feed Monitoring
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
Configure alerts for feeds that stop returning articles
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4 space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-gray-900">
|
||||||
|
Staleness Threshold (hours)
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Alert when a feed hasn't been fetched for this many hours. Set to 0 to disable.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
wire:model="feedStalenessThreshold"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
class="w-20 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
wire:click="updateFeedStalenessThreshold"
|
||||||
|
class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@error('feedStalenessThreshold')
|
||||||
|
<p class="text-sm text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Status Messages -->
|
<!-- Status Messages -->
|
||||||
@if ($successMessage)
|
@if ($successMessage)
|
||||||
<div class="bg-green-50 border border-green-200 rounded-md p-4">
|
<div class="bg-green-50 border border-green-200 rounded-md p-4">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Jobs\ArticleDiscoveryJob;
|
use App\Jobs\ArticleDiscoveryJob;
|
||||||
|
use App\Jobs\CheckFeedStalenessJob;
|
||||||
use App\Jobs\PublishNextArticleJob;
|
use App\Jobs\PublishNextArticleJob;
|
||||||
use App\Jobs\SyncChannelPostsJob;
|
use App\Jobs\SyncChannelPostsJob;
|
||||||
use Illuminate\Support\Facades\Schedule;
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
|
@ -20,3 +21,9 @@
|
||||||
->name('refresh-articles')
|
->name('refresh-articles')
|
||||||
->withoutOverlapping()
|
->withoutOverlapping()
|
||||||
->onOneServer();
|
->onOneServer();
|
||||||
|
|
||||||
|
Schedule::job(new CheckFeedStalenessJob)
|
||||||
|
->hourly()
|
||||||
|
->name('check-feed-staleness')
|
||||||
|
->withoutOverlapping()
|
||||||
|
->onOneServer();
|
||||||
|
|
|
||||||
133
tests/Feature/Jobs/CheckFeedStalenessJobTest.php
Normal file
133
tests/Feature/Jobs/CheckFeedStalenessJobTest.php
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Jobs;
|
||||||
|
|
||||||
|
use App\Enums\NotificationSeverityEnum;
|
||||||
|
use App\Enums\NotificationTypeEnum;
|
||||||
|
use App\Jobs\CheckFeedStalenessJob;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use App\Models\Notification;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class CheckFeedStalenessJobTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_stale_feed_creates_notification(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create([
|
||||||
|
'is_active' => true,
|
||||||
|
'last_fetched_at' => now()->subHours(50),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('notifications', [
|
||||||
|
'type' => NotificationTypeEnum::FEED_STALE->value,
|
||||||
|
'severity' => NotificationSeverityEnum::WARNING->value,
|
||||||
|
'notifiable_type' => $feed->getMorphClass(),
|
||||||
|
'notifiable_id' => $feed->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fresh_feed_does_not_create_notification(): void
|
||||||
|
{
|
||||||
|
Feed::factory()->create([
|
||||||
|
'is_active' => true,
|
||||||
|
'last_fetched_at' => now()->subHours(10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('notifications', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_inactive_feed_does_not_create_notification(): void
|
||||||
|
{
|
||||||
|
Feed::factory()->create([
|
||||||
|
'is_active' => false,
|
||||||
|
'last_fetched_at' => now()->subHours(100),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('notifications', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_does_not_create_duplicate_notification_when_unread_exists(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create([
|
||||||
|
'is_active' => true,
|
||||||
|
'last_fetched_at' => now()->subHours(50),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Notification::factory()
|
||||||
|
->type(NotificationTypeEnum::FEED_STALE)
|
||||||
|
->unread()
|
||||||
|
->create([
|
||||||
|
'notifiable_type' => $feed->getMorphClass(),
|
||||||
|
'notifiable_id' => $feed->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('notifications', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_creates_new_notification_when_previous_is_read(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create([
|
||||||
|
'is_active' => true,
|
||||||
|
'last_fetched_at' => now()->subHours(50),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Notification::factory()
|
||||||
|
->type(NotificationTypeEnum::FEED_STALE)
|
||||||
|
->read()
|
||||||
|
->create([
|
||||||
|
'notifiable_type' => $feed->getMorphClass(),
|
||||||
|
'notifiable_id' => $feed->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('notifications', 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_threshold_zero_disables_check(): void
|
||||||
|
{
|
||||||
|
Setting::setFeedStalenessThreshold(0);
|
||||||
|
|
||||||
|
Feed::factory()->create([
|
||||||
|
'is_active' => true,
|
||||||
|
'last_fetched_at' => now()->subHours(100),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('notifications', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_never_fetched_feed_creates_notification(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create([
|
||||||
|
'is_active' => true,
|
||||||
|
'last_fetched_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dispatch();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('notifications', [
|
||||||
|
'type' => NotificationTypeEnum::FEED_STALE->value,
|
||||||
|
'notifiable_type' => $feed->getMorphClass(),
|
||||||
|
'notifiable_id' => $feed->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dispatch(): void
|
||||||
|
{
|
||||||
|
(new CheckFeedStalenessJob)->handle(app(\App\Services\Notification\NotificationService::class));
|
||||||
|
}
|
||||||
|
}
|
||||||
54
tests/Feature/Livewire/SettingsTest.php
Normal file
54
tests/Feature/Livewire/SettingsTest.php
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Livewire;
|
||||||
|
|
||||||
|
use App\Livewire\Settings;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class SettingsTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_mount_loads_feed_staleness_threshold(): void
|
||||||
|
{
|
||||||
|
Setting::setFeedStalenessThreshold(72);
|
||||||
|
|
||||||
|
Livewire::test(Settings::class)
|
||||||
|
->assertSet('feedStalenessThreshold', 72);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_mount_loads_default_feed_staleness_threshold(): void
|
||||||
|
{
|
||||||
|
Livewire::test(Settings::class)
|
||||||
|
->assertSet('feedStalenessThreshold', 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_feed_staleness_threshold_saves_value(): void
|
||||||
|
{
|
||||||
|
Livewire::test(Settings::class)
|
||||||
|
->set('feedStalenessThreshold', 24)
|
||||||
|
->call('updateFeedStalenessThreshold')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
$this->assertSame(24, Setting::getFeedStalenessThreshold());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_feed_staleness_threshold_validates_minimum(): void
|
||||||
|
{
|
||||||
|
Livewire::test(Settings::class)
|
||||||
|
->set('feedStalenessThreshold', -1)
|
||||||
|
->call('updateFeedStalenessThreshold')
|
||||||
|
->assertHasErrors(['feedStalenessThreshold' => 'min']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_feed_staleness_threshold_shows_success_message(): void
|
||||||
|
{
|
||||||
|
Livewire::test(Settings::class)
|
||||||
|
->set('feedStalenessThreshold', 24)
|
||||||
|
->call('updateFeedStalenessThreshold')
|
||||||
|
->assertSet('successMessage', 'Settings updated successfully!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -313,6 +313,56 @@ public function test_feed_settings_can_be_complex_structure(): void
|
||||||
$this->assertTrue($feed->settings['schedule']['enabled']);
|
$this->assertTrue($feed->settings['schedule']['enabled']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_scope_stale_includes_active_feeds_with_old_last_fetched_at(): void
|
||||||
|
{
|
||||||
|
$staleFeed = Feed::factory()->create([
|
||||||
|
'is_active' => true,
|
||||||
|
'last_fetched_at' => now()->subHours(50),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$staleFeeds = Feed::stale(48)->get();
|
||||||
|
|
||||||
|
$this->assertCount(1, $staleFeeds);
|
||||||
|
$this->assertTrue($staleFeeds->contains('id', $staleFeed->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_scope_stale_includes_active_feeds_never_fetched(): void
|
||||||
|
{
|
||||||
|
$neverFetchedFeed = Feed::factory()->create([
|
||||||
|
'is_active' => true,
|
||||||
|
'last_fetched_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$staleFeeds = Feed::stale(48)->get();
|
||||||
|
|
||||||
|
$this->assertCount(1, $staleFeeds);
|
||||||
|
$this->assertTrue($staleFeeds->contains('id', $neverFetchedFeed->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_scope_stale_excludes_fresh_feeds(): void
|
||||||
|
{
|
||||||
|
Feed::factory()->create([
|
||||||
|
'is_active' => true,
|
||||||
|
'last_fetched_at' => now()->subHours(10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$staleFeeds = Feed::stale(48)->get();
|
||||||
|
|
||||||
|
$this->assertCount(0, $staleFeeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_scope_stale_excludes_inactive_feeds(): void
|
||||||
|
{
|
||||||
|
Feed::factory()->create([
|
||||||
|
'is_active' => false,
|
||||||
|
'last_fetched_at' => now()->subHours(100),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$staleFeeds = Feed::stale(48)->get();
|
||||||
|
|
||||||
|
$this->assertCount(0, $staleFeeds);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_feed_can_have_null_last_fetched_at(): void
|
public function test_feed_can_have_null_last_fetched_at(): void
|
||||||
{
|
{
|
||||||
$feed = Feed::factory()->create(['last_fetched_at' => null]);
|
$feed = Feed::factory()->create(['last_fetched_at' => null]);
|
||||||
|
|
|
||||||
|
|
@ -39,4 +39,34 @@ public function test_set_article_publishing_interval_zero(): void
|
||||||
|
|
||||||
$this->assertSame(0, Setting::getArticlePublishingInterval());
|
$this->assertSame(0, Setting::getArticlePublishingInterval());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_get_feed_staleness_threshold_returns_default_when_not_set(): void
|
||||||
|
{
|
||||||
|
$this->assertSame(48, Setting::getFeedStalenessThreshold());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_feed_staleness_threshold_returns_stored_value(): void
|
||||||
|
{
|
||||||
|
Setting::set('feed_staleness_threshold', '72');
|
||||||
|
|
||||||
|
$this->assertSame(72, Setting::getFeedStalenessThreshold());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_set_feed_staleness_threshold_persists_value(): void
|
||||||
|
{
|
||||||
|
Setting::setFeedStalenessThreshold(24);
|
||||||
|
|
||||||
|
$this->assertSame(24, Setting::getFeedStalenessThreshold());
|
||||||
|
$this->assertDatabaseHas('settings', [
|
||||||
|
'key' => 'feed_staleness_threshold',
|
||||||
|
'value' => '24',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_set_feed_staleness_threshold_zero(): void
|
||||||
|
{
|
||||||
|
Setting::setFeedStalenessThreshold(0);
|
||||||
|
|
||||||
|
$this->assertSame(0, Setting::getFeedStalenessThreshold());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue