123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- <?php
- namespace Tests\Feature\Http\Auth;
- use App\Http\Controllers\Auth\LoginController;
- use App\Http\Middleware\RejectIfAuthenticated;
- use App\Http\Middleware\RejectIfDemoMode;
- use App\Http\Middleware\RejectIfReverseProxy;
- use App\Http\Middleware\SkipIfAuthenticated;
- use App\Listeners\Authentication\FailedLoginListener;
- use App\Listeners\Authentication\LoginListener;
- use App\Models\User;
- use App\Notifications\FailedLogin;
- use App\Notifications\SignedInWithNewDevice;
- use Illuminate\Support\Carbon;
- use Illuminate\Support\Facades\Config;
- use Illuminate\Support\Facades\Notification;
- use PHPUnit\Framework\Attributes\CoversClass;
- use Tests\FeatureTestCase;
- /**
- * LoginTest test class
- */
- #[CoversClass(LoginController::class)]
- #[CoversClass(RejectIfAuthenticated::class)]
- #[CoversClass(RejectIfReverseProxy::class)]
- #[CoversClass(RejectIfDemoMode::class)]
- #[CoversClass(SkipIfAuthenticated::class)]
- #[CoversClass(LoginListener::class)]
- #[CoversClass(FailedLoginListener::class)]
- class LoginTest extends FeatureTestCase
- {
- /**
- * @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
- */
- protected $user;
- /**
- * @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
- */
- protected $admin;
- private const PASSWORD = 'password';
- private const WRONG_PASSWORD = 'wrong_password';
- /**
- * @test
- */
- public function setUp() : void
- {
- parent::setUp();
- $this->user = User::factory()->create();
- $this->admin = User::factory()->administrator()->create();
- }
- /**
- * @test
- */
- public function test_user_login_returns_success()
- {
- $response = $this->json('POST', '/user/login', [
- 'email' => $this->user->email,
- 'password' => self::PASSWORD,
- ])
- ->assertOk()
- ->assertJsonFragment([
- 'message' => 'authenticated',
- 'id' => $this->user->id,
- 'name' => $this->user->name,
- 'email' => $this->user->email,
- 'is_admin' => false,
- ])
- ->assertJsonStructure([
- 'preferences',
- ]);
- }
- /**
- * @test
- */
- public function test_login_send_new_device_notification()
- {
- Notification::fake();
- $this->json('POST', '/user/login', [
- 'email' => $this->user->email,
- 'password' => self::PASSWORD,
- ])->assertOk();
- $this->actingAs($this->user, 'web-guard')
- ->json('GET', '/user/logout');
- $this->travel(1)->minute();
- $this->json('POST', '/user/login', [
- 'email' => $this->user->email,
- 'password' => self::PASSWORD,
- ], [
- 'HTTP_USER_AGENT' => 'NotSymfony',
- ])->assertOk();
- Notification::assertSentTo($this->user, SignedInWithNewDevice::class);
- }
- /**
- * @test
- */
- public function test_login_does_not_send_new_device_notification()
- {
- Notification::fake();
- $this->user['preferences->notifyOnNewAuthDevice'] = 0;
- $this->user->save();
- $this->json('POST', '/user/login', [
- 'email' => $this->user->email,
- 'password' => self::PASSWORD,
- ])->assertOk();
- $this->actingAs($this->user, 'web-guard')
- ->json('GET', '/user/logout');
- $this->travel(1)->minute();
- $this->json('POST', '/user/login', [
- 'email' => $this->user->email,
- 'password' => self::PASSWORD,
- ], [
- 'HTTP_USER_AGENT' => 'NotSymfony',
- ])->assertOk();
- Notification::assertNothingSentTo($this->user);
- }
- /**
- * @test
- */
- public function test_admin_login_returns_admin_role()
- {
- $response = $this->json('POST', '/user/login', [
- 'email' => $this->admin->email,
- 'password' => self::PASSWORD,
- ])
- ->assertOk()
- ->assertJsonFragment([
- 'is_admin' => true,
- ]);
- }
- /**
- * @test
- *
- * @covers \App\Rules\CaseInsensitiveEmailExists
- */
- public function test_user_login_with_uppercased_email_returns_success()
- {
- $response = $this->json('POST', '/user/login', [
- 'email' => strtoupper($this->user->email),
- 'password' => self::PASSWORD,
- ])
- ->assertOk()
- ->assertJsonFragment([
- 'message' => 'authenticated',
- 'name' => $this->user->name,
- ])
- ->assertJsonStructure([
- 'message',
- 'name',
- 'preferences',
- ]);
- }
- /**
- * @test
- *
- * @covers \App\Http\Middleware\SkipIfAuthenticated
- */
- public function test_user_login_already_authenticated_is_rejected()
- {
- $response = $this->json('POST', '/user/login', [
- 'email' => $this->user->email,
- 'password' => self::PASSWORD,
- ]);
- $response = $this->actingAs($this->user, 'web-guard')
- ->json('POST', '/user/login', [
- 'email' => $this->user->email,
- 'password' => self::PASSWORD,
- ])
- ->assertStatus(400)
- ->assertJsonStructure([
- 'message',
- ]);
- }
- /**
- * @test
- */
- public function test_user_login_with_missing_data_returns_validation_error()
- {
- $response = $this->json('POST', '/user/login', [
- 'email' => '',
- 'password' => '',
- ])
- ->assertStatus(422)
- ->assertJsonValidationErrors([
- 'email',
- 'password',
- ]);
- }
- /**
- * @test
- *
- * @covers \App\Exceptions\Handler
- */
- public function test_user_login_with_invalid_credentials_returns_unauthorized()
- {
- $response = $this->json('POST', '/user/login', [
- 'email' => $this->user->email,
- 'password' => self::WRONG_PASSWORD,
- ])
- ->assertStatus(401)
- ->assertJson([
- 'message' => 'unauthorized',
- ]);
- }
- /**
- * @test
- */
- public function test_login_with_invalid_credentials_send_failed_login_notification()
- {
- Notification::fake();
- $this->json('POST', '/user/login', [
- 'email' => $this->user->email,
- 'password' => self::WRONG_PASSWORD,
- ])->assertStatus(401);
- Notification::assertSentTo($this->user, FailedLogin::class);
- }
- /**
- * @test
- */
- public function test_login_with_invalid_credentials_does_not_send_new_device_notification()
- {
- Notification::fake();
- $this->user['preferences->notifyOnFailedLogin'] = 0;
- $this->user->save();
- $this->json('POST', '/user/login', [
- 'email' => $this->user->email,
- 'password' => self::WRONG_PASSWORD,
- ])->assertStatus(401);
- Notification::assertNothingSentTo($this->user);
- }
- /**
- * @test
- */
- public function test_too_many_login_attempts_with_invalid_credentials_returns_too_many_request_error()
- {
- $throttle = 8;
- Config::set('auth.throttle.login', $throttle);
- $post = [
- 'email' => $this->user->email,
- 'password' => self::WRONG_PASSWORD,
- ];
- for ($i = 0; $i < $throttle - 1; $i++) {
- $this->json('POST', '/user/login', $post);
- }
- $this->json('POST', '/user/login', $post)
- ->assertUnauthorized();
- $this->json('POST', '/user/login', $post)
- ->assertStatus(429);
- }
- /**
- * @test
- */
- public function test_user_logout_returns_validation_success()
- {
- $response = $this->json('POST', '/user/login', [
- 'email' => $this->user->email,
- 'password' => self::PASSWORD,
- ]);
- $response = $this->actingAs($this->user, 'web-guard')
- ->json('GET', '/user/logout')
- ->assertOk()
- ->assertExactJson([
- 'message' => 'signed out',
- ]);
- }
- /**
- * @test
- *
- * @covers \App\Http\Middleware\KickOutInactiveUser
- * @covers \App\Http\Middleware\LogUserLastSeen
- */
- public function test_user_logout_after_inactivity_returns_teapot()
- {
- // Set the autolock period to 1 minute
- $this->user['preferences->kickUserAfter'] = 1;
- $this->user->save();
- $response = $this->json('POST', '/user/login', [
- 'email' => $this->user->email,
- 'password' => self::PASSWORD,
- ]);
- // Ping a protected endpoint to log last_seen_at time
- $response = $this->actingAs($this->user, 'api-guard')
- ->json('GET', '/api/v1/twofaccounts');
- $this->travelTo(Carbon::now()->addMinutes(2));
- $response = $this->actingAs($this->user, 'api-guard')
- ->json('GET', '/api/v1/twofaccounts')
- ->assertStatus(418);
- }
- }
|