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', ]); (new ValidationService($fetcher))->validate($article); } 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]); } }