seed(RoleSeeder::class); $this->org = Organisation::factory()->create(); $this->orgAdmin = User::factory()->create(); $this->org->users()->attach($this->orgAdmin, ['role' => 'org_admin']); } // --- INVITE --- public function test_org_admin_can_invite_user(): void { Mail::fake(); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson("/api/v1/organisations/{$this->org->id}/invite", [ 'email' => 'newuser@test.nl', 'role' => 'org_member', ]); $response->assertCreated(); $this->assertDatabaseHas('user_invitations', [ 'email' => 'newuser@test.nl', 'organisation_id' => $this->org->id, 'status' => 'pending', ]); Mail::assertQueued(\App\Mail\InvitationMail::class); } public function test_org_member_cannot_invite_user(): void { $member = User::factory()->create(); $this->org->users()->attach($member, ['role' => 'org_member']); Sanctum::actingAs($member); $response = $this->postJson("/api/v1/organisations/{$this->org->id}/invite", [ 'email' => 'newuser@test.nl', 'role' => 'org_member', ]); $response->assertForbidden(); } public function test_invite_duplicate_email_returns_422(): void { Mail::fake(); Sanctum::actingAs($this->orgAdmin); UserInvitation::factory()->create([ 'email' => 'existing@test.nl', 'organisation_id' => $this->org->id, 'invited_by_user_id' => $this->orgAdmin->id, 'status' => 'pending', 'expires_at' => now()->addDays(7), ]); $response = $this->postJson("/api/v1/organisations/{$this->org->id}/invite", [ 'email' => 'existing@test.nl', 'role' => 'org_member', ]); $response->assertUnprocessable(); } public function test_invite_existing_member_returns_422(): void { Mail::fake(); $existingUser = User::factory()->create(['email' => 'member@test.nl']); $this->org->users()->attach($existingUser, ['role' => 'org_member']); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson("/api/v1/organisations/{$this->org->id}/invite", [ 'email' => 'member@test.nl', 'role' => 'org_member', ]); $response->assertUnprocessable(); } // --- SHOW --- public function test_show_invitation_by_token(): void { $invitation = UserInvitation::factory()->create([ 'organisation_id' => $this->org->id, 'invited_by_user_id' => $this->orgAdmin->id, ]); $response = $this->getJson("/api/v1/invitations/{$invitation->token}"); $response->assertOk(); $response->assertJsonPath('data.organisation.name', $this->org->name); } public function test_show_expired_invitation_returns_status_expired(): void { $invitation = UserInvitation::factory()->create([ 'organisation_id' => $this->org->id, 'invited_by_user_id' => $this->orgAdmin->id, 'status' => 'pending', 'expires_at' => now()->subDay(), ]); $response = $this->getJson("/api/v1/invitations/{$invitation->token}"); $response->assertOk(); $response->assertJsonPath('data.status', 'expired'); } public function test_show_unknown_token_returns_404(): void { $response = $this->getJson('/api/v1/invitations/nonexistent-token'); $response->assertNotFound(); } // --- ACCEPT --- public function test_accept_with_new_account(): void { $invitation = UserInvitation::factory()->create([ 'email' => 'newuser@test.nl', 'organisation_id' => $this->org->id, 'invited_by_user_id' => $this->orgAdmin->id, 'status' => 'pending', 'expires_at' => now()->addDays(7), ]); $response = $this->postJson("/api/v1/invitations/{$invitation->token}/accept", [ 'name' => 'New User', 'password' => 'password123', 'password_confirmation' => 'password123', ]); $response->assertOk(); $response->assertJsonStructure(['data' => ['user' => ['id', 'name', 'email'], 'token']]); $this->assertDatabaseHas('users', ['email' => 'newuser@test.nl']); $this->assertDatabaseHas('organisation_user', [ 'organisation_id' => $this->org->id, 'role' => $invitation->role, ]); $this->assertDatabaseHas('user_invitations', [ 'id' => $invitation->id, 'status' => 'accepted', ]); } public function test_accept_with_existing_account(): void { $existingUser = User::factory()->create(['email' => 'existing@test.nl']); $invitation = UserInvitation::factory()->create([ 'email' => 'existing@test.nl', 'organisation_id' => $this->org->id, 'invited_by_user_id' => $this->orgAdmin->id, 'status' => 'pending', 'expires_at' => now()->addDays(7), ]); $response = $this->postJson("/api/v1/invitations/{$invitation->token}/accept"); $response->assertOk(); $response->assertJsonStructure(['data' => ['user', 'token']]); $this->assertDatabaseHas('organisation_user', [ 'user_id' => $existingUser->id, 'organisation_id' => $this->org->id, ]); } public function test_accept_expired_invitation_returns_422(): void { $invitation = UserInvitation::factory()->create([ 'organisation_id' => $this->org->id, 'invited_by_user_id' => $this->orgAdmin->id, 'status' => 'pending', 'expires_at' => now()->subDay(), ]); $response = $this->postJson("/api/v1/invitations/{$invitation->token}/accept", [ 'name' => 'Test', 'password' => 'password123', 'password_confirmation' => 'password123', ]); $response->assertUnprocessable(); } public function test_accept_already_accepted_invitation_returns_422(): void { $invitation = UserInvitation::factory()->create([ 'organisation_id' => $this->org->id, 'invited_by_user_id' => $this->orgAdmin->id, 'status' => 'accepted', 'expires_at' => now()->addDays(7), ]); $response = $this->postJson("/api/v1/invitations/{$invitation->token}/accept", [ 'name' => 'Test', 'password' => 'password123', 'password_confirmation' => 'password123', ]); $response->assertUnprocessable(); } // --- REVOKE --- public function test_org_admin_can_revoke_invitation(): void { Sanctum::actingAs($this->orgAdmin); $invitation = UserInvitation::factory()->create([ 'organisation_id' => $this->org->id, 'invited_by_user_id' => $this->orgAdmin->id, 'status' => 'pending', ]); $response = $this->deleteJson( "/api/v1/organisations/{$this->org->id}/invitations/{$invitation->id}" ); $response->assertNoContent(); $this->assertDatabaseHas('user_invitations', [ 'id' => $invitation->id, 'status' => 'expired', ]); } public function test_org_member_cannot_revoke_invitation(): void { $member = User::factory()->create(); $this->org->users()->attach($member, ['role' => 'org_member']); Sanctum::actingAs($member); $invitation = UserInvitation::factory()->create([ 'organisation_id' => $this->org->id, 'invited_by_user_id' => $this->orgAdmin->id, 'status' => 'pending', ]); $response = $this->deleteJson( "/api/v1/organisations/{$this->org->id}/invitations/{$invitation->id}" ); $response->assertForbidden(); } public function test_unauthenticated_cannot_invite(): void { $response = $this->postJson("/api/v1/organisations/{$this->org->id}/invite", [ 'email' => 'test@test.nl', 'role' => 'org_member', ]); $response->assertUnauthorized(); } }