104 lines
3.7 KiB
PHP
104 lines
3.7 KiB
PHP
<?php
|
|
|
|
use App\Models\User;
|
|
use App\Models\Dynamic;
|
|
use App\Models\DynamicInvitation;
|
|
use App\Mail\DynamicInvitationMail;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\URL;
|
|
|
|
test('only owners can invite other users to a dynamic', function () {
|
|
Mail::fake();
|
|
|
|
$owner = User::factory()->create();
|
|
$participant = User::factory()->create();
|
|
$dynamic = Dynamic::factory()->create();
|
|
|
|
$dynamic->participants()->attach($owner->id, ['role' => 'owner']);
|
|
$dynamic->participants()->attach($participant->id, ['role' => 'participant']);
|
|
|
|
// 1. Participant tries to send an invite (forbidden)
|
|
$response = $this->actingAs($participant)
|
|
->post(route('dynamics.invitations.store', $dynamic), [
|
|
'email' => 'invitee@example.com',
|
|
'role' => 'participant',
|
|
]);
|
|
|
|
$response->assertStatus(403);
|
|
Mail::assertNothingSent();
|
|
|
|
// 2. Owner sends a valid invite (allowed)
|
|
$response = $this->actingAs($owner)
|
|
->post(route('dynamics.invitations.store', $dynamic), [
|
|
'email' => 'invitee@example.com',
|
|
'role' => 'participant',
|
|
]);
|
|
|
|
$response->assertRedirect();
|
|
$response->assertSessionHasNoErrors();
|
|
|
|
// Verify invitation is stored
|
|
$invitation = DynamicInvitation::firstWhere('email', 'invitee@example.com');
|
|
expect($invitation)->not->toBeNull();
|
|
expect($invitation->role)->toBe('participant');
|
|
expect($invitation->dynamic_id)->toBe($dynamic->id);
|
|
|
|
// Verify email was dispatched
|
|
Mail::assertSent(DynamicInvitationMail::class, function ($mail) use ($invitation) {
|
|
return $mail->hasTo($invitation->email);
|
|
});
|
|
});
|
|
|
|
test('only the user with the specified email address can accept the link', function () {
|
|
$owner = User::factory()->create();
|
|
$invitee = User::factory()->create(['email' => 'matching@example.com']);
|
|
$hijacker = User::factory()->create(['email' => 'hijacker@example.com']);
|
|
$dynamic = Dynamic::factory()->create();
|
|
|
|
$dynamic->participants()->attach($owner->id, ['role' => 'owner']);
|
|
|
|
// Create a pending invitation
|
|
$invitation = $dynamic->invitations()->create([
|
|
'email' => 'matching@example.com',
|
|
'role' => 'editor',
|
|
'token' => 'secure_test_token_123',
|
|
'expires_at' => now()->addDays(7),
|
|
]);
|
|
|
|
// Generate a secure, valid signed URL
|
|
$signedUrl = URL::temporarySignedRoute(
|
|
'dynamics.invitations.accept',
|
|
$invitation->expires_at,
|
|
['token' => $invitation->token]
|
|
);
|
|
|
|
// 1. Unauthenticated user tries to accept (redirected to login)
|
|
$response = $this->get($signedUrl);
|
|
$response->assertRedirect('/login');
|
|
|
|
// 2. Different user (hijacker) tries to accept (forbidden / 403)
|
|
$response = $this->actingAs($hijacker)->get($signedUrl);
|
|
$response->assertStatus(403);
|
|
expect($dynamic->participants()->where('user_id', $hijacker->id)->exists())->toBeFalse();
|
|
|
|
// 3. Intended user accepts the signed invitation link (success)
|
|
$response = $this->actingAs($invitee)->get($signedUrl);
|
|
$response->assertRedirect(route('dynamics.show', $dynamic));
|
|
|
|
// Verify invitee is joined as a participant with the specified role
|
|
$isJoined = $dynamic->participants()
|
|
->where('user_id', $invitee->id)
|
|
->wherePivot('role', 'editor')
|
|
->exists();
|
|
|
|
expect($isJoined)->toBeTrue();
|
|
|
|
// Verify invitation is deleted from the database
|
|
expect(DynamicInvitation::where('token', 'secure_test_token_123')->exists())->toBeFalse();
|
|
|
|
// Verify system notification is added to Dynamic activity chat
|
|
$chatMessages = $dynamic->chat->messages;
|
|
expect($chatMessages)->not->toBeEmpty();
|
|
expect($chatMessages->last()->content)->toBe("{$invitee->name} joined the Dynamic as a EDITOR");
|
|
});
|