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(['approval_status' => 'rejected']); $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_routes(): void { $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, 'approval_status' => 'approved' ]); $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, 'approval_status' => 'approved', ]); // 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, 'approval_status' => 'approved']); $platformInstance = PlatformInstance::factory()->create(); $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 ]); // 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, 'approval_status' => 'approved']); $platformInstance = PlatformInstance::factory()->create(); $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') ->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_routes(): void { // Arrange $feed = Feed::factory()->create(); $article = Article::factory()->create(['feed_id' => $feed->id, 'approval_status' => 'approved']); $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)->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, 'approval_status' => 'approved']); $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)->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); } }