fedi-feed-router/tests/Unit/Services/ValidationServiceTest.php

383 lines
14 KiB
PHP
Raw Normal View History

2025-08-02 03:48:06 +02:00
<?php
namespace Tests\Unit\Services;
use App\Enums\ApprovalStatusEnum;
2025-08-02 03:48:06 +02:00
use App\Models\Article;
use App\Models\Feed;
use App\Models\Keyword;
use App\Models\PlatformChannel;
use App\Models\Route;
use App\Models\RouteArticle;
use App\Models\Setting;
use App\Services\Article\ArticleFetcher;
use App\Services\Article\ValidationService;
2025-08-02 03:48:06 +02:00
use Illuminate\Foundation\Testing\RefreshDatabase;
2025-08-15 02:50:42 +02:00
use Mockery;
use Mockery\MockInterface;
use Tests\TestCase;
2025-08-02 03:48:06 +02:00
class ValidationServiceTest extends TestCase
{
use RefreshDatabase;
2025-08-15 02:50:42 +02:00
private ValidationService $validationService;
private MockInterface $articleFetcher;
2025-08-15 02:50:42 +02:00
protected function setUp(): void
{
parent::setUp();
$this->articleFetcher = Mockery::mock(ArticleFetcher::class);
$this->validationService = new ValidationService($this->articleFetcher);
2025-08-15 02:50:42 +02:00
}
2025-08-15 02:50:42 +02:00
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
2025-08-02 03:48:06 +02:00
private function mockFetchReturning(Article $article, ?string $content, ?string $title = 'Test Title', ?string $description = 'Test description'): void
2025-08-02 03:48:06 +02:00
{
$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);
}
2025-08-10 15:20:28 +02:00
public function test_validate_sets_validated_at_on_article(): void
{
2025-08-02 03:48:06 +02:00
$feed = Feed::factory()->create();
/** @var Route $route */
$route = Route::factory()->active()->create(['feed_id' => $feed->id]);
Keyword::factory()->active()->create([
2025-08-02 03:48:06 +02:00
'feed_id' => $feed->id,
'platform_channel_id' => $route->platform_channel_id,
'keyword' => 'Belgium',
2025-08-02 03:48:06 +02:00
]);
$article = Article::factory()->create(['feed_id' => $feed->id]);
$this->mockFetchReturning($article, 'Article about Belgium');
$this->validationService->validate($article);
2025-08-11 18:26:00 +02:00
$this->assertNotNull($article->fresh()->validated_at);
2025-08-02 03:48:06 +02:00
}
public function test_validate_creates_route_articles_for_active_routes(): void
2025-08-02 03:48:06 +02:00
{
$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());
}
2025-08-10 15:20:28 +02:00
public function test_validate_skips_inactive_routes(): void
{
2025-08-02 03:48:06 +02:00
$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([
2025-08-02 03:48:06 +02:00
'feed_id' => $feed->id,
'platform_channel_id' => $route->platform_channel_id,
'keyword' => 'Belgium',
2025-08-02 03:48:06 +02:00
]);
$article = Article::factory()->create(['feed_id' => $feed->id]);
$this->mockFetchReturning($article, 'Article about Belgium politics');
$this->validationService->validate($article);
2025-08-11 18:26:00 +02:00
$routeArticle = RouteArticle::where('article_id', $article->id)->first();
$this->assertEquals(ApprovalStatusEnum::PENDING, $routeArticle->approval_status);
2025-08-02 03:48:06 +02:00
}
public function test_validate_sets_rejected_when_no_keywords_match(): void
2025-08-02 03:48:06 +02:00
{
$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',
2025-08-10 15:20:28 +02:00
]);
$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
{
2025-08-02 03:48:06 +02:00
$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([
2025-08-02 03:48:06 +02:00
'feed_id' => $feed->id,
'platform_channel_id' => $channel1->id,
]);
Route::factory()->active()->create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel2->id,
2025-08-02 03:48:06 +02:00
]);
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');
2025-08-11 18:26:00 +02:00
$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);
2025-08-02 03:48:06 +02:00
}
public function test_validate_auto_approves_when_global_setting_off_and_keywords_match(): void
2025-08-02 03:48:06 +02:00
{
Setting::setBool('enable_publishing_approvals', false);
2025-08-10 15:20:28 +02:00
2025-08-02 03:48:06 +02:00
$feed = Feed::factory()->create();
/** @var Route $route */
$route = Route::factory()->active()->create(['feed_id' => $feed->id]);
Keyword::factory()->active()->create([
2025-08-02 03:48:06 +02:00
'feed_id' => $feed->id,
'platform_channel_id' => $route->platform_channel_id,
'keyword' => 'Belgium',
2025-08-02 03:48:06 +02:00
]);
$article = Article::factory()->create(['feed_id' => $feed->id]);
$this->mockFetchReturning($article, 'Article about Belgium');
2025-08-11 18:26:00 +02:00
2025-08-15 02:50:42 +02:00
$this->validationService->validate($article);
2025-08-11 18:26:00 +02:00
$routeArticle = RouteArticle::where('article_id', $article->id)->first();
$this->assertEquals(ApprovalStatusEnum::APPROVED, $routeArticle->approval_status);
2025-08-02 03:48:06 +02:00
}
public function test_validate_route_auto_approve_overrides_global_setting(): void
2025-08-02 03:48:06 +02:00
{
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',
2025-08-10 15:20:28 +02:00
]);
$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);
2025-08-02 03:48:06 +02:00
$feed = Feed::factory()->create();
/** @var Route $route */
$route = Route::factory()->active()->create([
2025-08-02 03:48:06 +02:00
'feed_id' => $feed->id,
'auto_approve' => false,
]);
Keyword::factory()->active()->create([
'feed_id' => $feed->id,
'platform_channel_id' => $route->platform_channel_id,
'keyword' => 'Belgium',
2025-08-02 03:48:06 +02:00
]);
$article = Article::factory()->create(['feed_id' => $feed->id]);
$this->mockFetchReturning($article, 'Article about Belgium');
2025-08-11 18:26:00 +02:00
$this->validationService->validate($article);
2025-08-11 18:26:00 +02:00
$routeArticle = RouteArticle::where('article_id', $article->id)->first();
$this->assertEquals(ApprovalStatusEnum::PENDING, $routeArticle->approval_status);
2025-08-02 03:48:06 +02:00
}
public function test_validate_does_not_auto_approve_rejected_articles(): void
2025-08-02 03:48:06 +02:00
{
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',
2025-08-10 15:20:28 +02:00
]);
$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
{
2025-08-02 03:48:06 +02:00
$feed = Feed::factory()->create();
Route::factory()->active()->create(['feed_id' => $feed->id]);
2025-08-11 18:26:00 +02:00
2025-08-02 03:48:06 +02:00
$article = Article::factory()->create([
'feed_id' => $feed->id,
'title' => 'Old Title',
2025-08-02 03:48:06 +02:00
]);
$this->mockFetchReturning($article, 'Content about Belgium', 'New Title', 'New description');
2025-08-02 03:48:06 +02:00
2025-08-15 02:50:42 +02:00
$result = $this->validationService->validate($article);
2025-08-11 18:26:00 +02:00
$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);
2025-08-02 03:48:06 +02:00
}
2025-08-11 18:26:00 +02:00
}