shouldReceive('fetchPostsSince')->andReturn(collect()); $factoryStub = Mockery::mock(FediverseClientFactory::class); $factoryStub->shouldReceive('for')->andReturn($clientStub); $this->app->instance(FediverseClientFactory::class, $factoryStub); } public function test_it_exits_zero_when_there_are_no_enabled_instances(): void { $this->artisan('fedi-discover:poll') ->assertExitCode(0); } public function test_it_calls_the_action_for_each_enabled_instance_and_skips_disabled(): void { $enabled1 = Instance::create([ 'type' => InstanceType::Mastodon, 'url' => 'https://mastodon.social', 'enabled' => true, 'interval_seconds' => 600, ]); $enabled2 = Instance::create([ 'type' => InstanceType::Mastodon, 'url' => 'https://fosstodon.org', 'enabled' => true, 'interval_seconds' => 600, ]); Instance::create([ 'type' => InstanceType::Mastodon, 'url' => 'https://disabled.example', 'enabled' => false, 'interval_seconds' => 600, ]); $calledWith = []; $action = Mockery::mock(PollFediverseAction::class); $action->shouldReceive('execute') ->twice() ->withArgs(function (Instance $instance) use (&$calledWith): bool { $calledWith[] = $instance->url; return true; }); $this->app->instance(PollFediverseAction::class, $action); $this->artisan('fedi-discover:poll')->assertExitCode(0); $this->assertEqualsCanonicalizing( [$enabled1->url, $enabled2->url], $calledWith, ); } public function test_one_instance_throwing_does_not_stop_remaining_instances_from_being_polled(): void { Instance::create([ 'type' => InstanceType::Mastodon, 'url' => 'https://failing.example', 'enabled' => true, 'interval_seconds' => 600, ]); $healthy = Instance::create([ 'type' => InstanceType::Mastodon, 'url' => 'https://healthy.example', 'enabled' => true, 'interval_seconds' => 600, ]); $calledWith = []; $action = Mockery::mock(PollFediverseAction::class); $action->shouldReceive('execute') ->twice() ->andReturnUsing(function (Instance $instance) use (&$calledWith): void { $calledWith[] = $instance->url; if ($instance->url === 'https://failing.example') { throw new RuntimeException('Connection refused'); } }); $this->app->instance(PollFediverseAction::class, $action); $this->artisan('fedi-discover:poll')->assertExitCode(1); $this->assertEqualsCanonicalizing( ['https://failing.example', $healthy->url], $calledWith, ); } public function test_poll_failed_event_is_dispatched_when_action_throws(): void { Event::fake([PollFailed::class]); $instance = Instance::create([ 'type' => InstanceType::Mastodon, 'url' => 'https://failing.example', 'enabled' => true, 'interval_seconds' => 600, ]); $action = Mockery::mock(PollFediverseAction::class); $action->shouldReceive('execute') ->once() ->andReturnUsing(function (): void { throw new RuntimeException('Connection refused'); }); $this->app->instance(PollFediverseAction::class, $action); $this->artisan('fedi-discover:poll'); Event::assertDispatched(PollFailed::class, function (PollFailed $event) use ($instance): bool { return $event->instance->id === $instance->id && $event->message === 'Connection refused'; }); } public function test_poll_failed_event_is_not_dispatched_on_a_successful_poll(): void { Event::fake([PollFailed::class]); Instance::create([ 'type' => InstanceType::Mastodon, 'url' => 'https://healthy.example', 'enabled' => true, 'interval_seconds' => 600, ]); // setUp() already binds a no-op action stub via the factory; no override needed. $this->artisan('fedi-discover:poll'); Event::assertNotDispatched(PollFailed::class); } public function test_it_exits_one_when_at_least_one_instance_fails(): void { Instance::create([ 'type' => InstanceType::Mastodon, 'url' => 'https://failing.example', 'enabled' => true, 'interval_seconds' => 600, ]); Instance::create([ 'type' => InstanceType::Mastodon, 'url' => 'https://healthy.example', 'enabled' => true, 'interval_seconds' => 600, ]); $action = Mockery::mock(PollFediverseAction::class); $action->shouldReceive('execute') ->twice() ->andReturnUsing(function (Instance $instance): void { if ($instance->url === 'https://failing.example') { throw new RuntimeException('Connection refused'); } }); $this->app->instance(PollFediverseAction::class, $action); $this->artisan('fedi-discover:poll')->assertExitCode(1); } }