fedi-feed-router/backend/tests/Unit/Jobs/ArticleDiscoveryForFeedJobTest.php
2025-08-15 02:58:14 +02:00

222 lines
No EOL
6.9 KiB
PHP

<?php
namespace Tests\Unit\Jobs;
use App\Jobs\ArticleDiscoveryForFeedJob;
use App\Models\Feed;
use App\Services\Article\ArticleFetcher;
use App\Services\Log\LogSaver;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Mockery;
use Tests\TestCase;
class ArticleDiscoveryForFeedJobTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Queue::fake();
}
public function test_constructor_sets_correct_queue(): void
{
$feed = Feed::factory()->make();
$job = new ArticleDiscoveryForFeedJob($feed);
$this->assertEquals('feed-discovery', $job->queue);
}
public function test_job_implements_should_queue(): void
{
$feed = Feed::factory()->make();
$job = new ArticleDiscoveryForFeedJob($feed);
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job);
}
public function test_job_uses_queueable_trait(): void
{
$feed = Feed::factory()->make();
$job = new ArticleDiscoveryForFeedJob($feed);
$this->assertContains(
\Illuminate\Foundation\Queue\Queueable::class,
class_uses($job)
);
}
public function test_handle_fetches_articles_and_updates_feed(): void
{
// Arrange
$feed = Feed::factory()->create([
'name' => 'Test Feed',
'url' => 'https://example.com/feed',
'last_fetched_at' => null
]);
$mockArticles = collect(['article1', 'article2']);
// Mock ArticleFetcher
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
$articleFetcherMock->shouldReceive('getArticlesFromFeed')
->once()
->with($feed)
->andReturn($mockArticles);
// Mock LogSaver
$logSaverMock = Mockery::mock(LogSaver::class);
$logSaverMock->shouldReceive('info')
->with('Starting feed article fetch', null, [
'feed_id' => $feed->id,
'feed_name' => $feed->name,
'feed_url' => $feed->url
])
->once();
$logSaverMock->shouldReceive('info')
->with('Feed article fetch completed', null, [
'feed_id' => $feed->id,
'feed_name' => $feed->name,
'articles_count' => 2
])
->once();
$job = new ArticleDiscoveryForFeedJob($feed);
// Act
$job->handle($logSaverMock, $articleFetcherMock);
// Assert
$feed->refresh();
$this->assertNotNull($feed->last_fetched_at);
$this->assertTrue($feed->last_fetched_at->greaterThan(now()->subMinute()));
}
public function test_dispatch_for_all_active_feeds_dispatches_jobs_with_delay(): void
{
// Arrange
$feeds = Feed::factory()->count(3)->create(['is_active' => true]);
Feed::factory()->create(['is_active' => false]); // inactive feed should be ignored
// Mock LogSaver
$logSaverMock = Mockery::mock(LogSaver::class);
$logSaverMock->shouldReceive('info')
->times(3) // Once for each active feed
->with('Dispatched feed discovery job', null, Mockery::type('array'));
$this->app->instance(LogSaver::class, $logSaverMock);
// Act
ArticleDiscoveryForFeedJob::dispatchForAllActiveFeeds();
// Assert
Queue::assertPushed(ArticleDiscoveryForFeedJob::class, 3);
// Verify jobs were dispatched (cannot access private $feed property in test)
}
public function test_dispatch_for_all_active_feeds_applies_correct_delays(): void
{
// Arrange
Feed::factory()->count(2)->create(['is_active' => true]);
// Mock LogSaver
$logSaverMock = Mockery::mock(LogSaver::class);
$logSaverMock->shouldReceive('info')->times(2);
$this->app->instance(LogSaver::class, $logSaverMock);
// Act
ArticleDiscoveryForFeedJob::dispatchForAllActiveFeeds();
// Assert
Queue::assertPushed(ArticleDiscoveryForFeedJob::class, 2);
// Verify jobs are pushed with delays
Queue::assertPushed(ArticleDiscoveryForFeedJob::class, function ($job) {
return $job->delay !== null;
});
}
public function test_dispatch_for_all_active_feeds_with_no_active_feeds(): void
{
// Arrange
Feed::factory()->count(2)->create(['is_active' => false]);
// Act
ArticleDiscoveryForFeedJob::dispatchForAllActiveFeeds();
// Assert
Queue::assertNothingPushed();
}
public function test_feed_discovery_delay_constant_exists(): void
{
$reflection = new \ReflectionClass(ArticleDiscoveryForFeedJob::class);
$constant = $reflection->getConstant('FEED_DISCOVERY_DELAY_MINUTES');
$this->assertEquals(5, $constant);
}
public function test_job_can_be_serialized(): void
{
$feed = Feed::factory()->create(['name' => 'Test Feed']);
$job = new ArticleDiscoveryForFeedJob($feed);
$serialized = serialize($job);
$unserialized = unserialize($serialized);
$this->assertInstanceOf(ArticleDiscoveryForFeedJob::class, $unserialized);
$this->assertEquals($job->queue, $unserialized->queue);
// Note: Cannot test feed property directly as it's private
// but serialization/unserialization working proves the job structure is intact
}
public function test_handle_logs_start_message_with_correct_context(): void
{
// Arrange
$feed = Feed::factory()->create([
'name' => 'Test Feed',
'url' => 'https://example.com/feed'
]);
$mockArticles = collect([]);
// Mock ArticleFetcher
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
$articleFetcherMock->shouldReceive('getArticlesFromFeed')
->once()
->andReturn($mockArticles);
// Mock LogSaver with specific expectations
$logSaverMock = Mockery::mock(LogSaver::class);
$logSaverMock->shouldReceive('info')
->with('Starting feed article fetch', null, [
'feed_id' => $feed->id,
'feed_name' => 'Test Feed',
'feed_url' => 'https://example.com/feed'
])
->once();
$logSaverMock->shouldReceive('info')
->with('Feed article fetch completed', null, Mockery::type('array'))
->once();
$job = new ArticleDiscoveryForFeedJob($feed);
// Act
$job->handle($logSaverMock, $articleFetcherMock);
// Assert - Mockery expectations are verified in tearDown
$this->assertTrue(true);
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
}