3 - Add fedi-discover:poll command with failure isolation
This commit is contained in:
parent
fea8d48f6e
commit
1b652752e1
3 changed files with 201 additions and 0 deletions
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Lvl0\FediDiscover\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Lvl0\FediDiscover\Actions\PollFediverseAction;
|
||||
use Lvl0\FediDiscover\Models\Instance;
|
||||
use Throwable;
|
||||
|
||||
#[Signature('fedi-discover:poll')]
|
||||
#[Description('Poll all enabled fediverse instances for new URLs')]
|
||||
class PollInstancesCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PollFediverseAction $action
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$hadFailure = false;
|
||||
|
||||
Instance::enabled()
|
||||
->get()
|
||||
->each(function (Instance $instance) use (&$hadFailure) {
|
||||
try {
|
||||
$this->action->execute($instance);
|
||||
} catch (Throwable $e) {
|
||||
$hadFailure = true;
|
||||
}
|
||||
});
|
||||
|
||||
if ($hadFailure) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Lvl0\FediDiscover\Console\Commands\PollInstancesCommand;
|
||||
use Lvl0\FediDiscover\Console\Commands\ValidateInstancesCommand;
|
||||
use Lvl0\FediDiscover\Events\UrlDiscovered;
|
||||
|
||||
|
|
@ -14,6 +15,11 @@ class FediDiscoverServiceProvider extends ServiceProvider
|
|||
public function register(): void
|
||||
{
|
||||
$this->mergeConfigFrom(__DIR__ . '/../config/fedi-discover.php', 'fedi-discover');
|
||||
|
||||
$this->app->bind(
|
||||
\Lvl0\FediDiscover\Clients\FediverseClientInterface::class,
|
||||
\Lvl0\FediDiscover\Clients\MastodonClient::class,
|
||||
);
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
|
|
@ -30,6 +36,7 @@ public function boot(): void
|
|||
], 'fedi-discover-config');
|
||||
|
||||
$this->commands([
|
||||
PollInstancesCommand::class,
|
||||
ValidateInstancesCommand::class,
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Lvl0\FediDiscover\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Lvl0\FediDiscover\Actions\PollFediverseAction;
|
||||
use Lvl0\FediDiscover\Clients\FediverseClientInterface;
|
||||
use Lvl0\FediDiscover\Config\InstanceType;
|
||||
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 stub so the command can resolve PollFediverseAction
|
||||
// from the container without making real HTTP calls.
|
||||
$stub = Mockery::mock(FediverseClientInterface::class);
|
||||
$stub->shouldReceive('fetchPostsSince')->andReturn(collect());
|
||||
|
||||
$this->app->bind(FediverseClientInterface::class, fn () => $stub);
|
||||
}
|
||||
|
||||
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_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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue