144 - Replace ArticleFetcher with focused fetch actions
This commit is contained in:
parent
eefadff873
commit
dc64dd83b8
21 changed files with 260 additions and 254 deletions
36
app/Actions/FetchArticleDataAction.php
Normal file
36
app/Actions/FetchArticleDataAction.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Services\Factories\ArticleParserFactory;
|
||||
use App\Services\Http\HttpFetcher;
|
||||
use App\Services\Log\LogSaver;
|
||||
use Exception;
|
||||
|
||||
class FetchArticleDataAction
|
||||
{
|
||||
public function __construct(
|
||||
private LogSaver $logSaver
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function execute(Article $article): array
|
||||
{
|
||||
try {
|
||||
$html = HttpFetcher::fetchHtml($article->url);
|
||||
$parser = ArticleParserFactory::getParser($article->url);
|
||||
|
||||
return $parser->extractData($html);
|
||||
} catch (Exception $e) {
|
||||
$this->logSaver->error('Exception while fetching article data', null, [
|
||||
'url' => $article->url,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
36
app/Actions/FetchFeedArticlesAction.php
Normal file
36
app/Actions/FetchFeedArticlesAction.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Models\Feed;
|
||||
use App\Services\Log\LogSaver;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class FetchFeedArticlesAction
|
||||
{
|
||||
public function __construct(
|
||||
private LogSaver $logSaver,
|
||||
private FetchRssArticlesAction $fetchRssArticles,
|
||||
private FetchWebsiteArticlesAction $fetchWebsiteArticles,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Article>
|
||||
*/
|
||||
public function execute(Feed $feed): Collection
|
||||
{
|
||||
if ($feed->type === 'rss') {
|
||||
return $this->fetchRssArticles->execute($feed);
|
||||
} elseif ($feed->type === 'website') {
|
||||
return $this->fetchWebsiteArticles->execute($feed);
|
||||
}
|
||||
|
||||
$this->logSaver->warning('Unsupported feed type', null, [
|
||||
'feed_id' => $feed->id,
|
||||
'feed_type' => $feed->type,
|
||||
]);
|
||||
|
||||
return collect();
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@
|
|||
use App\Exceptions\PublishException;
|
||||
use App\Models\Article;
|
||||
use App\Models\RouteArticle;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use App\Services\Notification\NotificationService;
|
||||
use App\Services\Publishing\ArticlePublishingService;
|
||||
use App\Services\Publishing\PublishOutcome;
|
||||
|
|
@ -21,7 +20,7 @@
|
|||
class PublishRouteArticleAction
|
||||
{
|
||||
public function __construct(
|
||||
private ArticleFetcher $articleFetcher,
|
||||
private FetchArticleDataAction $fetchArticleData,
|
||||
private ArticlePublishingService $publishingService,
|
||||
private NotificationService $notificationService,
|
||||
) {}
|
||||
|
|
@ -90,7 +89,7 @@ private function hasPublishableContent(array $extractedData): bool
|
|||
private function resolvePublishData(Article $article): array
|
||||
{
|
||||
if (empty($article->description) && empty($article->image_url)) {
|
||||
return $this->articleFetcher->fetchArticleData($article);
|
||||
return $this->fetchArticleData->execute($article);
|
||||
}
|
||||
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@
|
|||
namespace App\Actions;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
|
||||
class ValidateArticleAction
|
||||
{
|
||||
public function __construct(
|
||||
private ArticleFetcher $articleFetcher,
|
||||
private FetchArticleDataAction $fetchArticleData,
|
||||
private CreateRouteArticlesAction $createRouteArticles,
|
||||
) {}
|
||||
|
||||
|
|
@ -16,7 +15,7 @@ public function execute(Article $article): Article
|
|||
{
|
||||
logger('Validating article for routes: '.$article->id);
|
||||
|
||||
$articleData = $this->articleFetcher->fetchArticleData($article);
|
||||
$articleData = $this->fetchArticleData->execute($article);
|
||||
|
||||
$updateData = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\FetchFeedArticlesAction;
|
||||
use App\Enums\ActivityTypeEnum;
|
||||
use App\Enums\NotificationSeverityEnum;
|
||||
use App\Enums\NotificationTypeEnum;
|
||||
use App\Events\ActivityLogged;
|
||||
use App\Models\Feed;
|
||||
use App\Models\Notification;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use App\Services\Log\LogSaver;
|
||||
use App\Services\Notification\NotificationService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
|
@ -26,7 +26,7 @@ public function __construct(
|
|||
$this->onQueue('feed-discovery');
|
||||
}
|
||||
|
||||
public function handle(LogSaver $logSaver, ArticleFetcher $articleFetcher, NotificationService $notificationService): void
|
||||
public function handle(LogSaver $logSaver, FetchFeedArticlesAction $fetchFeedArticles, NotificationService $notificationService): void
|
||||
{
|
||||
$logSaver->info('Starting feed article fetch', null, [
|
||||
'feed_id' => $this->feed->id,
|
||||
|
|
@ -34,7 +34,7 @@ public function handle(LogSaver $logSaver, ArticleFetcher $articleFetcher, Notif
|
|||
'feed_url' => $this->feed->url,
|
||||
]);
|
||||
|
||||
$articles = $articleFetcher->getArticlesFromFeed($this->feed);
|
||||
$articles = $fetchFeedArticles->execute($this->feed);
|
||||
|
||||
$logSaver->info('Feed article fetch completed', null, [
|
||||
'feed_id' => $this->feed->id,
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Article;
|
||||
|
||||
use App\Actions\FetchRssArticlesAction;
|
||||
use App\Actions\FetchWebsiteArticlesAction;
|
||||
use App\Models\Article;
|
||||
use App\Models\Feed;
|
||||
use App\Services\Factories\ArticleParserFactory;
|
||||
use App\Services\Http\HttpFetcher;
|
||||
use App\Services\Log\LogSaver;
|
||||
use Exception;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ArticleFetcher
|
||||
{
|
||||
public function __construct(
|
||||
private LogSaver $logSaver,
|
||||
private FetchRssArticlesAction $fetchRssArticles,
|
||||
private FetchWebsiteArticlesAction $fetchWebsiteArticles,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Article>
|
||||
*/
|
||||
public function getArticlesFromFeed(Feed $feed): Collection
|
||||
{
|
||||
if ($feed->type === 'rss') {
|
||||
return $this->fetchRssArticles->execute($feed);
|
||||
} elseif ($feed->type === 'website') {
|
||||
return $this->fetchWebsiteArticles->execute($feed);
|
||||
}
|
||||
|
||||
$this->logSaver->warning('Unsupported feed type', null, [
|
||||
'feed_id' => $feed->id,
|
||||
'feed_type' => $feed->type,
|
||||
]);
|
||||
|
||||
return collect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function fetchArticleData(Article $article): array
|
||||
{
|
||||
try {
|
||||
$html = HttpFetcher::fetchHtml($article->url);
|
||||
$parser = ArticleParserFactory::getParser($article->url);
|
||||
|
||||
return $parser->extractData($html);
|
||||
} catch (Exception $e) {
|
||||
$this->logSaver->error('Exception while fetching article data', null, [
|
||||
'url' => $article->url,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Actions\FetchArticleDataAction;
|
||||
use App\Actions\PublishRouteArticleAction;
|
||||
use App\Enums\PublishStatusEnum;
|
||||
use App\Events\RouteArticleApproved;
|
||||
|
|
@ -15,7 +16,6 @@
|
|||
use App\Models\Route;
|
||||
use App\Models\RouteArticle;
|
||||
use App\Modules\Lemmy\Services\LemmyPublisher;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use App\Services\Log\LogSaver;
|
||||
use App\Services\Notification\NotificationService;
|
||||
use App\Services\Publishing\ArticlePublishingService;
|
||||
|
|
@ -93,8 +93,8 @@ private function makeListener(): PublishApprovedArticleListener
|
|||
$service->shouldAllowMockingProtectedMethods();
|
||||
$service->shouldReceive('makePublisher')->andReturn($publisher);
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldNotReceive('fetchArticleData');
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldNotReceive('execute');
|
||||
|
||||
return new PublishApprovedArticleListener(new PublishRouteArticleAction($fetcher, $service, new NotificationService));
|
||||
}
|
||||
|
|
@ -159,8 +159,8 @@ public function test_two_queued_listeners_create_only_one_remote_post(): void
|
|||
$service->shouldAllowMockingProtectedMethods();
|
||||
$service->shouldReceive('makePublisher')->andReturn($publisher);
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldNotReceive('fetchArticleData');
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldNotReceive('execute');
|
||||
|
||||
$listener = new PublishApprovedArticleListener(new PublishRouteArticleAction($fetcher, $service, new NotificationService));
|
||||
$listener->handle(new RouteArticleApproved($routeArticle));
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature\Jobs;
|
||||
|
||||
use App\Actions\FetchFeedArticlesAction;
|
||||
use App\Enums\ActivityTypeEnum;
|
||||
use App\Enums\NotificationSeverityEnum;
|
||||
use App\Enums\NotificationTypeEnum;
|
||||
|
|
@ -10,7 +11,6 @@
|
|||
use App\Models\Article;
|
||||
use App\Models\Feed;
|
||||
use App\Models\Notification;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use App\Services\Log\LogSaver;
|
||||
use App\Services\Notification\NotificationService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
|
@ -27,8 +27,8 @@ class ArticleDiscoveryForFeedJobEmptyFetchTest extends TestCase
|
|||
*/
|
||||
private function runJobForFeed(Feed $feed, ?Collection $articles = null): void
|
||||
{
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldReceive('getArticlesFromFeed')
|
||||
$fetcher = Mockery::mock(FetchFeedArticlesAction::class);
|
||||
$fetcher->shouldReceive('execute')
|
||||
->andReturn($articles ?? collect());
|
||||
|
||||
(new ArticleDiscoveryForFeedJob($feed))->handle(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Actions\FetchArticleDataAction;
|
||||
use App\Actions\FetchFeedArticlesAction;
|
||||
use App\Enums\ApprovalStatusEnum;
|
||||
use App\Enums\LogLevelEnum;
|
||||
use App\Events\ActionPerformed;
|
||||
|
|
@ -23,7 +25,6 @@
|
|||
use App\Models\Route;
|
||||
use App\Models\RouteArticle;
|
||||
use App\Models\Setting;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use App\Services\Log\LogSaver;
|
||||
use App\Services\Notification\NotificationService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
|
@ -63,18 +64,18 @@ public function test_article_discovery_for_feed_job_processes_feed(): void
|
|||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// Mock the ArticleFetcher service in the container
|
||||
$mockFetcher = \Mockery::mock(ArticleFetcher::class);
|
||||
// Mock the feed fetch in the container
|
||||
$mockFetcher = \Mockery::mock(FetchFeedArticlesAction::class);
|
||||
$article1 = Article::factory()->create(['url' => 'https://example.com/article1', 'feed_id' => $feed->id]);
|
||||
$article2 = Article::factory()->create(['url' => 'https://example.com/article2', 'feed_id' => $feed->id]);
|
||||
$mockFetcher->shouldReceive('getArticlesFromFeed')
|
||||
$mockFetcher->shouldReceive('execute')
|
||||
->with($feed)
|
||||
->andReturn(collect([$article1, $article2]));
|
||||
|
||||
$this->app->instance(ArticleFetcher::class, $mockFetcher);
|
||||
$this->app->instance(FetchFeedArticlesAction::class, $mockFetcher);
|
||||
|
||||
$logSaver = app(LogSaver::class);
|
||||
$articleFetcher = app(ArticleFetcher::class);
|
||||
$articleFetcher = app(FetchFeedArticlesAction::class);
|
||||
$job = new ArticleDiscoveryForFeedJob($feed);
|
||||
$job->handle($logSaver, $articleFetcher, app(NotificationService::class));
|
||||
|
||||
|
|
@ -167,10 +168,10 @@ public function test_validate_article_listener_processes_new_article(): void
|
|||
'feed_id' => $feed->id,
|
||||
]);
|
||||
|
||||
// Mock ArticleFetcher to return valid article data
|
||||
$mockFetcher = \Mockery::mock(ArticleFetcher::class);
|
||||
$this->app->instance(ArticleFetcher::class, $mockFetcher);
|
||||
$mockFetcher->shouldReceive('fetchArticleData')
|
||||
// Mock the article-data fetch to return valid data
|
||||
$mockFetcher = \Mockery::mock(FetchArticleDataAction::class);
|
||||
$this->app->instance(FetchArticleDataAction::class, $mockFetcher);
|
||||
$mockFetcher->shouldReceive('execute')
|
||||
->with($article)
|
||||
->andReturn([
|
||||
'title' => 'Belgian News',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature\Listeners;
|
||||
|
||||
use App\Actions\FetchArticleDataAction;
|
||||
use App\Actions\PublishRouteArticleAction;
|
||||
use App\Enums\NotificationSeverityEnum;
|
||||
use App\Enums\NotificationTypeEnum;
|
||||
|
|
@ -13,7 +14,6 @@
|
|||
use App\Models\Notification;
|
||||
use App\Models\Route;
|
||||
use App\Models\RouteArticle;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use App\Services\Notification\NotificationService;
|
||||
use App\Services\Publishing\ArticlePublishingService;
|
||||
use App\Services\Publishing\PublishOutcome;
|
||||
|
|
@ -48,8 +48,8 @@ public function test_exception_during_publishing_creates_error_notification(): v
|
|||
{
|
||||
$routeArticle = $this->createApprovedRouteArticle();
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andThrow(new Exception('Connection refused'));
|
||||
|
||||
|
|
@ -76,8 +76,8 @@ public function test_no_publication_created_creates_warning_notification(): void
|
|||
|
||||
$extractedData = ['title' => 'Test Article', 'description' => 'Test description'];
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn($extractedData);
|
||||
|
||||
|
|
@ -106,8 +106,8 @@ public function test_successful_publish_does_not_create_notification(): void
|
|||
|
||||
$extractedData = ['title' => 'Test Article', 'description' => 'Test description'];
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn($extractedData);
|
||||
|
||||
|
|
@ -131,8 +131,8 @@ public function test_skips_already_published_to_channel(): void
|
|||
'platform_channel_id' => $routeArticle->platform_channel_id,
|
||||
]);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldNotReceive('fetchArticleData');
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldNotReceive('execute');
|
||||
|
||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||
$publishingServiceMock->shouldNotReceive('publishRouteArticle');
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Actions\FetchArticleDataAction;
|
||||
use App\Actions\PublishRouteArticleAction;
|
||||
use App\Enums\NotificationTypeEnum;
|
||||
use App\Enums\PublishStatusEnum;
|
||||
|
|
@ -17,7 +18,6 @@
|
|||
use App\Models\RouteArticle;
|
||||
use App\Modules\Lemmy\Services\LemmyApiService;
|
||||
use App\Modules\Lemmy\Services\LemmyPublisher;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use App\Services\Log\LogSaver;
|
||||
use App\Services\Notification\NotificationService;
|
||||
use App\Services\Publishing\ArticlePublishingService;
|
||||
|
|
@ -124,8 +124,8 @@ public function test_a_skipped_duplicate_is_not_reported_as_a_publish_failure():
|
|||
|
||||
PlatformChannelPost::storePost($channel, '555', $article->url, 'Already Posted');
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldNotReceive('fetchArticleData');
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldNotReceive('execute');
|
||||
|
||||
$publisher = Mockery::mock(LemmyPublisher::class);
|
||||
$publisher->shouldNotReceive('publishToChannel');
|
||||
|
|
@ -141,8 +141,8 @@ public function test_a_genuine_failure_is_still_reported(): void
|
|||
{
|
||||
[$routeArticle] = $this->fixture;
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldNotReceive('fetchArticleData');
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldNotReceive('execute');
|
||||
|
||||
$publisher = Mockery::mock(LemmyPublisher::class);
|
||||
$publisher->shouldReceive('publishToChannel')->andThrow(new \RuntimeException('Lemmy rejected the post'));
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace Tests\Feature;
|
||||
|
||||
use App\Actions\CreateRouteArticlesAction;
|
||||
use App\Actions\FetchArticleDataAction;
|
||||
use App\Actions\ValidateArticleAction;
|
||||
use App\Enums\ApprovalStatusEnum;
|
||||
use App\Livewire\Articles;
|
||||
|
|
@ -13,7 +14,6 @@
|
|||
use App\Models\Route;
|
||||
use App\Models\RouteArticle;
|
||||
use App\Models\User;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Mockery;
|
||||
|
|
@ -125,8 +125,8 @@ public function test_pending_articles_are_not_stamped_on_creation(): void
|
|||
|
||||
private function validate(Article $article): void
|
||||
{
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldReceive('fetchArticleData')
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldReceive('execute')
|
||||
->with($article)
|
||||
->once()
|
||||
->andReturn([
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace Tests\Feature;
|
||||
|
||||
use App\Actions\CreateRouteArticlesAction;
|
||||
use App\Actions\FetchArticleDataAction;
|
||||
use App\Actions\ValidateArticleAction;
|
||||
use App\Enums\ApprovalStatusEnum;
|
||||
use App\Events\NewArticleFetched;
|
||||
|
|
@ -13,7 +14,6 @@
|
|||
use App\Models\Keyword;
|
||||
use App\Models\Route;
|
||||
use App\Models\RouteArticle;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
|
@ -24,8 +24,8 @@ class ValidateArticleListenerTest extends TestCase
|
|||
|
||||
private function createListenerWithMockedFetcher(?string $content = 'Some article content'): ValidateArticleListener
|
||||
{
|
||||
$articleFetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcher->shouldReceive('fetchArticleData')->andReturn(
|
||||
$articleFetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcher->shouldReceive('execute')->andReturn(
|
||||
$content ? [
|
||||
'title' => 'Test Title',
|
||||
'description' => 'Test description',
|
||||
|
|
@ -111,8 +111,8 @@ public function test_listener_skips_articles_with_existing_publication(): void
|
|||
|
||||
public function test_listener_handles_validation_errors_gracefully(): void
|
||||
{
|
||||
$articleFetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcher->shouldReceive('fetchArticleData')->andThrow(new \Exception('Fetch failed'));
|
||||
$articleFetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcher->shouldReceive('execute')->andThrow(new \Exception('Fetch failed'));
|
||||
|
||||
$listener = new ValidateArticleListener(
|
||||
new ValidateArticleAction($articleFetcher, new CreateRouteArticlesAction)
|
||||
|
|
|
|||
|
|
@ -2,44 +2,42 @@
|
|||
|
||||
namespace Tests\Traits;
|
||||
|
||||
use App\Actions\FetchArticleDataAction;
|
||||
use App\Actions\FetchFeedArticlesAction;
|
||||
use App\Actions\FetchRssArticlesAction;
|
||||
use App\Actions\FetchWebsiteArticlesAction;
|
||||
use App\Actions\SaveArticleAction;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use App\Services\Log\LogSaver;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
|
||||
trait CreatesArticleFetcher
|
||||
trait CreatesFetchActions
|
||||
{
|
||||
protected function createArticleFetcher(?LogSaver $logSaver = null): ArticleFetcher
|
||||
{
|
||||
if (! $logSaver) {
|
||||
$logSaver = $this->mockLogSaver();
|
||||
}
|
||||
|
||||
return $this->articleFetcherWith($logSaver);
|
||||
}
|
||||
|
||||
/** @return array{ArticleFetcher, MockInterface} */
|
||||
protected function createArticleFetcherWithMockedLogSaver(): array
|
||||
{
|
||||
$logSaver = $this->mockLogSaver();
|
||||
|
||||
return [$this->articleFetcherWith($logSaver), $logSaver];
|
||||
}
|
||||
|
||||
private function articleFetcherWith(LogSaver $logSaver): ArticleFetcher
|
||||
protected function createFeedFetcher(?LogSaver $logSaver = null): FetchFeedArticlesAction
|
||||
{
|
||||
$logSaver ??= $this->mockLogSaver();
|
||||
$saveArticle = new SaveArticleAction($logSaver);
|
||||
|
||||
return new ArticleFetcher(
|
||||
return new FetchFeedArticlesAction(
|
||||
$logSaver,
|
||||
new FetchRssArticlesAction($logSaver, $saveArticle),
|
||||
new FetchWebsiteArticlesAction($logSaver, $saveArticle),
|
||||
);
|
||||
}
|
||||
|
||||
protected function createArticleDataFetcher(?LogSaver $logSaver = null): FetchArticleDataAction
|
||||
{
|
||||
return new FetchArticleDataAction($logSaver ?? $this->mockLogSaver());
|
||||
}
|
||||
|
||||
/** @return array{FetchFeedArticlesAction, MockInterface} */
|
||||
protected function createFeedFetcherWithMockedLogSaver(): array
|
||||
{
|
||||
$logSaver = $this->mockLogSaver();
|
||||
|
||||
return [$this->createFeedFetcher($logSaver), $logSaver];
|
||||
}
|
||||
|
||||
/** @return LogSaver&MockInterface */
|
||||
private function mockLogSaver(): MockInterface
|
||||
{
|
||||
|
|
@ -1,20 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Services;
|
||||
namespace Tests\Unit\Actions;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Models\Feed;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
use Tests\Traits\CreatesArticleFetcher;
|
||||
use Tests\Traits\CreatesFetchActions;
|
||||
|
||||
class ArticleFetcherTest extends TestCase
|
||||
class FetchFeedArticlesActionTest extends TestCase
|
||||
{
|
||||
use CreatesArticleFetcher, RefreshDatabase;
|
||||
use CreatesFetchActions, RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
|
|
@ -25,19 +24,18 @@ protected function setUp(): void
|
|||
'*' => Http::response('<html><body>Mock HTML content</body></html>', 200),
|
||||
]);
|
||||
|
||||
// Create ArticleFetcher only when needed - tests will create their own
|
||||
}
|
||||
|
||||
public function test_get_articles_from_feed_returns_collection(): void
|
||||
{
|
||||
$articleFetcher = $this->createArticleFetcher();
|
||||
$articleFetcher = $this->createFeedFetcher();
|
||||
|
||||
$feed = Feed::factory()->create([
|
||||
'type' => 'rss',
|
||||
'url' => 'https://example.com/feed.rss',
|
||||
]);
|
||||
|
||||
$result = $articleFetcher->getArticlesFromFeed($feed);
|
||||
$result = $articleFetcher->execute($feed);
|
||||
|
||||
$this->assertInstanceOf(Collection::class, $result);
|
||||
}
|
||||
|
|
@ -49,8 +47,8 @@ public function test_get_articles_from_rss_feed_returns_empty_collection(): void
|
|||
'url' => 'https://example.com/feed.rss',
|
||||
]);
|
||||
|
||||
$articleFetcher = $this->createArticleFetcher();
|
||||
$result = $articleFetcher->getArticlesFromFeed($feed);
|
||||
$articleFetcher = $this->createFeedFetcher();
|
||||
$result = $articleFetcher->execute($feed);
|
||||
|
||||
// RSS parsing is not implemented yet, should return empty collection
|
||||
$this->assertEmpty($result);
|
||||
|
|
@ -63,8 +61,8 @@ public function test_get_articles_from_website_feed_handles_no_parser(): void
|
|||
'url' => 'https://unsupported-site.com/',
|
||||
]);
|
||||
|
||||
$articleFetcher = $this->createArticleFetcher();
|
||||
$result = $articleFetcher->getArticlesFromFeed($feed);
|
||||
$articleFetcher = $this->createFeedFetcher();
|
||||
$result = $articleFetcher->execute($feed);
|
||||
|
||||
// Should return empty collection when no parser is available
|
||||
$this->assertInstanceOf(Collection::class, $result);
|
||||
|
|
@ -78,8 +76,8 @@ public function test_get_articles_from_unsupported_feed_type(): void
|
|||
'url' => 'https://unsupported-feed-type.com/feed',
|
||||
]);
|
||||
|
||||
$articleFetcher = $this->createArticleFetcher();
|
||||
$result = $articleFetcher->getArticlesFromFeed($feed);
|
||||
$articleFetcher = $this->createFeedFetcher();
|
||||
$result = $articleFetcher->execute($feed);
|
||||
|
||||
$this->assertInstanceOf(Collection::class, $result);
|
||||
$this->assertEmpty($result);
|
||||
|
|
@ -91,8 +89,8 @@ public function test_fetch_article_data_returns_array(): void
|
|||
'url' => 'https://example.com/article',
|
||||
]);
|
||||
|
||||
$articleFetcher = $this->createArticleFetcher();
|
||||
$result = $articleFetcher->fetchArticleData($article);
|
||||
$articleFetcher = $this->createArticleDataFetcher();
|
||||
$result = $articleFetcher->execute($article);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
// Will be empty array due to unsupported URL in test
|
||||
|
|
@ -105,8 +103,8 @@ public function test_fetch_article_data_handles_invalid_url(): void
|
|||
'url' => 'invalid-url',
|
||||
]);
|
||||
|
||||
$articleFetcher = $this->createArticleFetcher();
|
||||
$result = $articleFetcher->fetchArticleData($article);
|
||||
$articleFetcher = $this->createArticleDataFetcher();
|
||||
$result = $articleFetcher->execute($article);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertEmpty($result);
|
||||
|
|
@ -128,8 +126,8 @@ public function test_get_articles_from_feed_with_null_feed_type(): void
|
|||
$attributes['type'] = 'invalid_type';
|
||||
$property->setValue($feed, $attributes);
|
||||
|
||||
$articleFetcher = $this->createArticleFetcher();
|
||||
$result = $articleFetcher->getArticlesFromFeed($feed);
|
||||
$articleFetcher = $this->createFeedFetcher();
|
||||
$result = $articleFetcher->execute($feed);
|
||||
|
||||
$this->assertInstanceOf(Collection::class, $result);
|
||||
$this->assertEmpty($result);
|
||||
|
|
@ -148,8 +146,8 @@ public function test_get_articles_from_website_feed_with_supported_parser(): voi
|
|||
]);
|
||||
|
||||
// Test actual behavior - VRT parser should be available
|
||||
$articleFetcher = $this->createArticleFetcher();
|
||||
$result = $articleFetcher->getArticlesFromFeed($feed);
|
||||
$articleFetcher = $this->createFeedFetcher();
|
||||
$result = $articleFetcher->execute($feed);
|
||||
|
||||
$this->assertInstanceOf(Collection::class, $result);
|
||||
// VRT parser will process the mocked HTML response
|
||||
|
|
@ -164,8 +162,8 @@ public function test_get_articles_from_website_feed_handles_invalid_url(): void
|
|||
'url' => 'https://invalid-domain-that-does-not-exist-12345.com/',
|
||||
]);
|
||||
|
||||
$articleFetcher = $this->createArticleFetcher();
|
||||
$result = $articleFetcher->getArticlesFromFeed($feed);
|
||||
$articleFetcher = $this->createFeedFetcher();
|
||||
$result = $articleFetcher->execute($feed);
|
||||
|
||||
$this->assertInstanceOf(Collection::class, $result);
|
||||
$this->assertEmpty($result);
|
||||
|
|
@ -183,8 +181,8 @@ public function test_fetch_article_data_with_supported_parser(): void
|
|||
]);
|
||||
|
||||
// Test actual behavior - VRT parser should be available
|
||||
$articleFetcher = $this->createArticleFetcher();
|
||||
$result = $articleFetcher->fetchArticleData($article);
|
||||
$articleFetcher = $this->createArticleDataFetcher();
|
||||
$result = $articleFetcher->execute($article);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
// VRT parser will process the mocked HTML response
|
||||
|
|
@ -196,8 +194,8 @@ public function test_fetch_article_data_handles_unsupported_domain(): void
|
|||
'url' => 'https://unsupported-domain.com/article',
|
||||
]);
|
||||
|
||||
$articleFetcher = $this->createArticleFetcher();
|
||||
$result = $articleFetcher->fetchArticleData($article);
|
||||
$articleFetcher = $this->createArticleDataFetcher();
|
||||
$result = $articleFetcher->execute($article);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertEmpty($result);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Services;
|
||||
namespace Tests\Unit\Actions;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Models\Feed;
|
||||
|
|
@ -9,11 +9,11 @@
|
|||
use Illuminate\Support\Facades\Http;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
use Tests\Traits\CreatesArticleFetcher;
|
||||
use Tests\Traits\CreatesFetchActions;
|
||||
|
||||
class ArticleFetcherRssTest extends TestCase
|
||||
class FetchRssArticlesFeedTest extends TestCase
|
||||
{
|
||||
use CreatesArticleFetcher, RefreshDatabase;
|
||||
use CreatesFetchActions, RefreshDatabase;
|
||||
|
||||
private string $sampleRss;
|
||||
|
||||
|
|
@ -54,8 +54,8 @@ public function test_get_articles_from_rss_feed_returns_collection(): void
|
|||
'url' => 'https://www.theguardian.com/international/rss',
|
||||
]);
|
||||
|
||||
$fetcher = $this->createArticleFetcher();
|
||||
$result = $fetcher->getArticlesFromFeed($feed);
|
||||
$fetcher = $this->createFeedFetcher();
|
||||
$result = $fetcher->execute($feed);
|
||||
|
||||
$this->assertInstanceOf(Collection::class, $result);
|
||||
}
|
||||
|
|
@ -70,8 +70,8 @@ public function test_get_articles_from_rss_feed_creates_articles(): void
|
|||
'url' => 'https://www.theguardian.com/international/rss',
|
||||
]);
|
||||
|
||||
$fetcher = $this->createArticleFetcher();
|
||||
$result = $fetcher->getArticlesFromFeed($feed);
|
||||
$fetcher = $this->createFeedFetcher();
|
||||
$result = $fetcher->execute($feed);
|
||||
|
||||
$this->assertCount(2, $result);
|
||||
$this->assertDatabaseHas('articles', [
|
||||
|
|
@ -99,8 +99,8 @@ public function test_get_articles_from_rss_feed_does_not_duplicate_existing(): v
|
|||
'feed_id' => $feed->id,
|
||||
]);
|
||||
|
||||
$fetcher = $this->createArticleFetcher();
|
||||
$result = $fetcher->getArticlesFromFeed($feed);
|
||||
$fetcher = $this->createFeedFetcher();
|
||||
$result = $fetcher->execute($feed);
|
||||
|
||||
$this->assertCount(2, $result);
|
||||
$this->assertEquals(1, Article::where('url', 'https://www.theguardian.com/world/2026/mar/08/first-article')->count());
|
||||
|
|
@ -127,8 +127,8 @@ public function test_get_articles_from_rss_feed_returns_known_articles_when_none
|
|||
|
||||
$countBeforeFetch = Article::count();
|
||||
|
||||
$fetcher = $this->createArticleFetcher();
|
||||
$result = $fetcher->getArticlesFromFeed($feed);
|
||||
$fetcher = $this->createFeedFetcher();
|
||||
$result = $fetcher->execute($feed);
|
||||
|
||||
$this->assertCount(2, $result);
|
||||
$this->assertSame($countBeforeFetch, Article::count());
|
||||
|
|
@ -144,8 +144,8 @@ public function test_get_articles_from_rss_feed_handles_invalid_xml(): void
|
|||
'url' => 'https://www.theguardian.com/international/rss',
|
||||
]);
|
||||
|
||||
$fetcher = $this->createArticleFetcher();
|
||||
$result = $fetcher->getArticlesFromFeed($feed);
|
||||
$fetcher = $this->createFeedFetcher();
|
||||
$result = $fetcher->execute($feed);
|
||||
|
||||
$this->assertInstanceOf(Collection::class, $result);
|
||||
$this->assertEmpty($result);
|
||||
|
|
@ -163,8 +163,8 @@ public function test_get_articles_from_rss_feed_handles_empty_channel(): void
|
|||
'url' => 'https://www.theguardian.com/international/rss',
|
||||
]);
|
||||
|
||||
$fetcher = $this->createArticleFetcher();
|
||||
$result = $fetcher->getArticlesFromFeed($feed);
|
||||
$fetcher = $this->createFeedFetcher();
|
||||
$result = $fetcher->execute($feed);
|
||||
|
||||
$this->assertEmpty($result);
|
||||
}
|
||||
|
|
@ -179,8 +179,8 @@ public function test_get_articles_from_rss_feed_handles_http_failure(): void
|
|||
'url' => 'https://www.theguardian.com/international/rss',
|
||||
]);
|
||||
|
||||
$fetcher = $this->createArticleFetcher();
|
||||
$result = $fetcher->getArticlesFromFeed($feed);
|
||||
$fetcher = $this->createFeedFetcher();
|
||||
$result = $fetcher->execute($feed);
|
||||
|
||||
$this->assertEmpty($result);
|
||||
}
|
||||
|
|
@ -217,8 +217,8 @@ public function test_get_articles_from_belga_rss_feed_creates_articles(): void
|
|||
'url' => 'https://www.belganewsagency.eu/feed',
|
||||
]);
|
||||
|
||||
$fetcher = $this->createArticleFetcher();
|
||||
$result = $fetcher->getArticlesFromFeed($feed);
|
||||
$fetcher = $this->createFeedFetcher();
|
||||
$result = $fetcher->execute($feed);
|
||||
|
||||
$this->assertCount(2, $result);
|
||||
$this->assertDatabaseHas('articles', [
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Services;
|
||||
namespace Tests\Unit\Actions;
|
||||
|
||||
use App\Models\Feed;
|
||||
use App\Models\Language;
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
use Illuminate\Support\Facades\Http;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
use Tests\Traits\CreatesArticleFetcher;
|
||||
use Tests\Traits\CreatesFetchActions;
|
||||
|
||||
/**
|
||||
* Belga discovery runs through the website path against a JSON API rather than
|
||||
|
|
@ -16,9 +16,9 @@
|
|||
* registers a catch-all Http::fake in setUp() that a per-test fake cannot
|
||||
* override.
|
||||
*/
|
||||
class ArticleFetcherBelgaTest extends TestCase
|
||||
class FetchWebsiteArticlesBelgaTest extends TestCase
|
||||
{
|
||||
use CreatesArticleFetcher, RefreshDatabase;
|
||||
use CreatesFetchActions, RefreshDatabase;
|
||||
|
||||
private function apiResponse(): string
|
||||
{
|
||||
|
|
@ -45,7 +45,7 @@ public function test_creates_articles_from_belga_api_response(): void
|
|||
{
|
||||
Http::fake(['*' => Http::response($this->apiResponse(), 200)]);
|
||||
|
||||
$result = $this->createArticleFetcher()->getArticlesFromFeed($this->belgaFeed());
|
||||
$result = $this->createFeedFetcher()->execute($this->belgaFeed());
|
||||
|
||||
$this->assertCount(6, $result);
|
||||
$this->assertDatabaseHas('articles', [
|
||||
|
|
@ -62,7 +62,7 @@ public function test_associates_created_articles_with_the_feed(): void
|
|||
|
||||
$feed = $this->belgaFeed();
|
||||
|
||||
$this->createArticleFetcher()->getArticlesFromFeed($feed);
|
||||
$this->createFeedFetcher()->execute($feed);
|
||||
|
||||
$this->assertDatabaseHas('articles', [
|
||||
'url' => 'https://www.belganewsagency.eu/press-releases/35285/',
|
||||
|
|
@ -76,7 +76,7 @@ public function test_returns_empty_collection_when_api_returns_no_articles(): vo
|
|||
// and must be a no-op rather than an error.
|
||||
Http::fake(['*' => Http::response('{"data":[],"_meta":{"total":0}}', 200)]);
|
||||
|
||||
$result = $this->createArticleFetcher()->getArticlesFromFeed($this->belgaFeed());
|
||||
$result = $this->createFeedFetcher()->execute($this->belgaFeed());
|
||||
|
||||
$this->assertEmpty($result);
|
||||
$this->assertDatabaseCount('articles', 0);
|
||||
|
|
@ -87,7 +87,7 @@ public function test_returns_empty_collection_when_api_returns_an_error_page():
|
|||
// The failure mode that caused #115: a 404 HTML body reaching the parser.
|
||||
Http::fake(['*' => Http::response('<html><body>404 Not Found</body></html>', 200)]);
|
||||
|
||||
$result = $this->createArticleFetcher()->getArticlesFromFeed($this->belgaFeed());
|
||||
$result = $this->createFeedFetcher()->execute($this->belgaFeed());
|
||||
|
||||
$this->assertEmpty($result);
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Unit\Actions;
|
||||
|
||||
use App\Actions\FetchArticleDataAction;
|
||||
use App\Actions\PublishRouteArticleAction;
|
||||
use App\Enums\NotificationTypeEnum;
|
||||
use App\Enums\PublishStatusEnum;
|
||||
|
|
@ -10,7 +11,6 @@
|
|||
use App\Models\Feed;
|
||||
use App\Models\Route;
|
||||
use App\Models\RouteArticle;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use App\Services\Notification\NotificationService;
|
||||
use App\Services\Publishing\ArticlePublishingService;
|
||||
use App\Services\Publishing\PublishOutcome;
|
||||
|
|
@ -61,8 +61,8 @@ public function test_publish_uses_stored_article_data_without_fetching(): void
|
|||
'image_url' => 'https://cdn.test/stored.jpg',
|
||||
]);
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldNotReceive('fetchArticleData');
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldNotReceive('execute');
|
||||
|
||||
$publishingService = Mockery::mock(ArticlePublishingService::class);
|
||||
$publishingService->shouldReceive('publishRouteArticle')
|
||||
|
|
@ -87,8 +87,8 @@ public function test_publish_body_comes_from_description_not_content(): void
|
|||
'content' => 'The very much longer full article body text',
|
||||
]);
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldNotReceive('fetchArticleData');
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldNotReceive('execute');
|
||||
|
||||
$captured = null;
|
||||
$publishingService = Mockery::mock(ArticlePublishingService::class);
|
||||
|
|
@ -116,8 +116,8 @@ public function test_publish_falls_back_to_fetching_when_description_is_blank():
|
|||
|
||||
$fetched = ['title' => 'Fetched Title', 'description' => 'Fetched description'];
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldReceive('fetchArticleData')->once()->andReturn($fetched);
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldReceive('execute')->once()->andReturn($fetched);
|
||||
|
||||
$publishingService = Mockery::mock(ArticlePublishingService::class);
|
||||
$publishingService->shouldReceive('publishRouteArticle')
|
||||
|
|
@ -141,8 +141,8 @@ public function test_publish_falls_back_to_fetching_when_article_has_no_stored_d
|
|||
'thumbnail' => 'https://cdn.test/fetched.jpg',
|
||||
];
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldReceive('fetchArticleData')->once()->andReturn($fetched);
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldReceive('execute')->once()->andReturn($fetched);
|
||||
|
||||
$publishingService = Mockery::mock(ArticlePublishingService::class);
|
||||
$publishingService->shouldReceive('publishRouteArticle')
|
||||
|
|
@ -160,8 +160,8 @@ public function test_publish_fails_when_fallback_fetch_returns_nothing(): void
|
|||
{
|
||||
$routeArticle = $this->createRouteArticle(['title' => 'Unreachable Article'], unvalidated: true);
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldReceive('fetchArticleData')->once()->andReturn([]);
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldReceive('execute')->once()->andReturn([]);
|
||||
|
||||
$publishingService = Mockery::mock(ArticlePublishingService::class);
|
||||
$publishingService->shouldNotReceive('publishRouteArticle');
|
||||
|
|
@ -181,8 +181,8 @@ public function test_a_failure_stores_the_reason_and_stops_further_attempts(): v
|
|||
{
|
||||
$routeArticle = $this->createRouteArticle([], unvalidated: true);
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldReceive('fetchArticleData')->andReturn([]);
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldReceive('execute')->andReturn([]);
|
||||
|
||||
$publishingService = Mockery::mock(ArticlePublishingService::class);
|
||||
|
||||
|
|
@ -205,8 +205,8 @@ public function test_a_successful_publish_clears_an_earlier_failure(): void
|
|||
]);
|
||||
$routeArticle->recordPublishFailed('an earlier failure');
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldNotReceive('fetchArticleData');
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldNotReceive('execute');
|
||||
|
||||
$publishingService = Mockery::mock(ArticlePublishingService::class);
|
||||
$publishingService->shouldReceive('publishRouteArticle')
|
||||
|
|
@ -226,8 +226,8 @@ public function test_publish_fails_when_fallback_recovers_only_a_title(): void
|
|||
{
|
||||
$routeArticle = $this->createRouteArticle([], unvalidated: true);
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldReceive('fetchArticleData')->once()->andReturn(['title' => 'Recovered Title']);
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldReceive('execute')->once()->andReturn(['title' => 'Recovered Title']);
|
||||
|
||||
$publishingService = Mockery::mock(ArticlePublishingService::class);
|
||||
$publishingService->shouldNotReceive('publishRouteArticle');
|
||||
|
|
@ -245,8 +245,8 @@ public function test_publish_succeeds_when_fallback_returns_a_description_withou
|
|||
|
||||
$fetched = ['title' => 'Recovered Title', 'description' => 'Recovered description'];
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldReceive('fetchArticleData')->once()->andReturn($fetched);
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldReceive('execute')->once()->andReturn($fetched);
|
||||
|
||||
$publishingService = Mockery::mock(ArticlePublishingService::class);
|
||||
$publishingService->shouldReceive('publishRouteArticle')
|
||||
|
|
@ -268,8 +268,8 @@ public function test_publish_does_not_refetch_for_articles_stored_before_image_u
|
|||
'image_url' => null,
|
||||
]);
|
||||
|
||||
$fetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$fetcher->shouldNotReceive('fetchArticleData');
|
||||
$fetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$fetcher->shouldNotReceive('execute');
|
||||
|
||||
$publishingService = Mockery::mock(ArticlePublishingService::class);
|
||||
$publishingService->shouldReceive('publishRouteArticle')
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace Tests\Unit\Actions;
|
||||
|
||||
use App\Actions\CreateRouteArticlesAction;
|
||||
use App\Actions\FetchArticleDataAction;
|
||||
use App\Actions\ValidateArticleAction;
|
||||
use App\Enums\ApprovalStatusEnum;
|
||||
use App\Models\Article;
|
||||
|
|
@ -12,7 +13,6 @@
|
|||
use App\Models\Route;
|
||||
use App\Models\RouteArticle;
|
||||
use App\Models\Setting;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
|
|
@ -29,7 +29,7 @@ class ValidateArticleActionTest extends TestCase
|
|||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->articleFetcher = Mockery::mock(ArticleFetcher::class);
|
||||
$this->articleFetcher = Mockery::mock(FetchArticleDataAction::class);
|
||||
$this->validateArticle = new ValidateArticleAction($this->articleFetcher, new CreateRouteArticlesAction);
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ private function mockFetchReturning(Article $article, ?string $content, ?string
|
|||
}
|
||||
|
||||
$this->articleFetcher
|
||||
->shouldReceive('fetchArticleData')
|
||||
->shouldReceive('execute')
|
||||
->with($article)
|
||||
->once()
|
||||
->andReturn($data);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
namespace Tests\Unit\Jobs;
|
||||
|
||||
use App\Actions\FetchFeedArticlesAction;
|
||||
use App\Jobs\ArticleDiscoveryForFeedJob;
|
||||
use App\Models\Article;
|
||||
use App\Models\Feed;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use App\Services\Log\LogSaver;
|
||||
use App\Services\Notification\NotificationService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
|
@ -63,9 +63,9 @@ public function test_handle_fetches_articles_and_updates_feed(): void
|
|||
|
||||
$mockArticles = collect(['article1', 'article2']);
|
||||
|
||||
// Mock ArticleFetcher
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('getArticlesFromFeed')
|
||||
// Mock the feed fetch
|
||||
$articleFetcherMock = Mockery::mock(FetchFeedArticlesAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->with($feed)
|
||||
->andReturn($mockArticles);
|
||||
|
|
@ -189,9 +189,9 @@ public function test_handle_logs_start_message_with_correct_context(): void
|
|||
|
||||
$mockArticles = collect([new Article]);
|
||||
|
||||
// Mock ArticleFetcher
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('getArticlesFromFeed')
|
||||
// Mock the feed fetch
|
||||
$articleFetcherMock = Mockery::mock(FetchFeedArticlesAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn($mockArticles);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Unit\Jobs;
|
||||
|
||||
use App\Actions\FetchArticleDataAction;
|
||||
use App\Actions\PublishRouteArticleAction;
|
||||
use App\Enums\NotificationSeverityEnum;
|
||||
use App\Enums\NotificationTypeEnum;
|
||||
|
|
@ -15,7 +16,6 @@
|
|||
use App\Models\Route;
|
||||
use App\Models\RouteArticle;
|
||||
use App\Models\Setting;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use App\Services\Notification\NotificationService;
|
||||
use App\Services\Publishing\ArticlePublishingService;
|
||||
use App\Services\Publishing\PublishOutcome;
|
||||
|
|
@ -94,7 +94,7 @@ public function test_job_uses_queueable_trait(): void
|
|||
|
||||
public function test_handle_returns_early_when_no_approved_route_articles(): void
|
||||
{
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||
|
||||
$job = new PublishNextArticleJob;
|
||||
|
|
@ -113,7 +113,7 @@ public function test_handle_returns_early_when_no_unpublished_approved_route_art
|
|||
'platform_channel_id' => $routeArticle->platform_channel_id,
|
||||
]);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||
|
||||
$job = new PublishNextArticleJob;
|
||||
|
|
@ -131,7 +131,7 @@ public function test_handle_skips_non_approved_route_articles(): void
|
|||
|
||||
RouteArticle::factory()->forRoute($route)->pending()->create(['article_id' => $article->id]);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||
|
||||
$job = new PublishNextArticleJob;
|
||||
|
|
@ -160,8 +160,8 @@ public function test_handle_publishes_oldest_approved_route_article(): void
|
|||
|
||||
$extractedData = ['title' => 'Test Article', 'description' => 'Test description'];
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->with(Mockery::on(fn ($article) => $article->id === $olderArticle->id))
|
||||
->andReturn($extractedData);
|
||||
|
|
@ -189,8 +189,8 @@ public function test_handle_throws_exception_on_publishing_failure(): void
|
|||
$extractedData = ['title' => 'Test Article', 'description' => 'Test description'];
|
||||
$publishException = new PublishException($article, null);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn($extractedData);
|
||||
|
||||
|
|
@ -215,10 +215,10 @@ public function test_handle_skips_publishing_when_last_publication_within_interv
|
|||
]);
|
||||
Setting::setArticlePublishingInterval(10);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||
|
||||
$articleFetcherMock->shouldNotReceive('fetchArticleData');
|
||||
$articleFetcherMock->shouldNotReceive('execute');
|
||||
$publishingServiceMock->shouldNotReceive('publishRouteArticle');
|
||||
|
||||
$job = new PublishNextArticleJob;
|
||||
|
|
@ -235,10 +235,10 @@ public function test_handle_skips_publishing_when_daily_cap_reached(): void
|
|||
Setting::setArticlePublishingInterval(0);
|
||||
Setting::setDailyPublishCap(3);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||
|
||||
$articleFetcherMock->shouldNotReceive('fetchArticleData');
|
||||
$articleFetcherMock->shouldNotReceive('execute');
|
||||
$publishingServiceMock->shouldNotReceive('publishRouteArticle');
|
||||
|
||||
$job = new PublishNextArticleJob;
|
||||
|
|
@ -255,8 +255,8 @@ public function test_handle_publishes_when_below_daily_cap(): void
|
|||
Setting::setArticlePublishingInterval(0);
|
||||
Setting::setDailyPublishCap(3);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn(['title' => 'Test Article', 'description' => 'Test description']);
|
||||
|
||||
|
|
@ -279,8 +279,8 @@ public function test_handle_publishes_when_daily_cap_is_zero(): void
|
|||
Setting::setArticlePublishingInterval(0);
|
||||
Setting::setDailyPublishCap(0);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn(['title' => 'Test Article', 'description' => 'Test description']);
|
||||
|
||||
|
|
@ -307,10 +307,10 @@ public function test_daily_cap_counts_each_channel_publication_separately(): voi
|
|||
Setting::setArticlePublishingInterval(0);
|
||||
Setting::setDailyPublishCap(3);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||
|
||||
$articleFetcherMock->shouldNotReceive('fetchArticleData');
|
||||
$articleFetcherMock->shouldNotReceive('execute');
|
||||
$publishingServiceMock->shouldNotReceive('publishRouteArticle');
|
||||
|
||||
$job = new PublishNextArticleJob;
|
||||
|
|
@ -327,8 +327,8 @@ public function test_handle_ignores_publications_from_previous_days(): void
|
|||
Setting::setArticlePublishingInterval(0);
|
||||
Setting::setDailyPublishCap(3);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn(['title' => 'Test Article', 'description' => 'Test description']);
|
||||
|
||||
|
|
@ -354,8 +354,8 @@ public function test_handle_publishes_when_last_publication_beyond_interval(): v
|
|||
|
||||
$extractedData = ['title' => 'Test Article', 'description' => 'Test description'];
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn($extractedData);
|
||||
|
||||
|
|
@ -381,8 +381,8 @@ public function test_handle_publishes_when_interval_is_zero(): void
|
|||
|
||||
$extractedData = ['title' => 'Test Article', 'description' => 'Test description'];
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn($extractedData);
|
||||
|
||||
|
|
@ -408,8 +408,8 @@ public function test_handle_publishes_when_last_publication_exactly_at_interval(
|
|||
|
||||
$extractedData = ['title' => 'Test Article', 'description' => 'Test description'];
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn($extractedData);
|
||||
|
||||
|
|
@ -432,8 +432,8 @@ public function test_handle_publishes_when_no_previous_publications_exist(): voi
|
|||
|
||||
$extractedData = ['title' => 'Test Article', 'description' => 'Test description'];
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn($extractedData);
|
||||
|
||||
|
|
@ -454,8 +454,8 @@ public function test_handle_creates_warning_notification_when_no_publication_cre
|
|||
|
||||
$extractedData = ['title' => 'No Route Article', 'description' => 'Test description'];
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn($extractedData);
|
||||
|
||||
|
|
@ -486,8 +486,8 @@ public function test_handle_creates_notification_on_publish_exception(): void
|
|||
$extractedData = ['title' => 'Failing Article', 'description' => 'Test description'];
|
||||
$publishException = new PublishException($article, null);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn($extractedData);
|
||||
|
||||
|
|
@ -520,8 +520,8 @@ public function test_handle_skips_route_articles_that_previously_failed(): void
|
|||
$routeArticle = $this->createApprovedRouteArticle();
|
||||
$routeArticle->recordPublishFailed('couldnt_find_community');
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldNotReceive('fetchArticleData');
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldNotReceive('execute');
|
||||
|
||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||
$publishingServiceMock->shouldNotReceive('publishRouteArticle');
|
||||
|
|
@ -541,8 +541,8 @@ public function test_handle_publishes_a_later_article_when_the_oldest_has_failed
|
|||
$next = $this->createApprovedRouteArticle(['title' => 'Next Article']);
|
||||
$next->update(['created_at' => now()->subDay()]);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
$articleFetcherMock = Mockery::mock(FetchArticleDataAction::class);
|
||||
$articleFetcherMock->shouldReceive('execute')
|
||||
->once()
|
||||
->andReturn(['title' => 'Next Article', 'description' => 'Test description']);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue