From ab29b7e590adee873754ef86cde0a39f14302ecc Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Mon, 13 Apr 2026 01:04:45 +0100 Subject: [PATCH] Fix webhook active check and improve admin plugin edit screen Change webhook endpoint to only reject inactive plugins (is_active=false) instead of unapproved ones, allowing all active plugins to receive webhook data regardless of approval status. Admin plugin edit improvements: - Link composer package name to Packagist for free plugins - Hide repository URL for paid third-party plugins - Link license to license page for paid plugins, GitHub for free - Add 'Go to User' action on submitter field in Submission Info Co-Authored-By: Claude Opus 4.6 --- app/Filament/Resources/PluginResource.php | 36 ++++- .../Controllers/PluginWebhookController.php | 4 +- app/Models/Plugin.php | 5 + database/factories/PluginFactory.php | 7 + tests/Feature/Filament/PluginResourceTest.php | 145 ++++++++++++++++++ tests/Feature/PluginWebhookTest.php | 29 +++- 6 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 tests/Feature/Filament/PluginResourceTest.php diff --git a/app/Filament/Resources/PluginResource.php b/app/Filament/Resources/PluginResource.php index 2d8f2eef..f9b7203b 100644 --- a/app/Filament/Resources/PluginResource.php +++ b/app/Filament/Resources/PluginResource.php @@ -61,7 +61,17 @@ public static function form(Schema $schema): Schema Forms\Components\Placeholder::make('name') ->label('Composer Package Name') - ->content(fn (?Plugin $record) => $record?->name ?? '-'), + ->content(function (?Plugin $record) { + if (! $record?->name) { + return '-'; + } + + if ($record->isFree()) { + return new HtmlString(''.e($record->name).' ↗'); + } + + return $record->name; + }), Forms\Components\Select::make('type') ->options(PluginType::class), @@ -75,20 +85,24 @@ public static function form(Schema $schema): Schema ->label('Repository URL') ->content(fn (?Plugin $record) => $record?->repository_url ? new HtmlString(''.e($record->repository_url).' ↗') - : '-'), + : '-') + ->visible(fn (?Plugin $record) => ! ($record?->isPaid() && ! $record?->isOfficial())), Forms\Components\Placeholder::make('license_type') ->label('License') ->content(function (?Plugin $record) { $license = $record?->getLicense(); - $licenseUrl = $record?->getLicenseUrl(); if (! $license) { return '-'; } - if ($licenseUrl) { - return new HtmlString(''.e($license).' ↗'); + $url = $record->isPaid() + ? route('plugins.license', $record->routeParams()) + : $record->getLicenseUrl(); + + if ($url) { + return new HtmlString(''.e($license).' ↗'); } return $license; @@ -212,7 +226,17 @@ public static function form(Schema $schema): Schema Forms\Components\Select::make('user_id') ->relationship('user', 'email') ->searchable() - ->preload(), + ->preload() + ->suffixAction( + Action::make('viewUser') + ->label('Go to User') + ->icon('heroicon-o-arrow-top-right-on-square') + ->url(fn (?Plugin $record) => $record?->user_id + ? UserResource::getUrl('edit', ['record' => $record->user_id]) + : null) + ->openUrlInNewTab() + ->visible(fn (?Plugin $record) => $record?->user_id !== null), + ), Forms\Components\Placeholder::make('created_at') ->label('Submitted At') diff --git a/app/Http/Controllers/PluginWebhookController.php b/app/Http/Controllers/PluginWebhookController.php index a6360637..a1b57fc2 100644 --- a/app/Http/Controllers/PluginWebhookController.php +++ b/app/Http/Controllers/PluginWebhookController.php @@ -24,8 +24,8 @@ public function __invoke(Request $request, string $secret, PluginSyncService $sy return response()->json(['success' => true, 'message' => 'pong']); } - if (! $plugin->isApproved()) { - return response()->json(['error' => 'Plugin is not approved'], 403); + if (! $plugin->isActive()) { + return response()->json(['error' => 'Plugin is not active'], 403); } if ($event === 'release') { diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 2f18a935..7311195d 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -268,6 +268,11 @@ public function isRejected(): bool return $this->status === PluginStatus::Rejected; } + public function isActive(): bool + { + return $this->is_active ?? true; + } + public function isFree(): bool { return $this->type === PluginType::Free; diff --git a/database/factories/PluginFactory.php b/database/factories/PluginFactory.php index a66dafdd..a8830b6a 100644 --- a/database/factories/PluginFactory.php +++ b/database/factories/PluginFactory.php @@ -176,6 +176,13 @@ public function featured(): static ]); } + public function inactive(): static + { + return $this->state(fn (array $attributes) => [ + 'is_active' => false, + ]); + } + public function free(): static { return $this->state(fn (array $attributes) => [ diff --git a/tests/Feature/Filament/PluginResourceTest.php b/tests/Feature/Filament/PluginResourceTest.php new file mode 100644 index 00000000..739c80de --- /dev/null +++ b/tests/Feature/Filament/PluginResourceTest.php @@ -0,0 +1,145 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + } + + public function test_free_plugin_shows_packagist_link_for_composer_name(): void + { + $plugin = Plugin::factory()->free()->approved()->create([ + 'name' => 'acme/camera-123', + ]); + + $response = $this->actingAs($this->admin)->get( + PluginResource::getUrl('edit', ['record' => $plugin]) + ); + + $response->assertOk(); + $response->assertSee('https://packagist.org/packages/acme/camera-123'); + } + + public function test_paid_plugin_does_not_show_packagist_link(): void + { + $plugin = Plugin::factory()->paid()->approved()->create([ + 'name' => 'acme/paid-plugin-123', + ]); + + $response = $this->actingAs($this->admin)->get( + PluginResource::getUrl('edit', ['record' => $plugin]) + ); + + $response->assertOk(); + $response->assertDontSee('https://packagist.org/packages/acme/paid-plugin-123'); + } + + public function test_paid_third_party_plugin_hides_repository_url_placeholder(): void + { + $plugin = Plugin::factory()->paid()->approved()->create([ + 'name' => 'acme/paid-plugin-456', + 'repository_url' => 'https://github.com/acme/paid-plugin-456', + 'is_official' => false, + ]); + + Livewire::actingAs($this->admin) + ->test(EditPlugin::class, ['record' => $plugin->getRouteKey()]) + ->assertDontSee('Repository URL'); + } + + public function test_paid_official_plugin_shows_repository_url(): void + { + $plugin = Plugin::factory()->paid()->approved()->create([ + 'name' => 'nativephp/paid-official-789', + 'repository_url' => 'https://github.com/nativephp/paid-official-789', + 'is_official' => true, + ]); + + $response = $this->actingAs($this->admin)->get( + PluginResource::getUrl('edit', ['record' => $plugin]) + ); + + $response->assertOk(); + $response->assertSee('https://github.com/nativephp/paid-official-789'); + } + + public function test_free_plugin_shows_repository_url(): void + { + $plugin = Plugin::factory()->free()->approved()->create([ + 'name' => 'acme/free-plugin-321', + 'repository_url' => 'https://github.com/acme/free-plugin-321', + 'is_official' => false, + ]); + + $response = $this->actingAs($this->admin)->get( + PluginResource::getUrl('edit', ['record' => $plugin]) + ); + + $response->assertOk(); + $response->assertSee('https://github.com/acme/free-plugin-321'); + } + + public function test_paid_plugin_license_links_to_license_page(): void + { + $plugin = Plugin::factory()->paid()->approved()->create([ + 'name' => 'acme/paid-license-111', + 'composer_data' => ['license' => 'Commercial'], + ]); + + $response = $this->actingAs($this->admin)->get( + PluginResource::getUrl('edit', ['record' => $plugin]) + ); + + $response->assertOk(); + $response->assertSee(route('plugins.license', ['vendor' => 'acme', 'package' => 'paid-license-111'])); + } + + public function test_free_plugin_license_links_to_github(): void + { + $plugin = Plugin::factory()->free()->approved()->create([ + 'name' => 'acme/free-license-222', + 'repository_url' => 'https://github.com/acme/free-license-222', + 'composer_data' => ['license' => 'MIT'], + ]); + + $response = $this->actingAs($this->admin)->get( + PluginResource::getUrl('edit', ['record' => $plugin]) + ); + + $response->assertOk(); + $response->assertSee('https://github.com/acme/free-license-222/blob/main/LICENSE'); + } + + public function test_submission_info_shows_go_to_user_action(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->for($user)->approved()->create(); + + $response = $this->actingAs($this->admin)->get( + PluginResource::getUrl('edit', ['record' => $plugin]) + ); + + $response->assertOk(); + $expectedUrl = UserResource::getUrl('edit', ['record' => $user->id]); + $response->assertSee($expectedUrl); + } +} diff --git a/tests/Feature/PluginWebhookTest.php b/tests/Feature/PluginWebhookTest.php index ac4776df..e3e12345 100644 --- a/tests/Feature/PluginWebhookTest.php +++ b/tests/Feature/PluginWebhookTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature; use App\Models\Plugin; +use App\Services\PluginSyncService; use Illuminate\Foundation\Testing\RefreshDatabase; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -42,9 +43,9 @@ public function ping_event_succeeds_for_approved_plugin(): void } #[Test] - public function non_ping_event_returns_403_for_unapproved_plugin(): void + public function non_ping_event_returns_403_for_inactive_plugin(): void { - $plugin = Plugin::factory()->create(); + $plugin = Plugin::factory()->inactive()->create(); $response = $this->postJson( route('webhooks.plugins', $plugin->webhook_secret), @@ -53,7 +54,29 @@ public function non_ping_event_returns_403_for_unapproved_plugin(): void ); $response->assertForbidden() - ->assertJson(['error' => 'Plugin is not approved']); + ->assertJson(['error' => 'Plugin is not active']); + } + + #[Test] + public function non_ping_event_succeeds_for_unapproved_but_active_plugin(): void + { + $plugin = Plugin::factory()->create([ + 'is_active' => true, + 'last_synced_at' => now(), + ]); + + $this->mock(PluginSyncService::class, function ($mock) { + $mock->shouldReceive('sync')->once()->andReturn(true); + }); + + $response = $this->postJson( + route('webhooks.plugins', $plugin->webhook_secret), + [], + ['X-GitHub-Event' => 'push'] + ); + + $response->assertOk() + ->assertJson(['success' => true]); } #[Test]