Skip to content

Commit 49d7855

Browse files
simonhampclaude
andcommitted
Move namespace check before plugin creation in submission flow
Pre-validate composer.json and namespace availability by fetching from GitHub before creating any plugin record. Also split repo selector into account picker + searchable dropdown, fix dropdown click-outside behavior, and remove Stripe Connect section from My Plugins index. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c467039 commit 49d7855

2 files changed

Lines changed: 131 additions & 18 deletions

File tree

app/Livewire/Customer/Plugins/Create.php

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,38 @@ function ($attribute, $value, $fail): void {
136136
return;
137137
}
138138

139+
$repository = trim($this->repository, '/');
140+
$repositoryUrl = 'https://github.com/'.$repository;
141+
[$owner, $repo] = explode('/', $repository);
142+
143+
// Check composer.json and namespace availability before creating the plugin
144+
$githubService = GitHubUserService::for($user);
145+
$composerJson = $githubService->getComposerJson($owner, $repo);
146+
147+
if (! $composerJson || empty($composerJson['name'])) {
148+
session()->flash('error', 'Could not find a valid composer.json in the repository. Please ensure your repository contains a composer.json with a valid package name.');
149+
150+
return;
151+
}
152+
153+
$packageName = $composerJson['name'];
154+
$namespace = explode('/', $packageName)[0] ?? null;
155+
156+
if ($namespace && ! Plugin::isNamespaceAvailableForUser($namespace, $user->id)) {
157+
$errorMessage = Plugin::isReservedNamespace($namespace)
158+
? "The namespace '{$namespace}' is reserved and cannot be used for plugin submissions."
159+
: "The namespace '{$namespace}' is already claimed by another user. You cannot submit plugins under this namespace.";
160+
161+
session()->flash('error', $errorMessage);
162+
163+
return;
164+
}
165+
139166
$developerAccountId = null;
140167
if ($this->pluginType === 'paid' && $user->developerAccount) {
141168
$developerAccountId = $user->developerAccount->id;
142169
}
143170

144-
$repository = trim($this->repository, '/');
145-
$repositoryUrl = 'https://github.com/'.$repository;
146-
[$owner, $repo] = explode('/', $repository);
147-
148171
$plugin = $user->plugins()->create([
149172
'repository_url' => $repositoryUrl,
150173
'type' => $this->pluginType,
@@ -156,7 +179,6 @@ function ($attribute, $value, $fail): void {
156179

157180
$webhookInstalled = false;
158181
if ($user->hasGitHubToken()) {
159-
$githubService = GitHubUserService::for($user);
160182
$webhookResult = $githubService->createWebhook(
161183
$owner,
162184
$repo,
@@ -180,19 +202,6 @@ function ($attribute, $value, $fail): void {
180202
return;
181203
}
182204

183-
$namespace = $plugin->getVendorNamespace();
184-
if ($namespace && ! Plugin::isNamespaceAvailableForUser($namespace, $user->id)) {
185-
$plugin->delete();
186-
187-
$errorMessage = Plugin::isReservedNamespace($namespace)
188-
? "The namespace '{$namespace}' is reserved and cannot be used for plugin submissions."
189-
: "The namespace '{$namespace}' is already claimed by another user. You cannot submit plugins under this namespace.";
190-
191-
session()->flash('error', $errorMessage);
192-
193-
return;
194-
}
195-
196205
$user->notify(new PluginSubmitted($plugin));
197206

198207
$successMessage = 'Your plugin has been submitted for review!';

tests/Feature/Livewire/Customer/PluginCreateTest.php

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
use App\Features\ShowAuthButtons;
66
use App\Features\ShowPlugins;
77
use App\Livewire\Customer\Plugins\Create;
8+
use App\Models\Plugin;
89
use App\Models\User;
910
use Illuminate\Foundation\Testing\RefreshDatabase;
11+
use Illuminate\Support\Facades\Http;
1012
use Laravel\Pennant\Feature;
1113
use Livewire\Livewire;
1214
use Tests\TestCase;
@@ -42,6 +44,22 @@ private function sampleRepos(): array
4244
];
4345
}
4446

47+
private function fakeComposerJson(string $owner, string $repo, string $packageName): void
48+
{
49+
$composerJson = base64_encode(json_encode(['name' => $packageName]));
50+
51+
Http::fake([
52+
"api.github.com/repos/{$owner}/{$repo}/contents/composer.json*" => Http::response([
53+
'content' => $composerJson,
54+
]),
55+
'api.github.com/*' => Http::response([], 404),
56+
]);
57+
}
58+
59+
// ========================================
60+
// Owner/Repository Selection Tests
61+
// ========================================
62+
4563
public function test_owners_are_extracted_from_repositories(): void
4664
{
4765
$user = $this->createGitHubUser();
@@ -113,4 +131,90 @@ public function test_no_owner_selected_returns_empty_repositories(): void
113131

114132
$this->assertEmpty($ownerRepos);
115133
}
134+
135+
// ========================================
136+
// Namespace Validation Tests
137+
// ========================================
138+
139+
public function test_submission_blocked_when_namespace_claimed_by_another_user(): void
140+
{
141+
$existingUser = User::factory()->create();
142+
Plugin::factory()->for($existingUser)->create(['name' => 'acme/existing-plugin']);
143+
144+
$user = $this->createGitHubUser();
145+
146+
$this->fakeComposerJson('acme', 'new-plugin', 'acme/new-plugin');
147+
148+
Livewire::actingAs($user)->test(Create::class)
149+
->set('repository', 'acme/new-plugin')
150+
->set('pluginType', 'free')
151+
->call('submitPlugin')
152+
->assertNoRedirect();
153+
154+
$this->assertDatabaseMissing('plugins', [
155+
'repository_url' => 'https://github.com/acme/new-plugin',
156+
]);
157+
}
158+
159+
public function test_submission_blocked_for_reserved_namespace(): void
160+
{
161+
$user = $this->createGitHubUser();
162+
163+
$this->fakeComposerJson('nativephp', 'my-plugin', 'nativephp/my-plugin');
164+
165+
Livewire::actingAs($user)->test(Create::class)
166+
->set('repository', 'nativephp/my-plugin')
167+
->set('pluginType', 'free')
168+
->call('submitPlugin')
169+
->assertNoRedirect();
170+
171+
$this->assertDatabaseMissing('plugins', [
172+
'repository_url' => 'https://github.com/nativephp/my-plugin',
173+
]);
174+
}
175+
176+
public function test_submission_allowed_for_own_namespace(): void
177+
{
178+
$user = $this->createGitHubUser();
179+
Plugin::factory()->for($user)->create(['name' => 'myvendor/first-plugin']);
180+
181+
$composerJson = base64_encode(json_encode(['name' => 'myvendor/second-plugin']));
182+
183+
Http::fake([
184+
'api.github.com/repos/myvendor/second-plugin/contents/composer.json*' => Http::response([
185+
'content' => $composerJson,
186+
]),
187+
'api.github.com/repos/myvendor/second-plugin/hooks' => Http::response(['id' => 1]),
188+
'api.github.com/*' => Http::response([], 404),
189+
]);
190+
191+
Livewire::actingAs($user)->test(Create::class)
192+
->set('repository', 'myvendor/second-plugin')
193+
->set('pluginType', 'free')
194+
->call('submitPlugin');
195+
196+
$this->assertDatabaseHas('plugins', [
197+
'repository_url' => 'https://github.com/myvendor/second-plugin',
198+
'user_id' => $user->id,
199+
]);
200+
}
201+
202+
public function test_submission_blocked_when_composer_json_missing(): void
203+
{
204+
$user = $this->createGitHubUser();
205+
206+
Http::fake([
207+
'api.github.com/*' => Http::response([], 404),
208+
]);
209+
210+
Livewire::actingAs($user)->test(Create::class)
211+
->set('repository', 'testuser/no-composer')
212+
->set('pluginType', 'free')
213+
->call('submitPlugin')
214+
->assertNoRedirect();
215+
216+
$this->assertDatabaseMissing('plugins', [
217+
'repository_url' => 'https://github.com/testuser/no-composer',
218+
]);
219+
}
116220
}

0 commit comments

Comments
 (0)