articleFetcher = Mockery::mock(ArticleFetcher::class); $this->validationService = new ValidationService($this->articleFetcher); } protected function tearDown(): void { Mockery::close(); parent::tearDown(); } private function mockFetchReturning(Article $article, ?string $content, ?string $title = 'Test Title', ?string $description = 'Test description'): void { $data = []; if ($title) { $data['title'] = $title; } if ($description) { $data['description'] = $description; } if ($content) { $data['full_article'] = $content; } $this->articleFetcher ->shouldReceive('fetchArticleData') ->with($article) ->once() ->andReturn($data); } public function test_validate_sets_validated_at_on_article(): void { $feed = Feed::factory()->create(); /** @var Route $route */ $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', ]); $article = Article::factory()->create(['feed_id' => $feed->id]); $this->mockFetchReturning($article, 'Article about Belgium'); $this->validationService->validate($article); $this->assertNotNull($article->fresh()->validated_at); } public function test_validate_creates_route_articles_for_active_routes(): void { $feed = Feed::factory()->create(); Route::factory()->active()->create(['feed_id' => $feed->id]); Route::factory()->active()->create(['feed_id' => $feed->id]); $article = Article::factory()->create(['feed_id' => $feed->id]); $this->mockFetchReturning($article, 'Some article content'); $this->validationService->validate($article); $this->assertCount(2, RouteArticle::where('article_id', $article->id)->get()); } public function test_validate_skips_inactive_routes(): void { $feed = Feed::factory()->create(); Route::factory()->active()->create(['feed_id' => $feed->id]); Route::factory()->inactive()->create(['feed_id' => $feed->id]); $article = Article::factory()->create(['feed_id' => $feed->id]); $this->mockFetchReturning($article, 'Some article content'); $this->validationService->validate($article); $this->assertCount(1, RouteArticle::where('article_id', $article->id)->get()); } public function test_validate_sets_pending_when_keywords_match(): void { $feed = Feed::factory()->create(); /** @var Route $route */ $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', ]); $article = Article::factory()->create(['feed_id' => $feed->id]); $this->mockFetchReturning($article, 'Article about Belgium politics'); $this->validationService->validate($article); $routeArticle = RouteArticle::where('article_id', $article->id)->first(); $this->assertEquals(ApprovalStatusEnum::PENDING, $routeArticle->approval_status); } public function test_validate_sets_rejected_when_no_keywords_match(): void { $feed = Feed::factory()->create(); /** @var Route $route */ $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', ]); $article = Article::factory()->create(['feed_id' => $feed->id]); $this->mockFetchReturning($article, 'Article about random topics and weather'); $this->validationService->validate($article); $routeArticle = RouteArticle::where('article_id', $article->id)->first(); $this->assertEquals(ApprovalStatusEnum::REJECTED, $routeArticle->approval_status); } public function test_validate_sets_pending_when_route_has_no_keywords(): void { $feed = Feed::factory()->create(); Route::factory()->active()->create(['feed_id' => $feed->id]); $article = Article::factory()->create(['feed_id' => $feed->id]); $this->mockFetchReturning($article, 'Article about random topics'); $this->validationService->validate($article); $routeArticle = RouteArticle::where('article_id', $article->id)->first(); $this->assertEquals(ApprovalStatusEnum::PENDING, $routeArticle->approval_status); } public function test_validate_different_routes_get_different_statuses(): void { $feed = Feed::factory()->create(); $channel1 = PlatformChannel::factory()->create(); $channel2 = PlatformChannel::factory()->create(); Route::factory()->active()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel1->id, ]); Route::factory()->active()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel2->id, ]); Keyword::factory()->active()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel1->id, 'keyword' => 'Belgium', ]); Keyword::factory()->active()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel2->id, 'keyword' => 'Technology', ]); $article = Article::factory()->create(['feed_id' => $feed->id]); $this->mockFetchReturning($article, 'Article about Belgium'); $this->validationService->validate($article); $ra1 = RouteArticle::where('article_id', $article->id) ->where('platform_channel_id', $channel1->id)->first(); $ra2 = RouteArticle::where('article_id', $article->id) ->where('platform_channel_id', $channel2->id)->first(); $this->assertEquals(ApprovalStatusEnum::PENDING, $ra1->approval_status); $this->assertEquals(ApprovalStatusEnum::REJECTED, $ra2->approval_status); } public function test_validate_auto_approves_when_global_setting_off_and_keywords_match(): void { Setting::setBool('enable_publishing_approvals', false); $feed = Feed::factory()->create(); /** @var Route $route */ $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', ]); $article = Article::factory()->create(['feed_id' => $feed->id]); $this->mockFetchReturning($article, 'Article about Belgium'); $this->validationService->validate($article); $routeArticle = RouteArticle::where('article_id', $article->id)->first(); $this->assertEquals(ApprovalStatusEnum::APPROVED, $routeArticle->approval_status); } public function test_validate_route_auto_approve_overrides_global_setting(): void { Setting::setBool('enable_publishing_approvals', true); $feed = Feed::factory()->create(); /** @var Route $route */ $route = Route::factory()->active()->create([ 'feed_id' => $feed->id, 'auto_approve' => true, ]); 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]); $this->mockFetchReturning($article, 'Article about Belgium'); $this->validationService->validate($article); $routeArticle = RouteArticle::where('article_id', $article->id)->first(); $this->assertEquals(ApprovalStatusEnum::APPROVED, $routeArticle->approval_status); } public function test_validate_route_auto_approve_false_overrides_global_off(): void { Setting::setBool('enable_publishing_approvals', false); $feed = Feed::factory()->create(); /** @var Route $route */ $route = Route::factory()->active()->create([ 'feed_id' => $feed->id, 'auto_approve' => false, ]); 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]); $this->mockFetchReturning($article, 'Article about Belgium'); $this->validationService->validate($article); $routeArticle = RouteArticle::where('article_id', $article->id)->first(); $this->assertEquals(ApprovalStatusEnum::PENDING, $routeArticle->approval_status); } public function test_validate_does_not_auto_approve_rejected_articles(): void { Setting::setBool('enable_publishing_approvals', false); $feed = Feed::factory()->create(); /** @var Route $route */ $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', ]); $article = Article::factory()->create(['feed_id' => $feed->id]); $this->mockFetchReturning($article, 'Random content no match'); $this->validationService->validate($article); $routeArticle = RouteArticle::where('article_id', $article->id)->first(); $this->assertEquals(ApprovalStatusEnum::REJECTED, $routeArticle->approval_status); } public function test_validate_creates_no_route_articles_when_content_fetch_fails(): void { $feed = Feed::factory()->create(); Route::factory()->active()->create(['feed_id' => $feed->id]); $article = Article::factory()->create(['feed_id' => $feed->id]); $this->mockFetchReturning($article, null); $this->validationService->validate($article); $this->assertCount(0, RouteArticle::where('article_id', $article->id)->get()); $this->assertNotNull($article->fresh()->validated_at); } public function test_validate_updates_article_metadata(): void { $feed = Feed::factory()->create(); Route::factory()->active()->create(['feed_id' => $feed->id]); $article = Article::factory()->create([ 'feed_id' => $feed->id, 'title' => 'Old Title', ]); $this->mockFetchReturning($article, 'Content about Belgium', 'New Title', 'New description'); $result = $this->validationService->validate($article); $this->assertEquals('New Title', $result->title); $this->assertEquals('New description', $result->description); $this->assertEquals('Content about Belgium', $result->content); } public function test_validate_sets_validated_at_on_route_articles(): void { $feed = Feed::factory()->create(); Route::factory()->active()->create(['feed_id' => $feed->id]); $article = Article::factory()->create(['feed_id' => $feed->id]); $this->mockFetchReturning($article, 'Content about something'); $this->validationService->validate($article); $routeArticle = RouteArticle::where('article_id', $article->id)->first(); $this->assertNotNull($routeArticle->validated_at); } public function test_validate_keyword_matching_is_case_insensitive(): void { $feed = Feed::factory()->create(); /** @var Route $route */ $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', ]); $article = Article::factory()->create(['feed_id' => $feed->id]); $this->mockFetchReturning($article, 'Article about BELGIUM politics'); $this->validationService->validate($article); $routeArticle = RouteArticle::where('article_id', $article->id)->first(); $this->assertEquals(ApprovalStatusEnum::PENDING, $routeArticle->approval_status); } public function test_validate_only_uses_active_keywords(): void { $feed = Feed::factory()->create(); /** @var Route $route */ $route = Route::factory()->active()->create(['feed_id' => $feed->id]); Keyword::factory()->inactive()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $route->platform_channel_id, 'keyword' => 'Belgium', ]); $article = Article::factory()->create(['feed_id' => $feed->id]); $this->mockFetchReturning($article, 'Article about Belgium'); $this->validationService->validate($article); // No active keywords = matches everything = pending $routeArticle = RouteArticle::where('article_id', $article->id)->first(); $this->assertEquals(ApprovalStatusEnum::PENDING, $routeArticle->approval_status); } }