Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions app/Filament/Resources/PluginResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -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('<a href="'.e($record->getPackagistUrl()).'" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:underline">'.e($record->name).' ↗</a>');
}

return $record->name;
}),

Forms\Components\Select::make('type')
->options(PluginType::class),
Expand All @@ -75,20 +85,24 @@ public static function form(Schema $schema): Schema
->label('Repository URL')
->content(fn (?Plugin $record) => $record?->repository_url
? new HtmlString('<a href="'.e($record->repository_url).'" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:underline">'.e($record->repository_url).' ↗</a>')
: '-'),
: '-')
->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('<a href="'.e($licenseUrl).'" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:underline">'.e($license).' ↗</a>');
$url = $record->isPaid()
? route('plugins.license', $record->routeParams())
: $record->getLicenseUrl();

if ($url) {
return new HtmlString('<a href="'.e($url).'" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:underline">'.e($license).' ↗</a>');
}

return $license;
Expand Down Expand Up @@ -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')
Expand Down
4 changes: 2 additions & 2 deletions app/Http/Controllers/PluginWebhookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
5 changes: 5 additions & 0 deletions app/Models/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions database/factories/PluginFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) => [
Expand Down
145 changes: 145 additions & 0 deletions tests/Feature/Filament/PluginResourceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

namespace Tests\Feature\Filament;

use App\Filament\Resources\PluginResource;
use App\Filament\Resources\PluginResource\Pages\EditPlugin;
use App\Filament\Resources\UserResource;
use App\Models\Plugin;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;

class PluginResourceTest extends TestCase
{
use RefreshDatabase;

private User $admin;

protected function setUp(): void
{
parent::setUp();

$this->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);
}
}
29 changes: 26 additions & 3 deletions tests/Feature/PluginWebhookTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand All @@ -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]
Expand Down
Loading