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->uuid), [ '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->uuid), [ '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->uuid)); // 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("id}> joined the Dynamic as a EDITOR"); });