2 - Add fedi-discover:validate console command
This commit is contained in:
parent
fc1c8ba020
commit
52d6b493cb
6 changed files with 389 additions and 2 deletions
|
|
@ -0,0 +1,64 @@
|
|||
<?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\Models\Instance;
|
||||
|
||||
#[Signature('fedi-discover:validate {--enabled-only}')]
|
||||
#[Description('Validate saved instances')]
|
||||
class ValidateInstancesCommand extends Command
|
||||
{
|
||||
public function handle(): int
|
||||
{
|
||||
$instances = Instance::query();
|
||||
|
||||
if ($this->option('enabled-only')) {
|
||||
$instances->enabled();
|
||||
}
|
||||
|
||||
$instances = $instances->get();
|
||||
|
||||
$invalidInstances = collect();
|
||||
|
||||
$instances->each(function (Instance $instance) use ($invalidInstances) {
|
||||
$reasons = collect();
|
||||
|
||||
if (filter_var($instance->url, FILTER_VALIDATE_URL) === false) {
|
||||
$reasons->add('Invalid URL: ' . $instance->url);
|
||||
}
|
||||
|
||||
if ($instance->interval_seconds < 1) {
|
||||
$reasons->add('Invalid interval seconds: ' . $instance->interval_seconds);
|
||||
}
|
||||
|
||||
if ($reasons->isNotEmpty()) {
|
||||
$invalidInstances->add([
|
||||
'instance' => $instance,
|
||||
'reasons' => $reasons,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$this->info((string) $instances->count());
|
||||
$this->info(($instances->count() - $invalidInstances->count()) . ' valid');
|
||||
$this->line($invalidInstances->count() . ' invalid');
|
||||
|
||||
if ($invalidInstances->isNotEmpty()) {
|
||||
$invalidInstances->each(function (array $instanceArray) {
|
||||
$instance = $instanceArray['instance'];
|
||||
$reason = $instanceArray['reasons']->join(', ');
|
||||
$this->warn($instance->id . ' - ' . $instance->url);
|
||||
$this->line(' : ' . $reason);
|
||||
});
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
namespace Lvl0\FediDiscover\Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Lvl0\FediDiscover\Config\InstanceType;
|
||||
use Lvl0\FediDiscover\Models\Instance;
|
||||
|
||||
/**
|
||||
|
|
@ -20,7 +21,33 @@ class InstanceFactory extends Factory
|
|||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
'type' => null,
|
||||
'url' => fake()->url,
|
||||
'enabled' => null,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => [],
|
||||
'last_polled_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
public function type(InstanceType $type): self
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'type' => $type->value,
|
||||
]);
|
||||
}
|
||||
|
||||
public function enabled(): self
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'enabled' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function disabled(): self
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
namespace Lvl0\FediDiscover;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Lvl0\FediDiscover\Console\Commands\ValidateInstancesCommand;
|
||||
|
||||
class FediDiscoverServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -21,6 +22,10 @@ public function boot(): void
|
|||
$this->publishes([
|
||||
__DIR__ . '/../config/fedi-discover.php' => config_path('fedi-discover.php'),
|
||||
], 'fedi-discover-config');
|
||||
|
||||
$this->commands([
|
||||
ValidateInstancesCommand::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,25 @@
|
|||
|
||||
namespace Lvl0\FediDiscover\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Lvl0\FediDiscover\Config\InstanceType;
|
||||
use Lvl0\FediDiscover\Database\Factories\InstanceFactory;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property InstanceType $type
|
||||
* @property string $url
|
||||
* @property bool $enabled
|
||||
* @property int $interval_seconds
|
||||
* @property array<string, mixed> $extras
|
||||
* @property Carbon|null $last_polled_at
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
class Instance extends Model
|
||||
{
|
||||
/** @use HasFactory<InstanceFactory> */
|
||||
|
|
@ -26,7 +39,7 @@ class Instance extends Model
|
|||
'last_polled_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function scopeEnabled($query)
|
||||
public function scopeEnabled($query): Builder
|
||||
{
|
||||
return $query->where('enabled', true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Lvl0\FediDiscover\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Lvl0\FediDiscover\Config\InstanceConfig;
|
||||
use Lvl0\FediDiscover\Config\InstanceType;
|
||||
use Lvl0\FediDiscover\Models\Instance;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InstanceConfigPersistenceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_instance_config_toArray_is_mass_assignable_on_the_model(): void
|
||||
{
|
||||
$config = InstanceConfig::fromArray([
|
||||
'type' => InstanceType::Mastodon->value,
|
||||
'url' => 'https://mastodon.social',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => ['token' => 'abc123'],
|
||||
]);
|
||||
|
||||
Instance::create($config->toArray());
|
||||
|
||||
$this->artisan('fedi-discover:validate')
|
||||
->assertExitCode(0);
|
||||
}
|
||||
|
||||
public function test_an_instance_config_survives_a_write_read_cycle_through_the_model(): void
|
||||
{
|
||||
$original = InstanceConfig::fromArray([
|
||||
'type' => InstanceType::Mastodon->value,
|
||||
'url' => 'https://hachyderm.io',
|
||||
'enabled' => false,
|
||||
'interval_seconds' => 900,
|
||||
'extras' => ['foo' => 'bar'],
|
||||
]);
|
||||
|
||||
Instance::create($original->toArray());
|
||||
|
||||
$instance = Instance::query()->firstOrFail();
|
||||
|
||||
$roundTripped = InstanceConfig::fromArray([
|
||||
'type' => $instance->type->value,
|
||||
'url' => $instance->url,
|
||||
'enabled' => $instance->enabled,
|
||||
'interval_seconds' => $instance->interval_seconds,
|
||||
'extras' => $instance->extras,
|
||||
]);
|
||||
|
||||
$this->assertEquals($original, $roundTripped);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Lvl0\FediDiscover\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Lvl0\FediDiscover\Config\InstanceType;
|
||||
use Lvl0\FediDiscover\Models\Instance;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ValidateInstancesCommandTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_exits_zero_when_the_database_is_empty(): void
|
||||
{
|
||||
$this->artisan('fedi-discover:validate')
|
||||
->assertExitCode(0);
|
||||
}
|
||||
|
||||
public function test_it_exits_zero_when_all_instances_are_valid(): void
|
||||
{
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'https://mastodon.social',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
$this->artisan('fedi-discover:validate')
|
||||
->assertExitCode(0);
|
||||
}
|
||||
|
||||
public function test_it_exits_nonzero_when_a_row_has_an_invalid_url(): void
|
||||
{
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'not-a-url',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
$this->artisan('fedi-discover:validate')
|
||||
->assertExitCode(1);
|
||||
}
|
||||
|
||||
public function test_it_exits_nonzero_when_a_row_has_a_zero_interval(): void
|
||||
{
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'https://mastodon.social',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 0,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
$this->artisan('fedi-discover:validate')
|
||||
->assertExitCode(1);
|
||||
}
|
||||
|
||||
public function test_it_reports_summary_of_valid_and_invalid_counts(): void
|
||||
{
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'https://mastodon.social',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'https://hachyderm.io',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'bogus',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
$this->artisan('fedi-discover:validate')
|
||||
->expectsOutputToContain('3')
|
||||
->expectsOutputToContain('2 valid')
|
||||
->expectsOutputToContain('1 invalid')
|
||||
->assertExitCode(1);
|
||||
}
|
||||
|
||||
public function test_it_does_not_fail_fast_and_reports_every_invalid_row(): void
|
||||
{
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'bogus-one',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
$second = Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'https://mastodon.social',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 0,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
$this->artisan('fedi-discover:validate')
|
||||
->expectsOutputToContain('bogus-one')
|
||||
->expectsOutputToContain((string) $second->id)
|
||||
->assertExitCode(1);
|
||||
}
|
||||
|
||||
public function test_it_includes_the_validation_error_message_for_each_invalid_row(): void
|
||||
{
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'not-a-url',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
$this->artisan('fedi-discover:validate')
|
||||
->expectsOutputToContain('Invalid URL: not-a-url')
|
||||
->assertExitCode(1);
|
||||
}
|
||||
|
||||
public function test_summary_counts_are_accurate_when_mixed(): void
|
||||
{
|
||||
// 2 valid
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'https://mastodon.social',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => [],
|
||||
]);
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'https://hachyderm.io',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
// 3 invalid (different defects)
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'bogus-one',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => [],
|
||||
]);
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'https://fosstodon.org',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 0,
|
||||
'extras' => [],
|
||||
]);
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'also-bad',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => -5,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
$this->artisan('fedi-discover:validate')
|
||||
->expectsOutputToContain('5')
|
||||
->expectsOutputToContain('2 valid')
|
||||
->expectsOutputToContain('3 invalid')
|
||||
->assertExitCode(1);
|
||||
}
|
||||
|
||||
public function test_it_exits_zero_with_enabled_only_when_no_enabled_instances_exist(): void
|
||||
{
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'https://mastodon.social',
|
||||
'enabled' => false,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
$this->artisan('fedi-discover:validate', ['--enabled-only' => true])
|
||||
->assertExitCode(0);
|
||||
}
|
||||
|
||||
public function test_it_exits_zero_with_an_enabled_only_flag_when_disabled_rows_are_invalid(): void
|
||||
{
|
||||
// A disabled row that would fail InstanceConfig validation
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'broken-and-disabled',
|
||||
'enabled' => false,
|
||||
'interval_seconds' => 0,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
// A valid enabled row
|
||||
Instance::create([
|
||||
'type' => InstanceType::Mastodon,
|
||||
'url' => 'https://mastodon.social',
|
||||
'enabled' => true,
|
||||
'interval_seconds' => 600,
|
||||
'extras' => [],
|
||||
]);
|
||||
|
||||
$this->artisan('fedi-discover:validate', ['--enabled-only' => true])
|
||||
->assertExitCode(0);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue