296 lines
No EOL
9.5 KiB
PHP
296 lines
No EOL
9.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Jobs;
|
|
|
|
use Domains\Platform\Exceptions\PublishException;
|
|
use Domains\Platform\Jobs\PublishNextArticleJob;
|
|
use Domains\Article\Models\Article;
|
|
use Domains\Article\Models\ArticlePublication;
|
|
use Domains\Feed\Models\Feed;
|
|
use Domains\Article\Services\ArticleFetcher;
|
|
use Domains\Platform\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();
|
|
|
|
$this->assertEquals('publishing', $job->queue);
|
|
}
|
|
|
|
public function test_job_implements_should_queue(): void
|
|
{
|
|
$job = new PublishNextArticleJob();
|
|
|
|
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job);
|
|
}
|
|
|
|
public function test_job_implements_should_be_unique(): void
|
|
{
|
|
$job = new PublishNextArticleJob();
|
|
|
|
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldBeUnique::class, $job);
|
|
}
|
|
|
|
public function test_job_has_unique_for_property(): void
|
|
{
|
|
$job = new PublishNextArticleJob();
|
|
|
|
$this->assertEquals(300, $job->uniqueFor);
|
|
}
|
|
|
|
public function test_job_uses_queueable_trait(): void
|
|
{
|
|
$job = new PublishNextArticleJob();
|
|
|
|
$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();
|
|
|
|
// 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'
|
|
]);
|
|
|
|
// 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();
|
|
|
|
// 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'
|
|
]);
|
|
Article::factory()->create([
|
|
'feed_id' => $feed->id,
|
|
'approval_status' => 'rejected'
|
|
]);
|
|
|
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
|
// No expectations as handle should return early
|
|
|
|
$job = new PublishNextArticleJob();
|
|
|
|
// 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();
|
|
|
|
// Create older article first
|
|
$olderArticle = Article::factory()->create([
|
|
'feed_id' => $feed->id,
|
|
'approval_status' => 'approved',
|
|
'created_at' => now()->subHours(2)
|
|
]);
|
|
|
|
// Create newer article
|
|
$newerArticle = Article::factory()->create([
|
|
'feed_id' => $feed->id,
|
|
'approval_status' => 'approved',
|
|
'created_at' => now()->subHour()
|
|
]);
|
|
|
|
$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();
|
|
|
|
// 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'
|
|
]);
|
|
|
|
$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();
|
|
|
|
// 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'
|
|
]);
|
|
|
|
$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();
|
|
|
|
// 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();
|
|
|
|
$serialized = serialize($job);
|
|
$unserialized = unserialize($serialized);
|
|
|
|
$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'
|
|
]);
|
|
|
|
$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();
|
|
|
|
// Act
|
|
$job->handle($articleFetcherMock, $publishingServiceMock);
|
|
|
|
// Assert - Mockery expectations verified in tearDown
|
|
$this->assertTrue(true);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
Mockery::close();
|
|
parent::tearDown();
|
|
}
|
|
} |