Skip to content

Commit f7b6b03

Browse files
simonhampclaude
andcommitted
Add team seat validation, owner self-invite prevention, and fix seat counting to include owner
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 72a0d4e commit f7b6b03

File tree

4 files changed

+109
-4
lines changed

4 files changed

+109
-4
lines changed

app/Http/Controllers/TeamUserController.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ public function invite(InviteTeamUserRequest $request): RedirectResponse
4242

4343
$email = $request->validated()['email'];
4444

45+
// Prevent owner from inviting themselves
46+
if (strtolower($email) === strtolower($user->email)) {
47+
return back()->with('error', 'You cannot invite yourself to your own team.');
48+
}
49+
4550
// Check for duplicate (active or pending)
4651
$existingMember = $team->users()
4752
->where('email', $email)

app/Livewire/TeamManager.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ public function mount(Team $team): void
2020

2121
public function addSeats(int $count = 1): void
2222
{
23+
if ($count < 1 || $count > 50) {
24+
return;
25+
}
26+
2327
$owner = $this->team->owner;
2428
$subscription = $owner->subscription();
2529

@@ -64,6 +68,10 @@ public function addSeats(int $count = 1): void
6468

6569
public function removeSeats(int $count = 1): void
6670
{
71+
if ($count < 1 || $count > 50) {
72+
return;
73+
}
74+
6775
if ($this->team->extra_seats < $count) {
6876
return;
6977
}

app/Models/Team.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public function totalSeatCapacity(): int
7070

7171
public function occupiedSeatCount(): int
7272
{
73-
return $this->activeUserCount() + $this->pendingInvitations()->count();
73+
return 1 + $this->activeUserCount() + $this->pendingInvitations()->count();
7474
}
7575

7676
public function availableSeats(): int
@@ -85,7 +85,7 @@ public function isOverIncludedLimit(): bool
8585

8686
public function extraSeatsCount(): int
8787
{
88-
return max(0, $this->activeUserCount() - $this->includedSeats());
88+
return max(0, 1 + $this->activeUserCount() - $this->includedSeats());
8989
}
9090

9191
public function suspend(): bool

tests/Feature/TeamManagementTest.php

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use App\Jobs\RevokeTeamUserAccessJob;
99
use App\Jobs\SuspendTeamJob;
1010
use App\Jobs\UnsuspendTeamJob;
11+
use App\Livewire\TeamManager;
1112
use App\Models\License;
1213
use App\Models\Plugin;
1314
use App\Models\PluginLicense;
@@ -25,6 +26,7 @@
2526
use Laravel\Cashier\Events\WebhookReceived;
2627
use Laravel\Cashier\Subscription;
2728
use Laravel\Pennant\Feature;
29+
use Livewire\Livewire;
2830
use Tests\TestCase;
2931

3032
class TeamManagementTest extends TestCase
@@ -202,14 +204,31 @@ public function test_cannot_invite_when_team_suspended(): void
202204
Notification::assertNothingSent();
203205
}
204206

207+
public function test_owner_cannot_invite_themselves(): void
208+
{
209+
Notification::fake();
210+
211+
[$owner, $team] = $this->createTeamWithOwner();
212+
213+
$response = $this->actingAs($owner)
214+
->post(route('customer.team.invite'), ['email' => $owner->email]);
215+
216+
$response->assertSessionHas('error', 'You cannot invite yourself to your own team.');
217+
Notification::assertNothingSent();
218+
$this->assertDatabaseMissing('team_users', [
219+
'team_id' => $team->id,
220+
'email' => $owner->email,
221+
]);
222+
}
223+
205224
public function test_cannot_invite_beyond_seat_limit(): void
206225
{
207226
Notification::fake();
208227

209228
[$owner, $team] = $this->createTeamWithOwner();
210229

211-
// Create 10 active members to fill all seats
212-
TeamUser::factory()->count(10)->active()->create(['team_id' => $team->id]);
230+
// Create 9 active members to fill all seats (owner occupies 1 of 10 included seats)
231+
TeamUser::factory()->count(9)->active()->create(['team_id' => $team->id]);
213232

214233
$response = $this->actingAs($owner)
215234
->post(route('customer.team.invite'), ['email' => 'extra@example.com']);
@@ -873,4 +892,77 @@ public function test_team_detail_page_shows_owner_purchased_plugins(): void
873892
$response->assertSee('acme/shared-plugin');
874893
$response->assertSee('Accessible Plugins');
875894
}
895+
896+
// ========================================
897+
// Seat Validation Tests
898+
// ========================================
899+
900+
public function test_cannot_add_zero_seats(): void
901+
{
902+
[$owner, $team] = $this->createTeamWithOwner();
903+
904+
Livewire::actingAs($owner)
905+
->test(TeamManager::class, ['team' => $team])
906+
->call('addSeats', 0);
907+
908+
$this->assertEquals(0, $team->fresh()->extra_seats);
909+
}
910+
911+
public function test_cannot_add_negative_seats(): void
912+
{
913+
[$owner, $team] = $this->createTeamWithOwner();
914+
915+
Livewire::actingAs($owner)
916+
->test(TeamManager::class, ['team' => $team])
917+
->call('addSeats', -1);
918+
919+
$this->assertEquals(0, $team->fresh()->extra_seats);
920+
}
921+
922+
public function test_cannot_add_more_than_fifty_seats(): void
923+
{
924+
[$owner, $team] = $this->createTeamWithOwner();
925+
926+
Livewire::actingAs($owner)
927+
->test(TeamManager::class, ['team' => $team])
928+
->call('addSeats', 51);
929+
930+
$this->assertEquals(0, $team->fresh()->extra_seats);
931+
}
932+
933+
public function test_cannot_remove_zero_seats(): void
934+
{
935+
[$owner, $team] = $this->createTeamWithOwner();
936+
$team->update(['extra_seats' => 5]);
937+
938+
Livewire::actingAs($owner)
939+
->test(TeamManager::class, ['team' => $team])
940+
->call('removeSeats', 0);
941+
942+
$this->assertEquals(5, $team->fresh()->extra_seats);
943+
}
944+
945+
public function test_cannot_remove_negative_seats(): void
946+
{
947+
[$owner, $team] = $this->createTeamWithOwner();
948+
$team->update(['extra_seats' => 5]);
949+
950+
Livewire::actingAs($owner)
951+
->test(TeamManager::class, ['team' => $team])
952+
->call('removeSeats', -1);
953+
954+
$this->assertEquals(5, $team->fresh()->extra_seats);
955+
}
956+
957+
public function test_cannot_remove_more_than_fifty_seats(): void
958+
{
959+
[$owner, $team] = $this->createTeamWithOwner();
960+
$team->update(['extra_seats' => 60]);
961+
962+
Livewire::actingAs($owner)
963+
->test(TeamManager::class, ['team' => $team])
964+
->call('removeSeats', 51);
965+
966+
$this->assertEquals(60, $team->fresh()->extra_seats);
967+
}
876968
}

0 commit comments

Comments
 (0)