Release v1.4.0 #146

Merged
myrmidex merged 71 commits from release/v1.4.0 into main 2026-08-15 00:36:54 +02:00
21 changed files with 260 additions and 254 deletions
Showing only changes of commit dc64dd83b8 - Show all commits

View 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 [];
}
}
}

View 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();
}
}

View file

@ -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 [

View file

@ -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 = [];

View file

@ -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,

View file

@ -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 [];
}
}
}

View file

@ -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));

View file

@ -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(

View file

@ -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',

View file

@ -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');

View file

@ -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'));

View file

@ -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([

View file

@ -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)

View file

@ -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
{

View file

@ -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);

View file

@ -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', [

View file

@ -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);
}

View file

@ -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')

View file

@ -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);

View file

@ -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);

View file

@ -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']);