X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/a6633642232efd164d4708967ab59e498fbff896..refs/pull/3373/head:/tests/Auth/AuthTest.php diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php index a0de7f803..0ab6d0e8c 100644 --- a/tests/Auth/AuthTest.php +++ b/tests/Auth/AuthTest.php @@ -1,66 +1,72 @@ -visit('/') - ->seePageIs('/login'); + $this->get('/')->assertRedirect('/login'); } public function test_login() { - $this->login('admin@admin.com', 'password') - ->seePageIs('/'); + $this->login('admin@admin.com', 'password')->assertRedirect('/'); } public function test_public_viewing() { - $settings = app(SettingService::class); - $settings->put('app-public', 'true'); - $this->visit('/') - ->seePageIs('/') - ->see('Log In'); + $this->setSettings(['app-public' => 'true']); + $this->get('/') + ->assertOk() + ->assertSee('Log in'); } public function test_registration_showing() { // Ensure registration form is showing $this->setSettings(['registration-enabled' => 'true']); - $this->visit('/login') - ->see('Sign up') - ->click('Sign up') - ->seePageIs('/register'); + $this->get('/login') + ->assertElementContains('a[href="' . url('/https/source.bookstackapp.com/register') . '"]', 'Sign up'); } public function test_normal_registration() { // Set settings and get user instance - $this->setSettings(['registration-enabled' => 'true']); - $user = factory(User::class)->make(); + /** @var Role $registrationRole */ + $registrationRole = Role::query()->first(); + $this->setSettings(['registration-enabled' => 'true', 'registration-role' => $registrationRole->id]); + /** @var User $user */ + $user = User::factory()->make(); // Test form and ensure user is created - $this->visit('/register') - ->see('Sign Up') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/') - ->see($user->name) - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email]); + $this->get('/register') + ->assertSee('Sign Up') + ->assertElementContains('form[action="' . url('/https/source.bookstackapp.com/register') . '"]', 'Create Account'); + + $resp = $this->post('/register', $user->only('password', 'name', 'email')); + $resp->assertRedirect('/'); + + $resp = $this->get('/'); + $resp->assertOk(); + $resp->assertSee($user->name); + + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]); + + $user = User::query()->where('email', '=', $user->email)->first(); + $this->assertEquals(1, $user->roles()->count()); + $this->assertEquals($registrationRole->id, $user->roles()->first()->id); } public function test_empty_registration_redirects_back_with_errors() @@ -69,36 +75,33 @@ class AuthTest extends BrowserKitTest $this->setSettings(['registration-enabled' => 'true']); // Test form and ensure user is created - $this->visit('/register') - ->press('Create Account') - ->see('The name field is required') - ->seePageIs('/register'); + $this->get('/register'); + $this->post('/register', [])->assertRedirect('/register'); + $this->get('/register')->assertSee('The name field is required'); } public function test_registration_validation() { $this->setSettings(['registration-enabled' => 'true']); - $this->visit('/register') - ->type('1', '#name') - ->type('1', '#email') - ->type('1', '#password') - ->press('Create Account') - ->see('The name must be at least 2 characters.') - ->see('The email must be a valid email address.') - ->see('The password must be at least 8 characters.') - ->seePageIs('/register'); + $this->get('/register'); + $resp = $this->followingRedirects()->post('/register', [ + 'name' => '1', + 'email' => '1', + 'password' => '1', + ]); + $resp->assertSee('The name must be at least 2 characters.'); + $resp->assertSee('The email must be a valid email address.'); + $resp->assertSee('The password must be at least 8 characters.'); } public function test_sign_up_link_on_login() { - $this->visit('/login') - ->dontSee('Sign up'); + $this->get('/login')->assertDontSee('Sign up'); $this->setSettings(['registration-enabled' => 'true']); - $this->visit('/login') - ->see('Sign up'); + $this->get('/login')->assertSee('Sign up'); } public function test_confirmed_registration() @@ -108,287 +111,219 @@ class AuthTest extends BrowserKitTest // Set settings and get user instance $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']); - $user = factory(User::class)->make(); + $user = User::factory()->make(); // Go through registration process - $this->visit('/register') - ->see('Sign Up') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register/confirm') - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); + $resp = $this->post('/register', $user->only('name', 'email', 'password')); + $resp->assertRedirect('/register/confirm'); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); // Ensure notification sent - $dbUser = User::where('email', '=', $user->email)->first(); + /** @var User $dbUser */ + $dbUser = User::query()->where('email', '=', $user->email)->first(); Notification::assertSentTo($dbUser, ConfirmEmail::class); // Test access and resend confirmation email - $this->login($user->email, $user->password) - ->seePageIs('/register/confirm/awaiting') - ->see('Resend') - ->visit('/books') - ->seePageIs('/register/confirm/awaiting') - ->press('Resend Confirmation Email'); + $resp = $this->login($user->email, $user->password); + $resp->assertRedirect('/register/confirm/awaiting'); + + $resp = $this->get('/register/confirm/awaiting'); + $resp->assertElementContains('form[action="' . url('/https/source.bookstackapp.com/register/confirm/resend') . '"]', 'Resend'); + + $this->get('/books')->assertRedirect('/login'); + $this->post('/register/confirm/resend', $user->only('email')); // Get confirmation and confirm notification matches $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first(); - Notification::assertSentTo($dbUser, ConfirmEmail::class, function($notification, $channels) use ($emailConfirmation) { + Notification::assertSentTo($dbUser, ConfirmEmail::class, function ($notification, $channels) use ($emailConfirmation) { return $notification->token === $emailConfirmation->token; }); - + // Check confirmation email confirmation activation. - $this->visit('/register/confirm/' . $emailConfirmation->token) - ->seePageIs('/') - ->see($user->name) - ->notSeeInDatabase('email_confirmations', ['token' => $emailConfirmation->token]) - ->seeInDatabase('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]); + $this->get('/register/confirm/' . $emailConfirmation->token)->assertRedirect('/login'); + $this->get('/login')->assertSee('Your email has been confirmed! You should now be able to login using this email address.'); + $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]); + $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]); } public function test_restricted_registration() { $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']); - $user = factory(User::class)->make(); + $user = User::factory()->make(); + // Go through registration process - $this->visit('/register') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register') - ->dontSeeInDatabase('users', ['email' => $user->email]) - ->see('That email domain does not have access to this application'); + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register'); + $resp = $this->get('/register'); + $resp->assertSee('That email domain does not have access to this application'); + $this->assertDatabaseMissing('users', $user->only('email')); $user->email = 'barry@example.com'; - $this->visit('/register') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register/confirm') - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); - - $this->visit('/') - ->seePageIs('/register/confirm/awaiting'); - - auth()->logout(); - - $this->visit('/')->seePageIs('/login') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Log In') - ->seePageIs('/register/confirm/awaiting') - ->seeText('Email Address Not Confirmed'); + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register/confirm'); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); + + $this->assertNull(auth()->user()); + + $this->get('/')->assertRedirect('/login'); + $resp = $this->followingRedirects()->post('/login', $user->only('email', 'password')); + $resp->assertSee('Email Address Not Confirmed'); + $this->assertNull(auth()->user()); } public function test_restricted_registration_with_confirmation_disabled() { $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']); - $user = factory(User::class)->make(); + $user = User::factory()->make(); + // Go through registration process - $this->visit('/register') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register') - ->dontSeeInDatabase('users', ['email' => $user->email]) - ->see('That email domain does not have access to this application'); + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register'); + $this->assertDatabaseMissing('users', $user->only('email')); + $this->get('/register')->assertSee('That email domain does not have access to this application'); $user->email = 'barry@example.com'; - $this->visit('/register') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register/confirm') - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); - - $this->visit('/') - ->seePageIs('/register/confirm/awaiting'); - - auth()->logout(); - $this->visit('/')->seePageIs('/login') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Log In') - ->seePageIs('/register/confirm/awaiting') - ->seeText('Email Address Not Confirmed'); - } + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register/confirm'); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); - public function test_user_creation() - { - $user = factory(User::class)->make(); - $adminRole = Role::getRole('admin'); - - $this->asAdmin() - ->visit('/settings/users') - ->click('Add New User') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->check("roles[{$adminRole->id}]") - ->type($user->password, '#password') - ->type($user->password, '#password-confirm') - ->press('Save') - ->seePageIs('/settings/users') - ->seeInDatabase('users', $user->toArray()) - ->see($user->name); - } + $this->assertNull(auth()->user()); - public function test_user_updating() - { - $user = $this->getNormalUser(); - $password = $user->password; - $this->asAdmin() - ->visit('/settings/users') - ->click($user->name) - ->seePageIs('/settings/users/' . $user->id) - ->see($user->email) - ->type('Barry Scott', '#name') - ->press('Save') - ->seePageIs('/settings/users') - ->seeInDatabase('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password]) - ->notSeeInDatabase('users', ['name' => $user->name]); + $this->get('/')->assertRedirect('/login'); + $resp = $this->post('/login', $user->only('email', 'password')); + $resp->assertRedirect('/register/confirm/awaiting'); + $this->get('/register/confirm/awaiting')->assertSee('Email Address Not Confirmed'); + $this->assertNull(auth()->user()); } - public function test_user_password_update() + public function test_registration_role_unset_by_default() { - $user = $this->getNormalUser(); - $userProfilePage = '/settings/users/' . $user->id; - $this->asAdmin() - ->visit($userProfilePage) - ->type('newpassword', '#password') - ->press('Save') - ->seePageIs($userProfilePage) - ->see('Password confirmation required') - - ->type('newpassword', '#password') - ->type('newpassword', '#password-confirm') - ->press('Save') - ->seePageIs('/settings/users'); - - $userPassword = User::find($user->id)->password; - $this->assertTrue(Hash::check('newpassword', $userPassword)); - } + $this->assertFalse(setting('registration-role')); - public function test_user_deletion() - { - $userDetails = factory(User::class)->make(); - $user = $this->getEditor($userDetails->toArray()); - - $this->asAdmin() - ->visit('/settings/users/' . $user->id) - ->click('Delete User') - ->see($user->name) - ->press('Confirm') - ->seePageIs('/settings/users') - ->notSeeInDatabase('users', ['name' => $user->name]); + $resp = $this->asAdmin()->get('/settings/registration'); + $resp->assertElementContains('select[name="setting-registration-role"] option[value="0"][selected]', '-- None --'); } - public function test_user_cannot_be_deleted_if_last_admin() + public function test_logout() { - $adminRole = Role::getRole('admin'); - - // Delete all but one admin user if there are more than one - $adminUsers = $adminRole->users; - if (count($adminUsers) > 1) { - foreach ($adminUsers->splice(1) as $user) { - $user->delete(); - } - } - - // Ensure we currently only have 1 admin user - $this->assertEquals(1, $adminRole->users()->count()); - $user = $adminRole->users->first(); - - $this->asAdmin()->visit('/settings/users/' . $user->id) - ->click('Delete User') - ->press('Confirm') - ->seePageIs('/settings/users/' . $user->id) - ->see('You cannot delete the only admin'); + $this->asAdmin()->get('/')->assertOk(); + $this->post('/logout')->assertRedirect('/'); + $this->get('/')->assertRedirect('/login'); } - public function test_logout() + public function test_mfa_session_cleared_on_logout() { - $this->asAdmin() - ->visit('/') - ->seePageIs('/') - ->visit('/logout') - ->visit('/') - ->seePageIs('/login'); + $user = $this->getEditor(); + $mfaSession = $this->app->make(MfaSession::class); + + $mfaSession->markVerifiedForUser($user); + $this->assertTrue($mfaSession->isVerifiedForUser($user)); + + $this->asAdmin()->post('/logout'); + $this->assertFalse($mfaSession->isVerifiedForUser($user)); } public function test_reset_password_flow() { Notification::fake(); - $this->visit('/login')->click('Forgot Password?') - ->seePageIs('/password/email') - ->type('admin@admin.com', 'email') - ->press('Send Reset Link') - ->see('A password reset link will be sent to admin@admin.com if that email address is found in the system.'); + $this->get('/login') + ->assertElementContains('a[href="' . url('/https/source.bookstackapp.com/password/email') . '"]', 'Forgot Password?'); + + $this->get('/password/email') + ->assertElementContains('form[action="' . url('/https/source.bookstackapp.com/password/email') . '"]', 'Send Reset Link'); - $this->seeInDatabase('password_resets', [ - 'email' => 'admin@admin.com' + $resp = $this->post('/password/email', [ + 'email' => 'admin@admin.com', ]); + $resp->assertRedirect('/password/email'); - $user = User::where('email', '=', 'admin@admin.com')->first(); + $resp = $this->get('/password/email'); + $resp->assertSee('A password reset link will be sent to admin@admin.com if that email address is found in the system.'); + + $this->assertDatabaseHas('password_resets', [ + 'email' => 'admin@admin.com', + ]); + + /** @var User $user */ + $user = User::query()->where('email', '=', 'admin@admin.com')->first(); Notification::assertSentTo($user, ResetPassword::class); $n = Notification::sent($user, ResetPassword::class); - $this->visit('/password/reset/' . $n->first()->token) - ->see('Reset Password') - ->submitForm('Reset Password', [ - 'email' => 'admin@admin.com', - 'password' => 'randompass', - 'password_confirmation' => 'randompass' - ])->seePageIs('/') - ->see('Your password has been successfully reset'); + $this->get('/password/reset/' . $n->first()->token) + ->assertOk() + ->assertSee('Reset Password'); + + $resp = $this->post('/password/reset', [ + 'email' => 'admin@admin.com', + 'password' => 'randompass', + 'password_confirmation' => 'randompass', + 'token' => $n->first()->token, + ]); + $resp->assertRedirect('/'); + + $this->get('/')->assertSee('Your password has been successfully reset'); } public function test_reset_password_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery() { - $this->visit('/login')->click('Forgot Password?') - ->seePageIs('/password/email') - ->type('barry@admin.com', 'email') - ->press('Send Reset Link') - ->see('A password reset link will be sent to barry@admin.com if that email address is found in the system.') - ->dontSee('We can\'t find a user'); - - - $this->visit('/password/reset/arandometokenvalue') - ->see('Reset Password') - ->submitForm('Reset Password', [ - 'email' => 'barry@admin.com', - 'password' => 'randompass', - 'password_confirmation' => 'randompass' - ])->followRedirects() - ->seePageIs('/password/reset/arandometokenvalue') - ->dontSee('We can\'t find a user') - ->see('The password reset token is invalid for this email address.'); + $this->get('/password/email'); + $resp = $this->followingRedirects()->post('/password/email', [ + 'email' => 'barry@admin.com', + ]); + $resp->assertSee('A password reset link will be sent to barry@admin.com if that email address is found in the system.'); + $resp->assertDontSee('We can\'t find a user'); + + $this->get('/password/reset/arandometokenvalue')->assertSee('Reset Password'); + $resp = $this->post('/password/reset', [ + 'email' => 'barry@admin.com', + 'password' => 'randompass', + 'password_confirmation' => 'randompass', + 'token' => 'arandometokenvalue', + ]); + $resp->assertRedirect('/password/reset/arandometokenvalue'); + + $this->get('/password/reset/arandometokenvalue') + ->assertDontSee('We can\'t find a user') + ->assertSee('The password reset token is invalid for this email address.'); } public function test_reset_password_page_shows_sign_links() { $this->setSettings(['registration-enabled' => 'true']); - $this->visit('/password/email') - ->seeLink('Log in') - ->seeLink('Sign up'); + $this->get('/password/email') + ->assertElementContains('a', 'Log in') + ->assertElementContains('a', 'Sign up'); + } + + public function test_reset_password_request_is_throttled() + { + $editor = $this->getEditor(); + Notification::fake(); + $this->get('/password/email'); + $this->followingRedirects()->post('/password/email', [ + 'email' => $editor->email, + ]); + + $resp = $this->followingRedirects()->post('/password/email', [ + 'email' => $editor->email, + ]); + Notification::assertTimesSent(1, ResetPassword::class); + $resp->assertSee('A password reset link will be sent to ' . $editor->email . ' if that email address is found in the system.'); } public function test_login_redirects_to_initially_requested_url_correctly() { config()->set('app.url', 'http://localhost'); + /** @var Page $page */ $page = Page::query()->first(); - $this->visit($page->getUrl()) - ->seePageUrlIs(url('/login')); + $this->get($page->getUrl())->assertRedirect(url('/login')); $this->login('admin@admin.com', 'password') - ->seePageUrlIs($page->getUrl()); + ->assertRedirect($page->getUrl()); } public function test_login_intended_redirect_does_not_redirect_to_external_pages() @@ -399,7 +334,15 @@ class AuthTest extends BrowserKitTest $this->get('/login', ['referer' => 'https://example.com']); $login = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']); - $login->assertRedirectedTo('http://localhost'); + $login->assertRedirect('http://localhost'); + } + + public function test_login_intended_redirect_does_not_factor_mfa_routes() + { + $this->get('/books')->assertRedirect('/login'); + $this->get('/mfa/setup')->assertRedirect('/login'); + $login = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']); + $login->assertRedirect('/books'); } public function test_login_authenticates_admins_on_all_guards() @@ -408,6 +351,7 @@ class AuthTest extends BrowserKitTest $this->assertTrue(auth()->check()); $this->assertTrue(auth('ldap')->check()); $this->assertTrue(auth('saml2')->check()); + $this->assertTrue(auth('oidc')->check()); } public function test_login_authenticates_nonadmins_on_default_guard_only() @@ -420,6 +364,7 @@ class AuthTest extends BrowserKitTest $this->assertTrue(auth()->check()); $this->assertFalse(auth('ldap')->check()); $this->assertFalse(auth('saml2')->check()); + $this->assertFalse(auth('oidc')->check()); } public function test_failed_logins_are_logged_when_message_configured() @@ -434,14 +379,25 @@ class AuthTest extends BrowserKitTest $this->assertFalse($log->hasWarningThatContains('Failed login for admin@admin.com')); } + public function test_logged_in_user_with_unconfirmed_email_is_logged_out() + { + $this->setSettings(['registration-confirmation' => 'true']); + $user = $this->getEditor(); + $user->email_confirmed = false; + $user->save(); + + auth()->login($user); + $this->assertTrue(auth()->check()); + + $this->get('/books')->assertRedirect('/'); + $this->assertFalse(auth()->check()); + } + /** - * Perform a login + * Perform a login. */ - protected function login(string $email, string $password): AuthTest + protected function login(string $email, string $password): TestResponse { - return $this->visit('/login') - ->type($email, '#email') - ->type($password, '#password') - ->press('Log In'); + return $this->post('/login', compact('email', 'password')); } }