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