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(); } }