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(); } /** * @return array{RouteArticle, PlatformChannel, PlatformAccount, Article} */ private function createRouteArticleWithAccount(): array { $feed = Feed::factory()->create(); $platformInstance = PlatformInstance::factory()->create(); $channel = PlatformChannel::factory()->create(['platform_instance_id' => $platformInstance->id]); $account = PlatformAccount::factory()->create(); /** @var Route $route */ $route = Route::factory()->active()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, ]); $channel->platformAccounts()->attach($account->id, [ 'is_active' => true, 'priority' => 50, ]); $article = Article::factory()->create(['feed_id' => $feed->id]); /** @var RouteArticle $routeArticle */ $routeArticle = RouteArticle::factory()->forRoute($route)->approved()->create([ 'article_id' => $article->id, ]); return [$routeArticle, $channel, $account, $article]; } public function test_publish_route_article_returns_null_when_no_active_account(): void { $feed = Feed::factory()->create(); $channel = PlatformChannel::factory()->create(); /** @var Route $route */ $route = Route::factory()->active()->create([ 'feed_id' => $feed->id, 'platform_channel_id' => $channel->id, ]); $article = Article::factory()->create(['feed_id' => $feed->id]); /** @var RouteArticle $routeArticle */ $routeArticle = RouteArticle::factory()->forRoute($route)->approved()->create([ 'article_id' => $article->id, ]); $result = $this->service->publishRouteArticle($routeArticle, ['title' => 'Test']); $this->assertNull($result); $this->assertDatabaseCount('article_publications', 0); } public function test_publish_route_article_successfully_publishes(): void { [$routeArticle, $channel, $account, $article] = $this->createRouteArticleWithAccount(); $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); $result = $service->publishRouteArticle($routeArticle, ['title' => 'Hello']); $this->assertNotNull($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_route_article_handles_publishing_failure_gracefully(): void { [$routeArticle] = $this->createRouteArticleWithAccount(); $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); $result = $service->publishRouteArticle($routeArticle, ['title' => 'Hello']); $this->assertNull($result); $this->assertDatabaseCount('article_publications', 0); } public function test_publish_skips_duplicate_when_url_already_posted_to_channel(): void { [$routeArticle, $channel, $account, $article] = $this->createRouteArticleWithAccount(); // Simulate the URL already being posted to this channel PlatformChannelPost::storePost( PlatformEnum::LEMMY, (string) $channel->channel_id, $channel->name, '999', $article->url, 'Different Title', ); $publisherDouble = Mockery::mock(LemmyPublisher::class); $publisherDouble->shouldNotReceive('publishToChannel'); $service = Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial(); $service->shouldAllowMockingProtectedMethods(); $service->shouldReceive('makePublisher')->andReturn($publisherDouble); $result = $service->publishRouteArticle($routeArticle, ['title' => 'Some Title']); $this->assertNull($result); $this->assertDatabaseCount('article_publications', 0); } public function test_publish_skips_duplicate_when_title_already_posted_to_channel(): void { [$routeArticle, $channel, $account, $article] = $this->createRouteArticleWithAccount(); // 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', ); $publisherDouble = Mockery::mock(LemmyPublisher::class); $publisherDouble->shouldNotReceive('publishToChannel'); $service = Mockery::mock(ArticlePublishingService::class, [$this->logSaver])->makePartial(); $service->shouldAllowMockingProtectedMethods(); $service->shouldReceive('makePublisher')->andReturn($publisherDouble); $result = $service->publishRouteArticle($routeArticle, ['title' => 'Breaking News']); $this->assertNull($result); $this->assertDatabaseCount('article_publications', 0); } public function test_publish_proceeds_when_no_duplicate_exists(): void { [$routeArticle, $channel, $account, $article] = $this->createRouteArticleWithAccount(); // 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); $result = $service->publishRouteArticle($routeArticle, ['title' => 'Unique Title']); $this->assertNotNull($result); $this->assertDatabaseHas('article_publications', [ 'article_id' => $article->id, 'post_id' => 456, ]); } }