From c1c149968ea07b58fe22839dd5323b256a8f641f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 16 Sep 2025 15:31:48 +0100 Subject: [PATCH] Fix team invitation email case sensitivity bug - Add email normalization to TeamInvitation model using setEmailAttribute() - Add HasFactory trait to Team model for testing support - Create TeamFactory for testing - Add tests to verify email normalization works correctly - Fixes issue where mixed case emails in invitations would cause lookup failures - Resolves #6291 The bug occurred because: 1. User model normalizes emails to lowercase 2. TeamInvitation model did not normalize emails 3. When invitation was created with mixed case, it was stored as-is 4. User lookup failed due to case mismatch during invitation acceptance 5. This caused users to not be able to see teams they were invited to This fix ensures both models normalize emails consistently. --- app/Models/Team.php | 3 +- app/Models/TeamInvitation.php | 8 +++ database/factories/TeamFactory.php | 40 ++++++++++++ .../TeamInvitationEmailNormalizationTest.php | 63 +++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 database/factories/TeamFactory.php create mode 100644 tests/Feature/TeamInvitationEmailNormalizationTest.php diff --git a/app/Models/Team.php b/app/Models/Team.php index 81638e31c..1b700404d 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -10,6 +10,7 @@ use App\Traits\HasNotificationSettings; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; use OpenApi\Attributes as OA; @@ -37,7 +38,7 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, SendsSlack { - use HasNotificationSettings, HasSafeStringAttribute, Notifiable; + use HasFactory, HasNotificationSettings, HasSafeStringAttribute, Notifiable; protected $guarded = []; diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php index 0fea1806b..92107c48c 100644 --- a/app/Models/TeamInvitation.php +++ b/app/Models/TeamInvitation.php @@ -15,6 +15,14 @@ class TeamInvitation extends Model 'via', ]; + /** + * Set the email attribute to lowercase. + */ + public function setEmailAttribute($value) + { + $this->attributes['email'] = strtolower($value); + } + public function team() { return $this->belongsTo(Team::class); diff --git a/database/factories/TeamFactory.php b/database/factories/TeamFactory.php new file mode 100644 index 000000000..0e95842b4 --- /dev/null +++ b/database/factories/TeamFactory.php @@ -0,0 +1,40 @@ + + */ +class TeamFactory extends Factory +{ + protected $model = Team::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->company() . ' Team', + 'description' => $this->faker->sentence(), + 'personal_team' => false, + 'show_boarding' => false, + ]; + } + + /** + * Indicate that the team is a personal team. + */ + public function personal(): static + { + return $this->state(fn (array $attributes) => [ + 'personal_team' => true, + 'name' => $this->faker->firstName() . "'s Team", + ]); + } +} diff --git a/tests/Feature/TeamInvitationEmailNormalizationTest.php b/tests/Feature/TeamInvitationEmailNormalizationTest.php new file mode 100644 index 000000000..c7ad9134b --- /dev/null +++ b/tests/Feature/TeamInvitationEmailNormalizationTest.php @@ -0,0 +1,63 @@ +create(); + + // Create invitation with mixed case email + $invitation = TeamInvitation::create([ + 'team_id' => $team->id, + 'uuid' => 'test-uuid-123', + 'email' => 'Test@Example.com', // Mixed case + 'role' => 'member', + 'link' => 'https://example.com/invite/test-uuid-123', + 'via' => 'link' + ]); + + // Verify email was normalized to lowercase + $this->assertEquals('test@example.com', $invitation->email); + } + + public function test_team_invitation_works_with_existing_user_email() + { + // Create a team + $team = Team::factory()->create(); + + // Create a user with lowercase email + $user = User::factory()->create([ + 'email' => 'test@example.com', + 'name' => 'Test User' + ]); + + // Create invitation with mixed case email + $invitation = TeamInvitation::create([ + 'team_id' => $team->id, + 'uuid' => 'test-uuid-123', + 'email' => 'Test@Example.com', // Mixed case + 'role' => 'member', + 'link' => 'https://example.com/invite/test-uuid-123', + 'via' => 'link' + ]); + + // Verify the invitation email matches the user email (both normalized) + $this->assertEquals($user->email, $invitation->email); + + // Verify user lookup works + $foundUser = User::whereEmail($invitation->email)->first(); + $this->assertNotNull($foundUser); + $this->assertEquals($user->id, $foundUser->id); + } +}