diff --git a/composer.json b/composer.json index b904de68..357a47b7 100644 --- a/composer.json +++ b/composer.json @@ -132,7 +132,8 @@ "pestphp/pest-plugin": true, "php-http/discovery": true, "wikimedia/composer-merge-plugin": true - } + }, + "process-timeout": 900 }, "minimum-stability": "stable", "prefer-stable": true diff --git a/tests/Feature/ProfileInformationTest.php b/tests/Feature/ProfileInformationTest.php index f63ab911..a1970f7e 100644 --- a/tests/Feature/ProfileInformationTest.php +++ b/tests/Feature/ProfileInformationTest.php @@ -5,12 +5,9 @@ declare(strict_types=1); namespace Tests\Feature; use App\Enums\Weekday; -use App\Mail\VerifyUpdatedEmailMail; use App\Models\User; use App\Service\TimezoneService; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Facades\Mail; -use Illuminate\Support\Facades\URL; use Tests\TestCase; class ProfileInformationTest extends TestCase @@ -39,66 +36,4 @@ class ProfileInformationTest extends TestCase $user = $user->fresh(); $this->assertEquals($user->name, $user->name); } - - public function test_pending_email_verification_redirects_with_danger_banner_when_email_already_in_use(): void - { - // Arrange - User::factory()->create([ - 'email' => 'taken@example.com', - 'is_placeholder' => false, - ]); - $user = User::factory()->create([ - 'email' => 'current@example.com', - 'pending_email' => 'taken@example.com', - ]); - $this->actingAs($user); - $verificationUrl = URL::temporarySignedRoute( - 'users.verify-email-change', - now()->addMinutes(60), - [ - 'user' => $user->getKey(), - 'email' => 'taken@example.com', - ], - false - ); - - // Act - $response = $this->get($verificationUrl); - - // Assert - $response->assertRedirect(route('dashboard')); - $response->assertSessionHas('bannerStyle', 'danger'); - $response->assertSessionHas('bannerText', 'The email address is already in use.'); - $user = $user->fresh(); - $this->assertEquals('current@example.com', $user->email); - $this->assertEquals('taken@example.com', $user->pending_email); - } - - public function test_stale_pending_email_verification_link_is_rejected(): void - { - // Arrange - $user = User::factory()->create([ - 'email' => 'current@example.com', - 'pending_email' => 'newer@example.com', - ]); - $this->actingAs($user); - $verificationUrl = URL::temporarySignedRoute( - 'users.verify-email-change', - now()->addMinutes(60), - [ - 'user' => $user->getKey(), - 'email' => 'older@example.com', - ], - false - ); - - // Act - $response = $this->get($verificationUrl); - - // Assert - $response->assertForbidden(); - $user = $user->fresh(); - $this->assertEquals('current@example.com', $user->email); - $this->assertEquals('newer@example.com', $user->pending_email); - } } diff --git a/tests/Unit/Endpoint/Web/UserEndpointTest.php b/tests/Unit/Endpoint/Web/UserEndpointTest.php new file mode 100644 index 00000000..8c833876 --- /dev/null +++ b/tests/Unit/Endpoint/Web/UserEndpointTest.php @@ -0,0 +1,253 @@ +travelTo(Carbon::parse('2024-01-02 12:00:00', 'UTC')); + $user = User::factory()->create([ + 'email' => 'current@example.com', + 'pending_email' => 'new@example.com', + 'email_verified_at' => null, + ]); + $this->actingAs($user); + $verificationUrl = URL::temporarySignedRoute( + 'users.verify-email-change', + now()->addMinutes(60), + [ + 'user' => $user->getKey(), + 'email' => 'NEW@EXAMPLE.COM', + ], + false + ); + + // Act + $response = $this->get($verificationUrl); + + // Assert + $response->assertRedirect(route('dashboard')); + $response->assertSessionHas('bannerStyle', 'success'); + $response->assertSessionHas('bannerText', 'Your email address has been updated successfully.'); + $user->refresh(); + $this->assertSame('new@example.com', $user->email); + $this->assertNull($user->pending_email); + $this->assertTrue(now()->equalTo($user->email_verified_at)); + } + + public function test_pending_email_verification_is_rejected_for_another_authenticated_user(): void + { + // Arrange + $user = User::factory()->create([ + 'email' => 'current@example.com', + 'pending_email' => 'new@example.com', + ]); + $this->actingAs(User::factory()->create()); + $verificationUrl = URL::temporarySignedRoute( + 'users.verify-email-change', + now()->addMinutes(60), + [ + 'user' => $user->getKey(), + 'email' => 'new@example.com', + ], + false + ); + + // Act + $response = $this->get($verificationUrl); + + // Assert + $response->assertForbidden(); + $user->refresh(); + $this->assertSame('current@example.com', $user->email); + $this->assertSame('new@example.com', $user->pending_email); + } + + public function test_pending_email_verification_without_email_is_rejected(): void + { + // Arrange + $user = User::factory()->create([ + 'email' => 'current@example.com', + 'pending_email' => 'new@example.com', + ]); + $this->actingAs($user); + $verificationUrl = URL::temporarySignedRoute( + 'users.verify-email-change', + now()->addMinutes(60), + ['user' => $user->getKey()], + false + ); + + // Act + $response = $this->get($verificationUrl); + + // Assert + $response->assertForbidden(); + $user->refresh(); + $this->assertSame('current@example.com', $user->email); + $this->assertSame('new@example.com', $user->pending_email); + } + + public function test_pending_email_verification_with_non_string_email_is_rejected(): void + { + // Arrange + $user = User::factory()->create([ + 'email' => 'current@example.com', + 'pending_email' => 'new@example.com', + ]); + $this->actingAs($user); + $verificationUrl = URL::temporarySignedRoute( + 'users.verify-email-change', + now()->addMinutes(60), + [ + 'user' => $user->getKey(), + 'email' => ['new@example.com'], + ], + false + ); + + // Act + $response = $this->get($verificationUrl); + + // Assert + $response->assertForbidden(); + $user->refresh(); + $this->assertSame('current@example.com', $user->email); + $this->assertSame('new@example.com', $user->pending_email); + } + + public function test_stale_pending_email_verification_link_is_rejected(): void + { + // Arrange + $user = User::factory()->create([ + 'email' => 'current@example.com', + 'pending_email' => 'newer@example.com', + ]); + $this->actingAs($user); + $verificationUrl = URL::temporarySignedRoute( + 'users.verify-email-change', + now()->addMinutes(60), + [ + 'user' => $user->getKey(), + 'email' => 'older@example.com', + ], + false + ); + + // Act + $response = $this->get($verificationUrl); + + // Assert + $response->assertForbidden(); + $user->refresh(); + $this->assertSame('current@example.com', $user->email); + $this->assertSame('newer@example.com', $user->pending_email); + } + + public function test_pending_email_verification_redirects_with_danger_banner_when_email_already_in_use(): void + { + // Arrange + User::factory()->create([ + 'email' => 'taken@example.com', + 'is_placeholder' => false, + ]); + $user = User::factory()->create([ + 'email' => 'current@example.com', + 'pending_email' => 'taken@example.com', + ]); + $this->actingAs($user); + $verificationUrl = URL::temporarySignedRoute( + 'users.verify-email-change', + now()->addMinutes(60), + [ + 'user' => $user->getKey(), + 'email' => 'taken@example.com', + ], + false + ); + + // Act + $response = $this->get($verificationUrl); + + // Assert + $response->assertRedirect(route('dashboard')); + $response->assertSessionHas('bannerStyle', 'danger'); + $response->assertSessionHas('bannerText', 'The email address is already in use.'); + $user->refresh(); + $this->assertSame('current@example.com', $user->email); + $this->assertSame('taken@example.com', $user->pending_email); + } + + public function test_pending_email_verification_ignores_placeholder_users_with_the_same_email(): void + { + // Arrange + User::factory()->placeholder()->create([ + 'email' => 'new@example.com', + ]); + $user = User::factory()->create([ + 'email' => 'current@example.com', + 'pending_email' => 'new@example.com', + 'email_verified_at' => null, + ]); + $this->actingAs($user); + $verificationUrl = URL::temporarySignedRoute( + 'users.verify-email-change', + now()->addMinutes(60), + [ + 'user' => $user->getKey(), + 'email' => 'new@example.com', + ], + false + ); + + // Act + $response = $this->get($verificationUrl); + + // Assert + $response->assertRedirect(route('dashboard')); + $response->assertSessionHas('bannerStyle', 'success'); + $user->refresh(); + $this->assertSame('new@example.com', $user->email); + $this->assertNull($user->pending_email); + $this->assertNotNull($user->email_verified_at); + } + + public function test_pending_email_verification_with_invalid_signature_is_rejected(): void + { + // Arrange + $user = User::factory()->create([ + 'email' => 'current@example.com', + 'pending_email' => 'new@example.com', + ]); + $this->actingAs($user); + $verificationUrl = URL::temporarySignedRoute( + 'users.verify-email-change', + now()->addMinutes(60), + [ + 'user' => $user->getKey(), + 'email' => 'new@example.com', + ], + false + ); + + // Act + $response = $this->get($verificationUrl.'&invalid'); + + // Assert + $response->assertForbidden(); + $user->refresh(); + $this->assertSame('current@example.com', $user->email); + $this->assertSame('new@example.com', $user->pending_email); + } +} diff --git a/tests/Unit/Service/Dto/UserAgentDtoTest.php b/tests/Unit/Service/Dto/UserAgentDtoTest.php new file mode 100644 index 00000000..4dc83c75 --- /dev/null +++ b/tests/Unit/Service/Dto/UserAgentDtoTest.php @@ -0,0 +1,131 @@ +setUserAgent($userAgent); + + // Act + $platform = $agent->platform(); + $browser = $agent->browser(); + $isDesktop = $agent->isDesktop(); + + // Assert + $this->assertSame('Windows', $platform); + $this->assertSame('Chrome', $browser); + $this->assertTrue($isDesktop); + } + + public function test_edge_is_detected_before_chrome(): void + { + // Arrange + $userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0'; + $agent = new UserAgentDto; + $agent->setUserAgent($userAgent); + + // Act + $browser = $agent->browser(); + + // Assert + $this->assertSame('Edge', $browser); + } + + public function test_iphone_safari_is_detected_as_a_non_desktop_browser(): void + { + // Arrange + $userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'; + $agent = new UserAgentDto; + $agent->setUserAgent($userAgent); + + // Act + $platform = $agent->platform(); + $browser = $agent->browser(); + $isDesktop = $agent->isDesktop(); + + // Assert + $this->assertSame('iOS', $platform); + $this->assertSame('Safari', $browser); + $this->assertFalse($isDesktop); + } + + public function test_ipad_is_detected_as_non_desktop(): void + { + // Arrange + $userAgent = 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'; + $agent = new UserAgentDto; + $agent->setUserAgent($userAgent); + + // Act + $isDesktop = $agent->isDesktop(); + + // Assert + $this->assertFalse($isDesktop); + } + + public function test_unknown_user_agent_has_no_platform_or_browser_and_is_a_desktop(): void + { + // Arrange + $agent = new UserAgentDto; + $agent->setUserAgent('CustomClient/1.0'); + + // Act + $platform = $agent->platform(); + $browser = $agent->browser(); + $isDesktop = $agent->isDesktop(); + + // Assert + $this->assertNull($platform); + $this->assertNull($browser); + $this->assertTrue($isDesktop); + } + + public function test_cloudfront_desktop_header_is_detected_as_desktop(): void + { + // Arrange + $agent = new UserAgentDto; + $agent->setUserAgent('Amazon CloudFront'); + $agent->setHttpHeaders([ + 'HTTP_CLOUDFRONT_IS_DESKTOP_VIEWER' => 'true', + ]); + + // Act + $isDesktop = $agent->isDesktop(); + + // Assert + $this->assertTrue($isDesktop); + } + + public function test_cached_values_are_resolved_for_the_current_user_agent(): void + { + // Arrange + $agent = new UserAgentDto; + $agent->setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36'); + $agent->platform(); + $agent->browser(); + $agent->isDesktop(); + $agent->setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Version/17.0 Mobile/15E148 Safari/604.1'); + + // Act + $platform = $agent->platform(); + $browser = $agent->browser(); + $isDesktop = $agent->isDesktop(); + + // Assert + $this->assertSame('iOS', $platform); + $this->assertSame('Safari', $browser); + $this->assertFalse($isDesktop); + } +}