trove/packages/Lvl0/FediDiscover/tests/Feature/PollFediverseActionTest.php

223 lines
7.8 KiB
PHP

<?php
declare(strict_types=1);
namespace Lvl0\FediDiscover\Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Lvl0\FediDiscover\Actions\PollFediverseAction;
use Lvl0\FediDiscover\Clients\FediverseClient;
use Lvl0\FediDiscover\Clients\FediversePost;
use Lvl0\FediDiscover\Config\InstanceType;
use Lvl0\FediDiscover\Events\UrlDiscovered;
use Lvl0\FediDiscover\Models\Instance;
use Mockery;
use Tests\TestCase;
class PollFediverseActionTest extends TestCase
{
use RefreshDatabase;
public function test_it_fires_one_event_per_extracted_url(): void
{
Event::fake([UrlDiscovered::class]);
$this->poll([
new FediversePost('1', 'https://mastodon.social/@alice/1', 'See https://example.com/one and https://other.example/two'),
]);
Event::assertDispatched(UrlDiscovered::class, fn (UrlDiscovered $e) => $e->url === 'https://example.com/one');
Event::assertDispatched(UrlDiscovered::class, fn (UrlDiscovered $e) => $e->url === 'https://other.example/two');
Event::assertDispatchedTimes(UrlDiscovered::class, 2);
}
public function test_it_extracts_urls_from_html_anchor_tags(): void
{
Event::fake([UrlDiscovered::class]);
$this->poll([
new FediversePost('1', 'https://mastodon.social/@alice/1', '<p>Check <a href="https://example.com/article">this</a>!</p>'),
]);
Event::assertDispatched(UrlDiscovered::class, fn (UrlDiscovered $e) => $e->url === 'https://example.com/article');
Event::assertDispatchedTimes(UrlDiscovered::class, 1);
}
public function test_it_extracts_urls_from_markdown_links(): void
{
Event::fake([UrlDiscovered::class]);
$this->poll(
posts: [new FediversePost('1', 'https://lemmy.world/post/42', 'A [great article](https://example.com/article) about trees.')],
instanceUrl: 'https://lemmy.world',
);
Event::assertDispatched(UrlDiscovered::class, fn (UrlDiscovered $e) => $e->url === 'https://example.com/article');
Event::assertDispatchedTimes(UrlDiscovered::class, 1);
}
public function test_it_strips_trailing_punctuation_from_urls(): void
{
Event::fake([UrlDiscovered::class]);
$this->poll([
new FediversePost('1', 'https://mastodon.social/@alice/1', 'Check https://example.com/article, it is great. Also https://other.example/page.'),
]);
Event::assertDispatched(UrlDiscovered::class, fn (UrlDiscovered $e) => $e->url === 'https://example.com/article');
Event::assertDispatched(UrlDiscovered::class, fn (UrlDiscovered $e) => $e->url === 'https://other.example/page');
}
public function test_it_deduplicates_urls_within_a_single_post(): void
{
Event::fake([UrlDiscovered::class]);
$this->poll([
new FediversePost('1', 'https://mastodon.social/@alice/1', 'Here is https://example.com/article and again https://example.com/article'),
]);
Event::assertDispatchedTimes(UrlDiscovered::class, 1);
}
public function test_it_filters_urls_on_the_polling_instance_host(): void
{
Event::fake([UrlDiscovered::class]);
$this->poll([
new FediversePost('1', 'https://mastodon.social/@alice/1', 'See https://mastodon.social/@bob/42 and https://example.com/article'),
]);
Event::assertDispatched(UrlDiscovered::class, fn (UrlDiscovered $e) => $e->url === 'https://example.com/article');
Event::assertDispatchedTimes(UrlDiscovered::class, 1);
}
public function test_it_ignores_posts_with_a_null_body(): void
{
Event::fake([UrlDiscovered::class]);
$this->poll([
new FediversePost('1', 'https://mastodon.social/@alice/1', null),
]);
Event::assertNotDispatched(UrlDiscovered::class);
}
public function test_it_ignores_non_http_schemes(): void
{
Event::fake([UrlDiscovered::class]);
$this->poll([
new FediversePost('1', 'https://mastodon.social/@alice/1', 'Email mailto:alice@example.com or try ftp://files.example.com/x'),
]);
Event::assertNotDispatched(UrlDiscovered::class);
}
public function test_it_passes_post_self_url_and_body_through_to_the_event(): void
{
Event::fake([UrlDiscovered::class]);
$body = 'Here is https://example.com/article with surrounding context.';
$this->poll([
new FediversePost('1', 'https://mastodon.social/@alice/1', $body),
]);
Event::assertDispatched(UrlDiscovered::class, fn (UrlDiscovered $e) => $e->postUrl === 'https://mastodon.social/@alice/1' && $e->postBody === $body
);
}
public function test_it_processes_multiple_posts(): void
{
Event::fake([UrlDiscovered::class]);
$this->poll([
new FediversePost('1', 'https://mastodon.social/@alice/1', 'See https://example.com/one'),
new FediversePost('2', 'https://mastodon.social/@bob/2', 'Also https://example.com/two'),
]);
Event::assertDispatchedTimes(UrlDiscovered::class, 2);
}
public function test_it_updates_last_seen_id_to_the_first_posts_cursor(): void
{
$instance = $this->makeInstance();
// Clients return newest-first; the action treats posts[0]
// as the new high-water mark without inspecting cursor values.
$this->pollInstance($instance, [
new FediversePost('newest-cursor', 'https://mastodon.social/@alice/3', 'x'),
new FediversePost('middle-cursor', 'https://mastodon.social/@bob/2', 'y'),
new FediversePost('oldest-cursor', 'https://mastodon.social/@carol/1', 'z'),
]);
$this->assertSame('newest-cursor', $instance->fresh()->last_seen_id);
}
public function test_it_updates_last_polled_at(): void
{
$instance = $this->makeInstance();
$this->assertNull($instance->last_polled_at);
$this->pollInstance($instance, [
new FediversePost('1', 'https://mastodon.social/@alice/1', 'x'),
]);
$this->assertNotNull($instance->fresh()->last_polled_at);
}
public function test_it_passes_the_existing_last_seen_id_to_the_client(): void
{
$instance = $this->makeInstance(['last_seen_id' => '999']);
$client = Mockery::mock(FediverseClient::class);
$client->shouldReceive('fetchPostsSince')
->once()
->with($instance, $instance->last_seen_id)
->andReturn([]);
(new PollFediverseAction($client))->execute($instance);
}
public function test_it_leaves_last_seen_id_unchanged_when_no_posts_are_returned(): void
{
$instance = $this->makeInstance(['last_seen_id' => '500']);
$this->pollInstance($instance, []);
$this->assertSame('500', $instance->fresh()->last_seen_id);
}
/**
* @param array<FediversePost> $posts
*/
private function poll(array $posts, string $instanceUrl = 'https://mastodon.social'): void
{
$this->pollInstance($this->makeInstance(['url' => $instanceUrl]), $posts);
}
/**
* @param array<FediversePost> $posts
*/
private function pollInstance(Instance $instance, array $posts): void
{
$client = Mockery::mock(FediverseClient::class);
$client->shouldReceive('fetchPostsSince')->andReturn($posts);
(new PollFediverseAction($client))->execute($instance);
}
/**
* @param array<string, mixed> $overrides
*/
private function makeInstance(array $overrides = []): Instance
{
return Instance::create(array_merge([
'type' => InstanceType::Mastodon,
'url' => 'https://mastodon.social',
'enabled' => true,
'interval_seconds' => 600,
], $overrides));
}
}