2026-08-13 00:40:21 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Tests\Feature;
|
|
|
|
|
|
2026-08-13 23:16:43 +02:00
|
|
|
use App\Actions\CreateRouteArticlesAction;
|
2026-08-13 00:40:21 +02:00
|
|
|
use App\Enums\ApprovalStatusEnum;
|
|
|
|
|
use App\Livewire\Articles;
|
|
|
|
|
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\User;
|
|
|
|
|
use App\Services\Article\ArticleFetcher;
|
|
|
|
|
use App\Services\Article\ValidationService;
|
|
|
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
|
use Livewire\Livewire;
|
|
|
|
|
use Mockery;
|
|
|
|
|
use Tests\TestCase;
|
|
|
|
|
|
|
|
|
|
class RouteArticleDecisionStampingTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
use RefreshDatabase;
|
|
|
|
|
|
|
|
|
|
public function test_clearing_pending_articles_stamps_every_rejected_row(): void
|
|
|
|
|
{
|
|
|
|
|
$pending = RouteArticle::factory()->count(3)->create([
|
|
|
|
|
'approval_status' => ApprovalStatusEnum::PENDING,
|
|
|
|
|
'decided_at' => null,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
Livewire::test(Articles::class)->call('clear');
|
|
|
|
|
|
|
|
|
|
foreach ($pending as $routeArticle) {
|
|
|
|
|
/** @var RouteArticle $fresh */
|
|
|
|
|
$fresh = $routeArticle->fresh();
|
|
|
|
|
|
|
|
|
|
$this->assertNotNull($fresh->decided_at);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_the_api_clear_endpoint_stamps_every_rejected_row(): void
|
|
|
|
|
{
|
|
|
|
|
$pending = RouteArticle::factory()->count(3)->create([
|
|
|
|
|
'approval_status' => ApprovalStatusEnum::PENDING,
|
|
|
|
|
'decided_at' => null,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->actingAs(User::factory()->create())
|
|
|
|
|
->postJson('/api/v1/route-articles/clear')
|
|
|
|
|
->assertOk();
|
|
|
|
|
|
|
|
|
|
foreach ($pending as $routeArticle) {
|
|
|
|
|
/** @var RouteArticle $fresh */
|
|
|
|
|
$fresh = $routeArticle->fresh();
|
|
|
|
|
|
|
|
|
|
$this->assertNotNull($fresh->decided_at);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_restoring_to_pending_clears_the_decision_time(): void
|
|
|
|
|
{
|
|
|
|
|
/** @var RouteArticle $routeArticle */
|
|
|
|
|
$routeArticle = RouteArticle::factory()->create([
|
|
|
|
|
'approval_status' => ApprovalStatusEnum::REJECTED,
|
|
|
|
|
'decided_at' => now(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
Livewire::test(Articles::class)->call('restore', $routeArticle->id);
|
|
|
|
|
|
|
|
|
|
$this->assertNull($routeArticle->fresh()->decided_at);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_the_api_restore_endpoint_clears_the_decision_time(): void
|
|
|
|
|
{
|
|
|
|
|
/** @var RouteArticle $routeArticle */
|
|
|
|
|
$routeArticle = RouteArticle::factory()->create([
|
|
|
|
|
'approval_status' => ApprovalStatusEnum::REJECTED,
|
|
|
|
|
'decided_at' => now(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->actingAs(User::factory()->create())
|
|
|
|
|
->postJson("/api/v1/route-articles/{$routeArticle->id}/restore")
|
|
|
|
|
->assertOk();
|
|
|
|
|
|
|
|
|
|
$this->assertNull($routeArticle->fresh()->decided_at);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_auto_approved_articles_are_stamped_on_creation(): void
|
|
|
|
|
{
|
|
|
|
|
$article = $this->articleOnRouteWithoutKeywords(autoApprove: true);
|
|
|
|
|
|
|
|
|
|
$this->validate($article);
|
|
|
|
|
|
|
|
|
|
$routeArticle = RouteArticle::where('article_id', $article->id)->firstOrFail();
|
|
|
|
|
|
|
|
|
|
$this->assertSame(ApprovalStatusEnum::APPROVED, $routeArticle->approval_status);
|
|
|
|
|
$this->assertNotNull($routeArticle->decided_at);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_keyword_rejected_articles_are_stamped_on_creation(): void
|
|
|
|
|
{
|
|
|
|
|
$article = $this->articleOnRouteWithKeyword('nothing-that-matches');
|
|
|
|
|
|
|
|
|
|
$this->validate($article);
|
|
|
|
|
|
|
|
|
|
$routeArticle = RouteArticle::where('article_id', $article->id)->firstOrFail();
|
|
|
|
|
|
|
|
|
|
$this->assertSame(ApprovalStatusEnum::REJECTED, $routeArticle->approval_status);
|
|
|
|
|
$this->assertNotNull($routeArticle->decided_at);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_pending_articles_are_not_stamped_on_creation(): void
|
|
|
|
|
{
|
|
|
|
|
$article = $this->articleOnRouteWithoutKeywords(autoApprove: false);
|
|
|
|
|
|
|
|
|
|
$this->validate($article);
|
|
|
|
|
|
|
|
|
|
$routeArticle = RouteArticle::where('article_id', $article->id)->firstOrFail();
|
|
|
|
|
|
|
|
|
|
$this->assertSame(ApprovalStatusEnum::PENDING, $routeArticle->approval_status);
|
|
|
|
|
$this->assertNull($routeArticle->decided_at);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function validate(Article $article): void
|
|
|
|
|
{
|
|
|
|
|
$fetcher = Mockery::mock(ArticleFetcher::class);
|
|
|
|
|
$fetcher->shouldReceive('fetchArticleData')
|
|
|
|
|
->with($article)
|
|
|
|
|
->once()
|
|
|
|
|
->andReturn([
|
|
|
|
|
'title' => 'Test Title',
|
|
|
|
|
'description' => 'Test description',
|
|
|
|
|
'full_article' => 'Body text',
|
|
|
|
|
]);
|
|
|
|
|
|
2026-08-13 23:16:43 +02:00
|
|
|
(new ValidationService($fetcher, new CreateRouteArticlesAction))->validate($article);
|
2026-08-13 00:40:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function articleOnRouteWithoutKeywords(bool $autoApprove): Article
|
|
|
|
|
{
|
|
|
|
|
$feed = Feed::factory()->create();
|
|
|
|
|
$channel = PlatformChannel::factory()->create();
|
|
|
|
|
|
|
|
|
|
Route::factory()->create([
|
|
|
|
|
'feed_id' => $feed->id,
|
|
|
|
|
'platform_channel_id' => $channel->id,
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
'auto_approve' => $autoApprove,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return Article::factory()->create(['feed_id' => $feed->id, 'validated_at' => null]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function articleOnRouteWithKeyword(string $keyword): Article
|
|
|
|
|
{
|
|
|
|
|
$feed = Feed::factory()->create();
|
|
|
|
|
$channel = PlatformChannel::factory()->create();
|
|
|
|
|
|
|
|
|
|
Route::factory()->create([
|
|
|
|
|
'feed_id' => $feed->id,
|
|
|
|
|
'platform_channel_id' => $channel->id,
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
Keyword::factory()->active()->create([
|
|
|
|
|
'feed_id' => $feed->id,
|
|
|
|
|
'platform_channel_id' => $channel->id,
|
|
|
|
|
'keyword' => $keyword,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return Article::factory()->create(['feed_id' => $feed->id, 'validated_at' => null]);
|
|
|
|
|
}
|
|
|
|
|
}
|