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); } // --------------------------------------------------------------------------- // 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']); } // --------------------------------------------------------------------------- // 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; }); } }