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

202 lines
6.3 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\FediverseClientFactory;
use Lvl0\FediDiscover\Clients\FediverseClientInterface;
use Lvl0\FediDiscover\Config\InstanceType;
use Lvl0\FediDiscover\Events\PollFailed;
use Lvl0\FediDiscover\Models\Instance;
use Mockery;
use RuntimeException;
use Tests\TestCase;
class PollInstancesCommandTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Bind a no-op factory stub so the command can resolve PollFediverseAction
// from the container without making real HTTP calls.
$clientStub = Mockery::mock(FediverseClientInterface::class);
$clientStub->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);
}
}