fedi-feed-router/tests/Feature/MirrorDuplicateDetectionTest.php

199 lines
6.9 KiB
PHP
Raw Permalink Normal View History

<?php
namespace Tests\Feature;
use App\Actions\FetchArticleDataAction;
use App\Actions\PublishRouteArticleAction;
use App\Enums\NotificationTypeEnum;
use App\Enums\PublishStatusEnum;
use App\Events\RouteArticleApproved;
use App\Listeners\PublishApprovedArticleListener;
use App\Models\Article;
use App\Models\Feed;
use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use App\Models\PlatformChannelPost;
use App\Models\PlatformInstance;
use App\Models\Route;
use App\Models\RouteArticle;
use App\Modules\Lemmy\Services\LemmyApiService;
use App\Modules\Lemmy\Services\LemmyPublisher;
use App\Services\Log\LogSaver;
use App\Services\Notification\NotificationService;
use App\Services\Publishing\ArticlePublishingService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Mockery;
use Tests\TestCase;
class MirrorDuplicateDetectionTest extends TestCase
{
use RefreshDatabase;
/** @var array{RouteArticle, PlatformChannel, Article} */
private array $fixture;
protected function setUp(): void
{
parent::setUp();
$feed = Feed::factory()->create();
$instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.test']);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $instance->id,
'channel_id' => 'news',
'name' => 'news',
]);
$account = PlatformAccount::factory()->create(['instance_url' => 'https://lemmy.test']);
/** @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,
'url' => 'https://news.test/already-posted',
'title' => 'Already Posted',
]);
/** @var RouteArticle $routeArticle */
$routeArticle = RouteArticle::factory()->forRoute($route)->approved()->create([
'article_id' => $article->id,
]);
$this->fixture = [$routeArticle, $channel, $article];
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
private function service(?LemmyPublisher $publisher = null): ArticlePublishingService
{
$service = Mockery::mock(ArticlePublishingService::class, [app(LogSaver::class)])->makePartial();
$service->shouldAllowMockingProtectedMethods();
$service->shouldReceive('makePublisher')->andReturn(
$publisher ?? Mockery::mock(LemmyPublisher::class)
);
return $service;
}
public function test_sync_writes_the_key_the_duplicate_check_reads(): void
{
[$routeArticle, $channel, $article] = $this->fixture;
// Populate the mirror through the real sync path rather than seeding a
// row by hand, so write and read cannot silently disagree.
Http::fake([
'*/api/v3/post/list*' => Http::response([
'posts' => [[
'post' => [
'id' => 555,
'url' => $article->url,
'name' => 'Already Posted',
'published' => '2026-08-02T10:00:00Z',
],
]],
]),
]);
(new LemmyApiService('https://lemmy.test'))
->syncChannelPosts('token', $channel, 8);
$this->assertDatabaseCount('platform_channel_posts', 1);
$publisher = Mockery::mock(LemmyPublisher::class);
$publisher->shouldNotReceive('publishToChannel');
$result = $this->service($publisher)->publishRouteArticle($routeArticle, ['title' => 'Already Posted']);
$this->assertTrue($result->wasSkipped());
$this->assertDatabaseCount('article_publications', 0);
}
public function test_a_skipped_duplicate_is_not_reported_as_a_publish_failure(): void
{
[$routeArticle, $channel, $article] = $this->fixture;
PlatformChannelPost::storePost($channel, '555', $article->url, 'Already Posted');
$fetcher = Mockery::mock(FetchArticleDataAction::class);
$fetcher->shouldNotReceive('execute');
$publisher = Mockery::mock(LemmyPublisher::class);
$publisher->shouldNotReceive('publishToChannel');
(new PublishApprovedArticleListener(new PublishRouteArticleAction($fetcher, $this->service($publisher), new NotificationService)))
->handle(new RouteArticleApproved($routeArticle));
$this->assertSame(PublishStatusEnum::SKIPPED, $routeArticle->fresh()->publish_status);
$this->assertDatabaseCount('notifications', 0);
}
public function test_a_genuine_failure_is_still_reported(): void
{
[$routeArticle] = $this->fixture;
$fetcher = Mockery::mock(FetchArticleDataAction::class);
$fetcher->shouldNotReceive('execute');
$publisher = Mockery::mock(LemmyPublisher::class);
$publisher->shouldReceive('publishToChannel')->andThrow(new \RuntimeException('Lemmy rejected the post'));
(new PublishApprovedArticleListener(new PublishRouteArticleAction($fetcher, $this->service($publisher), new NotificationService)))
->handle(new RouteArticleApproved($routeArticle));
$this->assertSame(PublishStatusEnum::ERROR, $routeArticle->fresh()->publish_status);
$this->assertDatabaseHas('notifications', [
'type' => NotificationTypeEnum::PUBLISH_FAILED->value,
]);
}
public function test_mirror_is_keyed_by_the_local_channel(): void
{
[, $channel, $article] = $this->fixture;
PlatformChannelPost::storePost(
$channel,
'555',
$article->url,
'Already Posted',
);
$this->assertDatabaseHas('platform_channel_posts', [
'platform_channel_id' => $channel->id,
'post_id' => '555',
]);
}
public function test_a_second_instance_with_the_same_community_name_does_not_collide(): void
{
[, $channel, $article] = $this->fixture;
$otherInstance = PlatformInstance::factory()->create(['url' => 'https://other.test']);
$otherChannel = PlatformChannel::factory()->create([
'platform_instance_id' => $otherInstance->id,
'channel_id' => 'news',
'name' => 'news',
]);
PlatformChannelPost::storePost($channel, '1', $article->url, 'Same Title');
// Same community name on a different instance is a different community.
$this->assertFalse(
PlatformChannelPost::duplicateExists($otherChannel, $article->url, 'Same Title')
);
$this->assertTrue(
PlatformChannelPost::duplicateExists($channel, $article->url, 'Same Title')
);
}
}