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); + } +}