2026-04-26 03:31:32 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace Tests\Feature;
|
|
|
|
|
|
|
|
|
|
use App\Listeners\UrlDiscoveredListener;
|
|
|
|
|
use App\Models\Page;
|
|
|
|
|
use App\Models\PageLink;
|
|
|
|
|
use Carbon\CarbonImmutable;
|
|
|
|
|
use Illuminate\Events\CallQueuedListener;
|
|
|
|
|
use Illuminate\Support\Facades\Queue;
|
|
|
|
|
use Lvl0\FediDiscover\Config\InstanceType;
|
|
|
|
|
use Lvl0\FediDiscover\Events\UrlDiscovered;
|
|
|
|
|
use Lvl0\FediDiscover\Models\Instance;
|
|
|
|
|
|
|
|
|
|
class UrlDiscoveryTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Helpers
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
private function makeInstance(): Instance
|
|
|
|
|
{
|
|
|
|
|
return Instance::factory()
|
|
|
|
|
->type(InstanceType::Mastodon)
|
|
|
|
|
->enabled()
|
|
|
|
|
->create();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function makeEvent(Instance $instance, array $overrides = []): UrlDiscovered
|
|
|
|
|
{
|
|
|
|
|
return new UrlDiscovered(
|
|
|
|
|
url: $overrides['url'] ?? 'https://example-blog.com/article',
|
|
|
|
|
instanceId: $overrides['instanceId'] ?? $instance->id,
|
|
|
|
|
discoveredAt: $overrides['discoveredAt'] ?? CarbonImmutable::parse('2026-04-26T12:00:00Z'),
|
|
|
|
|
postUrl: array_key_exists('postUrl', $overrides) ? $overrides['postUrl'] : 'https://mastodon.social/@alice/109876543210',
|
|
|
|
|
postBody: array_key_exists('postBody', $overrides) ? $overrides['postBody'] : 'check this out https://example-blog.com/article',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Test 9 — happy path
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
public function test_listener_creates_target_page_and_source_page_with_link(): void
|
|
|
|
|
{
|
|
|
|
|
$instance = $this->makeInstance();
|
|
|
|
|
$discoveredAt = CarbonImmutable::parse('2026-04-26T12:00:00Z');
|
|
|
|
|
|
|
|
|
|
$event = new UrlDiscovered(
|
|
|
|
|
url: 'https://example-blog.com/article',
|
|
|
|
|
instanceId: $instance->id,
|
|
|
|
|
discoveredAt: $discoveredAt,
|
|
|
|
|
postUrl: 'https://mastodon.social/@alice/109876543210',
|
|
|
|
|
postBody: 'check this out https://example-blog.com/article',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
event($event);
|
|
|
|
|
|
|
|
|
|
// Target page
|
|
|
|
|
$targetPage = Page::where('url', 'https://example-blog.com/article')->first();
|
|
|
|
|
$this->assertNotNull($targetPage);
|
|
|
|
|
|
|
|
|
|
// Source page
|
|
|
|
|
$sourcePage = Page::where('url', 'https://mastodon.social/@alice/109876543210')->first();
|
|
|
|
|
$this->assertNotNull($sourcePage);
|
|
|
|
|
|
|
|
|
|
// Edge
|
|
|
|
|
$link = PageLink::where('source_page_id', $sourcePage->id)
|
|
|
|
|
->where('target_page_id', $targetPage->id)
|
|
|
|
|
->first();
|
|
|
|
|
$this->assertNotNull($link);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Test 10 — idempotency
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
public function test_listener_is_idempotent_on_repeated_event(): void
|
|
|
|
|
{
|
|
|
|
|
$instance = $this->makeInstance();
|
|
|
|
|
$event = $this->makeEvent($instance);
|
|
|
|
|
|
|
|
|
|
event($event);
|
|
|
|
|
event($event);
|
|
|
|
|
|
|
|
|
|
$this->assertSame(2, Page::count());
|
|
|
|
|
$this->assertSame(1, PageLink::count());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Test 11 — null postUrl: only target page, no edge
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
public function test_listener_with_null_post_url_creates_only_target_page(): void
|
|
|
|
|
{
|
|
|
|
|
$instance = $this->makeInstance();
|
|
|
|
|
$event = $this->makeEvent($instance, ['postUrl' => null, 'postBody' => null]);
|
|
|
|
|
|
|
|
|
|
event($event);
|
|
|
|
|
|
|
|
|
|
$this->assertSame(1, Page::count());
|
|
|
|
|
$this->assertSame(0, PageLink::count());
|
|
|
|
|
|
|
|
|
|
$targetPage = Page::where('url', 'https://example-blog.com/article')->first();
|
|
|
|
|
$this->assertNotNull($targetPage);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 16:09:28 +02:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Integration — UrlDiscovered event enqueues crawls for both pages via observer
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
public function test_url_discovered_event_enqueues_crawls_via_observer(): void
|
|
|
|
|
{
|
|
|
|
|
$instance = $this->makeInstance();
|
|
|
|
|
|
|
|
|
|
$event = new UrlDiscovered(
|
|
|
|
|
url: 'https://example-blog.com/article',
|
|
|
|
|
instanceId: $instance->id,
|
|
|
|
|
discoveredAt: CarbonImmutable::parse('2026-04-26T12:00:00Z'),
|
|
|
|
|
postUrl: 'https://mastodon.social/@alice/109876543210',
|
|
|
|
|
postBody: 'check this out https://example-blog.com/article',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
event($event);
|
|
|
|
|
|
|
|
|
|
// Listener creates 2 pages (target + source); observer fires for each → 2 crawl rows
|
|
|
|
|
$this->assertDatabaseCount('page_crawls', 2);
|
|
|
|
|
$this->assertDatabaseHas('page_crawls', ['domain' => 'example-blog.com']);
|
|
|
|
|
$this->assertDatabaseHas('page_crawls', ['domain' => 'mastodon.social']);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 03:31:32 +02:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Test 12 — listener is queued, not run inline
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
public function test_listener_is_pushed_to_queue_not_run_inline(): void
|
|
|
|
|
{
|
|
|
|
|
Queue::fake();
|
|
|
|
|
|
|
|
|
|
$instance = $this->makeInstance();
|
|
|
|
|
$event = $this->makeEvent($instance);
|
|
|
|
|
|
|
|
|
|
event($event);
|
|
|
|
|
|
|
|
|
|
Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job): bool {
|
|
|
|
|
return $job->class === UrlDiscoveredListener::class;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|