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

300 lines
11 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\Exceptions\PublishException;
use App\Models\Article;
use App\Models\ArticlePublication;
use App\Models\Feed;
use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use App\Models\PlatformInstance;
2025-08-10 16:18:09 +02:00
use App\Models\Route;
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\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use RuntimeException;
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();
}
public function test_publish_to_routed_channels_throws_exception_for_invalid_article(): void
{
2025-08-10 21:18:20 +02:00
$article = Article::factory()->create(['approval_status' => 'rejected']);
2025-08-06 21:49:13 +02:00
$extractedData = ['title' => 'Test Title'];
$this->expectException(PublishException::class);
$this->expectExceptionMessage('CANNOT_PUBLISH_INVALID_ARTICLE');
$this->service->publishToRoutedChannels($article, $extractedData);
}
2025-08-10 16:18:09 +02:00
public function test_publish_to_routed_channels_returns_empty_collection_when_no_active_routes(): void
2025-08-06 21:49:13 +02:00
{
$feed = Feed::factory()->create();
$article = Article::factory()->create([
'feed_id' => $feed->id,
2025-08-10 21:18:20 +02:00
'approval_status' => 'approved'
2025-08-06 21:49:13 +02:00
]);
$extractedData = ['title' => 'Test Title'];
$result = $this->service->publishToRoutedChannels($article, $extractedData);
$this->assertInstanceOf(EloquentCollection::class, $result);
$this->assertTrue($result->isEmpty());
}
2025-08-10 16:18:09 +02:00
public function test_publish_to_routed_channels_skips_routes_without_active_accounts(): void
2025-08-06 21:49:13 +02:00
{
2025-08-10 01:26:56 +02:00
// Arrange: valid article
$feed = Feed::factory()->create();
$article = Article::factory()->create([
'feed_id' => $feed->id,
2025-08-10 21:18:20 +02:00
'approval_status' => 'approved',
2025-08-10 01:26:56 +02:00
]);
2025-08-10 16:18:09 +02:00
// Create a route with a channel but no active accounts
2025-08-10 01:26:56 +02:00
$channel = PlatformChannel::factory()->create();
2025-08-10 16:18:09 +02:00
Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'is_active' => true,
'priority' => 50
]);
2025-08-10 01:26:56 +02:00
2025-08-10 16:18:09 +02:00
// Don't create any platform accounts for the channel
2025-08-10 01:26:56 +02:00
// Act
$result = $this->service->publishToRoutedChannels($article, ['title' => 'Test']);
// Assert
$this->assertTrue($result->isEmpty());
$this->assertDatabaseCount('article_publications', 0);
2025-08-06 21:49:13 +02:00
}
public function test_publish_to_routed_channels_successfully_publishes_to_channel(): void
{
2025-08-10 01:26:56 +02:00
// Arrange
$feed = Feed::factory()->create();
2025-08-10 21:18:20 +02:00
$article = Article::factory()->create(['feed_id' => $feed->id, 'approval_status' => 'approved']);
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
2025-08-10 16:18:09 +02:00
// Create route
Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'is_active' => true,
'priority' => 50
]);
2025-08-10 01:26:56 +02:00
2025-08-10 16:18:09 +02:00
// Attach account to channel as active
$channel->platformAccounts()->attach($account->id, [
'is_active' => true,
'priority' => 50
]);
2025-08-10 01:26:56 +02:00
// Mock publisher via service seam
$publisherDouble = \Mockery::mock(LemmyPublisher::class);
$publisherDouble->shouldReceive('publishToChannel')
->once()
->andReturn(['post_view' => ['post' => ['id' => 123]]]);
2025-08-15 02:50:42 +02:00
$service = \Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
2025-08-10 01:26:56 +02:00
$service->shouldAllowMockingProtectedMethods();
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
// Act
$result = $service->publishToRoutedChannels($article, ['title' => 'Hello']);
// Assert
$this->assertCount(1, $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_to_routed_channels_handles_publishing_failure_gracefully(): void
{
2025-08-10 01:26:56 +02:00
// Arrange
$feed = Feed::factory()->create();
2025-08-10 21:18:20 +02:00
$article = Article::factory()->create(['feed_id' => $feed->id, 'approval_status' => 'approved']);
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
2025-08-10 16:18:09 +02:00
// Create route
Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'is_active' => true,
'priority' => 50
]);
2025-08-10 01:26:56 +02:00
2025-08-10 16:18:09 +02:00
// Attach account to channel as active
$channel->platformAccounts()->attach($account->id, [
'is_active' => true,
'priority' => 50
]);
2025-08-10 01:26:56 +02:00
// Publisher throws an exception via service seam
$publisherDouble = \Mockery::mock(LemmyPublisher::class);
$publisherDouble->shouldReceive('publishToChannel')
->once()
->andThrow(new Exception('network error'));
2025-08-15 02:50:42 +02:00
$service = \Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
2025-08-10 01:26:56 +02:00
$service->shouldAllowMockingProtectedMethods();
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
// Act
$result = $service->publishToRoutedChannels($article, ['title' => 'Hello']);
// Assert
$this->assertTrue($result->isEmpty());
$this->assertDatabaseCount('article_publications', 0);
2025-08-06 21:49:13 +02:00
}
2025-08-10 16:18:09 +02:00
public function test_publish_to_routed_channels_publishes_to_multiple_routes(): void
2025-08-06 21:49:13 +02:00
{
2025-08-10 01:26:56 +02:00
// Arrange
$feed = Feed::factory()->create();
2025-08-10 21:18:20 +02:00
$article = Article::factory()->create(['feed_id' => $feed->id, 'approval_status' => 'approved']);
2025-08-10 01:26:56 +02:00
2025-08-10 16:18:09 +02:00
$platformInstance = PlatformInstance::factory()->create();
$channel1 = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
$channel2 = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
2025-08-10 01:26:56 +02:00
$account1 = PlatformAccount::factory()->create();
$account2 = PlatformAccount::factory()->create();
2025-08-10 16:18:09 +02:00
// 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
]);
2025-08-10 01:26:56 +02:00
$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]]]);
2025-08-15 02:50:42 +02:00
$service = \Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
2025-08-10 01:26:56 +02:00
$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]);
2025-08-06 21:49:13 +02:00
}
public function test_publish_to_routed_channels_filters_out_failed_publications(): void
{
2025-08-10 01:26:56 +02:00
// Arrange
$feed = Feed::factory()->create();
2025-08-10 21:18:20 +02:00
$article = Article::factory()->create(['feed_id' => $feed->id, 'approval_status' => 'approved']);
2025-08-10 01:26:56 +02:00
2025-08-10 16:18:09 +02:00
$platformInstance = PlatformInstance::factory()->create();
$channel1 = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
$channel2 = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]);
2025-08-10 01:26:56 +02:00
$account1 = PlatformAccount::factory()->create();
$account2 = PlatformAccount::factory()->create();
2025-08-10 16:18:09 +02:00
// 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
]);
2025-08-10 01:26:56 +02:00
$publisherDouble = \Mockery::mock(LemmyPublisher::class);
$publisherDouble->shouldReceive('publishToChannel')
->once()->andReturn(['post_view' => ['post' => ['id' => 300]]]);
$publisherDouble->shouldReceive('publishToChannel')
->once()->andThrow(new Exception('failed'));
2025-08-15 02:50:42 +02:00
$service = \Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial();
2025-08-10 01:26:56 +02:00
$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);
2025-08-06 21:49:13 +02:00
}
2025-08-10 01:26:56 +02:00
}