coolify/tests/Feature/TeamInvitationCsrfProtectionTest.php
Andras Bacsai 25d424c743 refactor: split invitation endpoint into GET (show) and POST (accept)
Refactor the invitation acceptance flow to use a landing page pattern:
- GET shows invitation details (team name, role, confirmation button)
- POST processes the acceptance with proper form submission
- Remove unused revoke GET route (handled by Livewire component)
- Add Blade view for the invitation landing page
- Add feature tests for the new invitation flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 14:30:27 +01:00

147 lines
4.5 KiB
PHP

<?php
use App\Models\Team;
use App\Models\TeamInvitation;
use App\Models\User;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->team = Team::factory()->create();
$this->user = User::factory()->create(['email' => 'invited@example.com']);
$this->invitation = TeamInvitation::create([
'team_id' => $this->team->id,
'uuid' => 'test-invitation-uuid',
'email' => 'invited@example.com',
'role' => 'member',
'link' => url('/invitations/test-invitation-uuid'),
'via' => 'link',
]);
});
test('GET invitation shows landing page without accepting', function () {
$this->actingAs($this->user);
$response = $this->get('/invitations/test-invitation-uuid');
$response->assertStatus(200);
$response->assertViewIs('invitation.accept');
$response->assertSee($this->team->name);
$response->assertSee('Accept Invitation');
// Invitation should NOT be deleted (not accepted yet)
$this->assertDatabaseHas('team_invitations', [
'uuid' => 'test-invitation-uuid',
]);
// User should NOT be added to the team
expect($this->user->teams()->where('team_id', $this->team->id)->exists())->toBeFalse();
});
test('GET invitation with reset-password query param does not reset password', function () {
$this->actingAs($this->user);
$originalPassword = $this->user->password;
$response = $this->get('/invitations/test-invitation-uuid?reset-password=1');
$response->assertStatus(200);
// Password should NOT be changed
$this->user->refresh();
expect($this->user->password)->toBe($originalPassword);
// Invitation should NOT be accepted
$this->assertDatabaseHas('team_invitations', [
'uuid' => 'test-invitation-uuid',
]);
});
test('POST invitation accepts and adds user to team', function () {
$this->actingAs($this->user);
$response = $this->post('/invitations/test-invitation-uuid');
$response->assertRedirect(route('team.index'));
// Invitation should be deleted
$this->assertDatabaseMissing('team_invitations', [
'uuid' => 'test-invitation-uuid',
]);
// User should be added to the team
expect($this->user->teams()->where('team_id', $this->team->id)->exists())->toBeTrue();
});
test('POST invitation without CSRF token is rejected', function () {
$this->actingAs($this->user);
$response = $this->withoutMiddleware(EncryptCookies::class)
->post('/invitations/test-invitation-uuid', [], [
'X-CSRF-TOKEN' => 'invalid-token',
]);
// Should be rejected with 419 (CSRF token mismatch)
$response->assertStatus(419);
// Invitation should NOT be accepted
$this->assertDatabaseHas('team_invitations', [
'uuid' => 'test-invitation-uuid',
]);
});
test('unauthenticated user cannot view invitation', function () {
$response = $this->get('/invitations/test-invitation-uuid');
$response->assertRedirect();
});
test('wrong user cannot view invitation', function () {
$otherUser = User::factory()->create(['email' => 'other@example.com']);
$this->actingAs($otherUser);
$response = $this->get('/invitations/test-invitation-uuid');
$response->assertStatus(400);
});
test('wrong user cannot accept invitation via POST', function () {
$otherUser = User::factory()->create(['email' => 'other@example.com']);
$this->actingAs($otherUser);
$response = $this->post('/invitations/test-invitation-uuid');
$response->assertStatus(400);
// Invitation should still exist
$this->assertDatabaseHas('team_invitations', [
'uuid' => 'test-invitation-uuid',
]);
});
test('GET revoke route no longer exists', function () {
$this->actingAs($this->user);
$response = $this->get('/invitations/test-invitation-uuid/revoke');
$response->assertStatus(404);
});
test('POST invitation for already-member user deletes invitation without duplicating', function () {
$this->user->teams()->attach($this->team->id, ['role' => 'member']);
$this->actingAs($this->user);
$response = $this->post('/invitations/test-invitation-uuid');
$response->assertRedirect(route('team.index'));
// Invitation should be deleted
$this->assertDatabaseMissing('team_invitations', [
'uuid' => 'test-invitation-uuid',
]);
// User should still have exactly one membership in this team
expect($this->user->teams()->where('team_id', $this->team->id)->count())->toBe(1);
});