createUserWithPermission(); $invitation = OrganizationInvitation::factory() ->forOrganization($user->organization) ->create(); // Act $acceptUrl = URL::temporarySignedRoute( 'team-invitations.accept', now()->addMinutes(60), [$invitation->getKey()] ); $response = $this->get($acceptUrl); // Assert $response->assertValid(); $response->assertRedirect(route('register')); $response->assertSessionHas('bannerText', 'Please create an account to finish joining the '.$user->organization->name.' organization.'); $response->assertSessionHas('bannerStyle', 'info'); $invitation->refresh(); $this->assertNotNull($invitation->accepted_at); } public function test_can_accept_invitation_without_an_account_with_the_email_address_and_redirects_to_registration(): void { // Arrange $user = $this->createUserWithPermission(); $invitation = OrganizationInvitation::factory() ->forOrganization($user->organization) ->create(); // Act $acceptUrl = URL::to(URL::temporarySignedRoute( 'organization-invitations.accept', now()->addMinutes(60), [$invitation->getKey()], false )); $response = $this->get($acceptUrl); // Assert $response->assertValid(); $response->assertRedirect(route('register')); $response->assertSessionHas('bannerText', 'Please create an account to finish joining the '.$user->organization->name.' organization.'); $response->assertSessionHas('bannerStyle', 'info'); $invitation->refresh(); $this->assertNotNull($invitation->accepted_at); } public function test_can_accept_invitation_with_an_account_with_the_email_address_and_redirects_to_dashboard(): void { // Arrange $user = $this->createUserWithPermission(); $user2 = $this->createUserWithPermission(); $invitation = OrganizationInvitation::factory() ->forOrganization($user->organization) ->create([ 'role' => Role::Employee->value, 'email' => $user2->user->email, ]); $this->actingAs($user2->user); // Act $acceptUrl = URL::to(URL::temporarySignedRoute( 'organization-invitations.accept', now()->addMinutes(60), [$invitation->getKey()], false )); $response = $this->get($acceptUrl); // Assert $response->assertValid(); $response->assertRedirect(route('dashboard')); $response->assertSessionHas('bannerText', 'Great! You have accepted the invitation to join the '.$user->organization->name.' organization.'); $response->assertSessionHas('bannerStyle', 'success'); $this->assertDatabaseHas(Member::class, [ 'user_id' => $user2->user->getKey(), 'organization_id' => $user->organization->getKey(), 'role' => Role::Employee->value, ]); $this->assertDatabaseMissing(OrganizationInvitation::class, [ 'id' => $invitation->getKey(), ]); } public function test_accepting_invitation_while_logged_out_redirects_to_login(): void { // Arrange $user = $this->createUserWithPermission(); $invitee = User::factory()->create([ 'email' => 'invitee@example.com', ]); $invitation = OrganizationInvitation::factory() ->forOrganization($user->organization) ->create([ 'role' => Role::Employee->value, 'email' => $invitee->email, ]); // Act (no actingAs — request is unauthenticated) $acceptUrl = URL::to(URL::temporarySignedRoute( 'organization-invitations.accept', now()->addMinutes(60), [$invitation->getKey()], false )); $response = $this->get($acceptUrl); // Assert $response->assertValid(); $response->assertRedirect(route('login')); $response->assertSessionHas('bannerText', 'Great! You have accepted the invitation to join the '.$user->organization->name.' organization. Please log in to access it.'); $response->assertSessionHas('bannerStyle', 'success'); // Member was added silently — invitation is consumed. $this->assertDatabaseHas(Member::class, [ 'user_id' => $invitee->getKey(), 'organization_id' => $user->organization->getKey(), 'role' => Role::Employee->value, ]); $this->assertDatabaseMissing(OrganizationInvitation::class, [ 'id' => $invitation->getKey(), ]); } public function test_fails_if_user_is_already_member_of_the_organization(): void { // Arrange $user = $this->createUserWithPermission(); $user2 = $this->createUserWithPermission(); $invitation = OrganizationInvitation::factory() ->forOrganization($user->organization) ->create([ 'role' => Role::Employee->value, 'email' => $user2->user->email, ]); Member::factory()->forOrganization($user->organization)->forUser($user2->user)->create(); $this->actingAs($user2->user); // Act $acceptUrl = URL::to(URL::temporarySignedRoute( 'organization-invitations.accept', now()->addMinutes(60), [$invitation->getKey()], false )); $response = $this->get($acceptUrl); // Assert $response->assertValid(); $response->assertRedirect(route('dashboard')); $response->assertSessionHas('bannerText', 'You are already a member of the '.$user->organization->name.' organization.'); $response->assertSessionHas('bannerStyle', 'danger'); } public function test_accepting_invitation_with_existing_account_migrates_data_of_placeholder_users_with_same_email_to_new_member(): void { // Arrange $user = $this->createUserWithPermission(); $user2 = $this->createUserWithPermission(); $invitation = OrganizationInvitation::factory() ->forOrganization($user->organization) ->create([ 'role' => Role::Employee->value, 'email' => $user2->user->email, ]); $placeholder1 = User::factory()->placeholder()->create([ 'email' => $user2->user->email, ]); $placeholder1Member = Member::factory()->forOrganization($user->organization)->forUser($placeholder1)->role(Role::Placeholder)->create(); $placeholder2 = User::factory()->placeholder()->create([ 'email' => $user2->user->email, ]); $placeholder2Member = Member::factory()->forOrganization($user->organization)->forUser($placeholder2)->role(Role::Placeholder)->create(); $this->actingAs($user2->user); // Act $acceptUrl = URL::to(URL::temporarySignedRoute( 'organization-invitations.accept', now()->addMinutes(60), [$invitation->getKey()], false )); $response = $this->get($acceptUrl); // Assert $response->assertValid(); $response->assertRedirect(route('dashboard')); $response->assertSessionHas('bannerText', 'Great! You have accepted the invitation to join the '.$user->organization->name.' organization.'); $response->assertSessionHas('bannerStyle', 'success'); $this->assertDatabaseHas(Member::class, [ 'user_id' => $user2->user->getKey(), 'organization_id' => $user->organization->getKey(), 'role' => Role::Employee->value, ]); $this->assertDatabaseMissing(User::class, [ 'id' => $placeholder1->getKey(), ]); $this->assertDatabaseMissing(User::class, [ 'id' => $placeholder2->getKey(), ]); $this->assertDatabaseMissing(Member::class, [ 'id' => $placeholder1Member->getKey(), ]); $this->assertDatabaseMissing(Member::class, [ 'id' => $placeholder2Member->getKey(), ]); $this->assertDatabaseMissing(OrganizationInvitation::class, [ 'id' => $invitation->getKey(), ]); } public function test_fails_with_invalid_signature(): void { // Arrange $user = $this->createUserWithPermission(); $invitation = OrganizationInvitation::factory() ->forOrganization($user->organization) ->create(); // Act $response = $this->get(URL::temporarySignedRoute( 'organization-invitations.accept', now()->addMinutes(60), [$invitation->getKey()]). '?invalid' ); // Assert $response->assertForbidden(); } public function test_fails_if_invitation_has_owner_role(): void { // Arrange $user = $this->createUserWithPermission(); $invitation = OrganizationInvitation::factory() ->forOrganization($user->organization) ->create([ 'role' => Role::Owner->value, ]); // Act $acceptUrl = URL::to(URL::temporarySignedRoute( 'organization-invitations.accept', now()->addMinutes(60), [$invitation->getKey()], false )); $response = $this->get($acceptUrl); // Assert $response->assertStatus(500); } }