fedi-feed-router/tests/Unit/Jobs/PublishNextArticleJobTest.php

445 lines
14 KiB
PHP
Raw Normal View History

2025-08-15 02:50:42 +02:00
<?php
namespace Tests\Unit\Jobs;
use App\Exceptions\PublishException;
use App\Jobs\PublishNextArticleJob;
use App\Models\Article;
use App\Models\ArticlePublication;
use App\Models\Feed;
use App\Models\Setting;
2025-08-15 02:50:42 +02:00
use App\Services\Article\ArticleFetcher;
use App\Services\Publishing\ArticlePublishingService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class PublishNextArticleJobTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
}
public function test_constructor_sets_correct_queue(): void
{
$job = new PublishNextArticleJob;
2025-08-15 02:50:42 +02:00
$this->assertEquals('publishing', $job->queue);
}
public function test_job_implements_should_queue(): void
{
$job = new PublishNextArticleJob;
2025-08-15 02:50:42 +02:00
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job);
}
public function test_job_implements_should_be_unique(): void
{
$job = new PublishNextArticleJob;
2025-08-15 02:50:42 +02:00
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldBeUnique::class, $job);
}
public function test_job_has_unique_for_property(): void
{
$job = new PublishNextArticleJob;
2025-08-15 02:50:42 +02:00
$this->assertEquals(300, $job->uniqueFor);
}
public function test_job_uses_queueable_trait(): void
{
$job = new PublishNextArticleJob;
2025-08-15 02:50:42 +02:00
$this->assertContains(
\Illuminate\Foundation\Queue\Queueable::class,
class_uses($job)
);
}
public function test_handle_returns_early_when_no_approved_articles(): void
{
// Arrange - No articles exist
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
// No expectations as handle should return early
$job = new PublishNextArticleJob;
2025-08-15 02:50:42 +02:00
// Act
$publishingServiceMock = \Mockery::mock(ArticlePublishingService::class);
$job->handle($articleFetcherMock, $publishingServiceMock);
// Assert - Should complete without error
$this->assertTrue(true);
}
public function test_handle_returns_early_when_no_unpublished_approved_articles(): void
{
// Arrange
$feed = Feed::factory()->create();
$article = Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'approved',
2025-08-15 02:50:42 +02:00
]);
2025-08-15 02:50:42 +02:00
// Create a publication record to mark it as already published
ArticlePublication::factory()->create(['article_id' => $article->id]);
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
// No expectations as handle should return early
$job = new PublishNextArticleJob;
2025-08-15 02:50:42 +02:00
// Act
$publishingServiceMock = \Mockery::mock(ArticlePublishingService::class);
$job->handle($articleFetcherMock, $publishingServiceMock);
// Assert - Should complete without error
$this->assertTrue(true);
}
public function test_handle_skips_non_approved_articles(): void
{
// Arrange
$feed = Feed::factory()->create();
Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'pending',
2025-08-15 02:50:42 +02:00
]);
Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'rejected',
2025-08-15 02:50:42 +02:00
]);
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
// No expectations as handle should return early
$job = new PublishNextArticleJob;
2025-08-15 02:50:42 +02:00
// Act
$publishingServiceMock = \Mockery::mock(ArticlePublishingService::class);
$job->handle($articleFetcherMock, $publishingServiceMock);
// Assert - Should complete without error (no approved articles to process)
$this->assertTrue(true);
}
public function test_handle_publishes_oldest_approved_article(): void
{
// Arrange
$feed = Feed::factory()->create();
2025-08-15 02:50:42 +02:00
// Create older article first
$olderArticle = Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'approved',
'created_at' => now()->subHours(2),
2025-08-15 02:50:42 +02:00
]);
2025-08-15 02:50:42 +02:00
// Create newer article
$newerArticle = Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'approved',
'created_at' => now()->subHour(),
2025-08-15 02:50:42 +02:00
]);
$extractedData = ['title' => 'Test Article', 'content' => 'Test content'];
// Mock ArticleFetcher
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
$articleFetcherMock->shouldReceive('fetchArticleData')
->once()
->with(Mockery::on(function ($article) use ($olderArticle) {
return $article->id === $olderArticle->id;
}))
->andReturn($extractedData);
// Mock ArticlePublishingService
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
$publishingServiceMock->shouldReceive('publishToRoutedChannels')
->once()
->with(
Mockery::on(function ($article) use ($olderArticle) {
return $article->id === $olderArticle->id;
}),
$extractedData
);
$job = new PublishNextArticleJob;
2025-08-15 02:50:42 +02:00
// Act
$job->handle($articleFetcherMock, $publishingServiceMock);
// Assert - Mockery expectations are verified in tearDown
$this->assertTrue(true);
}
public function test_handle_throws_exception_on_publishing_failure(): void
{
// Arrange
$feed = Feed::factory()->create();
$article = Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'approved',
2025-08-15 02:50:42 +02:00
]);
$extractedData = ['title' => 'Test Article'];
$publishException = new PublishException($article, null);
// Mock ArticleFetcher
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
$articleFetcherMock->shouldReceive('fetchArticleData')
->once()
->with(Mockery::type(Article::class))
->andReturn($extractedData);
// Mock ArticlePublishingService to throw exception
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
$publishingServiceMock->shouldReceive('publishToRoutedChannels')
->once()
->andThrow($publishException);
$job = new PublishNextArticleJob;
2025-08-15 02:50:42 +02:00
// Assert
$this->expectException(PublishException::class);
// Act
$job->handle($articleFetcherMock, $publishingServiceMock);
}
public function test_handle_logs_publishing_start(): void
{
// Arrange
$feed = Feed::factory()->create();
$article = Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'approved',
'title' => 'Test Article Title',
'url' => 'https://example.com/article',
2025-08-15 02:50:42 +02:00
]);
$extractedData = ['title' => 'Test Article'];
// Mock ArticleFetcher
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
$articleFetcherMock->shouldReceive('fetchArticleData')
->once()
->andReturn($extractedData);
// Mock ArticlePublishingService
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
$publishingServiceMock->shouldReceive('publishToRoutedChannels')->once();
$job = new PublishNextArticleJob;
2025-08-15 02:50:42 +02:00
// Act
$job->handle($articleFetcherMock, $publishingServiceMock);
// Assert - Verify the job completes (logging is verified by observing no exceptions)
$this->assertTrue(true);
}
public function test_job_can_be_serialized(): void
{
$job = new PublishNextArticleJob;
2025-08-15 02:50:42 +02:00
$serialized = serialize($job);
$unserialized = unserialize($serialized);
2025-08-15 02:50:42 +02:00
$this->assertInstanceOf(PublishNextArticleJob::class, $unserialized);
$this->assertEquals($job->queue, $unserialized->queue);
$this->assertEquals($job->uniqueFor, $unserialized->uniqueFor);
}
public function test_handle_fetches_article_data_before_publishing(): void
{
// Arrange
$feed = Feed::factory()->create();
$article = Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'approved',
2025-08-15 02:50:42 +02:00
]);
$extractedData = ['title' => 'Extracted Title', 'content' => 'Extracted Content'];
// Mock ArticleFetcher with specific expectations
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
$articleFetcherMock->shouldReceive('fetchArticleData')
->once()
->with(Mockery::type(Article::class))
->andReturn($extractedData);
// Mock publishing service to receive the extracted data
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
$publishingServiceMock->shouldReceive('publishToRoutedChannels')
->once()
->with(Mockery::type(Article::class), $extractedData);
$job = new PublishNextArticleJob;
2025-08-15 02:50:42 +02:00
// Act
$job->handle($articleFetcherMock, $publishingServiceMock);
// Assert - Mockery expectations verified in tearDown
$this->assertTrue(true);
}
public function test_handle_skips_publishing_when_last_publication_within_interval(): void
{
$feed = Feed::factory()->create();
$article = Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'approved',
]);
// Last publication was 3 minutes ago, interval is 10 minutes
ArticlePublication::factory()->create([
'published_at' => now()->subMinutes(3),
]);
Setting::setArticlePublishingInterval(10);
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
// Neither should be called
$articleFetcherMock->shouldNotReceive('fetchArticleData');
$publishingServiceMock->shouldNotReceive('publishToRoutedChannels');
$job = new PublishNextArticleJob;
$job->handle($articleFetcherMock, $publishingServiceMock);
$this->assertTrue(true);
}
public function test_handle_publishes_when_last_publication_beyond_interval(): void
{
$feed = Feed::factory()->create();
$article = Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'approved',
]);
// Last publication was 15 minutes ago, interval is 10 minutes
ArticlePublication::factory()->create([
'published_at' => now()->subMinutes(15),
]);
Setting::setArticlePublishingInterval(10);
$extractedData = ['title' => 'Test Article'];
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
$articleFetcherMock->shouldReceive('fetchArticleData')
->once()
->andReturn($extractedData);
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
$publishingServiceMock->shouldReceive('publishToRoutedChannels')
->once();
$job = new PublishNextArticleJob;
$job->handle($articleFetcherMock, $publishingServiceMock);
$this->assertTrue(true);
}
public function test_handle_publishes_when_interval_is_zero(): void
{
$feed = Feed::factory()->create();
$article = Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'approved',
]);
// Last publication was just now, but interval is 0
ArticlePublication::factory()->create([
'published_at' => now(),
]);
Setting::setArticlePublishingInterval(0);
$extractedData = ['title' => 'Test Article'];
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
$articleFetcherMock->shouldReceive('fetchArticleData')
->once()
->andReturn($extractedData);
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
$publishingServiceMock->shouldReceive('publishToRoutedChannels')
->once();
$job = new PublishNextArticleJob;
$job->handle($articleFetcherMock, $publishingServiceMock);
$this->assertTrue(true);
}
public function test_handle_publishes_when_last_publication_exactly_at_interval(): void
{
$feed = Feed::factory()->create();
$article = Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'approved',
]);
// Last publication was exactly 10 minutes ago, interval is 10 minutes — should publish
ArticlePublication::factory()->create([
'published_at' => now()->subMinutes(10),
]);
Setting::setArticlePublishingInterval(10);
$extractedData = ['title' => 'Test Article'];
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
$articleFetcherMock->shouldReceive('fetchArticleData')
->once()
->andReturn($extractedData);
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
$publishingServiceMock->shouldReceive('publishToRoutedChannels')
->once();
$job = new PublishNextArticleJob;
$job->handle($articleFetcherMock, $publishingServiceMock);
$this->assertTrue(true);
}
public function test_handle_publishes_when_no_previous_publications_exist(): void
{
$feed = Feed::factory()->create();
$article = Article::factory()->create([
'feed_id' => $feed->id,
'approval_status' => 'approved',
]);
Setting::setArticlePublishingInterval(10);
$extractedData = ['title' => 'Test Article'];
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
$articleFetcherMock->shouldReceive('fetchArticleData')
->once()
->andReturn($extractedData);
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
$publishingServiceMock->shouldReceive('publishToRoutedChannels')
->once();
$job = new PublishNextArticleJob;
$job->handle($articleFetcherMock, $publishingServiceMock);
$this->assertTrue(true);
}
2025-08-15 02:50:42 +02:00
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
}