From 16353fe60add0fe6bb5590f1194f0a2bdfdda945 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 9 Aug 2026 23:28:59 +0200 Subject: [PATCH] 119 - Fail publishing when article content cannot be recovered --- app/Actions/PublishRouteArticleAction.php | 13 +++- .../PublishApprovedArticleListenerTest.php | 4 +- .../Actions/PublishRouteArticleActionTest.php | 60 +++++++++++++++++++ tests/Unit/Jobs/PublishNextArticleJobTest.php | 16 ++--- 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/app/Actions/PublishRouteArticleAction.php b/app/Actions/PublishRouteArticleAction.php index deef6d52..46357425 100644 --- a/app/Actions/PublishRouteArticleAction.php +++ b/app/Actions/PublishRouteArticleAction.php @@ -35,7 +35,10 @@ public function execute(RouteArticle $routeArticle): PublishOutcome try { $extractedData = $this->resolvePublishData($article); - $outcome = $this->publishingService->publishRouteArticle($routeArticle, $extractedData); + + $outcome = $this->hasPublishableContent($extractedData) + ? $this->publishingService->publishRouteArticle($routeArticle, $extractedData) + : PublishOutcome::failure('Could not recover the article content to publish'); } catch (Exception $e) { $routeArticle->update(['publish_status' => PublishStatusEnum::ERROR]); @@ -64,6 +67,14 @@ public function execute(RouteArticle $routeArticle): PublishOutcome return $outcome; } + /** + * @param array $extractedData + */ + private function hasPublishableContent(array $extractedData): bool + { + return ! empty($extractedData['description']); + } + /** * @return array */ diff --git a/tests/Feature/Listeners/PublishApprovedArticleListenerTest.php b/tests/Feature/Listeners/PublishApprovedArticleListenerTest.php index 4f1cbe3c..3e80de61 100644 --- a/tests/Feature/Listeners/PublishApprovedArticleListenerTest.php +++ b/tests/Feature/Listeners/PublishApprovedArticleListenerTest.php @@ -74,7 +74,7 @@ public function test_no_publication_created_creates_warning_notification(): void { $routeArticle = $this->createApprovedRouteArticle(); - $extractedData = ['title' => 'Test Article']; + $extractedData = ['title' => 'Test Article', 'description' => 'Test description']; $articleFetcherMock = Mockery::mock(ArticleFetcher::class); $articleFetcherMock->shouldReceive('fetchArticleData') @@ -104,7 +104,7 @@ public function test_successful_publish_does_not_create_notification(): void { $routeArticle = $this->createApprovedRouteArticle(); - $extractedData = ['title' => 'Test Article']; + $extractedData = ['title' => 'Test Article', 'description' => 'Test description']; $articleFetcherMock = Mockery::mock(ArticleFetcher::class); $articleFetcherMock->shouldReceive('fetchArticleData') diff --git a/tests/Unit/Actions/PublishRouteArticleActionTest.php b/tests/Unit/Actions/PublishRouteArticleActionTest.php index d3798776..5a805bd5 100644 --- a/tests/Unit/Actions/PublishRouteArticleActionTest.php +++ b/tests/Unit/Actions/PublishRouteArticleActionTest.php @@ -3,6 +3,7 @@ namespace Tests\Unit\Actions; use App\Actions\PublishRouteArticleAction; +use App\Enums\NotificationTypeEnum; use App\Enums\PublishStatusEnum; use App\Models\Article; use App\Models\ArticlePublication; @@ -155,6 +156,65 @@ public function test_publish_falls_back_to_fetching_when_article_has_no_stored_d $this->assertSame(PublishStatusEnum::PUBLISHED, $routeArticle->fresh()->publish_status); } + public function test_publish_fails_when_fallback_fetch_returns_nothing(): void + { + $routeArticle = $this->createRouteArticle(['title' => 'Unreachable Article'], unvalidated: true); + + $fetcher = Mockery::mock(ArticleFetcher::class); + $fetcher->shouldReceive('fetchArticleData')->once()->andReturn([]); + + $publishingService = Mockery::mock(ArticlePublishingService::class); + $publishingService->shouldNotReceive('publishRouteArticle'); + + $outcome = (new PublishRouteArticleAction($fetcher, $publishingService, new NotificationService)) + ->execute($routeArticle); + + $this->assertTrue($outcome->failed()); + $this->assertSame(PublishStatusEnum::ERROR, $routeArticle->fresh()->publish_status); + $this->assertDatabaseCount('article_publications', 0); + $this->assertDatabaseHas('notifications', [ + 'type' => NotificationTypeEnum::PUBLISH_FAILED->value, + ]); + } + + public function test_publish_fails_when_fallback_recovers_only_a_title(): void + { + $routeArticle = $this->createRouteArticle([], unvalidated: true); + + $fetcher = Mockery::mock(ArticleFetcher::class); + $fetcher->shouldReceive('fetchArticleData')->once()->andReturn(['title' => 'Recovered Title']); + + $publishingService = Mockery::mock(ArticlePublishingService::class); + $publishingService->shouldNotReceive('publishRouteArticle'); + + $outcome = (new PublishRouteArticleAction($fetcher, $publishingService, new NotificationService)) + ->execute($routeArticle); + + $this->assertTrue($outcome->failed()); + $this->assertSame(PublishStatusEnum::ERROR, $routeArticle->fresh()->publish_status); + } + + public function test_publish_succeeds_when_fallback_returns_a_description_without_a_thumbnail(): void + { + $routeArticle = $this->createRouteArticle([], unvalidated: true); + + $fetched = ['title' => 'Recovered Title', 'description' => 'Recovered description']; + + $fetcher = Mockery::mock(ArticleFetcher::class); + $fetcher->shouldReceive('fetchArticleData')->once()->andReturn($fetched); + + $publishingService = Mockery::mock(ArticlePublishingService::class); + $publishingService->shouldReceive('publishRouteArticle') + ->once() + ->with(Mockery::any(), $fetched) + ->andReturn(PublishOutcome::published($this->makePublication())); + + (new PublishRouteArticleAction($fetcher, $publishingService, new NotificationService)) + ->execute($routeArticle); + + $this->assertSame(PublishStatusEnum::PUBLISHED, $routeArticle->fresh()->publish_status); + } + public function test_publish_does_not_refetch_for_articles_stored_before_image_url_existed(): void { $routeArticle = $this->createRouteArticle([ diff --git a/tests/Unit/Jobs/PublishNextArticleJobTest.php b/tests/Unit/Jobs/PublishNextArticleJobTest.php index 43913a39..63adbef7 100644 --- a/tests/Unit/Jobs/PublishNextArticleJobTest.php +++ b/tests/Unit/Jobs/PublishNextArticleJobTest.php @@ -157,7 +157,7 @@ public function test_handle_publishes_oldest_approved_route_article(): void 'created_at' => now()->subHour(), ]); - $extractedData = ['title' => 'Test Article', 'content' => 'Test content']; + $extractedData = ['title' => 'Test Article', 'description' => 'Test description']; $articleFetcherMock = Mockery::mock(ArticleFetcher::class); $articleFetcherMock->shouldReceive('fetchArticleData') @@ -185,7 +185,7 @@ public function test_handle_throws_exception_on_publishing_failure(): void $routeArticle = $this->createApprovedRouteArticle(); $article = $routeArticle->article; - $extractedData = ['title' => 'Test Article']; + $extractedData = ['title' => 'Test Article', 'description' => 'Test description']; $publishException = new PublishException($article, null); $articleFetcherMock = Mockery::mock(ArticleFetcher::class); @@ -235,7 +235,7 @@ public function test_handle_publishes_when_last_publication_beyond_interval(): v ]); Setting::setArticlePublishingInterval(10); - $extractedData = ['title' => 'Test Article']; + $extractedData = ['title' => 'Test Article', 'description' => 'Test description']; $articleFetcherMock = Mockery::mock(ArticleFetcher::class); $articleFetcherMock->shouldReceive('fetchArticleData') @@ -262,7 +262,7 @@ public function test_handle_publishes_when_interval_is_zero(): void ]); Setting::setArticlePublishingInterval(0); - $extractedData = ['title' => 'Test Article']; + $extractedData = ['title' => 'Test Article', 'description' => 'Test description']; $articleFetcherMock = Mockery::mock(ArticleFetcher::class); $articleFetcherMock->shouldReceive('fetchArticleData') @@ -289,7 +289,7 @@ public function test_handle_publishes_when_last_publication_exactly_at_interval( ]); Setting::setArticlePublishingInterval(10); - $extractedData = ['title' => 'Test Article']; + $extractedData = ['title' => 'Test Article', 'description' => 'Test description']; $articleFetcherMock = Mockery::mock(ArticleFetcher::class); $articleFetcherMock->shouldReceive('fetchArticleData') @@ -313,7 +313,7 @@ public function test_handle_publishes_when_no_previous_publications_exist(): voi Setting::setArticlePublishingInterval(10); - $extractedData = ['title' => 'Test Article']; + $extractedData = ['title' => 'Test Article', 'description' => 'Test description']; $articleFetcherMock = Mockery::mock(ArticleFetcher::class); $articleFetcherMock->shouldReceive('fetchArticleData') @@ -335,7 +335,7 @@ public function test_handle_creates_warning_notification_when_no_publication_cre { $routeArticle = $this->createApprovedRouteArticle(['title' => 'No Route Article']); - $extractedData = ['title' => 'No Route Article']; + $extractedData = ['title' => 'No Route Article', 'description' => 'Test description']; $articleFetcherMock = Mockery::mock(ArticleFetcher::class); $articleFetcherMock->shouldReceive('fetchArticleData') @@ -366,7 +366,7 @@ public function test_handle_creates_notification_on_publish_exception(): void $routeArticle = $this->createApprovedRouteArticle(['title' => 'Failing Article']); $article = $routeArticle->article; - $extractedData = ['title' => 'Failing Article']; + $extractedData = ['title' => 'Failing Article', 'description' => 'Test description']; $publishException = new PublishException($article, null); $articleFetcherMock = Mockery::mock(ArticleFetcher::class);