OpenProject is the leading open source project management software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
openproject/spec/controllers/account_controller_spec.rb

1086 lines
32 KiB

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require 'spec_helper'
describe AccountController,
skip_2fa_stage: true do
class UserHook < OpenProject::Hook::ViewListener
attr_reader :registered_user, :first_login_user
def user_registered(context)
@registered_user = context[:user]
end
def user_first_login(context)
@first_login_user = context[:user]
end
def reset!
@registered_user = nil
@first_login_user = nil
end
end
let(:hook) { UserHook.instance }
let(:user) { build_stubbed(:user) }
before do
hook.reset!
end
context 'GET #login' do
let(:setup) {}
let(:params) { {} }
before do
setup
get :login, params:
end
it 'renders the view' do
expect(response).to render_template 'login'
expect(response).to be_successful
end
context 'user already logged in' do
let(:setup) { login_as user }
it 'redirects to home' do
expect(response)
.to redirect_to my_page_path
end
end
context 'user already logged in and back url present' do
let(:setup) { login_as user }
let(:params) { { back_url: "/projects" } }
it 'redirects to back_url value' do
expect(response)
.to redirect_to projects_path
end
end
context 'user already logged in and invalid back url present' do
let(:setup) { login_as user }
let(:params) { { back_url: 'http://test.foo/work_packages/show/1' } }
it 'redirects to home' do
expect(response).to redirect_to my_page_path
end
end
end
context 'POST #login' do
shared_let(:admin) { create :admin }
describe 'wrong password' do
it 'redirects back to login' do
post :login, params: { username: 'admin', password: 'bad' }
expect(response).to be_successful
expect(response).to render_template 'login'
expect(flash[:error]).to include 'Invalid user or password'
end
end
context 'with first login' do
before do
admin.update first_login: true
post :login, params: { username: admin.login, password: 'adminADMIN!' }
end
it 'redirect to default path with ?first_time_user=true' do
expect(response).to redirect_to "/?first_time_user=true"
end
it 'calls the user_first_login hook' do
expect(hook.first_login_user).to eq admin
end
end
context 'without first login' do
before do
post :login, params: { username: admin.login, password: 'adminADMIN!' }
end
it 'redirect to the my page' do
expect(response).to redirect_to "/my/page"
end
it 'does not call the user_first_login hook' do
expect(hook.first_login_user).to be_nil
end
end
describe 'User logging in with back_url' do
it 'redirects to a relative path' do
post :login,
params: { username: admin.login, password: 'adminADMIN!', back_url: '/' }
expect(response).to redirect_to root_path
end
it 'redirects to an absolute path given the same host' do
# note: test.host is the hostname during tests
post :login,
params: {
username: admin.login,
password: 'adminADMIN!',
back_url: 'http://test.host/work_packages/show/1'
}
expect(response).to redirect_to '/work_packages/show/1'
end
it 'does not redirect to another host' do
post :login,
params: {
username: admin.login,
password: 'adminADMIN!',
back_url: 'http://test.foo/work_packages/show/1'
}
expect(response).to redirect_to my_page_path
end
it 'does not redirect to another host with a protocol relative url' do
post :login,
params: {
username: admin.login,
password: 'adminADMIN!',
back_url: '//test.foo/fake'
}
expect(response).to redirect_to my_page_path
end
it 'does not redirect to logout' do
post :login,
params: {
username: admin.login,
password: 'adminADMIN!',
back_url: '/logout'
}
expect(response).to redirect_to my_page_path
end
context 'with an auth source',
with_settings: { self_registration: Setting::SelfRegistration.disabled } do
let(:auth_source) { create :ldap_auth_source }
it 'creates the user on the fly' do
allow(AuthSource).to receive(:authenticate).and_return(login: 'foo',
firstname: 'Foo',
lastname: 'Smith',
mail: 'foo@bar.com',
auth_source_id: auth_source.id)
post :login, params: { username: 'foo', password: 'bar' }
expect(response).to redirect_to home_url(first_time_user: true)
user = User.find_by(login: 'foo')
expect(user).to be_an_instance_of User
expect(user.auth_source_id).to eq(auth_source.id)
expect(user.current_password).to be_nil
end
end
context 'with a relative url root' do
before do
@old_relative_url_root = OpenProject::Configuration['rails_relative_url_root']
OpenProject::Configuration['rails_relative_url_root'] = '/openproject'
end
after do
OpenProject::Configuration['rails_relative_url_root'] = @old_relative_url_root
end
it 'redirects to the same subdirectory with an absolute path' do
post :login,
params: {
username: admin.login,
password: 'adminADMIN!',
back_url: 'http://test.host/openproject/work_packages/show/1'
}
expect(response).to redirect_to '/openproject/work_packages/show/1'
end
it 'redirects to the same subdirectory with a relative path' do
post :login,
params: {
username: admin.login,
password: 'adminADMIN!',
back_url: '/openproject/work_packages/show/1'
}
expect(response).to redirect_to '/openproject/work_packages/show/1'
end
it 'does not redirect to another subdirectory with an absolute path' do
post :login,
params: {
username: admin.login,
password: 'adminADMIN!',
back_url: 'http://test.host/foo/work_packages/show/1'
}
expect(response).to redirect_to my_page_path
end
it 'does not redirect to another subdirectory with a relative path' do
post :login,
params: {
username: admin.login,
password: 'adminADMIN!',
back_url: '/foo/work_packages/show/1'
}
expect(response).to redirect_to my_page_path
end
it 'does not redirect to another subdirectory by going up the path hierarchy' do
post :login,
params: {
username: admin.login,
password: 'adminADMIN!',
back_url: 'http://test.host/openproject/../foo/work_packages/show/1'
}
expect(response).to redirect_to my_page_path
end
it 'does not redirect to another subdirectory with a protocol relative path' do
post :login,
params: {
username: admin.login,
password: 'adminADMIN!',
back_url: '//test.host/foo/work_packages/show/1'
}
expect(response).to redirect_to my_page_path
end
end
end
context 'GET #logout' do
shared_let(:admin) { create :admin }
it 'calls reset_session' do
expect(@controller).to receive(:reset_session).once
login_as admin
get :logout
expect(response).to be_redirect
end
context 'with a user with an SSO provider attached' do
let(:user) { build_stubbed :user, login: 'bob', identity_url: 'saml:foo' }
let(:slo_callback) { nil }
let(:sso_provider) do
{ name: 'saml', single_sign_out_callback: slo_callback }
end
before do
allow(OpenProject::Plugins::AuthPlugin)
.to(receive(:login_provider_for))
.and_return(sso_provider)
login_as user
end
context 'with no provider' do
it 'will redirect to default' do
get :logout
expect(response).to redirect_to home_path
end
end
context 'with a redirecting callback' do
let(:slo_callback) do
Proc.new do |prev_session, prev_user|
if prev_session[:foo] && prev_user[:login] = 'bob'
redirect_to '/login'
end
end
end
context 'with direct login and redirecting callback',
with_settings: { login_required?: true },
with_config: { omniauth_direct_login_provider: 'foo' } do
it 'will still call the callback' do
# Set the previous session
session[:foo] = 'bar'
get :logout
expect(response).to redirect_to '/login'
# Expect session to be cleared
expect(session[:foo]).to be_nil
end
end
it 'will call the callback' do
# Set the previous session
session[:foo] = 'bar'
get :logout
expect(response).to redirect_to '/login'
# Expect session to be cleared
expect(session[:foo]).to be_nil
end
end
context 'with a no-op callback' do
it 'will redirect to default if the callback does nothing' do
was_called = false
sso_provider[:single_sign_out_callback] = Proc.new do
was_called = true
end
get :logout
expect(was_called).to be true
expect(response).to redirect_to home_path
end
end
context 'with a provider that does not have slo_callback' do
let(:slo_callback) { nil }
it 'will redirect to default if the callback does nothing' do
get :logout
expect(response).to redirect_to home_path
end
end
end
end
describe 'for a user trying to log in via an API request' do
before do
post :login,
params: {
username: admin.login,
password: 'adminADMIN!'
},
format: :json
end
it 'returns a 410' do
expect(response.code.to_s).to eql('410')
end
it 'does not login the user' do
expect(@controller.send(:current_user).anonymous?).to be_truthy
end
end
context 'with disabled password login' do
before do
allow(OpenProject::Configuration).to receive(:disable_password_login?).and_return(true)
post :login
end
it 'is not found' do
expect(response.status).to eq 404
end
end
context 'with an auth source',
with_settings: { self_registration: Setting::SelfRegistration.disabled } do
let(:auth_source) { create :ldap_auth_source }
let(:user_attributes) do
{
login: 's.scallywag',
firstname: 'Scarlet',
lastname: 'Scallywag',
mail: 's.scallywag@openproject.com',
auth_source_id: auth_source.id
}
end
let(:authenticate) { true }
before do
allow(AuthSource).to receive(:authenticate).and_return(authenticate ? user_attributes : nil)
# required so that the register view can be rendered
allow_any_instance_of(User).to receive(:change_password_allowed?).and_return(false)
end
context 'with user limit reached' do
render_views
before do
allow(OpenProject::Enterprise).to receive(:user_limit_reached?).and_return(true)
post :login, params: { username: 'foo', password: 'bar' }
end
it 'shows the user limit error' do
expect(response.body).to have_text "User limit reached"
end
it 'renders the register form' do
expect(response.body).to include "/account/register"
end
end
end
end
describe '#login with omniauth_direct_login enabled',
with_config: { omniauth_direct_login_provider: 'some_provider' } do
describe 'GET' do
it 'redirects to some_provider' do
get :login
expect(response).to redirect_to '/auth/some_provider'
end
end
describe 'POST' do
it 'redirects to some_provider' do
post :login, params: { username: 'foo', password: 'bar' }
expect(response).to redirect_to '/auth/some_provider'
end
end
end
describe 'Login for user with forced password change' do
let(:admin) { create(:admin, force_password_change: true) }
before do
allow_any_instance_of(User).to receive(:change_password_allowed?).and_return(false)
end
describe "Missing flash data for user initiated password change" do
before do
post 'change_password',
flash: {
_password_change_user_id: nil
},
params: {
username: admin.login,
password: 'whatever',
new_password: 'whatever',
new_password_confirmation: 'whatever2'
}
end
it 'renders 404' do
expect(response.status).to eq 404
end
end
describe "User who is not allowed to change password can't login" do
before do
post 'change_password',
params: {
password_change_user_id: admin.id,
username: admin.login,
password: 'adminADMIN!',
new_password: 'adminADMIN!New',
new_password_confirmation: 'adminADMIN!New'
}
end
it 'redirects to the login page' do
expect(response).to redirect_to '/login'
end
end
describe 'User who is not allowed to change password, is not redirected to the login page' do
before do
post 'login', params: { username: admin.login, password: 'adminADMIN!' }
end
it 'redirects to the login page' do
expect(response).to redirect_to '/login'
end
end
end
describe 'POST #change_password' do
context 'with disabled password login' do
before do
allow(OpenProject::Configuration).to receive(:disable_password_login?).and_return(true)
post :change_password
end
it 'is not found' do
expect(response.status).to eq 404
end
end
end
shared_examples 'registration disabled' do
it 'redirects to back the login page' do
expect(response).to redirect_to signin_path
end
it 'informs the user that registration is disabled' do
expect(flash[:error]).to eq(I18n.t('account.error_self_registration_disabled'))
end
it 'does not call the user_registered callback' do
expect(hook.registered_user).to be_nil
end
end
context 'GET #register' do
context 'with self registration on',
with_settings: { self_registration: Setting::SelfRegistration.automatic } do
context 'and password login enabled' do
before do
get :register
end
it 'is successful' do
expect(subject).to respond_with :success
expect(response).to render_template :register
expect(assigns[:user]).not_to be_nil
expect(assigns[:user].notification_settings.size).to eq(1)
end
end
context 'and password login disabled' do
before do
allow(OpenProject::Configuration).to receive(:disable_password_login?).and_return(true)
get :register
end
it_behaves_like 'registration disabled'
end
end
context 'with self registration off',
with_settings: { self_registration: Setting::SelfRegistration.disabled } do
before do
get :register
end
it_behaves_like 'registration disabled'
end
context 'with self registration off but an ongoing invitation activation',
with_settings: { self_registration: Setting::SelfRegistration.disabled } do
let(:token) { create :invitation_token }
before do
session[:invitation_token] = token.value
get :register
end
it 'is successful' do
expect(subject).to respond_with :success
expect(response).to render_template :register
expect(assigns[:user]).not_to be_nil
end
end
end
# See integration/account_test.rb for the full test
context 'POST #register' do
context 'with self registration on automatic',
with_settings: { self_registration: Setting::SelfRegistration.automatic } do
before do
allow(OpenProject::Configuration).to receive(:disable_password_login?).and_return(false)
end
context 'with password login enabled' do
# expects `redirect_to_path`
shared_examples 'automatic self registration succeeds' do
before do
post :register,
params: {
user: {
login: 'register',
password: 'adminADMIN!',
password_confirmation: 'adminADMIN!',
firstname: 'John',
lastname: 'Doe',
mail: 'register@example.com'
}
}
end
it 'redirects to the expected path' do
expect(subject).to respond_with :redirect
expect(assigns[:user]).not_to be_nil
expect(subject).to redirect_to(redirect_to_path)
expect(User.where(login: 'register').last).not_to be_nil
end
it 'set the user status to active' do
user = User.where(login: 'register').last
expect(user).not_to be_nil
expect(user).to be_active
end
it 'calls the user_registered callback' do
user = hook.registered_user
expect(user.mail).to eq "register@example.com"
expect(user).to be_active
end
end
it_behaves_like 'automatic self registration succeeds' do
let(:redirect_to_path) { "/?first_time_user=true" }
it "calls the user_first_login callback" do
user = hook.first_login_user
expect(user.mail).to eq "register@example.com"
end
end
context "with user limit reached" do
let!(:admin) { create :admin }
let(:params) do
{
user: {
login: 'register',
password: 'adminADMIN!',
password_confirmation: 'adminADMIN!',
firstname: 'John',
lastname: 'Doe',
mail: 'register@example.com'
}
}
end
before do
allow(OpenProject::Enterprise).to receive(:user_limit_reached?).and_return(true)
post :register, params:
end
it "fails" do
expect(subject).to redirect_to(signin_path)
expect(flash[:error]).to match /user limit reached/
end
it "notifies the admins about the issue" do
perform_enqueued_jobs
mail = ActionMailer::Base.deliveries.detect { |mail| mail.to.first == admin.mail }
expect(mail).to be_present
expect(mail.subject).to match /limit reached/
expect(mail.body.parts.first.to_s).to match /new user \(#{params[:user][:mail]}\)/
end
it 'does not call the user_registered callback' do
expect(hook.registered_user).to be_nil
end
end
end
context 'with password login disabled' do
before do
allow(OpenProject::Configuration).to receive(:disable_password_login?).and_return(true)
post :register
end
it_behaves_like 'registration disabled'
end
end
context 'with self registration by email',
with_settings: { self_registration: Setting::SelfRegistration.by_email } do
context 'with password login enabled' do
before do
Token::Invitation.delete_all
post :register,
params: {
user: {
login: 'register',
password: 'adminADMIN!',
password_confirmation: 'adminADMIN!',
firstname: 'John',
lastname: 'Doe',
mail: 'register@example.com'
}
}
end
it 'redirects to the login page' do
expect(subject).to redirect_to '/login'
end
it "doesn't activate the user but sends out a token instead" do
expect(User.find_by_login('register')).not_to be_active
token = Token::Invitation.last
expect(token.user.mail).to eq('register@example.com')
expect(token).not_to be_expired
end
it 'calls the user_registered callback' do
user = hook.registered_user
expect(user.mail).to eq "register@example.com"
expect(user).to be_registered
end
end
context 'with password login disabled' do
before do
allow(OpenProject::Configuration).to receive(:disable_password_login?).and_return(true)
post :register
end
it_behaves_like 'registration disabled'
end
end
context 'with manual activation',
with_settings: { self_registration: Setting::SelfRegistration.manual } do
let(:user_hash) do
{ login: 'register',
password: 'adminADMIN!',
password_confirmation: 'adminADMIN!',
firstname: 'John',
lastname: 'Doe',
mail: 'register@example.com' }
end
context 'without back_url' do
before do
post :register, params: { user: user_hash }
end
it 'redirects to the login page' do
expect(response).to redirect_to '/login'
end
it "doesn't activate the user" do
expect(User.find_by_login('register')).not_to be_active
end
it 'calls the user_registered callback' do
user = hook.registered_user
expect(user.mail).to eq "register@example.com"
expect(user).to be_registered
end
end
context 'with back_url' do
before do
post :register, params: { user: user_hash, back_url: 'https://example.net/some_back_url' }
end
it 'preserves the back url' do
expect(response).to redirect_to(
'/login?back_url=https%3A%2F%2Fexample.net%2Fsome_back_url'
)
end
it 'calls the user_registered callback' do
user = hook.registered_user
expect(user.mail).to eq "register@example.com"
expect(user).to be_registered
end
end
context 'with password login disabled' do
before do
allow(OpenProject::Configuration).to receive(:disable_password_login?).and_return(true)
post :register
end
it_behaves_like 'registration disabled'
end
end
context 'with self registration off',
with_settings: { self_registration: Setting::SelfRegistration.disabled } do
before do
post :register,
params: {
user: {
login: 'register',
password: 'adminADMIN!',
password_confirmation: 'adminADMIN!',
firstname: 'John',
lastname: 'Doe',
mail: 'register@example.com'
}
}
end
it_behaves_like 'registration disabled'
end
context 'with on-the-fly registration',
with_settings: { self_registration: Setting::SelfRegistration.disabled } do
before do
allow_any_instance_of(User).to receive(:change_password_allowed?).and_return(false)
allow(AuthSource).to receive(:authenticate).and_return(login: 'foo',
lastname: 'Smith',
auth_source_id: 66)
end
context 'with password login enabled' do
before do
post :login, params: { username: 'foo', password: 'bar' }
end
it 'registers the user on-the-fly' do
expect(subject).to respond_with :success
expect(response).to render_template :register
post :register,
params: {
user: {
firstname: 'Foo',
lastname: 'Smith',
mail: 'foo@bar.com'
}
}
expect(response).to redirect_to home_path(first_time_user: true)
user = User.find_by_login('foo')
expect(user).to be_an_instance_of(User)
expect(user.auth_source_id).to be 66
expect(user.current_password).to be_nil
end
end
context 'with password login disabled' do
before do
allow(OpenProject::Configuration).to receive(:disable_password_login?).and_return(true)
end
describe 'login' do
before do
post :login, params: { username: 'foo', password: 'bar' }
end
it 'is not found' do
expect(response.status).to eq 404
end
end
describe 'registration' do
before do
post :register,
params: {
user: {
firstname: 'Foo',
lastname: 'Smith',
mail: 'foo@bar.com'
}
}
end
it_behaves_like 'registration disabled'
end
end
end
end
context 'POST activate' do
let!(:admin) { create :admin }
let(:user) { create :user, status: }
let(:status) { -1 }
let(:token) { Token::Invitation.create!(user_id: user.id) }
before do
allow(OpenProject::Enterprise).to receive(:user_limit_reached?).and_return(true)
post :activate, params: { token: token.value }
end
shared_examples "activation is blocked due to user limit" do
it "does not activate the user" do
expect(user.reload).not_to be_active
end
it "redirects back to the login page and shows the user limit error" do
expect(response).to redirect_to(signin_path)
expect(flash[:error]).to match /user limit reached.*contact.*admin/i
end
it "notifies the admins about the issue" do
perform_enqueued_jobs
mail = ActionMailer::Base.deliveries.detect { |mail| mail.to.first == admin.mail }
expect(mail).to be_present
expect(mail.subject).to match /limit reached/
end
end
context 'registered user' do
let(:status) { User.statuses[:registered] }
it_behaves_like "activation is blocked due to user limit"
end
context 'invited user' do
let(:status) { User.statuses[:invited] }
it_behaves_like "activation is blocked due to user limit"
end
end
describe 'GET #auth_source_sso_failed (/sso)' do
render_views
let(:failure) do
{
login:,
back_url: '/my/account',
ttl: 1
}
end
let(:auth_source) { create :ldap_auth_source }
let(:user) { create :user, status: 2, auth_source: }
let(:login) { user.login }
before do
session[:auth_source_sso_failure] = failure
end
context "with a non-active user" do
it "shows the non-active error message" do
get :auth_source_sso_failed
expect(session[:auth_source_sso_failure]).not_to be_present
expect(response.body)
.to have_text "Your account has not yet been activated."
expect(response.body)
.to have_text "Single Sign-On (SSO) for user '#{user.login}' failed"
end
end
context "with an invalid user" do
let!(:duplicate) { create :user, mail: "login@DerpLAP.net" }
let(:login) { 'foo' }
let(:attrs) do
{ mail: duplicate.mail, login:, firstname: 'bla', lastname: 'bar' }
end
before do
allow(AuthSource).to receive(:find_user).and_return attrs
end
it "shows the account creation form with an error" do
get :auth_source_sso_failed
expect(session[:auth_source_sso_failure]).not_to be_present
expect(response.body).to have_text "Create a new account"
expect(response.body).to have_text "This field is invalid: Email has already been taken."
end
end
context "with a missing email" do
let!(:duplicate) { create :user, mail: "login@DerpLAP.net" }
let(:login) { 'foo' }
let(:attrs) do
{ login:, firstname: 'bla', lastname: 'bar' }
end
before do
allow(AuthSource).to receive(:find_user).and_return attrs
end
it "shows the account creation form with an error" do
get :auth_source_sso_failed
expect(session[:auth_source_sso_failure]).not_to be_present
expect(response.body).to have_text "Create a new account"
expect(response.body).to have_text "This field is invalid: Email can't be blank."
end
end
end
describe 'POST #activate' do
shared_examples 'account activation' do
let(:token) { Token::Invitation.create user: }
let(:activation_params) do
{
token: token.value
}
end
context 'with an expired token' do
before do
token.update_column :expires_on, 1.day.ago
post :activate, params: activation_params
end
it 'fails and shows an expiration warning' do
expect(subject).to redirect_to('/')
expect(flash[:warning]).to include 'expired'
end
it 'deletes the old token and generates a new one' do
old_token = Token::Invitation.find_by(id: token.id)
new_token = Token::Invitation.find_by(user_id: token.user.id)
expect(old_token).to be_nil
expect(new_token).to be_present
expect(new_token).not_to be_expired
end
it 'sends out a new activation email' do
new_token = Token::Invitation.find_by(user_id: token.user.id)
perform_enqueued_jobs
mail = ActionMailer::Base.deliveries.last
expect(mail.parts.first.body.raw_source).to include "activate?token=#{new_token.value}"
end
end
end
context 'with an invited user' do
it_behaves_like 'account activation' do
let(:user) { create :user, status: 4 }
end
end
context 'with an registered user' do
it_behaves_like 'account activation' do
let(:user) { create :user, status: 2 }
end
end
end
end