Http::response([], 200), ]); (new MastodonClient)->fetchPostsSince($this->mastodonInstance(), null); Http::assertSent(fn ($request) => str_starts_with($request->url(), 'https://mastodon.social/api/v1/timelines/public') && $request->method() === 'GET' ); } public function test_it_omits_min_id_on_first_poll(): void { Http::fake(['*' => Http::response([], 200)]); (new MastodonClient)->fetchPostsSince($this->mastodonInstance(), null); Http::assertSent(fn ($request) => ! str_contains($request->url(), 'min_id')); } public function test_it_passes_min_id_on_subsequent_polls(): void { Http::fake(['*' => Http::response([], 200)]); (new MastodonClient)->fetchPostsSince($this->mastodonInstance(), '109876543210'); Http::assertSent(fn ($request) => str_contains($request->url(), 'min_id=109876543210')); } public function test_it_returns_an_empty_collection_when_the_api_returns_no_posts(): void { Http::fake(['*' => Http::response([], 200)]); $posts = (new MastodonClient)->fetchPostsSince($this->mastodonInstance(), null); $this->assertInstanceOf(Collection::class, $posts); $this->assertTrue($posts->isEmpty()); } public function test_it_maps_each_status_to_a_fediverse_post(): void { Http::fake([ '*' => Http::response([ $this->mastodonStatus(id: '109876543210', url: 'https://mastodon.social/@alice/109876543210', content: '
Hello
'), $this->mastodonStatus(id: '109876543211', url: 'https://mastodon.social/@bob/109876543211', content: 'World
'), ], 200), ]); $posts = (new MastodonClient)->fetchPostsSince($this->mastodonInstance(), null); $this->assertCount(2, $posts); $this->assertInstanceOf(FediversePost::class, $posts->first()); $this->assertSame('109876543210', $posts->first()->cursorId); $this->assertSame('https://mastodon.social/@alice/109876543210', $posts->first()->selfUrl); $this->assertSame('Hello
', $posts->first()->body); } public function test_it_falls_back_to_uri_when_url_is_null(): void { Http::fake([ '*' => Http::response([ $this->mastodonStatus( id: '109876543210', url: null, uri: 'https://hachyderm.io/users/bob/statuses/5678', content: 'federated post
' ), ], 200), ]); $posts = (new MastodonClient)->fetchPostsSince($this->mastodonInstance(), null); $this->assertSame('https://hachyderm.io/users/bob/statuses/5678', $posts->first()->selfUrl); } public function test_it_preserves_newest_first_ordering_from_the_api(): void { Http::fake([ '*' => Http::response([ $this->mastodonStatus(id: '300', url: 'https://mastodon.social/@a/300'), $this->mastodonStatus(id: '200', url: 'https://mastodon.social/@b/200'), $this->mastodonStatus(id: '100', url: 'https://mastodon.social/@c/100'), ], 200), ]); $posts = (new MastodonClient)->fetchPostsSince($this->mastodonInstance(), null); $this->assertSame(['300', '200', '100'], $posts->pluck('cursorId')->all()); } public function test_it_returns_an_empty_collection_on_a_non_2xx_response(): void { Http::fake(['*' => Http::response('Too many requests', 429)]); $posts = (new MastodonClient)->fetchPostsSince($this->mastodonInstance(), null); $this->assertInstanceOf(Collection::class, $posts); $this->assertTrue($posts->isEmpty()); } public function test_it_returns_an_empty_collection_when_the_response_body_is_not_json(): void { Http::fake(['*' => Http::response('error', 200)]); $posts = (new MastodonClient)->fetchPostsSince($this->mastodonInstance(), null); $this->assertInstanceOf(Collection::class, $posts); $this->assertTrue($posts->isEmpty()); } public function test_it_sends_the_configured_user_agent(): void { Http::fake(['*' => Http::response([], 200)]); (new MastodonClient)->fetchPostsSince($this->mastodonInstance(), null); $expected = config('fedi-discover.http.user_agent'); Http::assertSent(fn ($request) => $request->header('User-Agent')[0] === $expected); } private function mastodonInstance(): Instance { return new Instance([ 'type' => InstanceType::Mastodon, 'url' => 'https://mastodon.social', ]); } /** * @return arrayexample
', ): array { return [ 'id' => $id, 'url' => $url, 'uri' => $uri ?? "https://mastodon.social/users/x/statuses/{$id}", 'content' => $content, 'created_at' => '2026-04-25T10:00:00Z', 'account' => ['acct' => 'alice@mastodon.social'], ]; } }