diff --git a/app/Listeners/ValidateArticleListener.php b/app/Listeners/ValidateArticleListener.php index 0ef3d9b..27d4431 100644 --- a/app/Listeners/ValidateArticleListener.php +++ b/app/Listeners/ValidateArticleListener.php @@ -5,7 +5,6 @@ use App\Enums\LogLevelEnum; use App\Events\ActionPerformed; use App\Events\NewArticleFetched; -use App\Models\Setting; use App\Services\Article\ValidationService; use Exception; use Illuminate\Contracts\Queue\ShouldQueue; @@ -26,38 +25,18 @@ public function handle(NewArticleFetched $event): void return; } - // Only validate articles that are still pending - if (! $article->isPending()) { - return; - } - // Skip if already has publication (prevents duplicate processing) if ($article->articlePublication()->exists()) { return; } try { - $article = $this->validationService->validate($article); + $this->validationService->validate($article); } catch (Exception $e) { ActionPerformed::dispatch('Article validation failed', LogLevelEnum::ERROR, [ 'article_id' => $article->id, 'error' => $e->getMessage(), ]); - - return; - } - - if ($article->isValid()) { - // Double-check publication doesn't exist (race condition protection) - if ($article->articlePublication()->exists()) { - return; - } - - // If approvals are enabled, article waits for manual approval. - // If approvals are disabled, auto-approve and publish. - if (! Setting::isPublishingApprovalsEnabled()) { - $article->approve(); - } } } } diff --git a/tests/Feature/JobsAndEventsTest.php b/tests/Feature/JobsAndEventsTest.php index ed85e4e..c398cbc 100644 --- a/tests/Feature/JobsAndEventsTest.php +++ b/tests/Feature/JobsAndEventsTest.php @@ -175,12 +175,17 @@ public function test_exception_logged_event_is_dispatched(): void public function test_validate_article_listener_processes_new_article(): void { - Event::fake([ArticleApproved::class]); - // Disable approvals so listener auto-approves valid articles Setting::setBool('enable_publishing_approvals', false); $feed = Feed::factory()->create(); + $route = \App\Models\Route::factory()->active()->create(['feed_id' => $feed->id]); + \App\Models\Keyword::factory()->active()->create([ + 'feed_id' => $feed->id, + 'platform_channel_id' => $route->platform_channel_id, + 'keyword' => 'Belgium', + ]); + $article = Article::factory()->create([ 'feed_id' => $feed->id, 'approval_status' => 'pending', @@ -203,45 +208,13 @@ public function test_validate_article_listener_processes_new_article(): void $listener->handle($event); $article->refresh(); - $this->assertEquals('approved', $article->approval_status); - Event::assertDispatched(ArticleApproved::class); + $this->assertNotNull($article->validated_at); + + $routeArticle = \App\Models\RouteArticle::where('article_id', $article->id)->first(); + $this->assertNotNull($routeArticle); + $this->assertEquals(\App\Enums\ApprovalStatusEnum::APPROVED, $routeArticle->approval_status); } - // Test removed - PublishApprovedArticle and ArticleReadyToPublish classes no longer exist - // public function test_publish_approved_article_listener_queues_job(): void - // { - // Event::fake(); - - // $article = Article::factory()->create([ - // 'approval_status' => 'approved', - // 'approval_status' => 'approved', - // ]); - - // $listener = new PublishApprovedArticle(); - // $event = new ArticleApproved($article); - - // $listener->handle($event); - - // Event::assertDispatched(ArticleReadyToPublish::class); - // } - - // Test removed - PublishArticle and ArticleReadyToPublish classes no longer exist - // public function test_publish_article_listener_queues_publish_job(): void - // { - // Queue::fake(); - - // $article = Article::factory()->create([ - // 'approval_status' => 'approved', - // ]); - - // $listener = new PublishArticle(); - // $event = new ArticleReadyToPublish($article); - - // $listener->handle($event); - - // Queue::assertPushed(PublishNextArticleJob::class); - // } - public function test_log_exception_to_database_listener_creates_log(): void { $log = Log::factory()->create([ diff --git a/tests/Feature/ValidateArticleListenerTest.php b/tests/Feature/ValidateArticleListenerTest.php index cf7366f..a5412ca 100644 --- a/tests/Feature/ValidateArticleListenerTest.php +++ b/tests/Feature/ValidateArticleListenerTest.php @@ -2,81 +2,95 @@ namespace Tests\Feature; -use App\Events\ArticleApproved; +use App\Enums\ApprovalStatusEnum; use App\Events\NewArticleFetched; use App\Listeners\ValidateArticleListener; use App\Models\Article; use App\Models\ArticlePublication; use App\Models\Feed; -use App\Services\Article\ValidationService; +use App\Models\Keyword; +use App\Models\Route; +use App\Models\RouteArticle; +use App\Services\Article\ArticleFetcher; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\Http; +use Mockery; use Tests\TestCase; class ValidateArticleListenerTest extends TestCase { use RefreshDatabase; - public function test_listener_validates_article_and_dispatches_ready_to_publish_event(): void + private function createListenerWithMockedFetcher(?string $content = 'Some article content'): ValidateArticleListener { - Event::fake([ArticleApproved::class]); + $articleFetcher = Mockery::mock(ArticleFetcher::class); + $articleFetcher->shouldReceive('fetchArticleData')->andReturn( + $content ? [ + 'title' => 'Test Title', + 'description' => 'Test description', + 'full_article' => $content, + ] : [] + ); - // Mock HTTP requests - Http::fake([ - 'https://example.com/article' => Http::response('Article content', 200), + return new ValidateArticleListener( + new \App\Services\Article\ValidationService($articleFetcher) + ); + } + + protected function tearDown(): void + { + Mockery::close(); + parent::tearDown(); + } + + public function test_listener_validates_article_and_creates_route_articles(): void + { + $feed = Feed::factory()->create(); + $route = Route::factory()->active()->create(['feed_id' => $feed->id]); + Keyword::factory()->active()->create([ + 'feed_id' => $feed->id, + 'platform_channel_id' => $route->platform_channel_id, + 'keyword' => 'Belgium', ]); - $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, - 'url' => 'https://example.com/article', 'approval_status' => 'pending', ]); - $listener = app(ValidateArticleListener::class); - $event = new NewArticleFetched($article); - - $listener->handle($event); + $listener = $this->createListenerWithMockedFetcher('Article about Belgium'); + $listener->handle(new NewArticleFetched($article)); $article->refresh(); + $this->assertNotNull($article->validated_at); - if ($article->isValid()) { - Event::assertDispatched(ArticleApproved::class, function (ArticleApproved $event) use ($article) { - return $event->article->id === $article->id; - }); - } else { - Event::assertNotDispatched(ArticleApproved::class); - } + $routeArticle = RouteArticle::where('article_id', $article->id)->first(); + $this->assertNotNull($routeArticle); + $this->assertEquals(ApprovalStatusEnum::PENDING, $routeArticle->approval_status); } public function test_listener_skips_already_validated_articles(): void { - Event::fake([ArticleApproved::class]); - $feed = Feed::factory()->create(); + Route::factory()->active()->create(['feed_id' => $feed->id]); + $article = Article::factory()->create([ 'feed_id' => $feed->id, - 'url' => 'https://example.com/article', - 'approval_status' => 'approved', + 'validated_at' => now(), ]); - $listener = app(ValidateArticleListener::class); - $event = new NewArticleFetched($article); + $listener = $this->createListenerWithMockedFetcher(); + $listener->handle(new NewArticleFetched($article)); - $listener->handle($event); - - Event::assertNotDispatched(ArticleApproved::class); + $this->assertCount(0, RouteArticle::where('article_id', $article->id)->get()); } public function test_listener_skips_articles_with_existing_publication(): void { - Event::fake([ArticleApproved::class]); - $feed = Feed::factory()->create(); + Route::factory()->active()->create(['feed_id' => $feed->id]); + $article = Article::factory()->create([ 'feed_id' => $feed->id, - 'url' => 'https://example.com/article', 'approval_status' => 'pending', ]); @@ -88,38 +102,30 @@ public function test_listener_skips_articles_with_existing_publication(): void 'published_by' => 'test-user', ]); - $listener = app(ValidateArticleListener::class); - $event = new NewArticleFetched($article); + $listener = $this->createListenerWithMockedFetcher(); + $listener->handle(new NewArticleFetched($article)); - $listener->handle($event); - - Event::assertNotDispatched(ArticleApproved::class); + $this->assertCount(0, RouteArticle::where('article_id', $article->id)->get()); } - public function test_listener_calls_validation_service(): void + public function test_listener_handles_validation_errors_gracefully(): void { - Event::fake([ArticleApproved::class]); + $articleFetcher = Mockery::mock(ArticleFetcher::class); + $articleFetcher->shouldReceive('fetchArticleData')->andThrow(new \Exception('Fetch failed')); - // Mock HTTP requests - Http::fake([ - 'https://example.com/article' => Http::response('Article content', 200), - ]); + $listener = new ValidateArticleListener( + new \App\Services\Article\ValidationService($articleFetcher) + ); $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, - 'url' => 'https://example.com/article', 'approval_status' => 'pending', ]); - $listener = app(ValidateArticleListener::class); - $event = new NewArticleFetched($article); + $listener->handle(new NewArticleFetched($article)); - $listener->handle($event); - - // Verify that the article was processed by ValidationService - $article->refresh(); - $this->assertNotEquals('pending', $article->approval_status, 'Article should have been validated'); - $this->assertContains($article->approval_status, ['approved', 'rejected'], 'Article should have validation result'); + $this->assertNull($article->fresh()->validated_at); + $this->assertCount(0, RouteArticle::where('article_id', $article->id)->get()); } }