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); } protected function tearDown(): void { Mockery::close(); parent::tearDown(); } 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, 'validated_at' => now(), ]); $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, 'validated_at' => now(), ]); // 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, 'validated_at' => now()]); $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, [$this->logSaver])->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, 'validated_at' => now()]); $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, [$this->logSaver])->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, 'validated_at' => now()]); $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, [$this->logSaver])->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, 'validated_at' => now()]); $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, [$this->logSaver])->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); } public function test_publish_skips_duplicate_when_url_already_posted_to_channel(): void { // Arrange $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, 'validated_at' => now(), 'url' => 'https://example.com/article-1', ]); $platformInstance = PlatformInstance::factory()->create(['platform' => 'lemmy']); $channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]); $account = PlatformAccount::factory()->create(); Route::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'is_active' => true, 'priority' => 50, ]); $channel->platformAccounts()->attach($account->id, [ 'is_active' => true, 'priority' => 50, ]); // Simulate the URL already being posted to this channel (synced from Lemmy) PlatformChannelPost::storePost( PlatformEnum::LEMMY, (string) $channel->channel_id, $channel->name, '999', 'https://example.com/article-1', 'Different Title', ); // Publisher should never be called $publisherDouble = \Mockery::mock(LemmyPublisher::class); $publisherDouble->shouldNotReceive('publishToChannel'); $service = \Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial(); $service->shouldAllowMockingProtectedMethods(); $service->shouldReceive('makePublisher')->andReturn($publisherDouble); // Act $result = $service->publishToRoutedChannels($article, ['title' => 'Some Title']); // Assert $this->assertTrue($result->isEmpty()); $this->assertDatabaseCount('article_publications', 0); } public function test_publish_skips_duplicate_when_title_already_posted_to_channel(): void { // Arrange $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, 'validated_at' => now(), 'url' => 'https://example.com/article-new-url', 'title' => 'Breaking News: Something Happened', ]); $platformInstance = PlatformInstance::factory()->create(['platform' => 'lemmy']); $channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]); $account = PlatformAccount::factory()->create(); Route::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'is_active' => true, 'priority' => 50, ]); $channel->platformAccounts()->attach($account->id, [ 'is_active' => true, 'priority' => 50, ]); // 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: Something Happened', ); // Publisher should never be called $publisherDouble = \Mockery::mock(LemmyPublisher::class); $publisherDouble->shouldNotReceive('publishToChannel'); $service = \Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial(); $service->shouldAllowMockingProtectedMethods(); $service->shouldReceive('makePublisher')->andReturn($publisherDouble); // Act $result = $service->publishToRoutedChannels($article, ['title' => 'Breaking News: Something Happened']); // Assert $this->assertTrue($result->isEmpty()); $this->assertDatabaseCount('article_publications', 0); } public function test_publish_proceeds_when_no_duplicate_exists(): void { // Arrange $feed = Feed::factory()->create(); $article = Article::factory()->create([ 'feed_id' => $feed->id, 'validated_at' => now(), 'url' => 'https://example.com/unique-article', ]); $platformInstance = PlatformInstance::factory()->create(['platform' => 'lemmy']); $channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]); $account = PlatformAccount::factory()->create(); Route::create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, 'is_active' => true, 'priority' => 50, ]); $channel->platformAccounts()->attach($account->id, [ 'is_active' => true, 'priority' => 50, ]); // 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); // Act $result = $service->publishToRoutedChannels($article, ['title' => 'Unique Title']); // Assert $this->assertCount(1, $result); $this->assertDatabaseHas('article_publications', [ 'article_id' => $article->id, 'post_id' => 456, ]); } }