fedi-feed-router/tests/Unit/Services/Publishing/ArticlePublishingServiceTest.php

231 lines
8.2 KiB
PHP
Raw Normal View History

2025-08-06 21:49:13 +02:00
<?php
namespace Tests\Unit\Services\Publishing;
use App\Enums\PlatformEnum;
use App\Models\Article;
use App\Models\Feed;
use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use App\Models\PlatformChannelPost;
2025-08-06 21:49:13 +02:00
use App\Models\PlatformInstance;
2025-08-10 16:18:09 +02:00
use App\Models\Route;
use App\Models\RouteArticle;
2025-08-06 21:49:13 +02:00
use App\Modules\Lemmy\Services\LemmyPublisher;
use App\Services\Log\LogSaver;
use App\Services\Publishing\ArticlePublishingService;
use Exception;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class ArticlePublishingServiceTest extends TestCase
{
use RefreshDatabase;
protected ArticlePublishingService $service;
2025-08-15 02:50:42 +02:00
protected LogSaver $logSaver;
2025-08-06 21:49:13 +02:00
protected function setUp(): void
{
parent::setUp();
2025-08-15 02:50:42 +02:00
$this->logSaver = Mockery::mock(LogSaver::class);
$this->logSaver->shouldReceive('info')->zeroOrMoreTimes();
$this->logSaver->shouldReceive('warning')->zeroOrMoreTimes();
$this->logSaver->shouldReceive('error')->zeroOrMoreTimes();
$this->logSaver->shouldReceive('debug')->zeroOrMoreTimes();
$this->service = new ArticlePublishingService($this->logSaver);
2025-08-06 21:49:13 +02:00
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
/**
* @return array{RouteArticle, PlatformChannel, PlatformAccount, Article}
*/
private function createRouteArticleWithAccount(): array
2025-08-06 21:49:13 +02:00
{
$feed = Feed::factory()->create();
2025-08-10 16:18:09 +02:00
$platformInstance = PlatformInstance::factory()->create();
$channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
$account = PlatformAccount::factory()->create();
2025-08-10 01:26:56 +02:00
/** @var Route $route */
$route = Route::factory()->active()->create([
2025-08-10 16:18:09 +02:00
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
]);
2025-08-10 01:26:56 +02:00
2025-08-10 16:18:09 +02:00
$channel->platformAccounts()->attach($account->id, [
'is_active' => true,
'priority' => 50,
2025-08-10 16:18:09 +02:00
]);
2025-08-10 01:26:56 +02:00
$article = Article::factory()->create(['feed_id' => $feed->id]);
2025-08-10 01:26:56 +02:00
/** @var RouteArticle $routeArticle */
$routeArticle = RouteArticle::factory()->forRoute($route)->approved()->create([
2025-08-10 01:26:56 +02:00
'article_id' => $article->id,
]);
return [$routeArticle, $channel, $account, $article];
2025-08-06 21:49:13 +02:00
}
public function test_publish_route_article_returns_null_when_no_active_account(): void
2025-08-06 21:49:13 +02:00
{
2025-08-10 01:26:56 +02:00
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create();
2025-08-10 01:26:56 +02:00
/** @var Route $route */
$route = Route::factory()->active()->create([
2025-08-10 16:18:09 +02:00
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
]);
2025-08-10 01:26:56 +02:00
$article = Article::factory()->create(['feed_id' => $feed->id]);
2025-08-10 01:26:56 +02:00
/** @var RouteArticle $routeArticle */
$routeArticle = RouteArticle::factory()->forRoute($route)->approved()->create([
'article_id' => $article->id,
]);
2025-08-10 01:26:56 +02:00
$result = $this->service->publishRouteArticle($routeArticle, ['title' => 'Test']);
2025-08-10 01:26:56 +02:00
$this->assertNull($result);
2025-08-10 01:26:56 +02:00
$this->assertDatabaseCount('article_publications', 0);
2025-08-06 21:49:13 +02:00
}
public function test_publish_route_article_successfully_publishes(): void
2025-08-06 21:49:13 +02:00
{
[$routeArticle, $channel, $account, $article] = $this->createRouteArticleWithAccount();
2025-08-10 16:18:09 +02:00
$publisherDouble = Mockery::mock(LemmyPublisher::class);
2025-08-10 01:26:56 +02:00
$publisherDouble->shouldReceive('publishToChannel')
->once()
->andReturn(['post_view' => ['post' => ['id' => 123]]]);
$service = Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
2025-08-10 01:26:56 +02:00
$service->shouldAllowMockingProtectedMethods();
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
$result = $service->publishRouteArticle($routeArticle, ['title' => 'Hello']);
2025-08-10 01:26:56 +02:00
$this->assertNotNull($result);
$this->assertDatabaseHas('article_publications', [
'article_id' => $article->id,
'platform_channel_id' => $channel->id,
'post_id' => 123,
'published_by' => $account->username,
]);
2025-08-06 21:49:13 +02:00
}
public function test_publish_route_article_handles_publishing_failure_gracefully(): void
2025-08-06 21:49:13 +02:00
{
[$routeArticle] = $this->createRouteArticleWithAccount();
2025-08-10 16:18:09 +02:00
$publisherDouble = Mockery::mock(LemmyPublisher::class);
2025-08-10 01:26:56 +02:00
$publisherDouble->shouldReceive('publishToChannel')
->once()
->andThrow(new Exception('network error'));
$service = Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
2025-08-10 01:26:56 +02:00
$service->shouldAllowMockingProtectedMethods();
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
$result = $service->publishRouteArticle($routeArticle, ['title' => 'Hello']);
2025-08-10 01:26:56 +02:00
$this->assertNull($result);
$this->assertDatabaseCount('article_publications', 0);
2025-08-06 21:49:13 +02:00
}
public function test_publish_skips_duplicate_when_url_already_posted_to_channel(): void
{
[$routeArticle, $channel, $account, $article] = $this->createRouteArticleWithAccount();
// Simulate the URL already being posted to this channel
PlatformChannelPost::storePost(
PlatformEnum::LEMMY,
(string) $channel->channel_id,
$channel->name,
'999',
$article->url,
'Different Title',
);
$publisherDouble = Mockery::mock(LemmyPublisher::class);
$publisherDouble->shouldNotReceive('publishToChannel');
$service = Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
$service->shouldAllowMockingProtectedMethods();
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
$result = $service->publishRouteArticle($routeArticle, ['title' => 'Some Title']);
$this->assertNull($result);
$this->assertDatabaseCount('article_publications', 0);
}
public function test_publish_skips_duplicate_when_title_already_posted_to_channel(): void
{
[$routeArticle, $channel, $account, $article] = $this->createRouteArticleWithAccount();
// Simulate the same title already posted with a different URL
PlatformChannelPost::storePost(
PlatformEnum::LEMMY,
(string) $channel->channel_id,
$channel->name,
'888',
'https://example.com/different-url',
'Breaking News',
);
$publisherDouble = Mockery::mock(LemmyPublisher::class);
$publisherDouble->shouldNotReceive('publishToChannel');
$service = Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
$service->shouldAllowMockingProtectedMethods();
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
$result = $service->publishRouteArticle($routeArticle, ['title' => 'Breaking News']);
$this->assertNull($result);
$this->assertDatabaseCount('article_publications', 0);
}
public function test_publish_proceeds_when_no_duplicate_exists(): void
{
[$routeArticle, $channel, $account, $article] = $this->createRouteArticleWithAccount();
// Existing post in the channel has a completely different URL and title
PlatformChannelPost::storePost(
PlatformEnum::LEMMY,
(string) $channel->channel_id,
$channel->name,
'777',
'https://example.com/other-article',
'Totally Different Title',
);
$publisherDouble = Mockery::mock(LemmyPublisher::class);
$publisherDouble->shouldReceive('publishToChannel')
->once()
->andReturn(['post_view' => ['post' => ['id' => 456]]]);
$service = Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
$service->shouldAllowMockingProtectedMethods();
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
$result = $service->publishRouteArticle($routeArticle, ['title' => 'Unique Title']);
$this->assertNotNull($result);
$this->assertDatabaseHas('article_publications', [
'article_id' => $article->id,
'post_id' => 456,
]);
}
2025-08-10 01:26:56 +02:00
}