Release v1.3.0 #100
8 changed files with 155 additions and 449 deletions
|
|
@ -2,16 +2,13 @@
|
||||||
|
|
||||||
namespace App\Events;
|
namespace App\Events;
|
||||||
|
|
||||||
use App\Models\Article;
|
use App\Models\RouteArticle;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
class ArticleApproved
|
class RouteArticleApproved
|
||||||
{
|
{
|
||||||
use Dispatchable, SerializesModels;
|
use Dispatchable, SerializesModels;
|
||||||
|
|
||||||
public function __construct(public Article $article)
|
public function __construct(public RouteArticle $routeArticle) {}
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
use App\Enums\NotificationSeverityEnum;
|
use App\Enums\NotificationSeverityEnum;
|
||||||
use App\Enums\NotificationTypeEnum;
|
use App\Enums\NotificationTypeEnum;
|
||||||
use App\Events\ActionPerformed;
|
use App\Events\ActionPerformed;
|
||||||
use App\Events\ArticleApproved;
|
use App\Events\RouteArticleApproved;
|
||||||
use App\Services\Article\ArticleFetcher;
|
use App\Services\Article\ArticleFetcher;
|
||||||
use App\Services\Notification\NotificationService;
|
use App\Services\Notification\NotificationService;
|
||||||
use App\Services\Publishing\ArticlePublishingService;
|
use App\Services\Publishing\ArticlePublishingService;
|
||||||
|
|
@ -23,32 +23,30 @@ public function __construct(
|
||||||
private NotificationService $notificationService,
|
private NotificationService $notificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(ArticleApproved $event): void
|
public function handle(RouteArticleApproved $event): void
|
||||||
{
|
{
|
||||||
$article = $event->article->fresh();
|
$routeArticle = $event->routeArticle;
|
||||||
|
$article = $routeArticle->article;
|
||||||
|
|
||||||
// Skip if already published
|
// Skip if already published to this channel
|
||||||
if ($article->articlePublication()->exists()) {
|
if ($article->articlePublications()
|
||||||
|
->where('platform_channel_id', $routeArticle->platform_channel_id)
|
||||||
|
->exists()
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$article->update(['publish_status' => 'publishing']);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$extractedData = $this->articleFetcher->fetchArticleData($article);
|
$extractedData = $this->articleFetcher->fetchArticleData($article);
|
||||||
$publications = $this->publishingService->publishToRoutedChannels($article, $extractedData);
|
$publication = $this->publishingService->publishRouteArticle($routeArticle, $extractedData);
|
||||||
|
|
||||||
if ($publications->isNotEmpty()) {
|
|
||||||
$article->update(['publish_status' => 'published']);
|
|
||||||
|
|
||||||
|
if ($publication) {
|
||||||
ActionPerformed::dispatch('Published approved article', LogLevelEnum::INFO, [
|
ActionPerformed::dispatch('Published approved article', LogLevelEnum::INFO, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'title' => $article->title,
|
'title' => $article->title,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
$article->update(['publish_status' => 'error']);
|
ActionPerformed::dispatch('No publication created for approved article', LogLevelEnum::WARNING, [
|
||||||
|
|
||||||
ActionPerformed::dispatch('No publications created for approved article', LogLevelEnum::WARNING, [
|
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'title' => $article->title,
|
'title' => $article->title,
|
||||||
]);
|
]);
|
||||||
|
|
@ -57,13 +55,11 @@ public function handle(ArticleApproved $event): void
|
||||||
NotificationTypeEnum::PUBLISH_FAILED,
|
NotificationTypeEnum::PUBLISH_FAILED,
|
||||||
NotificationSeverityEnum::WARNING,
|
NotificationSeverityEnum::WARNING,
|
||||||
"Publish failed: {$article->title}",
|
"Publish failed: {$article->title}",
|
||||||
'No publications were created for this article. Check channel routing configuration.',
|
'No publication was created for this article. Check channel routing configuration.',
|
||||||
$article,
|
$article,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$article->update(['publish_status' => 'error']);
|
|
||||||
|
|
||||||
ActionPerformed::dispatch('Failed to publish approved article', LogLevelEnum::ERROR, [
|
ActionPerformed::dispatch('Failed to publish approved article', LogLevelEnum::ERROR, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Enums\ApprovalStatusEnum;
|
use App\Enums\ApprovalStatusEnum;
|
||||||
|
use App\Events\RouteArticleApproved;
|
||||||
use Database\Factories\RouteArticleFactory;
|
use Database\Factories\RouteArticleFactory;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
@ -88,6 +89,8 @@ public function isRejected(): bool
|
||||||
public function approve(): void
|
public function approve(): void
|
||||||
{
|
{
|
||||||
$this->update(['approval_status' => ApprovalStatusEnum::APPROVED]);
|
$this->update(['approval_status' => ApprovalStatusEnum::APPROVED]);
|
||||||
|
|
||||||
|
event(new RouteArticleApproved($this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reject(): void
|
public function reject(): void
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ public function boot(): void
|
||||||
);
|
);
|
||||||
|
|
||||||
Event::listen(
|
Event::listen(
|
||||||
\App\Events\ArticleApproved::class,
|
\App\Events\RouteArticleApproved::class,
|
||||||
\App\Listeners\PublishApprovedArticleListener::class,
|
\App\Listeners\PublishApprovedArticleListener::class,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,12 @@
|
||||||
use App\Exceptions\PublishException;
|
use App\Exceptions\PublishException;
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\ArticlePublication;
|
use App\Models\ArticlePublication;
|
||||||
use App\Models\Keyword;
|
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
use App\Models\PlatformChannelPost;
|
use App\Models\PlatformChannelPost;
|
||||||
use App\Models\Route;
|
|
||||||
use App\Models\RouteArticle;
|
use App\Models\RouteArticle;
|
||||||
use App\Modules\Lemmy\Services\LemmyPublisher;
|
use App\Modules\Lemmy\Services\LemmyPublisher;
|
||||||
use App\Services\Log\LogSaver;
|
use App\Services\Log\LogSaver;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
class ArticlePublishingService
|
class ArticlePublishingService
|
||||||
|
|
@ -63,63 +60,6 @@ public function publishRouteArticle(RouteArticle $routeArticle, array $extracted
|
||||||
return $this->publishToChannel($article, $extractedData, $channel, $account);
|
return $this->publishToChannel($article, $extractedData, $channel, $account);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use publishRouteArticle() instead. Kept for PublishApprovedArticleListener compatibility.
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $extractedData
|
|
||||||
* @return Collection<int, ArticlePublication>
|
|
||||||
*
|
|
||||||
* @throws PublishException
|
|
||||||
*/
|
|
||||||
public function publishToRoutedChannels(Article $article, array $extractedData): Collection
|
|
||||||
{
|
|
||||||
$feed = $article->feed;
|
|
||||||
|
|
||||||
$activeRoutes = Route::where('feed_id', $feed->id)
|
|
||||||
->where('is_active', true)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$keywordsByChannel = Keyword::where('feed_id', $feed->id)
|
|
||||||
->where('is_active', true)
|
|
||||||
->get()
|
|
||||||
->groupBy('platform_channel_id');
|
|
||||||
|
|
||||||
$matchingRoutes = $activeRoutes->filter(function (Route $route) use ($extractedData, $keywordsByChannel) {
|
|
||||||
$keywords = $keywordsByChannel->get($route->platform_channel_id, collect());
|
|
||||||
if ($keywords->isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$articleContent = ($extractedData['full_article'] ?? '').
|
|
||||||
' '.($extractedData['title'] ?? '').
|
|
||||||
' '.($extractedData['description'] ?? '');
|
|
||||||
|
|
||||||
foreach ($keywords as $keyword) {
|
|
||||||
if (stripos($articleContent, $keyword->keyword) !== false) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return $matchingRoutes->map(function (Route $route) use ($article, $extractedData) {
|
|
||||||
$channel = PlatformChannel::with(['platformInstance', 'activePlatformAccounts'])->find($route->platform_channel_id);
|
|
||||||
$account = $channel?->activePlatformAccounts()->first();
|
|
||||||
|
|
||||||
if (! $account) {
|
|
||||||
$this->logSaver->warning('No active account for channel', $channel, [
|
|
||||||
'article_id' => $article->id,
|
|
||||||
'route_priority' => $route->priority,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->publishToChannel($article, $extractedData, $channel, $account);
|
|
||||||
})->filter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $extractedData
|
* @param array<string, mixed> $extractedData
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Events\ActionPerformed;
|
use App\Events\ActionPerformed;
|
||||||
use App\Events\ArticleApproved;
|
|
||||||
// use App\Events\ArticleReadyToPublish; // Class no longer exists
|
|
||||||
use App\Events\ExceptionLogged;
|
use App\Events\ExceptionLogged;
|
||||||
use App\Events\ExceptionOccurred;
|
use App\Events\ExceptionOccurred;
|
||||||
use App\Events\NewArticleFetched;
|
use App\Events\NewArticleFetched;
|
||||||
|
|
@ -13,8 +11,6 @@
|
||||||
use App\Jobs\PublishNextArticleJob;
|
use App\Jobs\PublishNextArticleJob;
|
||||||
use App\Jobs\SyncChannelPostsJob;
|
use App\Jobs\SyncChannelPostsJob;
|
||||||
use App\Listeners\LogExceptionToDatabase;
|
use App\Listeners\LogExceptionToDatabase;
|
||||||
// use App\Listeners\PublishApprovedArticle; // Class no longer exists
|
|
||||||
// use App\Listeners\PublishArticle; // Class no longer exists
|
|
||||||
use App\Listeners\ValidateArticleListener;
|
use App\Listeners\ValidateArticleListener;
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
|
|
@ -116,33 +112,6 @@ public function test_new_article_fetched_event_is_dispatched(): void
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_article_approved_event_is_dispatched(): void
|
|
||||||
{
|
|
||||||
Event::fake();
|
|
||||||
|
|
||||||
$article = Article::factory()->create();
|
|
||||||
|
|
||||||
event(new ArticleApproved($article));
|
|
||||||
|
|
||||||
Event::assertDispatched(ArticleApproved::class, function (ArticleApproved $event) use ($article) {
|
|
||||||
return $event->article->id === $article->id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test removed - ArticleReadyToPublish class no longer exists
|
|
||||||
// public function test_article_ready_to_publish_event_is_dispatched(): void
|
|
||||||
// {
|
|
||||||
// Event::fake();
|
|
||||||
|
|
||||||
// $article = Article::factory()->create();
|
|
||||||
|
|
||||||
// event(new ArticleReadyToPublish($article));
|
|
||||||
|
|
||||||
// Event::assertDispatched(ArticleReadyToPublish::class, function (ArticleReadyToPublish $event) use ($article) {
|
|
||||||
// return $event->article->id === $article->id;
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
public function test_exception_occurred_event_is_dispatched(): void
|
public function test_exception_occurred_event_is_dispatched(): void
|
||||||
{
|
{
|
||||||
Event::fake();
|
Event::fake();
|
||||||
|
|
@ -248,13 +217,8 @@ public function test_event_listener_registration_works(): void
|
||||||
$listeners = Event::getListeners(NewArticleFetched::class);
|
$listeners = Event::getListeners(NewArticleFetched::class);
|
||||||
$this->assertNotEmpty($listeners);
|
$this->assertNotEmpty($listeners);
|
||||||
|
|
||||||
// ArticleApproved event exists but has no listeners after publishing redesign
|
$listeners = Event::getListeners(\App\Events\RouteArticleApproved::class);
|
||||||
// $listeners = Event::getListeners(ArticleApproved::class);
|
$this->assertNotEmpty($listeners);
|
||||||
// $this->assertNotEmpty($listeners);
|
|
||||||
|
|
||||||
// ArticleReadyToPublish no longer exists - removed this check
|
|
||||||
// $listeners = Event::getListeners(ArticleReadyToPublish::class);
|
|
||||||
// $this->assertNotEmpty($listeners);
|
|
||||||
|
|
||||||
$listeners = Event::getListeners(ExceptionOccurred::class);
|
$listeners = Event::getListeners(ExceptionOccurred::class);
|
||||||
$this->assertNotEmpty($listeners);
|
$this->assertNotEmpty($listeners);
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,19 @@
|
||||||
|
|
||||||
use App\Enums\NotificationSeverityEnum;
|
use App\Enums\NotificationSeverityEnum;
|
||||||
use App\Enums\NotificationTypeEnum;
|
use App\Enums\NotificationTypeEnum;
|
||||||
use App\Events\ArticleApproved;
|
use App\Events\RouteArticleApproved;
|
||||||
use App\Listeners\PublishApprovedArticleListener;
|
use App\Listeners\PublishApprovedArticleListener;
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
|
use App\Models\ArticlePublication;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
use App\Models\Notification;
|
use App\Models\Notification;
|
||||||
|
use App\Models\Route;
|
||||||
|
use App\Models\RouteArticle;
|
||||||
use App\Services\Article\ArticleFetcher;
|
use App\Services\Article\ArticleFetcher;
|
||||||
use App\Services\Notification\NotificationService;
|
use App\Services\Notification\NotificationService;
|
||||||
use App\Services\Publishing\ArticlePublishingService;
|
use App\Services\Publishing\ArticlePublishingService;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Mockery;
|
use Mockery;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -22,15 +24,28 @@ class PublishApprovedArticleListenerTest extends TestCase
|
||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
public function test_exception_during_publishing_creates_error_notification(): void
|
private function createApprovedRouteArticle(string $title = 'Test Article'): RouteArticle
|
||||||
{
|
{
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
|
/** @var Route $route */
|
||||||
|
$route = Route::factory()->active()->create(['feed_id' => $feed->id]);
|
||||||
$article = Article::factory()->create([
|
$article = Article::factory()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
|
'title' => $title,
|
||||||
'title' => 'Test Article',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/** @var RouteArticle $routeArticle */
|
||||||
|
$routeArticle = RouteArticle::factory()->forRoute($route)->approved()->create([
|
||||||
|
'article_id' => $article->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $routeArticle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_exception_during_publishing_creates_error_notification(): void
|
||||||
|
{
|
||||||
|
$routeArticle = $this->createApprovedRouteArticle();
|
||||||
|
|
||||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||||
->once()
|
->once()
|
||||||
|
|
@ -39,13 +54,13 @@ public function test_exception_during_publishing_creates_error_notification(): v
|
||||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||||
|
|
||||||
$listener = new PublishApprovedArticleListener($articleFetcherMock, $publishingServiceMock, new NotificationService);
|
$listener = new PublishApprovedArticleListener($articleFetcherMock, $publishingServiceMock, new NotificationService);
|
||||||
$listener->handle(new ArticleApproved($article));
|
$listener->handle(new RouteArticleApproved($routeArticle));
|
||||||
|
|
||||||
$this->assertDatabaseHas('notifications', [
|
$this->assertDatabaseHas('notifications', [
|
||||||
'type' => NotificationTypeEnum::PUBLISH_FAILED->value,
|
'type' => NotificationTypeEnum::PUBLISH_FAILED->value,
|
||||||
'severity' => NotificationSeverityEnum::ERROR->value,
|
'severity' => NotificationSeverityEnum::ERROR->value,
|
||||||
'notifiable_type' => $article->getMorphClass(),
|
'notifiable_type' => $routeArticle->article->getMorphClass(),
|
||||||
'notifiable_id' => $article->id,
|
'notifiable_id' => $routeArticle->article_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$notification = Notification::first();
|
$notification = Notification::first();
|
||||||
|
|
@ -53,14 +68,9 @@ public function test_exception_during_publishing_creates_error_notification(): v
|
||||||
$this->assertStringContainsString('Connection refused', $notification->message);
|
$this->assertStringContainsString('Connection refused', $notification->message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_no_publications_created_creates_warning_notification(): void
|
public function test_no_publication_created_creates_warning_notification(): void
|
||||||
{
|
{
|
||||||
$feed = Feed::factory()->create();
|
$routeArticle = $this->createApprovedRouteArticle();
|
||||||
$article = Article::factory()->create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
|
|
||||||
'title' => 'Test Article',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$extractedData = ['title' => 'Test Article'];
|
$extractedData = ['title' => 'Test Article'];
|
||||||
|
|
||||||
|
|
@ -70,18 +80,18 @@ public function test_no_publications_created_creates_warning_notification(): voi
|
||||||
->andReturn($extractedData);
|
->andReturn($extractedData);
|
||||||
|
|
||||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||||
$publishingServiceMock->shouldReceive('publishToRoutedChannels')
|
$publishingServiceMock->shouldReceive('publishRouteArticle')
|
||||||
->once()
|
->once()
|
||||||
->andReturn(new Collection);
|
->andReturn(null);
|
||||||
|
|
||||||
$listener = new PublishApprovedArticleListener($articleFetcherMock, $publishingServiceMock, new NotificationService);
|
$listener = new PublishApprovedArticleListener($articleFetcherMock, $publishingServiceMock, new NotificationService);
|
||||||
$listener->handle(new ArticleApproved($article));
|
$listener->handle(new RouteArticleApproved($routeArticle));
|
||||||
|
|
||||||
$this->assertDatabaseHas('notifications', [
|
$this->assertDatabaseHas('notifications', [
|
||||||
'type' => NotificationTypeEnum::PUBLISH_FAILED->value,
|
'type' => NotificationTypeEnum::PUBLISH_FAILED->value,
|
||||||
'severity' => NotificationSeverityEnum::WARNING->value,
|
'severity' => NotificationSeverityEnum::WARNING->value,
|
||||||
'notifiable_type' => $article->getMorphClass(),
|
'notifiable_type' => $routeArticle->article->getMorphClass(),
|
||||||
'notifiable_id' => $article->id,
|
'notifiable_id' => $routeArticle->article_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$notification = Notification::first();
|
$notification = Notification::first();
|
||||||
|
|
@ -90,12 +100,7 @@ public function test_no_publications_created_creates_warning_notification(): voi
|
||||||
|
|
||||||
public function test_successful_publish_does_not_create_notification(): void
|
public function test_successful_publish_does_not_create_notification(): void
|
||||||
{
|
{
|
||||||
$feed = Feed::factory()->create();
|
$routeArticle = $this->createApprovedRouteArticle();
|
||||||
$article = Article::factory()->create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
|
|
||||||
'title' => 'Test Article',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$extractedData = ['title' => 'Test Article'];
|
$extractedData = ['title' => 'Test Article'];
|
||||||
|
|
||||||
|
|
@ -105,16 +110,37 @@ public function test_successful_publish_does_not_create_notification(): void
|
||||||
->andReturn($extractedData);
|
->andReturn($extractedData);
|
||||||
|
|
||||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||||
$publishingServiceMock->shouldReceive('publishToRoutedChannels')
|
$publishingServiceMock->shouldReceive('publishRouteArticle')
|
||||||
->once()
|
->once()
|
||||||
->andReturn(new Collection(['publication']));
|
->andReturn(ArticlePublication::factory()->make());
|
||||||
|
|
||||||
$listener = new PublishApprovedArticleListener($articleFetcherMock, $publishingServiceMock, new NotificationService);
|
$listener = new PublishApprovedArticleListener($articleFetcherMock, $publishingServiceMock, new NotificationService);
|
||||||
$listener->handle(new ArticleApproved($article));
|
$listener->handle(new RouteArticleApproved($routeArticle));
|
||||||
|
|
||||||
$this->assertDatabaseCount('notifications', 0);
|
$this->assertDatabaseCount('notifications', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_skips_already_published_to_channel(): void
|
||||||
|
{
|
||||||
|
$routeArticle = $this->createApprovedRouteArticle();
|
||||||
|
|
||||||
|
ArticlePublication::factory()->create([
|
||||||
|
'article_id' => $routeArticle->article_id,
|
||||||
|
'platform_channel_id' => $routeArticle->platform_channel_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||||
|
$articleFetcherMock->shouldNotReceive('fetchArticleData');
|
||||||
|
|
||||||
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||||
|
$publishingServiceMock->shouldNotReceive('publishRouteArticle');
|
||||||
|
|
||||||
|
$listener = new PublishApprovedArticleListener($articleFetcherMock, $publishingServiceMock, new NotificationService);
|
||||||
|
$listener->handle(new RouteArticleApproved($routeArticle));
|
||||||
|
|
||||||
|
$this->assertTrue(true);
|
||||||
|
}
|
||||||
|
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
{
|
{
|
||||||
Mockery::close();
|
Mockery::close();
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@
|
||||||
use App\Models\PlatformChannelPost;
|
use App\Models\PlatformChannelPost;
|
||||||
use App\Models\PlatformInstance;
|
use App\Models\PlatformInstance;
|
||||||
use App\Models\Route;
|
use App\Models\Route;
|
||||||
|
use App\Models\RouteArticle;
|
||||||
use App\Modules\Lemmy\Services\LemmyPublisher;
|
use App\Modules\Lemmy\Services\LemmyPublisher;
|
||||||
use App\Services\Log\LogSaver;
|
use App\Services\Log\LogSaver;
|
||||||
use App\Services\Publishing\ArticlePublishingService;
|
use App\Services\Publishing\ArticlePublishingService;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
@ -44,90 +44,77 @@ protected function tearDown(): void
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_publish_to_routed_channels_returns_empty_collection_when_no_active_routes(): void
|
/**
|
||||||
|
* @return array{RouteArticle, PlatformChannel, PlatformAccount, Article}
|
||||||
|
*/
|
||||||
|
private function createRouteArticleWithAccount(): array
|
||||||
{
|
{
|
||||||
$feed = Feed::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$article = Article::factory()->create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
|
|
||||||
'validated_at' => now(),
|
|
||||||
]);
|
|
||||||
$extractedData = ['title' => 'Test Title'];
|
|
||||||
|
|
||||||
$result = $this->service->publishToRoutedChannels($article, $extractedData);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(EloquentCollection::class, $result);
|
|
||||||
$this->assertTrue($result->isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_publish_to_routed_channels_skips_routes_without_active_accounts(): void
|
|
||||||
{
|
|
||||||
// Arrange: valid article
|
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$article = Article::factory()->create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
|
|
||||||
'validated_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create a route with a channel but no active accounts
|
|
||||||
$channel = PlatformChannel::factory()->create();
|
|
||||||
|
|
||||||
Route::create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'platform_channel_id' => $channel->id,
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Don't create any platform accounts for the channel
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$result = $this->service->publishToRoutedChannels($article, ['title' => 'Test']);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertTrue($result->isEmpty());
|
|
||||||
$this->assertDatabaseCount('article_publications', 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_publish_to_routed_channels_successfully_publishes_to_channel(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$article = Article::factory()->create(['feed_id' => $feed->id, 'validated_at' => now()]);
|
|
||||||
|
|
||||||
$platformInstance = PlatformInstance::factory()->create();
|
$platformInstance = PlatformInstance::factory()->create();
|
||||||
$channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
$channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
||||||
$account = PlatformAccount::factory()->create();
|
$account = PlatformAccount::factory()->create();
|
||||||
|
|
||||||
// Create route
|
/** @var Route $route */
|
||||||
Route::create([
|
$route = Route::factory()->active()->create([
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Attach account to channel as active
|
|
||||||
$channel->platformAccounts()->attach($account->id, [
|
$channel->platformAccounts()->attach($account->id, [
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'priority' => 50,
|
'priority' => 50,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Mock publisher via service seam
|
$article = Article::factory()->create(['feed_id' => $feed->id]);
|
||||||
$publisherDouble = \Mockery::mock(LemmyPublisher::class);
|
|
||||||
|
/** @var RouteArticle $routeArticle */
|
||||||
|
$routeArticle = RouteArticle::factory()->forRoute($route)->approved()->create([
|
||||||
|
'article_id' => $article->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [$routeArticle, $channel, $account, $article];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_publish_route_article_returns_null_when_no_active_account(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
/** @var Route $route */
|
||||||
|
$route = Route::factory()->active()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$article = Article::factory()->create(['feed_id' => $feed->id]);
|
||||||
|
|
||||||
|
/** @var RouteArticle $routeArticle */
|
||||||
|
$routeArticle = RouteArticle::factory()->forRoute($route)->approved()->create([
|
||||||
|
'article_id' => $article->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->service->publishRouteArticle($routeArticle, ['title' => 'Test']);
|
||||||
|
|
||||||
|
$this->assertNull($result);
|
||||||
|
$this->assertDatabaseCount('article_publications', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_publish_route_article_successfully_publishes(): void
|
||||||
|
{
|
||||||
|
[$routeArticle, $channel, $account, $article] = $this->createRouteArticleWithAccount();
|
||||||
|
|
||||||
|
$publisherDouble = Mockery::mock(LemmyPublisher::class);
|
||||||
$publisherDouble->shouldReceive('publishToChannel')
|
$publisherDouble->shouldReceive('publishToChannel')
|
||||||
->once()
|
->once()
|
||||||
->andReturn(['post_view' => ['post' => ['id' => 123]]]);
|
->andReturn(['post_view' => ['post' => ['id' => 123]]]);
|
||||||
$service = \Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
|
|
||||||
|
$service = Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
|
||||||
$service->shouldAllowMockingProtectedMethods();
|
$service->shouldAllowMockingProtectedMethods();
|
||||||
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
|
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
|
||||||
|
|
||||||
// Act
|
$result = $service->publishRouteArticle($routeArticle, ['title' => 'Hello']);
|
||||||
$result = $service->publishToRoutedChannels($article, ['title' => 'Hello']);
|
|
||||||
|
|
||||||
// Assert
|
$this->assertNotNull($result);
|
||||||
$this->assertCount(1, $result);
|
|
||||||
$this->assertDatabaseHas('article_publications', [
|
$this->assertDatabaseHas('article_publications', [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
|
|
@ -136,236 +123,55 @@ public function test_publish_to_routed_channels_successfully_publishes_to_channe
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_publish_to_routed_channels_handles_publishing_failure_gracefully(): void
|
public function test_publish_route_article_handles_publishing_failure_gracefully(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
[$routeArticle] = $this->createRouteArticleWithAccount();
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$article = Article::factory()->create(['feed_id' => $feed->id, 'validated_at' => now()]);
|
|
||||||
|
|
||||||
$platformInstance = PlatformInstance::factory()->create();
|
$publisherDouble = Mockery::mock(LemmyPublisher::class);
|
||||||
$channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
|
||||||
$account = PlatformAccount::factory()->create();
|
|
||||||
|
|
||||||
// Create route
|
|
||||||
Route::create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'platform_channel_id' => $channel->id,
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Attach account to channel as active
|
|
||||||
$channel->platformAccounts()->attach($account->id, [
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Publisher throws an exception via service seam
|
|
||||||
$publisherDouble = \Mockery::mock(LemmyPublisher::class);
|
|
||||||
$publisherDouble->shouldReceive('publishToChannel')
|
$publisherDouble->shouldReceive('publishToChannel')
|
||||||
->once()
|
->once()
|
||||||
->andThrow(new Exception('network error'));
|
->andThrow(new Exception('network error'));
|
||||||
$service = \Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
|
|
||||||
|
$service = Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
|
||||||
$service->shouldAllowMockingProtectedMethods();
|
$service->shouldAllowMockingProtectedMethods();
|
||||||
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
|
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
|
||||||
|
|
||||||
// Act
|
$result = $service->publishRouteArticle($routeArticle, ['title' => 'Hello']);
|
||||||
$result = $service->publishToRoutedChannels($article, ['title' => 'Hello']);
|
|
||||||
|
|
||||||
// Assert
|
$this->assertNull($result);
|
||||||
$this->assertTrue($result->isEmpty());
|
|
||||||
$this->assertDatabaseCount('article_publications', 0);
|
$this->assertDatabaseCount('article_publications', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_publish_to_routed_channels_publishes_to_multiple_routes(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$article = Article::factory()->create(['feed_id' => $feed->id, 'validated_at' => now()]);
|
|
||||||
|
|
||||||
$platformInstance = PlatformInstance::factory()->create();
|
|
||||||
$channel1 = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
|
||||||
$channel2 = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
|
||||||
$account1 = PlatformAccount::factory()->create();
|
|
||||||
$account2 = PlatformAccount::factory()->create();
|
|
||||||
|
|
||||||
// Create routes
|
|
||||||
Route::create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'platform_channel_id' => $channel1->id,
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 100,
|
|
||||||
]);
|
|
||||||
|
|
||||||
Route::create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'platform_channel_id' => $channel2->id,
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Attach accounts to channels as active
|
|
||||||
$channel1->platformAccounts()->attach($account1->id, [
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
$channel2->platformAccounts()->attach($account2->id, [
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$publisherDouble = \Mockery::mock(LemmyPublisher::class);
|
|
||||||
$publisherDouble->shouldReceive('publishToChannel')
|
|
||||||
->once()->andReturn(['post_view' => ['post' => ['id' => 100]]]);
|
|
||||||
$publisherDouble->shouldReceive('publishToChannel')
|
|
||||||
->once()->andReturn(['post_view' => ['post' => ['id' => 200]]]);
|
|
||||||
$service = \Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
|
|
||||||
$service->shouldAllowMockingProtectedMethods();
|
|
||||||
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$result = $service->publishToRoutedChannels($article, ['title' => 'Hello']);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertCount(2, $result);
|
|
||||||
$this->assertDatabaseHas('article_publications', ['post_id' => 100]);
|
|
||||||
$this->assertDatabaseHas('article_publications', ['post_id' => 200]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_publish_to_routed_channels_filters_out_failed_publications(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$article = Article::factory()->create(['feed_id' => $feed->id, 'validated_at' => now()]);
|
|
||||||
|
|
||||||
$platformInstance = PlatformInstance::factory()->create();
|
|
||||||
$channel1 = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
|
||||||
$channel2 = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
|
||||||
$account1 = PlatformAccount::factory()->create();
|
|
||||||
$account2 = PlatformAccount::factory()->create();
|
|
||||||
|
|
||||||
// Create routes
|
|
||||||
Route::create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'platform_channel_id' => $channel1->id,
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 100,
|
|
||||||
]);
|
|
||||||
|
|
||||||
Route::create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'platform_channel_id' => $channel2->id,
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Attach accounts to channels as active
|
|
||||||
$channel1->platformAccounts()->attach($account1->id, [
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
$channel2->platformAccounts()->attach($account2->id, [
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$publisherDouble = \Mockery::mock(LemmyPublisher::class);
|
|
||||||
$publisherDouble->shouldReceive('publishToChannel')
|
|
||||||
->once()->andReturn(['post_view' => ['post' => ['id' => 300]]]);
|
|
||||||
$publisherDouble->shouldReceive('publishToChannel')
|
|
||||||
->once()->andThrow(new Exception('failed'));
|
|
||||||
$service = \Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
|
|
||||||
$service->shouldAllowMockingProtectedMethods();
|
|
||||||
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$result = $service->publishToRoutedChannels($article, ['title' => 'Hello']);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertCount(1, $result);
|
|
||||||
$this->assertDatabaseHas('article_publications', ['post_id' => 300]);
|
|
||||||
$this->assertDatabaseCount('article_publications', 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_publish_skips_duplicate_when_url_already_posted_to_channel(): void
|
public function test_publish_skips_duplicate_when_url_already_posted_to_channel(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
[$routeArticle, $channel, $account, $article] = $this->createRouteArticleWithAccount();
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$article = Article::factory()->create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
|
|
||||||
'validated_at' => now(),
|
// Simulate the URL already being posted to this channel
|
||||||
'url' => 'https://example.com/article-1',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$platformInstance = PlatformInstance::factory()->create(['platform' => 'lemmy']);
|
|
||||||
$channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
|
||||||
$account = PlatformAccount::factory()->create();
|
|
||||||
|
|
||||||
Route::create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'platform_channel_id' => $channel->id,
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$channel->platformAccounts()->attach($account->id, [
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Simulate the URL already being posted to this channel (synced from Lemmy)
|
|
||||||
PlatformChannelPost::storePost(
|
PlatformChannelPost::storePost(
|
||||||
PlatformEnum::LEMMY,
|
PlatformEnum::LEMMY,
|
||||||
(string) $channel->channel_id,
|
(string) $channel->channel_id,
|
||||||
$channel->name,
|
$channel->name,
|
||||||
'999',
|
'999',
|
||||||
'https://example.com/article-1',
|
$article->url,
|
||||||
'Different Title',
|
'Different Title',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Publisher should never be called
|
$publisherDouble = Mockery::mock(LemmyPublisher::class);
|
||||||
$publisherDouble = \Mockery::mock(LemmyPublisher::class);
|
|
||||||
$publisherDouble->shouldNotReceive('publishToChannel');
|
$publisherDouble->shouldNotReceive('publishToChannel');
|
||||||
$service = \Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
|
|
||||||
|
$service = Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
|
||||||
$service->shouldAllowMockingProtectedMethods();
|
$service->shouldAllowMockingProtectedMethods();
|
||||||
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
|
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
|
||||||
|
|
||||||
// Act
|
$result = $service->publishRouteArticle($routeArticle, ['title' => 'Some Title']);
|
||||||
$result = $service->publishToRoutedChannels($article, ['title' => 'Some Title']);
|
|
||||||
|
|
||||||
// Assert
|
$this->assertNull($result);
|
||||||
$this->assertTrue($result->isEmpty());
|
|
||||||
$this->assertDatabaseCount('article_publications', 0);
|
$this->assertDatabaseCount('article_publications', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_publish_skips_duplicate_when_title_already_posted_to_channel(): void
|
public function test_publish_skips_duplicate_when_title_already_posted_to_channel(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
[$routeArticle, $channel, $account, $article] = $this->createRouteArticleWithAccount();
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$article = Article::factory()->create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
|
|
||||||
'validated_at' => now(),
|
|
||||||
'url' => 'https://example.com/article-new-url',
|
|
||||||
'title' => 'Breaking News: Something Happened',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$platformInstance = PlatformInstance::factory()->create(['platform' => 'lemmy']);
|
|
||||||
$channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
|
||||||
$account = PlatformAccount::factory()->create();
|
|
||||||
|
|
||||||
Route::create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'platform_channel_id' => $channel->id,
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$channel->platformAccounts()->attach($account->id, [
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Simulate the same title already posted with a different URL
|
// Simulate the same title already posted with a different URL
|
||||||
PlatformChannelPost::storePost(
|
PlatformChannelPost::storePost(
|
||||||
|
|
@ -374,50 +180,25 @@ public function test_publish_skips_duplicate_when_title_already_posted_to_channe
|
||||||
$channel->name,
|
$channel->name,
|
||||||
'888',
|
'888',
|
||||||
'https://example.com/different-url',
|
'https://example.com/different-url',
|
||||||
'Breaking News: Something Happened',
|
'Breaking News',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Publisher should never be called
|
$publisherDouble = Mockery::mock(LemmyPublisher::class);
|
||||||
$publisherDouble = \Mockery::mock(LemmyPublisher::class);
|
|
||||||
$publisherDouble->shouldNotReceive('publishToChannel');
|
$publisherDouble->shouldNotReceive('publishToChannel');
|
||||||
$service = \Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
|
|
||||||
|
$service = Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
|
||||||
$service->shouldAllowMockingProtectedMethods();
|
$service->shouldAllowMockingProtectedMethods();
|
||||||
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
|
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
|
||||||
|
|
||||||
// Act
|
$result = $service->publishRouteArticle($routeArticle, ['title' => 'Breaking News']);
|
||||||
$result = $service->publishToRoutedChannels($article, ['title' => 'Breaking News: Something Happened']);
|
|
||||||
|
|
||||||
// Assert
|
$this->assertNull($result);
|
||||||
$this->assertTrue($result->isEmpty());
|
|
||||||
$this->assertDatabaseCount('article_publications', 0);
|
$this->assertDatabaseCount('article_publications', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_publish_proceeds_when_no_duplicate_exists(): void
|
public function test_publish_proceeds_when_no_duplicate_exists(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
[$routeArticle, $channel, $account, $article] = $this->createRouteArticleWithAccount();
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$article = Article::factory()->create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
|
|
||||||
'validated_at' => now(),
|
|
||||||
'url' => 'https://example.com/unique-article',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$platformInstance = PlatformInstance::factory()->create(['platform' => 'lemmy']);
|
|
||||||
$channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
|
|
||||||
$account = PlatformAccount::factory()->create();
|
|
||||||
|
|
||||||
Route::create([
|
|
||||||
'feed_id' => $feed->id,
|
|
||||||
'platform_channel_id' => $channel->id,
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$channel->platformAccounts()->attach($account->id, [
|
|
||||||
'is_active' => true,
|
|
||||||
'priority' => 50,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Existing post in the channel has a completely different URL and title
|
// Existing post in the channel has a completely different URL and title
|
||||||
PlatformChannelPost::storePost(
|
PlatformChannelPost::storePost(
|
||||||
|
|
@ -429,19 +210,18 @@ public function test_publish_proceeds_when_no_duplicate_exists(): void
|
||||||
'Totally Different Title',
|
'Totally Different Title',
|
||||||
);
|
);
|
||||||
|
|
||||||
$publisherDouble = \Mockery::mock(LemmyPublisher::class);
|
$publisherDouble = Mockery::mock(LemmyPublisher::class);
|
||||||
$publisherDouble->shouldReceive('publishToChannel')
|
$publisherDouble->shouldReceive('publishToChannel')
|
||||||
->once()
|
->once()
|
||||||
->andReturn(['post_view' => ['post' => ['id' => 456]]]);
|
->andReturn(['post_view' => ['post' => ['id' => 456]]]);
|
||||||
$service = \Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
|
|
||||||
|
$service = Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
|
||||||
$service->shouldAllowMockingProtectedMethods();
|
$service->shouldAllowMockingProtectedMethods();
|
||||||
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
|
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
|
||||||
|
|
||||||
// Act
|
$result = $service->publishRouteArticle($routeArticle, ['title' => 'Unique Title']);
|
||||||
$result = $service->publishToRoutedChannels($article, ['title' => 'Unique Title']);
|
|
||||||
|
|
||||||
// Assert
|
$this->assertNotNull($result);
|
||||||
$this->assertCount(1, $result);
|
|
||||||
$this->assertDatabaseHas('article_publications', [
|
$this->assertDatabaseHas('article_publications', [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'post_id' => 456,
|
'post_id' => 456,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue