222 lines
6.9 KiB
PHP
222 lines
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();
|
||
|
|
}
|
||
|
|
}
|