295 lines
13 KiB
PHP
295 lines
13 KiB
PHP
<?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;
|
|
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;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->service = new ArticlePublishingService();
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
Mockery::close();
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function test_publish_to_routed_channels_throws_exception_for_invalid_article(): void
|
|
{
|
|
$article = Article::factory()->create(['is_valid' => false]);
|
|
$extractedData = ['title' => 'Test Title'];
|
|
|
|
$this->expectException(PublishException::class);
|
|
$this->expectExceptionMessage('CANNOT_PUBLISH_INVALID_ARTICLE');
|
|
|
|
$this->service->publishToRoutedChannels($article, $extractedData);
|
|
}
|
|
|
|
public function test_publish_to_routed_channels_returns_empty_collection_when_no_active_channels(): void
|
|
{
|
|
$feed = Feed::factory()->create();
|
|
$article = Article::factory()->create([
|
|
'feed_id' => $feed->id,
|
|
'is_valid' => true
|
|
]);
|
|
$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_channels_without_active_accounts(): void
|
|
{
|
|
// Arrange: valid article
|
|
$feed = Feed::factory()->create();
|
|
$article = Article::factory()->create([
|
|
'feed_id' => $feed->id,
|
|
'is_valid' => true,
|
|
]);
|
|
|
|
// Create an active channel with no active accounts
|
|
$channel = PlatformChannel::factory()->create();
|
|
$channel->load('platformInstance');
|
|
|
|
// Mock feed->activeChannels()->with()->get() chain to return our channel
|
|
$relationMock = \Mockery::mock(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class);
|
|
$relationMock->shouldReceive('with')->andReturnSelf();
|
|
$relationMock->shouldReceive('get')->andReturn(new EloquentCollection([$channel]));
|
|
|
|
$feedMock = \Mockery::mock(Feed::class)->makePartial();
|
|
$feedMock->setRawAttributes($feed->getAttributes());
|
|
$feedMock->shouldReceive('activeChannels')->andReturn($relationMock);
|
|
|
|
// Attach mocked feed to the article relation
|
|
$article->setRelation('feed', $feedMock);
|
|
|
|
// No publisher should be constructed because there are no active accounts
|
|
|
|
// Also ensure channel->activePlatformAccounts() returns no accounts via relation mock
|
|
$channelPartial = \Mockery::mock($channel)->makePartial();
|
|
$accountsRelation = \Mockery::mock(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class);
|
|
$accountsRelation->shouldReceive('first')->andReturn(null);
|
|
$channelPartial->shouldReceive('activePlatformAccounts')->andReturn($accountsRelation);
|
|
|
|
// Replace channel in relation return with the partial mock
|
|
$relationMock->shouldReceive('get')->andReturn(new EloquentCollection([$channelPartial]));
|
|
|
|
// 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, 'is_valid' => true]);
|
|
|
|
$channel = PlatformChannel::factory()->create();
|
|
$channel->load('platformInstance');
|
|
|
|
// Create an active account and pretend it's active for the channel via relation mock
|
|
$account = PlatformAccount::factory()->create();
|
|
$channelMock = \Mockery::mock($channel)->makePartial();
|
|
$accountsRelation = \Mockery::mock(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class);
|
|
$accountsRelation->shouldReceive('first')->andReturn($account);
|
|
$channelMock->shouldReceive('activePlatformAccounts')->andReturn($accountsRelation);
|
|
|
|
// Mock feed activeChannels chain
|
|
$relationMock = \Mockery::mock(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class);
|
|
$relationMock->shouldReceive('with')->andReturnSelf();
|
|
$relationMock->shouldReceive('get')->andReturn(new EloquentCollection([$channelMock]));
|
|
$feedMock = \Mockery::mock(Feed::class)->makePartial();
|
|
$feedMock->setRawAttributes($feed->getAttributes());
|
|
$feedMock->shouldReceive('activeChannels')->andReturn($relationMock);
|
|
$article->setRelation('feed', $feedMock);
|
|
|
|
// Mock publisher via service seam
|
|
$publisherDouble = \Mockery::mock(LemmyPublisher::class);
|
|
$publisherDouble->shouldReceive('publishToChannel')
|
|
->once()
|
|
->andReturn(['post_view' => ['post' => ['id' => 123]]]);
|
|
$service = \Mockery::mock(ArticlePublishingService::class)->makePartial();
|
|
$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,
|
|
]);
|
|
}
|
|
|
|
public function test_publish_to_routed_channels_handles_publishing_failure_gracefully(): void
|
|
{
|
|
// Arrange
|
|
$feed = Feed::factory()->create();
|
|
$article = Article::factory()->create(['feed_id' => $feed->id, 'is_valid' => true]);
|
|
|
|
$channel = PlatformChannel::factory()->create();
|
|
$channel->load('platformInstance');
|
|
|
|
$account = PlatformAccount::factory()->create();
|
|
$channelMock = \Mockery::mock($channel)->makePartial();
|
|
$accountsRelation = \Mockery::mock(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class);
|
|
$accountsRelation->shouldReceive('first')->andReturn($account);
|
|
$channelMock->shouldReceive('activePlatformAccounts')->andReturn($accountsRelation);
|
|
|
|
$relationMock = \Mockery::mock(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class);
|
|
$relationMock->shouldReceive('with')->andReturnSelf();
|
|
$relationMock->shouldReceive('get')->andReturn(new EloquentCollection([$channelMock]));
|
|
$feedMock = \Mockery::mock(Feed::class)->makePartial();
|
|
$feedMock->setRawAttributes($feed->getAttributes());
|
|
$feedMock->shouldReceive('activeChannels')->andReturn($relationMock);
|
|
$article->setRelation('feed', $feedMock);
|
|
|
|
// Publisher throws an exception via service seam
|
|
$publisherDouble = \Mockery::mock(LemmyPublisher::class);
|
|
$publisherDouble->shouldReceive('publishToChannel')
|
|
->once()
|
|
->andThrow(new Exception('network error'));
|
|
$service = \Mockery::mock(ArticlePublishingService::class)->makePartial();
|
|
$service->shouldAllowMockingProtectedMethods();
|
|
$service->shouldReceive('makePublisher')->andReturn($publisherDouble);
|
|
|
|
// Act
|
|
$result = $service->publishToRoutedChannels($article, ['title' => 'Hello']);
|
|
|
|
// Assert
|
|
$this->assertTrue($result->isEmpty());
|
|
$this->assertDatabaseCount('article_publications', 0);
|
|
}
|
|
|
|
public function test_publish_to_routed_channels_publishes_to_multiple_channels(): void
|
|
{
|
|
// Arrange
|
|
$feed = Feed::factory()->create();
|
|
$article = Article::factory()->create(['feed_id' => $feed->id, 'is_valid' => true]);
|
|
|
|
$channel1 = PlatformChannel::factory()->create();
|
|
$channel2 = PlatformChannel::factory()->create();
|
|
$channel1->load('platformInstance');
|
|
$channel2->load('platformInstance');
|
|
|
|
$account1 = PlatformAccount::factory()->create();
|
|
$account2 = PlatformAccount::factory()->create();
|
|
|
|
$channelMock1 = \Mockery::mock($channel1)->makePartial();
|
|
$accountsRelation1 = \Mockery::mock(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class);
|
|
$accountsRelation1->shouldReceive('first')->andReturn($account1);
|
|
$channelMock1->shouldReceive('activePlatformAccounts')->andReturn($accountsRelation1);
|
|
$channelMock2 = \Mockery::mock($channel2)->makePartial();
|
|
$accountsRelation2 = \Mockery::mock(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class);
|
|
$accountsRelation2->shouldReceive('first')->andReturn($account2);
|
|
$channelMock2->shouldReceive('activePlatformAccounts')->andReturn($accountsRelation2);
|
|
|
|
$relationMock = \Mockery::mock(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class);
|
|
$relationMock->shouldReceive('with')->andReturnSelf();
|
|
$relationMock->shouldReceive('get')->andReturn(new EloquentCollection([$channelMock1, $channelMock2]));
|
|
$feedMock = \Mockery::mock(Feed::class)->makePartial();
|
|
$feedMock->setRawAttributes($feed->getAttributes());
|
|
$feedMock->shouldReceive('activeChannels')->andReturn($relationMock);
|
|
$article->setRelation('feed', $feedMock);
|
|
|
|
$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)->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, 'is_valid' => true]);
|
|
|
|
$channel1 = PlatformChannel::factory()->create();
|
|
$channel2 = PlatformChannel::factory()->create();
|
|
$channel1->load('platformInstance');
|
|
$channel2->load('platformInstance');
|
|
|
|
$account1 = PlatformAccount::factory()->create();
|
|
$account2 = PlatformAccount::factory()->create();
|
|
|
|
$channelMock1 = \Mockery::mock($channel1)->makePartial();
|
|
$accountsRelation1 = \Mockery::mock(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class);
|
|
$accountsRelation1->shouldReceive('first')->andReturn($account1);
|
|
$channelMock1->shouldReceive('activePlatformAccounts')->andReturn($accountsRelation1);
|
|
$channelMock2 = \Mockery::mock($channel2)->makePartial();
|
|
$accountsRelation2 = \Mockery::mock(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class);
|
|
$accountsRelation2->shouldReceive('first')->andReturn($account2);
|
|
$channelMock2->shouldReceive('activePlatformAccounts')->andReturn($accountsRelation2);
|
|
|
|
$relationMock = \Mockery::mock(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class);
|
|
$relationMock->shouldReceive('with')->andReturnSelf();
|
|
$relationMock->shouldReceive('get')->andReturn(new EloquentCollection([$channelMock1, $channelMock2]));
|
|
$feedMock = \Mockery::mock(Feed::class)->makePartial();
|
|
$feedMock->setRawAttributes($feed->getAttributes());
|
|
$feedMock->shouldReceive('activeChannels')->andReturn($relationMock);
|
|
$article->setRelation('feed', $feedMock);
|
|
|
|
$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)->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);
|
|
}
|
|
}
|