diff --git a/test/functional/account_controller_openid_test.rb b/test/functional/account_controller_openid_test.rb
new file mode 100644
index 000000000..6b3adc3eb
--- /dev/null
+++ b/test/functional/account_controller_openid_test.rb
@@ -0,0 +1,165 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class AccountControllerOpenidTest < ActionController::TestCase
+ tests AccountController
+ fixtures :users, :roles
+
+ def setup
+ User.current = nil
+ Setting.openid = '1'
+ end
+
+ def teardown
+ Setting.openid = '0'
+ end
+
+ if Object.const_defined?(:OpenID)
+
+ def test_login_with_openid_for_existing_user
+ Setting.self_registration = '3'
+ existing_user = User.new(:firstname => 'Cool',
+ :lastname => 'User',
+ :mail => 'user@somedomain.com',
+ :identity_url => 'http://openid.example.com/good_user')
+ existing_user.login = 'cool_user'
+ assert existing_user.save!
+
+ post :login, :openid_url => existing_user.identity_url
+ assert_redirected_to '/my/page'
+ end
+
+ def test_login_with_invalid_openid_provider
+ Setting.self_registration = '0'
+ post :login, :openid_url => 'http;//openid.example.com/good_user'
+ assert_redirected_to home_url
+ end
+
+ def test_login_with_openid_for_existing_non_active_user
+ Setting.self_registration = '2'
+ existing_user = User.new(:firstname => 'Cool',
+ :lastname => 'User',
+ :mail => 'user@somedomain.com',
+ :identity_url => 'http://openid.example.com/good_user',
+ :status => User::STATUS_REGISTERED)
+ existing_user.login = 'cool_user'
+ assert existing_user.save!
+
+ post :login, :openid_url => existing_user.identity_url
+ assert_redirected_to '/login'
+ end
+
+ def test_login_with_openid_with_new_user_created
+ Setting.self_registration = '3'
+ post :login, :openid_url => 'http://openid.example.com/good_user'
+ assert_redirected_to '/my/account'
+ user = User.find_by_login('cool_user')
+ assert user
+ assert_equal 'Cool', user.firstname
+ assert_equal 'User', user.lastname
+ end
+
+ def test_login_with_openid_with_new_user_and_self_registration_off
+ Setting.self_registration = '0'
+ post :login, :openid_url => 'http://openid.example.com/good_user'
+ assert_redirected_to home_url
+ user = User.find_by_login('cool_user')
+ assert_nil user
+ end
+
+ def test_login_with_openid_with_new_user_created_with_email_activation_should_have_a_token
+ Setting.self_registration = '1'
+ post :login, :openid_url => 'http://openid.example.com/good_user'
+ assert_redirected_to '/login'
+ user = User.find_by_login('cool_user')
+ assert user
+
+ token = Token.find_by_user_id_and_action(user.id, 'register')
+ assert token
+ end
+
+ def test_login_with_openid_with_new_user_created_with_manual_activation
+ Setting.self_registration = '2'
+ post :login, :openid_url => 'http://openid.example.com/good_user'
+ assert_redirected_to '/login'
+ user = User.find_by_login('cool_user')
+ assert user
+ assert_equal User::STATUS_REGISTERED, user.status
+ end
+
+ def test_login_with_openid_with_new_user_with_conflict_should_register
+ Setting.self_registration = '3'
+ existing_user = User.new(:firstname => 'Cool', :lastname => 'User', :mail => 'user@somedomain.com')
+ existing_user.login = 'cool_user'
+ assert existing_user.save!
+
+ post :login, :openid_url => 'http://openid.example.com/good_user'
+ assert_response :success
+ assert_template 'register'
+ assert assigns(:user)
+ assert_equal 'http://openid.example.com/good_user', assigns(:user)[:identity_url]
+ end
+
+ def test_login_with_openid_with_new_user_with_missing_information_should_register
+ Setting.self_registration = '3'
+
+ post :login, :openid_url => 'http://openid.example.com/good_blank_user'
+ assert_response :success
+ assert_template 'register'
+ assert assigns(:user)
+ assert_equal 'http://openid.example.com/good_blank_user', assigns(:user)[:identity_url]
+
+ assert_select 'input[name=?]', 'user[login]'
+ assert_select 'input[name=?]', 'user[password]'
+ assert_select 'input[name=?]', 'user[password_confirmation]'
+ assert_select 'input[name=?][value=?]', 'user[identity_url]', 'http://openid.example.com/good_blank_user'
+ end
+
+ def test_register_after_login_failure_should_not_require_user_to_enter_a_password
+ Setting.self_registration = '3'
+
+ assert_difference 'User.count' do
+ post :register, :user => {
+ :login => 'good_blank_user',
+ :password => '',
+ :password_confirmation => '',
+ :firstname => 'Cool',
+ :lastname => 'User',
+ :mail => 'user@somedomain.com',
+ :identity_url => 'http://openid.example.com/good_blank_user'
+ }
+ assert_response 302
+ end
+
+ user = User.first(:order => 'id DESC')
+ assert_equal 'http://openid.example.com/good_blank_user', user.identity_url
+ assert user.hashed_password.blank?, "Hashed password was #{user.hashed_password}"
+ end
+
+ def test_setting_openid_should_return_true_when_set_to_true
+ assert_equal true, Setting.openid?
+ end
+
+ else
+ puts "Skipping openid tests."
+
+ def test_dummy
+ end
+ end
+end
diff --git a/test/functional/account_controller_test.rb b/test/functional/account_controller_test.rb
new file mode 100644
index 000000000..0f9ae8eed
--- /dev/null
+++ b/test/functional/account_controller_test.rb
@@ -0,0 +1,277 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class AccountControllerTest < ActionController::TestCase
+ fixtures :users, :roles
+
+ def setup
+ User.current = nil
+ end
+
+ def test_get_login
+ get :login
+ assert_response :success
+ assert_template 'login'
+
+ assert_select 'input[name=username]'
+ assert_select 'input[name=password]'
+ end
+
+ def test_get_login_while_logged_in_should_redirect_to_home
+ @request.session[:user_id] = 2
+
+ get :login
+ assert_redirected_to '/'
+ assert_equal 2, @request.session[:user_id]
+ end
+
+ def test_login_should_redirect_to_back_url_param
+ # request.uri is "test.host" in test environment
+ post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.host/issues/show/1'
+ assert_redirected_to '/issues/show/1'
+ end
+
+ def test_login_should_not_redirect_to_another_host
+ post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.foo/fake'
+ assert_redirected_to '/my/page'
+ end
+
+ def test_login_with_wrong_password
+ post :login, :username => 'admin', :password => 'bad'
+ assert_response :success
+ assert_template 'login'
+
+ assert_select 'div.flash.error', :text => /Invalid user or password/
+ assert_select 'input[name=username][value=admin]'
+ assert_select 'input[name=password]'
+ assert_select 'input[name=password][value]', 0
+ end
+
+ def test_login_should_rescue_auth_source_exception
+ source = AuthSource.create!(:name => 'Test')
+ User.find(2).update_attribute :auth_source_id, source.id
+ AuthSource.any_instance.stubs(:authenticate).raises(AuthSourceException.new("Something wrong"))
+
+ post :login, :username => 'jsmith', :password => 'jsmith'
+ assert_response 500
+ assert_error_tag :content => /Something wrong/
+ end
+
+ def test_login_should_reset_session
+ @controller.expects(:reset_session).once
+
+ post :login, :username => 'jsmith', :password => 'jsmith'
+ assert_response 302
+ end
+
+ def test_get_logout_should_not_logout
+ @request.session[:user_id] = 2
+ get :logout
+ assert_response :success
+ assert_template 'logout'
+
+ assert_equal 2, @request.session[:user_id]
+ end
+
+ def test_logout
+ @request.session[:user_id] = 2
+ post :logout
+ assert_redirected_to '/'
+ assert_nil @request.session[:user_id]
+ end
+
+ def test_logout_should_reset_session
+ @controller.expects(:reset_session).once
+
+ @request.session[:user_id] = 2
+ post :logout
+ assert_response 302
+ end
+
+ def test_get_register_with_registration_on
+ with_settings :self_registration => '3' do
+ get :register
+ assert_response :success
+ assert_template 'register'
+ assert_not_nil assigns(:user)
+
+ assert_select 'input[name=?]', 'user[password]'
+ assert_select 'input[name=?]', 'user[password_confirmation]'
+ end
+ end
+
+ def test_get_register_should_detect_user_language
+ with_settings :self_registration => '3' do
+ @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'
+ get :register
+ assert_response :success
+ assert_not_nil assigns(:user)
+ assert_equal 'fr', assigns(:user).language
+ assert_select 'select[name=?]', 'user[language]' do
+ assert_select 'option[value=fr][selected=selected]'
+ end
+ end
+ end
+
+ def test_get_register_with_registration_off_should_redirect
+ with_settings :self_registration => '0' do
+ get :register
+ assert_redirected_to '/'
+ end
+ end
+
+ # See integration/account_test.rb for the full test
+ def test_post_register_with_registration_on
+ with_settings :self_registration => '3' do
+ assert_difference 'User.count' do
+ post :register, :user => {
+ :login => 'register',
+ :password => 'secret123',
+ :password_confirmation => 'secret123',
+ :firstname => 'John',
+ :lastname => 'Doe',
+ :mail => 'register@example.com'
+ }
+ assert_redirected_to '/my/account'
+ end
+ user = User.first(:order => 'id DESC')
+ assert_equal 'register', user.login
+ assert_equal 'John', user.firstname
+ assert_equal 'Doe', user.lastname
+ assert_equal 'register@example.com', user.mail
+ assert user.check_password?('secret123')
+ assert user.active?
+ end
+ end
+
+ def test_post_register_with_registration_off_should_redirect
+ with_settings :self_registration => '0' do
+ assert_no_difference 'User.count' do
+ post :register, :user => {
+ :login => 'register',
+ :password => 'test',
+ :password_confirmation => 'test',
+ :firstname => 'John',
+ :lastname => 'Doe',
+ :mail => 'register@example.com'
+ }
+ assert_redirected_to '/'
+ end
+ end
+ end
+
+ def test_get_lost_password_should_display_lost_password_form
+ get :lost_password
+ assert_response :success
+ assert_select 'input[name=mail]'
+ end
+
+ def test_lost_password_for_active_user_should_create_a_token
+ Token.delete_all
+ ActionMailer::Base.deliveries.clear
+ assert_difference 'ActionMailer::Base.deliveries.size' do
+ assert_difference 'Token.count' do
+ with_settings :host_name => 'mydomain.foo', :protocol => 'http' do
+ post :lost_password, :mail => 'JSmith@somenet.foo'
+ assert_redirected_to '/login'
+ end
+ end
+ end
+
+ token = Token.order('id DESC').first
+ assert_equal User.find(2), token.user
+ assert_equal 'recovery', token.action
+
+ assert_select_email do
+ assert_select "a[href=?]", "http://mydomain.foo/account/lost_password?token=#{token.value}"
+ end
+ end
+
+ def test_lost_password_for_unknown_user_should_fail
+ Token.delete_all
+ assert_no_difference 'Token.count' do
+ post :lost_password, :mail => 'invalid@somenet.foo'
+ assert_response :success
+ end
+ end
+
+ def test_lost_password_for_non_active_user_should_fail
+ Token.delete_all
+ assert User.find(2).lock!
+
+ assert_no_difference 'Token.count' do
+ post :lost_password, :mail => 'JSmith@somenet.foo'
+ assert_response :success
+ end
+ end
+
+ def test_get_lost_password_with_token_should_display_the_password_recovery_form
+ user = User.find(2)
+ token = Token.create!(:action => 'recovery', :user => user)
+
+ get :lost_password, :token => token.value
+ assert_response :success
+ assert_template 'password_recovery'
+
+ assert_select 'input[type=hidden][name=token][value=?]', token.value
+ end
+
+ def test_get_lost_password_with_invalid_token_should_redirect
+ get :lost_password, :token => "abcdef"
+ assert_redirected_to '/'
+ end
+
+ def test_post_lost_password_with_token_should_change_the_user_password
+ user = User.find(2)
+ token = Token.create!(:action => 'recovery', :user => user)
+
+ post :lost_password, :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123'
+ assert_redirected_to '/login'
+ user.reload
+ assert user.check_password?('newpass123')
+ assert_nil Token.find_by_id(token.id), "Token was not deleted"
+ end
+
+ def test_post_lost_password_with_token_for_non_active_user_should_fail
+ user = User.find(2)
+ token = Token.create!(:action => 'recovery', :user => user)
+ user.lock!
+
+ post :lost_password, :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123'
+ assert_redirected_to '/'
+ assert ! user.check_password?('newpass123')
+ end
+
+ def test_post_lost_password_with_token_and_password_confirmation_failure_should_redisplay_the_form
+ user = User.find(2)
+ token = Token.create!(:action => 'recovery', :user => user)
+
+ post :lost_password, :token => token.value, :new_password => 'newpass', :new_password_confirmation => 'wrongpass'
+ assert_response :success
+ assert_template 'password_recovery'
+ assert_not_nil Token.find_by_id(token.id), "Token was deleted"
+
+ assert_select 'input[type=hidden][name=token][value=?]', token.value
+ end
+
+ def test_post_lost_password_with_invalid_token_should_redirect
+ post :lost_password, :token => "abcdef", :new_password => 'newpass', :new_password_confirmation => 'newpass'
+ assert_redirected_to '/'
+ end
+end
diff --git a/test/functional/activities_controller_test.rb b/test/functional/activities_controller_test.rb
new file mode 100644
index 000000000..4866e227a
--- /dev/null
+++ b/test/functional/activities_controller_test.rb
@@ -0,0 +1,150 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class ActivitiesControllerTest < ActionController::TestCase
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :groups_users,
+ :enabled_modules,
+ :journals, :journal_details
+
+
+ def test_project_index
+ get :index, :id => 1, :with_subprojects => 0
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:events_by_day)
+
+ assert_select 'h3', :text => /#{2.days.ago.to_date.day}/
+ assert_select 'dl dt.issue-edit a', :text => /(#{IssueStatus.find(2).name})/
+ end
+
+ def test_project_index_with_invalid_project_id_should_respond_404
+ get :index, :id => 299
+ assert_response 404
+ end
+
+ def test_previous_project_index
+ get :index, :id => 1, :from => 2.days.ago.to_date
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:events_by_day)
+
+ assert_select 'h3', :text => /#{3.days.ago.to_date.day}/
+ assert_select 'dl dt.issue a', :text => /Can't print recipes/
+ end
+
+ def test_global_index
+ @request.session[:user_id] = 1
+ get :index
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:events_by_day)
+
+ i5 = Issue.find(5)
+ d5 = User.find(1).time_to_date(i5.created_on)
+
+ assert_select 'h3', :text => /#{d5.day}/
+ assert_select 'dl dt.issue a', :text => /Subproject issue/
+ end
+
+ def test_user_index
+ @request.session[:user_id] = 1
+ get :index, :user_id => 2
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:events_by_day)
+
+ assert_select 'h2 a[href=/users/2]', :text => 'John Smith'
+
+ i1 = Issue.find(1)
+ d1 = User.find(1).time_to_date(i1.created_on)
+
+ assert_select 'h3', :text => /#{d1.day}/
+ assert_select 'dl dt.issue a', :text => /Can't print recipes/
+ end
+
+ def test_user_index_with_invalid_user_id_should_respond_404
+ get :index, :user_id => 299
+ assert_response 404
+ end
+
+ def test_index_atom_feed
+ get :index, :format => 'atom', :with_subprojects => 0
+ assert_response :success
+ assert_template 'common/feed'
+
+ assert_select 'feed' do
+ assert_select 'link[rel=self][href=?]', 'http://test.host/activity.atom?with_subprojects=0'
+ assert_select 'link[rel=alternate][href=?]', 'http://test.host/activity?with_subprojects=0'
+ assert_select 'entry' do
+ assert_select 'link[href=?]', 'http://test.host/issues/11'
+ end
+ end
+ end
+
+ def test_index_atom_feed_with_explicit_selection
+ get :index, :format => 'atom', :with_subprojects => 0,
+ :show_changesets => 1,
+ :show_documents => 1,
+ :show_files => 1,
+ :show_issues => 1,
+ :show_messages => 1,
+ :show_news => 1,
+ :show_time_entries => 1,
+ :show_wiki_edits => 1
+
+ assert_response :success
+ assert_template 'common/feed'
+
+ assert_select 'feed' do
+ assert_select 'link[rel=self][href=?]', 'http://test.host/activity.atom?show_changesets=1&show_documents=1&show_files=1&show_issues=1&show_messages=1&show_news=1&show_time_entries=1&show_wiki_edits=1&with_subprojects=0'
+ assert_select 'link[rel=alternate][href=?]', 'http://test.host/activity?show_changesets=1&show_documents=1&show_files=1&show_issues=1&show_messages=1&show_news=1&show_time_entries=1&show_wiki_edits=1&with_subprojects=0'
+ assert_select 'entry' do
+ assert_select 'link[href=?]', 'http://test.host/issues/11'
+ end
+ end
+ end
+
+ def test_index_atom_feed_with_one_item_type
+ get :index, :format => 'atom', :show_issues => '1'
+ assert_response :success
+ assert_template 'common/feed'
+
+ assert_select 'title', :text => /Issues/
+ end
+
+ def test_index_should_show_private_notes_with_permission_only
+ journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Private notes with searchkeyword', :private_notes => true)
+ @request.session[:user_id] = 2
+
+ get :index
+ assert_response :success
+ assert_include journal, assigns(:events_by_day).values.flatten
+
+ Role.find(1).remove_permission! :view_private_notes
+ get :index
+ assert_response :success
+ assert_not_include journal, assigns(:events_by_day).values.flatten
+ end
+end
diff --git a/test/functional/admin_controller_test.rb b/test/functional/admin_controller_test.rb
new file mode 100644
index 000000000..794775c0c
--- /dev/null
+++ b/test/functional/admin_controller_test.rb
@@ -0,0 +1,167 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class AdminControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles
+
+ def setup
+ User.current = nil
+ @request.session[:user_id] = 1 # admin
+ end
+
+ def test_index
+ get :index
+ assert_select 'div.nodata', 0
+ end
+
+ def test_index_with_no_configuration_data
+ delete_configuration_data
+ get :index
+ assert_select 'div.nodata'
+ end
+
+ def test_projects
+ get :projects
+ assert_response :success
+ assert_template 'projects'
+ assert_not_nil assigns(:projects)
+ # active projects only
+ assert_nil assigns(:projects).detect {|u| !u.active?}
+ end
+
+ def test_projects_with_status_filter
+ get :projects, :status => 1
+ assert_response :success
+ assert_template 'projects'
+ assert_not_nil assigns(:projects)
+ # active projects only
+ assert_nil assigns(:projects).detect {|u| !u.active?}
+ end
+
+ def test_projects_with_name_filter
+ get :projects, :name => 'store', :status => ''
+ assert_response :success
+ assert_template 'projects'
+ projects = assigns(:projects)
+ assert_not_nil projects
+ assert_equal 1, projects.size
+ assert_equal 'OnlineStore', projects.first.name
+ end
+
+ def test_load_default_configuration_data
+ delete_configuration_data
+ post :default_configuration, :lang => 'fr'
+ assert_response :redirect
+ assert_nil flash[:error]
+ assert IssueStatus.find_by_name('Nouveau')
+ end
+
+ def test_load_default_configuration_data_should_rescue_error
+ delete_configuration_data
+ Redmine::DefaultData::Loader.stubs(:load).raises(Exception.new("Something went wrong"))
+ post :default_configuration, :lang => 'fr'
+ assert_response :redirect
+ assert_not_nil flash[:error]
+ assert_match /Something went wrong/, flash[:error]
+ end
+
+ def test_test_email
+ user = User.find(1)
+ user.pref.no_self_notified = '1'
+ user.pref.save!
+ ActionMailer::Base.deliveries.clear
+
+ get :test_email
+ assert_redirected_to '/settings?tab=notifications'
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ user = User.find(1)
+ assert_equal [user.mail], mail.bcc
+ end
+
+ def test_test_email_failure_should_display_the_error
+ Mailer.stubs(:test_email).raises(Exception, 'Some error message')
+ get :test_email
+ assert_redirected_to '/settings?tab=notifications'
+ assert_match /Some error message/, flash[:error]
+ end
+
+ def test_no_plugins
+ Redmine::Plugin.clear
+
+ get :plugins
+ assert_response :success
+ assert_template 'plugins'
+ end
+
+ def test_plugins
+ # Register a few plugins
+ Redmine::Plugin.register :foo do
+ name 'Foo plugin'
+ author 'John Smith'
+ description 'This is a test plugin'
+ version '0.0.1'
+ settings :default => {'sample_setting' => 'value', 'foo'=>'bar'}, :partial => 'foo/settings'
+ end
+ Redmine::Plugin.register :bar do
+ end
+
+ get :plugins
+ assert_response :success
+ assert_template 'plugins'
+
+ assert_select 'tr#plugin-foo' do
+ assert_select 'td span.name', :text => 'Foo plugin'
+ assert_select 'td.configure a[href=/settings/plugin/foo]'
+ end
+ assert_select 'tr#plugin-bar' do
+ assert_select 'td span.name', :text => 'Bar'
+ assert_select 'td.configure a', 0
+ end
+ end
+
+ def test_info
+ get :info
+ assert_response :success
+ assert_template 'info'
+ end
+
+ def test_admin_menu_plugin_extension
+ Redmine::MenuManager.map :admin_menu do |menu|
+ menu.push :test_admin_menu_plugin_extension, '/foo/bar', :caption => 'Test'
+ end
+
+ get :index
+ assert_response :success
+ assert_select 'div#admin-menu a[href=/foo/bar]', :text => 'Test'
+
+ Redmine::MenuManager.map :admin_menu do |menu|
+ menu.delete :test_admin_menu_plugin_extension
+ end
+ end
+
+ private
+
+ def delete_configuration_data
+ Role.delete_all('builtin = 0')
+ Tracker.delete_all
+ IssueStatus.delete_all
+ Enumeration.delete_all
+ end
+end
diff --git a/test/functional/attachments_controller_test.rb b/test/functional/attachments_controller_test.rb
new file mode 100644
index 000000000..44abadd10
--- /dev/null
+++ b/test/functional/attachments_controller_test.rb
@@ -0,0 +1,385 @@
+# encoding: utf-8
+#
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class AttachmentsControllerTest < ActionController::TestCase
+ fixtures :users, :projects, :roles, :members, :member_roles,
+ :enabled_modules, :issues, :trackers, :attachments,
+ :versions, :wiki_pages, :wikis, :documents
+
+ def setup
+ User.current = nil
+ set_fixtures_attachments_directory
+ end
+
+ def teardown
+ set_tmp_attachments_directory
+ end
+
+ def test_show_diff
+ ['inline', 'sbs'].each do |dt|
+ # 060719210727_changeset_utf8.diff
+ get :show, :id => 14, :type => dt
+ assert_response :success
+ assert_template 'diff'
+ assert_equal 'text/html', @response.content_type
+ assert_tag 'th',
+ :attributes => {:class => /filename/},
+ :content => /issues_controller.rb\t\(révision 1484\)/
+ assert_tag 'td',
+ :attributes => {:class => /line-code/},
+ :content => /Demande créée avec succès/
+ end
+ set_tmp_attachments_directory
+ end
+
+ def test_show_diff_replace_cannot_convert_content
+ with_settings :repositories_encodings => 'UTF-8' do
+ ['inline', 'sbs'].each do |dt|
+ # 060719210727_changeset_iso8859-1.diff
+ get :show, :id => 5, :type => dt
+ assert_response :success
+ assert_template 'diff'
+ assert_equal 'text/html', @response.content_type
+ assert_tag 'th',
+ :attributes => {:class => "filename"},
+ :content => /issues_controller.rb\t\(r\?vision 1484\)/
+ assert_tag 'td',
+ :attributes => {:class => /line-code/},
+ :content => /Demande cr\?\?e avec succ\?s/
+ end
+ end
+ set_tmp_attachments_directory
+ end
+
+ def test_show_diff_latin_1
+ with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
+ ['inline', 'sbs'].each do |dt|
+ # 060719210727_changeset_iso8859-1.diff
+ get :show, :id => 5, :type => dt
+ assert_response :success
+ assert_template 'diff'
+ assert_equal 'text/html', @response.content_type
+ assert_tag 'th',
+ :attributes => {:class => "filename"},
+ :content => /issues_controller.rb\t\(révision 1484\)/
+ assert_tag 'td',
+ :attributes => {:class => /line-code/},
+ :content => /Demande créée avec succès/
+ end
+ end
+ set_tmp_attachments_directory
+ end
+
+ def test_save_diff_type
+ user1 = User.find(1)
+ user1.pref[:diff_type] = nil
+ user1.preference.save
+ user = User.find(1)
+ assert_nil user.pref[:diff_type]
+
+ @request.session[:user_id] = 1 # admin
+ get :show, :id => 5
+ assert_response :success
+ assert_template 'diff'
+ user.reload
+ assert_equal "inline", user.pref[:diff_type]
+ get :show, :id => 5, :type => 'sbs'
+ assert_response :success
+ assert_template 'diff'
+ user.reload
+ assert_equal "sbs", user.pref[:diff_type]
+ end
+
+ def test_diff_show_filename_in_mercurial_export
+ set_tmp_attachments_directory
+ a = Attachment.new(:container => Issue.find(1),
+ :file => uploaded_test_file("hg-export.diff", "text/plain"),
+ :author => User.find(1))
+ assert a.save
+ assert_equal 'hg-export.diff', a.filename
+
+ get :show, :id => a.id, :type => 'inline'
+ assert_response :success
+ assert_template 'diff'
+ assert_equal 'text/html', @response.content_type
+ assert_select 'th.filename', :text => 'test1.txt'
+ end
+
+ def test_show_text_file
+ get :show, :id => 4
+ assert_response :success
+ assert_template 'file'
+ assert_equal 'text/html', @response.content_type
+ set_tmp_attachments_directory
+ end
+
+ def test_show_text_file_utf_8
+ set_tmp_attachments_directory
+ a = Attachment.new(:container => Issue.find(1),
+ :file => uploaded_test_file("japanese-utf-8.txt", "text/plain"),
+ :author => User.find(1))
+ assert a.save
+ assert_equal 'japanese-utf-8.txt', a.filename
+
+ str_japanese = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"
+ str_japanese.force_encoding('UTF-8') if str_japanese.respond_to?(:force_encoding)
+
+ get :show, :id => a.id
+ assert_response :success
+ assert_template 'file'
+ assert_equal 'text/html', @response.content_type
+ assert_tag :tag => 'th',
+ :content => '1',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td', :content => /#{str_japanese}/ }
+ end
+
+ def test_show_text_file_replace_cannot_convert_content
+ set_tmp_attachments_directory
+ with_settings :repositories_encodings => 'UTF-8' do
+ a = Attachment.new(:container => Issue.find(1),
+ :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
+ :author => User.find(1))
+ assert a.save
+ assert_equal 'iso8859-1.txt', a.filename
+
+ get :show, :id => a.id
+ assert_response :success
+ assert_template 'file'
+ assert_equal 'text/html', @response.content_type
+ assert_tag :tag => 'th',
+ :content => '7',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td', :content => /Demande cr\?\?e avec succ\?s/ }
+ end
+ end
+
+ def test_show_text_file_latin_1
+ set_tmp_attachments_directory
+ with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
+ a = Attachment.new(:container => Issue.find(1),
+ :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
+ :author => User.find(1))
+ assert a.save
+ assert_equal 'iso8859-1.txt', a.filename
+
+ get :show, :id => a.id
+ assert_response :success
+ assert_template 'file'
+ assert_equal 'text/html', @response.content_type
+ assert_tag :tag => 'th',
+ :content => '7',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td', :content => /Demande créée avec succès/ }
+ end
+ end
+
+ def test_show_text_file_should_send_if_too_big
+ Setting.file_max_size_displayed = 512
+ Attachment.find(4).update_attribute :filesize, 754.kilobyte
+
+ get :show, :id => 4
+ assert_response :success
+ assert_equal 'application/x-ruby', @response.content_type
+ set_tmp_attachments_directory
+ end
+
+ def test_show_other
+ get :show, :id => 6
+ assert_response :success
+ assert_equal 'application/octet-stream', @response.content_type
+ set_tmp_attachments_directory
+ end
+
+ def test_show_file_from_private_issue_without_permission
+ get :show, :id => 15
+ assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2F15'
+ set_tmp_attachments_directory
+ end
+
+ def test_show_file_from_private_issue_with_permission
+ @request.session[:user_id] = 2
+ get :show, :id => 15
+ assert_response :success
+ assert_tag 'h2', :content => /private.diff/
+ set_tmp_attachments_directory
+ end
+
+ def test_show_file_without_container_should_be_allowed_to_author
+ set_tmp_attachments_directory
+ attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
+
+ @request.session[:user_id] = 2
+ get :show, :id => attachment.id
+ assert_response 200
+ end
+
+ def test_show_file_without_container_should_be_denied_to_other_users
+ set_tmp_attachments_directory
+ attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
+
+ @request.session[:user_id] = 3
+ get :show, :id => attachment.id
+ assert_response 403
+ end
+
+ def test_show_invalid_should_respond_with_404
+ get :show, :id => 999
+ assert_response 404
+ end
+
+ def test_download_text_file
+ get :download, :id => 4
+ assert_response :success
+ assert_equal 'application/x-ruby', @response.content_type
+ set_tmp_attachments_directory
+ end
+
+ def test_download_version_file_with_issue_tracking_disabled
+ Project.find(1).disable_module! :issue_tracking
+ get :download, :id => 9
+ assert_response :success
+ end
+
+ def test_download_should_assign_content_type_if_blank
+ Attachment.find(4).update_attribute(:content_type, '')
+
+ get :download, :id => 4
+ assert_response :success
+ assert_equal 'text/x-ruby', @response.content_type
+ set_tmp_attachments_directory
+ end
+
+ def test_download_missing_file
+ get :download, :id => 2
+ assert_response 404
+ set_tmp_attachments_directory
+ end
+
+ def test_download_should_be_denied_without_permission
+ get :download, :id => 7
+ assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7'
+ set_tmp_attachments_directory
+ end
+
+ if convert_installed?
+ def test_thumbnail
+ Attachment.clear_thumbnails
+ @request.session[:user_id] = 2
+
+ get :thumbnail, :id => 16
+ assert_response :success
+ assert_equal 'image/png', response.content_type
+ end
+
+ def test_thumbnail_should_not_exceed_maximum_size
+ Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 800}
+
+ @request.session[:user_id] = 2
+ get :thumbnail, :id => 16, :size => 2000
+ end
+
+ def test_thumbnail_should_round_size
+ Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 250}
+
+ @request.session[:user_id] = 2
+ get :thumbnail, :id => 16, :size => 260
+ end
+
+ def test_thumbnail_should_return_404_for_non_image_attachment
+ @request.session[:user_id] = 2
+
+ get :thumbnail, :id => 15
+ assert_response 404
+ end
+
+ def test_thumbnail_should_return_404_if_thumbnail_generation_failed
+ Attachment.any_instance.stubs(:thumbnail).returns(nil)
+ @request.session[:user_id] = 2
+
+ get :thumbnail, :id => 16
+ assert_response 404
+ end
+
+ def test_thumbnail_should_be_denied_without_permission
+ get :thumbnail, :id => 16
+ assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fthumbnail%2F16'
+ end
+ else
+ puts '(ImageMagick convert not available)'
+ end
+
+ def test_destroy_issue_attachment
+ set_tmp_attachments_directory
+ issue = Issue.find(3)
+ @request.session[:user_id] = 2
+
+ assert_difference 'issue.attachments.count', -1 do
+ assert_difference 'Journal.count' do
+ delete :destroy, :id => 1
+ assert_redirected_to '/projects/ecookbook'
+ end
+ end
+ assert_nil Attachment.find_by_id(1)
+ j = Journal.first(:order => 'id DESC')
+ assert_equal issue, j.journalized
+ assert_equal 'attachment', j.details.first.property
+ assert_equal '1', j.details.first.prop_key
+ assert_equal 'error281.txt', j.details.first.old_value
+ assert_equal User.find(2), j.user
+ end
+
+ def test_destroy_wiki_page_attachment
+ set_tmp_attachments_directory
+ @request.session[:user_id] = 2
+ assert_difference 'Attachment.count', -1 do
+ delete :destroy, :id => 3
+ assert_response 302
+ end
+ end
+
+ def test_destroy_project_attachment
+ set_tmp_attachments_directory
+ @request.session[:user_id] = 2
+ assert_difference 'Attachment.count', -1 do
+ delete :destroy, :id => 8
+ assert_response 302
+ end
+ end
+
+ def test_destroy_version_attachment
+ set_tmp_attachments_directory
+ @request.session[:user_id] = 2
+ assert_difference 'Attachment.count', -1 do
+ delete :destroy, :id => 9
+ assert_response 302
+ end
+ end
+
+ def test_destroy_without_permission
+ set_tmp_attachments_directory
+ assert_no_difference 'Attachment.count' do
+ delete :destroy, :id => 3
+ end
+ assert_response 302
+ assert Attachment.find_by_id(3)
+ end
+end
diff --git a/test/functional/auth_sources_controller_test.rb b/test/functional/auth_sources_controller_test.rb
new file mode 100644
index 000000000..469ae452a
--- /dev/null
+++ b/test/functional/auth_sources_controller_test.rb
@@ -0,0 +1,168 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class AuthSourcesControllerTest < ActionController::TestCase
+ fixtures :users, :auth_sources
+
+ def setup
+ @request.session[:user_id] = 1
+ end
+
+ def test_index
+ get :index
+
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:auth_sources)
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ source = assigns(:auth_source)
+ assert_equal AuthSourceLdap, source.class
+ assert source.new_record?
+
+ assert_select 'form#auth_source_form' do
+ assert_select 'input[name=type][value=AuthSourceLdap]'
+ assert_select 'input[name=?]', 'auth_source[host]'
+ end
+ end
+
+ def test_new_with_invalid_type_should_respond_with_404
+ get :new, :type => 'foo'
+ assert_response 404
+ end
+
+ def test_create
+ assert_difference 'AuthSourceLdap.count' do
+ post :create, :type => 'AuthSourceLdap', :auth_source => {:name => 'Test', :host => '127.0.0.1', :port => '389', :attr_login => 'cn'}
+ assert_redirected_to '/auth_sources'
+ end
+
+ source = AuthSourceLdap.order('id DESC').first
+ assert_equal 'Test', source.name
+ assert_equal '127.0.0.1', source.host
+ assert_equal 389, source.port
+ assert_equal 'cn', source.attr_login
+ end
+
+ def test_create_with_failure
+ assert_no_difference 'AuthSourceLdap.count' do
+ post :create, :type => 'AuthSourceLdap', :auth_source => {:name => 'Test', :host => '', :port => '389', :attr_login => 'cn'}
+ assert_response :success
+ assert_template 'new'
+ end
+ assert_error_tag :content => /host can't be blank/i
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_select 'form#auth_source_form' do
+ assert_select 'input[name=?]', 'auth_source[host]'
+ end
+ end
+
+ def test_edit_should_not_contain_password
+ AuthSource.find(1).update_column :account_password, 'secret'
+
+ get :edit, :id => 1
+ assert_response :success
+ assert_select 'input[value=secret]', 0
+ assert_select 'input[name=dummy_password][value=?]', /x+/
+ end
+
+ def test_edit_invalid_should_respond_with_404
+ get :edit, :id => 99
+ assert_response 404
+ end
+
+ def test_update
+ put :update, :id => 1, :auth_source => {:name => 'Renamed', :host => '192.168.0.10', :port => '389', :attr_login => 'uid'}
+ assert_redirected_to '/auth_sources'
+
+ source = AuthSourceLdap.find(1)
+ assert_equal 'Renamed', source.name
+ assert_equal '192.168.0.10', source.host
+ end
+
+ def test_update_with_failure
+ put :update, :id => 1, :auth_source => {:name => 'Renamed', :host => '', :port => '389', :attr_login => 'uid'}
+ assert_response :success
+ assert_template 'edit'
+ assert_error_tag :content => /host can't be blank/i
+ end
+
+ def test_destroy
+ assert_difference 'AuthSourceLdap.count', -1 do
+ delete :destroy, :id => 1
+ assert_redirected_to '/auth_sources'
+ end
+ end
+
+ def test_destroy_auth_source_in_use
+ User.find(2).update_attribute :auth_source_id, 1
+
+ assert_no_difference 'AuthSourceLdap.count' do
+ delete :destroy, :id => 1
+ assert_redirected_to '/auth_sources'
+ end
+ end
+
+ def test_test_connection
+ AuthSourceLdap.any_instance.stubs(:test_connection).returns(true)
+
+ get :test_connection, :id => 1
+ assert_redirected_to '/auth_sources'
+ assert_not_nil flash[:notice]
+ assert_match /successful/i, flash[:notice]
+ end
+
+ def test_test_connection_with_failure
+ AuthSourceLdap.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError.new("Something went wrong"))
+
+ get :test_connection, :id => 1
+ assert_redirected_to '/auth_sources'
+ assert_not_nil flash[:error]
+ assert_include 'Something went wrong', flash[:error]
+ end
+
+ def test_autocomplete_for_new_user
+ AuthSource.expects(:search).with('foo').returns([
+ {:login => 'foo1', :firstname => 'John', :lastname => 'Smith', :mail => 'foo1@example.net', :auth_source_id => 1},
+ {:login => 'Smith', :firstname => 'John', :lastname => 'Doe', :mail => 'foo2@example.net', :auth_source_id => 1}
+ ])
+
+ get :autocomplete_for_new_user, :term => 'foo'
+ assert_response :success
+ assert_equal 'application/json', response.content_type
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_kind_of Array, json
+ assert_equal 2, json.size
+ assert_equal 'foo1', json.first['value']
+ assert_equal 'foo1 (John Smith)', json.first['label']
+ end
+end
diff --git a/test/functional/boards_controller_test.rb b/test/functional/boards_controller_test.rb
new file mode 100644
index 000000000..910f16e9a
--- /dev/null
+++ b/test/functional/boards_controller_test.rb
@@ -0,0 +1,217 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class BoardsControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules
+
+ def setup
+ User.current = nil
+ end
+
+ def test_index
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:boards)
+ assert_not_nil assigns(:project)
+ end
+
+ def test_index_not_found
+ get :index, :project_id => 97
+ assert_response 404
+ end
+
+ def test_index_should_show_messages_if_only_one_board
+ Project.find(1).boards.slice(1..-1).each(&:destroy)
+
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:topics)
+ end
+
+ def test_show
+ get :show, :project_id => 1, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:board)
+ assert_not_nil assigns(:project)
+ assert_not_nil assigns(:topics)
+ end
+
+ def test_show_should_display_sticky_messages_first
+ Message.update_all(:sticky => 0)
+ Message.update_all({:sticky => 1}, {:id => 1})
+
+ get :show, :project_id => 1, :id => 1
+ assert_response :success
+
+ topics = assigns(:topics)
+ assert_not_nil topics
+ assert topics.size > 1, "topics size was #{topics.size}"
+ assert topics.first.sticky?
+ assert topics.first.updated_on < topics.second.updated_on
+ end
+
+ def test_show_should_display_message_with_last_reply_first
+ Message.update_all(:sticky => 0)
+
+ # Reply to an old topic
+ old_topic = Message.where(:board_id => 1, :parent_id => nil).order('created_on ASC').first
+ reply = Message.new(:board_id => 1, :subject => 'New reply', :content => 'New reply', :author_id => 2)
+ old_topic.children << reply
+
+ get :show, :project_id => 1, :id => 1
+ assert_response :success
+ topics = assigns(:topics)
+ assert_not_nil topics
+ assert_equal old_topic, topics.first
+ end
+
+ def test_show_with_permission_should_display_the_new_message_form
+ @request.session[:user_id] = 2
+ get :show, :project_id => 1, :id => 1
+ assert_response :success
+ assert_template 'show'
+
+ assert_select 'form#message-form' do
+ assert_select 'input[name=?]', 'message[subject]'
+ end
+ end
+
+ def test_show_atom
+ get :show, :project_id => 1, :id => 1, :format => 'atom'
+ assert_response :success
+ assert_template 'common/feed'
+ assert_not_nil assigns(:board)
+ assert_not_nil assigns(:project)
+ assert_not_nil assigns(:messages)
+ end
+
+ def test_show_not_found
+ get :index, :project_id => 1, :id => 97
+ assert_response 404
+ end
+
+ def test_new
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_select 'select[name=?]', 'board[parent_id]' do
+ assert_select 'option', (Project.find(1).boards.size + 1)
+ assert_select 'option[value=]', :text => ''
+ assert_select 'option[value=1]', :text => 'Help'
+ end
+ end
+
+ def test_new_without_project_boards
+ Project.find(1).boards.delete_all
+ @request.session[:user_id] = 2
+
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_select 'select[name=?]', 'board[parent_id]', 0
+ end
+
+ def test_create
+ @request.session[:user_id] = 2
+ assert_difference 'Board.count' do
+ post :create, :project_id => 1, :board => { :name => 'Testing', :description => 'Testing board creation'}
+ end
+ assert_redirected_to '/projects/ecookbook/settings/boards'
+ board = Board.first(:order => 'id DESC')
+ assert_equal 'Testing', board.name
+ assert_equal 'Testing board creation', board.description
+ end
+
+ def test_create_with_parent
+ @request.session[:user_id] = 2
+ assert_difference 'Board.count' do
+ post :create, :project_id => 1, :board => { :name => 'Testing', :description => 'Testing', :parent_id => 2}
+ end
+ assert_redirected_to '/projects/ecookbook/settings/boards'
+ board = Board.first(:order => 'id DESC')
+ assert_equal Board.find(2), board.parent
+ end
+
+ def test_create_with_failure
+ @request.session[:user_id] = 2
+ assert_no_difference 'Board.count' do
+ post :create, :project_id => 1, :board => { :name => '', :description => 'Testing board creation'}
+ end
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_edit
+ @request.session[:user_id] = 2
+ get :edit, :project_id => 1, :id => 2
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_edit_with_parent
+ board = Board.generate!(:project_id => 1, :parent_id => 2)
+ @request.session[:user_id] = 2
+ get :edit, :project_id => 1, :id => board.id
+ assert_response :success
+ assert_template 'edit'
+
+ assert_select 'select[name=?]', 'board[parent_id]' do
+ assert_select 'option[value=2][selected=selected]'
+ end
+ end
+
+ def test_update
+ @request.session[:user_id] = 2
+ assert_no_difference 'Board.count' do
+ put :update, :project_id => 1, :id => 2, :board => { :name => 'Testing', :description => 'Testing board update'}
+ end
+ assert_redirected_to '/projects/ecookbook/settings/boards'
+ assert_equal 'Testing', Board.find(2).name
+ end
+
+ def test_update_position
+ @request.session[:user_id] = 2
+ put :update, :project_id => 1, :id => 2, :board => { :move_to => 'highest'}
+ assert_redirected_to '/projects/ecookbook/settings/boards'
+ board = Board.find(2)
+ assert_equal 1, board.position
+ end
+
+ def test_update_with_failure
+ @request.session[:user_id] = 2
+ put :update, :project_id => 1, :id => 2, :board => { :name => '', :description => 'Testing board update'}
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 2
+ assert_difference 'Board.count', -1 do
+ delete :destroy, :project_id => 1, :id => 2
+ end
+ assert_redirected_to '/projects/ecookbook/settings/boards'
+ assert_nil Board.find_by_id(2)
+ end
+end
diff --git a/test/functional/calendars_controller_test.rb b/test/functional/calendars_controller_test.rb
new file mode 100644
index 000000000..dc302f456
--- /dev/null
+++ b/test/functional/calendars_controller_test.rb
@@ -0,0 +1,84 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class CalendarsControllerTest < ActionController::TestCase
+ fixtures :projects,
+ :trackers,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules
+
+ def test_show
+ get :show, :project_id => 1
+ assert_response :success
+ assert_template 'calendar'
+ assert_not_nil assigns(:calendar)
+ end
+
+ def test_show_should_run_custom_queries
+ @query = IssueQuery.create!(:name => 'Calendar', :is_public => true)
+
+ get :show, :query_id => @query.id
+ assert_response :success
+ end
+
+ def test_cross_project_calendar
+ get :show
+ assert_response :success
+ assert_template 'calendar'
+ assert_not_nil assigns(:calendar)
+ end
+
+ def test_week_number_calculation
+ Setting.start_of_week = 7
+
+ get :show, :month => '1', :year => '2010'
+ assert_response :success
+
+ assert_select 'tr' do
+ assert_select 'td.week-number', :text => '53'
+ assert_select 'td.odd', :text => '27'
+ assert_select 'td.even', :text => '2'
+ end
+
+ assert_select 'tr' do
+ assert_select 'td.week-number', :text => '1'
+ assert_select 'td.odd', :text => '3'
+ assert_select 'td.even', :text => '9'
+ end
+
+ Setting.start_of_week = 1
+ get :show, :month => '1', :year => '2010'
+ assert_response :success
+
+ assert_select 'tr' do
+ assert_select 'td.week-number', :text => '53'
+ assert_select 'td.even', :text => '28'
+ assert_select 'td.even', :text => '3'
+ end
+
+ assert_select 'tr' do
+ assert_select 'td.week-number', :text => '1'
+ assert_select 'td.even', :text => '4'
+ assert_select 'td.even', :text => '10'
+ end
+ end
+end
diff --git a/test/functional/comments_controller_test.rb b/test/functional/comments_controller_test.rb
new file mode 100644
index 000000000..b168e986d
--- /dev/null
+++ b/test/functional/comments_controller_test.rb
@@ -0,0 +1,64 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class CommentsControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news, :comments
+
+ def setup
+ User.current = nil
+ end
+
+ def test_add_comment
+ @request.session[:user_id] = 2
+ post :create, :id => 1, :comment => { :comments => 'This is a test comment' }
+ assert_redirected_to '/news/1'
+
+ comment = News.find(1).comments.last
+ assert_not_nil comment
+ assert_equal 'This is a test comment', comment.comments
+ assert_equal User.find(2), comment.author
+ end
+
+ def test_empty_comment_should_not_be_added
+ @request.session[:user_id] = 2
+ assert_no_difference 'Comment.count' do
+ post :create, :id => 1, :comment => { :comments => '' }
+ assert_response :redirect
+ assert_redirected_to '/news/1'
+ end
+ end
+
+ def test_create_should_be_denied_if_news_is_not_commentable
+ News.any_instance.stubs(:commentable?).returns(false)
+ @request.session[:user_id] = 2
+ assert_no_difference 'Comment.count' do
+ post :create, :id => 1, :comment => { :comments => 'This is a test comment' }
+ assert_response 403
+ end
+ end
+
+ def test_destroy_comment
+ comments_count = News.find(1).comments.size
+ @request.session[:user_id] = 2
+ delete :destroy, :id => 1, :comment_id => 2
+ assert_redirected_to '/news/1'
+ assert_nil Comment.find_by_id(2)
+ assert_equal comments_count - 1, News.find(1).comments.size
+ end
+end
diff --git a/test/functional/context_menus_controller_test.rb b/test/functional/context_menus_controller_test.rb
new file mode 100644
index 000000000..110fd7943
--- /dev/null
+++ b/test/functional/context_menus_controller_test.rb
@@ -0,0 +1,252 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class ContextMenusControllerTest < ActionController::TestCase
+ fixtures :projects,
+ :trackers,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules,
+ :workflows,
+ :journals, :journal_details,
+ :versions,
+ :issues, :issue_statuses, :issue_categories,
+ :users,
+ :enumerations,
+ :time_entries
+
+ def test_context_menu_one_issue
+ @request.session[:user_id] = 2
+ get :issues, :ids => [1]
+ assert_response :success
+ assert_template 'context_menu'
+
+ assert_select 'a.icon-edit[href=?]', '/issues/1/edit', :text => 'Edit'
+ assert_select 'a.icon-copy[href=?]', '/projects/ecookbook/issues/1/copy', :text => 'Copy'
+ assert_select 'a.icon-del[href=?]', '/issues?ids%5B%5D=1', :text => 'Delete'
+
+ # Statuses
+ assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bstatus_id%5D=5', :text => 'Closed'
+ assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bpriority_id%5D=8', :text => 'Immediate'
+ # No inactive priorities
+ assert_select 'a', :text => /Inactive Priority/, :count => 0
+ # Versions
+ assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bfixed_version_id%5D=3', :text => '2.0'
+ assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bfixed_version_id%5D=4', :text => 'eCookbook Subproject 1 - 2.0'
+ # Assignees
+ assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bassigned_to_id%5D=3', :text => 'Dave Lopper'
+ end
+
+ def test_context_menu_one_issue_by_anonymous
+ get :issues, :ids => [1]
+ assert_response :success
+ assert_template 'context_menu'
+ assert_tag :tag => 'a', :content => 'Delete',
+ :attributes => { :href => '#',
+ :class => 'icon-del disabled' }
+ end
+
+ def test_context_menu_multiple_issues_of_same_project
+ @request.session[:user_id] = 2
+ get :issues, :ids => [1, 2]
+ assert_response :success
+ assert_template 'context_menu'
+ assert_not_nil assigns(:issues)
+ assert_equal [1, 2], assigns(:issues).map(&:id).sort
+
+ ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&')
+
+ assert_select 'a.icon-edit[href=?]', "/issues/bulk_edit?#{ids}", :text => 'Edit'
+ assert_select 'a.icon-copy[href=?]', "/issues/bulk_edit?copy=1&#{ids}", :text => 'Copy'
+ assert_select 'a.icon-del[href=?]', "/issues?#{ids}", :text => 'Delete'
+
+ assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bstatus_id%5D=5", :text => 'Closed'
+ assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", :text => 'Immediate'
+ assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bassigned_to_id%5D=3", :text => 'Dave Lopper'
+ end
+
+ def test_context_menu_multiple_issues_of_different_projects
+ @request.session[:user_id] = 2
+ get :issues, :ids => [1, 2, 6]
+ assert_response :success
+ assert_template 'context_menu'
+ assert_not_nil assigns(:issues)
+ assert_equal [1, 2, 6], assigns(:issues).map(&:id).sort
+
+ ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&')
+
+ assert_select 'a.icon-edit[href=?]', "/issues/bulk_edit?#{ids}", :text => 'Edit'
+ assert_select 'a.icon-del[href=?]', "/issues?#{ids}", :text => 'Delete'
+
+ assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bstatus_id%5D=5", :text => 'Closed'
+ assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", :text => 'Immediate'
+ assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bassigned_to_id%5D=2", :text => 'John Smith'
+ end
+
+ def test_context_menu_should_include_list_custom_fields
+ field = IssueCustomField.create!(:name => 'List', :field_format => 'list',
+ :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
+ @request.session[:user_id] = 2
+ get :issues, :ids => [1]
+
+ assert_select "li.cf_#{field.id}" do
+ assert_select 'a[href=#]', :text => 'List'
+ assert_select 'ul' do
+ assert_select 'a', 3
+ assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=Foo", :text => 'Foo'
+ assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
+ end
+ end
+ end
+
+ def test_context_menu_should_not_include_null_value_for_required_custom_fields
+ field = IssueCustomField.create!(:name => 'List', :is_required => true, :field_format => 'list',
+ :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
+ @request.session[:user_id] = 2
+ get :issues, :ids => [1, 2]
+
+ assert_select "li.cf_#{field.id}" do
+ assert_select 'a[href=#]', :text => 'List'
+ assert_select 'ul' do
+ assert_select 'a', 2
+ assert_select 'a', :text => 'none', :count => 0
+ end
+ end
+ end
+
+ def test_context_menu_on_single_issue_should_select_current_custom_field_value
+ field = IssueCustomField.create!(:name => 'List', :field_format => 'list',
+ :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
+ issue = Issue.find(1)
+ issue.custom_field_values = {field.id => 'Bar'}
+ issue.save!
+ @request.session[:user_id] = 2
+ get :issues, :ids => [1]
+
+ assert_select "li.cf_#{field.id}" do
+ assert_select 'a[href=#]', :text => 'List'
+ assert_select 'ul' do
+ assert_select 'a', 3
+ assert_select 'a.icon-checked', :text => 'Bar'
+ end
+ end
+ end
+
+ def test_context_menu_should_include_bool_custom_fields
+ field = IssueCustomField.create!(:name => 'Bool', :field_format => 'bool',
+ :is_for_all => true, :tracker_ids => [1, 2, 3])
+ @request.session[:user_id] = 2
+ get :issues, :ids => [1]
+
+ assert_select "li.cf_#{field.id}" do
+ assert_select 'a[href=#]', :text => 'Bool'
+ assert_select 'ul' do
+ assert_select 'a', 3
+ assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=0", :text => 'No'
+ assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=1", :text => 'Yes'
+ assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
+ end
+ end
+ end
+
+ def test_context_menu_should_include_user_custom_fields
+ field = IssueCustomField.create!(:name => 'User', :field_format => 'user',
+ :is_for_all => true, :tracker_ids => [1, 2, 3])
+ @request.session[:user_id] = 2
+ get :issues, :ids => [1]
+
+ assert_select "li.cf_#{field.id}" do
+ assert_select 'a[href=#]', :text => 'User'
+ assert_select 'ul' do
+ assert_select 'a', Project.find(1).members.count + 1
+ assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=2", :text => 'John Smith'
+ assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
+ end
+ end
+ end
+
+ def test_context_menu_should_include_version_custom_fields
+ field = IssueCustomField.create!(:name => 'Version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1, 2, 3])
+ @request.session[:user_id] = 2
+ get :issues, :ids => [1]
+
+ assert_select "li.cf_#{field.id}" do
+ assert_select 'a[href=#]', :text => 'Version'
+ assert_select 'ul' do
+ assert_select 'a', Project.find(1).shared_versions.count + 1
+ assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=3", :text => '2.0'
+ assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
+ end
+ end
+ end
+
+ def test_context_menu_by_assignable_user_should_include_assigned_to_me_link
+ @request.session[:user_id] = 2
+ get :issues, :ids => [1]
+ assert_response :success
+ assert_template 'context_menu'
+
+ assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bassigned_to_id%5D=2', :text => / me /
+ end
+
+ def test_context_menu_should_propose_shared_versions_for_issues_from_different_projects
+ @request.session[:user_id] = 2
+ version = Version.create!(:name => 'Shared', :sharing => 'system', :project_id => 1)
+
+ get :issues, :ids => [1, 4]
+ assert_response :success
+ assert_template 'context_menu'
+
+ assert_include version, assigns(:versions)
+ assert_select 'a', :text => 'eCookbook - Shared'
+ end
+
+ def test_context_menu_issue_visibility
+ get :issues, :ids => [1, 4]
+ assert_response :success
+ assert_template 'context_menu'
+ assert_equal [1], assigns(:issues).collect(&:id)
+ end
+
+ def test_should_respond_with_404_without_ids
+ get :issues
+ assert_response 404
+ end
+
+ def test_time_entries_context_menu
+ @request.session[:user_id] = 2
+ get :time_entries, :ids => [1, 2]
+ assert_response :success
+ assert_template 'time_entries'
+
+ assert_select 'a:not(.disabled)', :text => 'Edit'
+ end
+
+ def test_time_entries_context_menu_without_edit_permission
+ @request.session[:user_id] = 2
+ Role.find_by_name('Manager').remove_permission! :edit_time_entries
+
+ get :time_entries, :ids => [1, 2]
+ assert_response :success
+ assert_template 'time_entries'
+ assert_select 'a.disabled', :text => 'Edit'
+ end
+end
diff --git a/test/functional/custom_fields_controller_test.rb b/test/functional/custom_fields_controller_test.rb
new file mode 100644
index 000000000..c8193c5d4
--- /dev/null
+++ b/test/functional/custom_fields_controller_test.rb
@@ -0,0 +1,176 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class CustomFieldsControllerTest < ActionController::TestCase
+ fixtures :custom_fields, :custom_values, :trackers, :users
+
+ def setup
+ @request.session[:user_id] = 1
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'index'
+ end
+
+ def test_new
+ custom_field_classes.each do |klass|
+ get :new, :type => klass.name
+ assert_response :success
+ assert_template 'new'
+ assert_kind_of klass, assigns(:custom_field)
+ assert_select 'form#custom_field_form' do
+ assert_select 'select#custom_field_field_format[name=?]', 'custom_field[field_format]'
+ assert_select 'input[type=hidden][name=type][value=?]', klass.name
+ end
+ end
+ end
+
+ def test_new_issue_custom_field
+ get :new, :type => 'IssueCustomField'
+ assert_response :success
+ assert_template 'new'
+ assert_select 'form#custom_field_form' do
+ assert_select 'select#custom_field_field_format[name=?]', 'custom_field[field_format]' do
+ assert_select 'option[value=user]', :text => 'User'
+ assert_select 'option[value=version]', :text => 'Version'
+ end
+ assert_select 'input[type=hidden][name=type][value=IssueCustomField]'
+ end
+ end
+
+ def test_default_value_should_be_an_input_for_string_custom_field
+ get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'string'}
+ assert_response :success
+ assert_select 'input[name=?]', 'custom_field[default_value]'
+ end
+
+ def test_default_value_should_be_a_textarea_for_text_custom_field
+ get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'text'}
+ assert_response :success
+ assert_select 'textarea[name=?]', 'custom_field[default_value]'
+ end
+
+ def test_default_value_should_be_a_checkbox_for_bool_custom_field
+ get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'bool'}
+ assert_response :success
+ assert_select 'input[name=?][type=checkbox]', 'custom_field[default_value]'
+ end
+
+ def test_default_value_should_not_be_present_for_user_custom_field
+ get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'user'}
+ assert_response :success
+ assert_select '[name=?]', 'custom_field[default_value]', 0
+ end
+
+ def test_new_js
+ get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'list'}, :format => 'js'
+ assert_response :success
+ assert_template 'new'
+ assert_equal 'text/javascript', response.content_type
+
+ field = assigns(:custom_field)
+ assert_equal 'list', field.field_format
+ end
+
+ def test_new_with_invalid_custom_field_class_should_render_404
+ get :new, :type => 'UnknownCustomField'
+ assert_response 404
+ end
+
+ def test_create_list_custom_field
+ assert_difference 'CustomField.count' do
+ post :create, :type => "IssueCustomField",
+ :custom_field => {:name => "test_post_new_list",
+ :default_value => "",
+ :min_length => "0",
+ :searchable => "0",
+ :regexp => "",
+ :is_for_all => "1",
+ :possible_values => "0.1\n0.2\n",
+ :max_length => "0",
+ :is_filter => "0",
+ :is_required =>"0",
+ :field_format => "list",
+ :tracker_ids => ["1", ""]}
+ end
+ assert_redirected_to '/custom_fields?tab=IssueCustomField'
+ field = IssueCustomField.find_by_name('test_post_new_list')
+ assert_not_nil field
+ assert_equal ["0.1", "0.2"], field.possible_values
+ assert_equal 1, field.trackers.size
+ end
+
+ def test_create_with_failure
+ assert_no_difference 'CustomField.count' do
+ post :create, :type => "IssueCustomField", :custom_field => {:name => ''}
+ end
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_edit
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ assert_tag 'input', :attributes => {:name => 'custom_field[name]', :value => 'Database'}
+ end
+
+ def test_edit_invalid_custom_field_should_render_404
+ get :edit, :id => 99
+ assert_response 404
+ end
+
+ def test_update
+ put :update, :id => 1, :custom_field => {:name => 'New name'}
+ assert_redirected_to '/custom_fields?tab=IssueCustomField'
+
+ field = CustomField.find(1)
+ assert_equal 'New name', field.name
+ end
+
+ def test_update_with_failure
+ put :update, :id => 1, :custom_field => {:name => ''}
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_destroy
+ custom_values_count = CustomValue.count(:conditions => {:custom_field_id => 1})
+ assert custom_values_count > 0
+
+ assert_difference 'CustomField.count', -1 do
+ assert_difference 'CustomValue.count', - custom_values_count do
+ delete :destroy, :id => 1
+ end
+ end
+
+ assert_redirected_to '/custom_fields?tab=IssueCustomField'
+ assert_nil CustomField.find_by_id(1)
+ assert_nil CustomValue.find_by_custom_field_id(1)
+ end
+
+ def custom_field_classes
+ files = Dir.glob(File.join(Rails.root, 'app/models/*_custom_field.rb')).map {|f| File.basename(f).sub(/\.rb$/, '') }
+ classes = files.map(&:classify).map(&:constantize)
+ assert classes.size > 0
+ classes
+ end
+end
diff --git a/test/functional/documents_controller_test.rb b/test/functional/documents_controller_test.rb
new file mode 100644
index 000000000..7fb7bdaf7
--- /dev/null
+++ b/test/functional/documents_controller_test.rb
@@ -0,0 +1,186 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class DocumentsControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles,
+ :enabled_modules, :documents, :enumerations,
+ :groups_users, :attachments
+
+ def setup
+ User.current = nil
+ end
+
+ def test_index
+ # Sets a default category
+ e = Enumeration.find_by_name('Technical documentation')
+ e.update_attributes(:is_default => true)
+
+ get :index, :project_id => 'ecookbook'
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:grouped)
+
+ # Default category selected in the new document form
+ assert_tag :select, :attributes => {:name => 'document[category_id]'},
+ :child => {:tag => 'option', :attributes => {:selected => 'selected'},
+ :content => 'Technical documentation'}
+
+ assert ! DocumentCategory.find(16).active?
+ assert_no_tag :option, :attributes => {:value => '16'},
+ :parent => {:tag => 'select', :attributes => {:id => 'document_category_id'} }
+ end
+
+ def test_index_grouped_by_date
+ get :index, :project_id => 'ecookbook', :sort_by => 'date'
+ assert_response :success
+ assert_tag 'h3', :content => '2007-02-12'
+ end
+
+ def test_index_grouped_by_title
+ get :index, :project_id => 'ecookbook', :sort_by => 'title'
+ assert_response :success
+ assert_tag 'h3', :content => 'T'
+ end
+
+ def test_index_grouped_by_author
+ get :index, :project_id => 'ecookbook', :sort_by => 'author'
+ assert_response :success
+ assert_tag 'h3', :content => 'John Smith'
+ end
+
+ def test_index_with_long_description
+ # adds a long description to the first document
+ doc = documents(:documents_001)
+ doc.update_attributes(:description => < 'ecookbook'
+ assert_response :success
+ assert_template 'index'
+
+ # should only truncate on new lines to avoid breaking wiki formatting
+ assert_select '.wiki p', :text => (doc.description.split("\n").first + '...')
+ assert_select '.wiki p', :text => Regexp.new(Regexp.escape("EndOfLineHere..."))
+ end
+
+ def test_show
+ get :show, :id => 1
+ assert_response :success
+ assert_template 'show'
+ end
+
+ def test_new
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_create_with_one_attachment
+ ActionMailer::Base.deliveries.clear
+ @request.session[:user_id] = 2
+ set_tmp_attachments_directory
+
+ with_settings :notified_events => %w(document_added) do
+ post :create, :project_id => 'ecookbook',
+ :document => { :title => 'DocumentsControllerTest#test_post_new',
+ :description => 'This is a new document',
+ :category_id => 2},
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
+ end
+ assert_redirected_to '/projects/ecookbook/documents'
+
+ document = Document.find_by_title('DocumentsControllerTest#test_post_new')
+ assert_not_nil document
+ assert_equal Enumeration.find(2), document.category
+ assert_equal 1, document.attachments.size
+ assert_equal 'testfile.txt', document.attachments.first.filename
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_create_with_failure
+ @request.session[:user_id] = 2
+ assert_no_difference 'Document.count' do
+ post :create, :project_id => 'ecookbook', :document => { :title => ''}
+ end
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_create_non_default_category
+ @request.session[:user_id] = 2
+ category2 = Enumeration.find_by_name('User documentation')
+ category2.update_attributes(:is_default => true)
+ category1 = Enumeration.find_by_name('Uncategorized')
+ post :create,
+ :project_id => 'ecookbook',
+ :document => { :title => 'no default',
+ :description => 'This is a new document',
+ :category_id => category1.id }
+ assert_redirected_to '/projects/ecookbook/documents'
+ doc = Document.find_by_title('no default')
+ assert_not_nil doc
+ assert_equal category1.id, doc.category_id
+ assert_equal category1, doc.category
+ end
+
+ def test_edit
+ @request.session[:user_id] = 2
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_update
+ @request.session[:user_id] = 2
+ put :update, :id => 1, :document => {:title => 'test_update'}
+ assert_redirected_to '/documents/1'
+ document = Document.find(1)
+ assert_equal 'test_update', document.title
+ end
+
+ def test_update_with_failure
+ @request.session[:user_id] = 2
+ put :update, :id => 1, :document => {:title => ''}
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 2
+ assert_difference 'Document.count', -1 do
+ delete :destroy, :id => 1
+ end
+ assert_redirected_to '/projects/ecookbook/documents'
+ assert_nil Document.find_by_id(1)
+ end
+
+ def test_add_attachment
+ @request.session[:user_id] = 2
+ assert_difference 'Attachment.count' do
+ post :add_attachment, :id => 1,
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
+ end
+ attachment = Attachment.first(:order => 'id DESC')
+ assert_equal Document.find(1), attachment.container
+ end
+end
diff --git a/test/functional/enumerations_controller_test.rb b/test/functional/enumerations_controller_test.rb
new file mode 100644
index 000000000..8e18ea194
--- /dev/null
+++ b/test/functional/enumerations_controller_test.rb
@@ -0,0 +1,136 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class EnumerationsControllerTest < ActionController::TestCase
+ fixtures :enumerations, :issues, :users
+
+ def setup
+ @request.session[:user_id] = 1 # admin
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'index'
+ end
+
+ def test_index_should_require_admin
+ @request.session[:user_id] = nil
+ get :index
+ assert_response 302
+ end
+
+ def test_new
+ get :new, :type => 'IssuePriority'
+ assert_response :success
+ assert_template 'new'
+ assert_kind_of IssuePriority, assigns(:enumeration)
+ assert_tag 'input', :attributes => {:name => 'enumeration[type]', :value => 'IssuePriority'}
+ assert_tag 'input', :attributes => {:name => 'enumeration[name]'}
+ end
+
+ def test_new_with_invalid_type_should_respond_with_404
+ get :new, :type => 'UnknownType'
+ assert_response 404
+ end
+
+ def test_create
+ assert_difference 'IssuePriority.count' do
+ post :create, :enumeration => {:type => 'IssuePriority', :name => 'Lowest'}
+ end
+ assert_redirected_to '/enumerations'
+ e = IssuePriority.find_by_name('Lowest')
+ assert_not_nil e
+ end
+
+ def test_create_with_failure
+ assert_no_difference 'IssuePriority.count' do
+ post :create, :enumeration => {:type => 'IssuePriority', :name => ''}
+ end
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_edit
+ get :edit, :id => 6
+ assert_response :success
+ assert_template 'edit'
+ assert_tag 'input', :attributes => {:name => 'enumeration[name]', :value => 'High'}
+ end
+
+ def test_edit_invalid_should_respond_with_404
+ get :edit, :id => 999
+ assert_response 404
+ end
+
+ def test_update
+ assert_no_difference 'IssuePriority.count' do
+ put :update, :id => 6, :enumeration => {:type => 'IssuePriority', :name => 'New name'}
+ end
+ assert_redirected_to '/enumerations'
+ e = IssuePriority.find(6)
+ assert_equal 'New name', e.name
+ end
+
+ def test_update_with_failure
+ assert_no_difference 'IssuePriority.count' do
+ put :update, :id => 6, :enumeration => {:type => 'IssuePriority', :name => ''}
+ end
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_destroy_enumeration_not_in_use
+ assert_difference 'IssuePriority.count', -1 do
+ delete :destroy, :id => 7
+ end
+ assert_redirected_to :controller => 'enumerations', :action => 'index'
+ assert_nil Enumeration.find_by_id(7)
+ end
+
+ def test_destroy_enumeration_in_use
+ assert_no_difference 'IssuePriority.count' do
+ delete :destroy, :id => 4
+ end
+ assert_response :success
+ assert_template 'destroy'
+ assert_not_nil Enumeration.find_by_id(4)
+ assert_select 'select[name=reassign_to_id]' do
+ assert_select 'option[value=6]', :text => 'High'
+ end
+ end
+
+ def test_destroy_enumeration_in_use_with_reassignment
+ issue = Issue.where(:priority_id => 4).first
+ assert_difference 'IssuePriority.count', -1 do
+ delete :destroy, :id => 4, :reassign_to_id => 6
+ end
+ assert_redirected_to :controller => 'enumerations', :action => 'index'
+ assert_nil Enumeration.find_by_id(4)
+ # check that the issue was reassign
+ assert_equal 6, issue.reload.priority_id
+ end
+
+ def test_destroy_enumeration_in_use_with_blank_reassignment
+ assert_no_difference 'IssuePriority.count' do
+ delete :destroy, :id => 4, :reassign_to_id => ''
+ end
+ assert_response :success
+ end
+end
diff --git a/test/functional/files_controller_test.rb b/test/functional/files_controller_test.rb
new file mode 100644
index 000000000..0cdb64057
--- /dev/null
+++ b/test/functional/files_controller_test.rb
@@ -0,0 +1,109 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class FilesControllerTest < ActionController::TestCase
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules,
+ :journals, :journal_details,
+ :attachments,
+ :versions
+
+ def setup
+ @request.session[:user_id] = nil
+ Setting.default_language = 'en'
+ end
+
+ def test_index
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:containers)
+
+ # file attached to the project
+ assert_tag :a, :content => 'project_file.zip',
+ :attributes => { :href => '/attachments/download/8/project_file.zip' }
+
+ # file attached to a project's version
+ assert_tag :a, :content => 'version_file.zip',
+ :attributes => { :href => '/attachments/download/9/version_file.zip' }
+ end
+
+ def test_new
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_tag 'select', :attributes => {:name => 'version_id'}
+ end
+
+ def test_new_without_versions
+ Version.delete_all
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_no_tag 'select', :attributes => {:name => 'version_id'}
+ end
+
+ def test_create_file
+ set_tmp_attachments_directory
+ @request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+
+ with_settings :notified_events => %w(file_added) do
+ assert_difference 'Attachment.count' do
+ post :create, :project_id => 1, :version_id => '',
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
+ assert_response :redirect
+ end
+ end
+ assert_redirected_to '/projects/ecookbook/files'
+ a = Attachment.order('created_on DESC').first
+ assert_equal 'testfile.txt', a.filename
+ assert_equal Project.find(1), a.container
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert_equal "[eCookbook] New file", mail.subject
+ assert_mail_body_match 'testfile.txt', mail
+ end
+
+ def test_create_version_file
+ set_tmp_attachments_directory
+ @request.session[:user_id] = 2
+
+ assert_difference 'Attachment.count' do
+ post :create, :project_id => 1, :version_id => '2',
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
+ assert_response :redirect
+ end
+ assert_redirected_to '/projects/ecookbook/files'
+ a = Attachment.order('created_on DESC').first
+ assert_equal 'testfile.txt', a.filename
+ assert_equal Version.find(2), a.container
+ end
+
+end
diff --git a/test/functional/gantts_controller_test.rb b/test/functional/gantts_controller_test.rb
new file mode 100644
index 000000000..858340448
--- /dev/null
+++ b/test/functional/gantts_controller_test.rb
@@ -0,0 +1,122 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class GanttsControllerTest < ActionController::TestCase
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules,
+ :versions
+
+ def test_gantt_should_work
+ i2 = Issue.find(2)
+ i2.update_attribute(:due_date, 1.month.from_now)
+ get :show, :project_id => 1
+ assert_response :success
+ assert_template 'gantts/show'
+ assert_not_nil assigns(:gantt)
+ # Issue with start and due dates
+ i = Issue.find(1)
+ assert_not_nil i.due_date
+ assert_select "div a.issue", /##{i.id}/
+ # Issue with on a targeted version should not be in the events but loaded in the html
+ i = Issue.find(2)
+ assert_select "div a.issue", /##{i.id}/
+ end
+
+ def test_gantt_should_work_without_issue_due_dates
+ Issue.update_all("due_date = NULL")
+ get :show, :project_id => 1
+ assert_response :success
+ assert_template 'gantts/show'
+ assert_not_nil assigns(:gantt)
+ end
+
+ def test_gantt_should_work_without_issue_and_version_due_dates
+ Issue.update_all("due_date = NULL")
+ Version.update_all("effective_date = NULL")
+ get :show, :project_id => 1
+ assert_response :success
+ assert_template 'gantts/show'
+ assert_not_nil assigns(:gantt)
+ end
+
+ def test_gantt_should_work_cross_project
+ get :show
+ assert_response :success
+ assert_template 'gantts/show'
+ assert_not_nil assigns(:gantt)
+ assert_not_nil assigns(:gantt).query
+ assert_nil assigns(:gantt).project
+ end
+
+ def test_gantt_should_not_disclose_private_projects
+ get :show
+ assert_response :success
+ assert_template 'gantts/show'
+ assert_tag 'a', :content => /eCookbook/
+ # Root private project
+ assert_no_tag 'a', {:content => /OnlineStore/}
+ # Private children of a public project
+ assert_no_tag 'a', :content => /Private child of eCookbook/
+ end
+
+ def test_gantt_should_display_relations
+ IssueRelation.delete_all
+ issue1 = Issue.generate!(:start_date => 1.day.from_now, :due_date => 3.day.from_now)
+ issue2 = Issue.generate!(:start_date => 1.day.from_now, :due_date => 3.day.from_now)
+ IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => 'precedes')
+
+ get :show
+ assert_response :success
+
+ relations = assigns(:gantt).relations
+ assert_kind_of Hash, relations
+ assert relations.present?
+ assert_select 'div.task_todo[id=?][data-rels*=?]', "task-todo-issue-#{issue1.id}", issue2.id.to_s
+ assert_select 'div.task_todo[id=?]:not([data-rels])', "task-todo-issue-#{issue2.id}"
+ end
+
+ def test_gantt_should_export_to_pdf
+ get :show, :project_id => 1, :format => 'pdf'
+ assert_response :success
+ assert_equal 'application/pdf', @response.content_type
+ assert @response.body.starts_with?('%PDF')
+ assert_not_nil assigns(:gantt)
+ end
+
+ def test_gantt_should_export_to_pdf_cross_project
+ get :show, :format => 'pdf'
+ assert_response :success
+ assert_equal 'application/pdf', @response.content_type
+ assert @response.body.starts_with?('%PDF')
+ assert_not_nil assigns(:gantt)
+ end
+
+ if Object.const_defined?(:Magick)
+ def test_gantt_should_export_to_png
+ get :show, :project_id => 1, :format => 'png'
+ assert_response :success
+ assert_equal 'image/png', @response.content_type
+ end
+ end
+end
diff --git a/test/functional/groups_controller_test.rb b/test/functional/groups_controller_test.rb
new file mode 100644
index 000000000..67edb9261
--- /dev/null
+++ b/test/functional/groups_controller_test.rb
@@ -0,0 +1,202 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class GroupsControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :members, :member_roles, :roles, :groups_users
+
+ def setup
+ @request.session[:user_id] = 1
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'index'
+ end
+
+ def test_show
+ get :show, :id => 10
+ assert_response :success
+ assert_template 'show'
+ end
+
+ def test_show_invalid_should_return_404
+ get :show, :id => 99
+ assert_response 404
+ end
+
+ def test_new
+ get :new
+ assert_response :success
+ assert_template 'new'
+ assert_select 'input[name=?]', 'group[name]'
+ end
+
+ def test_create
+ assert_difference 'Group.count' do
+ post :create, :group => {:name => 'New group'}
+ end
+ assert_redirected_to '/groups'
+ group = Group.first(:order => 'id DESC')
+ assert_equal 'New group', group.name
+ assert_equal [], group.users
+ end
+
+ def test_create_and_continue
+ assert_difference 'Group.count' do
+ post :create, :group => {:name => 'New group'}, :continue => 'Create and continue'
+ end
+ assert_redirected_to '/groups/new'
+ group = Group.first(:order => 'id DESC')
+ assert_equal 'New group', group.name
+ end
+
+ def test_create_with_failure
+ assert_no_difference 'Group.count' do
+ post :create, :group => {:name => ''}
+ end
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_edit
+ get :edit, :id => 10
+ assert_response :success
+ assert_template 'edit'
+
+ assert_select 'div#tab-content-users'
+ assert_select 'div#tab-content-memberships' do
+ assert_select 'a', :text => 'Private child of eCookbook'
+ end
+ end
+
+ def test_update
+ new_name = 'New name'
+ put :update, :id => 10, :group => {:name => new_name}
+ assert_redirected_to '/groups'
+ group = Group.find(10)
+ assert_equal new_name, group.name
+ end
+
+ def test_update_with_failure
+ put :update, :id => 10, :group => {:name => ''}
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_destroy
+ assert_difference 'Group.count', -1 do
+ post :destroy, :id => 10
+ end
+ assert_redirected_to '/groups'
+ end
+
+ def test_add_users
+ assert_difference 'Group.find(10).users.count', 2 do
+ post :add_users, :id => 10, :user_ids => ['2', '3']
+ end
+ end
+
+ def test_xhr_add_users
+ assert_difference 'Group.find(10).users.count', 2 do
+ xhr :post, :add_users, :id => 10, :user_ids => ['2', '3']
+ assert_response :success
+ assert_template 'add_users'
+ assert_equal 'text/javascript', response.content_type
+ end
+ assert_match /John Smith/, response.body
+ end
+
+ def test_remove_user
+ assert_difference 'Group.find(10).users.count', -1 do
+ delete :remove_user, :id => 10, :user_id => '8'
+ end
+ end
+
+ def test_xhr_remove_user
+ assert_difference 'Group.find(10).users.count', -1 do
+ xhr :delete, :remove_user, :id => 10, :user_id => '8'
+ assert_response :success
+ assert_template 'remove_user'
+ assert_equal 'text/javascript', response.content_type
+ end
+ end
+
+ def test_new_membership
+ assert_difference 'Group.find(10).members.count' do
+ post :edit_membership, :id => 10, :membership => { :project_id => 2, :role_ids => ['1', '2']}
+ end
+ end
+
+ def test_xhr_new_membership
+ assert_difference 'Group.find(10).members.count' do
+ xhr :post, :edit_membership, :id => 10, :membership => { :project_id => 2, :role_ids => ['1', '2']}
+ assert_response :success
+ assert_template 'edit_membership'
+ assert_equal 'text/javascript', response.content_type
+ end
+ assert_match /OnlineStore/, response.body
+ end
+
+ def test_xhr_new_membership_with_failure
+ assert_no_difference 'Group.find(10).members.count' do
+ xhr :post, :edit_membership, :id => 10, :membership => { :project_id => 999, :role_ids => ['1', '2']}
+ assert_response :success
+ assert_template 'edit_membership'
+ assert_equal 'text/javascript', response.content_type
+ end
+ assert_match /alert/, response.body, "Alert message not sent"
+ end
+
+ def test_edit_membership
+ assert_no_difference 'Group.find(10).members.count' do
+ post :edit_membership, :id => 10, :membership_id => 6, :membership => { :role_ids => ['1', '3']}
+ end
+ end
+
+ def test_xhr_edit_membership
+ assert_no_difference 'Group.find(10).members.count' do
+ xhr :post, :edit_membership, :id => 10, :membership_id => 6, :membership => { :role_ids => ['1', '3']}
+ assert_response :success
+ assert_template 'edit_membership'
+ assert_equal 'text/javascript', response.content_type
+ end
+ end
+
+ def test_destroy_membership
+ assert_difference 'Group.find(10).members.count', -1 do
+ post :destroy_membership, :id => 10, :membership_id => 6
+ end
+ end
+
+ def test_xhr_destroy_membership
+ assert_difference 'Group.find(10).members.count', -1 do
+ xhr :post, :destroy_membership, :id => 10, :membership_id => 6
+ assert_response :success
+ assert_template 'destroy_membership'
+ assert_equal 'text/javascript', response.content_type
+ end
+ end
+
+ def test_autocomplete_for_user
+ get :autocomplete_for_user, :id => 10, :q => 'smi', :format => 'js'
+ assert_response :success
+ assert_include 'John Smith', response.body
+ end
+end
diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb
new file mode 100644
index 000000000..59f7a858a
--- /dev/null
+++ b/test/functional/issues_controller_test.rb
@@ -0,0 +1,3898 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class IssuesControllerTest < ActionController::TestCase
+ fixtures :projects,
+ :users,
+ :roles,
+ :members,
+ :member_roles,
+ :issues,
+ :issue_statuses,
+ :versions,
+ :trackers,
+ :projects_trackers,
+ :issue_categories,
+ :enabled_modules,
+ :enumerations,
+ :attachments,
+ :workflows,
+ :custom_fields,
+ :custom_values,
+ :custom_fields_projects,
+ :custom_fields_trackers,
+ :time_entries,
+ :journals,
+ :journal_details,
+ :queries,
+ :repositories,
+ :changesets
+
+ include Redmine::I18n
+
+ def setup
+ User.current = nil
+ end
+
+ def test_index
+ with_settings :default_language => "en" do
+ get :index
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:issues)
+ assert_nil assigns(:project)
+
+ # links to visible issues
+ assert_select 'a[href=/issues/1]', :text => /Can't print recipes/
+ assert_select 'a[href=/issues/5]', :text => /Subproject issue/
+ # private projects hidden
+ assert_select 'a[href=/issues/6]', 0
+ assert_select 'a[href=/issues/4]', 0
+ # project column
+ assert_select 'th', :text => /Project/
+ end
+ end
+
+ def test_index_should_not_list_issues_when_module_disabled
+ EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
+ get :index
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:issues)
+ assert_nil assigns(:project)
+
+ assert_select 'a[href=/issues/1]', 0
+ assert_select 'a[href=/issues/5]', :text => /Subproject issue/
+ end
+
+ def test_index_should_list_visible_issues_only
+ get :index, :per_page => 100
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ assert_nil assigns(:issues).detect {|issue| !issue.visible?}
+ end
+
+ def test_index_with_project
+ Setting.display_subprojects_issues = 0
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:issues)
+
+ assert_select 'a[href=/issues/1]', :text => /Can't print recipes/
+ assert_select 'a[href=/issues/5]', 0
+ end
+
+ def test_index_with_project_and_subprojects
+ Setting.display_subprojects_issues = 1
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:issues)
+
+ assert_select 'a[href=/issues/1]', :text => /Can't print recipes/
+ assert_select 'a[href=/issues/5]', :text => /Subproject issue/
+ assert_select 'a[href=/issues/6]', 0
+ end
+
+ def test_index_with_project_and_subprojects_should_show_private_subprojects_with_permission
+ @request.session[:user_id] = 2
+ Setting.display_subprojects_issues = 1
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:issues)
+
+ assert_select 'a[href=/issues/1]', :text => /Can't print recipes/
+ assert_select 'a[href=/issues/5]', :text => /Subproject issue/
+ assert_select 'a[href=/issues/6]', :text => /Issue of a private subproject/
+ end
+
+ def test_index_with_project_and_default_filter
+ get :index, :project_id => 1, :set_filter => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:issues)
+
+ query = assigns(:query)
+ assert_not_nil query
+ # default filter
+ assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
+ end
+
+ def test_index_with_project_and_filter
+ get :index, :project_id => 1, :set_filter => 1,
+ :f => ['tracker_id'],
+ :op => {'tracker_id' => '='},
+ :v => {'tracker_id' => ['1']}
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:issues)
+
+ query = assigns(:query)
+ assert_not_nil query
+ assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
+ end
+
+ def test_index_with_short_filters
+ to_test = {
+ 'status_id' => {
+ 'o' => { :op => 'o', :values => [''] },
+ 'c' => { :op => 'c', :values => [''] },
+ '7' => { :op => '=', :values => ['7'] },
+ '7|3|4' => { :op => '=', :values => ['7', '3', '4'] },
+ '=7' => { :op => '=', :values => ['7'] },
+ '!3' => { :op => '!', :values => ['3'] },
+ '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }},
+ 'subject' => {
+ 'This is a subject' => { :op => '=', :values => ['This is a subject'] },
+ 'o' => { :op => '=', :values => ['o'] },
+ '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] },
+ '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }},
+ 'tracker_id' => {
+ '3' => { :op => '=', :values => ['3'] },
+ '=3' => { :op => '=', :values => ['3'] }},
+ 'start_date' => {
+ '2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
+ '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
+ '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
+ '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] },
+ '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] },
+ ' { :op => ' ['2'] },
+ '>t+2' => { :op => '>t+', :values => ['2'] },
+ 't+2' => { :op => 't+', :values => ['2'] },
+ 't' => { :op => 't', :values => [''] },
+ 'w' => { :op => 'w', :values => [''] },
+ '>t-2' => { :op => '>t-', :values => ['2'] },
+ ' { :op => ' ['2'] },
+ 't-2' => { :op => 't-', :values => ['2'] }},
+ 'created_on' => {
+ '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
+ ' { :op => ' ['2'] },
+ '>t-2' => { :op => '>t-', :values => ['2'] },
+ 't-2' => { :op => 't-', :values => ['2'] }},
+ 'cf_1' => {
+ 'c' => { :op => '=', :values => ['c'] },
+ '!c' => { :op => '!', :values => ['c'] },
+ '!*' => { :op => '!*', :values => [''] },
+ '*' => { :op => '*', :values => [''] }},
+ 'estimated_hours' => {
+ '=13.4' => { :op => '=', :values => ['13.4'] },
+ '>=45' => { :op => '>=', :values => ['45'] },
+ '<=125' => { :op => '<=', :values => ['125'] },
+ '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] },
+ '!*' => { :op => '!*', :values => [''] },
+ '*' => { :op => '*', :values => [''] }}
+ }
+
+ default_filter = { 'status_id' => {:operator => 'o', :values => [''] }}
+
+ to_test.each do |field, expression_and_expected|
+ expression_and_expected.each do |filter_expression, expected|
+
+ get :index, :set_filter => 1, field => filter_expression
+
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:issues)
+
+ query = assigns(:query)
+ assert_not_nil query
+ assert query.has_filter?(field)
+ assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters)
+ end
+ end
+ end
+
+ def test_index_with_project_and_empty_filters
+ get :index, :project_id => 1, :set_filter => 1, :fields => ['']
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:issues)
+
+ query = assigns(:query)
+ assert_not_nil query
+ # no filter
+ assert_equal({}, query.filters)
+ end
+
+ def test_index_with_project_custom_field_filter
+ field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
+ CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
+ CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
+ filter_name = "project.cf_#{field.id}"
+ @request.session[:user_id] = 1
+
+ get :index, :set_filter => 1,
+ :f => [filter_name],
+ :op => {filter_name => '='},
+ :v => {filter_name => ['Foo']}
+ assert_response :success
+ assert_template 'index'
+ assert_equal [3, 5], assigns(:issues).map(&:project_id).uniq.sort
+ end
+
+ def test_index_with_query
+ get :index, :project_id => 1, :query_id => 5
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:issues)
+ assert_nil assigns(:issue_count_by_group)
+ end
+
+ def test_index_with_query_grouped_by_tracker
+ get :index, :project_id => 1, :query_id => 6
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:issues)
+ assert_not_nil assigns(:issue_count_by_group)
+ end
+
+ def test_index_with_query_grouped_by_list_custom_field
+ get :index, :project_id => 1, :query_id => 9
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:issues)
+ assert_not_nil assigns(:issue_count_by_group)
+ end
+
+ def test_index_with_query_grouped_by_user_custom_field
+ cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
+ CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
+ CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
+ CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
+ CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')
+
+ get :index, :project_id => 1, :set_filter => 1, :group_by => "cf_#{cf.id}"
+ assert_response :success
+
+ assert_select 'tr.group', 3
+ assert_select 'tr.group' do
+ assert_select 'a', :text => 'John Smith'
+ assert_select 'span.count', :text => '1'
+ end
+ assert_select 'tr.group' do
+ assert_select 'a', :text => 'Dave Lopper'
+ assert_select 'span.count', :text => '2'
+ end
+ end
+
+ def test_index_with_query_grouped_by_tracker
+ 3.times {|i| Issue.generate!(:tracker_id => (i + 1))}
+
+ get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc'
+ assert_response :success
+
+ trackers = assigns(:issues).map(&:tracker).uniq
+ assert_equal [1, 2, 3], trackers.map(&:id)
+ end
+
+ def test_index_with_query_grouped_by_tracker_in_reverse_order
+ 3.times {|i| Issue.generate!(:tracker_id => (i + 1))}
+
+ get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc,tracker:desc'
+ assert_response :success
+
+ trackers = assigns(:issues).map(&:tracker).uniq
+ assert_equal [3, 2, 1], trackers.map(&:id)
+ end
+
+ def test_index_with_query_id_and_project_id_should_set_session_query
+ get :index, :project_id => 1, :query_id => 4
+ assert_response :success
+ assert_kind_of Hash, session[:query]
+ assert_equal 4, session[:query][:id]
+ assert_equal 1, session[:query][:project_id]
+ end
+
+ def test_index_with_invalid_query_id_should_respond_404
+ get :index, :project_id => 1, :query_id => 999
+ assert_response 404
+ end
+
+ def test_index_with_cross_project_query_in_session_should_show_project_issues
+ q = IssueQuery.create!(:name => "test", :user_id => 2, :is_public => false, :project => nil)
+ @request.session[:query] = {:id => q.id, :project_id => 1}
+
+ with_settings :display_subprojects_issues => '0' do
+ get :index, :project_id => 1
+ end
+ assert_response :success
+ assert_not_nil assigns(:query)
+ assert_equal q.id, assigns(:query).id
+ assert_equal 1, assigns(:query).project_id
+ assert_equal [1], assigns(:issues).map(&:project_id).uniq
+ end
+
+ def test_private_query_should_not_be_available_to_other_users
+ q = IssueQuery.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
+ @request.session[:user_id] = 3
+
+ get :index, :query_id => q.id
+ assert_response 403
+ end
+
+ def test_private_query_should_be_available_to_its_user
+ q = IssueQuery.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
+ @request.session[:user_id] = 2
+
+ get :index, :query_id => q.id
+ assert_response :success
+ end
+
+ def test_public_query_should_be_available_to_other_users
+ q = IssueQuery.create!(:name => "private", :user => User.find(2), :is_public => true, :project => nil)
+ @request.session[:user_id] = 3
+
+ get :index, :query_id => q.id
+ assert_response :success
+ end
+
+ def test_index_should_omit_page_param_in_export_links
+ get :index, :page => 2
+ assert_response :success
+ assert_select 'a.atom[href=/issues.atom]'
+ assert_select 'a.csv[href=/issues.csv]'
+ assert_select 'a.pdf[href=/issues.pdf]'
+ assert_select 'form#csv-export-form[action=/issues.csv]'
+ end
+
+ def test_index_csv
+ get :index, :format => 'csv'
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ assert_equal 'text/csv; header=present', @response.content_type
+ assert @response.body.starts_with?("#,")
+ lines = @response.body.chomp.split("\n")
+ assert_equal assigns(:query).columns.size, lines[0].split(',').size
+ end
+
+ def test_index_csv_with_project
+ get :index, :project_id => 1, :format => 'csv'
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ assert_equal 'text/csv; header=present', @response.content_type
+ end
+
+ def test_index_csv_with_description
+ Issue.generate!(:description => 'test_index_csv_with_description')
+
+ with_settings :default_language => 'en' do
+ get :index, :format => 'csv', :description => '1'
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ end
+
+ assert_equal 'text/csv; header=present', response.content_type
+ headers = response.body.chomp.split("\n").first.split(',')
+ assert_include 'Description', headers
+ assert_include 'test_index_csv_with_description', response.body
+ end
+
+ def test_index_csv_with_spent_time_column
+ issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'test_index_csv_with_spent_time_column', :author_id => 2)
+ TimeEntry.create!(:project => issue.project, :issue => issue, :hours => 7.33, :user => User.find(2), :spent_on => Date.today)
+
+ get :index, :format => 'csv', :set_filter => '1', :c => %w(subject spent_hours)
+ assert_response :success
+ assert_equal 'text/csv; header=present', @response.content_type
+ lines = @response.body.chomp.split("\n")
+ assert_include "#{issue.id},#{issue.subject},7.33", lines
+ end
+
+ def test_index_csv_with_all_columns
+ get :index, :format => 'csv', :columns => 'all'
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ assert_equal 'text/csv; header=present', @response.content_type
+ assert_match /\A#,/, response.body
+ lines = response.body.chomp.split("\n")
+ assert_equal assigns(:query).available_inline_columns.size, lines[0].split(',').size
+ end
+
+ def test_index_csv_with_multi_column_field
+ CustomField.find(1).update_attribute :multiple, true
+ issue = Issue.find(1)
+ issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
+ issue.save!
+
+ get :index, :format => 'csv', :columns => 'all'
+ assert_response :success
+ lines = @response.body.chomp.split("\n")
+ assert lines.detect {|line| line.include?('"MySQL, Oracle"')}
+ end
+
+ def test_index_csv_should_format_float_custom_fields_with_csv_decimal_separator
+ field = IssueCustomField.create!(:name => 'Float', :is_for_all => true, :tracker_ids => [1], :field_format => 'float')
+ issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id => '185.6'})
+
+ with_settings :default_language => 'fr' do
+ get :index, :format => 'csv', :columns => 'all'
+ assert_response :success
+ issue_line = response.body.chomp.split("\n").map {|line| line.split(';')}.detect {|line| line[0]==issue.id.to_s}
+ assert_include '185,60', issue_line
+ end
+
+ with_settings :default_language => 'en' do
+ get :index, :format => 'csv', :columns => 'all'
+ assert_response :success
+ issue_line = response.body.chomp.split("\n").map {|line| line.split(',')}.detect {|line| line[0]==issue.id.to_s}
+ assert_include '185.60', issue_line
+ end
+ end
+
+ def test_index_csv_big_5
+ with_settings :default_language => "zh-TW" do
+ str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
+ str_big5 = "\xa4@\xa4\xeb"
+ if str_utf8.respond_to?(:force_encoding)
+ str_utf8.force_encoding('UTF-8')
+ str_big5.force_encoding('Big5')
+ end
+ issue = Issue.generate!(:subject => str_utf8)
+
+ get :index, :project_id => 1,
+ :f => ['subject'],
+ :op => '=', :values => [str_utf8],
+ :format => 'csv'
+ assert_equal 'text/csv; header=present', @response.content_type
+ lines = @response.body.chomp.split("\n")
+ s1 = "\xaa\xac\xbaA"
+ if str_utf8.respond_to?(:force_encoding)
+ s1.force_encoding('Big5')
+ end
+ assert_include s1, lines[0]
+ assert_include str_big5, lines[1]
+ end
+ end
+
+ def test_index_csv_cannot_convert_should_be_replaced_big_5
+ with_settings :default_language => "zh-TW" do
+ str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
+ if str_utf8.respond_to?(:force_encoding)
+ str_utf8.force_encoding('UTF-8')
+ end
+ issue = Issue.generate!(:subject => str_utf8)
+
+ get :index, :project_id => 1,
+ :f => ['subject'],
+ :op => '=', :values => [str_utf8],
+ :c => ['status', 'subject'],
+ :format => 'csv',
+ :set_filter => 1
+ assert_equal 'text/csv; header=present', @response.content_type
+ lines = @response.body.chomp.split("\n")
+ s1 = "\xaa\xac\xbaA" # status
+ if str_utf8.respond_to?(:force_encoding)
+ s1.force_encoding('Big5')
+ end
+ assert lines[0].include?(s1)
+ s2 = lines[1].split(",")[2]
+ if s1.respond_to?(:force_encoding)
+ s3 = "\xa5H?" # subject
+ s3.force_encoding('Big5')
+ assert_equal s3, s2
+ elsif RUBY_PLATFORM == 'java'
+ assert_equal "??", s2
+ else
+ assert_equal "\xa5H???", s2
+ end
+ end
+ end
+
+ def test_index_csv_tw
+ with_settings :default_language => "zh-TW" do
+ str1 = "test_index_csv_tw"
+ issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
+
+ get :index, :project_id => 1,
+ :f => ['subject'],
+ :op => '=', :values => [str1],
+ :c => ['estimated_hours', 'subject'],
+ :format => 'csv',
+ :set_filter => 1
+ assert_equal 'text/csv; header=present', @response.content_type
+ lines = @response.body.chomp.split("\n")
+ assert_equal "#{issue.id},1234.50,#{str1}", lines[1]
+ end
+ end
+
+ def test_index_csv_fr
+ with_settings :default_language => "fr" do
+ str1 = "test_index_csv_fr"
+ issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
+
+ get :index, :project_id => 1,
+ :f => ['subject'],
+ :op => '=', :values => [str1],
+ :c => ['estimated_hours', 'subject'],
+ :format => 'csv',
+ :set_filter => 1
+ assert_equal 'text/csv; header=present', @response.content_type
+ lines = @response.body.chomp.split("\n")
+ assert_equal "#{issue.id};1234,50;#{str1}", lines[1]
+ end
+ end
+
+ def test_index_pdf
+ ["en", "zh", "zh-TW", "ja", "ko"].each do |lang|
+ with_settings :default_language => lang do
+
+ get :index
+ assert_response :success
+ assert_template 'index'
+
+ if lang == "ja"
+ if RUBY_PLATFORM != 'java'
+ assert_equal "CP932", l(:general_pdf_encoding)
+ end
+ if RUBY_PLATFORM == 'java' && l(:general_pdf_encoding) == "CP932"
+ next
+ end
+ end
+
+ get :index, :format => 'pdf'
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ assert_equal 'application/pdf', @response.content_type
+
+ get :index, :project_id => 1, :format => 'pdf'
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ assert_equal 'application/pdf', @response.content_type
+
+ get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ assert_equal 'application/pdf', @response.content_type
+ end
+ end
+ end
+
+ def test_index_pdf_with_query_grouped_by_list_custom_field
+ get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ assert_not_nil assigns(:issue_count_by_group)
+ assert_equal 'application/pdf', @response.content_type
+ end
+
+ def test_index_atom
+ get :index, :project_id => 'ecookbook', :format => 'atom'
+ assert_response :success
+ assert_template 'common/feed'
+ assert_equal 'application/atom+xml', response.content_type
+
+ assert_select 'feed' do
+ assert_select 'link[rel=self][href=?]', 'http://test.host/projects/ecookbook/issues.atom'
+ assert_select 'link[rel=alternate][href=?]', 'http://test.host/projects/ecookbook/issues'
+ assert_select 'entry link[href=?]', 'http://test.host/issues/1'
+ end
+ end
+
+ def test_index_sort
+ get :index, :sort => 'tracker,id:desc'
+ assert_response :success
+
+ sort_params = @request.session['issues_index_sort']
+ assert sort_params.is_a?(String)
+ assert_equal 'tracker,id:desc', sort_params
+
+ issues = assigns(:issues)
+ assert_not_nil issues
+ assert !issues.empty?
+ assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
+ end
+
+ def test_index_sort_by_field_not_included_in_columns
+ Setting.issue_list_default_columns = %w(subject author)
+ get :index, :sort => 'tracker'
+ end
+
+ def test_index_sort_by_assigned_to
+ get :index, :sort => 'assigned_to'
+ assert_response :success
+ assignees = assigns(:issues).collect(&:assigned_to).compact
+ assert_equal assignees.sort, assignees
+ end
+
+ def test_index_sort_by_assigned_to_desc
+ get :index, :sort => 'assigned_to:desc'
+ assert_response :success
+ assignees = assigns(:issues).collect(&:assigned_to).compact
+ assert_equal assignees.sort.reverse, assignees
+ end
+
+ def test_index_group_by_assigned_to
+ get :index, :group_by => 'assigned_to', :sort => 'priority'
+ assert_response :success
+ end
+
+ def test_index_sort_by_author
+ get :index, :sort => 'author'
+ assert_response :success
+ authors = assigns(:issues).collect(&:author)
+ assert_equal authors.sort, authors
+ end
+
+ def test_index_sort_by_author_desc
+ get :index, :sort => 'author:desc'
+ assert_response :success
+ authors = assigns(:issues).collect(&:author)
+ assert_equal authors.sort.reverse, authors
+ end
+
+ def test_index_group_by_author
+ get :index, :group_by => 'author', :sort => 'priority'
+ assert_response :success
+ end
+
+ def test_index_sort_by_spent_hours
+ get :index, :sort => 'spent_hours:desc'
+ assert_response :success
+ hours = assigns(:issues).collect(&:spent_hours)
+ assert_equal hours.sort.reverse, hours
+ end
+
+ def test_index_sort_by_user_custom_field
+ cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
+ CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
+ CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
+ CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
+ CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')
+
+ get :index, :project_id => 1, :set_filter => 1, :sort => "cf_#{cf.id},id"
+ assert_response :success
+
+ assert_equal [2, 3, 1], assigns(:issues).select {|issue| issue.custom_field_value(cf).present?}.map(&:id)
+ end
+
+ def test_index_with_columns
+ columns = ['tracker', 'subject', 'assigned_to']
+ get :index, :set_filter => 1, :c => columns
+ assert_response :success
+
+ # query should use specified columns
+ query = assigns(:query)
+ assert_kind_of IssueQuery, query
+ assert_equal columns, query.column_names.map(&:to_s)
+
+ # columns should be stored in session
+ assert_kind_of Hash, session[:query]
+ assert_kind_of Array, session[:query][:column_names]
+ assert_equal columns, session[:query][:column_names].map(&:to_s)
+
+ # ensure only these columns are kept in the selected columns list
+ assert_select 'select#selected_columns option' do
+ assert_select 'option', 3
+ assert_select 'option[value=tracker]'
+ assert_select 'option[value=project]', 0
+ end
+ end
+
+ def test_index_without_project_should_implicitly_add_project_column_to_default_columns
+ Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
+ get :index, :set_filter => 1
+
+ # query should use specified columns
+ query = assigns(:query)
+ assert_kind_of IssueQuery, query
+ assert_equal [:id, :project, :tracker, :subject, :assigned_to], query.columns.map(&:name)
+ end
+
+ def test_index_without_project_and_explicit_default_columns_should_not_add_project_column
+ Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
+ columns = ['id', 'tracker', 'subject', 'assigned_to']
+ get :index, :set_filter => 1, :c => columns
+
+ # query should use specified columns
+ query = assigns(:query)
+ assert_kind_of IssueQuery, query
+ assert_equal columns.map(&:to_sym), query.columns.map(&:name)
+ end
+
+ def test_index_with_custom_field_column
+ columns = %w(tracker subject cf_2)
+ get :index, :set_filter => 1, :c => columns
+ assert_response :success
+
+ # query should use specified columns
+ query = assigns(:query)
+ assert_kind_of IssueQuery, query
+ assert_equal columns, query.column_names.map(&:to_s)
+
+ assert_select 'table.issues td.cf_2.string'
+ end
+
+ def test_index_with_multi_custom_field_column
+ field = CustomField.find(1)
+ field.update_attribute :multiple, true
+ issue = Issue.find(1)
+ issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
+ issue.save!
+
+ get :index, :set_filter => 1, :c => %w(tracker subject cf_1)
+ assert_response :success
+
+ assert_select 'table.issues td.cf_1', :text => 'MySQL, Oracle'
+ end
+
+ def test_index_with_multi_user_custom_field_column
+ field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
+ :tracker_ids => [1], :is_for_all => true)
+ issue = Issue.find(1)
+ issue.custom_field_values = {field.id => ['2', '3']}
+ issue.save!
+
+ get :index, :set_filter => 1, :c => ['tracker', 'subject', "cf_#{field.id}"]
+ assert_response :success
+
+ assert_select "table.issues td.cf_#{field.id}" do
+ assert_select 'a', 2
+ assert_select 'a[href=?]', '/users/2', :text => 'John Smith'
+ assert_select 'a[href=?]', '/users/3', :text => 'Dave Lopper'
+ end
+ end
+
+ def test_index_with_date_column
+ with_settings :date_format => '%d/%m/%Y' do
+ Issue.find(1).update_attribute :start_date, '1987-08-24'
+
+ get :index, :set_filter => 1, :c => %w(start_date)
+
+ assert_select "table.issues td.start_date", :text => '24/08/1987'
+ end
+ end
+
+ def test_index_with_done_ratio_column
+ Issue.find(1).update_attribute :done_ratio, 40
+
+ get :index, :set_filter => 1, :c => %w(done_ratio)
+
+ assert_select 'table.issues td.done_ratio' do
+ assert_select 'table.progress' do
+ assert_select 'td.closed[style=?]', 'width: 40%;'
+ end
+ end
+ end
+
+ def test_index_with_spent_hours_column
+ get :index, :set_filter => 1, :c => %w(subject spent_hours)
+
+ assert_select 'table.issues tr#issue-3 td.spent_hours', :text => '1.00'
+ end
+
+ def test_index_should_not_show_spent_hours_column_without_permission
+ Role.anonymous.remove_permission! :view_time_entries
+ get :index, :set_filter => 1, :c => %w(subject spent_hours)
+
+ assert_select 'td.spent_hours', 0
+ end
+
+ def test_index_with_fixed_version_column
+ get :index, :set_filter => 1, :c => %w(fixed_version)
+
+ assert_select 'table.issues td.fixed_version' do
+ assert_select 'a[href=?]', '/versions/2', :text => '1.0'
+ end
+ end
+
+ def test_index_with_relations_column
+ IssueRelation.delete_all
+ IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(7))
+ IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(8), :issue_to => Issue.find(1))
+ IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(1), :issue_to => Issue.find(11))
+ IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(12), :issue_to => Issue.find(2))
+
+ get :index, :set_filter => 1, :c => %w(subject relations)
+ assert_response :success
+ assert_select "tr#issue-1 td.relations" do
+ assert_select "span", 3
+ assert_select "span", :text => "Related to #7"
+ assert_select "span", :text => "Related to #8"
+ assert_select "span", :text => "Blocks #11"
+ end
+ assert_select "tr#issue-2 td.relations" do
+ assert_select "span", 1
+ assert_select "span", :text => "Blocked by #12"
+ end
+ assert_select "tr#issue-3 td.relations" do
+ assert_select "span", 0
+ end
+
+ get :index, :set_filter => 1, :c => %w(relations), :format => 'csv'
+ assert_response :success
+ assert_equal 'text/csv; header=present', response.content_type
+ lines = response.body.chomp.split("\n")
+ assert_include '1,"Related to #7, Related to #8, Blocks #11"', lines
+ assert_include '2,Blocked by #12', lines
+ assert_include '3,""', lines
+
+ get :index, :set_filter => 1, :c => %w(subject relations), :format => 'pdf'
+ assert_response :success
+ assert_equal 'application/pdf', response.content_type
+ end
+
+ def test_index_with_description_column
+ get :index, :set_filter => 1, :c => %w(subject description)
+
+ assert_select 'table.issues thead th', 3 # columns: chekbox + id + subject
+ assert_select 'td.description[colspan=3]', :text => 'Unable to print recipes'
+
+ get :index, :set_filter => 1, :c => %w(subject description), :format => 'pdf'
+ assert_response :success
+ assert_equal 'application/pdf', response.content_type
+ end
+
+ def test_index_send_html_if_query_is_invalid
+ get :index, :f => ['start_date'], :op => {:start_date => '='}
+ assert_equal 'text/html', @response.content_type
+ assert_template 'index'
+ end
+
+ def test_index_send_nothing_if_query_is_invalid
+ get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv'
+ assert_equal 'text/csv', @response.content_type
+ assert @response.body.blank?
+ end
+
+ def test_show_by_anonymous
+ get :show, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_equal Issue.find(1), assigns(:issue)
+
+ assert_select 'div.issue div.description', :text => /Unable to print recipes/
+
+ # anonymous role is allowed to add a note
+ assert_select 'form#issue-form' do
+ assert_select 'fieldset' do
+ assert_select 'legend', :text => 'Notes'
+ assert_select 'textarea[name=?]', 'issue[notes]'
+ end
+ end
+
+ assert_select 'title', :text => "Bug #1: Can't print recipes - eCookbook - Redmine"
+ end
+
+ def test_show_by_manager
+ @request.session[:user_id] = 2
+ get :show, :id => 1
+ assert_response :success
+
+ assert_select 'a', :text => /Quote/
+
+ assert_select 'form#issue-form' do
+ assert_select 'fieldset' do
+ assert_select 'legend', :text => 'Change properties'
+ assert_select 'input[name=?]', 'issue[subject]'
+ end
+ assert_select 'fieldset' do
+ assert_select 'legend', :text => 'Log time'
+ assert_select 'input[name=?]', 'time_entry[hours]'
+ end
+ assert_select 'fieldset' do
+ assert_select 'legend', :text => 'Notes'
+ assert_select 'textarea[name=?]', 'issue[notes]'
+ end
+ end
+ end
+
+ def test_show_should_display_update_form
+ @request.session[:user_id] = 2
+ get :show, :id => 1
+ assert_response :success
+
+ assert_select 'form#issue-form' do
+ assert_select 'input[name=?]', 'issue[is_private]'
+ assert_select 'select[name=?]', 'issue[project_id]'
+ assert_select 'select[name=?]', 'issue[tracker_id]'
+ assert_select 'input[name=?]', 'issue[subject]'
+ assert_select 'textarea[name=?]', 'issue[description]'
+ assert_select 'select[name=?]', 'issue[status_id]'
+ assert_select 'select[name=?]', 'issue[priority_id]'
+ assert_select 'select[name=?]', 'issue[assigned_to_id]'
+ assert_select 'select[name=?]', 'issue[category_id]'
+ assert_select 'select[name=?]', 'issue[fixed_version_id]'
+ assert_select 'input[name=?]', 'issue[parent_issue_id]'
+ assert_select 'input[name=?]', 'issue[start_date]'
+ assert_select 'input[name=?]', 'issue[due_date]'
+ assert_select 'select[name=?]', 'issue[done_ratio]'
+ assert_select 'input[name=?]', 'issue[custom_field_values][2]'
+ assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0
+ assert_select 'textarea[name=?]', 'issue[notes]'
+ end
+ end
+
+ def test_show_should_display_update_form_with_minimal_permissions
+ Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
+ WorkflowTransition.delete_all :role_id => 1
+
+ @request.session[:user_id] = 2
+ get :show, :id => 1
+ assert_response :success
+
+ assert_select 'form#issue-form' do
+ assert_select 'input[name=?]', 'issue[is_private]', 0
+ assert_select 'select[name=?]', 'issue[project_id]', 0
+ assert_select 'select[name=?]', 'issue[tracker_id]', 0
+ assert_select 'input[name=?]', 'issue[subject]', 0
+ assert_select 'textarea[name=?]', 'issue[description]', 0
+ assert_select 'select[name=?]', 'issue[status_id]', 0
+ assert_select 'select[name=?]', 'issue[priority_id]', 0
+ assert_select 'select[name=?]', 'issue[assigned_to_id]', 0
+ assert_select 'select[name=?]', 'issue[category_id]', 0
+ assert_select 'select[name=?]', 'issue[fixed_version_id]', 0
+ assert_select 'input[name=?]', 'issue[parent_issue_id]', 0
+ assert_select 'input[name=?]', 'issue[start_date]', 0
+ assert_select 'input[name=?]', 'issue[due_date]', 0
+ assert_select 'select[name=?]', 'issue[done_ratio]', 0
+ assert_select 'input[name=?]', 'issue[custom_field_values][2]', 0
+ assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0
+ assert_select 'textarea[name=?]', 'issue[notes]'
+ end
+ end
+
+ def test_show_should_display_update_form_with_workflow_permissions
+ Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
+
+ @request.session[:user_id] = 2
+ get :show, :id => 1
+ assert_response :success
+
+ assert_select 'form#issue-form' do
+ assert_select 'input[name=?]', 'issue[is_private]', 0
+ assert_select 'select[name=?]', 'issue[project_id]', 0
+ assert_select 'select[name=?]', 'issue[tracker_id]', 0
+ assert_select 'input[name=?]', 'issue[subject]', 0
+ assert_select 'textarea[name=?]', 'issue[description]', 0
+ assert_select 'select[name=?]', 'issue[status_id]'
+ assert_select 'select[name=?]', 'issue[priority_id]', 0
+ assert_select 'select[name=?]', 'issue[assigned_to_id]'
+ assert_select 'select[name=?]', 'issue[category_id]', 0
+ assert_select 'select[name=?]', 'issue[fixed_version_id]'
+ assert_select 'input[name=?]', 'issue[parent_issue_id]', 0
+ assert_select 'input[name=?]', 'issue[start_date]', 0
+ assert_select 'input[name=?]', 'issue[due_date]', 0
+ assert_select 'select[name=?]', 'issue[done_ratio]'
+ assert_select 'input[name=?]', 'issue[custom_field_values][2]', 0
+ assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0
+ assert_select 'textarea[name=?]', 'issue[notes]'
+ end
+ end
+
+ def test_show_should_not_display_update_form_without_permissions
+ Role.find(1).update_attribute :permissions, [:view_issues]
+
+ @request.session[:user_id] = 2
+ get :show, :id => 1
+ assert_response :success
+
+ assert_select 'form#issue-form', 0
+ end
+
+ def test_update_form_should_not_display_inactive_enumerations
+ assert !IssuePriority.find(15).active?
+
+ @request.session[:user_id] = 2
+ get :show, :id => 1
+ assert_response :success
+
+ assert_select 'form#issue-form' do
+ assert_select 'select[name=?]', 'issue[priority_id]' do
+ assert_select 'option[value=4]'
+ assert_select 'option[value=15]', 0
+ end
+ end
+ end
+
+ def test_update_form_should_allow_attachment_upload
+ @request.session[:user_id] = 2
+ get :show, :id => 1
+
+ assert_select 'form#issue-form[method=post][enctype=multipart/form-data]' do
+ assert_select 'input[type=file][name=?]', 'attachments[dummy][file]'
+ end
+ end
+
+ def test_show_should_deny_anonymous_access_without_permission
+ Role.anonymous.remove_permission!(:view_issues)
+ get :show, :id => 1
+ assert_response :redirect
+ end
+
+ def test_show_should_deny_anonymous_access_to_private_issue
+ Issue.update_all(["is_private = ?", true], "id = 1")
+ get :show, :id => 1
+ assert_response :redirect
+ end
+
+ def test_show_should_deny_non_member_access_without_permission
+ Role.non_member.remove_permission!(:view_issues)
+ @request.session[:user_id] = 9
+ get :show, :id => 1
+ assert_response 403
+ end
+
+ def test_show_should_deny_non_member_access_to_private_issue
+ Issue.update_all(["is_private = ?", true], "id = 1")
+ @request.session[:user_id] = 9
+ get :show, :id => 1
+ assert_response 403
+ end
+
+ def test_show_should_deny_member_access_without_permission
+ Role.find(1).remove_permission!(:view_issues)
+ @request.session[:user_id] = 2
+ get :show, :id => 1
+ assert_response 403
+ end
+
+ def test_show_should_deny_member_access_to_private_issue_without_permission
+ Issue.update_all(["is_private = ?", true], "id = 1")
+ @request.session[:user_id] = 3
+ get :show, :id => 1
+ assert_response 403
+ end
+
+ def test_show_should_allow_author_access_to_private_issue
+ Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1")
+ @request.session[:user_id] = 3
+ get :show, :id => 1
+ assert_response :success
+ end
+
+ def test_show_should_allow_assignee_access_to_private_issue
+ Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1")
+ @request.session[:user_id] = 3
+ get :show, :id => 1
+ assert_response :success
+ end
+
+ def test_show_should_allow_member_access_to_private_issue_with_permission
+ Issue.update_all(["is_private = ?", true], "id = 1")
+ User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
+ @request.session[:user_id] = 3
+ get :show, :id => 1
+ assert_response :success
+ end
+
+ def test_show_should_not_disclose_relations_to_invisible_issues
+ Setting.cross_project_issue_relations = '1'
+ IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
+ # Relation to a private project issue
+ IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
+
+ get :show, :id => 1
+ assert_response :success
+
+ assert_select 'div#relations' do
+ assert_select 'a', :text => /#2$/
+ assert_select 'a', :text => /#4$/, :count => 0
+ end
+ end
+
+ def test_show_should_list_subtasks
+ Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
+
+ get :show, :id => 1
+ assert_response :success
+
+ assert_select 'div#issue_tree' do
+ assert_select 'td.subject', :text => /Child Issue/
+ end
+ end
+
+ def test_show_should_list_parents
+ issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
+
+ get :show, :id => issue.id
+ assert_response :success
+
+ assert_select 'div.subject' do
+ assert_select 'h3', 'Child Issue'
+ assert_select 'a[href=/issues/1]'
+ end
+ end
+
+ def test_show_should_not_display_prev_next_links_without_query_in_session
+ get :show, :id => 1
+ assert_response :success
+ assert_nil assigns(:prev_issue_id)
+ assert_nil assigns(:next_issue_id)
+
+ assert_select 'div.next-prev-links', 0
+ end
+
+ def test_show_should_display_prev_next_links_with_query_in_session
+ @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
+ @request.session['issues_index_sort'] = 'id'
+
+ with_settings :display_subprojects_issues => '0' do
+ get :show, :id => 3
+ end
+
+ assert_response :success
+ # Previous and next issues for all projects
+ assert_equal 2, assigns(:prev_issue_id)
+ assert_equal 5, assigns(:next_issue_id)
+
+ count = Issue.open.visible.count
+
+ assert_select 'div.next-prev-links' do
+ assert_select 'a[href=/issues/2]', :text => /Previous/
+ assert_select 'a[href=/issues/5]', :text => /Next/
+ assert_select 'span.position', :text => "3 of #{count}"
+ end
+ end
+
+ def test_show_should_display_prev_next_links_with_saved_query_in_session
+ query = IssueQuery.create!(:name => 'test', :is_public => true, :user_id => 1,
+ :filters => {'status_id' => {:values => ['5'], :operator => '='}},
+ :sort_criteria => [['id', 'asc']])
+ @request.session[:query] = {:id => query.id, :project_id => nil}
+
+ get :show, :id => 11
+
+ assert_response :success
+ assert_equal query, assigns(:query)
+ # Previous and next issues for all projects
+ assert_equal 8, assigns(:prev_issue_id)
+ assert_equal 12, assigns(:next_issue_id)
+
+ assert_select 'div.next-prev-links' do
+ assert_select 'a[href=/issues/8]', :text => /Previous/
+ assert_select 'a[href=/issues/12]', :text => /Next/
+ end
+ end
+
+ def test_show_should_display_prev_next_links_with_query_and_sort_on_association
+ @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
+
+ %w(project tracker status priority author assigned_to category fixed_version).each do |assoc_sort|
+ @request.session['issues_index_sort'] = assoc_sort
+
+ get :show, :id => 3
+ assert_response :success, "Wrong response status for #{assoc_sort} sort"
+
+ assert_select 'div.next-prev-links' do
+ assert_select 'a', :text => /(Previous|Next)/
+ end
+ end
+ end
+
+ def test_show_should_display_prev_next_links_with_project_query_in_session
+ @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
+ @request.session['issues_index_sort'] = 'id'
+
+ with_settings :display_subprojects_issues => '0' do
+ get :show, :id => 3
+ end
+
+ assert_response :success
+ # Previous and next issues inside project
+ assert_equal 2, assigns(:prev_issue_id)
+ assert_equal 7, assigns(:next_issue_id)
+
+ assert_select 'div.next-prev-links' do
+ assert_select 'a[href=/issues/2]', :text => /Previous/
+ assert_select 'a[href=/issues/7]', :text => /Next/
+ end
+ end
+
+ def test_show_should_not_display_prev_link_for_first_issue
+ @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
+ @request.session['issues_index_sort'] = 'id'
+
+ with_settings :display_subprojects_issues => '0' do
+ get :show, :id => 1
+ end
+
+ assert_response :success
+ assert_nil assigns(:prev_issue_id)
+ assert_equal 2, assigns(:next_issue_id)
+
+ assert_select 'div.next-prev-links' do
+ assert_select 'a', :text => /Previous/, :count => 0
+ assert_select 'a[href=/issues/2]', :text => /Next/
+ end
+ end
+
+ def test_show_should_not_display_prev_next_links_for_issue_not_in_query_results
+ @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'c'}}, :project_id => 1}
+ @request.session['issues_index_sort'] = 'id'
+
+ get :show, :id => 1
+
+ assert_response :success
+ assert_nil assigns(:prev_issue_id)
+ assert_nil assigns(:next_issue_id)
+
+ assert_select 'a', :text => /Previous/, :count => 0
+ assert_select 'a', :text => /Next/, :count => 0
+ end
+
+ def test_show_show_should_display_prev_next_links_with_query_sort_by_user_custom_field
+ cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
+ CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
+ CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
+ CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
+ CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')
+
+ query = IssueQuery.create!(:name => 'test', :is_public => true, :user_id => 1, :filters => {},
+ :sort_criteria => [["cf_#{cf.id}", 'asc'], ['id', 'asc']])
+ @request.session[:query] = {:id => query.id, :project_id => nil}
+
+ get :show, :id => 3
+ assert_response :success
+
+ assert_equal 2, assigns(:prev_issue_id)
+ assert_equal 1, assigns(:next_issue_id)
+
+ assert_select 'div.next-prev-links' do
+ assert_select 'a[href=/issues/2]', :text => /Previous/
+ assert_select 'a[href=/issues/1]', :text => /Next/
+ end
+ end
+
+ def test_show_should_display_link_to_the_assignee
+ get :show, :id => 2
+ assert_response :success
+ assert_select '.assigned-to' do
+ assert_select 'a[href=/users/3]'
+ end
+ end
+
+ def test_show_should_display_visible_changesets_from_other_projects
+ project = Project.find(2)
+ issue = project.issues.first
+ issue.changeset_ids = [102]
+ issue.save!
+ # changesets from other projects should be displayed even if repository
+ # is disabled on issue's project
+ project.disable_module! :repository
+
+ @request.session[:user_id] = 2
+ get :show, :id => issue.id
+
+ assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/3'
+ end
+
+ def test_show_should_display_watchers
+ @request.session[:user_id] = 2
+ Issue.find(1).add_watcher User.find(2)
+
+ get :show, :id => 1
+ assert_select 'div#watchers ul' do
+ assert_select 'li' do
+ assert_select 'a[href=/users/2]'
+ assert_select 'a img[alt=Delete]'
+ end
+ end
+ end
+
+ def test_show_should_display_watchers_with_gravatars
+ @request.session[:user_id] = 2
+ Issue.find(1).add_watcher User.find(2)
+
+ with_settings :gravatar_enabled => '1' do
+ get :show, :id => 1
+ end
+
+ assert_select 'div#watchers ul' do
+ assert_select 'li' do
+ assert_select 'img.gravatar'
+ assert_select 'a[href=/users/2]'
+ assert_select 'a img[alt=Delete]'
+ end
+ end
+ end
+
+ def test_show_with_thumbnails_enabled_should_display_thumbnails
+ @request.session[:user_id] = 2
+
+ with_settings :thumbnails_enabled => '1' do
+ get :show, :id => 14
+ assert_response :success
+ end
+
+ assert_select 'div.thumbnails' do
+ assert_select 'a[href=/attachments/16/testfile.png]' do
+ assert_select 'img[src=/attachments/thumbnail/16]'
+ end
+ end
+ end
+
+ def test_show_with_thumbnails_disabled_should_not_display_thumbnails
+ @request.session[:user_id] = 2
+
+ with_settings :thumbnails_enabled => '0' do
+ get :show, :id => 14
+ assert_response :success
+ end
+
+ assert_select 'div.thumbnails', 0
+ end
+
+ def test_show_with_multi_custom_field
+ field = CustomField.find(1)
+ field.update_attribute :multiple, true
+ issue = Issue.find(1)
+ issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
+ issue.save!
+
+ get :show, :id => 1
+ assert_response :success
+
+ assert_select 'td', :text => 'MySQL, Oracle'
+ end
+
+ def test_show_with_multi_user_custom_field
+ field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
+ :tracker_ids => [1], :is_for_all => true)
+ issue = Issue.find(1)
+ issue.custom_field_values = {field.id => ['2', '3']}
+ issue.save!
+
+ get :show, :id => 1
+ assert_response :success
+
+ # TODO: should display links
+ assert_select 'td', :text => 'Dave Lopper, John Smith'
+ end
+
+ def test_show_should_display_private_notes_with_permission_only
+ journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true, :user_id => 1)
+ @request.session[:user_id] = 2
+
+ get :show, :id => 2
+ assert_response :success
+ assert_include journal, assigns(:journals)
+
+ Role.find(1).remove_permission! :view_private_notes
+ get :show, :id => 2
+ assert_response :success
+ assert_not_include journal, assigns(:journals)
+ end
+
+ def test_show_atom
+ get :show, :id => 2, :format => 'atom'
+ assert_response :success
+ assert_template 'journals/index'
+ # Inline image
+ assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
+ end
+
+ def test_show_export_to_pdf
+ get :show, :id => 3, :format => 'pdf'
+ assert_response :success
+ assert_equal 'application/pdf', @response.content_type
+ assert @response.body.starts_with?('%PDF')
+ assert_not_nil assigns(:issue)
+ end
+
+ def test_show_export_to_pdf_with_ancestors
+ issue = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1)
+
+ get :show, :id => issue.id, :format => 'pdf'
+ assert_response :success
+ assert_equal 'application/pdf', @response.content_type
+ assert @response.body.starts_with?('%PDF')
+ end
+
+ def test_show_export_to_pdf_with_descendants
+ c1 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1)
+ c2 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1)
+ c3 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => c1.id)
+
+ get :show, :id => 1, :format => 'pdf'
+ assert_response :success
+ assert_equal 'application/pdf', @response.content_type
+ assert @response.body.starts_with?('%PDF')
+ end
+
+ def test_show_export_to_pdf_with_journals
+ get :show, :id => 1, :format => 'pdf'
+ assert_response :success
+ assert_equal 'application/pdf', @response.content_type
+ assert @response.body.starts_with?('%PDF')
+ end
+
+ def test_show_export_to_pdf_with_changesets
+ Issue.find(3).changesets = Changeset.find_all_by_id(100, 101, 102)
+
+ get :show, :id => 3, :format => 'pdf'
+ assert_response :success
+ assert_equal 'application/pdf', @response.content_type
+ assert @response.body.starts_with?('%PDF')
+ end
+
+ def test_show_invalid_should_respond_with_404
+ get :show, :id => 999
+ assert_response 404
+ end
+
+ def test_get_new
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :tracker_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_select 'form#issue-form' do
+ assert_select 'input[name=?]', 'issue[is_private]'
+ assert_select 'select[name=?]', 'issue[project_id]', 0
+ assert_select 'select[name=?]', 'issue[tracker_id]'
+ assert_select 'input[name=?]', 'issue[subject]'
+ assert_select 'textarea[name=?]', 'issue[description]'
+ assert_select 'select[name=?]', 'issue[status_id]'
+ assert_select 'select[name=?]', 'issue[priority_id]'
+ assert_select 'select[name=?]', 'issue[assigned_to_id]'
+ assert_select 'select[name=?]', 'issue[category_id]'
+ assert_select 'select[name=?]', 'issue[fixed_version_id]'
+ assert_select 'input[name=?]', 'issue[parent_issue_id]'
+ assert_select 'input[name=?]', 'issue[start_date]'
+ assert_select 'input[name=?]', 'issue[due_date]'
+ assert_select 'select[name=?]', 'issue[done_ratio]'
+ assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Default string'
+ assert_select 'input[name=?]', 'issue[watcher_user_ids][]'
+ end
+
+ # Be sure we don't display inactive IssuePriorities
+ assert ! IssuePriority.find(15).active?
+ assert_select 'select[name=?]', 'issue[priority_id]' do
+ assert_select 'option[value=15]', 0
+ end
+ end
+
+ def test_get_new_with_minimal_permissions
+ Role.find(1).update_attribute :permissions, [:add_issues]
+ WorkflowTransition.delete_all :role_id => 1
+
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :tracker_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_select 'form#issue-form' do
+ assert_select 'input[name=?]', 'issue[is_private]', 0
+ assert_select 'select[name=?]', 'issue[project_id]', 0
+ assert_select 'select[name=?]', 'issue[tracker_id]'
+ assert_select 'input[name=?]', 'issue[subject]'
+ assert_select 'textarea[name=?]', 'issue[description]'
+ assert_select 'select[name=?]', 'issue[status_id]'
+ assert_select 'select[name=?]', 'issue[priority_id]'
+ assert_select 'select[name=?]', 'issue[assigned_to_id]'
+ assert_select 'select[name=?]', 'issue[category_id]'
+ assert_select 'select[name=?]', 'issue[fixed_version_id]'
+ assert_select 'input[name=?]', 'issue[parent_issue_id]', 0
+ assert_select 'input[name=?]', 'issue[start_date]'
+ assert_select 'input[name=?]', 'issue[due_date]'
+ assert_select 'select[name=?]', 'issue[done_ratio]'
+ assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Default string'
+ assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0
+ end
+ end
+
+ def test_get_new_with_list_custom_field
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :tracker_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_select 'select.list_cf[name=?]', 'issue[custom_field_values][1]' do
+ assert_select 'option', 4
+ assert_select 'option[value=MySQL]', :text => 'MySQL'
+ end
+ end
+
+ def test_get_new_with_multi_custom_field
+ field = IssueCustomField.find(1)
+ field.update_attribute :multiple, true
+
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :tracker_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_select 'select[name=?][multiple=multiple]', 'issue[custom_field_values][1][]' do
+ assert_select 'option', 3
+ assert_select 'option[value=MySQL]', :text => 'MySQL'
+ end
+ assert_select 'input[name=?][type=hidden][value=?]', 'issue[custom_field_values][1][]', ''
+ end
+
+ def test_get_new_with_multi_user_custom_field
+ field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
+ :tracker_ids => [1], :is_for_all => true)
+
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :tracker_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_select 'select[name=?][multiple=multiple]', "issue[custom_field_values][#{field.id}][]" do
+ assert_select 'option', Project.find(1).users.count
+ assert_select 'option[value=2]', :text => 'John Smith'
+ end
+ assert_select 'input[name=?][type=hidden][value=?]', "issue[custom_field_values][#{field.id}][]", ''
+ end
+
+ def test_get_new_with_date_custom_field
+ field = IssueCustomField.create!(:name => 'Date', :field_format => 'date', :tracker_ids => [1], :is_for_all => true)
+
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :tracker_id => 1
+ assert_response :success
+
+ assert_select 'input[name=?]', "issue[custom_field_values][#{field.id}]"
+ end
+
+ def test_get_new_with_text_custom_field
+ field = IssueCustomField.create!(:name => 'Text', :field_format => 'text', :tracker_ids => [1], :is_for_all => true)
+
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :tracker_id => 1
+ assert_response :success
+
+ assert_select 'textarea[name=?]', "issue[custom_field_values][#{field.id}]"
+ end
+
+ def test_get_new_without_default_start_date_is_creation_date
+ Setting.default_issue_start_date_to_creation_date = 0
+
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :tracker_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_select 'input[name=?]', 'issue[start_date]'
+ assert_select 'input[name=?][value]', 'issue[start_date]', 0
+ end
+
+ def test_get_new_with_default_start_date_is_creation_date
+ Setting.default_issue_start_date_to_creation_date = 1
+
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :tracker_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_select 'input[name=?][value=?]', 'issue[start_date]', Date.today.to_s
+ end
+
+ def test_get_new_form_should_allow_attachment_upload
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :tracker_id => 1
+
+ assert_select 'form[id=issue-form][method=post][enctype=multipart/form-data]' do
+ assert_select 'input[name=?][type=file]', 'attachments[dummy][file]'
+ end
+ end
+
+ def test_get_new_should_prefill_the_form_from_params
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1,
+ :issue => {:tracker_id => 3, :description => 'Prefilled', :custom_field_values => {'2' => 'Custom field value'}}
+
+ issue = assigns(:issue)
+ assert_equal 3, issue.tracker_id
+ assert_equal 'Prefilled', issue.description
+ assert_equal 'Custom field value', issue.custom_field_value(2)
+
+ assert_select 'select[name=?]', 'issue[tracker_id]' do
+ assert_select 'option[value=3][selected=selected]'
+ end
+ assert_select 'textarea[name=?]', 'issue[description]', :text => /Prefilled/
+ assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Custom field value'
+ end
+
+ def test_get_new_should_mark_required_fields
+ cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
+ cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
+ WorkflowPermission.delete_all
+ WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required')
+ WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required')
+ @request.session[:user_id] = 2
+
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_select 'label[for=issue_start_date]' do
+ assert_select 'span[class=required]', 0
+ end
+ assert_select 'label[for=issue_due_date]' do
+ assert_select 'span[class=required]'
+ end
+ assert_select 'label[for=?]', "issue_custom_field_values_#{cf1.id}" do
+ assert_select 'span[class=required]', 0
+ end
+ assert_select 'label[for=?]', "issue_custom_field_values_#{cf2.id}" do
+ assert_select 'span[class=required]'
+ end
+ end
+
+ def test_get_new_should_not_display_readonly_fields
+ cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
+ cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
+ WorkflowPermission.delete_all
+ WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
+ WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
+ @request.session[:user_id] = 2
+
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_select 'input[name=?]', 'issue[start_date]'
+ assert_select 'input[name=?]', 'issue[due_date]', 0
+ assert_select 'input[name=?]', "issue[custom_field_values][#{cf1.id}]"
+ assert_select 'input[name=?]', "issue[custom_field_values][#{cf2.id}]", 0
+ end
+
+ def test_get_new_without_tracker_id
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ issue = assigns(:issue)
+ assert_not_nil issue
+ assert_equal Project.find(1).trackers.first, issue.tracker
+ end
+
+ def test_get_new_with_no_default_status_should_display_an_error
+ @request.session[:user_id] = 2
+ IssueStatus.delete_all
+
+ get :new, :project_id => 1
+ assert_response 500
+ assert_error_tag :content => /No default issue/
+ end
+
+ def test_get_new_with_no_tracker_should_display_an_error
+ @request.session[:user_id] = 2
+ Tracker.delete_all
+
+ get :new, :project_id => 1
+ assert_response 500
+ assert_error_tag :content => /No tracker/
+ end
+
+ def test_update_form_for_new_issue
+ @request.session[:user_id] = 2
+ xhr :post, :update_form, :project_id => 1,
+ :issue => {:tracker_id => 2,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5}
+ assert_response :success
+ assert_template 'update_form'
+ assert_template 'form'
+ assert_equal 'text/javascript', response.content_type
+
+ issue = assigns(:issue)
+ assert_kind_of Issue, issue
+ assert_equal 1, issue.project_id
+ assert_equal 2, issue.tracker_id
+ assert_equal 'This is the test_new issue', issue.subject
+ end
+
+ def test_update_form_for_new_issue_should_propose_transitions_based_on_initial_status
+ @request.session[:user_id] = 2
+ WorkflowTransition.delete_all
+ WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2)
+ WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5)
+ WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4)
+
+ xhr :post, :update_form, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :status_id => 5,
+ :subject => 'This is an issue'}
+
+ assert_equal 5, assigns(:issue).status_id
+ assert_equal [1,2,5], assigns(:allowed_statuses).map(&:id).sort
+ end
+
+ def test_post_create
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 3,
+ :status_id => 2,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5,
+ :start_date => '2010-11-07',
+ :estimated_hours => '',
+ :custom_field_values => {'2' => 'Value for field 2'}}
+ end
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
+
+ issue = Issue.find_by_subject('This is the test_new issue')
+ assert_not_nil issue
+ assert_equal 2, issue.author_id
+ assert_equal 3, issue.tracker_id
+ assert_equal 2, issue.status_id
+ assert_equal Date.parse('2010-11-07'), issue.start_date
+ assert_nil issue.estimated_hours
+ v = issue.custom_values.where(:custom_field_id => 2).first
+ assert_not_nil v
+ assert_equal 'Value for field 2', v.value
+ end
+
+ def test_post_new_with_group_assignment
+ group = Group.find(11)
+ project = Project.find(1)
+ project.members << Member.new(:principal => group, :roles => [Role.givable.first])
+
+ with_settings :issue_group_assignment => '1' do
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :create, :project_id => project.id,
+ :issue => {:tracker_id => 3,
+ :status_id => 1,
+ :subject => 'This is the test_new_with_group_assignment issue',
+ :assigned_to_id => group.id}
+ end
+ end
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
+
+ issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue')
+ assert_not_nil issue
+ assert_equal group, issue.assigned_to
+ end
+
+ def test_post_create_without_start_date_and_default_start_date_is_not_creation_date
+ Setting.default_issue_start_date_to_creation_date = 0
+
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 3,
+ :status_id => 2,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5,
+ :estimated_hours => '',
+ :custom_field_values => {'2' => 'Value for field 2'}}
+ end
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
+
+ issue = Issue.find_by_subject('This is the test_new issue')
+ assert_not_nil issue
+ assert_nil issue.start_date
+ end
+
+ def test_post_create_without_start_date_and_default_start_date_is_creation_date
+ Setting.default_issue_start_date_to_creation_date = 1
+
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 3,
+ :status_id => 2,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5,
+ :estimated_hours => '',
+ :custom_field_values => {'2' => 'Value for field 2'}}
+ end
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
+
+ issue = Issue.find_by_subject('This is the test_new issue')
+ assert_not_nil issue
+ assert_equal Date.today, issue.start_date
+ end
+
+ def test_post_create_and_continue
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5},
+ :continue => ''
+ end
+
+ issue = Issue.first(:order => 'id DESC')
+ assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3}
+ assert_not_nil flash[:notice], "flash was not set"
+ assert_include %|##{issue.id}|, flash[:notice], "issue link not found in the flash message"
+ end
+
+ def test_post_create_without_custom_fields_param
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5}
+ end
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
+ end
+
+ def test_post_create_with_multi_custom_field
+ field = IssueCustomField.find_by_name('Database')
+ field.update_attribute(:multiple, true)
+
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5,
+ :custom_field_values => {'1' => ['', 'MySQL', 'Oracle']}}
+ end
+ assert_response 302
+ issue = Issue.first(:order => 'id DESC')
+ assert_equal ['MySQL', 'Oracle'], issue.custom_field_value(1).sort
+ end
+
+ def test_post_create_with_empty_multi_custom_field
+ field = IssueCustomField.find_by_name('Database')
+ field.update_attribute(:multiple, true)
+
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5,
+ :custom_field_values => {'1' => ['']}}
+ end
+ assert_response 302
+ issue = Issue.first(:order => 'id DESC')
+ assert_equal [''], issue.custom_field_value(1).sort
+ end
+
+ def test_post_create_with_multi_user_custom_field
+ field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
+ :tracker_ids => [1], :is_for_all => true)
+
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5,
+ :custom_field_values => {field.id.to_s => ['', '2', '3']}}
+ end
+ assert_response 302
+ issue = Issue.first(:order => 'id DESC')
+ assert_equal ['2', '3'], issue.custom_field_value(field).sort
+ end
+
+ def test_post_create_with_required_custom_field_and_without_custom_fields_param
+ field = IssueCustomField.find_by_name('Database')
+ field.update_attribute(:is_required, true)
+
+ @request.session[:user_id] = 2
+ assert_no_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5}
+ end
+ assert_response :success
+ assert_template 'new'
+ issue = assigns(:issue)
+ assert_not_nil issue
+ assert_error_tag :content => /Database can't be blank/
+ end
+
+ def test_create_should_validate_required_fields
+ cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
+ cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
+ WorkflowPermission.delete_all
+ WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'required')
+ WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required')
+ @request.session[:user_id] = 2
+
+ assert_no_difference 'Issue.count' do
+ post :create, :project_id => 1, :issue => {
+ :tracker_id => 2,
+ :status_id => 1,
+ :subject => 'Test',
+ :start_date => '',
+ :due_date => '',
+ :custom_field_values => {cf1.id.to_s => '', cf2.id.to_s => ''}
+ }
+ assert_response :success
+ assert_template 'new'
+ end
+
+ assert_error_tag :content => /Due date can't be blank/i
+ assert_error_tag :content => /Bar can't be blank/i
+ end
+
+ def test_create_should_ignore_readonly_fields
+ cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
+ cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
+ WorkflowPermission.delete_all
+ WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
+ WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1, :issue => {
+ :tracker_id => 2,
+ :status_id => 1,
+ :subject => 'Test',
+ :start_date => '2012-07-14',
+ :due_date => '2012-07-16',
+ :custom_field_values => {cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'}
+ }
+ assert_response 302
+ end
+
+ issue = Issue.first(:order => 'id DESC')
+ assert_equal Date.parse('2012-07-14'), issue.start_date
+ assert_nil issue.due_date
+ assert_equal 'value1', issue.custom_field_value(cf1)
+ assert_nil issue.custom_field_value(cf2)
+ end
+
+ def test_post_create_with_watchers
+ @request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+
+ assert_difference 'Watcher.count', 2 do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is a new issue with watchers',
+ :description => 'This is the description',
+ :priority_id => 5,
+ :watcher_user_ids => ['2', '3']}
+ end
+ issue = Issue.find_by_subject('This is a new issue with watchers')
+ assert_not_nil issue
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
+
+ # Watchers added
+ assert_equal [2, 3], issue.watcher_user_ids.sort
+ assert issue.watched_by?(User.find(3))
+ # Watchers notified
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
+ end
+
+ def test_post_create_subissue
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is a child issue',
+ :parent_issue_id => '2'}
+ assert_response 302
+ end
+ issue = Issue.order('id DESC').first
+ assert_equal Issue.find(2), issue.parent
+ end
+
+ def test_post_create_subissue_with_sharp_parent_id
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is a child issue',
+ :parent_issue_id => '#2'}
+ assert_response 302
+ end
+ issue = Issue.order('id DESC').first
+ assert_equal Issue.find(2), issue.parent
+ end
+
+ def test_post_create_subissue_with_non_visible_parent_id_should_not_validate
+ @request.session[:user_id] = 2
+
+ assert_no_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is a child issue',
+ :parent_issue_id => '4'}
+
+ assert_response :success
+ assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', '4'
+ assert_error_tag :content => /Parent task is invalid/i
+ end
+ end
+
+ def test_post_create_subissue_with_non_numeric_parent_id_should_not_validate
+ @request.session[:user_id] = 2
+
+ assert_no_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is a child issue',
+ :parent_issue_id => '01ABC'}
+
+ assert_response :success
+ assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', '01ABC'
+ assert_error_tag :content => /Parent task is invalid/i
+ end
+ end
+
+ def test_post_create_private
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is a private issue',
+ :is_private => '1'}
+ end
+ issue = Issue.first(:order => 'id DESC')
+ assert issue.is_private?
+ end
+
+ def test_post_create_private_with_set_own_issues_private_permission
+ role = Role.find(1)
+ role.remove_permission! :set_issues_private
+ role.add_permission! :set_own_issues_private
+
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is a private issue',
+ :is_private => '1'}
+ end
+ issue = Issue.first(:order => 'id DESC')
+ assert issue.is_private?
+ end
+
+ def test_post_create_should_send_a_notification
+ ActionMailer::Base.deliveries.clear
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 3,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5,
+ :estimated_hours => '',
+ :custom_field_values => {'2' => 'Value for field 2'}}
+ end
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
+
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_post_create_should_preserve_fields_values_on_validation_failure
+ @request.session[:user_id] = 2
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ # empty subject
+ :subject => '',
+ :description => 'This is a description',
+ :priority_id => 6,
+ :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
+ assert_response :success
+ assert_template 'new'
+
+ assert_select 'textarea[name=?]', 'issue[description]', :text => 'This is a description'
+ assert_select 'select[name=?]', 'issue[priority_id]' do
+ assert_select 'option[value=6][selected=selected]', :text => 'High'
+ end
+ # Custom fields
+ assert_select 'select[name=?]', 'issue[custom_field_values][1]' do
+ assert_select 'option[value=Oracle][selected=selected]', :text => 'Oracle'
+ end
+ assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Value for field 2'
+ end
+
+ def test_post_create_with_failure_should_preserve_watchers
+ assert !User.find(8).member_of?(Project.find(1))
+
+ @request.session[:user_id] = 2
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :watcher_user_ids => ['3', '8']}
+ assert_response :success
+ assert_template 'new'
+
+ assert_select 'input[name=?][value=2]:not(checked)', 'issue[watcher_user_ids][]'
+ assert_select 'input[name=?][value=3][checked=checked]', 'issue[watcher_user_ids][]'
+ assert_select 'input[name=?][value=8][checked=checked]', 'issue[watcher_user_ids][]'
+ end
+
+ def test_post_create_should_ignore_non_safe_attributes
+ @request.session[:user_id] = 2
+ assert_nothing_raised do
+ post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
+ end
+ end
+
+ def test_post_create_with_attachment
+ set_tmp_attachments_directory
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count' do
+ assert_difference 'Attachment.count' do
+ post :create, :project_id => 1,
+ :issue => { :tracker_id => '1', :subject => 'With attachment' },
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
+ end
+ end
+
+ issue = Issue.first(:order => 'id DESC')
+ attachment = Attachment.first(:order => 'id DESC')
+
+ assert_equal issue, attachment.container
+ assert_equal 2, attachment.author_id
+ assert_equal 'testfile.txt', attachment.filename
+ assert_equal 'text/plain', attachment.content_type
+ assert_equal 'test file', attachment.description
+ assert_equal 59, attachment.filesize
+ assert File.exists?(attachment.diskfile)
+ assert_equal 59, File.size(attachment.diskfile)
+ end
+
+ def test_post_create_with_failure_should_save_attachments
+ set_tmp_attachments_directory
+ @request.session[:user_id] = 2
+
+ assert_no_difference 'Issue.count' do
+ assert_difference 'Attachment.count' do
+ post :create, :project_id => 1,
+ :issue => { :tracker_id => '1', :subject => '' },
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
+ assert_response :success
+ assert_template 'new'
+ end
+ end
+
+ attachment = Attachment.first(:order => 'id DESC')
+ assert_equal 'testfile.txt', attachment.filename
+ assert File.exists?(attachment.diskfile)
+ assert_nil attachment.container
+
+ assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token
+ assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt'
+ end
+
+ def test_post_create_with_failure_should_keep_saved_attachments
+ set_tmp_attachments_directory
+ attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
+ @request.session[:user_id] = 2
+
+ assert_no_difference 'Issue.count' do
+ assert_no_difference 'Attachment.count' do
+ post :create, :project_id => 1,
+ :issue => { :tracker_id => '1', :subject => '' },
+ :attachments => {'p0' => {'token' => attachment.token}}
+ assert_response :success
+ assert_template 'new'
+ end
+ end
+
+ assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token
+ assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt'
+ end
+
+ def test_post_create_should_attach_saved_attachments
+ set_tmp_attachments_directory
+ attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count' do
+ assert_no_difference 'Attachment.count' do
+ post :create, :project_id => 1,
+ :issue => { :tracker_id => '1', :subject => 'Saved attachments' },
+ :attachments => {'p0' => {'token' => attachment.token}}
+ assert_response 302
+ end
+ end
+
+ issue = Issue.first(:order => 'id DESC')
+ assert_equal 1, issue.attachments.count
+
+ attachment.reload
+ assert_equal issue, attachment.container
+ end
+
+ context "without workflow privilege" do
+ setup do
+ WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id])
+ Role.anonymous.add_permission! :add_issues, :add_issue_notes
+ end
+
+ context "#new" do
+ should "propose default status only" do
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+ assert_select 'select[name=?]', 'issue[status_id]' do
+ assert_select 'option', 1
+ assert_select 'option[value=?]', IssueStatus.default.id.to_s
+ end
+ end
+
+ should "accept default status" do
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is an issue',
+ :status_id => 1}
+ end
+ issue = Issue.last(:order => 'id')
+ assert_equal IssueStatus.default, issue.status
+ end
+
+ should "ignore unauthorized status" do
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is an issue',
+ :status_id => 3}
+ end
+ issue = Issue.last(:order => 'id')
+ assert_equal IssueStatus.default, issue.status
+ end
+ end
+
+ context "#update" do
+ should "ignore status change" do
+ assert_difference 'Journal.count' do
+ put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'}
+ end
+ assert_equal 1, Issue.find(1).status_id
+ end
+
+ should "ignore attributes changes" do
+ assert_difference 'Journal.count' do
+ put :update, :id => 1, :issue => {:subject => 'changed', :assigned_to_id => 2, :notes => 'just trying'}
+ end
+ issue = Issue.find(1)
+ assert_equal "Can't print recipes", issue.subject
+ assert_nil issue.assigned_to
+ end
+ end
+ end
+
+ context "with workflow privilege" do
+ setup do
+ WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id])
+ WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
+ WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
+ Role.anonymous.add_permission! :add_issues, :add_issue_notes
+ end
+
+ context "#update" do
+ should "accept authorized status" do
+ assert_difference 'Journal.count' do
+ put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'}
+ end
+ assert_equal 3, Issue.find(1).status_id
+ end
+
+ should "ignore unauthorized status" do
+ assert_difference 'Journal.count' do
+ put :update, :id => 1, :issue => {:status_id => 2, :notes => 'just trying'}
+ end
+ assert_equal 1, Issue.find(1).status_id
+ end
+
+ should "accept authorized attributes changes" do
+ assert_difference 'Journal.count' do
+ put :update, :id => 1, :issue => {:assigned_to_id => 2, :notes => 'just trying'}
+ end
+ issue = Issue.find(1)
+ assert_equal 2, issue.assigned_to_id
+ end
+
+ should "ignore unauthorized attributes changes" do
+ assert_difference 'Journal.count' do
+ put :update, :id => 1, :issue => {:subject => 'changed', :notes => 'just trying'}
+ end
+ issue = Issue.find(1)
+ assert_equal "Can't print recipes", issue.subject
+ end
+ end
+
+ context "and :edit_issues permission" do
+ setup do
+ Role.anonymous.add_permission! :add_issues, :edit_issues
+ end
+
+ should "accept authorized status" do
+ assert_difference 'Journal.count' do
+ put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'}
+ end
+ assert_equal 3, Issue.find(1).status_id
+ end
+
+ should "ignore unauthorized status" do
+ assert_difference 'Journal.count' do
+ put :update, :id => 1, :issue => {:status_id => 2, :notes => 'just trying'}
+ end
+ assert_equal 1, Issue.find(1).status_id
+ end
+
+ should "accept authorized attributes changes" do
+ assert_difference 'Journal.count' do
+ put :update, :id => 1, :issue => {:subject => 'changed', :assigned_to_id => 2, :notes => 'just trying'}
+ end
+ issue = Issue.find(1)
+ assert_equal "changed", issue.subject
+ assert_equal 2, issue.assigned_to_id
+ end
+ end
+ end
+
+ def test_new_as_copy
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :copy_from => 1
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:issue)
+ orig = Issue.find(1)
+ assert_equal 1, assigns(:issue).project_id
+ assert_equal orig.subject, assigns(:issue).subject
+ assert assigns(:issue).copy?
+
+ assert_select 'form[id=issue-form][action=/projects/ecookbook/issues]' do
+ assert_select 'select[name=?]', 'issue[project_id]' do
+ assert_select 'option[value=1][selected=selected]', :text => 'eCookbook'
+ assert_select 'option[value=2]:not([selected])', :text => 'OnlineStore'
+ end
+ assert_select 'input[name=copy_from][value=1]'
+ end
+
+ # "New issue" menu item should not link to copy
+ assert_select '#main-menu a.new-issue[href=/projects/ecookbook/issues/new]'
+ end
+
+ def test_new_as_copy_with_attachments_should_show_copy_attachments_checkbox
+ @request.session[:user_id] = 2
+ issue = Issue.find(3)
+ assert issue.attachments.count > 0
+ get :new, :project_id => 1, :copy_from => 3
+
+ assert_select 'input[name=copy_attachments][type=checkbox][checked=checked][value=1]'
+ end
+
+ def test_new_as_copy_without_attachments_should_not_show_copy_attachments_checkbox
+ @request.session[:user_id] = 2
+ issue = Issue.find(3)
+ issue.attachments.delete_all
+ get :new, :project_id => 1, :copy_from => 3
+
+ assert_select 'input[name=copy_attachments]', 0
+ end
+
+ def test_new_as_copy_with_subtasks_should_show_copy_subtasks_checkbox
+ @request.session[:user_id] = 2
+ issue = Issue.generate_with_descendants!
+ get :new, :project_id => 1, :copy_from => issue.id
+
+ assert_select 'input[type=checkbox][name=copy_subtasks][checked=checked][value=1]'
+ end
+
+ def test_new_as_copy_with_invalid_issue_should_respond_with_404
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :copy_from => 99999
+ assert_response 404
+ end
+
+ def test_create_as_copy_on_different_project
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1, :copy_from => 1,
+ :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
+
+ assert_not_nil assigns(:issue)
+ assert assigns(:issue).copy?
+ end
+ issue = Issue.first(:order => 'id DESC')
+ assert_redirected_to "/issues/#{issue.id}"
+
+ assert_equal 2, issue.project_id
+ assert_equal 3, issue.tracker_id
+ assert_equal 'Copy', issue.subject
+ end
+
+ def test_create_as_copy_should_copy_attachments
+ @request.session[:user_id] = 2
+ issue = Issue.find(3)
+ count = issue.attachments.count
+ assert count > 0
+
+ assert_difference 'Issue.count' do
+ assert_difference 'Attachment.count', count do
+ assert_no_difference 'Journal.count' do
+ post :create, :project_id => 1, :copy_from => 3,
+ :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'},
+ :copy_attachments => '1'
+ end
+ end
+ end
+ copy = Issue.first(:order => 'id DESC')
+ assert_equal count, copy.attachments.count
+ assert_equal issue.attachments.map(&:filename).sort, copy.attachments.map(&:filename).sort
+ end
+
+ def test_create_as_copy_without_copy_attachments_option_should_not_copy_attachments
+ @request.session[:user_id] = 2
+ issue = Issue.find(3)
+ count = issue.attachments.count
+ assert count > 0
+
+ assert_difference 'Issue.count' do
+ assert_no_difference 'Attachment.count' do
+ assert_no_difference 'Journal.count' do
+ post :create, :project_id => 1, :copy_from => 3,
+ :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'}
+ end
+ end
+ end
+ copy = Issue.first(:order => 'id DESC')
+ assert_equal 0, copy.attachments.count
+ end
+
+ def test_create_as_copy_with_attachments_should_add_new_files
+ @request.session[:user_id] = 2
+ issue = Issue.find(3)
+ count = issue.attachments.count
+ assert count > 0
+
+ assert_difference 'Issue.count' do
+ assert_difference 'Attachment.count', count + 1 do
+ assert_no_difference 'Journal.count' do
+ post :create, :project_id => 1, :copy_from => 3,
+ :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'},
+ :copy_attachments => '1',
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
+ end
+ end
+ end
+ copy = Issue.first(:order => 'id DESC')
+ assert_equal count + 1, copy.attachments.count
+ end
+
+ def test_create_as_copy_should_add_relation_with_copied_issue
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count' do
+ assert_difference 'IssueRelation.count' do
+ post :create, :project_id => 1, :copy_from => 1,
+ :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
+ end
+ end
+ copy = Issue.first(:order => 'id DESC')
+ assert_equal 1, copy.relations.size
+ end
+
+ def test_create_as_copy_should_copy_subtasks
+ @request.session[:user_id] = 2
+ issue = Issue.generate_with_descendants!
+ count = issue.descendants.count
+
+ assert_difference 'Issue.count', count+1 do
+ assert_no_difference 'Journal.count' do
+ post :create, :project_id => 1, :copy_from => issue.id,
+ :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with subtasks'},
+ :copy_subtasks => '1'
+ end
+ end
+ copy = Issue.where(:parent_id => nil).first(:order => 'id DESC')
+ assert_equal count, copy.descendants.count
+ assert_equal issue.descendants.map(&:subject).sort, copy.descendants.map(&:subject).sort
+ end
+
+ def test_create_as_copy_without_copy_subtasks_option_should_not_copy_subtasks
+ @request.session[:user_id] = 2
+ issue = Issue.generate_with_descendants!
+
+ assert_difference 'Issue.count', 1 do
+ assert_no_difference 'Journal.count' do
+ post :create, :project_id => 1, :copy_from => 3,
+ :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with subtasks'}
+ end
+ end
+ copy = Issue.where(:parent_id => nil).first(:order => 'id DESC')
+ assert_equal 0, copy.descendants.count
+ end
+
+ def test_create_as_copy_with_failure
+ @request.session[:user_id] = 2
+ post :create, :project_id => 1, :copy_from => 1,
+ :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => ''}
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:issue)
+ assert assigns(:issue).copy?
+
+ assert_select 'form#issue-form[action=/projects/ecookbook/issues]' do
+ assert_select 'select[name=?]', 'issue[project_id]' do
+ assert_select 'option[value=1]:not([selected])', :text => 'eCookbook'
+ assert_select 'option[value=2][selected=selected]', :text => 'OnlineStore'
+ end
+ assert_select 'input[name=copy_from][value=1]'
+ end
+ end
+
+ def test_create_as_copy_on_project_without_permission_should_ignore_target_project
+ @request.session[:user_id] = 2
+ assert !User.find(2).member_of?(Project.find(4))
+
+ assert_difference 'Issue.count' do
+ post :create, :project_id => 1, :copy_from => 1,
+ :issue => {:project_id => '4', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
+ end
+ issue = Issue.first(:order => 'id DESC')
+ assert_equal 1, issue.project_id
+ end
+
+ def test_get_edit
+ @request.session[:user_id] = 2
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ assert_not_nil assigns(:issue)
+ assert_equal Issue.find(1), assigns(:issue)
+
+ # Be sure we don't display inactive IssuePriorities
+ assert ! IssuePriority.find(15).active?
+ assert_select 'select[name=?]', 'issue[priority_id]' do
+ assert_select 'option[value=15]', 0
+ end
+ end
+
+ def test_get_edit_should_display_the_time_entry_form_with_log_time_permission
+ @request.session[:user_id] = 2
+ Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time]
+
+ get :edit, :id => 1
+ assert_select 'input[name=?]', 'time_entry[hours]'
+ end
+
+ def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission
+ @request.session[:user_id] = 2
+ Role.find_by_name('Manager').remove_permission! :log_time
+
+ get :edit, :id => 1
+ assert_select 'input[name=?]', 'time_entry[hours]', 0
+ end
+
+ def test_get_edit_with_params
+ @request.session[:user_id] = 2
+ get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
+ :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => 10 }
+ assert_response :success
+ assert_template 'edit'
+
+ issue = assigns(:issue)
+ assert_not_nil issue
+
+ assert_equal 5, issue.status_id
+ assert_select 'select[name=?]', 'issue[status_id]' do
+ assert_select 'option[value=5][selected=selected]', :text => 'Closed'
+ end
+
+ assert_equal 7, issue.priority_id
+ assert_select 'select[name=?]', 'issue[priority_id]' do
+ assert_select 'option[value=7][selected=selected]', :text => 'Urgent'
+ end
+
+ assert_select 'input[name=?][value=2.5]', 'time_entry[hours]'
+ assert_select 'select[name=?]', 'time_entry[activity_id]' do
+ assert_select 'option[value=10][selected=selected]', :text => 'Development'
+ end
+ assert_select 'input[name=?][value=test_get_edit_with_params]', 'time_entry[comments]'
+ end
+
+ def test_get_edit_with_multi_custom_field
+ field = CustomField.find(1)
+ field.update_attribute :multiple, true
+ issue = Issue.find(1)
+ issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
+ issue.save!
+
+ @request.session[:user_id] = 2
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+
+ assert_select 'select[name=?][multiple=multiple]', 'issue[custom_field_values][1][]' do
+ assert_select 'option', 3
+ assert_select 'option[value=MySQL][selected=selected]'
+ assert_select 'option[value=Oracle][selected=selected]'
+ assert_select 'option[value=PostgreSQL]:not([selected])'
+ end
+ end
+
+ def test_update_form_for_existing_issue
+ @request.session[:user_id] = 2
+ xhr :put, :update_form, :project_id => 1,
+ :id => 1,
+ :issue => {:tracker_id => 2,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5}
+ assert_response :success
+ assert_equal 'text/javascript', response.content_type
+ assert_template 'update_form'
+ assert_template 'form'
+
+ issue = assigns(:issue)
+ assert_kind_of Issue, issue
+ assert_equal 1, issue.id
+ assert_equal 1, issue.project_id
+ assert_equal 2, issue.tracker_id
+ assert_equal 'This is the test_new issue', issue.subject
+ end
+
+ def test_update_form_for_existing_issue_should_keep_issue_author
+ @request.session[:user_id] = 3
+ xhr :put, :update_form, :project_id => 1, :id => 1, :issue => {:subject => 'Changed'}
+ assert_response :success
+ assert_equal 'text/javascript', response.content_type
+
+ issue = assigns(:issue)
+ assert_equal User.find(2), issue.author
+ assert_equal 2, issue.author_id
+ assert_not_equal User.current, issue.author
+ end
+
+ def test_update_form_for_existing_issue_should_propose_transitions_based_on_initial_status
+ @request.session[:user_id] = 2
+ WorkflowTransition.delete_all
+ WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1)
+ WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5)
+ WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 5, :new_status_id => 4)
+
+ xhr :put, :update_form, :project_id => 1,
+ :id => 2,
+ :issue => {:tracker_id => 2,
+ :status_id => 5,
+ :subject => 'This is an issue'}
+
+ assert_equal 5, assigns(:issue).status_id
+ assert_equal [1,2,5], assigns(:allowed_statuses).map(&:id).sort
+ end
+
+ def test_update_form_for_existing_issue_with_project_change
+ @request.session[:user_id] = 2
+ xhr :put, :update_form, :project_id => 1,
+ :id => 1,
+ :issue => {:project_id => 2,
+ :tracker_id => 2,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5}
+ assert_response :success
+ assert_template 'form'
+
+ issue = assigns(:issue)
+ assert_kind_of Issue, issue
+ assert_equal 1, issue.id
+ assert_equal 2, issue.project_id
+ assert_equal 2, issue.tracker_id
+ assert_equal 'This is the test_new issue', issue.subject
+ end
+
+ def test_put_update_without_custom_fields_param
+ @request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+
+ issue = Issue.find(1)
+ assert_equal '125', issue.custom_value_for(2).value
+ old_subject = issue.subject
+ new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
+
+ assert_difference('Journal.count') do
+ assert_difference('JournalDetail.count', 2) do
+ put :update, :id => 1, :issue => {:subject => new_subject,
+ :priority_id => '6',
+ :category_id => '1' # no change
+ }
+ end
+ end
+ assert_redirected_to :action => 'show', :id => '1'
+ issue.reload
+ assert_equal new_subject, issue.subject
+ # Make sure custom fields were not cleared
+ assert_equal '125', issue.custom_value_for(2).value
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
+ assert_mail_body_match "Subject changed from #{old_subject} to #{new_subject}", mail
+ end
+
+ def test_put_update_with_project_change
+ @request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+
+ assert_difference('Journal.count') do
+ assert_difference('JournalDetail.count', 3) do
+ put :update, :id => 1, :issue => {:project_id => '2',
+ :tracker_id => '1', # no change
+ :priority_id => '6',
+ :category_id => '3'
+ }
+ end
+ end
+ assert_redirected_to :action => 'show', :id => '1'
+ issue = Issue.find(1)
+ assert_equal 2, issue.project_id
+ assert_equal 1, issue.tracker_id
+ assert_equal 6, issue.priority_id
+ assert_equal 3, issue.category_id
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
+ assert_mail_body_match "Project changed from eCookbook to OnlineStore", mail
+ end
+
+ def test_put_update_with_tracker_change
+ @request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+
+ assert_difference('Journal.count') do
+ assert_difference('JournalDetail.count', 2) do
+ put :update, :id => 1, :issue => {:project_id => '1',
+ :tracker_id => '2',
+ :priority_id => '6'
+ }
+ end
+ end
+ assert_redirected_to :action => 'show', :id => '1'
+ issue = Issue.find(1)
+ assert_equal 1, issue.project_id
+ assert_equal 2, issue.tracker_id
+ assert_equal 6, issue.priority_id
+ assert_equal 1, issue.category_id
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
+ assert_mail_body_match "Tracker changed from Bug to Feature request", mail
+ end
+
+ def test_put_update_with_custom_field_change
+ @request.session[:user_id] = 2
+ issue = Issue.find(1)
+ assert_equal '125', issue.custom_value_for(2).value
+
+ assert_difference('Journal.count') do
+ assert_difference('JournalDetail.count', 3) do
+ put :update, :id => 1, :issue => {:subject => 'Custom field change',
+ :priority_id => '6',
+ :category_id => '1', # no change
+ :custom_field_values => { '2' => 'New custom value' }
+ }
+ end
+ end
+ assert_redirected_to :action => 'show', :id => '1'
+ issue.reload
+ assert_equal 'New custom value', issue.custom_value_for(2).value
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert_mail_body_match "Searchable field changed from 125 to New custom value", mail
+ end
+
+ def test_put_update_with_multi_custom_field_change
+ field = CustomField.find(1)
+ field.update_attribute :multiple, true
+ issue = Issue.find(1)
+ issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
+ issue.save!
+
+ @request.session[:user_id] = 2
+ assert_difference('Journal.count') do
+ assert_difference('JournalDetail.count', 3) do
+ put :update, :id => 1,
+ :issue => {
+ :subject => 'Custom field change',
+ :custom_field_values => { '1' => ['', 'Oracle', 'PostgreSQL'] }
+ }
+ end
+ end
+ assert_redirected_to :action => 'show', :id => '1'
+ assert_equal ['Oracle', 'PostgreSQL'], Issue.find(1).custom_field_value(1).sort
+ end
+
+ def test_put_update_with_status_and_assignee_change
+ issue = Issue.find(1)
+ assert_equal 1, issue.status_id
+ @request.session[:user_id] = 2
+ assert_difference('TimeEntry.count', 0) do
+ put :update,
+ :id => 1,
+ :issue => { :status_id => 2, :assigned_to_id => 3, :notes => 'Assigned to dlopper' },
+ :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
+ end
+ assert_redirected_to :action => 'show', :id => '1'
+ issue.reload
+ assert_equal 2, issue.status_id
+ j = Journal.order('id DESC').first
+ assert_equal 'Assigned to dlopper', j.notes
+ assert_equal 2, j.details.size
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_mail_body_match "Status changed from New to Assigned", mail
+ # subject should contain the new status
+ assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
+ end
+
+ def test_put_update_with_note_only
+ notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
+ # anonymous user
+ put :update,
+ :id => 1,
+ :issue => { :notes => notes }
+ assert_redirected_to :action => 'show', :id => '1'
+ j = Journal.order('id DESC').first
+ assert_equal notes, j.notes
+ assert_equal 0, j.details.size
+ assert_equal User.anonymous, j.user
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_mail_body_match notes, mail
+ end
+
+ def test_put_update_with_private_note_only
+ notes = 'Private note'
+ @request.session[:user_id] = 2
+
+ assert_difference 'Journal.count' do
+ put :update, :id => 1, :issue => {:notes => notes, :private_notes => '1'}
+ assert_redirected_to :action => 'show', :id => '1'
+ end
+
+ j = Journal.order('id DESC').first
+ assert_equal notes, j.notes
+ assert_equal true, j.private_notes
+ end
+
+ def test_put_update_with_private_note_and_changes
+ notes = 'Private note'
+ @request.session[:user_id] = 2
+
+ assert_difference 'Journal.count', 2 do
+ put :update, :id => 1, :issue => {:subject => 'New subject', :notes => notes, :private_notes => '1'}
+ assert_redirected_to :action => 'show', :id => '1'
+ end
+
+ j = Journal.order('id DESC').first
+ assert_equal notes, j.notes
+ assert_equal true, j.private_notes
+ assert_equal 0, j.details.count
+
+ j = Journal.order('id DESC').offset(1).first
+ assert_nil j.notes
+ assert_equal false, j.private_notes
+ assert_equal 1, j.details.count
+ end
+
+ def test_put_update_with_note_and_spent_time
+ @request.session[:user_id] = 2
+ spent_hours_before = Issue.find(1).spent_hours
+ assert_difference('TimeEntry.count') do
+ put :update,
+ :id => 1,
+ :issue => { :notes => '2.5 hours added' },
+ :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
+ end
+ assert_redirected_to :action => 'show', :id => '1'
+
+ issue = Issue.find(1)
+
+ j = Journal.order('id DESC').first
+ assert_equal '2.5 hours added', j.notes
+ assert_equal 0, j.details.size
+
+ t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
+ assert_not_nil t
+ assert_equal 2.5, t.hours
+ assert_equal spent_hours_before + 2.5, issue.spent_hours
+ end
+
+ def test_put_update_should_preserve_parent_issue_even_if_not_visible
+ parent = Issue.generate!(:project_id => 1, :is_private => true)
+ issue = Issue.generate!(:parent_issue_id => parent.id)
+ assert !parent.visible?(User.find(3))
+ @request.session[:user_id] = 3
+
+ get :edit, :id => issue.id
+ assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', parent.id.to_s
+
+ put :update, :id => issue.id, :issue => {:subject => 'New subject', :parent_issue_id => parent.id.to_s}
+ assert_response 302
+ assert_equal parent, issue.parent
+ end
+
+ def test_put_update_with_attachment_only
+ set_tmp_attachments_directory
+
+ # Delete all fixtured journals, a race condition can occur causing the wrong
+ # journal to get fetched in the next find.
+ Journal.delete_all
+
+ # anonymous user
+ assert_difference 'Attachment.count' do
+ put :update, :id => 1,
+ :issue => {:notes => ''},
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
+ end
+
+ assert_redirected_to :action => 'show', :id => '1'
+ j = Issue.find(1).journals.reorder('id DESC').first
+ assert j.notes.blank?
+ assert_equal 1, j.details.size
+ assert_equal 'testfile.txt', j.details.first.value
+ assert_equal User.anonymous, j.user
+
+ attachment = Attachment.first(:order => 'id DESC')
+ assert_equal Issue.find(1), attachment.container
+ assert_equal User.anonymous, attachment.author
+ assert_equal 'testfile.txt', attachment.filename
+ assert_equal 'text/plain', attachment.content_type
+ assert_equal 'test file', attachment.description
+ assert_equal 59, attachment.filesize
+ assert File.exists?(attachment.diskfile)
+ assert_equal 59, File.size(attachment.diskfile)
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_mail_body_match 'testfile.txt', mail
+ end
+
+ def test_put_update_with_failure_should_save_attachments
+ set_tmp_attachments_directory
+ @request.session[:user_id] = 2
+
+ assert_no_difference 'Journal.count' do
+ assert_difference 'Attachment.count' do
+ put :update, :id => 1,
+ :issue => { :subject => '' },
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
+ assert_response :success
+ assert_template 'edit'
+ end
+ end
+
+ attachment = Attachment.first(:order => 'id DESC')
+ assert_equal 'testfile.txt', attachment.filename
+ assert File.exists?(attachment.diskfile)
+ assert_nil attachment.container
+
+ assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token
+ assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt'
+ end
+
+ def test_put_update_with_failure_should_keep_saved_attachments
+ set_tmp_attachments_directory
+ attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
+ @request.session[:user_id] = 2
+
+ assert_no_difference 'Journal.count' do
+ assert_no_difference 'Attachment.count' do
+ put :update, :id => 1,
+ :issue => { :subject => '' },
+ :attachments => {'p0' => {'token' => attachment.token}}
+ assert_response :success
+ assert_template 'edit'
+ end
+ end
+
+ assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token
+ assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt'
+ end
+
+ def test_put_update_should_attach_saved_attachments
+ set_tmp_attachments_directory
+ attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
+ @request.session[:user_id] = 2
+
+ assert_difference 'Journal.count' do
+ assert_difference 'JournalDetail.count' do
+ assert_no_difference 'Attachment.count' do
+ put :update, :id => 1,
+ :issue => {:notes => 'Attachment added'},
+ :attachments => {'p0' => {'token' => attachment.token}}
+ assert_redirected_to '/issues/1'
+ end
+ end
+ end
+
+ attachment.reload
+ assert_equal Issue.find(1), attachment.container
+
+ journal = Journal.first(:order => 'id DESC')
+ assert_equal 1, journal.details.size
+ assert_equal 'testfile.txt', journal.details.first.value
+ end
+
+ def test_put_update_with_attachment_that_fails_to_save
+ set_tmp_attachments_directory
+
+ # Delete all fixtured journals, a race condition can occur causing the wrong
+ # journal to get fetched in the next find.
+ Journal.delete_all
+
+ # Mock out the unsaved attachment
+ Attachment.any_instance.stubs(:create).returns(Attachment.new)
+
+ # anonymous user
+ put :update,
+ :id => 1,
+ :issue => {:notes => ''},
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
+ assert_redirected_to :action => 'show', :id => '1'
+ assert_equal '1 file(s) could not be saved.', flash[:warning]
+ end
+
+ def test_put_update_with_no_change
+ issue = Issue.find(1)
+ issue.journals.clear
+ ActionMailer::Base.deliveries.clear
+
+ put :update,
+ :id => 1,
+ :issue => {:notes => ''}
+ assert_redirected_to :action => 'show', :id => '1'
+
+ issue.reload
+ assert issue.journals.empty?
+ # No email should be sent
+ assert ActionMailer::Base.deliveries.empty?
+ end
+
+ def test_put_update_should_send_a_notification
+ @request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+ issue = Issue.find(1)
+ old_subject = issue.subject
+ new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
+
+ put :update, :id => 1, :issue => {:subject => new_subject,
+ :priority_id => '6',
+ :category_id => '1' # no change
+ }
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_put_update_with_invalid_spent_time_hours_only
+ @request.session[:user_id] = 2
+ notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
+
+ assert_no_difference('Journal.count') do
+ put :update,
+ :id => 1,
+ :issue => {:notes => notes},
+ :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
+ end
+ assert_response :success
+ assert_template 'edit'
+
+ assert_error_tag :descendant => {:content => /Activity can't be blank/}
+ assert_select 'textarea[name=?]', 'issue[notes]', :text => notes
+ assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2z'
+ end
+
+ def test_put_update_with_invalid_spent_time_comments_only
+ @request.session[:user_id] = 2
+ notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
+
+ assert_no_difference('Journal.count') do
+ put :update,
+ :id => 1,
+ :issue => {:notes => notes},
+ :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""}
+ end
+ assert_response :success
+ assert_template 'edit'
+
+ assert_error_tag :descendant => {:content => /Activity can't be blank/}
+ assert_error_tag :descendant => {:content => /Hours can't be blank/}
+ assert_select 'textarea[name=?]', 'issue[notes]', :text => notes
+ assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'this is my comment'
+ end
+
+ def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
+ issue = Issue.find(2)
+ @request.session[:user_id] = 2
+
+ put :update,
+ :id => issue.id,
+ :issue => {
+ :fixed_version_id => 4
+ }
+
+ assert_response :redirect
+ issue.reload
+ assert_equal 4, issue.fixed_version_id
+ assert_not_equal issue.project_id, issue.fixed_version.project_id
+ end
+
+ def test_put_update_should_redirect_back_using_the_back_url_parameter
+ issue = Issue.find(2)
+ @request.session[:user_id] = 2
+
+ put :update,
+ :id => issue.id,
+ :issue => {
+ :fixed_version_id => 4
+ },
+ :back_url => '/issues'
+
+ assert_response :redirect
+ assert_redirected_to '/issues'
+ end
+
+ def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
+ issue = Issue.find(2)
+ @request.session[:user_id] = 2
+
+ put :update,
+ :id => issue.id,
+ :issue => {
+ :fixed_version_id => 4
+ },
+ :back_url => 'http://google.com'
+
+ assert_response :redirect
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
+ end
+
+ def test_get_bulk_edit
+ @request.session[:user_id] = 2
+ get :bulk_edit, :ids => [1, 2]
+ assert_response :success
+ assert_template 'bulk_edit'
+
+ assert_select 'ul#bulk-selection' do
+ assert_select 'li', 2
+ assert_select 'li a', :text => 'Bug #1'
+ end
+
+ assert_select 'form#bulk_edit_form[action=?]', '/issues/bulk_update' do
+ assert_select 'input[name=?]', 'ids[]', 2
+ assert_select 'input[name=?][value=1][type=hidden]', 'ids[]'
+
+ assert_select 'select[name=?]', 'issue[project_id]'
+ assert_select 'input[name=?]', 'issue[parent_issue_id]'
+
+ # Project specific custom field, date type
+ field = CustomField.find(9)
+ assert !field.is_for_all?
+ assert_equal 'date', field.field_format
+ assert_select 'input[name=?]', 'issue[custom_field_values][9]'
+
+ # System wide custom field
+ assert CustomField.find(1).is_for_all?
+ assert_select 'select[name=?]', 'issue[custom_field_values][1]'
+
+ # Be sure we don't display inactive IssuePriorities
+ assert ! IssuePriority.find(15).active?
+ assert_select 'select[name=?]', 'issue[priority_id]' do
+ assert_select 'option[value=15]', 0
+ end
+ end
+ end
+
+ def test_get_bulk_edit_on_different_projects
+ @request.session[:user_id] = 2
+ get :bulk_edit, :ids => [1, 2, 6]
+ assert_response :success
+ assert_template 'bulk_edit'
+
+ # Can not set issues from different projects as children of an issue
+ assert_select 'input[name=?]', 'issue[parent_issue_id]', 0
+
+ # Project specific custom field, date type
+ field = CustomField.find(9)
+ assert !field.is_for_all?
+ assert !field.project_ids.include?(Issue.find(6).project_id)
+ assert_select 'input[name=?]', 'issue[custom_field_values][9]', 0
+ end
+
+ def test_get_bulk_edit_with_user_custom_field
+ field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true)
+
+ @request.session[:user_id] = 2
+ get :bulk_edit, :ids => [1, 2]
+ assert_response :success
+ assert_template 'bulk_edit'
+
+ assert_select 'select.user_cf[name=?]', "issue[custom_field_values][#{field.id}]" do
+ assert_select 'option', Project.find(1).users.count + 2 # "no change" + "none" options
+ end
+ end
+
+ def test_get_bulk_edit_with_version_custom_field
+ field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true)
+
+ @request.session[:user_id] = 2
+ get :bulk_edit, :ids => [1, 2]
+ assert_response :success
+ assert_template 'bulk_edit'
+
+ assert_select 'select.version_cf[name=?]', "issue[custom_field_values][#{field.id}]" do
+ assert_select 'option', Project.find(1).shared_versions.count + 2 # "no change" + "none" options
+ end
+ end
+
+ def test_get_bulk_edit_with_multi_custom_field
+ field = CustomField.find(1)
+ field.update_attribute :multiple, true
+
+ @request.session[:user_id] = 2
+ get :bulk_edit, :ids => [1, 2]
+ assert_response :success
+ assert_template 'bulk_edit'
+
+ assert_select 'select[name=?]', 'issue[custom_field_values][1][]' do
+ assert_select 'option', field.possible_values.size + 1 # "none" options
+ end
+ end
+
+ def test_bulk_edit_should_only_propose_statuses_allowed_for_all_issues
+ WorkflowTransition.delete_all
+ WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 1)
+ WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
+ WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
+ WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1)
+ WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3)
+ WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5)
+ @request.session[:user_id] = 2
+ get :bulk_edit, :ids => [1, 2]
+
+ assert_response :success
+ statuses = assigns(:available_statuses)
+ assert_not_nil statuses
+ assert_equal [1, 3], statuses.map(&:id).sort
+
+ assert_select 'select[name=?]', 'issue[status_id]' do
+ assert_select 'option', 3 # 2 statuses + "no change" option
+ end
+ end
+
+ def test_bulk_edit_should_propose_target_project_open_shared_versions
+ @request.session[:user_id] = 2
+ post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1}
+ assert_response :success
+ assert_template 'bulk_edit'
+ assert_equal Project.find(1).shared_versions.open.all.sort, assigns(:versions).sort
+
+ assert_select 'select[name=?]', 'issue[fixed_version_id]' do
+ assert_select 'option', :text => '2.0'
+ end
+ end
+
+ def test_bulk_edit_should_propose_target_project_categories
+ @request.session[:user_id] = 2
+ post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1}
+ assert_response :success
+ assert_template 'bulk_edit'
+ assert_equal Project.find(1).issue_categories.sort, assigns(:categories).sort
+
+ assert_select 'select[name=?]', 'issue[category_id]' do
+ assert_select 'option', :text => 'Recipes'
+ end
+ end
+
+ def test_bulk_update
+ @request.session[:user_id] = 2
+ # update issues priority
+ post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
+ :issue => {:priority_id => 7,
+ :assigned_to_id => '',
+ :custom_field_values => {'2' => ''}}
+
+ assert_response 302
+ # check that the issues were updated
+ assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
+
+ issue = Issue.find(1)
+ journal = issue.journals.reorder('created_on DESC').first
+ assert_equal '125', issue.custom_value_for(2).value
+ assert_equal 'Bulk editing', journal.notes
+ assert_equal 1, journal.details.size
+ end
+
+ def test_bulk_update_with_group_assignee
+ group = Group.find(11)
+ project = Project.find(1)
+ project.members << Member.new(:principal => group, :roles => [Role.givable.first])
+
+ @request.session[:user_id] = 2
+ # update issues assignee
+ post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
+ :issue => {:priority_id => '',
+ :assigned_to_id => group.id,
+ :custom_field_values => {'2' => ''}}
+
+ assert_response 302
+ assert_equal [group, group], Issue.find_all_by_id([1, 2]).collect {|i| i.assigned_to}
+ end
+
+ def test_bulk_update_on_different_projects
+ @request.session[:user_id] = 2
+ # update issues priority
+ post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
+ :issue => {:priority_id => 7,
+ :assigned_to_id => '',
+ :custom_field_values => {'2' => ''}}
+
+ assert_response 302
+ # check that the issues were updated
+ assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
+
+ issue = Issue.find(1)
+ journal = issue.journals.reorder('created_on DESC').first
+ assert_equal '125', issue.custom_value_for(2).value
+ assert_equal 'Bulk editing', journal.notes
+ assert_equal 1, journal.details.size
+ end
+
+ def test_bulk_update_on_different_projects_without_rights
+ @request.session[:user_id] = 3
+ user = User.find(3)
+ action = { :controller => "issues", :action => "bulk_update" }
+ assert user.allowed_to?(action, Issue.find(1).project)
+ assert ! user.allowed_to?(action, Issue.find(6).project)
+ post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
+ :issue => {:priority_id => 7,
+ :assigned_to_id => '',
+ :custom_field_values => {'2' => ''}}
+ assert_response 403
+ assert_not_equal "Bulk should fail", Journal.last.notes
+ end
+
+ def test_bullk_update_should_send_a_notification
+ @request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+ post(:bulk_update,
+ {
+ :ids => [1, 2],
+ :notes => 'Bulk editing',
+ :issue => {
+ :priority_id => 7,
+ :assigned_to_id => '',
+ :custom_field_values => {'2' => ''}
+ }
+ })
+
+ assert_response 302
+ assert_equal 2, ActionMailer::Base.deliveries.size
+ end
+
+ def test_bulk_update_project
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
+ # Issues moved to project 2
+ assert_equal 2, Issue.find(1).project_id
+ assert_equal 2, Issue.find(2).project_id
+ # No tracker change
+ assert_equal 1, Issue.find(1).tracker_id
+ assert_equal 2, Issue.find(2).tracker_id
+ end
+
+ def test_bulk_update_project_on_single_issue_should_follow_when_needed
+ @request.session[:user_id] = 2
+ post :bulk_update, :id => 1, :issue => {:project_id => '2'}, :follow => '1'
+ assert_redirected_to '/issues/1'
+ end
+
+ def test_bulk_update_project_on_multiple_issues_should_follow_when_needed
+ @request.session[:user_id] = 2
+ post :bulk_update, :id => [1, 2], :issue => {:project_id => '2'}, :follow => '1'
+ assert_redirected_to '/projects/onlinestore/issues'
+ end
+
+ def test_bulk_update_tracker
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1, 2], :issue => {:tracker_id => '2'}
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
+ assert_equal 2, Issue.find(1).tracker_id
+ assert_equal 2, Issue.find(2).tracker_id
+ end
+
+ def test_bulk_update_status
+ @request.session[:user_id] = 2
+ # update issues priority
+ post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
+ :issue => {:priority_id => '',
+ :assigned_to_id => '',
+ :status_id => '5'}
+
+ assert_response 302
+ issue = Issue.find(1)
+ assert issue.closed?
+ end
+
+ def test_bulk_update_priority
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6}
+
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
+ assert_equal 6, Issue.find(1).priority_id
+ assert_equal 6, Issue.find(2).priority_id
+ end
+
+ def test_bulk_update_with_notes
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1, 2], :notes => 'Moving two issues'
+
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
+ assert_equal 'Moving two issues', Issue.find(1).journals.sort_by(&:id).last.notes
+ assert_equal 'Moving two issues', Issue.find(2).journals.sort_by(&:id).last.notes
+ end
+
+ def test_bulk_update_parent_id
+ IssueRelation.delete_all
+
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1, 3],
+ :notes => 'Bulk editing parent',
+ :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'}
+
+ assert_response 302
+ parent = Issue.find(2)
+ assert_equal parent.id, Issue.find(1).parent_id
+ assert_equal parent.id, Issue.find(3).parent_id
+ assert_equal [1, 3], parent.children.collect(&:id).sort
+ end
+
+ def test_bulk_update_custom_field
+ @request.session[:user_id] = 2
+ # update issues priority
+ post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
+ :issue => {:priority_id => '',
+ :assigned_to_id => '',
+ :custom_field_values => {'2' => '777'}}
+
+ assert_response 302
+
+ issue = Issue.find(1)
+ journal = issue.journals.reorder('created_on DESC').first
+ assert_equal '777', issue.custom_value_for(2).value
+ assert_equal 1, journal.details.size
+ assert_equal '125', journal.details.first.old_value
+ assert_equal '777', journal.details.first.value
+ end
+
+ def test_bulk_update_custom_field_to_blank
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1, 3], :notes => 'Bulk editing custom field',
+ :issue => {:priority_id => '',
+ :assigned_to_id => '',
+ :custom_field_values => {'1' => '__none__'}}
+ assert_response 302
+ assert_equal '', Issue.find(1).custom_field_value(1)
+ assert_equal '', Issue.find(3).custom_field_value(1)
+ end
+
+ def test_bulk_update_multi_custom_field
+ field = CustomField.find(1)
+ field.update_attribute :multiple, true
+
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1, 2, 3], :notes => 'Bulk editing multi custom field',
+ :issue => {:priority_id => '',
+ :assigned_to_id => '',
+ :custom_field_values => {'1' => ['MySQL', 'Oracle']}}
+
+ assert_response 302
+
+ assert_equal ['MySQL', 'Oracle'], Issue.find(1).custom_field_value(1).sort
+ assert_equal ['MySQL', 'Oracle'], Issue.find(3).custom_field_value(1).sort
+ # the custom field is not associated with the issue tracker
+ assert_nil Issue.find(2).custom_field_value(1)
+ end
+
+ def test_bulk_update_multi_custom_field_to_blank
+ field = CustomField.find(1)
+ field.update_attribute :multiple, true
+
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1, 3], :notes => 'Bulk editing multi custom field',
+ :issue => {:priority_id => '',
+ :assigned_to_id => '',
+ :custom_field_values => {'1' => ['__none__']}}
+ assert_response 302
+ assert_equal [''], Issue.find(1).custom_field_value(1)
+ assert_equal [''], Issue.find(3).custom_field_value(1)
+ end
+
+ def test_bulk_update_unassign
+ assert_not_nil Issue.find(2).assigned_to
+ @request.session[:user_id] = 2
+ # unassign issues
+ post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
+ assert_response 302
+ # check that the issues were updated
+ assert_nil Issue.find(2).assigned_to
+ end
+
+ def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
+ @request.session[:user_id] = 2
+
+ post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
+
+ assert_response :redirect
+ issues = Issue.find([1,2])
+ issues.each do |issue|
+ assert_equal 4, issue.fixed_version_id
+ assert_not_equal issue.project_id, issue.fixed_version.project_id
+ end
+ end
+
+ def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1,2], :back_url => '/issues'
+
+ assert_response :redirect
+ assert_redirected_to '/issues'
+ end
+
+ def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
+
+ assert_response :redirect
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
+ end
+
+ def test_bulk_update_with_failure_should_set_flash
+ @request.session[:user_id] = 2
+ Issue.update_all("subject = ''", "id = 2") # Make it invalid
+ post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6}
+
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
+ assert_equal 'Failed to save 1 issue(s) on 2 selected: #2.', flash[:error]
+ end
+
+ def test_get_bulk_copy
+ @request.session[:user_id] = 2
+ get :bulk_edit, :ids => [1, 2, 3], :copy => '1'
+ assert_response :success
+ assert_template 'bulk_edit'
+
+ issues = assigns(:issues)
+ assert_not_nil issues
+ assert_equal [1, 2, 3], issues.map(&:id).sort
+
+ assert_select 'input[name=copy_attachments]'
+ end
+
+ def test_bulk_copy_to_another_project
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count', 2 do
+ assert_no_difference 'Project.find(1).issues.count' do
+ post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}, :copy => '1'
+ end
+ end
+ assert_redirected_to '/projects/ecookbook/issues'
+
+ copies = Issue.all(:order => 'id DESC', :limit => issues.size)
+ copies.each do |copy|
+ assert_equal 2, copy.project_id
+ end
+ end
+
+ def test_bulk_copy_should_allow_not_changing_the_issue_attributes
+ @request.session[:user_id] = 2
+ issues = [
+ Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1, :priority_id => 2, :subject => 'issue 1', :author_id => 1, :assigned_to_id => nil),
+ Issue.create!(:project_id => 2, :tracker_id => 3, :status_id => 2, :priority_id => 1, :subject => 'issue 2', :author_id => 2, :assigned_to_id => 3)
+ ]
+
+ assert_difference 'Issue.count', issues.size do
+ post :bulk_update, :ids => issues.map(&:id), :copy => '1',
+ :issue => {
+ :project_id => '', :tracker_id => '', :assigned_to_id => '',
+ :status_id => '', :start_date => '', :due_date => ''
+ }
+ end
+
+ copies = Issue.all(:order => 'id DESC', :limit => issues.size)
+ issues.each do |orig|
+ copy = copies.detect {|c| c.subject == orig.subject}
+ assert_not_nil copy
+ assert_equal orig.project_id, copy.project_id
+ assert_equal orig.tracker_id, copy.tracker_id
+ assert_equal orig.status_id, copy.status_id
+ assert_equal orig.assigned_to_id, copy.assigned_to_id
+ assert_equal orig.priority_id, copy.priority_id
+ end
+ end
+
+ def test_bulk_copy_should_allow_changing_the_issue_attributes
+ # Fixes random test failure with Mysql
+ # where Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
+ # doesn't return the expected results
+ Issue.delete_all("project_id=2")
+
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count', 2 do
+ assert_no_difference 'Project.find(1).issues.count' do
+ post :bulk_update, :ids => [1, 2], :copy => '1',
+ :issue => {
+ :project_id => '2', :tracker_id => '', :assigned_to_id => '4',
+ :status_id => '1', :start_date => '2009-12-01', :due_date => '2009-12-31'
+ }
+ end
+ end
+
+ copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
+ assert_equal 2, copied_issues.size
+ copied_issues.each do |issue|
+ assert_equal 2, issue.project_id, "Project is incorrect"
+ assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
+ assert_equal 1, issue.status_id, "Status is incorrect"
+ assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
+ assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
+ end
+ end
+
+ def test_bulk_copy_should_allow_adding_a_note
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count', 1 do
+ post :bulk_update, :ids => [1], :copy => '1',
+ :notes => 'Copying one issue',
+ :issue => {
+ :project_id => '', :tracker_id => '', :assigned_to_id => '4',
+ :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31'
+ }
+ end
+
+ issue = Issue.first(:order => 'id DESC')
+ assert_equal 1, issue.journals.size
+ journal = issue.journals.first
+ assert_equal 0, journal.details.size
+ assert_equal 'Copying one issue', journal.notes
+ end
+
+ def test_bulk_copy_should_allow_not_copying_the_attachments
+ attachment_count = Issue.find(3).attachments.size
+ assert attachment_count > 0
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count', 1 do
+ assert_no_difference 'Attachment.count' do
+ post :bulk_update, :ids => [3], :copy => '1',
+ :issue => {
+ :project_id => ''
+ }
+ end
+ end
+ end
+
+ def test_bulk_copy_should_allow_copying_the_attachments
+ attachment_count = Issue.find(3).attachments.size
+ assert attachment_count > 0
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count', 1 do
+ assert_difference 'Attachment.count', attachment_count do
+ post :bulk_update, :ids => [3], :copy => '1', :copy_attachments => '1',
+ :issue => {
+ :project_id => ''
+ }
+ end
+ end
+ end
+
+ def test_bulk_copy_should_add_relations_with_copied_issues
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count', 2 do
+ assert_difference 'IssueRelation.count', 2 do
+ post :bulk_update, :ids => [1, 3], :copy => '1',
+ :issue => {
+ :project_id => '1'
+ }
+ end
+ end
+ end
+
+ def test_bulk_copy_should_allow_not_copying_the_subtasks
+ issue = Issue.generate_with_descendants!
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count', 1 do
+ post :bulk_update, :ids => [issue.id], :copy => '1',
+ :issue => {
+ :project_id => ''
+ }
+ end
+ end
+
+ def test_bulk_copy_should_allow_copying_the_subtasks
+ issue = Issue.generate_with_descendants!
+ count = issue.descendants.count
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count', count+1 do
+ post :bulk_update, :ids => [issue.id], :copy => '1', :copy_subtasks => '1',
+ :issue => {
+ :project_id => ''
+ }
+ end
+ copy = Issue.where(:parent_id => nil).order("id DESC").first
+ assert_equal count, copy.descendants.count
+ end
+
+ def test_bulk_copy_should_not_copy_selected_subtasks_twice
+ issue = Issue.generate_with_descendants!
+ count = issue.descendants.count
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count', count+1 do
+ post :bulk_update, :ids => issue.self_and_descendants.map(&:id), :copy => '1', :copy_subtasks => '1',
+ :issue => {
+ :project_id => ''
+ }
+ end
+ copy = Issue.where(:parent_id => nil).order("id DESC").first
+ assert_equal count, copy.descendants.count
+ end
+
+ def test_bulk_copy_to_another_project_should_follow_when_needed
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1], :copy => '1', :issue => {:project_id => 2}, :follow => '1'
+ issue = Issue.first(:order => 'id DESC')
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
+ end
+
+ def test_destroy_issue_with_no_time_entries
+ assert_nil TimeEntry.find_by_issue_id(2)
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count', -1 do
+ delete :destroy, :id => 2
+ end
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert_nil Issue.find_by_id(2)
+ end
+
+ def test_destroy_issues_with_time_entries
+ @request.session[:user_id] = 2
+
+ assert_no_difference 'Issue.count' do
+ delete :destroy, :ids => [1, 3]
+ end
+ assert_response :success
+ assert_template 'destroy'
+ assert_not_nil assigns(:hours)
+ assert Issue.find_by_id(1) && Issue.find_by_id(3)
+
+ assert_select 'form' do
+ assert_select 'input[name=_method][value=delete]'
+ end
+ end
+
+ def test_destroy_issues_and_destroy_time_entries
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count', -2 do
+ assert_difference 'TimeEntry.count', -3 do
+ delete :destroy, :ids => [1, 3], :todo => 'destroy'
+ end
+ end
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
+ assert_nil TimeEntry.find_by_id([1, 2])
+ end
+
+ def test_destroy_issues_and_assign_time_entries_to_project
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count', -2 do
+ assert_no_difference 'TimeEntry.count' do
+ delete :destroy, :ids => [1, 3], :todo => 'nullify'
+ end
+ end
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
+ assert_nil TimeEntry.find(1).issue_id
+ assert_nil TimeEntry.find(2).issue_id
+ end
+
+ def test_destroy_issues_and_reassign_time_entries_to_another_issue
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count', -2 do
+ assert_no_difference 'TimeEntry.count' do
+ delete :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
+ end
+ end
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
+ assert_equal 2, TimeEntry.find(1).issue_id
+ assert_equal 2, TimeEntry.find(2).issue_id
+ end
+
+ def test_destroy_issues_from_different_projects
+ @request.session[:user_id] = 2
+
+ assert_difference 'Issue.count', -3 do
+ delete :destroy, :ids => [1, 2, 6], :todo => 'destroy'
+ end
+ assert_redirected_to :controller => 'issues', :action => 'index'
+ assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
+ end
+
+ def test_destroy_parent_and_child_issues
+ parent = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Parent Issue')
+ child = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Child Issue', :parent_issue_id => parent.id)
+ assert child.is_descendant_of?(parent.reload)
+
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count', -2 do
+ delete :destroy, :ids => [parent.id, child.id], :todo => 'destroy'
+ end
+ assert_response 302
+ end
+
+ def test_destroy_invalid_should_respond_with_404
+ @request.session[:user_id] = 2
+ assert_no_difference 'Issue.count' do
+ delete :destroy, :id => 999
+ end
+ assert_response 404
+ end
+
+ def test_default_search_scope
+ get :index
+
+ assert_select 'div#quick-search form' do
+ assert_select 'input[name=issues][value=1][type=hidden]'
+ end
+ end
+end
diff --git a/test/functional/issues_controller_transaction_test.rb b/test/functional/issues_controller_transaction_test.rb
new file mode 100644
index 000000000..002b854f8
--- /dev/null
+++ b/test/functional/issues_controller_transaction_test.rb
@@ -0,0 +1,263 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+require 'issues_controller'
+
+class IssuesControllerTransactionTest < ActionController::TestCase
+ tests IssuesController
+ fixtures :projects,
+ :users,
+ :roles,
+ :members,
+ :member_roles,
+ :issues,
+ :issue_statuses,
+ :versions,
+ :trackers,
+ :projects_trackers,
+ :issue_categories,
+ :enabled_modules,
+ :enumerations,
+ :attachments,
+ :workflows,
+ :custom_fields,
+ :custom_values,
+ :custom_fields_projects,
+ :custom_fields_trackers,
+ :time_entries,
+ :journals,
+ :journal_details,
+ :queries
+
+ self.use_transactional_fixtures = false
+
+ def setup
+ User.current = nil
+ end
+
+ def test_update_stale_issue_should_not_update_the_issue
+ issue = Issue.find(2)
+ @request.session[:user_id] = 2
+
+ assert_no_difference 'Journal.count' do
+ assert_no_difference 'TimeEntry.count' do
+ put :update,
+ :id => issue.id,
+ :issue => {
+ :fixed_version_id => 4,
+ :notes => 'My notes',
+ :lock_version => (issue.lock_version - 1)
+ },
+ :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
+ end
+ end
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_select 'div.conflict'
+ assert_select 'input[name=?][value=?]', 'conflict_resolution', 'overwrite'
+ assert_select 'input[name=?][value=?]', 'conflict_resolution', 'add_notes'
+ assert_select 'label' do
+ assert_select 'input[name=?][value=?]', 'conflict_resolution', 'cancel'
+ assert_select 'a[href=/issues/2]'
+ end
+ end
+
+ def test_update_stale_issue_should_save_attachments
+ set_tmp_attachments_directory
+ issue = Issue.find(2)
+ @request.session[:user_id] = 2
+
+ assert_no_difference 'Journal.count' do
+ assert_no_difference 'TimeEntry.count' do
+ assert_difference 'Attachment.count' do
+ put :update,
+ :id => issue.id,
+ :issue => {
+ :fixed_version_id => 4,
+ :notes => 'My notes',
+ :lock_version => (issue.lock_version - 1)
+ },
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}},
+ :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
+ end
+ end
+ end
+
+ assert_response :success
+ assert_template 'edit'
+ attachment = Attachment.first(:order => 'id DESC')
+ assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
+ assert_tag 'input', :attributes => {:name => 'attachments[p0][filename]', :value => 'testfile.txt'}
+ end
+
+ def test_update_stale_issue_without_notes_should_not_show_add_notes_option
+ issue = Issue.find(2)
+ @request.session[:user_id] = 2
+
+ put :update, :id => issue.id,
+ :issue => {
+ :fixed_version_id => 4,
+ :notes => '',
+ :lock_version => (issue.lock_version - 1)
+ }
+
+ assert_tag 'div', :attributes => {:class => 'conflict'}
+ assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'}
+ assert_no_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'}
+ assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'}
+ end
+
+ def test_update_stale_issue_should_show_conflicting_journals
+ @request.session[:user_id] = 2
+
+ put :update, :id => 1,
+ :issue => {
+ :fixed_version_id => 4,
+ :notes => '',
+ :lock_version => 2
+ },
+ :last_journal_id => 1
+
+ assert_not_nil assigns(:conflict_journals)
+ assert_equal 1, assigns(:conflict_journals).size
+ assert_equal 2, assigns(:conflict_journals).first.id
+ assert_tag 'div', :attributes => {:class => 'conflict'},
+ :descendant => {:content => /Some notes with Redmine links/}
+ end
+
+ def test_update_stale_issue_without_previous_journal_should_show_all_journals
+ @request.session[:user_id] = 2
+
+ put :update, :id => 1,
+ :issue => {
+ :fixed_version_id => 4,
+ :notes => '',
+ :lock_version => 2
+ },
+ :last_journal_id => ''
+
+ assert_not_nil assigns(:conflict_journals)
+ assert_equal 2, assigns(:conflict_journals).size
+ assert_tag 'div', :attributes => {:class => 'conflict'},
+ :descendant => {:content => /Some notes with Redmine links/}
+ assert_tag 'div', :attributes => {:class => 'conflict'},
+ :descendant => {:content => /Journal notes/}
+ end
+
+ def test_update_stale_issue_should_show_private_journals_with_permission_only
+ journal = Journal.create!(:journalized => Issue.find(1), :notes => 'Privates notes', :private_notes => true, :user_id => 1)
+
+ @request.session[:user_id] = 2
+ put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => ''
+ assert_include journal, assigns(:conflict_journals)
+
+ Role.find(1).remove_permission! :view_private_notes
+ put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => ''
+ assert_not_include journal, assigns(:conflict_journals)
+ end
+
+ def test_update_stale_issue_with_overwrite_conflict_resolution_should_update
+ @request.session[:user_id] = 2
+
+ assert_difference 'Journal.count' do
+ put :update, :id => 1,
+ :issue => {
+ :fixed_version_id => 4,
+ :notes => 'overwrite_conflict_resolution',
+ :lock_version => 2
+ },
+ :conflict_resolution => 'overwrite'
+ end
+
+ assert_response 302
+ issue = Issue.find(1)
+ assert_equal 4, issue.fixed_version_id
+ journal = Journal.first(:order => 'id DESC')
+ assert_equal 'overwrite_conflict_resolution', journal.notes
+ assert journal.details.any?
+ end
+
+ def test_update_stale_issue_with_add_notes_conflict_resolution_should_update
+ @request.session[:user_id] = 2
+
+ assert_difference 'Journal.count' do
+ put :update, :id => 1,
+ :issue => {
+ :fixed_version_id => 4,
+ :notes => 'add_notes_conflict_resolution',
+ :lock_version => 2
+ },
+ :conflict_resolution => 'add_notes'
+ end
+
+ assert_response 302
+ issue = Issue.find(1)
+ assert_nil issue.fixed_version_id
+ journal = Journal.first(:order => 'id DESC')
+ assert_equal 'add_notes_conflict_resolution', journal.notes
+ assert journal.details.empty?
+ end
+
+ def test_update_stale_issue_with_cancel_conflict_resolution_should_redirect_without_updating
+ @request.session[:user_id] = 2
+
+ assert_no_difference 'Journal.count' do
+ put :update, :id => 1,
+ :issue => {
+ :fixed_version_id => 4,
+ :notes => 'add_notes_conflict_resolution',
+ :lock_version => 2
+ },
+ :conflict_resolution => 'cancel'
+ end
+
+ assert_redirected_to '/issues/1'
+ issue = Issue.find(1)
+ assert_nil issue.fixed_version_id
+ end
+
+ def test_put_update_with_spent_time_and_failure_should_not_add_spent_time
+ @request.session[:user_id] = 2
+
+ assert_no_difference('TimeEntry.count') do
+ put :update,
+ :id => 1,
+ :issue => { :subject => '' },
+ :time_entry => { :hours => '2.5', :comments => 'should not be added', :activity_id => TimeEntryActivity.first.id }
+ assert_response :success
+ end
+
+ assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2.5'
+ assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'should not be added'
+ assert_select 'select[name=?]', 'time_entry[activity_id]' do
+ assert_select 'option[value=?][selected=selected]', TimeEntryActivity.first.id
+ end
+ end
+
+ def test_index_should_rescue_invalid_sql_query
+ IssueQuery.any_instance.stubs(:statement).returns("INVALID STATEMENT")
+
+ get :index
+ assert_response 500
+ assert_tag 'p', :content => /An error occurred/
+ assert_nil session[:query]
+ assert_nil session[:issues_index_sort]
+ end
+end
diff --git a/test/functional/journals_controller_test.rb b/test/functional/journals_controller_test.rb
new file mode 100644
index 000000000..0d7e5a5a1
--- /dev/null
+++ b/test/functional/journals_controller_test.rb
@@ -0,0 +1,147 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class JournalsControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :members, :member_roles, :roles, :issues, :journals, :journal_details, :enabled_modules,
+ :trackers, :issue_statuses, :enumerations, :custom_fields, :custom_values, :custom_fields_projects
+
+ def setup
+ User.current = nil
+ end
+
+ def test_index
+ get :index, :project_id => 1
+ assert_response :success
+ assert_not_nil assigns(:journals)
+ assert_equal 'application/atom+xml', @response.content_type
+ end
+
+ def test_index_should_return_privates_notes_with_permission_only
+ journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true, :user_id => 1)
+ @request.session[:user_id] = 2
+
+ get :index, :project_id => 1
+ assert_response :success
+ assert_include journal, assigns(:journals)
+
+ Role.find(1).remove_permission! :view_private_notes
+ get :index, :project_id => 1
+ assert_response :success
+ assert_not_include journal, assigns(:journals)
+ end
+
+ def test_diff
+ get :diff, :id => 3, :detail_id => 4
+ assert_response :success
+ assert_template 'diff'
+
+ assert_tag 'span',
+ :attributes => {:class => 'diff_out'},
+ :content => /removed/
+ assert_tag 'span',
+ :attributes => {:class => 'diff_in'},
+ :content => /added/
+ end
+
+ def test_reply_to_issue
+ @request.session[:user_id] = 2
+ xhr :get, :new, :id => 6
+ assert_response :success
+ assert_template 'new'
+ assert_equal 'text/javascript', response.content_type
+ assert_include '> This is an issue', response.body
+ end
+
+ def test_reply_to_issue_without_permission
+ @request.session[:user_id] = 7
+ xhr :get, :new, :id => 6
+ assert_response 403
+ end
+
+ def test_reply_to_note
+ @request.session[:user_id] = 2
+ xhr :get, :new, :id => 6, :journal_id => 4
+ assert_response :success
+ assert_template 'new'
+ assert_equal 'text/javascript', response.content_type
+ assert_include '> A comment with a private version', response.body
+ end
+
+ def test_reply_to_private_note_should_fail_without_permission
+ journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true)
+ @request.session[:user_id] = 2
+
+ xhr :get, :new, :id => 2, :journal_id => journal.id
+ assert_response :success
+ assert_template 'new'
+ assert_equal 'text/javascript', response.content_type
+ assert_include '> Privates notes', response.body
+
+ Role.find(1).remove_permission! :view_private_notes
+ xhr :get, :new, :id => 2, :journal_id => journal.id
+ assert_response 404
+ end
+
+ def test_edit_xhr
+ @request.session[:user_id] = 1
+ xhr :get, :edit, :id => 2
+ assert_response :success
+ assert_template 'edit'
+ assert_equal 'text/javascript', response.content_type
+ assert_include 'textarea', response.body
+ end
+
+ def test_edit_private_note_should_fail_without_permission
+ journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true)
+ @request.session[:user_id] = 2
+ Role.find(1).add_permission! :edit_issue_notes
+
+ xhr :get, :edit, :id => journal.id
+ assert_response :success
+ assert_template 'edit'
+ assert_equal 'text/javascript', response.content_type
+ assert_include 'textarea', response.body
+
+ Role.find(1).remove_permission! :view_private_notes
+ xhr :get, :edit, :id => journal.id
+ assert_response 404
+ end
+
+ def test_update_xhr
+ @request.session[:user_id] = 1
+ xhr :post, :edit, :id => 2, :notes => 'Updated notes'
+ assert_response :success
+ assert_template 'update'
+ assert_equal 'text/javascript', response.content_type
+ assert_equal 'Updated notes', Journal.find(2).notes
+ assert_include 'journal-2-notes', response.body
+ end
+
+ def test_update_xhr_with_empty_notes_should_delete_the_journal
+ @request.session[:user_id] = 1
+ assert_difference 'Journal.count', -1 do
+ xhr :post, :edit, :id => 2, :notes => ''
+ assert_response :success
+ assert_template 'update'
+ assert_equal 'text/javascript', response.content_type
+ end
+ assert_nil Journal.find_by_id(2)
+ assert_include 'change-2', response.body
+ end
+end
diff --git a/test/functional/mail_handler_controller_test.rb b/test/functional/mail_handler_controller_test.rb
new file mode 100644
index 000000000..d9c70fa5a
--- /dev/null
+++ b/test/functional/mail_handler_controller_test.rb
@@ -0,0 +1,74 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class MailHandlerControllerTest < ActionController::TestCase
+ fixtures :users, :projects, :enabled_modules, :roles, :members, :member_roles, :issues, :issue_statuses,
+ :trackers, :projects_trackers, :enumerations
+
+ FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
+
+ def setup
+ User.current = nil
+ end
+
+ def test_should_create_issue
+ # Enable API and set a key
+ Setting.mail_handler_api_enabled = 1
+ Setting.mail_handler_api_key = 'secret'
+
+ assert_difference 'Issue.count' do
+ post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
+ end
+ assert_response 201
+ end
+
+ def test_should_respond_with_422_if_not_created
+ Project.find('onlinestore').destroy
+
+ Setting.mail_handler_api_enabled = 1
+ Setting.mail_handler_api_key = 'secret'
+
+ assert_no_difference 'Issue.count' do
+ post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
+ end
+ assert_response 422
+ end
+
+ def test_should_not_allow_with_api_disabled
+ # Disable API
+ Setting.mail_handler_api_enabled = 0
+ Setting.mail_handler_api_key = 'secret'
+
+ assert_no_difference 'Issue.count' do
+ post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
+ end
+ assert_response 403
+ end
+
+ def test_should_not_allow_with_wrong_key
+ # Disable API
+ Setting.mail_handler_api_enabled = 1
+ Setting.mail_handler_api_key = 'secret'
+
+ assert_no_difference 'Issue.count' do
+ post :index, :key => 'wrong', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
+ end
+ assert_response 403
+ end
+end
diff --git a/test/functional/members_controller_test.rb b/test/functional/members_controller_test.rb
new file mode 100644
index 000000000..365a4b4bd
--- /dev/null
+++ b/test/functional/members_controller_test.rb
@@ -0,0 +1,111 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class MembersControllerTest < ActionController::TestCase
+ fixtures :projects, :members, :member_roles, :roles, :users
+
+ def setup
+ User.current = nil
+ @request.session[:user_id] = 2
+ end
+
+ def test_create
+ assert_difference 'Member.count' do
+ post :create, :project_id => 1, :membership => {:role_ids => [1], :user_id => 7}
+ end
+ assert_redirected_to '/projects/ecookbook/settings/members'
+ assert User.find(7).member_of?(Project.find(1))
+ end
+
+ def test_create_multiple
+ assert_difference 'Member.count', 3 do
+ post :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]}
+ end
+ assert_redirected_to '/projects/ecookbook/settings/members'
+ assert User.find(7).member_of?(Project.find(1))
+ end
+
+ def test_xhr_create
+ assert_difference 'Member.count', 3 do
+ xhr :post, :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]}
+ assert_response :success
+ assert_template 'create'
+ assert_equal 'text/javascript', response.content_type
+ end
+ assert User.find(7).member_of?(Project.find(1))
+ assert User.find(8).member_of?(Project.find(1))
+ assert User.find(9).member_of?(Project.find(1))
+ assert_include 'tab-content-members', response.body
+ end
+
+ def test_xhr_create_with_failure
+ assert_no_difference 'Member.count' do
+ xhr :post, :create, :project_id => 1, :membership => {:role_ids => [], :user_ids => [7, 8, 9]}
+ assert_response :success
+ assert_template 'create'
+ assert_equal 'text/javascript', response.content_type
+ end
+ assert_match /alert/, response.body, "Alert message not sent"
+ end
+
+ def test_edit
+ assert_no_difference 'Member.count' do
+ put :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3}
+ end
+ assert_redirected_to '/projects/ecookbook/settings/members'
+ end
+
+ def test_xhr_edit
+ assert_no_difference 'Member.count' do
+ xhr :put, :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3}
+ assert_response :success
+ assert_template 'update'
+ assert_equal 'text/javascript', response.content_type
+ end
+ member = Member.find(2)
+ assert_equal [1], member.role_ids
+ assert_equal 3, member.user_id
+ assert_include 'tab-content-members', response.body
+ end
+
+ def test_destroy
+ assert_difference 'Member.count', -1 do
+ delete :destroy, :id => 2
+ end
+ assert_redirected_to '/projects/ecookbook/settings/members'
+ assert !User.find(3).member_of?(Project.find(1))
+ end
+
+ def test_xhr_destroy
+ assert_difference 'Member.count', -1 do
+ xhr :delete, :destroy, :id => 2
+ assert_response :success
+ assert_template 'destroy'
+ assert_equal 'text/javascript', response.content_type
+ end
+ assert_nil Member.find_by_id(2)
+ assert_include 'tab-content-members', response.body
+ end
+
+ def test_autocomplete
+ get :autocomplete, :project_id => 1, :q => 'mis', :format => 'js'
+ assert_response :success
+ assert_include 'User Misc', response.body
+ end
+end
diff --git a/test/functional/messages_controller_test.rb b/test/functional/messages_controller_test.rb
new file mode 100644
index 000000000..a4dee170d
--- /dev/null
+++ b/test/functional/messages_controller_test.rb
@@ -0,0 +1,217 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class MessagesControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules
+
+ def setup
+ User.current = nil
+ end
+
+ def test_show
+ get :show, :board_id => 1, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:board)
+ assert_not_nil assigns(:project)
+ assert_not_nil assigns(:topic)
+ end
+
+ def test_show_should_contain_reply_field_tags_for_quoting
+ @request.session[:user_id] = 2
+ get :show, :board_id => 1, :id => 1
+ assert_response :success
+
+ # tags required by MessagesController#quote
+ assert_tag 'input', :attributes => {:id => 'message_subject'}
+ assert_tag 'textarea', :attributes => {:id => 'message_content'}
+ assert_tag 'div', :attributes => {:id => 'reply'}
+ end
+
+ def test_show_with_pagination
+ message = Message.find(1)
+ assert_difference 'Message.count', 30 do
+ 30.times do
+ message.children << Message.new(:subject => 'Reply', :content => 'Reply body', :author_id => 2, :board_id => 1)
+ end
+ end
+ get :show, :board_id => 1, :id => 1, :r => message.children.last(:order => 'id').id
+ assert_response :success
+ assert_template 'show'
+ replies = assigns(:replies)
+ assert_not_nil replies
+ assert !replies.include?(message.children.first(:order => 'id'))
+ assert replies.include?(message.children.last(:order => 'id'))
+ end
+
+ def test_show_with_reply_permission
+ @request.session[:user_id] = 2
+ get :show, :board_id => 1, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_tag :div, :attributes => { :id => 'reply' },
+ :descendant => { :tag => 'textarea', :attributes => { :id => 'message_content' } }
+ end
+
+ def test_show_message_not_found
+ get :show, :board_id => 1, :id => 99999
+ assert_response 404
+ end
+
+ def test_show_message_from_invalid_board_should_respond_with_404
+ get :show, :board_id => 999, :id => 1
+ assert_response 404
+ end
+
+ def test_get_new
+ @request.session[:user_id] = 2
+ get :new, :board_id => 1
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_get_new_with_invalid_board
+ @request.session[:user_id] = 2
+ get :new, :board_id => 99
+ assert_response 404
+ end
+
+ def test_post_new
+ @request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+
+ with_settings :notified_events => %w(message_posted) do
+ post :new, :board_id => 1,
+ :message => { :subject => 'Test created message',
+ :content => 'Message body'}
+ end
+ message = Message.find_by_subject('Test created message')
+ assert_not_nil message
+ assert_redirected_to "/boards/1/topics/#{message.to_param}"
+ assert_equal 'Message body', message.content
+ assert_equal 2, message.author_id
+ assert_equal 1, message.board_id
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert_equal "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] Test created message", mail.subject
+ assert_mail_body_match 'Message body', mail
+ # author
+ assert mail.bcc.include?('jsmith@somenet.foo')
+ # project member
+ assert mail.bcc.include?('dlopper@somenet.foo')
+ end
+
+ def test_get_edit
+ @request.session[:user_id] = 2
+ get :edit, :board_id => 1, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_post_edit
+ @request.session[:user_id] = 2
+ post :edit, :board_id => 1, :id => 1,
+ :message => { :subject => 'New subject',
+ :content => 'New body'}
+ assert_redirected_to '/boards/1/topics/1'
+ message = Message.find(1)
+ assert_equal 'New subject', message.subject
+ assert_equal 'New body', message.content
+ end
+
+ def test_post_edit_sticky_and_locked
+ @request.session[:user_id] = 2
+ post :edit, :board_id => 1, :id => 1,
+ :message => { :subject => 'New subject',
+ :content => 'New body',
+ :locked => '1',
+ :sticky => '1'}
+ assert_redirected_to '/boards/1/topics/1'
+ message = Message.find(1)
+ assert_equal true, message.sticky?
+ assert_equal true, message.locked?
+ end
+
+ def test_post_edit_should_allow_to_change_board
+ @request.session[:user_id] = 2
+ post :edit, :board_id => 1, :id => 1,
+ :message => { :subject => 'New subject',
+ :content => 'New body',
+ :board_id => 2}
+ assert_redirected_to '/boards/2/topics/1'
+ message = Message.find(1)
+ assert_equal Board.find(2), message.board
+ end
+
+ def test_reply
+ @request.session[:user_id] = 2
+ post :reply, :board_id => 1, :id => 1, :reply => { :content => 'This is a test reply', :subject => 'Test reply' }
+ reply = Message.order('id DESC').first
+ assert_redirected_to "/boards/1/topics/1?r=#{reply.id}"
+ assert Message.find_by_subject('Test reply')
+ end
+
+ def test_destroy_topic
+ @request.session[:user_id] = 2
+ assert_difference 'Message.count', -3 do
+ post :destroy, :board_id => 1, :id => 1
+ end
+ assert_redirected_to '/projects/ecookbook/boards/1'
+ assert_nil Message.find_by_id(1)
+ end
+
+ def test_destroy_reply
+ @request.session[:user_id] = 2
+ assert_difference 'Message.count', -1 do
+ post :destroy, :board_id => 1, :id => 2
+ end
+ assert_redirected_to '/boards/1/topics/1?r=2'
+ assert_nil Message.find_by_id(2)
+ end
+
+ def test_quote
+ @request.session[:user_id] = 2
+ xhr :get, :quote, :board_id => 1, :id => 3
+ assert_response :success
+ assert_equal 'text/javascript', response.content_type
+ assert_template 'quote'
+ assert_include 'RE: First post', response.body
+ assert_include '> An other reply', response.body
+ end
+
+ def test_preview_new
+ @request.session[:user_id] = 2
+ post :preview,
+ :board_id => 1,
+ :message => {:subject => "", :content => "Previewed text"}
+ assert_response :success
+ assert_template 'common/_preview'
+ end
+
+ def test_preview_edit
+ @request.session[:user_id] = 2
+ post :preview,
+ :id => 4,
+ :board_id => 1,
+ :message => {:subject => "", :content => "Previewed text"}
+ assert_response :success
+ assert_template 'common/_preview'
+ end
+end
diff --git a/test/functional/my_controller_test.rb b/test/functional/my_controller_test.rb
new file mode 100644
index 000000000..7e9677051
--- /dev/null
+++ b/test/functional/my_controller_test.rb
@@ -0,0 +1,248 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class MyControllerTest < ActionController::TestCase
+ fixtures :users, :user_preferences, :roles, :projects, :members, :member_roles,
+ :issues, :issue_statuses, :trackers, :enumerations, :custom_fields, :auth_sources
+
+ def setup
+ @request.session[:user_id] = 2
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'page'
+ end
+
+ def test_page
+ get :page
+ assert_response :success
+ assert_template 'page'
+ end
+
+ def test_page_with_timelog_block
+ preferences = User.find(2).pref
+ preferences[:my_page_layout] = {'top' => ['timelog']}
+ preferences.save!
+ TimeEntry.create!(:user => User.find(2), :spent_on => Date.yesterday, :issue_id => 1, :hours => 2.5, :activity_id => 10)
+
+ get :page
+ assert_response :success
+ assert_select 'tr.time-entry' do
+ assert_select 'td.subject a[href=/issues/1]'
+ assert_select 'td.hours', :text => '2.50'
+ end
+ end
+
+ def test_page_with_all_blocks
+ blocks = MyController::BLOCKS.keys
+ preferences = User.find(2).pref
+ preferences[:my_page_layout] = {'top' => blocks}
+ preferences.save!
+
+ get :page
+ assert_response :success
+ assert_select 'div.mypage-box', blocks.size
+ end
+
+ def test_my_account_should_show_editable_custom_fields
+ get :account
+ assert_response :success
+ assert_template 'account'
+ assert_equal User.find(2), assigns(:user)
+
+ assert_tag :input, :attributes => { :name => 'user[custom_field_values][4]'}
+ end
+
+ def test_my_account_should_not_show_non_editable_custom_fields
+ UserCustomField.find(4).update_attribute :editable, false
+
+ get :account
+ assert_response :success
+ assert_template 'account'
+ assert_equal User.find(2), assigns(:user)
+
+ assert_no_tag :input, :attributes => { :name => 'user[custom_field_values][4]'}
+ end
+
+ def test_update_account
+ post :account,
+ :user => {
+ :firstname => "Joe",
+ :login => "root",
+ :admin => 1,
+ :group_ids => ['10'],
+ :custom_field_values => {"4" => "0100562500"}
+ }
+
+ assert_redirected_to '/my/account'
+ user = User.find(2)
+ assert_equal user, assigns(:user)
+ assert_equal "Joe", user.firstname
+ assert_equal "jsmith", user.login
+ assert_equal "0100562500", user.custom_value_for(4).value
+ # ignored
+ assert !user.admin?
+ assert user.groups.empty?
+ end
+
+ def test_my_account_should_show_destroy_link
+ get :account
+ assert_select 'a[href=/my/account/destroy]'
+ end
+
+ def test_get_destroy_should_display_the_destroy_confirmation
+ get :destroy
+ assert_response :success
+ assert_template 'destroy'
+ assert_select 'form[action=/my/account/destroy]' do
+ assert_select 'input[name=confirm]'
+ end
+ end
+
+ def test_post_destroy_without_confirmation_should_not_destroy_account
+ assert_no_difference 'User.count' do
+ post :destroy
+ end
+ assert_response :success
+ assert_template 'destroy'
+ end
+
+ def test_post_destroy_without_confirmation_should_destroy_account
+ assert_difference 'User.count', -1 do
+ post :destroy, :confirm => '1'
+ end
+ assert_redirected_to '/'
+ assert_match /deleted/i, flash[:notice]
+ end
+
+ def test_post_destroy_with_unsubscribe_not_allowed_should_not_destroy_account
+ User.any_instance.stubs(:own_account_deletable?).returns(false)
+
+ assert_no_difference 'User.count' do
+ post :destroy, :confirm => '1'
+ end
+ assert_redirected_to '/my/account'
+ end
+
+ def test_change_password
+ get :password
+ assert_response :success
+ assert_template 'password'
+
+ # non matching password confirmation
+ post :password, :password => 'jsmith',
+ :new_password => 'secret123',
+ :new_password_confirmation => 'secret1234'
+ assert_response :success
+ assert_template 'password'
+ assert_error_tag :content => /Password doesn't match confirmation/
+
+ # wrong password
+ post :password, :password => 'wrongpassword',
+ :new_password => 'secret123',
+ :new_password_confirmation => 'secret123'
+ assert_response :success
+ assert_template 'password'
+ assert_equal 'Wrong password', flash[:error]
+
+ # good password
+ post :password, :password => 'jsmith',
+ :new_password => 'secret123',
+ :new_password_confirmation => 'secret123'
+ assert_redirected_to '/my/account'
+ assert User.try_to_login('jsmith', 'secret123')
+ end
+
+ def test_change_password_should_redirect_if_user_cannot_change_its_password
+ User.find(2).update_attribute(:auth_source_id, 1)
+
+ get :password
+ assert_not_nil flash[:error]
+ assert_redirected_to '/my/account'
+ end
+
+ def test_page_layout
+ get :page_layout
+ assert_response :success
+ assert_template 'page_layout'
+ end
+
+ def test_add_block
+ post :add_block, :block => 'issuesreportedbyme'
+ assert_redirected_to '/my/page_layout'
+ assert User.find(2).pref[:my_page_layout]['top'].include?('issuesreportedbyme')
+ end
+
+ def test_add_invalid_block_should_redirect
+ post :add_block, :block => 'invalid'
+ assert_redirected_to '/my/page_layout'
+ end
+
+ def test_remove_block
+ post :remove_block, :block => 'issuesassignedtome'
+ assert_redirected_to '/my/page_layout'
+ assert !User.find(2).pref[:my_page_layout].values.flatten.include?('issuesassignedtome')
+ end
+
+ def test_order_blocks
+ xhr :post, :order_blocks, :group => 'left', 'blocks' => ['documents', 'calendar', 'latestnews']
+ assert_response :success
+ assert_equal ['documents', 'calendar', 'latestnews'], User.find(2).pref[:my_page_layout]['left']
+ end
+
+ def test_reset_rss_key_with_existing_key
+ @previous_token_value = User.find(2).rss_key # Will generate one if it's missing
+ post :reset_rss_key
+
+ assert_not_equal @previous_token_value, User.find(2).rss_key
+ assert User.find(2).rss_token
+ assert_match /reset/, flash[:notice]
+ assert_redirected_to '/my/account'
+ end
+
+ def test_reset_rss_key_without_existing_key
+ assert_nil User.find(2).rss_token
+ post :reset_rss_key
+
+ assert User.find(2).rss_token
+ assert_match /reset/, flash[:notice]
+ assert_redirected_to '/my/account'
+ end
+
+ def test_reset_api_key_with_existing_key
+ @previous_token_value = User.find(2).api_key # Will generate one if it's missing
+ post :reset_api_key
+
+ assert_not_equal @previous_token_value, User.find(2).api_key
+ assert User.find(2).api_token
+ assert_match /reset/, flash[:notice]
+ assert_redirected_to '/my/account'
+ end
+
+ def test_reset_api_key_without_existing_key
+ assert_nil User.find(2).api_token
+ post :reset_api_key
+
+ assert User.find(2).api_token
+ assert_match /reset/, flash[:notice]
+ assert_redirected_to '/my/account'
+ end
+end
diff --git a/test/functional/news_controller_test.rb b/test/functional/news_controller_test.rb
new file mode 100644
index 000000000..65ee30e4b
--- /dev/null
+++ b/test/functional/news_controller_test.rb
@@ -0,0 +1,165 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class NewsControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news, :comments
+
+ def setup
+ User.current = nil
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:newss)
+ assert_nil assigns(:project)
+ end
+
+ def test_index_with_project
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:newss)
+ end
+
+ def test_index_with_invalid_project_should_respond_with_404
+ get :index, :project_id => 999
+ assert_response 404
+ end
+
+ def test_show
+ get :show, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_tag :tag => 'h2', :content => /eCookbook first release/
+ end
+
+ def test_show_should_show_attachments
+ attachment = Attachment.first
+ attachment.container = News.find(1)
+ attachment.save!
+
+ get :show, :id => 1
+ assert_response :success
+ assert_tag 'a', :content => attachment.filename
+ end
+
+ def test_show_not_found
+ get :show, :id => 999
+ assert_response 404
+ end
+
+ def test_get_new
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_post_create
+ ActionMailer::Base.deliveries.clear
+ @request.session[:user_id] = 2
+
+ with_settings :notified_events => %w(news_added) do
+ post :create, :project_id => 1, :news => { :title => 'NewsControllerTest',
+ :description => 'This is the description',
+ :summary => '' }
+ end
+ assert_redirected_to '/projects/ecookbook/news'
+
+ news = News.find_by_title('NewsControllerTest')
+ assert_not_nil news
+ assert_equal 'This is the description', news.description
+ assert_equal User.find(2), news.author
+ assert_equal Project.find(1), news.project
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_post_create_with_attachment
+ set_tmp_attachments_directory
+ @request.session[:user_id] = 2
+ assert_difference 'News.count' do
+ assert_difference 'Attachment.count' do
+ post :create, :project_id => 1,
+ :news => { :title => 'Test', :description => 'This is the description' },
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
+ end
+ end
+ attachment = Attachment.first(:order => 'id DESC')
+ news = News.first(:order => 'id DESC')
+ assert_equal news, attachment.container
+ end
+
+ def test_post_create_with_validation_failure
+ @request.session[:user_id] = 2
+ post :create, :project_id => 1, :news => { :title => '',
+ :description => 'This is the description',
+ :summary => '' }
+ assert_response :success
+ assert_template 'new'
+ assert_not_nil assigns(:news)
+ assert assigns(:news).new_record?
+ assert_error_tag :content => /title can't be blank/i
+ end
+
+ def test_get_edit
+ @request.session[:user_id] = 2
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_put_update
+ @request.session[:user_id] = 2
+ put :update, :id => 1, :news => { :description => 'Description changed by test_post_edit' }
+ assert_redirected_to '/news/1'
+ news = News.find(1)
+ assert_equal 'Description changed by test_post_edit', news.description
+ end
+
+ def test_put_update_with_attachment
+ set_tmp_attachments_directory
+ @request.session[:user_id] = 2
+ assert_no_difference 'News.count' do
+ assert_difference 'Attachment.count' do
+ put :update, :id => 1,
+ :news => { :description => 'This is the description' },
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
+ end
+ end
+ attachment = Attachment.first(:order => 'id DESC')
+ assert_equal News.find(1), attachment.container
+ end
+
+ def test_update_with_failure
+ @request.session[:user_id] = 2
+ put :update, :id => 1, :news => { :description => '' }
+ assert_response :success
+ assert_template 'edit'
+ assert_error_tag :content => /description can't be blank/i
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 2
+ delete :destroy, :id => 1
+ assert_redirected_to '/projects/ecookbook/news'
+ assert_nil News.find_by_id(1)
+ end
+end
diff --git a/test/functional/previews_controller_test.rb b/test/functional/previews_controller_test.rb
new file mode 100644
index 000000000..f99ccb401
--- /dev/null
+++ b/test/functional/previews_controller_test.rb
@@ -0,0 +1,81 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class PreviewsControllerTest < ActionController::TestCase
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules,
+ :journals, :journal_details,
+ :news
+
+ def test_preview_new_issue
+ @request.session[:user_id] = 2
+ post :issue, :project_id => '1', :issue => {:description => 'Foo'}
+ assert_response :success
+ assert_template 'preview'
+ assert_not_nil assigns(:description)
+ end
+
+ def test_preview_issue_notes
+ @request.session[:user_id] = 2
+ post :issue, :project_id => '1', :id => 1,
+ :issue => {:description => Issue.find(1).description, :notes => 'Foo'}
+ assert_response :success
+ assert_template 'preview'
+ assert_not_nil assigns(:notes)
+ end
+
+ def test_preview_journal_notes_for_update
+ @request.session[:user_id] = 2
+ post :issue, :project_id => '1', :id => 1, :notes => 'Foo'
+ assert_response :success
+ assert_template 'preview'
+ assert_not_nil assigns(:notes)
+ assert_tag :p, :content => 'Foo'
+ end
+
+ def test_preview_new_news
+ get :news, :project_id => 1,
+ :news => {:title => '',
+ :description => 'News description',
+ :summary => ''}
+ assert_response :success
+ assert_template 'common/_preview'
+ assert_tag :tag => 'fieldset', :attributes => { :class => 'preview' },
+ :content => /News description/
+ end
+
+ def test_existing_new_news
+ get :news, :project_id => 1, :id => 2,
+ :news => {:title => '',
+ :description => 'News description',
+ :summary => ''}
+ assert_response :success
+ assert_template 'common/_preview'
+ assert_equal News.find(2), assigns(:previewed)
+ assert_not_nil assigns(:attachments)
+
+ assert_tag :tag => 'fieldset', :attributes => { :class => 'preview' },
+ :content => /News description/
+ end
+end
diff --git a/test/functional/project_enumerations_controller_test.rb b/test/functional/project_enumerations_controller_test.rb
new file mode 100644
index 000000000..a41d22948
--- /dev/null
+++ b/test/functional/project_enumerations_controller_test.rb
@@ -0,0 +1,217 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class ProjectEnumerationsControllerTest < ActionController::TestCase
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules,
+ :custom_fields, :custom_fields_projects,
+ :custom_fields_trackers, :custom_values,
+ :time_entries
+
+ self.use_transactional_fixtures = false
+
+ def setup
+ @request.session[:user_id] = nil
+ Setting.default_language = 'en'
+ end
+
+ def test_update_to_override_system_activities
+ @request.session[:user_id] = 2 # manager
+ billable_field = TimeEntryActivityCustomField.find_by_name("Billable")
+
+ put :update, :project_id => 1, :enumerations => {
+ "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate
+ "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value
+ "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value
+ "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes
+ }
+
+ assert_response :redirect
+ assert_redirected_to '/projects/ecookbook/settings/activities'
+
+ # Created project specific activities...
+ project = Project.find('ecookbook')
+
+ # ... Design
+ design = project.time_entry_activities.find_by_name("Design")
+ assert design, "Project activity not found"
+
+ assert_equal 9, design.parent_id # Relate to the system activity
+ assert_not_equal design.parent.id, design.id # Different records
+ assert_equal design.parent.name, design.name # Same name
+ assert !design.active?
+
+ # ... Development
+ development = project.time_entry_activities.find_by_name("Development")
+ assert development, "Project activity not found"
+
+ assert_equal 10, development.parent_id # Relate to the system activity
+ assert_not_equal development.parent.id, development.id # Different records
+ assert_equal development.parent.name, development.name # Same name
+ assert development.active?
+ assert_equal "0", development.custom_value_for(billable_field).value
+
+ # ... Inactive Activity
+ previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity")
+ assert previously_inactive, "Project activity not found"
+
+ assert_equal 14, previously_inactive.parent_id # Relate to the system activity
+ assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records
+ assert_equal previously_inactive.parent.name, previously_inactive.name # Same name
+ assert previously_inactive.active?
+ assert_equal "1", previously_inactive.custom_value_for(billable_field).value
+
+ # ... QA
+ assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified"
+ end
+
+ def test_update_will_update_project_specific_activities
+ @request.session[:user_id] = 2 # manager
+
+ project_activity = TimeEntryActivity.new({
+ :name => 'Project Specific',
+ :parent => TimeEntryActivity.first,
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity.save
+ project_activity_two = TimeEntryActivity.new({
+ :name => 'Project Specific Two',
+ :parent => TimeEntryActivity.last,
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity_two.save
+
+
+ put :update, :project_id => 1, :enumerations => {
+ project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate
+ project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate
+ }
+
+ assert_response :redirect
+ assert_redirected_to '/projects/ecookbook/settings/activities'
+
+ # Created project specific activities...
+ project = Project.find('ecookbook')
+ assert_equal 2, project.time_entry_activities.count
+
+ activity_one = project.time_entry_activities.find_by_name(project_activity.name)
+ assert activity_one, "Project activity not found"
+ assert_equal project_activity.id, activity_one.id
+ assert !activity_one.active?
+
+ activity_two = project.time_entry_activities.find_by_name(project_activity_two.name)
+ assert activity_two, "Project activity not found"
+ assert_equal project_activity_two.id, activity_two.id
+ assert !activity_two.active?
+ end
+
+ def test_update_when_creating_new_activities_will_convert_existing_data
+ assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
+
+ @request.session[:user_id] = 2 # manager
+ put :update, :project_id => 1, :enumerations => {
+ "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"} # Design, De-activate
+ }
+ assert_response :redirect
+
+ # No more TimeEntries using the system activity
+ assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries still assigned to system activities"
+ # All TimeEntries using project activity
+ project_specific_activity = TimeEntryActivity.find_by_parent_id_and_project_id(9, 1)
+ assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_specific_activity.id, 1).size, "No Time Entries assigned to the project activity"
+ end
+
+ def test_update_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised
+ # TODO: Need to cause an exception on create but these tests
+ # aren't setup for mocking. Just create a record now so the
+ # second one is a dupicate
+ parent = TimeEntryActivity.find(9)
+ TimeEntryActivity.create!({:name => parent.name, :project_id => 1, :position => parent.position, :active => true})
+ TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1), :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'})
+
+ assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
+ assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size
+
+ @request.session[:user_id] = 2 # manager
+ put :update, :project_id => 1, :enumerations => {
+ "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design
+ "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"} # Development, Change custom value
+ }
+ assert_response :redirect
+
+ # TimeEntries shouldn't have been reassigned on the failed record
+ assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries are not assigned to system activities"
+ # TimeEntries shouldn't have been reassigned on the saved record either
+ assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size, "Time Entries are not assigned to system activities"
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 2 # manager
+ project_activity = TimeEntryActivity.new({
+ :name => 'Project Specific',
+ :parent => TimeEntryActivity.first,
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity.save
+ project_activity_two = TimeEntryActivity.new({
+ :name => 'Project Specific Two',
+ :parent => TimeEntryActivity.last,
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity_two.save
+
+ delete :destroy, :project_id => 1
+ assert_response :redirect
+ assert_redirected_to '/projects/ecookbook/settings/activities'
+
+ assert_nil TimeEntryActivity.find_by_id(project_activity.id)
+ assert_nil TimeEntryActivity.find_by_id(project_activity_two.id)
+ end
+
+ def test_destroy_should_reassign_time_entries_back_to_the_system_activity
+ @request.session[:user_id] = 2 # manager
+ project_activity = TimeEntryActivity.new({
+ :name => 'Project Specific Design',
+ :parent => TimeEntryActivity.find(9),
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity.save
+ assert TimeEntry.update_all("activity_id = '#{project_activity.id}'", ["project_id = ? AND activity_id = ?", 1, 9])
+ assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size
+
+ delete :destroy, :project_id => 1
+ assert_response :redirect
+ assert_redirected_to '/projects/ecookbook/settings/activities'
+
+ assert_nil TimeEntryActivity.find_by_id(project_activity.id)
+ assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size, "TimeEntries still assigned to project specific activity"
+ assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "TimeEntries still assigned to project specific activity"
+ end
+
+end
diff --git a/test/functional/queries_controller_test.rb b/test/functional/queries_controller_test.rb
new file mode 100644
index 000000000..a232fec2e
--- /dev/null
+++ b/test/functional/queries_controller_test.rb
@@ -0,0 +1,290 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class QueriesControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries, :enabled_modules
+
+ def setup
+ User.current = nil
+ end
+
+ def test_index
+ get :index
+ # HTML response not implemented
+ assert_response 406
+ end
+
+ def test_new_project_query
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]',
+ :checked => nil }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => nil,
+ :disabled => nil }
+ assert_select 'select[name=?]', 'c[]' do
+ assert_select 'option[value=tracker]'
+ assert_select 'option[value=subject]'
+ end
+ end
+
+ def test_new_global_query
+ @request.session[:user_id] = 2
+ get :new
+ assert_response :success
+ assert_template 'new'
+ assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]' }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => 'checked',
+ :disabled => nil }
+ end
+
+ def test_new_on_invalid_project
+ @request.session[:user_id] = 2
+ get :new, :project_id => 'invalid'
+ assert_response 404
+ end
+
+ def test_create_project_public_query
+ @request.session[:user_id] = 2
+ post :create,
+ :project_id => 'ecookbook',
+ :default_columns => '1',
+ :f => ["status_id", "assigned_to_id"],
+ :op => {"assigned_to_id" => "=", "status_id" => "o"},
+ :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
+ :query => {"name" => "test_new_project_public_query", "is_public" => "1"}
+
+ q = Query.find_by_name('test_new_project_public_query')
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
+ assert q.is_public?
+ assert q.has_default_columns?
+ assert q.valid?
+ end
+
+ def test_create_project_private_query
+ @request.session[:user_id] = 3
+ post :create,
+ :project_id => 'ecookbook',
+ :default_columns => '1',
+ :fields => ["status_id", "assigned_to_id"],
+ :operators => {"assigned_to_id" => "=", "status_id" => "o"},
+ :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
+ :query => {"name" => "test_new_project_private_query", "is_public" => "1"}
+
+ q = Query.find_by_name('test_new_project_private_query')
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
+ assert !q.is_public?
+ assert q.has_default_columns?
+ assert q.valid?
+ end
+
+ def test_create_global_private_query_with_custom_columns
+ @request.session[:user_id] = 3
+ post :create,
+ :fields => ["status_id", "assigned_to_id"],
+ :operators => {"assigned_to_id" => "=", "status_id" => "o"},
+ :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
+ :query => {"name" => "test_new_global_private_query", "is_public" => "1"},
+ :c => ["", "tracker", "subject", "priority", "category"]
+
+ q = Query.find_by_name('test_new_global_private_query')
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
+ assert !q.is_public?
+ assert !q.has_default_columns?
+ assert_equal [:id, :tracker, :subject, :priority, :category], q.columns.collect {|c| c.name}
+ assert q.valid?
+ end
+
+ def test_create_global_query_with_custom_filters
+ @request.session[:user_id] = 3
+ post :create,
+ :fields => ["assigned_to_id"],
+ :operators => {"assigned_to_id" => "="},
+ :values => { "assigned_to_id" => ["me"]},
+ :query => {"name" => "test_new_global_query"}
+
+ q = Query.find_by_name('test_new_global_query')
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
+ assert !q.has_filter?(:status_id)
+ assert_equal ['assigned_to_id'], q.filters.keys
+ assert q.valid?
+ end
+
+ def test_create_with_sort
+ @request.session[:user_id] = 1
+ post :create,
+ :default_columns => '1',
+ :operators => {"status_id" => "o"},
+ :values => {"status_id" => ["1"]},
+ :query => {:name => "test_new_with_sort",
+ :is_public => "1",
+ :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
+
+ query = Query.find_by_name("test_new_with_sort")
+ assert_not_nil query
+ assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
+ end
+
+ def test_create_with_failure
+ @request.session[:user_id] = 2
+ assert_no_difference '::Query.count' do
+ post :create, :project_id => 'ecookbook', :query => {:name => ''}
+ end
+ assert_response :success
+ assert_template 'new'
+ assert_select 'input[name=?]', 'query[name]'
+ end
+
+ def test_edit_global_public_query
+ @request.session[:user_id] = 1
+ get :edit, :id => 4
+ assert_response :success
+ assert_template 'edit'
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]',
+ :checked => 'checked' }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => 'checked',
+ :disabled => 'disabled' }
+ end
+
+ def test_edit_global_private_query
+ @request.session[:user_id] = 3
+ get :edit, :id => 3
+ assert_response :success
+ assert_template 'edit'
+ assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]' }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => 'checked',
+ :disabled => 'disabled' }
+ end
+
+ def test_edit_project_private_query
+ @request.session[:user_id] = 3
+ get :edit, :id => 2
+ assert_response :success
+ assert_template 'edit'
+ assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]' }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => nil,
+ :disabled => nil }
+ end
+
+ def test_edit_project_public_query
+ @request.session[:user_id] = 2
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]',
+ :checked => 'checked'
+ }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => nil,
+ :disabled => 'disabled' }
+ end
+
+ def test_edit_sort_criteria
+ @request.session[:user_id] = 1
+ get :edit, :id => 5
+ assert_response :success
+ assert_template 'edit'
+ assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
+ :child => { :tag => 'option', :attributes => { :value => 'priority',
+ :selected => 'selected' } }
+ assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
+ :child => { :tag => 'option', :attributes => { :value => 'desc',
+ :selected => 'selected' } }
+ end
+
+ def test_edit_invalid_query
+ @request.session[:user_id] = 2
+ get :edit, :id => 99
+ assert_response 404
+ end
+
+ def test_udpate_global_private_query
+ @request.session[:user_id] = 3
+ put :update,
+ :id => 3,
+ :default_columns => '1',
+ :fields => ["status_id", "assigned_to_id"],
+ :operators => {"assigned_to_id" => "=", "status_id" => "o"},
+ :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
+ :query => {"name" => "test_edit_global_private_query", "is_public" => "1"}
+
+ assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
+ q = Query.find_by_name('test_edit_global_private_query')
+ assert !q.is_public?
+ assert q.has_default_columns?
+ assert q.valid?
+ end
+
+ def test_update_global_public_query
+ @request.session[:user_id] = 1
+ put :update,
+ :id => 4,
+ :default_columns => '1',
+ :fields => ["status_id", "assigned_to_id"],
+ :operators => {"assigned_to_id" => "=", "status_id" => "o"},
+ :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
+ :query => {"name" => "test_edit_global_public_query", "is_public" => "1"}
+
+ assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
+ q = Query.find_by_name('test_edit_global_public_query')
+ assert q.is_public?
+ assert q.has_default_columns?
+ assert q.valid?
+ end
+
+ def test_update_with_failure
+ @request.session[:user_id] = 1
+ put :update, :id => 4, :query => {:name => ''}
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 2
+ delete :destroy, :id => 1
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
+ assert_nil Query.find_by_id(1)
+ end
+
+ def test_backslash_should_be_escaped_in_filters
+ @request.session[:user_id] = 2
+ get :new, :subject => 'foo/bar'
+ assert_response :success
+ assert_template 'new'
+ assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body
+ end
+end
diff --git a/test/functional/reports_controller_test.rb b/test/functional/reports_controller_test.rb
new file mode 100644
index 000000000..56a1aca3f
--- /dev/null
+++ b/test/functional/reports_controller_test.rb
@@ -0,0 +1,67 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class ReportsControllerTest < ActionController::TestCase
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules,
+ :versions
+
+ def test_get_issue_report
+ get :issue_report, :id => 1
+
+ assert_response :success
+ assert_template 'issue_report'
+
+ [:issues_by_tracker, :issues_by_version, :issues_by_category, :issues_by_assigned_to,
+ :issues_by_author, :issues_by_subproject, :issues_by_priority].each do |ivar|
+ assert_not_nil assigns(ivar)
+ end
+
+ assert_equal IssuePriority.all.reverse, assigns(:priorities)
+ end
+
+ def test_get_issue_report_details
+ %w(tracker version priority category assigned_to author subproject).each do |detail|
+ get :issue_report_details, :id => 1, :detail => detail
+
+ assert_response :success
+ assert_template 'issue_report_details'
+ assert_not_nil assigns(:field)
+ assert_not_nil assigns(:rows)
+ assert_not_nil assigns(:data)
+ assert_not_nil assigns(:report_title)
+ end
+ end
+
+ def test_get_issue_report_details_by_priority
+ get :issue_report_details, :id => 1, :detail => 'priority'
+ assert_equal IssuePriority.all.reverse, assigns(:rows)
+ end
+
+ def test_get_issue_report_details_with_an_invalid_detail
+ get :issue_report_details, :id => 1, :detail => 'invalid'
+
+ assert_redirected_to '/projects/ecookbook/issues/report'
+ end
+end
diff --git a/test/functional/repositories_bazaar_controller_test.rb b/test/functional/repositories_bazaar_controller_test.rb
new file mode 100644
index 000000000..3da5400b4
--- /dev/null
+++ b/test/functional/repositories_bazaar_controller_test.rb
@@ -0,0 +1,198 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class RepositoriesBazaarControllerTest < ActionController::TestCase
+ tests RepositoriesController
+
+ fixtures :projects, :users, :roles, :members, :member_roles,
+ :repositories, :enabled_modules
+
+ REPOSITORY_PATH = Rails.root.join('tmp/test/bazaar_repository/trunk').to_s
+ PRJ_ID = 3
+
+ def setup
+ User.current = nil
+ @project = Project.find(PRJ_ID)
+ @repository = Repository::Bazaar.create(
+ :project => @project,
+ :url => REPOSITORY_PATH,
+ :log_encoding => 'UTF-8')
+ assert @repository
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_get_new
+ @request.session[:user_id] = 1
+ @project.repository.destroy
+ get :new, :project_id => 'subproject1', :repository_scm => 'Bazaar'
+ assert_response :success
+ assert_template 'new'
+ assert_kind_of Repository::Bazaar, assigns(:repository)
+ assert assigns(:repository).new_record?
+ end
+
+ def test_browse_root
+ get :show, :id => PRJ_ID
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal 2, assigns(:entries).size
+ assert assigns(:entries).detect {|e| e.name == 'directory' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'doc-mkdir.txt' && e.kind == 'file'}
+ end
+
+ def test_browse_directory
+ get :show, :id => PRJ_ID, :path => repository_path_hash(['directory'])[:param]
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['doc-ls.txt', 'document.txt', 'edit.png'], assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+ assert_not_nil entry
+ assert_equal 'file', entry.kind
+ assert_equal 'directory/edit.png', entry.path
+ end
+
+ def test_browse_at_given_revision
+ get :show, :id => PRJ_ID, :path => repository_path_hash([])[:param],
+ :rev => 3
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['directory', 'doc-deleted.txt', 'doc-ls.txt', 'doc-mkdir.txt'],
+ assigns(:entries).collect(&:name)
+ end
+
+ def test_changes
+ get :changes, :id => PRJ_ID,
+ :path => repository_path_hash(['doc-mkdir.txt'])[:param]
+ assert_response :success
+ assert_template 'changes'
+ assert_tag :tag => 'h2', :content => 'doc-mkdir.txt'
+ end
+
+ def test_entry_show
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['directory', 'doc-ls.txt'])[:param]
+ assert_response :success
+ assert_template 'entry'
+ # Line 19
+ assert_tag :tag => 'th',
+ :content => /29/,
+ :attributes => { :class => /line-num/ },
+ :sibling => { :tag => 'td', :content => /Show help message/ }
+ end
+
+ def test_entry_download
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['directory', 'doc-ls.txt'])[:param],
+ :format => 'raw'
+ assert_response :success
+ # File content
+ assert @response.body.include?('Show help message')
+ end
+
+ def test_directory_entry
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['directory'])[:param]
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entry)
+ assert_equal 'directory', assigns(:entry).name
+ end
+
+ def test_diff
+ # Full diff of changeset 3
+ ['inline', 'sbs'].each do |dt|
+ get :diff, :id => PRJ_ID, :rev => 3, :type => dt
+ assert_response :success
+ assert_template 'diff'
+ # Line 11 removed
+ assert_tag :tag => 'th',
+ :content => '11',
+ :sibling => { :tag => 'td',
+ :attributes => { :class => /diff_out/ },
+ :content => /Display more information/ }
+ end
+ end
+
+ def test_annotate
+ get :annotate, :id => PRJ_ID,
+ :path => repository_path_hash(['doc-mkdir.txt'])[:param]
+ assert_response :success
+ assert_template 'annotate'
+ assert_tag :tag => 'th', :content => '2',
+ :sibling => {
+ :tag => 'td',
+ :child => {
+ :tag => 'a',
+ :content => '3'
+ }
+ }
+ assert_tag :tag => 'th', :content => '2',
+ :sibling => { :tag => 'td', :content => /jsmith/ }
+ assert_tag :tag => 'th', :content => '2',
+ :sibling => {
+ :tag => 'td',
+ :child => {
+ :tag => 'a',
+ :content => '3'
+ }
+ }
+ assert_tag :tag => 'th', :content => '2',
+ :sibling => { :tag => 'td', :content => /Main purpose/ }
+ end
+
+ def test_destroy_valid_repository
+ @request.session[:user_id] = 1 # admin
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ assert @repository.changesets.count > 0
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+
+ def test_destroy_invalid_repository
+ @request.session[:user_id] = 1 # admin
+ @project.repository.destroy
+ @repository = Repository::Bazaar.create!(
+ :project => @project,
+ :url => "/invalid",
+ :log_encoding => 'UTF-8')
+ @repository.fetch_changesets
+ @repository.reload
+ assert_equal 0, @repository.changesets.count
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+ else
+ puts "Bazaar test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+end
diff --git a/test/functional/repositories_controller_test.rb b/test/functional/repositories_controller_test.rb
new file mode 100644
index 000000000..315090c59
--- /dev/null
+++ b/test/functional/repositories_controller_test.rb
@@ -0,0 +1,264 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class RepositoriesControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
+ :repositories, :issues, :issue_statuses, :changesets, :changes,
+ :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
+
+ def setup
+ User.current = nil
+ end
+
+ def test_new
+ @request.session[:user_id] = 1
+ get :new, :project_id => 'subproject1'
+ assert_response :success
+ assert_template 'new'
+ assert_kind_of Repository::Subversion, assigns(:repository)
+ assert assigns(:repository).new_record?
+ assert_tag 'input', :attributes => {:name => 'repository[url]', :disabled => nil}
+ end
+
+ def test_new_should_propose_enabled_scm_only
+ @request.session[:user_id] = 1
+ with_settings :enabled_scm => ['Mercurial', 'Git'] do
+ get :new, :project_id => 'subproject1'
+ end
+ assert_response :success
+ assert_template 'new'
+ assert_kind_of Repository::Mercurial, assigns(:repository)
+ assert_tag 'select', :attributes => {:name => 'repository_scm'},
+ :children => {:count => 3}
+ assert_tag 'select', :attributes => {:name => 'repository_scm'},
+ :child => {:tag => 'option', :attributes => {:value => 'Mercurial', :selected => 'selected'}}
+ assert_tag 'select', :attributes => {:name => 'repository_scm'},
+ :child => {:tag => 'option', :attributes => {:value => 'Git', :selected => nil}}
+ end
+
+ def test_create
+ @request.session[:user_id] = 1
+ assert_difference 'Repository.count' do
+ post :create, :project_id => 'subproject1',
+ :repository_scm => 'Subversion',
+ :repository => {:url => 'file:///test', :is_default => '1', :identifier => ''}
+ end
+ assert_response 302
+ repository = Repository.first(:order => 'id DESC')
+ assert_kind_of Repository::Subversion, repository
+ assert_equal 'file:///test', repository.url
+ end
+
+ def test_create_with_failure
+ @request.session[:user_id] = 1
+ assert_no_difference 'Repository.count' do
+ post :create, :project_id => 'subproject1',
+ :repository_scm => 'Subversion',
+ :repository => {:url => 'invalid'}
+ end
+ assert_response :success
+ assert_template 'new'
+ assert_kind_of Repository::Subversion, assigns(:repository)
+ assert assigns(:repository).new_record?
+ end
+
+ def test_edit
+ @request.session[:user_id] = 1
+ get :edit, :id => 11
+ assert_response :success
+ assert_template 'edit'
+ assert_equal Repository.find(11), assigns(:repository)
+ assert_tag 'input', :attributes => {:name => 'repository[url]', :value => 'svn://localhost/test', :disabled => 'disabled'}
+ end
+
+ def test_update
+ @request.session[:user_id] = 1
+ put :update, :id => 11, :repository => {:password => 'test_update'}
+ assert_response 302
+ assert_equal 'test_update', Repository.find(11).password
+ end
+
+ def test_update_with_failure
+ @request.session[:user_id] = 1
+ put :update, :id => 11, :repository => {:password => 'x'*260}
+ assert_response :success
+ assert_template 'edit'
+ assert_equal Repository.find(11), assigns(:repository)
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 1
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => 11
+ end
+ assert_response 302
+ assert_nil Repository.find_by_id(11)
+ end
+
+ def test_revisions
+ get :revisions, :id => 1
+ assert_response :success
+ assert_template 'revisions'
+ assert_equal Repository.find(10), assigns(:repository)
+ assert_not_nil assigns(:changesets)
+ end
+
+ def test_revisions_for_other_repository
+ repository = Repository::Subversion.create!(:project_id => 1, :identifier => 'foo', :url => 'file:///foo')
+
+ get :revisions, :id => 1, :repository_id => 'foo'
+ assert_response :success
+ assert_template 'revisions'
+ assert_equal repository, assigns(:repository)
+ assert_not_nil assigns(:changesets)
+ end
+
+ def test_revisions_for_invalid_repository
+ get :revisions, :id => 1, :repository_id => 'foo'
+ assert_response 404
+ end
+
+ def test_revision
+ get :revision, :id => 1, :rev => 1
+ assert_response :success
+ assert_not_nil assigns(:changeset)
+ assert_equal "1", assigns(:changeset).revision
+ end
+
+ def test_revision_should_not_change_the_project_menu_link
+ get :revision, :id => 1, :rev => 1
+ assert_response :success
+
+ assert_tag 'a', :attributes => {:href => '/projects/ecookbook/repository', :class => /repository/},
+ :ancestor => {:attributes => {:id => 'main-menu'}}
+ end
+
+ def test_revision_with_before_nil_and_afer_normal
+ get :revision, {:id => 1, :rev => 1}
+ assert_response :success
+ assert_template 'revision'
+ assert_no_tag :tag => "div", :attributes => { :class => "contextual" },
+ :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/0'}
+ }
+ assert_tag :tag => "div", :attributes => { :class => "contextual" },
+ :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/2'}
+ }
+ end
+
+ def test_add_related_issue
+ @request.session[:user_id] = 2
+ assert_difference 'Changeset.find(103).issues.size' do
+ xhr :post, :add_related_issue, :id => 1, :rev => 4, :issue_id => 2, :format => 'js'
+ assert_response :success
+ assert_template 'add_related_issue'
+ assert_equal 'text/javascript', response.content_type
+ end
+ assert_equal [2], Changeset.find(103).issue_ids
+ assert_include 'related-issues', response.body
+ assert_include 'Feature request #2', response.body
+ end
+
+ def test_add_related_issue_with_invalid_issue_id
+ @request.session[:user_id] = 2
+ assert_no_difference 'Changeset.find(103).issues.size' do
+ xhr :post, :add_related_issue, :id => 1, :rev => 4, :issue_id => 9999, :format => 'js'
+ assert_response :success
+ assert_template 'add_related_issue'
+ assert_equal 'text/javascript', response.content_type
+ end
+ assert_include 'alert("Issue is invalid")', response.body
+ end
+
+ def test_remove_related_issue
+ Changeset.find(103).issues << Issue.find(1)
+ Changeset.find(103).issues << Issue.find(2)
+
+ @request.session[:user_id] = 2
+ assert_difference 'Changeset.find(103).issues.size', -1 do
+ xhr :delete, :remove_related_issue, :id => 1, :rev => 4, :issue_id => 2, :format => 'js'
+ assert_response :success
+ assert_template 'remove_related_issue'
+ assert_equal 'text/javascript', response.content_type
+ end
+ assert_equal [1], Changeset.find(103).issue_ids
+ assert_include 'related-issue-2', response.body
+ end
+
+ def test_graph_commits_per_month
+ # Make sure there's some data to display
+ latest = Project.find(1).repository.changesets.maximum(:commit_date)
+ assert_not_nil latest
+ Date.stubs(:today).returns(latest.to_date + 10)
+
+ get :graph, :id => 1, :graph => 'commits_per_month'
+ assert_response :success
+ assert_equal 'image/svg+xml', @response.content_type
+ end
+
+ def test_graph_commits_per_author
+ get :graph, :id => 1, :graph => 'commits_per_author'
+ assert_response :success
+ assert_equal 'image/svg+xml', @response.content_type
+ end
+
+ def test_get_committers
+ @request.session[:user_id] = 2
+ # add a commit with an unknown user
+ Changeset.create!(
+ :repository => Project.find(1).repository,
+ :committer => 'foo',
+ :committed_on => Time.now,
+ :revision => 100,
+ :comments => 'Committed by foo.'
+ )
+
+ get :committers, :id => 10
+ assert_response :success
+ assert_template 'committers'
+
+ assert_tag :td, :content => 'dlopper',
+ :sibling => { :tag => 'td',
+ :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} },
+ :child => { :tag => 'option', :content => 'Dave Lopper',
+ :attributes => { :value => '3', :selected => 'selected' }}}}
+ assert_tag :td, :content => 'foo',
+ :sibling => { :tag => 'td',
+ :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} }}}
+ assert_no_tag :td, :content => 'foo',
+ :sibling => { :tag => 'td',
+ :descendant => { :tag => 'option', :attributes => { :selected => 'selected' }}}
+ end
+
+ def test_post_committers
+ @request.session[:user_id] = 2
+ # add a commit with an unknown user
+ c = Changeset.create!(
+ :repository => Project.find(1).repository,
+ :committer => 'foo',
+ :committed_on => Time.now,
+ :revision => 100,
+ :comments => 'Committed by foo.'
+ )
+ assert_no_difference "Changeset.count(:conditions => 'user_id = 3')" do
+ post :committers, :id => 10, :committers => { '0' => ['foo', '2'], '1' => ['dlopper', '3']}
+ assert_response 302
+ assert_equal User.find(2), c.reload.user
+ end
+ end
+end
diff --git a/test/functional/repositories_cvs_controller_test.rb b/test/functional/repositories_cvs_controller_test.rb
new file mode 100644
index 000000000..828d364f1
--- /dev/null
+++ b/test/functional/repositories_cvs_controller_test.rb
@@ -0,0 +1,274 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class RepositoriesCvsControllerTest < ActionController::TestCase
+ tests RepositoriesController
+
+ fixtures :projects, :users, :roles, :members, :member_roles,
+ :repositories, :enabled_modules
+
+ REPOSITORY_PATH = Rails.root.join('tmp/test/cvs_repository').to_s
+ REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
+ # CVS module
+ MODULE_NAME = 'test'
+ PRJ_ID = 3
+ NUM_REV = 7
+
+ def setup
+ Setting.default_language = 'en'
+ User.current = nil
+
+ @project = Project.find(PRJ_ID)
+ @repository = Repository::Cvs.create(:project => Project.find(PRJ_ID),
+ :root_url => REPOSITORY_PATH,
+ :url => MODULE_NAME,
+ :log_encoding => 'UTF-8')
+ assert @repository
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_get_new
+ @request.session[:user_id] = 1
+ @project.repository.destroy
+ get :new, :project_id => 'subproject1', :repository_scm => 'Cvs'
+ assert_response :success
+ assert_template 'new'
+ assert_kind_of Repository::Cvs, assigns(:repository)
+ assert assigns(:repository).new_record?
+ end
+
+ def test_browse_root
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal 3, assigns(:entries).size
+
+ entry = assigns(:entries).detect {|e| e.name == 'images'}
+ assert_equal 'dir', entry.kind
+
+ entry = assigns(:entries).detect {|e| e.name == 'README'}
+ assert_equal 'file', entry.kind
+
+ assert_not_nil assigns(:changesets)
+ assert assigns(:changesets).size > 0
+ end
+
+ def test_browse_directory
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param]
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['add.png', 'delete.png', 'edit.png'], assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+ assert_not_nil entry
+ assert_equal 'file', entry.kind
+ assert_equal 'images/edit.png', entry.path
+ end
+
+ def test_browse_at_given_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param],
+ :rev => 1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
+ end
+
+ def test_entry
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
+ assert_response :success
+ assert_template 'entry'
+ assert_no_tag :tag => 'td',
+ :attributes => { :class => /line-code/},
+ :content => /before_filter/
+ end
+
+ def test_entry_at_given_revision
+ # changesets must be loaded
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param],
+ :rev => 2
+ assert_response :success
+ assert_template 'entry'
+ # this line was removed in r3
+ assert_tag :tag => 'td',
+ :attributes => { :class => /line-code/},
+ :content => /before_filter/
+ end
+
+ def test_entry_not_found
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['sources', 'zzz.c'])[:param]
+ assert_tag :tag => 'p',
+ :attributes => { :id => /errorExplanation/ },
+ :content => /The entry or revision was not found in the repository/
+ end
+
+ def test_entry_download
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param],
+ :format => 'raw'
+ assert_response :success
+ end
+
+ def test_directory_entry
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['sources'])[:param]
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entry)
+ assert_equal 'sources', assigns(:entry).name
+ end
+
+ def test_diff
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ ['inline', 'sbs'].each do |dt|
+ get :diff, :id => PRJ_ID, :rev => 3, :type => dt
+ assert_response :success
+ assert_template 'diff'
+ assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_out' },
+ :content => /before_filter :require_login/
+ assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_in' },
+ :content => /with one change/
+ end
+ end
+
+ def test_diff_new_files
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ ['inline', 'sbs'].each do |dt|
+ get :diff, :id => PRJ_ID, :rev => 1, :type => dt
+ assert_response :success
+ assert_template 'diff'
+ assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_in' },
+ :content => /watched.remove_watcher/
+ assert_tag :tag => 'th', :attributes => { :class => 'filename' },
+ :content => /test\/README/
+ assert_tag :tag => 'th', :attributes => { :class => 'filename' },
+ :content => /test\/images\/delete.png /
+ assert_tag :tag => 'th', :attributes => { :class => 'filename' },
+ :content => /test\/images\/edit.png/
+ assert_tag :tag => 'th', :attributes => { :class => 'filename' },
+ :content => /test\/sources\/watchers_controller.rb/
+ end
+ end
+
+ def test_annotate
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :annotate, :id => PRJ_ID,
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
+ assert_response :success
+ assert_template 'annotate'
+
+ # 1.1 line
+ assert_select 'tr' do
+ assert_select 'th.line-num', :text => '21'
+ assert_select 'td.revision', :text => /1.1/
+ assert_select 'td.author', :text => /LANG/
+ end
+ # 1.2 line
+ assert_select 'tr' do
+ assert_select 'th.line-num', :text => '32'
+ assert_select 'td.revision', :text => /1.2/
+ assert_select 'td.author', :text => /LANG/
+ end
+ end
+
+ def test_destroy_valid_repository
+ @request.session[:user_id] = 1 # admin
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+
+ def test_destroy_invalid_repository
+ @request.session[:user_id] = 1 # admin
+ @project.repository.destroy
+ @repository = Repository::Cvs.create!(
+ :project => Project.find(PRJ_ID),
+ :root_url => "/invalid",
+ :url => MODULE_NAME,
+ :log_encoding => 'UTF-8'
+ )
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal 0, @repository.changesets.count
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+ else
+ puts "CVS test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+end
diff --git a/test/functional/repositories_darcs_controller_test.rb b/test/functional/repositories_darcs_controller_test.rb
new file mode 100644
index 000000000..8798217c7
--- /dev/null
+++ b/test/functional/repositories_darcs_controller_test.rb
@@ -0,0 +1,165 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class RepositoriesDarcsControllerTest < ActionController::TestCase
+ tests RepositoriesController
+
+ fixtures :projects, :users, :roles, :members, :member_roles,
+ :repositories, :enabled_modules
+
+ REPOSITORY_PATH = Rails.root.join('tmp/test/darcs_repository').to_s
+ PRJ_ID = 3
+ NUM_REV = 6
+
+ def setup
+ User.current = nil
+ @project = Project.find(PRJ_ID)
+ @repository = Repository::Darcs.create(
+ :project => @project,
+ :url => REPOSITORY_PATH,
+ :log_encoding => 'UTF-8'
+ )
+ assert @repository
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_get_new
+ @request.session[:user_id] = 1
+ @project.repository.destroy
+ get :new, :project_id => 'subproject1', :repository_scm => 'Darcs'
+ assert_response :success
+ assert_template 'new'
+ assert_kind_of Repository::Darcs, assigns(:repository)
+ assert assigns(:repository).new_record?
+ end
+
+ def test_browse_root
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal 3, assigns(:entries).size
+ assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
+ end
+
+ def test_browse_directory
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param]
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+ assert_not_nil entry
+ assert_equal 'file', entry.kind
+ assert_equal 'images/edit.png', entry.path
+ end
+
+ def test_browse_at_given_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param],
+ :rev => 1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png'], assigns(:entries).collect(&:name)
+ end
+
+ def test_changes
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :changes, :id => PRJ_ID,
+ :path => repository_path_hash(['images', 'edit.png'])[:param]
+ assert_response :success
+ assert_template 'changes'
+ assert_tag :tag => 'h2', :content => 'edit.png'
+ end
+
+ def test_diff
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ # Full diff of changeset 5
+ ['inline', 'sbs'].each do |dt|
+ get :diff, :id => PRJ_ID, :rev => 5, :type => dt
+ assert_response :success
+ assert_template 'diff'
+ # Line 22 removed
+ assert_tag :tag => 'th',
+ :content => '22',
+ :sibling => { :tag => 'td',
+ :attributes => { :class => /diff_out/ },
+ :content => /def remove/ }
+ end
+ end
+
+ def test_destroy_valid_repository
+ @request.session[:user_id] = 1 # admin
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+
+ def test_destroy_invalid_repository
+ @request.session[:user_id] = 1 # admin
+ @project.repository.destroy
+ @repository = Repository::Darcs.create!(
+ :project => @project,
+ :url => "/invalid",
+ :log_encoding => 'UTF-8'
+ )
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal 0, @repository.changesets.count
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+ else
+ puts "Darcs test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+end
diff --git a/test/functional/repositories_filesystem_controller_test.rb b/test/functional/repositories_filesystem_controller_test.rb
new file mode 100644
index 000000000..ca4909959
--- /dev/null
+++ b/test/functional/repositories_filesystem_controller_test.rb
@@ -0,0 +1,164 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class RepositoriesFilesystemControllerTest < ActionController::TestCase
+ tests RepositoriesController
+
+ fixtures :projects, :users, :roles, :members, :member_roles,
+ :repositories, :enabled_modules
+
+ REPOSITORY_PATH = Rails.root.join('tmp/test/filesystem_repository').to_s
+ PRJ_ID = 3
+
+ def setup
+ @ruby19_non_utf8_pass =
+ (RUBY_VERSION >= '1.9' && Encoding.default_external.to_s != 'UTF-8')
+ User.current = nil
+ Setting.enabled_scm << 'Filesystem' unless Setting.enabled_scm.include?('Filesystem')
+ @project = Project.find(PRJ_ID)
+ @repository = Repository::Filesystem.create(
+ :project => @project,
+ :url => REPOSITORY_PATH,
+ :path_encoding => ''
+ )
+ assert @repository
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_get_new
+ @request.session[:user_id] = 1
+ @project.repository.destroy
+ get :new, :project_id => 'subproject1', :repository_scm => 'Filesystem'
+ assert_response :success
+ assert_template 'new'
+ assert_kind_of Repository::Filesystem, assigns(:repository)
+ assert assigns(:repository).new_record?
+ end
+
+ def test_browse_root
+ @repository.fetch_changesets
+ @repository.reload
+ get :show, :id => PRJ_ID
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert assigns(:entries).size > 0
+ assert_not_nil assigns(:changesets)
+ assert assigns(:changesets).size == 0
+
+ assert_no_tag 'input', :attributes => {:name => 'rev'}
+ assert_no_tag 'a', :content => 'Statistics'
+ assert_no_tag 'a', :content => 'Atom'
+ end
+
+ def test_show_no_extension
+ get :entry, :id => PRJ_ID, :path => repository_path_hash(['test'])[:param]
+ assert_response :success
+ assert_template 'entry'
+ assert_tag :tag => 'th',
+ :content => '1',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td', :content => /TEST CAT/ }
+ end
+
+ def test_entry_download_no_extension
+ get :raw, :id => PRJ_ID, :path => repository_path_hash(['test'])[:param]
+ assert_response :success
+ assert_equal 'application/octet-stream', @response.content_type
+ end
+
+ def test_show_non_ascii_contents
+ with_settings :repositories_encodings => 'UTF-8,EUC-JP' do
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['japanese', 'euc-jp.txt'])[:param]
+ assert_response :success
+ assert_template 'entry'
+ assert_tag :tag => 'th',
+ :content => '2',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td', :content => /japanese/ }
+ if @ruby19_non_utf8_pass
+ puts "TODO: show repository file contents test fails in Ruby 1.9 " +
+ "and Encoding.default_external is not UTF-8. " +
+ "Current value is '#{Encoding.default_external.to_s}'"
+ else
+ str_japanese = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"
+ str_japanese.force_encoding('UTF-8') if str_japanese.respond_to?(:force_encoding)
+ assert_tag :tag => 'th',
+ :content => '3',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td', :content => /#{str_japanese}/ }
+ end
+ end
+ end
+
+ def test_show_utf16
+ enc = (RUBY_VERSION == "1.9.2" ? 'UTF-16LE' : 'UTF-16')
+ with_settings :repositories_encodings => enc do
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['japanese', 'utf-16.txt'])[:param]
+ assert_response :success
+ assert_tag :tag => 'th',
+ :content => '2',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td', :content => /japanese/ }
+ end
+ end
+
+ def test_show_text_file_should_send_if_too_big
+ with_settings :file_max_size_displayed => 1 do
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['japanese', 'big-file.txt'])[:param]
+ assert_response :success
+ assert_equal 'text/plain', @response.content_type
+ end
+ end
+
+ def test_destroy_valid_repository
+ @request.session[:user_id] = 1 # admin
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+
+ def test_destroy_invalid_repository
+ @request.session[:user_id] = 1 # admin
+ @project.repository.destroy
+ @repository = Repository::Filesystem.create!(
+ :project => @project,
+ :url => "/invalid",
+ :path_encoding => ''
+ )
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+ else
+ puts "Filesystem test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+end
diff --git a/test/functional/repositories_git_controller_test.rb b/test/functional/repositories_git_controller_test.rb
new file mode 100644
index 000000000..adb06b081
--- /dev/null
+++ b/test/functional/repositories_git_controller_test.rb
@@ -0,0 +1,638 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class RepositoriesGitControllerTest < ActionController::TestCase
+ tests RepositoriesController
+
+ fixtures :projects, :users, :roles, :members, :member_roles,
+ :repositories, :enabled_modules
+
+ REPOSITORY_PATH = Rails.root.join('tmp/test/git_repository').to_s
+ REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
+ PRJ_ID = 3
+ CHAR_1_HEX = "\xc3\x9c"
+ NUM_REV = 28
+
+ ## Git, Mercurial and CVS path encodings are binary.
+ ## Subversion supports URL encoding for path.
+ ## Redmine Mercurial adapter and extension use URL encoding.
+ ## Git accepts only binary path in command line parameter.
+ ## So, there is no way to use binary command line parameter in JRuby.
+ JRUBY_SKIP = (RUBY_PLATFORM == 'java')
+ JRUBY_SKIP_STR = "TODO: This test fails in JRuby"
+
+ def setup
+ @ruby19_non_utf8_pass =
+ (RUBY_VERSION >= '1.9' && Encoding.default_external.to_s != 'UTF-8')
+
+ User.current = nil
+ @project = Project.find(PRJ_ID)
+ @repository = Repository::Git.create(
+ :project => @project,
+ :url => REPOSITORY_PATH,
+ :path_encoding => 'ISO-8859-1'
+ )
+ assert @repository
+ @char_1 = CHAR_1_HEX.dup
+ if @char_1.respond_to?(:force_encoding)
+ @char_1.force_encoding('UTF-8')
+ end
+ end
+
+ def test_create_and_update
+ @request.session[:user_id] = 1
+ assert_difference 'Repository.count' do
+ post :create, :project_id => 'subproject1',
+ :repository_scm => 'Git',
+ :repository => {
+ :url => '/test',
+ :is_default => '0',
+ :identifier => 'test-create',
+ :extra_report_last_commit => '1',
+ }
+ end
+ assert_response 302
+ repository = Repository.first(:order => 'id DESC')
+ assert_kind_of Repository::Git, repository
+ assert_equal '/test', repository.url
+ assert_equal true, repository.extra_report_last_commit
+
+ put :update, :id => repository.id,
+ :repository => {
+ :extra_report_last_commit => '0'
+ }
+ assert_response 302
+ repo2 = Repository.find(repository.id)
+ assert_equal false, repo2.extra_report_last_commit
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ ## Ruby uses ANSI api to fork a process on Windows.
+ ## Japanese Shift_JIS and Traditional Chinese Big5 have 0x5c(backslash) problem
+ ## and these are incompatible with ASCII.
+ ## Git for Windows (msysGit) changed internal API from ANSI to Unicode in 1.7.10
+ ## http://code.google.com/p/msysgit/issues/detail?id=80
+ ## So, Latin-1 path tests fail on Japanese Windows
+ WINDOWS_PASS = (Redmine::Platform.mswin? &&
+ Redmine::Scm::Adapters::GitAdapter.client_version_above?([1, 7, 10]))
+ WINDOWS_SKIP_STR = "TODO: This test fails in Git for Windows above 1.7.10"
+
+ def test_get_new
+ @request.session[:user_id] = 1
+ @project.repository.destroy
+ get :new, :project_id => 'subproject1', :repository_scm => 'Git'
+ assert_response :success
+ assert_template 'new'
+ assert_kind_of Repository::Git, assigns(:repository)
+ assert assigns(:repository).new_record?
+ end
+
+ def test_browse_root
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+
+ get :show, :id => PRJ_ID
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal 9, assigns(:entries).size
+ assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'this_is_a_really_long_and_verbose_directory_name' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
+ assert assigns(:entries).detect {|e| e.name == 'copied_README' && e.kind == 'file'}
+ assert assigns(:entries).detect {|e| e.name == 'new_file.txt' && e.kind == 'file'}
+ assert assigns(:entries).detect {|e| e.name == 'renamed_test.txt' && e.kind == 'file'}
+ assert assigns(:entries).detect {|e| e.name == 'filemane with spaces.txt' && e.kind == 'file'}
+ assert assigns(:entries).detect {|e| e.name == ' filename with a leading space.txt ' && e.kind == 'file'}
+ assert_not_nil assigns(:changesets)
+ assert assigns(:changesets).size > 0
+ end
+
+ def test_browse_branch
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID, :rev => 'test_branch'
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal 4, assigns(:entries).size
+ assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
+ assert assigns(:entries).detect {|e| e.name == 'test.txt' && e.kind == 'file'}
+ assert_not_nil assigns(:changesets)
+ assert assigns(:changesets).size > 0
+ end
+
+ def test_browse_tag
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ [
+ "tag00.lightweight",
+ "tag01.annotated",
+ ].each do |t1|
+ get :show, :id => PRJ_ID, :rev => t1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert assigns(:entries).size > 0
+ assert_not_nil assigns(:changesets)
+ assert assigns(:changesets).size > 0
+ end
+ end
+
+ def test_browse_directory
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param]
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['edit.png'], assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+ assert_not_nil entry
+ assert_equal 'file', entry.kind
+ assert_equal 'images/edit.png', entry.path
+ assert_not_nil assigns(:changesets)
+ assert assigns(:changesets).size > 0
+ end
+
+ def test_browse_at_given_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param],
+ :rev => '7234cb2750b63f47bff735edc50a1c0a433c2518'
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png'], assigns(:entries).collect(&:name)
+ assert_not_nil assigns(:changesets)
+ assert assigns(:changesets).size > 0
+ end
+
+ def test_changes
+ get :changes, :id => PRJ_ID,
+ :path => repository_path_hash(['images', 'edit.png'])[:param]
+ assert_response :success
+ assert_template 'changes'
+ assert_tag :tag => 'h2', :content => 'edit.png'
+ end
+
+ def test_entry_show
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
+ assert_response :success
+ assert_template 'entry'
+ # Line 19
+ assert_tag :tag => 'th',
+ :content => '11',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ }
+ end
+
+ def test_entry_show_latin_1
+ if @ruby19_non_utf8_pass
+ puts_ruby19_non_utf8_pass()
+ elsif WINDOWS_PASS
+ puts WINDOWS_SKIP_STR
+ elsif JRUBY_SKIP
+ puts JRUBY_SKIP_STR
+ else
+ with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
+ ['57ca437c', '57ca437c0acbbcb749821fdf3726a1367056d364'].each do |r1|
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param],
+ :rev => r1
+ assert_response :success
+ assert_template 'entry'
+ assert_tag :tag => 'th',
+ :content => '1',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td',
+ :content => /test-#{@char_1}.txt/ }
+ end
+ end
+ end
+ end
+
+ def test_entry_download
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param],
+ :format => 'raw'
+ assert_response :success
+ # File content
+ assert @response.body.include?('WITHOUT ANY WARRANTY')
+ end
+
+ def test_directory_entry
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['sources'])[:param]
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entry)
+ assert_equal 'sources', assigns(:entry).name
+ end
+
+ def test_diff
+ assert_equal true, @repository.is_default
+ assert_nil @repository.identifier
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ # Full diff of changeset 2f9c0091
+ ['inline', 'sbs'].each do |dt|
+ get :diff,
+ :id => PRJ_ID,
+ :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7',
+ :type => dt
+ assert_response :success
+ assert_template 'diff'
+ # Line 22 removed
+ assert_tag :tag => 'th',
+ :content => /22/,
+ :sibling => { :tag => 'td',
+ :attributes => { :class => /diff_out/ },
+ :content => /def remove/ }
+ assert_tag :tag => 'h2', :content => /2f9c0091/
+ end
+ end
+
+ def test_diff_with_rev_and_path
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ with_settings :diff_max_lines_displayed => 1000 do
+ # Full diff of changeset 2f9c0091
+ ['inline', 'sbs'].each do |dt|
+ get :diff,
+ :id => PRJ_ID,
+ :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7',
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param],
+ :type => dt
+ assert_response :success
+ assert_template 'diff'
+ # Line 22 removed
+ assert_tag :tag => 'th',
+ :content => '22',
+ :sibling => { :tag => 'td',
+ :attributes => { :class => /diff_out/ },
+ :content => /def remove/ }
+ assert_tag :tag => 'h2', :content => /2f9c0091/
+ end
+ end
+ end
+
+ def test_diff_truncated
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+
+ with_settings :diff_max_lines_displayed => 5 do
+ # Truncated diff of changeset 2f9c0091
+ with_cache do
+ with_settings :default_language => 'en' do
+ get :diff, :id => PRJ_ID, :type => 'inline',
+ :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
+ assert_response :success
+ assert @response.body.include?("... This diff was truncated")
+ end
+ with_settings :default_language => 'fr' do
+ get :diff, :id => PRJ_ID, :type => 'inline',
+ :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
+ assert_response :success
+ assert ! @response.body.include?("... This diff was truncated")
+ assert @response.body.include?("... Ce diff")
+ end
+ end
+ end
+ end
+
+ def test_diff_two_revs
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ ['inline', 'sbs'].each do |dt|
+ get :diff,
+ :id => PRJ_ID,
+ :rev => '61b685fbe55ab05b5ac68402d5720c1a6ac973d1',
+ :rev_to => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7',
+ :type => dt
+ assert_response :success
+ assert_template 'diff'
+ diff = assigns(:diff)
+ assert_not_nil diff
+ assert_tag :tag => 'h2', :content => /2f9c0091:61b685fb/
+ assert_tag :tag => "form",
+ :attributes => {
+ :action => "/projects/subproject1/repository/revisions/" +
+ "61b685fbe55ab05b5ac68402d5720c1a6ac973d1/diff"
+ }
+ assert_tag :tag => 'input',
+ :attributes => {
+ :id => "rev_to",
+ :name => "rev_to",
+ :type => "hidden",
+ :value => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
+ }
+ end
+ end
+
+ def test_diff_path_in_subrepo
+ repo = Repository::Git.create(
+ :project => @project,
+ :url => REPOSITORY_PATH,
+ :identifier => 'test-diff-path',
+ :path_encoding => 'ISO-8859-1'
+ );
+ assert repo
+ assert_equal false, repo.is_default
+ assert_equal 'test-diff-path', repo.identifier
+ get :diff,
+ :id => PRJ_ID,
+ :repository_id => 'test-diff-path',
+ :rev => '61b685fbe55ab05b',
+ :rev_to => '2f9c0091c754a91a',
+ :type => 'inline'
+ assert_response :success
+ assert_template 'diff'
+ diff = assigns(:diff)
+ assert_not_nil diff
+ assert_tag :tag => "form",
+ :attributes => {
+ :action => "/projects/subproject1/repository/test-diff-path/" +
+ "revisions/61b685fbe55ab05b/diff"
+ }
+ assert_tag :tag => 'input',
+ :attributes => {
+ :id => "rev_to",
+ :name => "rev_to",
+ :type => "hidden",
+ :value => '2f9c0091c754a91a'
+ }
+ end
+
+ def test_diff_latin_1
+ if @ruby19_non_utf8_pass
+ puts_ruby19_non_utf8_pass()
+ else
+ with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
+ ['57ca437c', '57ca437c0acbbcb749821fdf3726a1367056d364'].each do |r1|
+ ['inline', 'sbs'].each do |dt|
+ get :diff, :id => PRJ_ID, :rev => r1, :type => dt
+ assert_response :success
+ assert_template 'diff'
+ assert_tag :tag => 'thead',
+ :descendant => {
+ :tag => 'th',
+ :attributes => { :class => 'filename' } ,
+ :content => /latin-1-dir\/test-#{@char_1}.txt/ ,
+ },
+ :sibling => {
+ :tag => 'tbody',
+ :descendant => {
+ :tag => 'td',
+ :attributes => { :class => /diff_in/ },
+ :content => /test-#{@char_1}.txt/
+ }
+ }
+ end
+ end
+ end
+ end
+ end
+
+ def test_diff_should_show_filenames
+ get :diff, :id => PRJ_ID, :rev => 'deff712f05a90d96edbd70facc47d944be5897e3', :type => 'inline'
+ assert_response :success
+ assert_template 'diff'
+ # modified file
+ assert_select 'th.filename', :text => 'sources/watchers_controller.rb'
+ # deleted file
+ assert_select 'th.filename', :text => 'test.txt'
+ end
+
+ def test_save_diff_type
+ user1 = User.find(1)
+ user1.pref[:diff_type] = nil
+ user1.preference.save
+ user = User.find(1)
+ assert_nil user.pref[:diff_type]
+
+ @request.session[:user_id] = 1 # admin
+ get :diff,
+ :id => PRJ_ID,
+ :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
+ assert_response :success
+ assert_template 'diff'
+ user.reload
+ assert_equal "inline", user.pref[:diff_type]
+ get :diff,
+ :id => PRJ_ID,
+ :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7',
+ :type => 'sbs'
+ assert_response :success
+ assert_template 'diff'
+ user.reload
+ assert_equal "sbs", user.pref[:diff_type]
+ end
+
+ def test_annotate
+ get :annotate, :id => PRJ_ID,
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
+ assert_response :success
+ assert_template 'annotate'
+
+ # Line 23, changeset 2f9c0091
+ assert_select 'tr' do
+ assert_select 'th.line-num', :text => '23'
+ assert_select 'td.revision', :text => /2f9c0091/
+ assert_select 'td.author', :text => 'jsmith'
+ assert_select 'td', :text => /remove_watcher/
+ end
+ end
+
+ def test_annotate_at_given_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :annotate, :id => PRJ_ID, :rev => 'deff7',
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
+ assert_response :success
+ assert_template 'annotate'
+ assert_tag :tag => 'h2', :content => /@ deff712f/
+ end
+
+ def test_annotate_binary_file
+ get :annotate, :id => PRJ_ID,
+ :path => repository_path_hash(['images', 'edit.png'])[:param]
+ assert_response 500
+ assert_tag :tag => 'p', :attributes => { :id => /errorExplanation/ },
+ :content => /cannot be annotated/
+ end
+
+ def test_annotate_error_when_too_big
+ with_settings :file_max_size_displayed => 1 do
+ get :annotate, :id => PRJ_ID,
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param],
+ :rev => 'deff712f'
+ assert_response 500
+ assert_tag :tag => 'p', :attributes => { :id => /errorExplanation/ },
+ :content => /exceeds the maximum text file size/
+
+ get :annotate, :id => PRJ_ID,
+ :path => repository_path_hash(['README'])[:param],
+ :rev => '7234cb2'
+ assert_response :success
+ assert_template 'annotate'
+ end
+ end
+
+ def test_annotate_latin_1
+ if @ruby19_non_utf8_pass
+ puts_ruby19_non_utf8_pass()
+ elsif WINDOWS_PASS
+ puts WINDOWS_SKIP_STR
+ elsif JRUBY_SKIP
+ puts JRUBY_SKIP_STR
+ else
+ with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
+ ['57ca437c', '57ca437c0acbbcb749821fdf3726a1367056d364'].each do |r1|
+ get :annotate, :id => PRJ_ID,
+ :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param],
+ :rev => r1
+ assert_tag :tag => 'th',
+ :content => '1',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td',
+ :content => /test-#{@char_1}.txt/ }
+ end
+ end
+ end
+ end
+
+ def test_revisions
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :revisions, :id => PRJ_ID
+ assert_response :success
+ assert_template 'revisions'
+ assert_tag :tag => 'form',
+ :attributes => {
+ :method => 'get',
+ :action => '/projects/subproject1/repository/revision'
+ }
+ end
+
+ def test_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ ['61b685fbe55ab05b5ac68402d5720c1a6ac973d1', '61b685f'].each do |r|
+ get :revision, :id => PRJ_ID, :rev => r
+ assert_response :success
+ assert_template 'revision'
+ end
+ end
+
+ def test_empty_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ ['', ' ', nil].each do |r|
+ get :revision, :id => PRJ_ID, :rev => r
+ assert_response 404
+ assert_error_tag :content => /was not found/
+ end
+ end
+
+ def test_destroy_valid_repository
+ @request.session[:user_id] = 1 # admin
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+
+ def test_destroy_invalid_repository
+ @request.session[:user_id] = 1 # admin
+ @project.repository.destroy
+ @repository = Repository::Git.create!(
+ :project => @project,
+ :url => "/invalid",
+ :path_encoding => 'ISO-8859-1'
+ )
+ @repository.fetch_changesets
+ @repository.reload
+ assert_equal 0, @repository.changesets.count
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+
+ private
+
+ def puts_ruby19_non_utf8_pass
+ puts "TODO: This test fails in Ruby 1.9 " +
+ "and Encoding.default_external is not UTF-8. " +
+ "Current value is '#{Encoding.default_external.to_s}'"
+ end
+ else
+ puts "Git test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+
+ private
+ def with_cache(&block)
+ before = ActionController::Base.perform_caching
+ ActionController::Base.perform_caching = true
+ block.call
+ ActionController::Base.perform_caching = before
+ end
+end
diff --git a/test/functional/repositories_mercurial_controller_test.rb b/test/functional/repositories_mercurial_controller_test.rb
new file mode 100644
index 000000000..071fa7930
--- /dev/null
+++ b/test/functional/repositories_mercurial_controller_test.rb
@@ -0,0 +1,525 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class RepositoriesMercurialControllerTest < ActionController::TestCase
+ tests RepositoriesController
+
+ fixtures :projects, :users, :roles, :members, :member_roles,
+ :repositories, :enabled_modules
+
+ REPOSITORY_PATH = Rails.root.join('tmp/test/mercurial_repository').to_s
+ CHAR_1_HEX = "\xc3\x9c"
+ PRJ_ID = 3
+ NUM_REV = 32
+
+ ruby19_non_utf8_pass =
+ (RUBY_VERSION >= '1.9' && Encoding.default_external.to_s != 'UTF-8')
+
+ def setup
+ User.current = nil
+ @project = Project.find(PRJ_ID)
+ @repository = Repository::Mercurial.create(
+ :project => @project,
+ :url => REPOSITORY_PATH,
+ :path_encoding => 'ISO-8859-1'
+ )
+ assert @repository
+ @diff_c_support = true
+ @char_1 = CHAR_1_HEX.dup
+ @tag_char_1 = "tag-#{CHAR_1_HEX}-00"
+ @branch_char_0 = "branch-#{CHAR_1_HEX}-00"
+ @branch_char_1 = "branch-#{CHAR_1_HEX}-01"
+ if @char_1.respond_to?(:force_encoding)
+ @char_1.force_encoding('UTF-8')
+ @tag_char_1.force_encoding('UTF-8')
+ @branch_char_0.force_encoding('UTF-8')
+ @branch_char_1.force_encoding('UTF-8')
+ end
+ end
+
+ if ruby19_non_utf8_pass
+ puts "TODO: Mercurial functional test fails in Ruby 1.9 " +
+ "and Encoding.default_external is not UTF-8. " +
+ "Current value is '#{Encoding.default_external.to_s}'"
+ def test_fake; assert true end
+ elsif File.directory?(REPOSITORY_PATH)
+
+ def test_get_new
+ @request.session[:user_id] = 1
+ @project.repository.destroy
+ get :new, :project_id => 'subproject1', :repository_scm => 'Mercurial'
+ assert_response :success
+ assert_template 'new'
+ assert_kind_of Repository::Mercurial, assigns(:repository)
+ assert assigns(:repository).new_record?
+ end
+
+ def test_show_root
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal 4, assigns(:entries).size
+ assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
+ assert_not_nil assigns(:changesets)
+ assert assigns(:changesets).size > 0
+ end
+
+ def test_show_directory
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param]
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+ assert_not_nil entry
+ assert_equal 'file', entry.kind
+ assert_equal 'images/edit.png', entry.path
+ assert_not_nil assigns(:changesets)
+ assert assigns(:changesets).size > 0
+ end
+
+ def test_show_at_given_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ [0, '0', '0885933ad4f6'].each do |r1|
+ get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param],
+ :rev => r1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png'], assigns(:entries).collect(&:name)
+ assert_not_nil assigns(:changesets)
+ assert assigns(:changesets).size > 0
+ end
+ end
+
+ def test_show_directory_sql_escape_percent
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ [13, '13', '3a330eb32958'].each do |r1|
+ get :show, :id => PRJ_ID,
+ :path => repository_path_hash(['sql_escape', 'percent%dir'])[:param],
+ :rev => r1
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:entries)
+ assert_equal ['percent%file1.txt', 'percentfile1.txt'],
+ assigns(:entries).collect(&:name)
+ changesets = assigns(:changesets)
+ assert_not_nil changesets
+ assert assigns(:changesets).size > 0
+ assert_equal %w(13 11 10 9), changesets.collect(&:revision)
+ end
+ end
+
+ def test_show_directory_latin_1_path
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ [21, '21', 'adf805632193'].each do |r1|
+ get :show, :id => PRJ_ID,
+ :path => repository_path_hash(['latin-1-dir'])[:param],
+ :rev => r1
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:entries)
+ assert_equal ["make-latin-1-file.rb",
+ "test-#{@char_1}-1.txt",
+ "test-#{@char_1}-2.txt",
+ "test-#{@char_1}.txt"], assigns(:entries).collect(&:name)
+ changesets = assigns(:changesets)
+ assert_not_nil changesets
+ assert_equal %w(21 20 19 18 17), changesets.collect(&:revision)
+ end
+ end
+
+ def show_should_show_branch_selection_form
+ @repository.fetch_changesets
+ @project.reload
+ get :show, :id => PRJ_ID
+ assert_tag 'form', :attributes => {:id => 'revision_selector', :action => '/projects/subproject1/repository/show'}
+ assert_tag 'select', :attributes => {:name => 'branch'},
+ :child => {:tag => 'option', :attributes => {:value => 'test-branch-01'}},
+ :parent => {:tag => 'form', :attributes => {:id => 'revision_selector'}}
+ end
+
+ def test_show_branch
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ [
+ 'default',
+ @branch_char_1,
+ 'branch (1)[2]&,%.-3_4',
+ @branch_char_0,
+ 'test_branch.latin-1',
+ 'test-branch-00',
+ ].each do |bra|
+ get :show, :id => PRJ_ID, :rev => bra
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert assigns(:entries).size > 0
+ assert_not_nil assigns(:changesets)
+ assert assigns(:changesets).size > 0
+ end
+ end
+
+ def test_show_tag
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ [
+ @tag_char_1,
+ 'tag_test.00',
+ 'tag-init-revision'
+ ].each do |tag|
+ get :show, :id => PRJ_ID, :rev => tag
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert assigns(:entries).size > 0
+ assert_not_nil assigns(:changesets)
+ assert assigns(:changesets).size > 0
+ end
+ end
+
+ def test_changes
+ get :changes, :id => PRJ_ID,
+ :path => repository_path_hash(['images', 'edit.png'])[:param]
+ assert_response :success
+ assert_template 'changes'
+ assert_tag :tag => 'h2', :content => 'edit.png'
+ end
+
+ def test_entry_show
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
+ assert_response :success
+ assert_template 'entry'
+ # Line 10
+ assert_tag :tag => 'th',
+ :content => '10',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ }
+ end
+
+ def test_entry_show_latin_1_path
+ [21, '21', 'adf805632193'].each do |r1|
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}-2.txt"])[:param],
+ :rev => r1
+ assert_response :success
+ assert_template 'entry'
+ assert_tag :tag => 'th',
+ :content => '1',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td',
+ :content => /Mercurial is a distributed version control system/ }
+ end
+ end
+
+ def test_entry_show_latin_1_contents
+ with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
+ [27, '27', '7bbf4c738e71'].each do |r1|
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param],
+ :rev => r1
+ assert_response :success
+ assert_template 'entry'
+ assert_tag :tag => 'th',
+ :content => '1',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td',
+ :content => /test-#{@char_1}.txt/ }
+ end
+ end
+ end
+
+ def test_entry_download
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param],
+ :format => 'raw'
+ assert_response :success
+ # File content
+ assert @response.body.include?('WITHOUT ANY WARRANTY')
+ end
+
+ def test_entry_binary_force_download
+ get :entry, :id => PRJ_ID, :rev => 1,
+ :path => repository_path_hash(['images', 'edit.png'])[:param]
+ assert_response :success
+ assert_equal 'image/png', @response.content_type
+ end
+
+ def test_directory_entry
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['sources'])[:param]
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entry)
+ assert_equal 'sources', assigns(:entry).name
+ end
+
+ def test_diff
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ [4, '4', 'def6d2f1254a'].each do |r1|
+ # Full diff of changeset 4
+ ['inline', 'sbs'].each do |dt|
+ get :diff, :id => PRJ_ID, :rev => r1, :type => dt
+ assert_response :success
+ assert_template 'diff'
+ if @diff_c_support
+ # Line 22 removed
+ assert_tag :tag => 'th',
+ :content => '22',
+ :sibling => { :tag => 'td',
+ :attributes => { :class => /diff_out/ },
+ :content => /def remove/ }
+ assert_tag :tag => 'h2', :content => /4:def6d2f1254a/
+ end
+ end
+ end
+ end
+
+ def test_diff_two_revs
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ [2, '400bb8672109', '400', 400].each do |r1|
+ [4, 'def6d2f1254a'].each do |r2|
+ ['inline', 'sbs'].each do |dt|
+ get :diff,
+ :id => PRJ_ID,
+ :rev => r1,
+ :rev_to => r2,
+ :type => dt
+ assert_response :success
+ assert_template 'diff'
+ diff = assigns(:diff)
+ assert_not_nil diff
+ assert_tag :tag => 'h2',
+ :content => /4:def6d2f1254a 2:400bb8672109/
+ end
+ end
+ end
+ end
+
+ def test_diff_latin_1_path
+ with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
+ [21, 'adf805632193'].each do |r1|
+ ['inline', 'sbs'].each do |dt|
+ get :diff, :id => PRJ_ID, :rev => r1, :type => dt
+ assert_response :success
+ assert_template 'diff'
+ assert_tag :tag => 'thead',
+ :descendant => {
+ :tag => 'th',
+ :attributes => { :class => 'filename' } ,
+ :content => /latin-1-dir\/test-#{@char_1}-2.txt/ ,
+ },
+ :sibling => {
+ :tag => 'tbody',
+ :descendant => {
+ :tag => 'td',
+ :attributes => { :class => /diff_in/ },
+ :content => /It is written in Python/
+ }
+ }
+ end
+ end
+ end
+ end
+
+ def test_diff_should_show_modified_filenames
+ get :diff, :id => PRJ_ID, :rev => '400bb8672109', :type => 'inline'
+ assert_response :success
+ assert_template 'diff'
+ assert_select 'th.filename', :text => 'sources/watchers_controller.rb'
+ end
+
+ def test_diff_should_show_deleted_filenames
+ get :diff, :id => PRJ_ID, :rev => 'b3a615152df8', :type => 'inline'
+ assert_response :success
+ assert_template 'diff'
+ assert_select 'th.filename', :text => 'sources/welcome_controller.rb'
+ end
+
+ def test_annotate
+ get :annotate, :id => PRJ_ID,
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
+ assert_response :success
+ assert_template 'annotate'
+
+ # Line 22, revision 4:def6d2f1254a
+ assert_select 'tr' do
+ assert_select 'th.line-num', :text => '22'
+ assert_select 'td.revision', :text => '4:def6d2f1254a'
+ assert_select 'td.author', :text => 'jsmith'
+ assert_select 'td', :text => /remove_watcher/
+ end
+ end
+
+ def test_annotate_not_in_tip
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :annotate, :id => PRJ_ID,
+ :path => repository_path_hash(['sources', 'welcome_controller.rb'])[:param]
+ assert_response 404
+ assert_error_tag :content => /was not found/
+ end
+
+ def test_annotate_at_given_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ [2, '400bb8672109', '400', 400].each do |r1|
+ get :annotate, :id => PRJ_ID, :rev => r1,
+ :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param]
+ assert_response :success
+ assert_template 'annotate'
+ assert_tag :tag => 'h2', :content => /@ 2:400bb8672109/
+ end
+ end
+
+ def test_annotate_latin_1_path
+ [21, '21', 'adf805632193'].each do |r1|
+ get :annotate, :id => PRJ_ID,
+ :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}-2.txt"])[:param],
+ :rev => r1
+ assert_response :success
+ assert_template 'annotate'
+ assert_tag :tag => 'th',
+ :content => '1',
+ :attributes => { :class => 'line-num' },
+ :sibling =>
+ {
+ :tag => 'td',
+ :attributes => { :class => 'revision' },
+ :child => { :tag => 'a', :content => '20:709858aafd1b' }
+ }
+ assert_tag :tag => 'th',
+ :content => '1',
+ :attributes => { :class => 'line-num' },
+ :sibling =>
+ {
+ :tag => 'td' ,
+ :content => 'jsmith' ,
+ :attributes => { :class => 'author' },
+ }
+ assert_tag :tag => 'th',
+ :content => '1',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td',
+ :content => /Mercurial is a distributed version control system/ }
+
+ end
+ end
+
+ def test_annotate_latin_1_contents
+ with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
+ [27, '7bbf4c738e71'].each do |r1|
+ get :annotate, :id => PRJ_ID,
+ :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param],
+ :rev => r1
+ assert_tag :tag => 'th',
+ :content => '1',
+ :attributes => { :class => 'line-num' },
+ :sibling => { :tag => 'td',
+ :content => /test-#{@char_1}.txt/ }
+ end
+ end
+ end
+
+ def test_empty_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ ['', ' ', nil].each do |r|
+ get :revision, :id => PRJ_ID, :rev => r
+ assert_response 404
+ assert_error_tag :content => /was not found/
+ end
+ end
+
+ def test_destroy_valid_repository
+ @request.session[:user_id] = 1 # admin
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ assert_equal NUM_REV, @repository.changesets.count
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+
+ def test_destroy_invalid_repository
+ @request.session[:user_id] = 1 # admin
+ @project.repository.destroy
+ @repository = Repository::Mercurial.create!(
+ :project => Project.find(PRJ_ID),
+ :url => "/invalid",
+ :path_encoding => 'ISO-8859-1'
+ )
+ @repository.fetch_changesets
+ assert_equal 0, @repository.changesets.count
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+ else
+ puts "Mercurial test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+end
diff --git a/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb
new file mode 100644
index 000000000..9ca427549
--- /dev/null
+++ b/test/functional/repositories_subversion_controller_test.rb
@@ -0,0 +1,425 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class RepositoriesSubversionControllerTest < ActionController::TestCase
+ tests RepositoriesController
+
+ fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
+ :repositories, :issues, :issue_statuses, :changesets, :changes,
+ :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
+
+ PRJ_ID = 3
+ NUM_REV = 11
+
+ def setup
+ Setting.default_language = 'en'
+ User.current = nil
+
+ @project = Project.find(PRJ_ID)
+ @repository = Repository::Subversion.create(:project => @project,
+ :url => self.class.subversion_repository_url)
+ assert @repository
+ end
+
+ if repository_configured?('subversion')
+ def test_new
+ @request.session[:user_id] = 1
+ @project.repository.destroy
+ get :new, :project_id => 'subproject1', :repository_scm => 'Subversion'
+ assert_response :success
+ assert_template 'new'
+ assert_kind_of Repository::Subversion, assigns(:repository)
+ assert assigns(:repository).new_record?
+ end
+
+ def test_show
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_not_nil assigns(:changesets)
+
+ entry = assigns(:entries).detect {|e| e.name == 'subversion_test'}
+ assert_not_nil entry
+ assert_equal 'dir', entry.kind
+ assert_select 'tr.dir a[href=/projects/subproject1/repository/show/subversion_test]'
+
+ assert_tag 'input', :attributes => {:name => 'rev'}
+ assert_tag 'a', :content => 'Statistics'
+ assert_tag 'a', :content => 'Atom'
+ assert_tag :tag => 'a',
+ :attributes => {:href => '/projects/subproject1/repository'},
+ :content => 'root'
+ end
+
+ def test_show_non_default
+ Repository::Subversion.create(:project => @project,
+ :url => self.class.subversion_repository_url,
+ :is_default => false, :identifier => 'svn')
+
+ get :show, :id => PRJ_ID, :repository_id => 'svn'
+ assert_response :success
+ assert_template 'show'
+ assert_select 'tr.dir a[href=/projects/subproject1/repository/svn/show/subversion_test]'
+ # Repository menu should link to the main repo
+ assert_select '#main-menu a[href=/projects/subproject1/repository]'
+ end
+
+ def test_browse_directory
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID, :path => repository_path_hash(['subversion_test'])[:param]
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal [
+ '[folder_with_brackets]', 'folder', '.project',
+ 'helloworld.c', 'textfile.txt'
+ ],
+ assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'helloworld.c'}
+ assert_equal 'file', entry.kind
+ assert_equal 'subversion_test/helloworld.c', entry.path
+ assert_tag :a, :content => 'helloworld.c', :attributes => { :class => /text\-x\-c/ }
+ end
+
+ def test_browse_at_given_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :show, :id => PRJ_ID, :path => repository_path_hash(['subversion_test'])[:param],
+ :rev => 4
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['folder', '.project', 'helloworld.c', 'helloworld.rb', 'textfile.txt'],
+ assigns(:entries).collect(&:name)
+ end
+
+ def test_file_changes
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :changes, :id => PRJ_ID,
+ :path => repository_path_hash(['subversion_test', 'folder', 'helloworld.rb'])[:param]
+ assert_response :success
+ assert_template 'changes'
+
+ changesets = assigns(:changesets)
+ assert_not_nil changesets
+ assert_equal %w(6 3 2), changesets.collect(&:revision)
+
+ # svn properties displayed with svn >= 1.5 only
+ if Redmine::Scm::Adapters::SubversionAdapter.client_version_above?([1, 5, 0])
+ assert_not_nil assigns(:properties)
+ assert_equal 'native', assigns(:properties)['svn:eol-style']
+ assert_tag :ul,
+ :child => { :tag => 'li',
+ :child => { :tag => 'b', :content => 'svn:eol-style' },
+ :child => { :tag => 'span', :content => 'native' } }
+ end
+ end
+
+ def test_directory_changes
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :changes, :id => PRJ_ID,
+ :path => repository_path_hash(['subversion_test', 'folder'])[:param]
+ assert_response :success
+ assert_template 'changes'
+
+ changesets = assigns(:changesets)
+ assert_not_nil changesets
+ assert_equal %w(10 9 7 6 5 2), changesets.collect(&:revision)
+ end
+
+ def test_entry
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['subversion_test', 'helloworld.c'])[:param]
+ assert_response :success
+ assert_template 'entry'
+ end
+
+ def test_entry_should_send_if_too_big
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ # no files in the test repo is larger than 1KB...
+ with_settings :file_max_size_displayed => 0 do
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['subversion_test', 'helloworld.c'])[:param]
+ assert_response :success
+ assert_equal 'attachment; filename="helloworld.c"',
+ @response.headers['Content-Disposition']
+ end
+ end
+
+ def test_entry_should_send_images_inline
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['subversion_test', 'folder', 'subfolder', 'rubylogo.gif'])[:param]
+ assert_response :success
+ assert_equal 'inline; filename="rubylogo.gif"', response.headers['Content-Disposition']
+ end
+
+ def test_entry_at_given_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['subversion_test', 'helloworld.rb'])[:param],
+ :rev => 2
+ assert_response :success
+ assert_template 'entry'
+ # this line was removed in r3 and file was moved in r6
+ assert_tag :tag => 'td', :attributes => { :class => /line-code/},
+ :content => /Here's the code/
+ end
+
+ def test_entry_not_found
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['subversion_test', 'zzz.c'])[:param]
+ assert_tag :tag => 'p', :attributes => { :id => /errorExplanation/ },
+ :content => /The entry or revision was not found in the repository/
+ end
+
+ def test_entry_download
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :raw, :id => PRJ_ID,
+ :path => repository_path_hash(['subversion_test', 'helloworld.c'])[:param]
+ assert_response :success
+ assert_equal 'attachment; filename="helloworld.c"', @response.headers['Content-Disposition']
+ end
+
+ def test_directory_entry
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :entry, :id => PRJ_ID,
+ :path => repository_path_hash(['subversion_test', 'folder'])[:param]
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entry)
+ assert_equal 'folder', assigns(:entry).name
+ end
+
+ # TODO: this test needs fixtures.
+ def test_revision
+ get :revision, :id => 1, :rev => 2
+ assert_response :success
+ assert_template 'revision'
+
+ assert_select 'ul' do
+ assert_select 'li' do
+ # link to the entry at rev 2
+ assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/2/entry/test/some/path/in/the/repo', :text => 'repo'
+ # link to partial diff
+ assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/2/diff/test/some/path/in/the/repo'
+ end
+ end
+ end
+
+ def test_invalid_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :revision, :id => PRJ_ID, :rev => 'something_weird'
+ assert_response 404
+ assert_error_tag :content => /was not found/
+ end
+
+ def test_invalid_revision_diff
+ get :diff, :id => PRJ_ID, :rev => '1', :rev_to => 'something_weird'
+ assert_response 404
+ assert_error_tag :content => /was not found/
+ end
+
+ def test_empty_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ ['', ' ', nil].each do |r|
+ get :revision, :id => PRJ_ID, :rev => r
+ assert_response 404
+ assert_error_tag :content => /was not found/
+ end
+ end
+
+ # TODO: this test needs fixtures.
+ def test_revision_with_repository_pointing_to_a_subdirectory
+ r = Project.find(1).repository
+ # Changes repository url to a subdirectory
+ r.update_attribute :url, (r.url + '/test/some')
+
+ get :revision, :id => 1, :rev => 2
+ assert_response :success
+ assert_template 'revision'
+
+ assert_select 'ul' do
+ assert_select 'li' do
+ # link to the entry at rev 2
+ assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/2/entry/path/in/the/repo', :text => 'repo'
+ # link to partial diff
+ assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/2/diff/path/in/the/repo'
+ end
+ end
+ end
+
+ def test_revision_diff
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ ['inline', 'sbs'].each do |dt|
+ get :diff, :id => PRJ_ID, :rev => 3, :type => dt
+ assert_response :success
+ assert_template 'diff'
+ assert_select 'h2', :text => /Revision 3/
+ assert_select 'th.filename', :text => 'subversion_test/textfile.txt'
+ end
+ end
+
+ def test_revision_diff_raw_format
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+
+ get :diff, :id => PRJ_ID, :rev => 3, :format => 'diff'
+ assert_response :success
+ assert_equal 'text/x-patch', @response.content_type
+ assert_equal 'Index: subversion_test/textfile.txt', @response.body.split(/\r?\n/).first
+ end
+
+ def test_directory_diff
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ ['inline', 'sbs'].each do |dt|
+ get :diff, :id => PRJ_ID, :rev => 6, :rev_to => 2,
+ :path => repository_path_hash(['subversion_test', 'folder'])[:param],
+ :type => dt
+ assert_response :success
+ assert_template 'diff'
+
+ diff = assigns(:diff)
+ assert_not_nil diff
+ # 2 files modified
+ assert_equal 2, Redmine::UnifiedDiff.new(diff).size
+ assert_tag :tag => 'h2', :content => /2:6/
+ end
+ end
+
+ def test_annotate
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :annotate, :id => PRJ_ID,
+ :path => repository_path_hash(['subversion_test', 'helloworld.c'])[:param]
+ assert_response :success
+ assert_template 'annotate'
+
+ assert_select 'tr' do
+ assert_select 'th.line-num', :text => '1'
+ assert_select 'td.revision', :text => '4'
+ assert_select 'td.author', :text => 'jp'
+ assert_select 'td', :text => /stdio.h/
+ end
+ # Same revision
+ assert_select 'tr' do
+ assert_select 'th.line-num', :text => '2'
+ assert_select 'td.revision', :text => ''
+ assert_select 'td.author', :text => ''
+ end
+ end
+
+ def test_annotate_at_given_revision
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get :annotate, :id => PRJ_ID, :rev => 8,
+ :path => repository_path_hash(['subversion_test', 'helloworld.c'])[:param]
+ assert_response :success
+ assert_template 'annotate'
+ assert_tag :tag => 'h2', :content => /@ 8/
+ end
+
+ def test_destroy_valid_repository
+ @request.session[:user_id] = 1 # admin
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ assert_equal NUM_REV, @repository.changesets.count
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+
+ def test_destroy_invalid_repository
+ @request.session[:user_id] = 1 # admin
+ @project.repository.destroy
+ @repository = Repository::Subversion.create!(
+ :project => @project,
+ :url => "file:///invalid")
+ @repository.fetch_changesets
+ assert_equal 0, @repository.changesets.count
+
+ assert_difference 'Repository.count', -1 do
+ delete :destroy, :id => @repository.id
+ end
+ assert_response 302
+ @project.reload
+ assert_nil @project.repository
+ end
+ else
+ puts "Subversion test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+end
diff --git a/test/functional/roles_controller_test.rb b/test/functional/roles_controller_test.rb
new file mode 100644
index 000000000..179eb90b4
--- /dev/null
+++ b/test/functional/roles_controller_test.rb
@@ -0,0 +1,216 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class RolesControllerTest < ActionController::TestCase
+ fixtures :roles, :users, :members, :member_roles, :workflows, :trackers
+
+ def setup
+ User.current = nil
+ @request.session[:user_id] = 1 # admin
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'index'
+
+ assert_not_nil assigns(:roles)
+ assert_equal Role.order('builtin, position').all, assigns(:roles)
+
+ assert_tag :tag => 'a', :attributes => { :href => '/roles/1/edit' },
+ :content => 'Manager'
+ end
+
+ def test_new
+ get :new
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_new_with_copy
+ copy_from = Role.find(2)
+
+ get :new, :copy => copy_from.id.to_s
+ assert_response :success
+ assert_template 'new'
+
+ role = assigns(:role)
+ assert_equal copy_from.permissions, role.permissions
+
+ assert_select 'form' do
+ # blank name
+ assert_select 'input[name=?][value=]', 'role[name]'
+ # edit_project permission checked
+ assert_select 'input[type=checkbox][name=?][value=edit_project][checked=checked]', 'role[permissions][]'
+ # add_project permission not checked
+ assert_select 'input[type=checkbox][name=?][value=add_project]', 'role[permissions][]'
+ assert_select 'input[type=checkbox][name=?][value=add_project][checked=checked]', 'role[permissions][]', 0
+ # workflow copy selected
+ assert_select 'select[name=?]', 'copy_workflow_from' do
+ assert_select 'option[value=2][selected=selected]'
+ end
+ end
+ end
+
+ def test_create_with_validaton_failure
+ post :create, :role => {:name => '',
+ :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
+ :assignable => '0'}
+
+ assert_response :success
+ assert_template 'new'
+ assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' }
+ end
+
+ def test_create_without_workflow_copy
+ post :create, :role => {:name => 'RoleWithoutWorkflowCopy',
+ :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
+ :assignable => '0'}
+
+ assert_redirected_to '/roles'
+ role = Role.find_by_name('RoleWithoutWorkflowCopy')
+ assert_not_nil role
+ assert_equal [:add_issues, :edit_issues, :log_time], role.permissions
+ assert !role.assignable?
+ end
+
+ def test_create_with_workflow_copy
+ post :create, :role => {:name => 'RoleWithWorkflowCopy',
+ :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
+ :assignable => '0'},
+ :copy_workflow_from => '1'
+
+ assert_redirected_to '/roles'
+ role = Role.find_by_name('RoleWithWorkflowCopy')
+ assert_not_nil role
+ assert_equal Role.find(1).workflow_rules.size, role.workflow_rules.size
+ end
+
+ def test_edit
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ assert_equal Role.find(1), assigns(:role)
+ assert_select 'select[name=?]', 'role[issues_visibility]'
+ end
+
+ def test_edit_anonymous
+ get :edit, :id => Role.anonymous.id
+ assert_response :success
+ assert_template 'edit'
+ assert_select 'select[name=?]', 'role[issues_visibility]', 0
+ end
+
+ def test_edit_invalid_should_respond_with_404
+ get :edit, :id => 999
+ assert_response 404
+ end
+
+ def test_update
+ put :update, :id => 1,
+ :role => {:name => 'Manager',
+ :permissions => ['edit_project', ''],
+ :assignable => '0'}
+
+ assert_redirected_to '/roles'
+ role = Role.find(1)
+ assert_equal [:edit_project], role.permissions
+ end
+
+ def test_update_with_failure
+ put :update, :id => 1, :role => {:name => ''}
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_destroy
+ r = Role.create!(:name => 'ToBeDestroyed', :permissions => [:view_wiki_pages])
+
+ delete :destroy, :id => r
+ assert_redirected_to '/roles'
+ assert_nil Role.find_by_id(r.id)
+ end
+
+ def test_destroy_role_in_use
+ delete :destroy, :id => 1
+ assert_redirected_to '/roles'
+ assert_equal 'This role is in use and cannot be deleted.', flash[:error]
+ assert_not_nil Role.find_by_id(1)
+ end
+
+ def test_get_permissions
+ get :permissions
+ assert_response :success
+ assert_template 'permissions'
+
+ assert_not_nil assigns(:roles)
+ assert_equal Role.order('builtin, position').all, assigns(:roles)
+
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'permissions[3][]',
+ :value => 'add_issues',
+ :checked => 'checked' }
+
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'permissions[3][]',
+ :value => 'delete_issues',
+ :checked => nil }
+ end
+
+ def test_post_permissions
+ post :permissions, :permissions => { '0' => '', '1' => ['edit_issues'], '3' => ['add_issues', 'delete_issues']}
+ assert_redirected_to '/roles'
+
+ assert_equal [:edit_issues], Role.find(1).permissions
+ assert_equal [:add_issues, :delete_issues], Role.find(3).permissions
+ assert Role.find(2).permissions.empty?
+ end
+
+ def test_clear_all_permissions
+ post :permissions, :permissions => { '0' => '' }
+ assert_redirected_to '/roles'
+ assert Role.find(1).permissions.empty?
+ end
+
+ def test_move_highest
+ put :update, :id => 3, :role => {:move_to => 'highest'}
+ assert_redirected_to '/roles'
+ assert_equal 1, Role.find(3).position
+ end
+
+ def test_move_higher
+ position = Role.find(3).position
+ put :update, :id => 3, :role => {:move_to => 'higher'}
+ assert_redirected_to '/roles'
+ assert_equal position - 1, Role.find(3).position
+ end
+
+ def test_move_lower
+ position = Role.find(2).position
+ put :update, :id => 2, :role => {:move_to => 'lower'}
+ assert_redirected_to '/roles'
+ assert_equal position + 1, Role.find(2).position
+ end
+
+ def test_move_lowest
+ put :update, :id => 2, :role => {:move_to => 'lowest'}
+ assert_redirected_to '/roles'
+ assert_equal Role.count, Role.find(2).position
+ end
+end
diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb
new file mode 100644
index 000000000..30664f03c
--- /dev/null
+++ b/test/functional/search_controller_test.rb
@@ -0,0 +1,265 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class SearchControllerTest < ActionController::TestCase
+ fixtures :projects, :enabled_modules, :roles, :users, :members, :member_roles,
+ :issues, :trackers, :issue_statuses, :enumerations,
+ :custom_fields, :custom_values,
+ :repositories, :changesets
+
+ def setup
+ User.current = nil
+ end
+
+ def test_search_for_projects
+ get :index
+ assert_response :success
+ assert_template 'index'
+
+ get :index, :q => "cook"
+ assert_response :success
+ assert_template 'index'
+ assert assigns(:results).include?(Project.find(1))
+ end
+
+ def test_search_all_projects
+ get :index, :q => 'recipe subproject commit', :all_words => ''
+ assert_response :success
+ assert_template 'index'
+
+ assert assigns(:results).include?(Issue.find(2))
+ assert assigns(:results).include?(Issue.find(5))
+ assert assigns(:results).include?(Changeset.find(101))
+ assert_tag :dt, :attributes => { :class => /issue/ },
+ :child => { :tag => 'a', :content => /Add ingredients categories/ },
+ :sibling => { :tag => 'dd', :content => /should be classified by categories/ }
+
+ assert assigns(:results_by_type).is_a?(Hash)
+ assert_equal 5, assigns(:results_by_type)['changesets']
+ assert_tag :a, :content => 'Changesets (5)'
+ end
+
+ def test_search_issues
+ get :index, :q => 'issue', :issues => 1
+ assert_response :success
+ assert_template 'index'
+
+ assert_equal true, assigns(:all_words)
+ assert_equal false, assigns(:titles_only)
+ assert assigns(:results).include?(Issue.find(8))
+ assert assigns(:results).include?(Issue.find(5))
+ assert_tag :dt, :attributes => { :class => /issue closed/ },
+ :child => { :tag => 'a', :content => /Closed/ }
+ end
+
+ def test_search_issues_should_search_notes
+ Journal.create!(:journalized => Issue.find(2), :notes => 'Issue notes with searchkeyword')
+
+ get :index, :q => 'searchkeyword', :issues => 1
+ assert_response :success
+ assert_include Issue.find(2), assigns(:results)
+ end
+
+ def test_search_issues_with_multiple_matches_in_journals_should_return_issue_once
+ Journal.create!(:journalized => Issue.find(2), :notes => 'Issue notes with searchkeyword')
+ Journal.create!(:journalized => Issue.find(2), :notes => 'Issue notes with searchkeyword')
+
+ get :index, :q => 'searchkeyword', :issues => 1
+ assert_response :success
+ assert_include Issue.find(2), assigns(:results)
+ assert_equal 1, assigns(:results).size
+ end
+
+ def test_search_issues_should_search_private_notes_with_permission_only
+ Journal.create!(:journalized => Issue.find(2), :notes => 'Private notes with searchkeyword', :private_notes => true)
+ @request.session[:user_id] = 2
+
+ Role.find(1).add_permission! :view_private_notes
+ get :index, :q => 'searchkeyword', :issues => 1
+ assert_response :success
+ assert_include Issue.find(2), assigns(:results)
+
+ Role.find(1).remove_permission! :view_private_notes
+ get :index, :q => 'searchkeyword', :issues => 1
+ assert_response :success
+ assert_not_include Issue.find(2), assigns(:results)
+ end
+
+ def test_search_all_projects_with_scope_param
+ get :index, :q => 'issue', :scope => 'all'
+ assert_response :success
+ assert_template 'index'
+ assert assigns(:results).present?
+ end
+
+ def test_search_my_projects
+ @request.session[:user_id] = 2
+ get :index, :id => 1, :q => 'recipe subproject', :scope => 'my_projects', :all_words => ''
+ assert_response :success
+ assert_template 'index'
+ assert assigns(:results).include?(Issue.find(1))
+ assert !assigns(:results).include?(Issue.find(5))
+ end
+
+ def test_search_my_projects_without_memberships
+ # anonymous user has no memberships
+ get :index, :id => 1, :q => 'recipe subproject', :scope => 'my_projects', :all_words => ''
+ assert_response :success
+ assert_template 'index'
+ assert assigns(:results).empty?
+ end
+
+ def test_search_project_and_subprojects
+ get :index, :id => 1, :q => 'recipe subproject', :scope => 'subprojects', :all_words => ''
+ assert_response :success
+ assert_template 'index'
+ assert assigns(:results).include?(Issue.find(1))
+ assert assigns(:results).include?(Issue.find(5))
+ end
+
+ def test_search_without_searchable_custom_fields
+ CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}"
+
+ get :index, :id => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:project)
+
+ get :index, :id => 1, :q => "can"
+ assert_response :success
+ assert_template 'index'
+ end
+
+ def test_search_with_searchable_custom_fields
+ get :index, :id => 1, :q => "stringforcustomfield"
+ assert_response :success
+ results = assigns(:results)
+ assert_not_nil results
+ assert_equal 1, results.size
+ assert results.include?(Issue.find(7))
+ end
+
+ def test_search_all_words
+ # 'all words' is on by default
+ get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1'
+ assert_equal true, assigns(:all_words)
+ results = assigns(:results)
+ assert_not_nil results
+ assert_equal 1, results.size
+ assert results.include?(Issue.find(3))
+ end
+
+ def test_search_one_of_the_words
+ get :index, :id => 1, :q => 'recipe updating saving', :all_words => ''
+ assert_equal false, assigns(:all_words)
+ results = assigns(:results)
+ assert_not_nil results
+ assert_equal 3, results.size
+ assert results.include?(Issue.find(3))
+ end
+
+ def test_search_titles_only_without_result
+ get :index, :id => 1, :q => 'recipe updating saving', :titles_only => '1'
+ results = assigns(:results)
+ assert_not_nil results
+ assert_equal 0, results.size
+ end
+
+ def test_search_titles_only
+ get :index, :id => 1, :q => 'recipe', :titles_only => '1'
+ assert_equal true, assigns(:titles_only)
+ results = assigns(:results)
+ assert_not_nil results
+ assert_equal 2, results.size
+ end
+
+ def test_search_content
+ Issue.update_all("description = 'This is a searchkeywordinthecontent'", "id=1")
+
+ get :index, :id => 1, :q => 'searchkeywordinthecontent', :titles_only => ''
+ assert_equal false, assigns(:titles_only)
+ results = assigns(:results)
+ assert_not_nil results
+ assert_equal 1, results.size
+ end
+
+ def test_search_with_offset
+ get :index, :q => 'coo', :offset => '20080806073000'
+ assert_response :success
+ results = assigns(:results)
+ assert results.any?
+ assert results.map(&:event_datetime).max < '20080806T073000'.to_time
+ end
+
+ def test_search_previous_with_offset
+ get :index, :q => 'coo', :offset => '20080806073000', :previous => '1'
+ assert_response :success
+ results = assigns(:results)
+ assert results.any?
+ assert results.map(&:event_datetime).min >= '20080806T073000'.to_time
+ end
+
+ def test_search_with_invalid_project_id
+ get :index, :id => 195, :q => 'recipe'
+ assert_response 404
+ assert_nil assigns(:results)
+ end
+
+ def test_quick_jump_to_issue
+ # issue of a public project
+ get :index, :q => "3"
+ assert_redirected_to '/issues/3'
+
+ # issue of a private project
+ get :index, :q => "4"
+ assert_response :success
+ assert_template 'index'
+ end
+
+ def test_large_integer
+ get :index, :q => '4615713488'
+ assert_response :success
+ assert_template 'index'
+ end
+
+ def test_tokens_with_quotes
+ get :index, :id => 1, :q => '"good bye" hello "bye bye"'
+ assert_equal ["good bye", "hello", "bye bye"], assigns(:tokens)
+ end
+
+ def test_results_should_be_escaped_once
+ assert Issue.find(1).update_attributes(:subject => ' escaped_once', :description => ' escaped_once')
+ get :index, :q => 'escaped_once'
+ assert_response :success
+ assert_select '#search-results' do
+ assert_select 'dt.issue a', :text => /<subject>/
+ assert_select 'dd', :text => /<description>/
+ end
+ end
+
+ def test_keywords_should_be_highlighted
+ assert Issue.find(1).update_attributes(:subject => 'subject highlighted', :description => 'description highlighted')
+ get :index, :q => 'highlighted'
+ assert_response :success
+ assert_select '#search-results' do
+ assert_select 'dt.issue a span.highlight', :text => 'highlighted'
+ assert_select 'dd span.highlight', :text => 'highlighted'
+ end
+ end
+end
diff --git a/test/functional/sessions_test.rb b/test/functional/sessions_test.rb
new file mode 100644
index 000000000..98a61f3e7
--- /dev/null
+++ b/test/functional/sessions_test.rb
@@ -0,0 +1,117 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class SessionStartTest < ActionController::TestCase
+ tests AccountController
+
+ fixtures :users
+
+ def test_login_should_set_session_timestamps
+ post :login, :username => 'jsmith', :password => 'jsmith'
+ assert_response 302
+ assert_equal 2, session[:user_id]
+ assert_not_nil session[:ctime]
+ assert_not_nil session[:atime]
+ end
+end
+
+class SessionsTest < ActionController::TestCase
+ tests WelcomeController
+
+ fixtures :users
+
+ def test_atime_from_user_session_should_be_updated
+ created = 2.hours.ago.utc.to_i
+ get :index, {}, {:user_id => 2, :ctime => created, :atime => created}
+ assert_response :success
+ assert_equal created, session[:ctime]
+ assert_not_equal created, session[:atime]
+ assert session[:atime] > created
+ end
+
+ def test_user_session_should_not_be_reset_if_lifetime_and_timeout_disabled
+ with_settings :session_lifetime => '0', :session_timeout => '0' do
+ get :index, {}, {:user_id => 2}
+ assert_response :success
+ end
+ end
+
+ def test_user_session_without_ctime_should_be_reset_if_lifetime_enabled
+ with_settings :session_lifetime => '720' do
+ get :index, {}, {:user_id => 2}
+ assert_redirected_to '/login'
+ end
+ end
+
+ def test_user_session_with_expired_ctime_should_be_reset_if_lifetime_enabled
+ with_settings :session_timeout => '720' do
+ get :index, {}, {:user_id => 2, :atime => 2.days.ago.utc.to_i}
+ assert_redirected_to '/login'
+ end
+ end
+
+ def test_user_session_with_valid_ctime_should_not_be_reset_if_lifetime_enabled
+ with_settings :session_timeout => '720' do
+ get :index, {}, {:user_id => 2, :atime => 3.hours.ago.utc.to_i}
+ assert_response :success
+ end
+ end
+
+ def test_user_session_without_atime_should_be_reset_if_timeout_enabled
+ with_settings :session_timeout => '60' do
+ get :index, {}, {:user_id => 2}
+ assert_redirected_to '/login'
+ end
+ end
+
+ def test_user_session_with_expired_atime_should_be_reset_if_timeout_enabled
+ with_settings :session_timeout => '60' do
+ get :index, {}, {:user_id => 2, :atime => 4.hours.ago.utc.to_i}
+ assert_redirected_to '/login'
+ end
+ end
+
+ def test_user_session_with_valid_atime_should_not_be_reset_if_timeout_enabled
+ with_settings :session_timeout => '60' do
+ get :index, {}, {:user_id => 2, :atime => 10.minutes.ago.utc.to_i}
+ assert_response :success
+ end
+ end
+
+ def test_expired_user_session_should_be_restarted_if_autologin
+ with_settings :session_lifetime => '720', :session_timeout => '60', :autologin => 7 do
+ token = Token.create!(:user_id => 2, :action => 'autologin', :created_on => 1.day.ago)
+ @request.cookies['autologin'] = token.value
+ created = 2.hours.ago.utc.to_i
+
+ get :index, {}, {:user_id => 2, :ctime => created, :atime => created}
+ assert_equal 2, session[:user_id]
+ assert_response :success
+ assert_not_equal created, session[:ctime]
+ assert session[:ctime] >= created
+ end
+ end
+
+ def test_anonymous_session_should_not_be_reset
+ with_settings :session_lifetime => '720', :session_timeout => '60' do
+ get :index
+ assert_response :success
+ end
+ end
+end
diff --git a/test/functional/settings_controller_test.rb b/test/functional/settings_controller_test.rb
new file mode 100644
index 000000000..df4abd6b7
--- /dev/null
+++ b/test/functional/settings_controller_test.rb
@@ -0,0 +1,131 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class SettingsControllerTest < ActionController::TestCase
+ fixtures :users
+
+ def setup
+ User.current = nil
+ @request.session[:user_id] = 1 # admin
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_get_edit
+ get :edit
+ assert_response :success
+ assert_template 'edit'
+
+ assert_tag 'input', :attributes => {:name => 'settings[enabled_scm][]', :value => ''}
+ end
+
+ def test_get_edit_should_preselect_default_issue_list_columns
+ with_settings :issue_list_default_columns => %w(tracker subject status updated_on) do
+ get :edit
+ assert_response :success
+ end
+
+ assert_select 'select[id=selected_columns][name=?]', 'settings[issue_list_default_columns][]' do
+ assert_select 'option', 4
+ assert_select 'option[value=tracker]', :text => 'Tracker'
+ assert_select 'option[value=subject]', :text => 'Subject'
+ assert_select 'option[value=status]', :text => 'Status'
+ assert_select 'option[value=updated_on]', :text => 'Updated'
+ end
+
+ assert_select 'select[id=available_columns]' do
+ assert_select 'option[value=tracker]', 0
+ assert_select 'option[value=priority]', :text => 'Priority'
+ end
+ end
+
+ def test_get_edit_without_trackers_should_succeed
+ Tracker.delete_all
+
+ get :edit
+ assert_response :success
+ end
+
+ def test_post_edit_notifications
+ post :edit, :settings => {:mail_from => 'functional@test.foo',
+ :bcc_recipients => '0',
+ :notified_events => %w(issue_added issue_updated news_added),
+ :emails_footer => 'Test footer'
+ }
+ assert_redirected_to '/settings'
+ assert_equal 'functional@test.foo', Setting.mail_from
+ assert !Setting.bcc_recipients?
+ assert_equal %w(issue_added issue_updated news_added), Setting.notified_events
+ assert_equal 'Test footer', Setting.emails_footer
+ Setting.clear_cache
+ end
+
+ def test_get_plugin_settings
+ Setting.stubs(:plugin_foo).returns({'sample_setting' => 'Plugin setting value'})
+ ActionController::Base.append_view_path(File.join(Rails.root, "test/fixtures/plugins"))
+ Redmine::Plugin.register :foo do
+ settings :partial => "foo_plugin/foo_plugin_settings"
+ end
+
+ get :plugin, :id => 'foo'
+ assert_response :success
+ assert_template 'plugin'
+ assert_tag 'form', :attributes => {:action => '/settings/plugin/foo'},
+ :descendant => {:tag => 'input', :attributes => {:name => 'settings[sample_setting]', :value => 'Plugin setting value'}}
+
+ Redmine::Plugin.clear
+ end
+
+ def test_get_invalid_plugin_settings
+ get :plugin, :id => 'none'
+ assert_response 404
+ end
+
+ def test_get_non_configurable_plugin_settings
+ Redmine::Plugin.register(:foo) {}
+
+ get :plugin, :id => 'foo'
+ assert_response 404
+
+ Redmine::Plugin.clear
+ end
+
+ def test_post_plugin_settings
+ Setting.expects(:plugin_foo=).with({'sample_setting' => 'Value'}).returns(true)
+ Redmine::Plugin.register(:foo) do
+ settings :partial => 'not blank' # so that configurable? is true
+ end
+
+ post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
+ assert_redirected_to '/settings/plugin/foo'
+ end
+
+ def test_post_non_configurable_plugin_settings
+ Redmine::Plugin.register(:foo) {}
+
+ post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
+ assert_response 404
+
+ Redmine::Plugin.clear
+ end
+end
diff --git a/test/functional/sys_controller_test.rb b/test/functional/sys_controller_test.rb
new file mode 100644
index 000000000..670c6b762
--- /dev/null
+++ b/test/functional/sys_controller_test.rb
@@ -0,0 +1,125 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class SysControllerTest < ActionController::TestCase
+ fixtures :projects, :repositories, :enabled_modules
+
+ def setup
+ Setting.sys_api_enabled = '1'
+ Setting.enabled_scm = %w(Subversion Git)
+ end
+
+ def teardown
+ Setting.clear_cache
+ end
+
+ def test_projects_with_repository_enabled
+ get :projects
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ with_options :tag => 'projects' do |test|
+ test.assert_tag :children => { :count => Project.active.has_module(:repository).count }
+ test.assert_tag 'project', :child => {:tag => 'identifier', :sibling => {:tag => 'is-public'}}
+ end
+ assert_no_tag 'extra-info'
+ assert_no_tag 'extra_info'
+ end
+
+ def test_create_project_repository
+ assert_nil Project.find(4).repository
+
+ post :create_project_repository, :id => 4,
+ :vendor => 'Subversion',
+ :repository => { :url => 'file:///create/project/repository/subproject2'}
+ assert_response :created
+ assert_equal 'application/xml', @response.content_type
+
+ r = Project.find(4).repository
+ assert r.is_a?(Repository::Subversion)
+ assert_equal 'file:///create/project/repository/subproject2', r.url
+
+ assert_tag 'repository-subversion',
+ :child => {
+ :tag => 'id', :content => r.id.to_s,
+ :sibling => {:tag => 'url', :content => r.url}
+ }
+ assert_no_tag 'extra-info'
+ assert_no_tag 'extra_info'
+ end
+
+ def test_create_already_existing
+ post :create_project_repository, :id => 1,
+ :vendor => 'Subversion',
+ :repository => { :url => 'file:///create/project/repository/subproject2'}
+
+ assert_response :conflict
+ end
+
+ def test_create_with_failure
+ post :create_project_repository, :id => 4,
+ :vendor => 'Subversion',
+ :repository => { :url => 'invalid url'}
+
+ assert_response :unprocessable_entity
+ end
+
+ def test_fetch_changesets
+ Repository::Subversion.any_instance.expects(:fetch_changesets).twice.returns(true)
+ get :fetch_changesets
+ assert_response :success
+ end
+
+ def test_fetch_changesets_one_project_by_identifier
+ Repository::Subversion.any_instance.expects(:fetch_changesets).once.returns(true)
+ get :fetch_changesets, :id => 'ecookbook'
+ assert_response :success
+ end
+
+ def test_fetch_changesets_one_project_by_id
+ Repository::Subversion.any_instance.expects(:fetch_changesets).once.returns(true)
+ get :fetch_changesets, :id => '1'
+ assert_response :success
+ end
+
+ def test_fetch_changesets_unknown_project
+ get :fetch_changesets, :id => 'unknown'
+ assert_response 404
+ end
+
+ def test_disabled_ws_should_respond_with_403_error
+ with_settings :sys_api_enabled => '0' do
+ get :projects
+ assert_response 403
+ end
+ end
+
+ def test_api_key
+ with_settings :sys_api_key => 'my_secret_key' do
+ get :projects, :key => 'my_secret_key'
+ assert_response :success
+ end
+ end
+
+ def test_wrong_key_should_respond_with_403_error
+ with_settings :sys_api_enabled => 'my_secret_key' do
+ get :projects, :key => 'wrong_key'
+ assert_response 403
+ end
+ end
+end
diff --git a/test/functional/time_entry_reports_controller_test.rb b/test/functional/time_entry_reports_controller_test.rb
new file mode 100644
index 000000000..755cdc345
--- /dev/null
+++ b/test/functional/time_entry_reports_controller_test.rb
@@ -0,0 +1,372 @@
+# -*- coding: utf-8 -*-
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class TimeEntryReportsControllerTest < ActionController::TestCase
+ tests TimelogController
+
+ fixtures :projects, :enabled_modules, :roles, :members, :member_roles,
+ :issues, :time_entries, :users, :trackers, :enumerations,
+ :issue_statuses, :custom_fields, :custom_values
+
+ include Redmine::I18n
+
+ def setup
+ Setting.default_language = "en"
+ end
+
+ def test_report_at_project_level
+ get :report, :project_id => 'ecookbook'
+ assert_response :success
+ assert_template 'report'
+ assert_tag :form,
+ :attributes => {:action => "/projects/ecookbook/time_entries/report", :id => 'query_form'}
+ end
+
+ def test_report_all_projects
+ get :report
+ assert_response :success
+ assert_template 'report'
+ assert_tag :form,
+ :attributes => {:action => "/time_entries/report", :id => 'query_form'}
+ end
+
+ def test_report_all_projects_denied
+ r = Role.anonymous
+ r.permissions.delete(:view_time_entries)
+ r.permissions_will_change!
+ r.save
+ get :report
+ assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport'
+ end
+
+ def test_report_all_projects_one_criteria
+ get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:report)
+ assert_equal "8.65", "%.2f" % assigns(:report).total_hours
+ end
+
+ def test_report_all_time
+ get :report, :project_id => 1, :criteria => ['project', 'issue']
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:report)
+ assert_equal "162.90", "%.2f" % assigns(:report).total_hours
+ end
+
+ def test_report_all_time_by_day
+ get :report, :project_id => 1, :criteria => ['project', 'issue'], :columns => 'day'
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:report)
+ assert_equal "162.90", "%.2f" % assigns(:report).total_hours
+ assert_tag :tag => 'th', :content => '2007-03-12'
+ end
+
+ def test_report_one_criteria
+ get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:report)
+ assert_equal "8.65", "%.2f" % assigns(:report).total_hours
+ end
+
+ def test_report_two_criteria
+ get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["user", "activity"]
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:report)
+ assert_equal "162.90", "%.2f" % assigns(:report).total_hours
+ end
+
+ def test_report_custom_field_criteria_with_multiple_values
+ field = TimeEntryCustomField.create!(:name => 'multi', :field_format => 'list', :possible_values => ['value1', 'value2'])
+ entry = TimeEntry.create!(:project => Project.find(1), :hours => 1, :activity_id => 10, :user => User.find(2), :spent_on => Date.today)
+ CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value1')
+ CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value2')
+
+ get :report, :project_id => 1, :columns => 'day', :criteria => ["cf_#{field.id}"]
+ assert_response :success
+ end
+
+ def test_report_one_day
+ get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criteria => ["user", "activity"]
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:report)
+ assert_equal "4.25", "%.2f" % assigns(:report).total_hours
+ end
+
+ def test_report_at_issue_level
+ get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["user", "activity"]
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:report)
+ assert_equal "154.25", "%.2f" % assigns(:report).total_hours
+ assert_tag :form,
+ :attributes => {:action => "/projects/ecookbook/issues/1/time_entries/report", :id => 'query_form'}
+ end
+
+ def test_report_by_week_should_use_commercial_year
+ TimeEntry.delete_all
+ TimeEntry.generate!(:hours => '2', :spent_on => '2009-12-25') # 2009-52
+ TimeEntry.generate!(:hours => '4', :spent_on => '2009-12-31') # 2009-53
+ TimeEntry.generate!(:hours => '8', :spent_on => '2010-01-01') # 2009-53
+ TimeEntry.generate!(:hours => '16', :spent_on => '2010-01-05') # 2010-1
+
+ get :report, :columns => 'week', :from => "2009-12-25", :to => "2010-01-05", :criteria => ["project"]
+ assert_response :success
+
+ assert_select '#time-report thead tr' do
+ assert_select 'th:nth-child(1)', :text => 'Project'
+ assert_select 'th:nth-child(2)', :text => '2009-52'
+ assert_select 'th:nth-child(3)', :text => '2009-53'
+ assert_select 'th:nth-child(4)', :text => '2010-1'
+ assert_select 'th:nth-child(5)', :text => 'Total time'
+ end
+ assert_select '#time-report tbody tr' do
+ assert_select 'td:nth-child(1)', :text => 'eCookbook'
+ assert_select 'td:nth-child(2)', :text => '2.00'
+ assert_select 'td:nth-child(3)', :text => '12.00'
+ assert_select 'td:nth-child(4)', :text => '16.00'
+ assert_select 'td:nth-child(5)', :text => '30.00' # Total
+ end
+ end
+
+ def test_report_should_propose_association_custom_fields
+ get :report
+ assert_response :success
+ assert_template 'report'
+
+ assert_select 'select[name=?]', 'criteria[]' do
+ assert_select 'option[value=cf_1]', {:text => 'Database'}, 'Issue custom field not found'
+ assert_select 'option[value=cf_3]', {:text => 'Development status'}, 'Project custom field not found'
+ assert_select 'option[value=cf_7]', {:text => 'Billable'}, 'TimeEntryActivity custom field not found'
+ end
+ end
+
+ def test_report_with_association_custom_fields
+ get :report, :criteria => ['cf_1', 'cf_3', 'cf_7']
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:report)
+ assert_equal 3, assigns(:report).criteria.size
+ assert_equal "162.90", "%.2f" % assigns(:report).total_hours
+
+ # Custom fields columns
+ assert_select 'th', :text => 'Database'
+ assert_select 'th', :text => 'Development status'
+ assert_select 'th', :text => 'Billable'
+
+ # Custom field row
+ assert_select 'tr' do
+ assert_select 'td', :text => 'MySQL'
+ assert_select 'td.hours', :text => '1.00'
+ end
+ end
+
+ def test_report_one_criteria_no_result
+ get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criteria => ['project']
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:report)
+ assert_equal "0.00", "%.2f" % assigns(:report).total_hours
+ end
+
+ def test_report_status_criterion
+ get :report, :project_id => 1, :criteria => ['status']
+ assert_response :success
+ assert_template 'report'
+ assert_tag :tag => 'th', :content => 'Status'
+ assert_tag :tag => 'td', :content => 'New'
+ end
+
+ def test_report_all_projects_csv_export
+ get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30",
+ :criteria => ["project", "user", "activity"], :format => "csv"
+ assert_response :success
+ assert_equal 'text/csv; header=present', @response.content_type
+ lines = @response.body.chomp.split("\n")
+ # Headers
+ assert_equal 'Project,User,Activity,2007-3,2007-4,Total time', lines.first
+ # Total row
+ assert_equal 'Total time,"","",154.25,8.65,162.90', lines.last
+ end
+
+ def test_report_csv_export
+ get :report, :project_id => 1, :columns => 'month',
+ :from => "2007-01-01", :to => "2007-06-30",
+ :criteria => ["project", "user", "activity"], :format => "csv"
+ assert_response :success
+ assert_equal 'text/csv; header=present', @response.content_type
+ lines = @response.body.chomp.split("\n")
+ # Headers
+ assert_equal 'Project,User,Activity,2007-3,2007-4,Total time', lines.first
+ # Total row
+ assert_equal 'Total time,"","",154.25,8.65,162.90', lines.last
+ end
+
+ def test_csv_big_5
+ Setting.default_language = "zh-TW"
+ str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
+ str_big5 = "\xa4@\xa4\xeb"
+ if str_utf8.respond_to?(:force_encoding)
+ str_utf8.force_encoding('UTF-8')
+ str_big5.force_encoding('Big5')
+ end
+ user = User.find_by_id(3)
+ user.firstname = str_utf8
+ user.lastname = "test-lastname"
+ assert user.save
+ comments = "test_csv_big_5"
+ te1 = TimeEntry.create(:spent_on => '2011-11-11',
+ :hours => 7.3,
+ :project => Project.find(1),
+ :user => user,
+ :activity => TimeEntryActivity.find_by_name('Design'),
+ :comments => comments)
+
+ te2 = TimeEntry.find_by_comments(comments)
+ assert_not_nil te2
+ assert_equal 7.3, te2.hours
+ assert_equal 3, te2.user_id
+
+ get :report, :project_id => 1, :columns => 'day',
+ :from => "2011-11-11", :to => "2011-11-11",
+ :criteria => ["user"], :format => "csv"
+ assert_response :success
+ assert_equal 'text/csv; header=present', @response.content_type
+ lines = @response.body.chomp.split("\n")
+ # Headers
+ s1 = "\xa5\xce\xa4\xe1,2011-11-11,\xc1`\xadp"
+ s2 = "\xc1`\xadp"
+ if s1.respond_to?(:force_encoding)
+ s1.force_encoding('Big5')
+ s2.force_encoding('Big5')
+ end
+ assert_equal s1, lines.first
+ # Total row
+ assert_equal "#{str_big5} #{user.lastname},7.30,7.30", lines[1]
+ assert_equal "#{s2},7.30,7.30", lines[2]
+
+ str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
+ if str_tw.respond_to?(:force_encoding)
+ str_tw.force_encoding('UTF-8')
+ end
+ assert_equal str_tw, l(:general_lang_name)
+ assert_equal 'Big5', l(:general_csv_encoding)
+ assert_equal ',', l(:general_csv_separator)
+ assert_equal '.', l(:general_csv_decimal_separator)
+ end
+
+ def test_csv_cannot_convert_should_be_replaced_big_5
+ Setting.default_language = "zh-TW"
+ str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
+ if str_utf8.respond_to?(:force_encoding)
+ str_utf8.force_encoding('UTF-8')
+ end
+ user = User.find_by_id(3)
+ user.firstname = str_utf8
+ user.lastname = "test-lastname"
+ assert user.save
+ comments = "test_replaced"
+ te1 = TimeEntry.create(:spent_on => '2011-11-11',
+ :hours => 7.3,
+ :project => Project.find(1),
+ :user => user,
+ :activity => TimeEntryActivity.find_by_name('Design'),
+ :comments => comments)
+
+ te2 = TimeEntry.find_by_comments(comments)
+ assert_not_nil te2
+ assert_equal 7.3, te2.hours
+ assert_equal 3, te2.user_id
+
+ get :report, :project_id => 1, :columns => 'day',
+ :from => "2011-11-11", :to => "2011-11-11",
+ :criteria => ["user"], :format => "csv"
+ assert_response :success
+ assert_equal 'text/csv; header=present', @response.content_type
+ lines = @response.body.chomp.split("\n")
+ # Headers
+ s1 = "\xa5\xce\xa4\xe1,2011-11-11,\xc1`\xadp"
+ if s1.respond_to?(:force_encoding)
+ s1.force_encoding('Big5')
+ end
+ assert_equal s1, lines.first
+ # Total row
+ s2 = ""
+ if s2.respond_to?(:force_encoding)
+ s2 = "\xa5H?"
+ s2.force_encoding('Big5')
+ elsif RUBY_PLATFORM == 'java'
+ s2 = "??"
+ else
+ s2 = "\xa5H???"
+ end
+ assert_equal "#{s2} #{user.lastname},7.30,7.30", lines[1]
+ end
+
+ def test_csv_fr
+ with_settings :default_language => "fr" do
+ str1 = "test_csv_fr"
+ user = User.find_by_id(3)
+ te1 = TimeEntry.create(:spent_on => '2011-11-11',
+ :hours => 7.3,
+ :project => Project.find(1),
+ :user => user,
+ :activity => TimeEntryActivity.find_by_name('Design'),
+ :comments => str1)
+
+ te2 = TimeEntry.find_by_comments(str1)
+ assert_not_nil te2
+ assert_equal 7.3, te2.hours
+ assert_equal 3, te2.user_id
+
+ get :report, :project_id => 1, :columns => 'day',
+ :from => "2011-11-11", :to => "2011-11-11",
+ :criteria => ["user"], :format => "csv"
+ assert_response :success
+ assert_equal 'text/csv; header=present', @response.content_type
+ lines = @response.body.chomp.split("\n")
+ # Headers
+ s1 = "Utilisateur;2011-11-11;Temps total"
+ s2 = "Temps total"
+ if s1.respond_to?(:force_encoding)
+ s1.force_encoding('ISO-8859-1')
+ s2.force_encoding('ISO-8859-1')
+ end
+ assert_equal s1, lines.first
+ # Total row
+ assert_equal "#{user.firstname} #{user.lastname};7,30;7,30", lines[1]
+ assert_equal "#{s2};7,30;7,30", lines[2]
+
+ str_fr = "Fran\xc3\xa7ais"
+ if str_fr.respond_to?(:force_encoding)
+ str_fr.force_encoding('UTF-8')
+ end
+ assert_equal str_fr, l(:general_lang_name)
+ assert_equal 'ISO-8859-1', l(:general_csv_encoding)
+ assert_equal ';', l(:general_csv_separator)
+ assert_equal ',', l(:general_csv_decimal_separator)
+ end
+ end
+end
diff --git a/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb
new file mode 100644
index 000000000..7b9c9e891
--- /dev/null
+++ b/test/functional/timelog_controller_test.rb
@@ -0,0 +1,604 @@
+# -*- coding: utf-8 -*-
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class TimelogControllerTest < ActionController::TestCase
+ fixtures :projects, :enabled_modules, :roles, :members,
+ :member_roles, :issues, :time_entries, :users,
+ :trackers, :enumerations, :issue_statuses,
+ :custom_fields, :custom_values,
+ :projects_trackers, :custom_fields_trackers,
+ :custom_fields_projects
+
+ include Redmine::I18n
+
+ def test_new_with_project_id
+ @request.session[:user_id] = 3
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+ assert_select 'select[name=?]', 'time_entry[project_id]', 0
+ assert_select 'input[name=?][value=1][type=hidden]', 'time_entry[project_id]'
+ end
+
+ def test_new_with_issue_id
+ @request.session[:user_id] = 3
+ get :new, :issue_id => 2
+ assert_response :success
+ assert_template 'new'
+ assert_select 'select[name=?]', 'time_entry[project_id]', 0
+ assert_select 'input[name=?][value=1][type=hidden]', 'time_entry[project_id]'
+ end
+
+ def test_new_without_project
+ @request.session[:user_id] = 3
+ get :new
+ assert_response :success
+ assert_template 'new'
+ assert_select 'select[name=?]', 'time_entry[project_id]'
+ assert_select 'input[name=?]', 'time_entry[project_id]', 0
+ end
+
+ def test_new_without_project_should_prefill_the_form
+ @request.session[:user_id] = 3
+ get :new, :time_entry => {:project_id => '1'}
+ assert_response :success
+ assert_template 'new'
+ assert_select 'select[name=?]', 'time_entry[project_id]' do
+ assert_select 'option[value=1][selected=selected]'
+ end
+ assert_select 'input[name=?]', 'time_entry[project_id]', 0
+ end
+
+ def test_new_without_project_should_deny_without_permission
+ Role.all.each {|role| role.remove_permission! :log_time}
+ @request.session[:user_id] = 3
+
+ get :new
+ assert_response 403
+ end
+
+ def test_new_should_select_default_activity
+ @request.session[:user_id] = 3
+ get :new, :project_id => 1
+ assert_response :success
+ assert_select 'select[name=?]', 'time_entry[activity_id]' do
+ assert_select 'option[selected=selected]', :text => 'Development'
+ end
+ end
+
+ def test_new_should_only_show_active_time_entry_activities
+ @request.session[:user_id] = 3
+ get :new, :project_id => 1
+ assert_response :success
+ assert_no_tag 'option', :content => 'Inactive Activity'
+ end
+
+ def test_get_edit_existing_time
+ @request.session[:user_id] = 2
+ get :edit, :id => 2, :project_id => nil
+ assert_response :success
+ assert_template 'edit'
+ # Default activity selected
+ assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/time_entries/2' }
+ end
+
+ def test_get_edit_with_an_existing_time_entry_with_inactive_activity
+ te = TimeEntry.find(1)
+ te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
+ te.save!
+
+ @request.session[:user_id] = 1
+ get :edit, :project_id => 1, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ # Blank option since nothing is pre-selected
+ assert_tag :tag => 'option', :content => '--- Please select ---'
+ end
+
+ def test_post_create
+ # TODO: should POST to issues’ time log instead of project. change form
+ # and routing
+ @request.session[:user_id] = 3
+ post :create, :project_id => 1,
+ :time_entry => {:comments => 'Some work on TimelogControllerTest',
+ # Not the default activity
+ :activity_id => '11',
+ :spent_on => '2008-03-14',
+ :issue_id => '1',
+ :hours => '7.3'}
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+
+ i = Issue.find(1)
+ t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
+ assert_not_nil t
+ assert_equal 11, t.activity_id
+ assert_equal 7.3, t.hours
+ assert_equal 3, t.user_id
+ assert_equal i, t.issue
+ assert_equal i.project, t.project
+ end
+
+ def test_post_create_with_blank_issue
+ # TODO: should POST to issues’ time log instead of project. change form
+ # and routing
+ @request.session[:user_id] = 3
+ post :create, :project_id => 1,
+ :time_entry => {:comments => 'Some work on TimelogControllerTest',
+ # Not the default activity
+ :activity_id => '11',
+ :issue_id => '',
+ :spent_on => '2008-03-14',
+ :hours => '7.3'}
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+
+ t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
+ assert_not_nil t
+ assert_equal 11, t.activity_id
+ assert_equal 7.3, t.hours
+ assert_equal 3, t.user_id
+ end
+
+ def test_create_and_continue
+ @request.session[:user_id] = 2
+ post :create, :project_id => 1,
+ :time_entry => {:activity_id => '11',
+ :issue_id => '',
+ :spent_on => '2008-03-14',
+ :hours => '7.3'},
+ :continue => '1'
+ assert_redirected_to '/projects/ecookbook/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D='
+ end
+
+ def test_create_and_continue_with_issue_id
+ @request.session[:user_id] = 2
+ post :create, :project_id => 1,
+ :time_entry => {:activity_id => '11',
+ :issue_id => '1',
+ :spent_on => '2008-03-14',
+ :hours => '7.3'},
+ :continue => '1'
+ assert_redirected_to '/projects/ecookbook/issues/1/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1'
+ end
+
+ def test_create_and_continue_without_project
+ @request.session[:user_id] = 2
+ post :create, :time_entry => {:project_id => '1',
+ :activity_id => '11',
+ :issue_id => '',
+ :spent_on => '2008-03-14',
+ :hours => '7.3'},
+ :continue => '1'
+
+ assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D=1'
+ end
+
+ def test_create_without_log_time_permission_should_be_denied
+ @request.session[:user_id] = 2
+ Role.find_by_name('Manager').remove_permission! :log_time
+ post :create, :project_id => 1,
+ :time_entry => {:activity_id => '11',
+ :issue_id => '',
+ :spent_on => '2008-03-14',
+ :hours => '7.3'}
+
+ assert_response 403
+ end
+
+ def test_create_with_failure
+ @request.session[:user_id] = 2
+ post :create, :project_id => 1,
+ :time_entry => {:activity_id => '',
+ :issue_id => '',
+ :spent_on => '2008-03-14',
+ :hours => '7.3'}
+
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_create_without_project
+ @request.session[:user_id] = 2
+ assert_difference 'TimeEntry.count' do
+ post :create, :time_entry => {:project_id => '1',
+ :activity_id => '11',
+ :issue_id => '',
+ :spent_on => '2008-03-14',
+ :hours => '7.3'}
+ end
+
+ assert_redirected_to '/projects/ecookbook/time_entries'
+ time_entry = TimeEntry.first(:order => 'id DESC')
+ assert_equal 1, time_entry.project_id
+ end
+
+ def test_create_without_project_should_fail_with_issue_not_inside_project
+ @request.session[:user_id] = 2
+ assert_no_difference 'TimeEntry.count' do
+ post :create, :time_entry => {:project_id => '1',
+ :activity_id => '11',
+ :issue_id => '5',
+ :spent_on => '2008-03-14',
+ :hours => '7.3'}
+ end
+
+ assert_response :success
+ assert assigns(:time_entry).errors[:issue_id].present?
+ end
+
+ def test_create_without_project_should_deny_without_permission
+ @request.session[:user_id] = 2
+ Project.find(3).disable_module!(:time_tracking)
+
+ assert_no_difference 'TimeEntry.count' do
+ post :create, :time_entry => {:project_id => '3',
+ :activity_id => '11',
+ :issue_id => '',
+ :spent_on => '2008-03-14',
+ :hours => '7.3'}
+ end
+
+ assert_response 403
+ end
+
+ def test_create_without_project_with_failure
+ @request.session[:user_id] = 2
+ assert_no_difference 'TimeEntry.count' do
+ post :create, :time_entry => {:project_id => '1',
+ :activity_id => '11',
+ :issue_id => '',
+ :spent_on => '2008-03-14',
+ :hours => ''}
+ end
+
+ assert_response :success
+ assert_tag 'select', :attributes => {:name => 'time_entry[project_id]'},
+ :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
+ end
+
+ def test_update
+ entry = TimeEntry.find(1)
+ assert_equal 1, entry.issue_id
+ assert_equal 2, entry.user_id
+
+ @request.session[:user_id] = 1
+ put :update, :id => 1,
+ :time_entry => {:issue_id => '2',
+ :hours => '8'}
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ entry.reload
+
+ assert_equal 8, entry.hours
+ assert_equal 2, entry.issue_id
+ assert_equal 2, entry.user_id
+ end
+
+ def test_get_bulk_edit
+ @request.session[:user_id] = 2
+ get :bulk_edit, :ids => [1, 2]
+ assert_response :success
+ assert_template 'bulk_edit'
+
+ assert_select 'ul#bulk-selection' do
+ assert_select 'li', 2
+ assert_select 'li a', :text => '03/23/2007 - eCookbook: 4.25 hours'
+ end
+
+ assert_select 'form#bulk_edit_form[action=?]', '/time_entries/bulk_update' do
+ # System wide custom field
+ assert_select 'select[name=?]', 'time_entry[custom_field_values][10]'
+
+ # Activities
+ assert_select 'select[name=?]', 'time_entry[activity_id]' do
+ assert_select 'option[value=]', :text => '(No change)'
+ assert_select 'option[value=9]', :text => 'Design'
+ end
+ end
+ end
+
+ def test_get_bulk_edit_on_different_projects
+ @request.session[:user_id] = 2
+ get :bulk_edit, :ids => [1, 2, 6]
+ assert_response :success
+ assert_template 'bulk_edit'
+ end
+
+ def test_bulk_update
+ @request.session[:user_id] = 2
+ # update time entry activity
+ post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
+
+ assert_response 302
+ # check that the issues were updated
+ assert_equal [9, 9], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.activity_id}
+ end
+
+ def test_bulk_update_with_failure
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
+
+ assert_response 302
+ assert_match /Failed to save 2 time entrie/, flash[:error]
+ end
+
+ def test_bulk_update_on_different_projects
+ @request.session[:user_id] = 2
+ # makes user a manager on the other project
+ Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
+
+ # update time entry activity
+ post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
+
+ assert_response 302
+ # check that the issues were updated
+ assert_equal [9, 9, 9], TimeEntry.find_all_by_id([1, 2, 4]).collect {|i| i.activity_id}
+ end
+
+ def test_bulk_update_on_different_projects_without_rights
+ @request.session[:user_id] = 3
+ user = User.find(3)
+ action = { :controller => "timelog", :action => "bulk_update" }
+ assert user.allowed_to?(action, TimeEntry.find(1).project)
+ assert ! user.allowed_to?(action, TimeEntry.find(5).project)
+ post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
+ assert_response 403
+ end
+
+ def test_bulk_update_custom_field
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
+
+ assert_response 302
+ assert_equal ["0", "0"], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.custom_value_for(10).value}
+ end
+
+ def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
+
+ assert_response :redirect
+ assert_redirected_to '/time_entries'
+ end
+
+ def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
+ @request.session[:user_id] = 2
+ post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
+
+ assert_response :redirect
+ assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
+ end
+
+ def test_post_bulk_update_without_edit_permission_should_be_denied
+ @request.session[:user_id] = 2
+ Role.find_by_name('Manager').remove_permission! :edit_time_entries
+ post :bulk_update, :ids => [1,2]
+
+ assert_response 403
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 2
+ delete :destroy, :id => 1
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert_equal I18n.t(:notice_successful_delete), flash[:notice]
+ assert_nil TimeEntry.find_by_id(1)
+ end
+
+ def test_destroy_should_fail
+ # simulate that this fails (e.g. due to a plugin), see #5700
+ TimeEntry.any_instance.expects(:destroy).returns(false)
+
+ @request.session[:user_id] = 2
+ delete :destroy, :id => 1
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
+ assert_not_nil TimeEntry.find_by_id(1)
+ end
+
+ def test_index_all_projects
+ get :index
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:total_hours)
+ assert_equal "162.90", "%.2f" % assigns(:total_hours)
+ assert_tag :form,
+ :attributes => {:action => "/time_entries", :id => 'query_form'}
+ end
+
+ def test_index_all_projects_should_show_log_time_link
+ @request.session[:user_id] = 2
+ get :index
+ assert_response :success
+ assert_template 'index'
+ assert_tag 'a', :attributes => {:href => '/time_entries/new'}, :content => /Log time/
+ end
+
+ def test_index_at_project_level
+ get :index, :project_id => 'ecookbook'
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:entries)
+ assert_equal 4, assigns(:entries).size
+ # project and subproject
+ assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
+ assert_not_nil assigns(:total_hours)
+ assert_equal "162.90", "%.2f" % assigns(:total_hours)
+ assert_tag :form,
+ :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
+ end
+
+ def test_index_at_project_level_with_date_range
+ get :index, :project_id => 'ecookbook',
+ :f => ['spent_on'],
+ :op => {'spent_on' => '><'},
+ :v => {'spent_on' => ['2007-03-20', '2007-04-30']}
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:entries)
+ assert_equal 3, assigns(:entries).size
+ assert_not_nil assigns(:total_hours)
+ assert_equal "12.90", "%.2f" % assigns(:total_hours)
+ assert_tag :form,
+ :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
+ end
+
+ def test_index_at_project_level_with_date_range_using_from_and_to_params
+ get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:entries)
+ assert_equal 3, assigns(:entries).size
+ assert_not_nil assigns(:total_hours)
+ assert_equal "12.90", "%.2f" % assigns(:total_hours)
+ assert_tag :form,
+ :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
+ end
+
+ def test_index_at_project_level_with_period
+ get :index, :project_id => 'ecookbook',
+ :f => ['spent_on'],
+ :op => {'spent_on' => '>t-'},
+ :v => {'spent_on' => ['7']}
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:entries)
+ assert_not_nil assigns(:total_hours)
+ assert_tag :form,
+ :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
+ end
+
+ def test_index_at_issue_level
+ get :index, :issue_id => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:entries)
+ assert_equal 2, assigns(:entries).size
+ assert_not_nil assigns(:total_hours)
+ assert_equal 154.25, assigns(:total_hours)
+ # display all time
+ assert_nil assigns(:from)
+ assert_nil assigns(:to)
+ # TODO: remove /projects/:project_id/issues/:issue_id/time_entries routes
+ # to use /issues/:issue_id/time_entries
+ assert_tag :form,
+ :attributes => {:action => "/projects/ecookbook/issues/1/time_entries", :id => 'query_form'}
+ end
+
+ def test_index_should_sort_by_spent_on_and_created_on
+ t1 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:00:00', :activity_id => 10)
+ t2 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10)
+ t3 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10)
+
+ get :index, :project_id => 1,
+ :f => ['spent_on'],
+ :op => {'spent_on' => '><'},
+ :v => {'spent_on' => ['2012-06-15', '2012-06-16']}
+ assert_response :success
+ assert_equal [t2, t1, t3], assigns(:entries)
+
+ get :index, :project_id => 1,
+ :f => ['spent_on'],
+ :op => {'spent_on' => '><'},
+ :v => {'spent_on' => ['2012-06-15', '2012-06-16']},
+ :sort => 'spent_on'
+ assert_response :success
+ assert_equal [t3, t1, t2], assigns(:entries)
+ end
+
+ def test_index_with_filter_on_issue_custom_field
+ issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
+ entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
+
+ get :index, :f => ['issue.cf_2'], :op => {'issue.cf_2' => '='}, :v => {'issue.cf_2' => ['filter_on_issue_custom_field']}
+ assert_response :success
+ assert_equal [entry], assigns(:entries)
+ end
+
+ def test_index_with_issue_custom_field_column
+ issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
+ entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
+
+ get :index, :c => %w(project spent_on issue comments hours issue.cf_2)
+ assert_response :success
+ assert_include :'issue.cf_2', assigns(:query).column_names
+ assert_select 'td.issue_cf_2', :text => 'filter_on_issue_custom_field'
+ end
+
+ def test_index_atom_feed
+ get :index, :project_id => 1, :format => 'atom'
+ assert_response :success
+ assert_equal 'application/atom+xml', @response.content_type
+ assert_not_nil assigns(:items)
+ assert assigns(:items).first.is_a?(TimeEntry)
+ end
+
+ def test_index_at_project_level_should_include_csv_export_dialog
+ get :index, :project_id => 'ecookbook',
+ :f => ['spent_on'],
+ :op => {'spent_on' => '>='},
+ :v => {'spent_on' => ['2007-04-01']},
+ :c => ['spent_on', 'user']
+ assert_response :success
+
+ assert_select '#csv-export-options' do
+ assert_select 'form[action=?][method=get]', '/projects/ecookbook/time_entries.csv' do
+ # filter
+ assert_select 'input[name=?][value=?]', 'f[]', 'spent_on'
+ assert_select 'input[name=?][value=?]', 'op[spent_on]', '>='
+ assert_select 'input[name=?][value=?]', 'v[spent_on][]', '2007-04-01'
+ # columns
+ assert_select 'input[name=?][value=?]', 'c[]', 'spent_on'
+ assert_select 'input[name=?][value=?]', 'c[]', 'user'
+ assert_select 'input[name=?]', 'c[]', 2
+ end
+ end
+ end
+
+ def test_index_cross_project_should_include_csv_export_dialog
+ get :index
+ assert_response :success
+
+ assert_select '#csv-export-options' do
+ assert_select 'form[action=?][method=get]', '/time_entries.csv'
+ end
+ end
+
+ def test_index_at_issue_level_should_include_csv_export_dialog
+ get :index, :project_id => 'ecookbook', :issue_id => 3
+ assert_response :success
+
+ assert_select '#csv-export-options' do
+ assert_select 'form[action=?][method=get]', '/projects/ecookbook/issues/3/time_entries.csv'
+ end
+ end
+
+ def test_index_csv_all_projects
+ Setting.date_format = '%m/%d/%Y'
+ get :index, :format => 'csv'
+ assert_response :success
+ assert_equal 'text/csv; header=present', response.content_type
+ end
+
+ def test_index_csv
+ Setting.date_format = '%m/%d/%Y'
+ get :index, :project_id => 1, :format => 'csv'
+ assert_response :success
+ assert_equal 'text/csv; header=present', response.content_type
+ end
+end
diff --git a/test/functional/trackers_controller_test.rb b/test/functional/trackers_controller_test.rb
new file mode 100644
index 000000000..79e2e819e
--- /dev/null
+++ b/test/functional/trackers_controller_test.rb
@@ -0,0 +1,211 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class TrackersControllerTest < ActionController::TestCase
+ fixtures :trackers, :projects, :projects_trackers, :users, :issues, :custom_fields
+
+ def setup
+ User.current = nil
+ @request.session[:user_id] = 1 # admin
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'index'
+ end
+
+ def test_index_by_anonymous_should_redirect_to_login_form
+ @request.session[:user_id] = nil
+ get :index
+ assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftrackers'
+ end
+
+ def test_index_by_user_should_respond_with_406
+ @request.session[:user_id] = 2
+ get :index
+ assert_response 406
+ end
+
+ def test_new
+ get :new
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_create
+ assert_difference 'Tracker.count' do
+ post :create, :tracker => { :name => 'New tracker', :project_ids => ['1', '', ''], :custom_field_ids => ['1', '6', ''] }
+ end
+ assert_redirected_to :action => 'index'
+ tracker = Tracker.first(:order => 'id DESC')
+ assert_equal 'New tracker', tracker.name
+ assert_equal [1], tracker.project_ids.sort
+ assert_equal Tracker::CORE_FIELDS, tracker.core_fields
+ assert_equal [1, 6], tracker.custom_field_ids.sort
+ assert_equal 0, tracker.workflow_rules.count
+ end
+
+ def create_with_disabled_core_fields
+ assert_difference 'Tracker.count' do
+ post :create, :tracker => { :name => 'New tracker', :core_fields => ['assigned_to_id', 'fixed_version_id', ''] }
+ end
+ assert_redirected_to :action => 'index'
+ tracker = Tracker.first(:order => 'id DESC')
+ assert_equal 'New tracker', tracker.name
+ assert_equal %w(assigned_to_id fixed_version_id), tracker.core_fields
+ end
+
+ def test_create_new_with_workflow_copy
+ assert_difference 'Tracker.count' do
+ post :create, :tracker => { :name => 'New tracker' }, :copy_workflow_from => 1
+ end
+ assert_redirected_to :action => 'index'
+ tracker = Tracker.find_by_name('New tracker')
+ assert_equal 0, tracker.projects.count
+ assert_equal Tracker.find(1).workflow_rules.count, tracker.workflow_rules.count
+ end
+
+ def test_create_with_failure
+ assert_no_difference 'Tracker.count' do
+ post :create, :tracker => { :name => '', :project_ids => ['1', '', ''], :custom_field_ids => ['1', '6', ''] }
+ end
+ assert_response :success
+ assert_template 'new'
+ assert_error_tag :content => /name can't be blank/i
+ end
+
+ def test_edit
+ Tracker.find(1).project_ids = [1, 3]
+
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+
+ assert_tag :input, :attributes => { :name => 'tracker[project_ids][]',
+ :value => '1',
+ :checked => 'checked' }
+
+ assert_tag :input, :attributes => { :name => 'tracker[project_ids][]',
+ :value => '2',
+ :checked => nil }
+
+ assert_tag :input, :attributes => { :name => 'tracker[project_ids][]',
+ :value => '',
+ :type => 'hidden'}
+ end
+
+ def test_edit_should_check_core_fields
+ tracker = Tracker.find(1)
+ tracker.core_fields = %w(assigned_to_id fixed_version_id)
+ tracker.save!
+
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+
+ assert_select 'input[name=?][value=assigned_to_id][checked=checked]', 'tracker[core_fields][]'
+ assert_select 'input[name=?][value=fixed_version_id][checked=checked]', 'tracker[core_fields][]'
+
+ assert_select 'input[name=?][value=category_id]', 'tracker[core_fields][]'
+ assert_select 'input[name=?][value=category_id][checked=checked]', 'tracker[core_fields][]', 0
+
+ assert_select 'input[name=?][value=][type=hidden]', 'tracker[core_fields][]'
+ end
+
+ def test_update
+ put :update, :id => 1, :tracker => { :name => 'Renamed',
+ :project_ids => ['1', '2', ''] }
+ assert_redirected_to :action => 'index'
+ assert_equal [1, 2], Tracker.find(1).project_ids.sort
+ end
+
+ def test_update_without_projects
+ put :update, :id => 1, :tracker => { :name => 'Renamed',
+ :project_ids => [''] }
+ assert_redirected_to :action => 'index'
+ assert Tracker.find(1).project_ids.empty?
+ end
+
+ def test_update_without_core_fields
+ put :update, :id => 1, :tracker => { :name => 'Renamed', :core_fields => [''] }
+ assert_redirected_to :action => 'index'
+ assert Tracker.find(1).core_fields.empty?
+ end
+
+ def test_update_with_failure
+ put :update, :id => 1, :tracker => { :name => '' }
+ assert_response :success
+ assert_template 'edit'
+ assert_error_tag :content => /name can't be blank/i
+ end
+
+ def test_move_lower
+ tracker = Tracker.find_by_position(1)
+ put :update, :id => 1, :tracker => { :move_to => 'lower' }
+ assert_equal 2, tracker.reload.position
+ end
+
+ def test_destroy
+ tracker = Tracker.create!(:name => 'Destroyable')
+ assert_difference 'Tracker.count', -1 do
+ delete :destroy, :id => tracker.id
+ end
+ assert_redirected_to :action => 'index'
+ assert_nil flash[:error]
+ end
+
+ def test_destroy_tracker_in_use
+ assert_no_difference 'Tracker.count' do
+ delete :destroy, :id => 1
+ end
+ assert_redirected_to :action => 'index'
+ assert_not_nil flash[:error]
+ end
+
+ def test_get_fields
+ get :fields
+ assert_response :success
+ assert_template 'fields'
+
+ assert_select 'form' do
+ assert_select 'input[type=checkbox][name=?][value=assigned_to_id]', 'trackers[1][core_fields][]'
+ assert_select 'input[type=checkbox][name=?][value=2]', 'trackers[1][custom_field_ids][]'
+
+ assert_select 'input[type=hidden][name=?][value=]', 'trackers[1][core_fields][]'
+ assert_select 'input[type=hidden][name=?][value=]', 'trackers[1][custom_field_ids][]'
+ end
+ end
+
+ def test_post_fields
+ post :fields, :trackers => {
+ '1' => {'core_fields' => ['assigned_to_id', 'due_date', ''], 'custom_field_ids' => ['1', '2']},
+ '2' => {'core_fields' => [''], 'custom_field_ids' => ['']}
+ }
+ assert_redirected_to '/trackers/fields'
+
+ tracker = Tracker.find(1)
+ assert_equal %w(assigned_to_id due_date), tracker.core_fields
+ assert_equal [1, 2], tracker.custom_field_ids.sort
+
+ tracker = Tracker.find(2)
+ assert_equal [], tracker.core_fields
+ assert_equal [], tracker.custom_field_ids.sort
+ end
+end
diff --git a/test/functional/versions_controller_test.rb b/test/functional/versions_controller_test.rb
new file mode 100644
index 000000000..6e8034f31
--- /dev/null
+++ b/test/functional/versions_controller_test.rb
@@ -0,0 +1,232 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class VersionsControllerTest < ActionController::TestCase
+ fixtures :projects, :versions, :issues, :users, :roles, :members, :member_roles, :enabled_modules, :issue_statuses, :issue_categories
+
+ def setup
+ User.current = nil
+ end
+
+ def test_index
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:versions)
+ # Version with no date set appears
+ assert assigns(:versions).include?(Version.find(3))
+ # Completed version doesn't appear
+ assert !assigns(:versions).include?(Version.find(1))
+ # Context menu on issues
+ assert_select "script", :text => Regexp.new(Regexp.escape("contextMenuInit('/issues/context_menu')"))
+ # Links to versions anchors
+ assert_tag 'a', :attributes => {:href => '#2.0'},
+ :ancestor => {:tag => 'div', :attributes => {:id => 'sidebar'}}
+ # Links to completed versions in the sidebar
+ assert_tag 'a', :attributes => {:href => '/versions/1'},
+ :ancestor => {:tag => 'div', :attributes => {:id => 'sidebar'}}
+ end
+
+ def test_index_with_completed_versions
+ get :index, :project_id => 1, :completed => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:versions)
+ # Version with no date set appears
+ assert assigns(:versions).include?(Version.find(3))
+ # Completed version appears
+ assert assigns(:versions).include?(Version.find(1))
+ end
+
+ def test_index_with_tracker_ids
+ get :index, :project_id => 1, :tracker_ids => [1, 3]
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:issues_by_version)
+ assert_nil assigns(:issues_by_version).values.flatten.detect {|issue| issue.tracker_id == 2}
+ end
+
+ def test_index_showing_subprojects_versions
+ @subproject_version = Version.create!(:project => Project.find(3), :name => "Subproject version")
+ get :index, :project_id => 1, :with_subprojects => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:versions)
+
+ assert assigns(:versions).include?(Version.find(4)), "Shared version not found"
+ assert assigns(:versions).include?(@subproject_version), "Subproject version not found"
+ end
+
+ def test_index_should_prepend_shared_versions
+ get :index, :project_id => 1
+ assert_response :success
+
+ assert_select '#sidebar' do
+ assert_select 'a[href=?]', '#2.0', :text => '2.0'
+ assert_select 'a[href=?]', '#subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0'
+ end
+ assert_select '#content' do
+ assert_select 'a[name=?]', '2.0', :text => '2.0'
+ assert_select 'a[name=?]', 'subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0'
+ end
+ end
+
+ def test_show
+ get :show, :id => 2
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:version)
+
+ assert_tag :tag => 'h2', :content => /1.0/
+ end
+
+ def test_show_should_display_nil_counts
+ with_settings :default_language => 'en' do
+ get :show, :id => 2, :status_by => 'category'
+ assert_response :success
+ assert_select 'div#status_by' do
+ assert_select 'select[name=status_by]' do
+ assert_select 'option[value=category][selected=selected]'
+ end
+ assert_select 'a', :text => 'none'
+ end
+ end
+ end
+
+ def test_new
+ @request.session[:user_id] = 2
+ get :new, :project_id => '1'
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_new_from_issue_form
+ @request.session[:user_id] = 2
+ xhr :get, :new, :project_id => '1'
+ assert_response :success
+ assert_template 'new'
+ assert_equal 'text/javascript', response.content_type
+ end
+
+ def test_create
+ @request.session[:user_id] = 2 # manager
+ assert_difference 'Version.count' do
+ post :create, :project_id => '1', :version => {:name => 'test_add_version'}
+ end
+ assert_redirected_to '/projects/ecookbook/settings/versions'
+ version = Version.find_by_name('test_add_version')
+ assert_not_nil version
+ assert_equal 1, version.project_id
+ end
+
+ def test_create_from_issue_form
+ @request.session[:user_id] = 2
+ assert_difference 'Version.count' do
+ xhr :post, :create, :project_id => '1', :version => {:name => 'test_add_version_from_issue_form'}
+ end
+ version = Version.find_by_name('test_add_version_from_issue_form')
+ assert_not_nil version
+ assert_equal 1, version.project_id
+
+ assert_response :success
+ assert_template 'create'
+ assert_equal 'text/javascript', response.content_type
+ assert_include 'test_add_version_from_issue_form', response.body
+ end
+
+ def test_create_from_issue_form_with_failure
+ @request.session[:user_id] = 2
+ assert_no_difference 'Version.count' do
+ xhr :post, :create, :project_id => '1', :version => {:name => ''}
+ end
+ assert_response :success
+ assert_template 'new'
+ assert_equal 'text/javascript', response.content_type
+ end
+
+ def test_get_edit
+ @request.session[:user_id] = 2
+ get :edit, :id => 2
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_close_completed
+ Version.update_all("status = 'open'")
+ @request.session[:user_id] = 2
+ put :close_completed, :project_id => 'ecookbook'
+ assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook'
+ assert_not_nil Version.find_by_status('closed')
+ end
+
+ def test_post_update
+ @request.session[:user_id] = 2
+ put :update, :id => 2,
+ :version => { :name => 'New version name',
+ :effective_date => Date.today.strftime("%Y-%m-%d")}
+ assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook'
+ version = Version.find(2)
+ assert_equal 'New version name', version.name
+ assert_equal Date.today, version.effective_date
+ end
+
+ def test_post_update_with_validation_failure
+ @request.session[:user_id] = 2
+ put :update, :id => 2,
+ :version => { :name => '',
+ :effective_date => Date.today.strftime("%Y-%m-%d")}
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 2
+ assert_difference 'Version.count', -1 do
+ delete :destroy, :id => 3
+ end
+ assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook'
+ assert_nil Version.find_by_id(3)
+ end
+
+ def test_destroy_version_in_use_should_fail
+ @request.session[:user_id] = 2
+ assert_no_difference 'Version.count' do
+ delete :destroy, :id => 2
+ end
+ assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook'
+ assert flash[:error].match(/Unable to delete version/)
+ assert Version.find_by_id(2)
+ end
+
+ def test_issue_status_by
+ xhr :get, :status_by, :id => 2
+ assert_response :success
+ assert_template 'status_by'
+ assert_template '_issue_counts'
+ end
+
+ def test_issue_status_by_status
+ xhr :get, :status_by, :id => 2, :status_by => 'status'
+ assert_response :success
+ assert_template 'status_by'
+ assert_template '_issue_counts'
+ assert_include 'Assigned', response.body
+ assert_include 'Closed', response.body
+ end
+end
diff --git a/test/functional/watchers_controller_test.rb b/test/functional/watchers_controller_test.rb
new file mode 100644
index 000000000..bc8b58b0c
--- /dev/null
+++ b/test/functional/watchers_controller_test.rb
@@ -0,0 +1,195 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class WatchersControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
+ :issues, :trackers, :projects_trackers, :issue_statuses, :enumerations, :watchers
+
+ def setup
+ User.current = nil
+ end
+
+ def test_watch_a_single_object
+ @request.session[:user_id] = 3
+ assert_difference('Watcher.count') do
+ xhr :post, :watch, :object_type => 'issue', :object_id => '1'
+ assert_response :success
+ assert_include '$(".issue-1-watcher")', response.body
+ end
+ assert Issue.find(1).watched_by?(User.find(3))
+ end
+
+ def test_watch_a_collection_with_a_single_object
+ @request.session[:user_id] = 3
+ assert_difference('Watcher.count') do
+ xhr :post, :watch, :object_type => 'issue', :object_id => ['1']
+ assert_response :success
+ assert_include '$(".issue-1-watcher")', response.body
+ end
+ assert Issue.find(1).watched_by?(User.find(3))
+ end
+
+ def test_watch_a_collection_with_multiple_objects
+ @request.session[:user_id] = 3
+ assert_difference('Watcher.count', 2) do
+ xhr :post, :watch, :object_type => 'issue', :object_id => ['1', '3']
+ assert_response :success
+ assert_include '$(".issue-bulk-watcher")', response.body
+ end
+ assert Issue.find(1).watched_by?(User.find(3))
+ assert Issue.find(3).watched_by?(User.find(3))
+ end
+
+ def test_watch_should_be_denied_without_permission
+ Role.find(2).remove_permission! :view_issues
+ @request.session[:user_id] = 3
+ assert_no_difference('Watcher.count') do
+ xhr :post, :watch, :object_type => 'issue', :object_id => '1'
+ assert_response 403
+ end
+ end
+
+ def test_watch_invalid_class_should_respond_with_404
+ @request.session[:user_id] = 3
+ assert_no_difference('Watcher.count') do
+ xhr :post, :watch, :object_type => 'foo', :object_id => '1'
+ assert_response 404
+ end
+ end
+
+ def test_watch_invalid_object_should_respond_with_404
+ @request.session[:user_id] = 3
+ assert_no_difference('Watcher.count') do
+ xhr :post, :watch, :object_type => 'issue', :object_id => '999'
+ assert_response 404
+ end
+ end
+
+ def test_unwatch
+ @request.session[:user_id] = 3
+ assert_difference('Watcher.count', -1) do
+ xhr :delete, :unwatch, :object_type => 'issue', :object_id => '2'
+ assert_response :success
+ assert_include '$(".issue-2-watcher")', response.body
+ end
+ assert !Issue.find(1).watched_by?(User.find(3))
+ end
+
+ def test_unwatch_a_collection_with_multiple_objects
+ @request.session[:user_id] = 3
+ Watcher.create!(:user_id => 3, :watchable => Issue.find(1))
+ Watcher.create!(:user_id => 3, :watchable => Issue.find(3))
+
+ assert_difference('Watcher.count', -2) do
+ xhr :delete, :unwatch, :object_type => 'issue', :object_id => ['1', '3']
+ assert_response :success
+ assert_include '$(".issue-bulk-watcher")', response.body
+ end
+ assert !Issue.find(1).watched_by?(User.find(3))
+ assert !Issue.find(3).watched_by?(User.find(3))
+ end
+
+ def test_new
+ @request.session[:user_id] = 2
+ xhr :get, :new, :object_type => 'issue', :object_id => '2'
+ assert_response :success
+ assert_match /ajax-modal/, response.body
+ end
+
+ def test_new_for_new_record_with_project_id
+ @request.session[:user_id] = 2
+ xhr :get, :new, :project_id => 1
+ assert_response :success
+ assert_equal Project.find(1), assigns(:project)
+ assert_match /ajax-modal/, response.body
+ end
+
+ def test_new_for_new_record_with_project_identifier
+ @request.session[:user_id] = 2
+ xhr :get, :new, :project_id => 'ecookbook'
+ assert_response :success
+ assert_equal Project.find(1), assigns(:project)
+ assert_match /ajax-modal/, response.body
+ end
+
+ def test_create
+ @request.session[:user_id] = 2
+ assert_difference('Watcher.count') do
+ xhr :post, :create, :object_type => 'issue', :object_id => '2', :watcher => {:user_id => '4'}
+ assert_response :success
+ assert_match /watchers/, response.body
+ assert_match /ajax-modal/, response.body
+ end
+ assert Issue.find(2).watched_by?(User.find(4))
+ end
+
+ def test_create_multiple
+ @request.session[:user_id] = 2
+ assert_difference('Watcher.count', 2) do
+ xhr :post, :create, :object_type => 'issue', :object_id => '2', :watcher => {:user_ids => ['4', '7']}
+ assert_response :success
+ assert_match /watchers/, response.body
+ assert_match /ajax-modal/, response.body
+ end
+ assert Issue.find(2).watched_by?(User.find(4))
+ assert Issue.find(2).watched_by?(User.find(7))
+ end
+
+ def test_autocomplete_on_watchable_creation
+ @request.session[:user_id] = 2
+ xhr :get, :autocomplete_for_user, :q => 'mi', :project_id => 'ecookbook'
+ assert_response :success
+ assert_select 'input', :count => 4
+ assert_select 'input[name=?][value=1]', 'watcher[user_ids][]'
+ assert_select 'input[name=?][value=2]', 'watcher[user_ids][]'
+ assert_select 'input[name=?][value=8]', 'watcher[user_ids][]'
+ assert_select 'input[name=?][value=9]', 'watcher[user_ids][]'
+ end
+
+ def test_autocomplete_on_watchable_update
+ @request.session[:user_id] = 2
+ xhr :get, :autocomplete_for_user, :q => 'mi', :object_id => '2' , :object_type => 'issue', :project_id => 'ecookbook'
+ assert_response :success
+ assert_select 'input', :count => 3
+ assert_select 'input[name=?][value=2]', 'watcher[user_ids][]'
+ assert_select 'input[name=?][value=8]', 'watcher[user_ids][]'
+ assert_select 'input[name=?][value=9]', 'watcher[user_ids][]'
+
+ end
+
+ def test_append
+ @request.session[:user_id] = 2
+ assert_no_difference 'Watcher.count' do
+ xhr :post, :append, :watcher => {:user_ids => ['4', '7']}, :project_id => 'ecookbook'
+ assert_response :success
+ assert_include 'watchers_inputs', response.body
+ assert_include 'issue[watcher_user_ids][]', response.body
+ end
+ end
+
+ def test_remove_watcher
+ @request.session[:user_id] = 2
+ assert_difference('Watcher.count', -1) do
+ xhr :delete, :destroy, :object_type => 'issue', :object_id => '2', :user_id => '3'
+ assert_response :success
+ assert_match /watchers/, response.body
+ end
+ assert !Issue.find(2).watched_by?(User.find(3))
+ end
+end
diff --git a/test/functional/welcome_controller_test.rb b/test/functional/welcome_controller_test.rb
new file mode 100644
index 000000000..94a99c223
--- /dev/null
+++ b/test/functional/welcome_controller_test.rb
@@ -0,0 +1,165 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class WelcomeControllerTest < ActionController::TestCase
+ fixtures :projects, :news, :users, :members
+
+ def setup
+ User.current = nil
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:news)
+ assert_not_nil assigns(:projects)
+ assert !assigns(:projects).include?(Project.where(:is_public => false).first)
+ end
+
+ def test_browser_language
+ Setting.default_language = 'en'
+ @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'
+ get :index
+ assert_equal :fr, @controller.current_language
+ end
+
+ def test_browser_language_alternate
+ Setting.default_language = 'en'
+ @request.env['HTTP_ACCEPT_LANGUAGE'] = 'zh-TW'
+ get :index
+ assert_equal :"zh-TW", @controller.current_language
+ end
+
+ def test_browser_language_alternate_not_valid
+ Setting.default_language = 'en'
+ @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr-CA'
+ get :index
+ assert_equal :fr, @controller.current_language
+ end
+
+ def test_robots
+ get :robots
+ assert_response :success
+ assert_equal 'text/plain', @response.content_type
+ assert @response.body.match(%r{^Disallow: /projects/ecookbook/issues\r?$})
+ end
+
+ def test_warn_on_leaving_unsaved_turn_on
+ user = User.find(2)
+ user.pref.warn_on_leaving_unsaved = '1'
+ user.pref.save!
+ @request.session[:user_id] = 2
+
+ get :index
+ assert_tag 'script',
+ :attributes => {:type => "text/javascript"},
+ :content => %r{warnLeavingUnsaved}
+ end
+
+ def test_warn_on_leaving_unsaved_turn_off
+ user = User.find(2)
+ user.pref.warn_on_leaving_unsaved = '0'
+ user.pref.save!
+ @request.session[:user_id] = 2
+
+ get :index
+ assert_no_tag 'script',
+ :attributes => {:type => "text/javascript"},
+ :content => %r{warnLeavingUnsaved}
+ end
+
+ def test_logout_link_should_post
+ @request.session[:user_id] = 2
+
+ get :index
+ assert_select 'a[href=/logout][data-method=post]', :text => 'Sign out'
+ end
+
+ def test_call_hook_mixed_in
+ assert @controller.respond_to?(:call_hook)
+ end
+
+ def test_project_jump_box_should_escape_names_once
+ Project.find(1).update_attribute :name, 'Foo & Bar'
+ @request.session[:user_id] = 2
+
+ get :index
+ assert_select "#header select" do
+ assert_select "option", :text => 'Foo & Bar'
+ end
+ end
+
+ context "test_api_offset_and_limit" do
+ context "without params" do
+ should "return 0, 25" do
+ assert_equal [0, 25], @controller.api_offset_and_limit({})
+ end
+ end
+
+ context "with limit" do
+ should "return 0, limit" do
+ assert_equal [0, 30], @controller.api_offset_and_limit({:limit => 30})
+ end
+
+ should "not exceed 100" do
+ assert_equal [0, 100], @controller.api_offset_and_limit({:limit => 120})
+ end
+
+ should "not be negative" do
+ assert_equal [0, 25], @controller.api_offset_and_limit({:limit => -10})
+ end
+ end
+
+ context "with offset" do
+ should "return offset, 25" do
+ assert_equal [10, 25], @controller.api_offset_and_limit({:offset => 10})
+ end
+
+ should "not be negative" do
+ assert_equal [0, 25], @controller.api_offset_and_limit({:offset => -10})
+ end
+
+ context "and limit" do
+ should "return offset, limit" do
+ assert_equal [10, 50], @controller.api_offset_and_limit({:offset => 10, :limit => 50})
+ end
+ end
+ end
+
+ context "with page" do
+ should "return offset, 25" do
+ assert_equal [0, 25], @controller.api_offset_and_limit({:page => 1})
+ assert_equal [50, 25], @controller.api_offset_and_limit({:page => 3})
+ end
+
+ should "not be negative" do
+ assert_equal [0, 25], @controller.api_offset_and_limit({:page => 0})
+ assert_equal [0, 25], @controller.api_offset_and_limit({:page => -2})
+ end
+
+ context "and limit" do
+ should "return offset, limit" do
+ assert_equal [0, 100], @controller.api_offset_and_limit({:page => 1, :limit => 100})
+ assert_equal [200, 100], @controller.api_offset_and_limit({:page => 3, :limit => 100})
+ end
+ end
+ end
+ end
+end
diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb
new file mode 100644
index 000000000..737a29947
--- /dev/null
+++ b/test/functional/wiki_controller_test.rb
@@ -0,0 +1,933 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class WikiControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles,
+ :enabled_modules, :wikis, :wiki_pages, :wiki_contents,
+ :wiki_content_versions, :attachments
+
+ def setup
+ User.current = nil
+ end
+
+ def test_show_start_page
+ get :show, :project_id => 'ecookbook'
+ assert_response :success
+ assert_template 'show'
+ assert_tag :tag => 'h1', :content => /CookBook documentation/
+
+ # child_pages macro
+ assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
+ :child => { :tag => 'li',
+ :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
+ :content => 'Page with an inline image' } }
+ end
+
+ def test_export_link
+ Role.anonymous.add_permission! :export_wiki_pages
+ get :show, :project_id => 'ecookbook'
+ assert_response :success
+ assert_tag 'a', :attributes => {:href => '/projects/ecookbook/wiki/CookBook_documentation.txt'}
+ end
+
+ def test_show_page_with_name
+ get :show, :project_id => 1, :id => 'Another_page'
+ assert_response :success
+ assert_template 'show'
+ assert_tag :tag => 'h1', :content => /Another page/
+ # Included page with an inline image
+ assert_tag :tag => 'p', :content => /This is an inline image/
+ assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3/logo.gif',
+ :alt => 'This is a logo' }
+ end
+
+ def test_show_old_version
+ get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2'
+ assert_response :success
+ assert_template 'show'
+
+ assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/1', :text => /Previous/
+ assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/diff', :text => /diff/
+ assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/3', :text => /Next/
+ assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
+ end
+
+ def test_show_old_version_with_attachments
+ page = WikiPage.find(4)
+ assert page.attachments.any?
+ content = page.content
+ content.text = "update"
+ content.save!
+
+ get :show, :project_id => 'ecookbook', :id => page.title, :version => '1'
+ assert_kind_of WikiContent::Version, assigns(:content)
+ assert_response :success
+ assert_template 'show'
+ end
+
+ def test_show_old_version_without_permission_should_be_denied
+ Role.anonymous.remove_permission! :view_wiki_edits
+
+ get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2'
+ assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fprojects%2Fecookbook%2Fwiki%2FCookBook_documentation%2F2'
+ end
+
+ def test_show_first_version
+ get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '1'
+ assert_response :success
+ assert_template 'show'
+
+ assert_select 'a', :text => /Previous/, :count => 0
+ assert_select 'a', :text => /diff/, :count => 0
+ assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => /Next/
+ assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
+ end
+
+ def test_show_redirected_page
+ WikiRedirect.create!(:wiki_id => 1, :title => 'Old_title', :redirects_to => 'Another_page')
+
+ get :show, :project_id => 'ecookbook', :id => 'Old_title'
+ assert_redirected_to '/projects/ecookbook/wiki/Another_page'
+ end
+
+ def test_show_with_sidebar
+ page = Project.find(1).wiki.pages.new(:title => 'Sidebar')
+ page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar')
+ page.save!
+
+ get :show, :project_id => 1, :id => 'Another_page'
+ assert_response :success
+ assert_tag :tag => 'div', :attributes => {:id => 'sidebar'},
+ :content => /Side bar content for test_show_with_sidebar/
+ end
+
+ def test_show_should_display_section_edit_links
+ @request.session[:user_id] = 2
+ get :show, :project_id => 1, :id => 'Page with sections'
+ assert_no_tag 'a', :attributes => {
+ :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=1'
+ }
+ assert_tag 'a', :attributes => {
+ :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
+ }
+ assert_tag 'a', :attributes => {
+ :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=3'
+ }
+ end
+
+ def test_show_current_version_should_display_section_edit_links
+ @request.session[:user_id] = 2
+ get :show, :project_id => 1, :id => 'Page with sections', :version => 3
+
+ assert_tag 'a', :attributes => {
+ :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
+ }
+ end
+
+ def test_show_old_version_should_not_display_section_edit_links
+ @request.session[:user_id] = 2
+ get :show, :project_id => 1, :id => 'Page with sections', :version => 2
+
+ assert_no_tag 'a', :attributes => {
+ :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
+ }
+ end
+
+ def test_show_unexistent_page_without_edit_right
+ get :show, :project_id => 1, :id => 'Unexistent page'
+ assert_response 404
+ end
+
+ def test_show_unexistent_page_with_edit_right
+ @request.session[:user_id] = 2
+ get :show, :project_id => 1, :id => 'Unexistent page'
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_show_unexistent_page_with_parent_should_preselect_parent
+ @request.session[:user_id] = 2
+ get :show, :project_id => 1, :id => 'Unexistent page', :parent => 'Another_page'
+ assert_response :success
+ assert_template 'edit'
+ assert_tag 'select', :attributes => {:name => 'wiki_page[parent_id]'},
+ :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}}
+ end
+
+ def test_show_should_not_show_history_without_permission
+ Role.anonymous.remove_permission! :view_wiki_edits
+ get :show, :project_id => 1, :id => 'Page with sections', :version => 2
+
+ assert_response 302
+ end
+
+ def test_create_page
+ @request.session[:user_id] = 2
+ assert_difference 'WikiPage.count' do
+ assert_difference 'WikiContent.count' do
+ put :update, :project_id => 1,
+ :id => 'New page',
+ :content => {:comments => 'Created the page',
+ :text => "h1. New page\n\nThis is a new page",
+ :version => 0}
+ end
+ end
+ assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page'
+ page = Project.find(1).wiki.find_page('New page')
+ assert !page.new_record?
+ assert_not_nil page.content
+ assert_nil page.parent
+ assert_equal 'Created the page', page.content.comments
+ end
+
+ def test_create_page_with_attachments
+ @request.session[:user_id] = 2
+ assert_difference 'WikiPage.count' do
+ assert_difference 'Attachment.count' do
+ put :update, :project_id => 1,
+ :id => 'New page',
+ :content => {:comments => 'Created the page',
+ :text => "h1. New page\n\nThis is a new page",
+ :version => 0},
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
+ end
+ end
+ page = Project.find(1).wiki.find_page('New page')
+ assert_equal 1, page.attachments.count
+ assert_equal 'testfile.txt', page.attachments.first.filename
+ end
+
+ def test_create_page_with_parent
+ @request.session[:user_id] = 2
+ assert_difference 'WikiPage.count' do
+ put :update, :project_id => 1, :id => 'New page',
+ :content => {:text => "h1. New page\n\nThis is a new page", :version => 0},
+ :wiki_page => {:parent_id => 2}
+ end
+ page = Project.find(1).wiki.find_page('New page')
+ assert_equal WikiPage.find(2), page.parent
+ end
+
+ def test_edit_page
+ @request.session[:user_id] = 2
+ get :edit, :project_id => 'ecookbook', :id => 'Another_page'
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_tag 'textarea',
+ :attributes => { :name => 'content[text]' },
+ :content => "\n"+WikiPage.find_by_title('Another_page').content.text
+ end
+
+ def test_edit_section
+ @request.session[:user_id] = 2
+ get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 2
+
+ assert_response :success
+ assert_template 'edit'
+
+ page = WikiPage.find_by_title('Page_with_sections')
+ section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
+
+ assert_tag 'textarea',
+ :attributes => { :name => 'content[text]' },
+ :content => "\n"+section
+ assert_tag 'input',
+ :attributes => { :name => 'section', :type => 'hidden', :value => '2' }
+ assert_tag 'input',
+ :attributes => { :name => 'section_hash', :type => 'hidden', :value => hash }
+ end
+
+ def test_edit_invalid_section_should_respond_with_404
+ @request.session[:user_id] = 2
+ get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 10
+
+ assert_response 404
+ end
+
+ def test_update_page
+ @request.session[:user_id] = 2
+ assert_no_difference 'WikiPage.count' do
+ assert_no_difference 'WikiContent.count' do
+ assert_difference 'WikiContent::Version.count' do
+ put :update, :project_id => 1,
+ :id => 'Another_page',
+ :content => {
+ :comments => "my comments",
+ :text => "edited",
+ :version => 1
+ }
+ end
+ end
+ end
+ assert_redirected_to '/projects/ecookbook/wiki/Another_page'
+
+ page = Wiki.find(1).pages.find_by_title('Another_page')
+ assert_equal "edited", page.content.text
+ assert_equal 2, page.content.version
+ assert_equal "my comments", page.content.comments
+ end
+
+ def test_update_page_with_parent
+ @request.session[:user_id] = 2
+ assert_no_difference 'WikiPage.count' do
+ assert_no_difference 'WikiContent.count' do
+ assert_difference 'WikiContent::Version.count' do
+ put :update, :project_id => 1,
+ :id => 'Another_page',
+ :content => {
+ :comments => "my comments",
+ :text => "edited",
+ :version => 1
+ },
+ :wiki_page => {:parent_id => '1'}
+ end
+ end
+ end
+ assert_redirected_to '/projects/ecookbook/wiki/Another_page'
+
+ page = Wiki.find(1).pages.find_by_title('Another_page')
+ assert_equal "edited", page.content.text
+ assert_equal 2, page.content.version
+ assert_equal "my comments", page.content.comments
+ assert_equal WikiPage.find(1), page.parent
+ end
+
+ def test_update_page_with_failure
+ @request.session[:user_id] = 2
+ assert_no_difference 'WikiPage.count' do
+ assert_no_difference 'WikiContent.count' do
+ assert_no_difference 'WikiContent::Version.count' do
+ put :update, :project_id => 1,
+ :id => 'Another_page',
+ :content => {
+ :comments => 'a' * 300, # failure here, comment is too long
+ :text => 'edited',
+ :version => 1
+ }
+ end
+ end
+ end
+ assert_response :success
+ assert_template 'edit'
+
+ assert_error_tag :descendant => {:content => /Comment is too long/}
+ assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => "\nedited"
+ assert_tag :tag => 'input', :attributes => {:id => 'content_version', :value => '1'}
+ end
+
+ def test_update_page_with_parent_change_only_should_not_create_content_version
+ @request.session[:user_id] = 2
+ assert_no_difference 'WikiPage.count' do
+ assert_no_difference 'WikiContent.count' do
+ assert_no_difference 'WikiContent::Version.count' do
+ put :update, :project_id => 1,
+ :id => 'Another_page',
+ :content => {
+ :comments => '',
+ :text => Wiki.find(1).find_page('Another_page').content.text,
+ :version => 1
+ },
+ :wiki_page => {:parent_id => '1'}
+ end
+ end
+ end
+ page = Wiki.find(1).pages.find_by_title('Another_page')
+ assert_equal 1, page.content.version
+ assert_equal WikiPage.find(1), page.parent
+ end
+
+ def test_update_page_with_attachments_only_should_not_create_content_version
+ @request.session[:user_id] = 2
+ assert_no_difference 'WikiPage.count' do
+ assert_no_difference 'WikiContent.count' do
+ assert_no_difference 'WikiContent::Version.count' do
+ assert_difference 'Attachment.count' do
+ put :update, :project_id => 1,
+ :id => 'Another_page',
+ :content => {
+ :comments => '',
+ :text => Wiki.find(1).find_page('Another_page').content.text,
+ :version => 1
+ },
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
+ end
+ end
+ end
+ end
+ page = Wiki.find(1).pages.find_by_title('Another_page')
+ assert_equal 1, page.content.version
+ end
+
+ def test_update_stale_page_should_not_raise_an_error
+ @request.session[:user_id] = 2
+ c = Wiki.find(1).find_page('Another_page').content
+ c.text = 'Previous text'
+ c.save!
+ assert_equal 2, c.version
+
+ assert_no_difference 'WikiPage.count' do
+ assert_no_difference 'WikiContent.count' do
+ assert_no_difference 'WikiContent::Version.count' do
+ put :update, :project_id => 1,
+ :id => 'Another_page',
+ :content => {
+ :comments => 'My comments',
+ :text => 'Text should not be lost',
+ :version => 1
+ }
+ end
+ end
+ end
+ assert_response :success
+ assert_template 'edit'
+ assert_tag :div,
+ :attributes => { :class => /error/ },
+ :content => /Data has been updated by another user/
+ assert_tag 'textarea',
+ :attributes => { :name => 'content[text]' },
+ :content => /Text should not be lost/
+ assert_tag 'input',
+ :attributes => { :name => 'content[comments]', :value => 'My comments' }
+
+ c.reload
+ assert_equal 'Previous text', c.text
+ assert_equal 2, c.version
+ end
+
+ def test_update_section
+ @request.session[:user_id] = 2
+ page = WikiPage.find_by_title('Page_with_sections')
+ section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
+ text = page.content.text
+
+ assert_no_difference 'WikiPage.count' do
+ assert_no_difference 'WikiContent.count' do
+ assert_difference 'WikiContent::Version.count' do
+ put :update, :project_id => 1, :id => 'Page_with_sections',
+ :content => {
+ :text => "New section content",
+ :version => 3
+ },
+ :section => 2,
+ :section_hash => hash
+ end
+ end
+ end
+ assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
+ assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.reload.content.text
+ end
+
+ def test_update_section_should_allow_stale_page_update
+ @request.session[:user_id] = 2
+ page = WikiPage.find_by_title('Page_with_sections')
+ section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
+ text = page.content.text
+
+ assert_no_difference 'WikiPage.count' do
+ assert_no_difference 'WikiContent.count' do
+ assert_difference 'WikiContent::Version.count' do
+ put :update, :project_id => 1, :id => 'Page_with_sections',
+ :content => {
+ :text => "New section content",
+ :version => 2 # Current version is 3
+ },
+ :section => 2,
+ :section_hash => hash
+ end
+ end
+ end
+ assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
+ page.reload
+ assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.content.text
+ assert_equal 4, page.content.version
+ end
+
+ def test_update_section_should_not_allow_stale_section_update
+ @request.session[:user_id] = 2
+
+ assert_no_difference 'WikiPage.count' do
+ assert_no_difference 'WikiContent.count' do
+ assert_no_difference 'WikiContent::Version.count' do
+ put :update, :project_id => 1, :id => 'Page_with_sections',
+ :content => {
+ :comments => 'My comments',
+ :text => "Text should not be lost",
+ :version => 3
+ },
+ :section => 2,
+ :section_hash => Digest::MD5.hexdigest("wrong hash")
+ end
+ end
+ end
+ assert_response :success
+ assert_template 'edit'
+ assert_tag :div,
+ :attributes => { :class => /error/ },
+ :content => /Data has been updated by another user/
+ assert_tag 'textarea',
+ :attributes => { :name => 'content[text]' },
+ :content => /Text should not be lost/
+ assert_tag 'input',
+ :attributes => { :name => 'content[comments]', :value => 'My comments' }
+ end
+
+ def test_preview
+ @request.session[:user_id] = 2
+ xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation',
+ :content => { :comments => '',
+ :text => 'this is a *previewed text*',
+ :version => 3 }
+ assert_response :success
+ assert_template 'common/_preview'
+ assert_tag :tag => 'strong', :content => /previewed text/
+ end
+
+ def test_preview_new_page
+ @request.session[:user_id] = 2
+ xhr :post, :preview, :project_id => 1, :id => 'New page',
+ :content => { :text => 'h1. New page',
+ :comments => '',
+ :version => 0 }
+ assert_response :success
+ assert_template 'common/_preview'
+ assert_tag :tag => 'h1', :content => /New page/
+ end
+
+ def test_history
+ @request.session[:user_id] = 2
+ get :history, :project_id => 'ecookbook', :id => 'CookBook_documentation'
+ assert_response :success
+ assert_template 'history'
+ assert_not_nil assigns(:versions)
+ assert_equal 3, assigns(:versions).size
+
+ assert_select "input[type=submit][name=commit]"
+ assert_select 'td' do
+ assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => '2'
+ assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/annotate', :text => 'Annotate'
+ assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => 'Delete'
+ end
+ end
+
+ def test_history_with_one_version
+ @request.session[:user_id] = 2
+ get :history, :project_id => 'ecookbook', :id => 'Another_page'
+ assert_response :success
+ assert_template 'history'
+ assert_not_nil assigns(:versions)
+ assert_equal 1, assigns(:versions).size
+ assert_select "input[type=submit][name=commit]", false
+ assert_select 'td' do
+ assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => '1'
+ assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1/annotate', :text => 'Annotate'
+ assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => 'Delete', :count => 0
+ end
+ end
+
+ def test_diff
+ content = WikiPage.find(1).content
+ assert_difference 'WikiContent::Version.count', 2 do
+ content.text = "Line removed\nThis is a sample text for testing diffs"
+ content.save!
+ content.text = "This is a sample text for testing diffs\nLine added"
+ content.save!
+ end
+
+ get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => content.version, :version_from => (content.version - 1)
+ assert_response :success
+ assert_template 'diff'
+ assert_select 'span.diff_out', :text => 'Line removed'
+ assert_select 'span.diff_in', :text => 'Line added'
+ end
+
+ def test_diff_with_invalid_version_should_respond_with_404
+ get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99'
+ assert_response 404
+ end
+
+ def test_diff_with_invalid_version_from_should_respond_with_404
+ get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99', :version_from => '98'
+ assert_response 404
+ end
+
+ def test_annotate
+ get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => 2
+ assert_response :success
+ assert_template 'annotate'
+
+ # Line 1
+ assert_tag :tag => 'tr', :child => {
+ :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1', :sibling => {
+ :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/, :sibling => {
+ :tag => 'td', :content => /h1\. CookBook documentation/
+ }
+ }
+ }
+
+ # Line 5
+ assert_tag :tag => 'tr', :child => {
+ :tag => 'th', :attributes => {:class => 'line-num'}, :content => '5', :sibling => {
+ :tag => 'td', :attributes => {:class => 'author'}, :content => /Redmine Admin/, :sibling => {
+ :tag => 'td', :content => /Some updated \[\[documentation\]\] here/
+ }
+ }
+ }
+ end
+
+ def test_annotate_with_invalid_version_should_respond_with_404
+ get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => '99'
+ assert_response 404
+ end
+
+ def test_get_rename
+ @request.session[:user_id] = 2
+ get :rename, :project_id => 1, :id => 'Another_page'
+ assert_response :success
+ assert_template 'rename'
+ assert_tag 'option',
+ :attributes => {:value => ''},
+ :content => '',
+ :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
+ assert_no_tag 'option',
+ :attributes => {:selected => 'selected'},
+ :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
+ end
+
+ def test_get_rename_child_page
+ @request.session[:user_id] = 2
+ get :rename, :project_id => 1, :id => 'Child_1'
+ assert_response :success
+ assert_template 'rename'
+ assert_tag 'option',
+ :attributes => {:value => ''},
+ :content => '',
+ :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
+ assert_tag 'option',
+ :attributes => {:value => '2', :selected => 'selected'},
+ :content => /Another page/,
+ :parent => {
+ :tag => 'select',
+ :attributes => {:name => 'wiki_page[parent_id]'}
+ }
+ end
+
+ def test_rename_with_redirect
+ @request.session[:user_id] = 2
+ post :rename, :project_id => 1, :id => 'Another_page',
+ :wiki_page => { :title => 'Another renamed page',
+ :redirect_existing_links => 1 }
+ assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
+ wiki = Project.find(1).wiki
+ # Check redirects
+ assert_not_nil wiki.find_page('Another page')
+ assert_nil wiki.find_page('Another page', :with_redirect => false)
+ end
+
+ def test_rename_without_redirect
+ @request.session[:user_id] = 2
+ post :rename, :project_id => 1, :id => 'Another_page',
+ :wiki_page => { :title => 'Another renamed page',
+ :redirect_existing_links => "0" }
+ assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
+ wiki = Project.find(1).wiki
+ # Check that there's no redirects
+ assert_nil wiki.find_page('Another page')
+ end
+
+ def test_rename_with_parent_assignment
+ @request.session[:user_id] = 2
+ post :rename, :project_id => 1, :id => 'Another_page',
+ :wiki_page => { :title => 'Another page', :redirect_existing_links => "0", :parent_id => '4' }
+ assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
+ assert_equal WikiPage.find(4), WikiPage.find_by_title('Another_page').parent
+ end
+
+ def test_rename_with_parent_unassignment
+ @request.session[:user_id] = 2
+ post :rename, :project_id => 1, :id => 'Child_1',
+ :wiki_page => { :title => 'Child 1', :redirect_existing_links => "0", :parent_id => '' }
+ assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Child_1'
+ assert_nil WikiPage.find_by_title('Child_1').parent
+ end
+
+ def test_destroy_a_page_without_children_should_not_ask_confirmation
+ @request.session[:user_id] = 2
+ delete :destroy, :project_id => 1, :id => 'Child_2'
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ end
+
+ def test_destroy_parent_should_ask_confirmation
+ @request.session[:user_id] = 2
+ assert_no_difference('WikiPage.count') do
+ delete :destroy, :project_id => 1, :id => 'Another_page'
+ end
+ assert_response :success
+ assert_template 'destroy'
+ assert_select 'form' do
+ assert_select 'input[name=todo][value=nullify]'
+ assert_select 'input[name=todo][value=destroy]'
+ assert_select 'input[name=todo][value=reassign]'
+ end
+ end
+
+ def test_destroy_parent_with_nullify_should_delete_parent_only
+ @request.session[:user_id] = 2
+ assert_difference('WikiPage.count', -1) do
+ delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'nullify'
+ end
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert_nil WikiPage.find_by_id(2)
+ end
+
+ def test_destroy_parent_with_cascade_should_delete_descendants
+ @request.session[:user_id] = 2
+ assert_difference('WikiPage.count', -4) do
+ delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'destroy'
+ end
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert_nil WikiPage.find_by_id(2)
+ assert_nil WikiPage.find_by_id(5)
+ end
+
+ def test_destroy_parent_with_reassign
+ @request.session[:user_id] = 2
+ assert_difference('WikiPage.count', -1) do
+ delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'reassign', :reassign_to_id => 1
+ end
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert_nil WikiPage.find_by_id(2)
+ assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent
+ end
+
+ def test_destroy_version
+ @request.session[:user_id] = 2
+ assert_difference 'WikiContent::Version.count', -1 do
+ assert_no_difference 'WikiContent.count' do
+ assert_no_difference 'WikiPage.count' do
+ delete :destroy_version, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => 2
+ assert_redirected_to '/projects/ecookbook/wiki/CookBook_documentation/history'
+ end
+ end
+ end
+ end
+
+ def test_index
+ get :index, :project_id => 'ecookbook'
+ assert_response :success
+ assert_template 'index'
+ pages = assigns(:pages)
+ assert_not_nil pages
+ assert_equal Project.find(1).wiki.pages.size, pages.size
+ assert_equal pages.first.content.updated_on, pages.first.updated_on
+
+ assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
+ :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' },
+ :content => 'CookBook documentation' },
+ :child => { :tag => 'ul',
+ :child => { :tag => 'li',
+ :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
+ :content => 'Page with an inline image' } } } },
+ :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' },
+ :content => 'Another page' } }
+ end
+
+ def test_index_should_include_atom_link
+ get :index, :project_id => 'ecookbook'
+ assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
+ end
+
+ def test_export_to_html
+ @request.session[:user_id] = 2
+ get :export, :project_id => 'ecookbook'
+
+ assert_response :success
+ assert_not_nil assigns(:pages)
+ assert assigns(:pages).any?
+ assert_equal "text/html", @response.content_type
+
+ assert_select "a[name=?]", "CookBook_documentation"
+ assert_select "a[name=?]", "Another_page"
+ assert_select "a[name=?]", "Page_with_an_inline_image"
+ end
+
+ def test_export_to_pdf
+ @request.session[:user_id] = 2
+ get :export, :project_id => 'ecookbook', :format => 'pdf'
+
+ assert_response :success
+ assert_not_nil assigns(:pages)
+ assert assigns(:pages).any?
+ assert_equal 'application/pdf', @response.content_type
+ assert_equal 'attachment; filename="ecookbook.pdf"', @response.headers['Content-Disposition']
+ assert @response.body.starts_with?('%PDF')
+ end
+
+ def test_export_without_permission_should_be_denied
+ @request.session[:user_id] = 2
+ Role.find_by_name('Manager').remove_permission! :export_wiki_pages
+ get :export, :project_id => 'ecookbook'
+
+ assert_response 403
+ end
+
+ def test_date_index
+ get :date_index, :project_id => 'ecookbook'
+
+ assert_response :success
+ assert_template 'date_index'
+ assert_not_nil assigns(:pages)
+ assert_not_nil assigns(:pages_by_date)
+
+ assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
+ end
+
+ def test_not_found
+ get :show, :project_id => 999
+ assert_response 404
+ end
+
+ def test_protect_page
+ page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
+ assert !page.protected?
+ @request.session[:user_id] = 2
+ post :protect, :project_id => 1, :id => page.title, :protected => '1'
+ assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
+ assert page.reload.protected?
+ end
+
+ def test_unprotect_page
+ page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
+ assert page.protected?
+ @request.session[:user_id] = 2
+ post :protect, :project_id => 1, :id => page.title, :protected => '0'
+ assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'CookBook_documentation'
+ assert !page.reload.protected?
+ end
+
+ def test_show_page_with_edit_link
+ @request.session[:user_id] = 2
+ get :show, :project_id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
+ end
+
+ def test_show_page_without_edit_link
+ @request.session[:user_id] = 4
+ get :show, :project_id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
+ end
+
+ def test_show_pdf
+ @request.session[:user_id] = 2
+ get :show, :project_id => 1, :format => 'pdf'
+ assert_response :success
+ assert_not_nil assigns(:page)
+ assert_equal 'application/pdf', @response.content_type
+ assert_equal 'attachment; filename="CookBook_documentation.pdf"',
+ @response.headers['Content-Disposition']
+ end
+
+ def test_show_html
+ @request.session[:user_id] = 2
+ get :show, :project_id => 1, :format => 'html'
+ assert_response :success
+ assert_not_nil assigns(:page)
+ assert_equal 'text/html', @response.content_type
+ assert_equal 'attachment; filename="CookBook_documentation.html"',
+ @response.headers['Content-Disposition']
+ assert_tag 'h1', :content => 'CookBook documentation'
+ end
+
+ def test_show_versioned_html
+ @request.session[:user_id] = 2
+ get :show, :project_id => 1, :format => 'html', :version => 2
+ assert_response :success
+ assert_not_nil assigns(:content)
+ assert_equal 2, assigns(:content).version
+ assert_equal 'text/html', @response.content_type
+ assert_equal 'attachment; filename="CookBook_documentation.html"',
+ @response.headers['Content-Disposition']
+ assert_tag 'h1', :content => 'CookBook documentation'
+ end
+
+ def test_show_txt
+ @request.session[:user_id] = 2
+ get :show, :project_id => 1, :format => 'txt'
+ assert_response :success
+ assert_not_nil assigns(:page)
+ assert_equal 'text/plain', @response.content_type
+ assert_equal 'attachment; filename="CookBook_documentation.txt"',
+ @response.headers['Content-Disposition']
+ assert_include 'h1. CookBook documentation', @response.body
+ end
+
+ def test_show_versioned_txt
+ @request.session[:user_id] = 2
+ get :show, :project_id => 1, :format => 'txt', :version => 2
+ assert_response :success
+ assert_not_nil assigns(:content)
+ assert_equal 2, assigns(:content).version
+ assert_equal 'text/plain', @response.content_type
+ assert_equal 'attachment; filename="CookBook_documentation.txt"',
+ @response.headers['Content-Disposition']
+ assert_include 'h1. CookBook documentation', @response.body
+ end
+
+ def test_edit_unprotected_page
+ # Non members can edit unprotected wiki pages
+ @request.session[:user_id] = 4
+ get :edit, :project_id => 1, :id => 'Another_page'
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_edit_protected_page_by_nonmember
+ # Non members can't edit protected wiki pages
+ @request.session[:user_id] = 4
+ get :edit, :project_id => 1, :id => 'CookBook_documentation'
+ assert_response 403
+ end
+
+ def test_edit_protected_page_by_member
+ @request.session[:user_id] = 2
+ get :edit, :project_id => 1, :id => 'CookBook_documentation'
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_history_of_non_existing_page_should_return_404
+ get :history, :project_id => 1, :id => 'Unknown_page'
+ assert_response 404
+ end
+
+ def test_add_attachment
+ @request.session[:user_id] = 2
+ assert_difference 'Attachment.count' do
+ post :add_attachment, :project_id => 1, :id => 'CookBook_documentation',
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
+ end
+ attachment = Attachment.first(:order => 'id DESC')
+ assert_equal Wiki.find(1).find_page('CookBook_documentation'), attachment.container
+ end
+end
diff --git a/test/functional/wikis_controller_test.rb b/test/functional/wikis_controller_test.rb
new file mode 100644
index 000000000..586bce664
--- /dev/null
+++ b/test/functional/wikis_controller_test.rb
@@ -0,0 +1,83 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class WikisControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis
+
+ def setup
+ User.current = nil
+ end
+
+ def test_create
+ @request.session[:user_id] = 1
+ assert_nil Project.find(3).wiki
+
+ assert_difference 'Wiki.count' do
+ xhr :post, :edit, :id => 3, :wiki => { :start_page => 'Start page' }
+ assert_response :success
+ assert_template 'edit'
+ assert_equal 'text/javascript', response.content_type
+ end
+
+ wiki = Project.find(3).wiki
+ assert_not_nil wiki
+ assert_equal 'Start page', wiki.start_page
+ end
+
+ def test_create_with_failure
+ @request.session[:user_id] = 1
+
+ assert_no_difference 'Wiki.count' do
+ xhr :post, :edit, :id => 3, :wiki => { :start_page => '' }
+ assert_response :success
+ assert_template 'edit'
+ assert_equal 'text/javascript', response.content_type
+ end
+
+ assert_include 'errorExplanation', response.body
+ assert_include 'Start page can't be blank', response.body
+ end
+
+ def test_update
+ @request.session[:user_id] = 1
+
+ assert_no_difference 'Wiki.count' do
+ xhr :post, :edit, :id => 1, :wiki => { :start_page => 'Other start page' }
+ assert_response :success
+ assert_template 'edit'
+ assert_equal 'text/javascript', response.content_type
+ end
+
+ wiki = Project.find(1).wiki
+ assert_equal 'Other start page', wiki.start_page
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 1
+ post :destroy, :id => 1, :confirm => 1
+ assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'ecookbook', :tab => 'wiki'
+ assert_nil Project.find(1).wiki
+ end
+
+ def test_not_found
+ @request.session[:user_id] = 1
+ post :destroy, :id => 999, :confirm => 1
+ assert_response 404
+ end
+end
diff --git a/test/functional/workflows_controller_test.rb b/test/functional/workflows_controller_test.rb
new file mode 100644
index 000000000..d04c3ec0a
--- /dev/null
+++ b/test/functional/workflows_controller_test.rb
@@ -0,0 +1,328 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class WorkflowsControllerTest < ActionController::TestCase
+ fixtures :roles, :trackers, :workflows, :users, :issue_statuses
+
+ def setup
+ User.current = nil
+ @request.session[:user_id] = 1 # admin
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'index'
+
+ count = WorkflowTransition.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2')
+ assert_tag :tag => 'a', :content => count.to_s,
+ :attributes => { :href => '/workflows/edit?role_id=1&tracker_id=2' }
+ end
+
+ def test_get_edit
+ get :edit
+ assert_response :success
+ assert_template 'edit'
+ assert_not_nil assigns(:roles)
+ assert_not_nil assigns(:trackers)
+ end
+
+ def test_get_edit_with_role_and_tracker
+ WorkflowTransition.delete_all
+ WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
+ WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
+
+ get :edit, :role_id => 2, :tracker_id => 1
+ assert_response :success
+ assert_template 'edit'
+
+ # used status only
+ assert_not_nil assigns(:statuses)
+ assert_equal [2, 3, 5], assigns(:statuses).collect(&:id)
+
+ # allowed transitions
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'issue_status[3][5][]',
+ :value => 'always',
+ :checked => 'checked' }
+ # not allowed
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'issue_status[3][2][]',
+ :value => 'always',
+ :checked => nil }
+ # unused
+ assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'issue_status[1][1][]' }
+ end
+
+ def test_get_edit_with_role_and_tracker_and_all_statuses
+ WorkflowTransition.delete_all
+
+ get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0'
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:statuses)
+ assert_equal IssueStatus.count, assigns(:statuses).size
+
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'issue_status[1][1][]',
+ :value => 'always',
+ :checked => nil }
+ end
+
+ def test_post_edit
+ post :edit, :role_id => 2, :tracker_id => 1,
+ :issue_status => {
+ '4' => {'5' => ['always']},
+ '3' => {'1' => ['always'], '2' => ['always']}
+ }
+ assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
+
+ assert_equal 3, WorkflowTransition.where(:tracker_id => 1, :role_id => 2).count
+ assert_not_nil WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2).first
+ assert_nil WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4).first
+ end
+
+ def test_post_edit_with_additional_transitions
+ post :edit, :role_id => 2, :tracker_id => 1,
+ :issue_status => {
+ '4' => {'5' => ['always']},
+ '3' => {'1' => ['author'], '2' => ['assignee'], '4' => ['author', 'assignee']}
+ }
+ assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
+
+ assert_equal 4, WorkflowTransition.where(:tracker_id => 1, :role_id => 2).count
+
+ w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5).first
+ assert ! w.author
+ assert ! w.assignee
+ w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1).first
+ assert w.author
+ assert ! w.assignee
+ w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2).first
+ assert ! w.author
+ assert w.assignee
+ w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4).first
+ assert w.author
+ assert w.assignee
+ end
+
+ def test_clear_workflow
+ assert WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0
+
+ post :edit, :role_id => 2, :tracker_id => 1
+ assert_equal 0, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2})
+ end
+
+ def test_get_permissions
+ get :permissions
+
+ assert_response :success
+ assert_template 'permissions'
+ assert_not_nil assigns(:roles)
+ assert_not_nil assigns(:trackers)
+ end
+
+ def test_get_permissions_with_role_and_tracker
+ WorkflowPermission.delete_all
+ WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required')
+ WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
+ WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly')
+
+ get :permissions, :role_id => 1, :tracker_id => 2
+ assert_response :success
+ assert_template 'permissions'
+
+ assert_select 'input[name=role_id][value=1]'
+ assert_select 'input[name=tracker_id][value=2]'
+
+ # Required field
+ assert_select 'select[name=?]', 'permissions[assigned_to_id][2]' do
+ assert_select 'option[value=]'
+ assert_select 'option[value=][selected=selected]', 0
+ assert_select 'option[value=readonly]', :text => 'Read-only'
+ assert_select 'option[value=readonly][selected=selected]', 0
+ assert_select 'option[value=required]', :text => 'Required'
+ assert_select 'option[value=required][selected=selected]'
+ end
+
+ # Read-only field
+ assert_select 'select[name=?]', 'permissions[fixed_version_id][3]' do
+ assert_select 'option[value=]'
+ assert_select 'option[value=][selected=selected]', 0
+ assert_select 'option[value=readonly]', :text => 'Read-only'
+ assert_select 'option[value=readonly][selected=selected]'
+ assert_select 'option[value=required]', :text => 'Required'
+ assert_select 'option[value=required][selected=selected]', 0
+ end
+
+ # Other field
+ assert_select 'select[name=?]', 'permissions[due_date][3]' do
+ assert_select 'option[value=]'
+ assert_select 'option[value=][selected=selected]', 0
+ assert_select 'option[value=readonly]', :text => 'Read-only'
+ assert_select 'option[value=readonly][selected=selected]', 0
+ assert_select 'option[value=required]', :text => 'Required'
+ assert_select 'option[value=required][selected=selected]', 0
+ end
+ end
+
+ def test_get_permissions_with_required_custom_field_should_not_show_required_option
+ cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :tracker_ids => [1], :is_required => true)
+
+ get :permissions, :role_id => 1, :tracker_id => 1
+ assert_response :success
+ assert_template 'permissions'
+
+ # Custom field that is always required
+ # The default option is "(Required)"
+ assert_select 'select[name=?]', "permissions[#{cf.id}][3]" do
+ assert_select 'option[value=]'
+ assert_select 'option[value=readonly]', :text => 'Read-only'
+ assert_select 'option[value=required]', 0
+ end
+ end
+
+ def test_get_permissions_with_role_and_tracker_and_all_statuses
+ WorkflowTransition.delete_all
+
+ get :permissions, :role_id => 1, :tracker_id => 2, :used_statuses_only => '0'
+ assert_response :success
+ assert_equal IssueStatus.sorted.all, assigns(:statuses)
+ end
+
+ def test_post_permissions
+ WorkflowPermission.delete_all
+
+ post :permissions, :role_id => 1, :tracker_id => 2, :permissions => {
+ 'assigned_to_id' => {'1' => '', '2' => 'readonly', '3' => ''},
+ 'fixed_version_id' => {'1' => 'required', '2' => 'readonly', '3' => ''},
+ 'due_date' => {'1' => '', '2' => '', '3' => ''},
+ }
+ assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2'
+
+ workflows = WorkflowPermission.all
+ assert_equal 3, workflows.size
+ workflows.each do |workflow|
+ assert_equal 1, workflow.role_id
+ assert_equal 2, workflow.tracker_id
+ end
+ assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'assigned_to_id' && wf.rule == 'readonly'}
+ assert workflows.detect {|wf| wf.old_status_id == 1 && wf.field_name == 'fixed_version_id' && wf.rule == 'required'}
+ assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'fixed_version_id' && wf.rule == 'readonly'}
+ end
+
+ def test_post_permissions_should_clear_permissions
+ WorkflowPermission.delete_all
+ WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required')
+ WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
+ wf1 = WorkflowPermission.create!(:role_id => 1, :tracker_id => 3, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
+ wf2 = WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly')
+
+ post :permissions, :role_id => 1, :tracker_id => 2
+ assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2'
+
+ workflows = WorkflowPermission.all
+ assert_equal 2, workflows.size
+ assert wf1.reload
+ assert wf2.reload
+ end
+
+ def test_get_copy
+ get :copy
+ assert_response :success
+ assert_template 'copy'
+ assert_select 'select[name=source_tracker_id]' do
+ assert_select 'option[value=1]', :text => 'Bug'
+ end
+ assert_select 'select[name=source_role_id]' do
+ assert_select 'option[value=2]', :text => 'Developer'
+ end
+ assert_select 'select[name=?]', 'target_tracker_ids[]' do
+ assert_select 'option[value=3]', :text => 'Support request'
+ end
+ assert_select 'select[name=?]', 'target_role_ids[]' do
+ assert_select 'option[value=1]', :text => 'Manager'
+ end
+ end
+
+ def test_post_copy_one_to_one
+ source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
+
+ post :copy, :source_tracker_id => '1', :source_role_id => '2',
+ :target_tracker_ids => ['3'], :target_role_ids => ['1']
+ assert_response 302
+ assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
+ end
+
+ def test_post_copy_one_to_many
+ source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
+
+ post :copy, :source_tracker_id => '1', :source_role_id => '2',
+ :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
+ assert_response 302
+ assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1)
+ assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
+ assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3)
+ assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3)
+ end
+
+ def test_post_copy_many_to_many
+ source_t2 = status_transitions(:tracker_id => 2, :role_id => 2)
+ source_t3 = status_transitions(:tracker_id => 3, :role_id => 2)
+
+ post :copy, :source_tracker_id => 'any', :source_role_id => '2',
+ :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
+ assert_response 302
+ assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1)
+ assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1)
+ assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3)
+ assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3)
+ end
+
+ def test_post_copy_with_incomplete_source_specification_should_fail
+ assert_no_difference 'WorkflowRule.count' do
+ post :copy,
+ :source_tracker_id => '', :source_role_id => '2',
+ :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
+ assert_response 200
+ assert_select 'div.flash.error', :text => 'Please select a source tracker or role'
+ end
+ end
+
+ def test_post_copy_with_incomplete_target_specification_should_fail
+ assert_no_difference 'WorkflowRule.count' do
+ post :copy,
+ :source_tracker_id => '1', :source_role_id => '2',
+ :target_tracker_ids => ['2', '3']
+ assert_response 200
+ assert_select 'div.flash.error', :text => 'Please select target tracker(s) and role(s)'
+ end
+ end
+
+ # Returns an array of status transitions that can be compared
+ def status_transitions(conditions)
+ WorkflowTransition.
+ where(conditions).
+ order('tracker_id, role_id, old_status_id, new_status_id').
+ all.
+ collect {|w| [w.old_status, w.new_status_id]}
+ end
+end
diff --git a/test/integration/account_test.rb b/test/integration/account_test.rb
new file mode 100644
index 000000000..de78ba7d3
--- /dev/null
+++ b/test/integration/account_test.rb
@@ -0,0 +1,212 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+begin
+ require 'mocha'
+rescue
+ # Won't run some tests
+end
+
+class AccountTest < ActionController::IntegrationTest
+ fixtures :users, :roles
+
+ # Replace this with your real tests.
+ def test_login
+ get "my/page"
+ assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fmy%2Fpage"
+ log_user('jsmith', 'jsmith')
+
+ get "my/account"
+ assert_response :success
+ assert_template "my/account"
+ end
+
+ def test_autologin
+ user = User.find(1)
+ Setting.autologin = "7"
+ Token.delete_all
+
+ # User logs in with 'autologin' checked
+ post '/login', :username => user.login, :password => 'admin', :autologin => 1
+ assert_redirected_to '/my/page'
+ token = Token.first
+ assert_not_nil token
+ assert_equal user, token.user
+ assert_equal 'autologin', token.action
+ assert_equal user.id, session[:user_id]
+ assert_equal token.value, cookies['autologin']
+
+ # Session is cleared
+ reset!
+ User.current = nil
+ # Clears user's last login timestamp
+ user.update_attribute :last_login_on, nil
+ assert_nil user.reload.last_login_on
+
+ # User comes back with his autologin cookie
+ cookies[:autologin] = token.value
+ get '/my/page'
+ assert_response :success
+ assert_template 'my/page'
+ assert_equal user.id, session[:user_id]
+ assert_not_nil user.reload.last_login_on
+ end
+
+ def test_autologin_should_use_autologin_cookie_name
+ Token.delete_all
+ Redmine::Configuration.stubs(:[]).with('autologin_cookie_name').returns('custom_autologin')
+ Redmine::Configuration.stubs(:[]).with('autologin_cookie_path').returns('/')
+ Redmine::Configuration.stubs(:[]).with('autologin_cookie_secure').returns(false)
+
+ with_settings :autologin => '7' do
+ assert_difference 'Token.count' do
+ post '/login', :username => 'admin', :password => 'admin', :autologin => 1
+ end
+ assert_response 302
+ assert cookies['custom_autologin'].present?
+ token = cookies['custom_autologin']
+
+ # Session is cleared
+ reset!
+ cookies['custom_autologin'] = token
+ get '/my/page'
+ assert_response :success
+
+ assert_difference 'Token.count', -1 do
+ post '/logout'
+ end
+ assert cookies['custom_autologin'].blank?
+ end
+ end
+
+ def test_lost_password
+ Token.delete_all
+
+ get "account/lost_password"
+ assert_response :success
+ assert_template "account/lost_password"
+ assert_select 'input[name=mail]'
+
+ post "account/lost_password", :mail => 'jSmith@somenet.foo'
+ assert_redirected_to "/login"
+
+ token = Token.first
+ assert_equal 'recovery', token.action
+ assert_equal 'jsmith@somenet.foo', token.user.mail
+ assert !token.expired?
+
+ get "account/lost_password", :token => token.value
+ assert_response :success
+ assert_template "account/password_recovery"
+ assert_select 'input[type=hidden][name=token][value=?]', token.value
+ assert_select 'input[name=new_password]'
+ assert_select 'input[name=new_password_confirmation]'
+
+ post "account/lost_password", :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123'
+ assert_redirected_to "/login"
+ assert_equal 'Password was successfully updated.', flash[:notice]
+
+ log_user('jsmith', 'newpass123')
+ assert_equal 0, Token.count
+ end
+
+ def test_register_with_automatic_activation
+ Setting.self_registration = '3'
+
+ get 'account/register'
+ assert_response :success
+ assert_template 'account/register'
+
+ post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
+ :password => "newpass123", :password_confirmation => "newpass123"}
+ assert_redirected_to '/my/account'
+ follow_redirect!
+ assert_response :success
+ assert_template 'my/account'
+
+ user = User.find_by_login('newuser')
+ assert_not_nil user
+ assert user.active?
+ assert_not_nil user.last_login_on
+ end
+
+ def test_register_with_manual_activation
+ Setting.self_registration = '2'
+
+ post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
+ :password => "newpass123", :password_confirmation => "newpass123"}
+ assert_redirected_to '/login'
+ assert !User.find_by_login('newuser').active?
+ end
+
+ def test_register_with_email_activation
+ Setting.self_registration = '1'
+ Token.delete_all
+
+ post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
+ :password => "newpass123", :password_confirmation => "newpass123"}
+ assert_redirected_to '/login'
+ assert !User.find_by_login('newuser').active?
+
+ token = Token.first
+ assert_equal 'register', token.action
+ assert_equal 'newuser@foo.bar', token.user.mail
+ assert !token.expired?
+
+ get 'account/activate', :token => token.value
+ assert_redirected_to '/login'
+ log_user('newuser', 'newpass123')
+ end
+
+ def test_onthefly_registration
+ # disable registration
+ Setting.self_registration = '0'
+ AuthSource.expects(:authenticate).returns({:login => 'foo', :firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com', :auth_source_id => 66})
+
+ post '/login', :username => 'foo', :password => 'bar'
+ assert_redirected_to '/my/page'
+
+ user = User.find_by_login('foo')
+ assert user.is_a?(User)
+ assert_equal 66, user.auth_source_id
+ assert user.hashed_password.blank?
+ end
+
+ def test_onthefly_registration_with_invalid_attributes
+ # disable registration
+ Setting.self_registration = '0'
+ AuthSource.expects(:authenticate).returns({:login => 'foo', :lastname => 'Smith', :auth_source_id => 66})
+
+ post '/login', :username => 'foo', :password => 'bar'
+ assert_response :success
+ assert_template 'account/register'
+ assert_tag :input, :attributes => { :name => 'user[firstname]', :value => '' }
+ assert_tag :input, :attributes => { :name => 'user[lastname]', :value => 'Smith' }
+ assert_no_tag :input, :attributes => { :name => 'user[login]' }
+ assert_no_tag :input, :attributes => { :name => 'user[password]' }
+
+ post 'account/register', :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'}
+ assert_redirected_to '/my/account'
+
+ user = User.find_by_login('foo')
+ assert user.is_a?(User)
+ assert_equal 66, user.auth_source_id
+ assert user.hashed_password.blank?
+ end
+end
diff --git a/test/integration/admin_test.rb b/test/integration/admin_test.rb
new file mode 100644
index 000000000..cd37c1f67
--- /dev/null
+++ b/test/integration/admin_test.rb
@@ -0,0 +1,61 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class AdminTest < ActionController::IntegrationTest
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules
+
+ def test_add_user
+ log_user("admin", "admin")
+ get "/users/new"
+ assert_response :success
+ assert_template "users/new"
+ post "/users",
+ :user => { :login => "psmith", :firstname => "Paul",
+ :lastname => "Smith", :mail => "psmith@somenet.foo",
+ :language => "en", :password => "psmith09",
+ :password_confirmation => "psmith09" }
+
+ user = User.find_by_login("psmith")
+ assert_kind_of User, user
+ assert_redirected_to "/users/#{ user.id }/edit"
+
+ logged_user = User.try_to_login("psmith", "psmith09")
+ assert_kind_of User, logged_user
+ assert_equal "Paul", logged_user.firstname
+
+ put "users/#{user.id}", :id => user.id, :user => { :status => User::STATUS_LOCKED }
+ assert_redirected_to "/users/#{ user.id }/edit"
+ locked_user = User.try_to_login("psmith", "psmith09")
+ assert_equal nil, locked_user
+ end
+
+ test "Add a user as an anonymous user should fail" do
+ post '/users',
+ :user => { :login => 'psmith', :firstname => 'Paul'},
+ :password => "psmith09", :password_confirmation => "psmith09"
+ assert_response :redirect
+ assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fusers"
+ end
+end
diff --git a/test/integration/api_test/attachments_test.rb b/test/integration/api_test/attachments_test.rb
new file mode 100644
index 000000000..af2f063fa
--- /dev/null
+++ b/test/integration/api_test/attachments_test.rb
@@ -0,0 +1,149 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::AttachmentsTest < Redmine::ApiTest::Base
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules,
+ :attachments
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ set_fixtures_attachments_directory
+ end
+
+ def teardown
+ set_tmp_attachments_directory
+ end
+
+ test "GET /attachments/:id.xml should return the attachment" do
+ get '/attachments/7.xml', {}, credentials('jsmith')
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag :tag => 'attachment',
+ :child => {
+ :tag => 'id',
+ :content => '7',
+ :sibling => {
+ :tag => 'filename',
+ :content => 'archive.zip',
+ :sibling => {
+ :tag => 'content_url',
+ :content => 'http://www.example.com/attachments/download/7/archive.zip'
+ }
+ }
+ }
+ end
+
+ test "GET /attachments/:id.xml should deny access without credentials" do
+ get '/attachments/7.xml'
+ assert_response 401
+ set_tmp_attachments_directory
+ end
+
+ test "GET /attachments/download/:id/:filename should return the attachment content" do
+ get '/attachments/download/7/archive.zip', {}, credentials('jsmith')
+ assert_response :success
+ assert_equal 'application/octet-stream', @response.content_type
+ set_tmp_attachments_directory
+ end
+
+ test "GET /attachments/download/:id/:filename should deny access without credentials" do
+ get '/attachments/download/7/archive.zip'
+ assert_response 302
+ set_tmp_attachments_directory
+ end
+
+ test "POST /uploads.xml should return the token" do
+ set_tmp_attachments_directory
+ assert_difference 'Attachment.count' do
+ post '/uploads.xml', 'File content', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith'))
+ assert_response :created
+ assert_equal 'application/xml', response.content_type
+ end
+
+ xml = Hash.from_xml(response.body)
+ assert_kind_of Hash, xml['upload']
+ token = xml['upload']['token']
+ assert_not_nil token
+
+ attachment = Attachment.first(:order => 'id DESC')
+ assert_equal token, attachment.token
+ assert_nil attachment.container
+ assert_equal 2, attachment.author_id
+ assert_equal 'File content'.size, attachment.filesize
+ assert attachment.content_type.blank?
+ assert attachment.filename.present?
+ assert_match /\d+_[0-9a-z]+/, attachment.diskfile
+ assert File.exist?(attachment.diskfile)
+ assert_equal 'File content', File.read(attachment.diskfile)
+ end
+
+ test "POST /uploads.json should return the token" do
+ set_tmp_attachments_directory
+ assert_difference 'Attachment.count' do
+ post '/uploads.json', 'File content', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith'))
+ assert_response :created
+ assert_equal 'application/json', response.content_type
+ end
+
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_kind_of Hash, json['upload']
+ token = json['upload']['token']
+ assert_not_nil token
+
+ attachment = Attachment.first(:order => 'id DESC')
+ assert_equal token, attachment.token
+ end
+
+ test "POST /uploads.xml should accept :filename param as the attachment filename" do
+ set_tmp_attachments_directory
+ assert_difference 'Attachment.count' do
+ post '/uploads.xml?filename=test.txt', 'File content', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith'))
+ assert_response :created
+ end
+
+ attachment = Attachment.order('id DESC').first
+ assert_equal 'test.txt', attachment.filename
+ assert_match /_test\.txt$/, attachment.diskfile
+ end
+
+ test "POST /uploads.xml should not accept other content types" do
+ set_tmp_attachments_directory
+ assert_no_difference 'Attachment.count' do
+ post '/uploads.xml', 'PNG DATA', {"CONTENT_TYPE" => 'image/png'}.merge(credentials('jsmith'))
+ assert_response 406
+ end
+ end
+
+ test "POST /uploads.xml should return errors if file is too big" do
+ set_tmp_attachments_directory
+ with_settings :attachment_max_size => 1 do
+ assert_no_difference 'Attachment.count' do
+ post '/uploads.xml', ('x' * 2048), {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith'))
+ assert_response 422
+ assert_tag 'error', :content => /exceeds the maximum allowed file size/
+ end
+ end
+ end
+end
diff --git a/test/integration/api_test/authentication_test.rb b/test/integration/api_test/authentication_test.rb
new file mode 100644
index 000000000..4dc5c092f
--- /dev/null
+++ b/test/integration/api_test/authentication_test.rb
@@ -0,0 +1,73 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::AuthenticationTest < Redmine::ApiTest::Base
+ fixtures :users
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ def teardown
+ Setting.rest_api_enabled = '0'
+ end
+
+ def test_api_request_should_not_use_user_session
+ log_user('jsmith', 'jsmith')
+
+ get '/users/current'
+ assert_response :success
+
+ get '/users/current.json'
+ assert_response 401
+ end
+
+ def test_api_should_accept_switch_user_header_for_admin_user
+ user = User.find(1)
+ su = User.find(4)
+
+ get '/users/current', {}, {'X-Redmine-API-Key' => user.api_key, 'X-Redmine-Switch-User' => su.login}
+ assert_response :success
+ assert_equal su, assigns(:user)
+ assert_equal su, User.current
+ end
+
+ def test_api_should_respond_with_412_when_trying_to_switch_to_a_invalid_user
+ get '/users/current', {}, {'X-Redmine-API-Key' => User.find(1).api_key, 'X-Redmine-Switch-User' => 'foobar'}
+ assert_response 412
+ end
+
+ def test_api_should_respond_with_412_when_trying_to_switch_to_a_locked_user
+ user = User.find(5)
+ assert user.locked?
+
+ get '/users/current', {}, {'X-Redmine-API-Key' => User.find(1).api_key, 'X-Redmine-Switch-User' => user.login}
+ assert_response 412
+ end
+
+ def test_api_should_not_accept_switch_user_header_for_non_admin_user
+ user = User.find(2)
+ su = User.find(4)
+
+ get '/users/current', {}, {'X-Redmine-API-Key' => user.api_key, 'X-Redmine-Switch-User' => su.login}
+ assert_response :success
+ assert_equal user, assigns(:user)
+ assert_equal user, User.current
+ end
+end
diff --git a/test/integration/api_test/disabled_rest_api_test.rb b/test/integration/api_test/disabled_rest_api_test.rb
new file mode 100644
index 000000000..c29094aa9
--- /dev/null
+++ b/test/integration/api_test/disabled_rest_api_test.rb
@@ -0,0 +1,78 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::DisabledRestApiTest < Redmine::ApiTest::Base
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules
+
+ def setup
+ Setting.rest_api_enabled = '0'
+ Setting.login_required = '1'
+ end
+
+ def teardown
+ Setting.rest_api_enabled = '1'
+ Setting.login_required = '0'
+ end
+
+ def test_with_a_valid_api_token
+ @user = User.generate!
+ @token = Token.create!(:user => @user, :action => 'api')
+
+ get "/news.xml?key=#{@token.value}"
+ assert_response :unauthorized
+ assert_equal User.anonymous, User.current
+
+ get "/news.json?key=#{@token.value}"
+ assert_response :unauthorized
+ assert_equal User.anonymous, User.current
+ end
+
+ def test_with_valid_username_password_http_authentication
+ @user = User.generate! do |user|
+ user.password = 'my_password'
+ end
+
+ get "/news.xml", nil, credentials(@user.login, 'my_password')
+ assert_response :unauthorized
+ assert_equal User.anonymous, User.current
+
+ get "/news.json", nil, credentials(@user.login, 'my_password')
+ assert_response :unauthorized
+ assert_equal User.anonymous, User.current
+ end
+
+ def test_with_valid_token_http_authentication
+ @user = User.generate!
+ @token = Token.create!(:user => @user, :action => 'api')
+
+ get "/news.xml", nil, credentials(@token.value, 'X')
+ assert_response :unauthorized
+ assert_equal User.anonymous, User.current
+
+ get "/news.json", nil, credentials(@token.value, 'X')
+ assert_response :unauthorized
+ assert_equal User.anonymous, User.current
+ end
+end
diff --git a/test/integration/api_test/enumerations_test.rb b/test/integration/api_test/enumerations_test.rb
new file mode 100644
index 000000000..f1000ffe4
--- /dev/null
+++ b/test/integration/api_test/enumerations_test.rb
@@ -0,0 +1,44 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::EnumerationsTest < Redmine::ApiTest::Base
+ fixtures :enumerations
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "/enumerations/issue_priorities" do
+ context "GET" do
+
+ should "return priorities" do
+ get '/enumerations/issue_priorities.xml'
+
+ assert_response :success
+ assert_equal 'application/xml', response.content_type
+ assert_select 'issue_priorities[type=array]' do
+ assert_select 'issue_priority' do
+ assert_select 'id', :text => '6'
+ assert_select 'name', :text => 'High'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/test/integration/api_test/groups_test.rb b/test/integration/api_test/groups_test.rb
new file mode 100644
index 000000000..e155b8d7c
--- /dev/null
+++ b/test/integration/api_test/groups_test.rb
@@ -0,0 +1,212 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::GroupsTest < Redmine::ApiTest::Base
+ fixtures :users, :groups_users
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "GET /groups" do
+ context ".xml" do
+ should "require authentication" do
+ get '/groups.xml'
+ assert_response 401
+ end
+
+ should "return groups" do
+ get '/groups.xml', {}, credentials('admin')
+ assert_response :success
+ assert_equal 'application/xml', response.content_type
+
+ assert_select 'groups' do
+ assert_select 'group' do
+ assert_select 'name', :text => 'A Team'
+ assert_select 'id', :text => '10'
+ end
+ end
+ end
+ end
+
+ context ".json" do
+ should "require authentication" do
+ get '/groups.json'
+ assert_response 401
+ end
+
+ should "return groups" do
+ get '/groups.json', {}, credentials('admin')
+ assert_response :success
+ assert_equal 'application/json', response.content_type
+
+ json = MultiJson.load(response.body)
+ groups = json['groups']
+ assert_kind_of Array, groups
+ group = groups.detect {|g| g['name'] == 'A Team'}
+ assert_not_nil group
+ assert_equal({'id' => 10, 'name' => 'A Team'}, group)
+ end
+ end
+ end
+
+ context "GET /groups/:id" do
+ context ".xml" do
+ should "return the group with its users" do
+ get '/groups/10.xml', {}, credentials('admin')
+ assert_response :success
+ assert_equal 'application/xml', response.content_type
+
+ assert_select 'group' do
+ assert_select 'name', :text => 'A Team'
+ assert_select 'id', :text => '10'
+ end
+ end
+
+ should "include users if requested" do
+ get '/groups/10.xml?include=users', {}, credentials('admin')
+ assert_response :success
+ assert_equal 'application/xml', response.content_type
+
+ assert_select 'group' do
+ assert_select 'users' do
+ assert_select 'user', Group.find(10).users.count
+ assert_select 'user[id=8]'
+ end
+ end
+ end
+
+ should "include memberships if requested" do
+ get '/groups/10.xml?include=memberships', {}, credentials('admin')
+ assert_response :success
+ assert_equal 'application/xml', response.content_type
+
+ assert_select 'group' do
+ assert_select 'memberships'
+ end
+ end
+ end
+ end
+
+ context "POST /groups" do
+ context "with valid parameters" do
+ context ".xml" do
+ should "create groups" do
+ assert_difference('Group.count') do
+ post '/groups.xml', {:group => {:name => 'Test', :user_ids => [2, 3]}}, credentials('admin')
+ assert_response :created
+ assert_equal 'application/xml', response.content_type
+ end
+
+ group = Group.order('id DESC').first
+ assert_equal 'Test', group.name
+ assert_equal [2, 3], group.users.map(&:id).sort
+
+ assert_select 'group' do
+ assert_select 'name', :text => 'Test'
+ end
+ end
+ end
+ end
+
+ context "with invalid parameters" do
+ context ".xml" do
+ should "return errors" do
+ assert_no_difference('Group.count') do
+ post '/groups.xml', {:group => {:name => ''}}, credentials('admin')
+ end
+ assert_response :unprocessable_entity
+ assert_equal 'application/xml', response.content_type
+
+ assert_select 'errors' do
+ assert_select 'error', :text => /Name can't be blank/
+ end
+ end
+ end
+ end
+ end
+
+ context "PUT /groups/:id" do
+ context "with valid parameters" do
+ context ".xml" do
+ should "update the group" do
+ put '/groups/10.xml', {:group => {:name => 'New name', :user_ids => [2, 3]}}, credentials('admin')
+ assert_response :ok
+ assert_equal '', @response.body
+
+ group = Group.find(10)
+ assert_equal 'New name', group.name
+ assert_equal [2, 3], group.users.map(&:id).sort
+ end
+ end
+ end
+
+ context "with invalid parameters" do
+ context ".xml" do
+ should "return errors" do
+ put '/groups/10.xml', {:group => {:name => ''}}, credentials('admin')
+ assert_response :unprocessable_entity
+ assert_equal 'application/xml', response.content_type
+
+ assert_select 'errors' do
+ assert_select 'error', :text => /Name can't be blank/
+ end
+ end
+ end
+ end
+ end
+
+ context "DELETE /groups/:id" do
+ context ".xml" do
+ should "delete the group" do
+ assert_difference 'Group.count', -1 do
+ delete '/groups/10.xml', {}, credentials('admin')
+ assert_response :ok
+ assert_equal '', @response.body
+ end
+ end
+ end
+ end
+
+ context "POST /groups/:id/users" do
+ context ".xml" do
+ should "add user to the group" do
+ assert_difference 'Group.find(10).users.count' do
+ post '/groups/10/users.xml', {:user_id => 5}, credentials('admin')
+ assert_response :ok
+ assert_equal '', @response.body
+ end
+ assert_include User.find(5), Group.find(10).users
+ end
+ end
+ end
+
+ context "DELETE /groups/:id/users/:user_id" do
+ context ".xml" do
+ should "remove user from the group" do
+ assert_difference 'Group.find(10).users.count', -1 do
+ delete '/groups/10/users/8.xml', {}, credentials('admin')
+ assert_response :ok
+ assert_equal '', @response.body
+ end
+ assert_not_include User.find(8), Group.find(10).users
+ end
+ end
+ end
+end
diff --git a/test/integration/api_test/http_basic_login_test.rb b/test/integration/api_test/http_basic_login_test.rb
new file mode 100644
index 000000000..e88d097ee
--- /dev/null
+++ b/test/integration/api_test/http_basic_login_test.rb
@@ -0,0 +1,54 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::HttpBasicLoginTest < Redmine::ApiTest::Base
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ Setting.login_required = '1'
+ end
+
+ def teardown
+ Setting.rest_api_enabled = '0'
+ Setting.login_required = '0'
+ end
+
+ # Using the NewsController because it's a simple API.
+ context "get /news" do
+ setup do
+ project = Project.find('onlinestore')
+ EnabledModule.create(:project => project, :name => 'news')
+ end
+
+ context "in :xml format" do
+ should_allow_http_basic_auth_with_username_and_password(:get, "/projects/onlinestore/news.xml")
+ end
+
+ context "in :json format" do
+ should_allow_http_basic_auth_with_username_and_password(:get, "/projects/onlinestore/news.json")
+ end
+ end
+end
diff --git a/test/integration/api_test/http_basic_login_with_api_token_test.rb b/test/integration/api_test/http_basic_login_with_api_token_test.rb
new file mode 100644
index 000000000..77c5dab79
--- /dev/null
+++ b/test/integration/api_test/http_basic_login_with_api_token_test.rb
@@ -0,0 +1,50 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::HttpBasicLoginWithApiTokenTest < Redmine::ApiTest::Base
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ Setting.login_required = '1'
+ end
+
+ def teardown
+ Setting.rest_api_enabled = '0'
+ Setting.login_required = '0'
+ end
+
+ # Using the NewsController because it's a simple API.
+ context "get /news" do
+
+ context "in :xml format" do
+ should_allow_http_basic_auth_with_key(:get, "/news.xml")
+ end
+
+ context "in :json format" do
+ should_allow_http_basic_auth_with_key(:get, "/news.json")
+ end
+ end
+end
diff --git a/test/integration/api_test/issue_categories_test.rb b/test/integration/api_test/issue_categories_test.rb
new file mode 100644
index 000000000..9e5448414
--- /dev/null
+++ b/test/integration/api_test/issue_categories_test.rb
@@ -0,0 +1,126 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::IssueCategoriesTest < Redmine::ApiTest::Base
+ fixtures :projects, :users, :issue_categories, :issues,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "GET /projects/:project_id/issue_categories.xml" do
+ should "return issue categories" do
+ get '/projects/1/issue_categories.xml', {}, credentials('jsmith')
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag :tag => 'issue_categories',
+ :child => {:tag => 'issue_category', :child => {:tag => 'id', :content => '2'}}
+ end
+ end
+
+ context "GET /issue_categories/2.xml" do
+ should "return requested issue category" do
+ get '/issue_categories/2.xml', {}, credentials('jsmith')
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag :tag => 'issue_category',
+ :child => {:tag => 'id', :content => '2'}
+ end
+ end
+
+ context "POST /projects/:project_id/issue_categories.xml" do
+ should "return create issue category" do
+ assert_difference 'IssueCategory.count' do
+ post '/projects/1/issue_categories.xml', {:issue_category => {:name => 'API'}}, credentials('jsmith')
+ end
+ assert_response :created
+ assert_equal 'application/xml', @response.content_type
+
+ category = IssueCategory.first(:order => 'id DESC')
+ assert_equal 'API', category.name
+ assert_equal 1, category.project_id
+ end
+
+ context "with invalid parameters" do
+ should "return errors" do
+ assert_no_difference 'IssueCategory.count' do
+ post '/projects/1/issue_categories.xml', {:issue_category => {:name => ''}}, credentials('jsmith')
+ end
+ assert_response :unprocessable_entity
+ assert_equal 'application/xml', @response.content_type
+
+ assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
+ end
+ end
+ end
+
+ context "PUT /issue_categories/2.xml" do
+ context "with valid parameters" do
+ should "update issue category" do
+ assert_no_difference 'IssueCategory.count' do
+ put '/issue_categories/2.xml', {:issue_category => {:name => 'API Update'}}, credentials('jsmith')
+ end
+ assert_response :ok
+ assert_equal '', @response.body
+ assert_equal 'API Update', IssueCategory.find(2).name
+ end
+ end
+
+ context "with invalid parameters" do
+ should "return errors" do
+ assert_no_difference 'IssueCategory.count' do
+ put '/issue_categories/2.xml', {:issue_category => {:name => ''}}, credentials('jsmith')
+ end
+ assert_response :unprocessable_entity
+ assert_equal 'application/xml', @response.content_type
+
+ assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
+ end
+ end
+ end
+
+ context "DELETE /issue_categories/1.xml" do
+ should "destroy issue categories" do
+ assert_difference 'IssueCategory.count', -1 do
+ delete '/issue_categories/1.xml', {}, credentials('jsmith')
+ end
+ assert_response :ok
+ assert_equal '', @response.body
+ assert_nil IssueCategory.find_by_id(1)
+ end
+
+ should "reassign issues with :reassign_to_id param" do
+ issue_count = Issue.count(:conditions => {:category_id => 1})
+ assert issue_count > 0
+
+ assert_difference 'IssueCategory.count', -1 do
+ assert_difference 'Issue.count(:conditions => {:category_id => 2})', 3 do
+ delete '/issue_categories/1.xml', {:reassign_to_id => 2}, credentials('jsmith')
+ end
+ end
+ assert_response :ok
+ assert_equal '', @response.body
+ assert_nil IssueCategory.find_by_id(1)
+ end
+ end
+end
diff --git a/test/integration/api_test/issue_relations_test.rb b/test/integration/api_test/issue_relations_test.rb
new file mode 100644
index 000000000..72c690e6a
--- /dev/null
+++ b/test/integration/api_test/issue_relations_test.rb
@@ -0,0 +1,106 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::IssueRelationsTest < Redmine::ApiTest::Base
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules,
+ :issue_relations
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "/issues/:issue_id/relations" do
+ context "GET" do
+ should "return issue relations" do
+ get '/issues/9/relations.xml', {}, credentials('jsmith')
+
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+
+ assert_tag :tag => 'relations',
+ :attributes => { :type => 'array' },
+ :child => {
+ :tag => 'relation',
+ :child => {
+ :tag => 'id',
+ :content => '1'
+ }
+ }
+ end
+ end
+
+ context "POST" do
+ should "create a relation" do
+ assert_difference('IssueRelation.count') do
+ post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'relates'}}, credentials('jsmith')
+ end
+
+ relation = IssueRelation.first(:order => 'id DESC')
+ assert_equal 2, relation.issue_from_id
+ assert_equal 7, relation.issue_to_id
+ assert_equal 'relates', relation.relation_type
+
+ assert_response :created
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'relation', :child => {:tag => 'id', :content => relation.id.to_s}
+ end
+
+ context "with failure" do
+ should "return the errors" do
+ assert_no_difference('IssueRelation.count') do
+ post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'foo'}}, credentials('jsmith')
+ end
+
+ assert_response :unprocessable_entity
+ assert_tag :errors, :child => {:tag => 'error', :content => /relation_type is not included in the list/}
+ end
+ end
+ end
+ end
+
+ context "/relations/:id" do
+ context "GET" do
+ should "return the relation" do
+ get '/relations/2.xml', {}, credentials('jsmith')
+
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'relation', :child => {:tag => 'id', :content => '2'}
+ end
+ end
+
+ context "DELETE" do
+ should "delete the relation" do
+ assert_difference('IssueRelation.count', -1) do
+ delete '/relations/2.xml', {}, credentials('jsmith')
+ end
+
+ assert_response :ok
+ assert_equal '', @response.body
+ assert_nil IssueRelation.find_by_id(2)
+ end
+ end
+ end
+end
diff --git a/test/integration/api_test/issue_statuses_test.rb b/test/integration/api_test/issue_statuses_test.rb
new file mode 100644
index 000000000..db8859b13
--- /dev/null
+++ b/test/integration/api_test/issue_statuses_test.rb
@@ -0,0 +1,51 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::IssueStatusesTest < Redmine::ApiTest::Base
+ fixtures :issue_statuses
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "/issue_statuses" do
+ context "GET" do
+
+ should "return issue statuses" do
+ get '/issue_statuses.xml'
+
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag :tag => 'issue_statuses',
+ :attributes => {:type => 'array'},
+ :child => {
+ :tag => 'issue_status',
+ :child => {
+ :tag => 'id',
+ :content => '2',
+ :sibling => {
+ :tag => 'name',
+ :content => 'Assigned'
+ }
+ }
+ }
+ end
+ end
+ end
+end
diff --git a/test/integration/api_test/issues_test.rb b/test/integration/api_test/issues_test.rb
new file mode 100644
index 000000000..176c18105
--- /dev/null
+++ b/test/integration/api_test/issues_test.rb
@@ -0,0 +1,846 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base
+ fixtures :projects,
+ :users,
+ :roles,
+ :members,
+ :member_roles,
+ :issues,
+ :issue_statuses,
+ :issue_relations,
+ :versions,
+ :trackers,
+ :projects_trackers,
+ :issue_categories,
+ :enabled_modules,
+ :enumerations,
+ :attachments,
+ :workflows,
+ :custom_fields,
+ :custom_values,
+ :custom_fields_projects,
+ :custom_fields_trackers,
+ :time_entries,
+ :journals,
+ :journal_details,
+ :queries,
+ :attachments
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "/issues" do
+ # Use a private project to make sure auth is really working and not just
+ # only showing public issues.
+ should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
+
+ should "contain metadata" do
+ get '/issues.xml'
+
+ assert_tag :tag => 'issues',
+ :attributes => {
+ :type => 'array',
+ :total_count => assigns(:issue_count),
+ :limit => 25,
+ :offset => 0
+ }
+ end
+
+ context "with offset and limit" do
+ should "use the params" do
+ get '/issues.xml?offset=2&limit=3'
+
+ assert_equal 3, assigns(:limit)
+ assert_equal 2, assigns(:offset)
+ assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}}
+ end
+ end
+
+ context "with nometa param" do
+ should "not contain metadata" do
+ get '/issues.xml?nometa=1'
+
+ assert_tag :tag => 'issues',
+ :attributes => {
+ :type => 'array',
+ :total_count => nil,
+ :limit => nil,
+ :offset => nil
+ }
+ end
+ end
+
+ context "with nometa header" do
+ should "not contain metadata" do
+ get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'}
+
+ assert_tag :tag => 'issues',
+ :attributes => {
+ :type => 'array',
+ :total_count => nil,
+ :limit => nil,
+ :offset => nil
+ }
+ end
+ end
+
+ context "with relations" do
+ should "display relations" do
+ get '/issues.xml?include=relations'
+
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'relations',
+ :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '3'}},
+ :children => {:count => 1},
+ :child => {
+ :tag => 'relation',
+ :attributes => {:id => '2', :issue_id => '2', :issue_to_id => '3',
+ :relation_type => 'relates'}
+ }
+ assert_tag 'relations',
+ :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '1'}},
+ :children => {:count => 0}
+ end
+ end
+
+ context "with invalid query params" do
+ should "return errors" do
+ get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}}
+
+ assert_response :unprocessable_entity
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'errors', :child => {:tag => 'error', :content => "Start date can't be blank"}
+ end
+ end
+
+ context "with custom field filter" do
+ should "show only issues with the custom field value" do
+ get '/issues.xml',
+ {:set_filter => 1, :f => ['cf_1'], :op => {:cf_1 => '='},
+ :v => {:cf_1 => ['MySQL']}}
+ expected_ids = Issue.visible.all(
+ :include => :custom_values,
+ :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
+ assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
+ ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
+ end
+ end
+ end
+
+ context "with custom field filter (shorthand method)" do
+ should "show only issues with the custom field value" do
+ get '/issues.xml', { :cf_1 => 'MySQL' }
+
+ expected_ids = Issue.visible.all(
+ :include => :custom_values,
+ :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
+
+ assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
+ ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
+ end
+ end
+ end
+ end
+
+ context "/index.json" do
+ should_allow_api_authentication(:get, "/projects/private-child/issues.json")
+ end
+
+ context "/index.xml with filter" do
+ should "show only issues with the status_id" do
+ get '/issues.xml?status_id=5'
+
+ expected_ids = Issue.visible.all(:conditions => {:status_id => 5}).map(&:id)
+
+ assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
+ ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
+ end
+ end
+ end
+
+ context "/index.json with filter" do
+ should "show only issues with the status_id" do
+ get '/issues.json?status_id=5'
+
+ json = ActiveSupport::JSON.decode(response.body)
+ status_ids_used = json['issues'].collect {|j| j['status']['id'] }
+ assert_equal 3, status_ids_used.length
+ assert status_ids_used.all? {|id| id == 5 }
+ end
+
+ end
+
+ # Issue 6 is on a private project
+ context "/issues/6.xml" do
+ should_allow_api_authentication(:get, "/issues/6.xml")
+ end
+
+ context "/issues/6.json" do
+ should_allow_api_authentication(:get, "/issues/6.json")
+ end
+
+ context "GET /issues/:id" do
+ context "with journals" do
+ context ".xml" do
+ should "display journals" do
+ get '/issues/1.xml?include=journals'
+
+ assert_tag :tag => 'issue',
+ :child => {
+ :tag => 'journals',
+ :attributes => { :type => 'array' },
+ :child => {
+ :tag => 'journal',
+ :attributes => { :id => '1'},
+ :child => {
+ :tag => 'details',
+ :attributes => { :type => 'array' },
+ :child => {
+ :tag => 'detail',
+ :attributes => { :name => 'status_id' },
+ :child => {
+ :tag => 'old_value',
+ :content => '1',
+ :sibling => {
+ :tag => 'new_value',
+ :content => '2'
+ }
+ }
+ }
+ }
+ }
+ }
+ end
+ end
+ end
+
+ context "with custom fields" do
+ context ".xml" do
+ should "display custom fields" do
+ get '/issues/3.xml'
+
+ assert_tag :tag => 'issue',
+ :child => {
+ :tag => 'custom_fields',
+ :attributes => { :type => 'array' },
+ :child => {
+ :tag => 'custom_field',
+ :attributes => { :id => '1'},
+ :child => {
+ :tag => 'value',
+ :content => 'MySQL'
+ }
+ }
+ }
+
+ assert_nothing_raised do
+ Hash.from_xml(response.body).to_xml
+ end
+ end
+ end
+ end
+
+ context "with multi custom fields" do
+ setup do
+ field = CustomField.find(1)
+ field.update_attribute :multiple, true
+ issue = Issue.find(3)
+ issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
+ issue.save!
+ end
+
+ context ".xml" do
+ should "display custom fields" do
+ get '/issues/3.xml'
+ assert_response :success
+ assert_tag :tag => 'issue',
+ :child => {
+ :tag => 'custom_fields',
+ :attributes => { :type => 'array' },
+ :child => {
+ :tag => 'custom_field',
+ :attributes => { :id => '1'},
+ :child => {
+ :tag => 'value',
+ :attributes => { :type => 'array' },
+ :children => { :count => 2 }
+ }
+ }
+ }
+
+ xml = Hash.from_xml(response.body)
+ custom_fields = xml['issue']['custom_fields']
+ assert_kind_of Array, custom_fields
+ field = custom_fields.detect {|f| f['id'] == '1'}
+ assert_kind_of Hash, field
+ assert_equal ['MySQL', 'Oracle'], field['value'].sort
+ end
+ end
+
+ context ".json" do
+ should "display custom fields" do
+ get '/issues/3.json'
+ assert_response :success
+ json = ActiveSupport::JSON.decode(response.body)
+ custom_fields = json['issue']['custom_fields']
+ assert_kind_of Array, custom_fields
+ field = custom_fields.detect {|f| f['id'] == 1}
+ assert_kind_of Hash, field
+ assert_equal ['MySQL', 'Oracle'], field['value'].sort
+ end
+ end
+ end
+
+ context "with empty value for multi custom field" do
+ setup do
+ field = CustomField.find(1)
+ field.update_attribute :multiple, true
+ issue = Issue.find(3)
+ issue.custom_field_values = {1 => ['']}
+ issue.save!
+ end
+
+ context ".xml" do
+ should "display custom fields" do
+ get '/issues/3.xml'
+ assert_response :success
+ assert_tag :tag => 'issue',
+ :child => {
+ :tag => 'custom_fields',
+ :attributes => { :type => 'array' },
+ :child => {
+ :tag => 'custom_field',
+ :attributes => { :id => '1'},
+ :child => {
+ :tag => 'value',
+ :attributes => { :type => 'array' },
+ :children => { :count => 0 }
+ }
+ }
+ }
+
+ xml = Hash.from_xml(response.body)
+ custom_fields = xml['issue']['custom_fields']
+ assert_kind_of Array, custom_fields
+ field = custom_fields.detect {|f| f['id'] == '1'}
+ assert_kind_of Hash, field
+ assert_equal [], field['value']
+ end
+ end
+
+ context ".json" do
+ should "display custom fields" do
+ get '/issues/3.json'
+ assert_response :success
+ json = ActiveSupport::JSON.decode(response.body)
+ custom_fields = json['issue']['custom_fields']
+ assert_kind_of Array, custom_fields
+ field = custom_fields.detect {|f| f['id'] == 1}
+ assert_kind_of Hash, field
+ assert_equal [], field['value'].sort
+ end
+ end
+ end
+
+ context "with attachments" do
+ context ".xml" do
+ should "display attachments" do
+ get '/issues/3.xml?include=attachments'
+
+ assert_tag :tag => 'issue',
+ :child => {
+ :tag => 'attachments',
+ :children => {:count => 5},
+ :child => {
+ :tag => 'attachment',
+ :child => {
+ :tag => 'filename',
+ :content => 'source.rb',
+ :sibling => {
+ :tag => 'content_url',
+ :content => 'http://www.example.com/attachments/download/4/source.rb'
+ }
+ }
+ }
+ }
+ end
+ end
+ end
+
+ context "with subtasks" do
+ setup do
+ @c1 = Issue.create!(
+ :status_id => 1, :subject => "child c1",
+ :tracker_id => 1, :project_id => 1, :author_id => 1,
+ :parent_issue_id => 1
+ )
+ @c2 = Issue.create!(
+ :status_id => 1, :subject => "child c2",
+ :tracker_id => 1, :project_id => 1, :author_id => 1,
+ :parent_issue_id => 1
+ )
+ @c3 = Issue.create!(
+ :status_id => 1, :subject => "child c3",
+ :tracker_id => 1, :project_id => 1, :author_id => 1,
+ :parent_issue_id => @c1.id
+ )
+ end
+
+ context ".xml" do
+ should "display children" do
+ get '/issues/1.xml?include=children'
+
+ assert_tag :tag => 'issue',
+ :child => {
+ :tag => 'children',
+ :children => {:count => 2},
+ :child => {
+ :tag => 'issue',
+ :attributes => {:id => @c1.id.to_s},
+ :child => {
+ :tag => 'subject',
+ :content => 'child c1',
+ :sibling => {
+ :tag => 'children',
+ :children => {:count => 1},
+ :child => {
+ :tag => 'issue',
+ :attributes => {:id => @c3.id.to_s}
+ }
+ }
+ }
+ }
+ }
+ end
+
+ context ".json" do
+ should "display children" do
+ get '/issues/1.json?include=children'
+
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_equal([
+ {
+ 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
+ 'children' => [{'id' => @c3.id, 'subject' => 'child c3',
+ 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
+ },
+ { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
+ ],
+ json['issue']['children'])
+ end
+ end
+ end
+ end
+ end
+
+ test "GET /issues/:id.xml?include=watchers should include watchers" do
+ Watcher.create!(:user_id => 3, :watchable => Issue.find(1))
+
+ get '/issues/1.xml?include=watchers', {}, credentials('jsmith')
+
+ assert_response :ok
+ assert_equal 'application/xml', response.content_type
+ assert_select 'issue' do
+ assert_select 'watchers', Issue.find(1).watchers.count
+ assert_select 'watchers' do
+ assert_select 'user[id=3]'
+ end
+ end
+ end
+
+ context "POST /issues.xml" do
+ should_allow_api_authentication(
+ :post,
+ '/issues.xml',
+ {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
+ {:success_code => :created}
+ )
+ should "create an issue with the attributes" do
+ assert_difference('Issue.count') do
+ post '/issues.xml',
+ {:issue => {:project_id => 1, :subject => 'API test',
+ :tracker_id => 2, :status_id => 3}}, credentials('jsmith')
+ end
+ issue = Issue.first(:order => 'id DESC')
+ assert_equal 1, issue.project_id
+ assert_equal 2, issue.tracker_id
+ assert_equal 3, issue.status_id
+ assert_equal 'API test', issue.subject
+
+ assert_response :created
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
+ end
+ end
+
+ test "POST /issues.xml with watcher_user_ids should create issue with watchers" do
+ assert_difference('Issue.count') do
+ post '/issues.xml',
+ {:issue => {:project_id => 1, :subject => 'Watchers',
+ :tracker_id => 2, :status_id => 3, :watcher_user_ids => [3, 1]}}, credentials('jsmith')
+ assert_response :created
+ end
+ issue = Issue.order('id desc').first
+ assert_equal 2, issue.watchers.size
+ assert_equal [1, 3], issue.watcher_user_ids.sort
+ end
+
+ context "POST /issues.xml with failure" do
+ should "have an errors tag" do
+ assert_no_difference('Issue.count') do
+ post '/issues.xml', {:issue => {:project_id => 1}}, credentials('jsmith')
+ end
+
+ assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
+ end
+ end
+
+ context "POST /issues.json" do
+ should_allow_api_authentication(:post,
+ '/issues.json',
+ {:issue => {:project_id => 1, :subject => 'API test',
+ :tracker_id => 2, :status_id => 3}},
+ {:success_code => :created})
+
+ should "create an issue with the attributes" do
+ assert_difference('Issue.count') do
+ post '/issues.json',
+ {:issue => {:project_id => 1, :subject => 'API test',
+ :tracker_id => 2, :status_id => 3}},
+ credentials('jsmith')
+ end
+
+ issue = Issue.first(:order => 'id DESC')
+ assert_equal 1, issue.project_id
+ assert_equal 2, issue.tracker_id
+ assert_equal 3, issue.status_id
+ assert_equal 'API test', issue.subject
+ end
+
+ end
+
+ context "POST /issues.json with failure" do
+ should "have an errors element" do
+ assert_no_difference('Issue.count') do
+ post '/issues.json', {:issue => {:project_id => 1}}, credentials('jsmith')
+ end
+
+ json = ActiveSupport::JSON.decode(response.body)
+ assert json['errors'].include?("Subject can't be blank")
+ end
+ end
+
+ # Issue 6 is on a private project
+ context "PUT /issues/6.xml" do
+ setup do
+ @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
+ end
+
+ should_allow_api_authentication(:put,
+ '/issues/6.xml',
+ {:issue => {:subject => 'API update', :notes => 'A new note'}},
+ {:success_code => :ok})
+
+ should "not create a new issue" do
+ assert_no_difference('Issue.count') do
+ put '/issues/6.xml', @parameters, credentials('jsmith')
+ end
+ end
+
+ should "create a new journal" do
+ assert_difference('Journal.count') do
+ put '/issues/6.xml', @parameters, credentials('jsmith')
+ end
+ end
+
+ should "add the note to the journal" do
+ put '/issues/6.xml', @parameters, credentials('jsmith')
+
+ journal = Journal.last
+ assert_equal "A new note", journal.notes
+ end
+
+ should "update the issue" do
+ put '/issues/6.xml', @parameters, credentials('jsmith')
+
+ issue = Issue.find(6)
+ assert_equal "API update", issue.subject
+ end
+
+ end
+
+ context "PUT /issues/3.xml with custom fields" do
+ setup do
+ @parameters = {
+ :issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' },
+ {'id' => '2', 'value' => '150'}]}
+ }
+ end
+
+ should "update custom fields" do
+ assert_no_difference('Issue.count') do
+ put '/issues/3.xml', @parameters, credentials('jsmith')
+ end
+
+ issue = Issue.find(3)
+ assert_equal '150', issue.custom_value_for(2).value
+ assert_equal 'PostgreSQL', issue.custom_value_for(1).value
+ end
+ end
+
+ context "PUT /issues/3.xml with multi custom fields" do
+ setup do
+ field = CustomField.find(1)
+ field.update_attribute :multiple, true
+ @parameters = {
+ :issue => {:custom_fields => [{'id' => '1', 'value' => ['MySQL', 'PostgreSQL'] },
+ {'id' => '2', 'value' => '150'}]}
+ }
+ end
+
+ should "update custom fields" do
+ assert_no_difference('Issue.count') do
+ put '/issues/3.xml', @parameters, credentials('jsmith')
+ end
+
+ issue = Issue.find(3)
+ assert_equal '150', issue.custom_value_for(2).value
+ assert_equal ['MySQL', 'PostgreSQL'], issue.custom_field_value(1).sort
+ end
+ end
+
+ context "PUT /issues/3.xml with project change" do
+ setup do
+ @parameters = {:issue => {:project_id => 2, :subject => 'Project changed'}}
+ end
+
+ should "update project" do
+ assert_no_difference('Issue.count') do
+ put '/issues/3.xml', @parameters, credentials('jsmith')
+ end
+
+ issue = Issue.find(3)
+ assert_equal 2, issue.project_id
+ assert_equal 'Project changed', issue.subject
+ end
+ end
+
+ context "PUT /issues/6.xml with failed update" do
+ setup do
+ @parameters = {:issue => {:subject => ''}}
+ end
+
+ should "not create a new issue" do
+ assert_no_difference('Issue.count') do
+ put '/issues/6.xml', @parameters, credentials('jsmith')
+ end
+ end
+
+ should "not create a new journal" do
+ assert_no_difference('Journal.count') do
+ put '/issues/6.xml', @parameters, credentials('jsmith')
+ end
+ end
+
+ should "have an errors tag" do
+ put '/issues/6.xml', @parameters, credentials('jsmith')
+
+ assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
+ end
+ end
+
+ context "PUT /issues/6.json" do
+ setup do
+ @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
+ end
+
+ should_allow_api_authentication(:put,
+ '/issues/6.json',
+ {:issue => {:subject => 'API update', :notes => 'A new note'}},
+ {:success_code => :ok})
+
+ should "update the issue" do
+ assert_no_difference('Issue.count') do
+ assert_difference('Journal.count') do
+ put '/issues/6.json', @parameters, credentials('jsmith')
+
+ assert_response :ok
+ assert_equal '', response.body
+ end
+ end
+
+ issue = Issue.find(6)
+ assert_equal "API update", issue.subject
+ journal = Journal.last
+ assert_equal "A new note", journal.notes
+ end
+ end
+
+ context "PUT /issues/6.json with failed update" do
+ should "return errors" do
+ assert_no_difference('Issue.count') do
+ assert_no_difference('Journal.count') do
+ put '/issues/6.json', {:issue => {:subject => ''}}, credentials('jsmith')
+
+ assert_response :unprocessable_entity
+ end
+ end
+
+ json = ActiveSupport::JSON.decode(response.body)
+ assert json['errors'].include?("Subject can't be blank")
+ end
+ end
+
+ context "DELETE /issues/1.xml" do
+ should_allow_api_authentication(:delete,
+ '/issues/6.xml',
+ {},
+ {:success_code => :ok})
+
+ should "delete the issue" do
+ assert_difference('Issue.count', -1) do
+ delete '/issues/6.xml', {}, credentials('jsmith')
+
+ assert_response :ok
+ assert_equal '', response.body
+ end
+
+ assert_nil Issue.find_by_id(6)
+ end
+ end
+
+ context "DELETE /issues/1.json" do
+ should_allow_api_authentication(:delete,
+ '/issues/6.json',
+ {},
+ {:success_code => :ok})
+
+ should "delete the issue" do
+ assert_difference('Issue.count', -1) do
+ delete '/issues/6.json', {}, credentials('jsmith')
+
+ assert_response :ok
+ assert_equal '', response.body
+ end
+
+ assert_nil Issue.find_by_id(6)
+ end
+ end
+
+ test "POST /issues/:id/watchers.xml should add watcher" do
+ assert_difference 'Watcher.count' do
+ post '/issues/1/watchers.xml', {:user_id => 3}, credentials('jsmith')
+
+ assert_response :ok
+ assert_equal '', response.body
+ end
+ watcher = Watcher.order('id desc').first
+ assert_equal Issue.find(1), watcher.watchable
+ assert_equal User.find(3), watcher.user
+ end
+
+ test "DELETE /issues/:id/watchers/:user_id.xml should remove watcher" do
+ Watcher.create!(:user_id => 3, :watchable => Issue.find(1))
+
+ assert_difference 'Watcher.count', -1 do
+ delete '/issues/1/watchers/3.xml', {}, credentials('jsmith')
+
+ assert_response :ok
+ assert_equal '', response.body
+ end
+ assert_equal false, Issue.find(1).watched_by?(User.find(3))
+ end
+
+ def test_create_issue_with_uploaded_file
+ set_tmp_attachments_directory
+ # upload the file
+ assert_difference 'Attachment.count' do
+ post '/uploads.xml', 'test_create_with_upload',
+ {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith'))
+ assert_response :created
+ end
+ xml = Hash.from_xml(response.body)
+ token = xml['upload']['token']
+ attachment = Attachment.first(:order => 'id DESC')
+
+ # create the issue with the upload's token
+ assert_difference 'Issue.count' do
+ post '/issues.xml',
+ {:issue => {:project_id => 1, :subject => 'Uploaded file',
+ :uploads => [{:token => token, :filename => 'test.txt',
+ :content_type => 'text/plain'}]}},
+ credentials('jsmith')
+ assert_response :created
+ end
+ issue = Issue.first(:order => 'id DESC')
+ assert_equal 1, issue.attachments.count
+ assert_equal attachment, issue.attachments.first
+
+ attachment.reload
+ assert_equal 'test.txt', attachment.filename
+ assert_equal 'text/plain', attachment.content_type
+ assert_equal 'test_create_with_upload'.size, attachment.filesize
+ assert_equal 2, attachment.author_id
+
+ # get the issue with its attachments
+ get "/issues/#{issue.id}.xml", :include => 'attachments'
+ assert_response :success
+ xml = Hash.from_xml(response.body)
+ attachments = xml['issue']['attachments']
+ assert_kind_of Array, attachments
+ assert_equal 1, attachments.size
+ url = attachments.first['content_url']
+ assert_not_nil url
+
+ # download the attachment
+ get url
+ assert_response :success
+ end
+
+ def test_update_issue_with_uploaded_file
+ set_tmp_attachments_directory
+ # upload the file
+ assert_difference 'Attachment.count' do
+ post '/uploads.xml', 'test_upload_with_upload',
+ {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith'))
+ assert_response :created
+ end
+ xml = Hash.from_xml(response.body)
+ token = xml['upload']['token']
+ attachment = Attachment.first(:order => 'id DESC')
+
+ # update the issue with the upload's token
+ assert_difference 'Journal.count' do
+ put '/issues/1.xml',
+ {:issue => {:notes => 'Attachment added',
+ :uploads => [{:token => token, :filename => 'test.txt',
+ :content_type => 'text/plain'}]}},
+ credentials('jsmith')
+ assert_response :ok
+ assert_equal '', @response.body
+ end
+
+ issue = Issue.find(1)
+ assert_include attachment, issue.attachments
+ end
+end
diff --git a/test/integration/api_test/jsonp_test.rb b/test/integration/api_test/jsonp_test.rb
new file mode 100644
index 000000000..df3be427c
--- /dev/null
+++ b/test/integration/api_test/jsonp_test.rb
@@ -0,0 +1,72 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::JsonpTest < Redmine::ApiTest::Base
+ fixtures :trackers
+
+ def test_should_ignore_jsonp_callback_with_jsonp_disabled
+ with_settings :jsonp_enabled => '0' do
+ get '/trackers.json?jsonp=handler'
+ end
+
+ assert_response :success
+ assert_match %r{^\{"trackers":.+\}$}, response.body
+ assert_equal 'application/json; charset=utf-8', response.headers['Content-Type']
+ end
+
+ def test_jsonp_should_accept_callback_param
+ with_settings :jsonp_enabled => '1' do
+ get '/trackers.json?callback=handler'
+ end
+
+ assert_response :success
+ assert_match %r{^handler\(\{"trackers":.+\}\)$}, response.body
+ assert_equal 'application/javascript; charset=utf-8', response.headers['Content-Type']
+ end
+
+ def test_jsonp_should_accept_jsonp_param
+ with_settings :jsonp_enabled => '1' do
+ get '/trackers.json?jsonp=handler'
+ end
+
+ assert_response :success
+ assert_match %r{^handler\(\{"trackers":.+\}\)$}, response.body
+ assert_equal 'application/javascript; charset=utf-8', response.headers['Content-Type']
+ end
+
+ def test_jsonp_should_strip_invalid_characters_from_callback
+ with_settings :jsonp_enabled => '1' do
+ get '/trackers.json?callback=+-aA$1_'
+ end
+
+ assert_response :success
+ assert_match %r{^aA1_\(\{"trackers":.+\}\)$}, response.body
+ assert_equal 'application/javascript; charset=utf-8', response.headers['Content-Type']
+ end
+
+ def test_jsonp_without_callback_should_return_json
+ with_settings :jsonp_enabled => '1' do
+ get '/trackers.json?callback='
+ end
+
+ assert_response :success
+ assert_match %r{^\{"trackers":.+\}$}, response.body
+ assert_equal 'application/json; charset=utf-8', response.headers['Content-Type']
+ end
+end
diff --git a/test/integration/api_test/memberships_test.rb b/test/integration/api_test/memberships_test.rb
new file mode 100644
index 000000000..12b7de7ef
--- /dev/null
+++ b/test/integration/api_test/memberships_test.rb
@@ -0,0 +1,200 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::MembershipsTest < Redmine::ApiTest::Base
+ fixtures :projects, :users, :roles, :members, :member_roles
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "/projects/:project_id/memberships" do
+ context "GET" do
+ context "xml" do
+ should "return memberships" do
+ get '/projects/1/memberships.xml', {}, credentials('jsmith')
+
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag :tag => 'memberships',
+ :attributes => {:type => 'array'},
+ :child => {
+ :tag => 'membership',
+ :child => {
+ :tag => 'id',
+ :content => '2',
+ :sibling => {
+ :tag => 'user',
+ :attributes => {:id => '3', :name => 'Dave Lopper'},
+ :sibling => {
+ :tag => 'roles',
+ :child => {
+ :tag => 'role',
+ :attributes => {:id => '2', :name => 'Developer'}
+ }
+ }
+ }
+ }
+ }
+ end
+ end
+
+ context "json" do
+ should "return memberships" do
+ get '/projects/1/memberships.json', {}, credentials('jsmith')
+
+ assert_response :success
+ assert_equal 'application/json', @response.content_type
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_equal({
+ "memberships" =>
+ [{"id"=>1,
+ "project" => {"name"=>"eCookbook", "id"=>1},
+ "roles" => [{"name"=>"Manager", "id"=>1}],
+ "user" => {"name"=>"John Smith", "id"=>2}},
+ {"id"=>2,
+ "project" => {"name"=>"eCookbook", "id"=>1},
+ "roles" => [{"name"=>"Developer", "id"=>2}],
+ "user" => {"name"=>"Dave Lopper", "id"=>3}}],
+ "limit" => 25,
+ "total_count" => 2,
+ "offset" => 0},
+ json)
+ end
+ end
+ end
+
+ context "POST" do
+ context "xml" do
+ should "create membership" do
+ assert_difference 'Member.count' do
+ post '/projects/1/memberships.xml', {:membership => {:user_id => 7, :role_ids => [2,3]}}, credentials('jsmith')
+
+ assert_response :created
+ end
+ end
+
+ should "return errors on failure" do
+ assert_no_difference 'Member.count' do
+ post '/projects/1/memberships.xml', {:membership => {:role_ids => [2,3]}}, credentials('jsmith')
+
+ assert_response :unprocessable_entity
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'errors', :child => {:tag => 'error', :content => "Principal can't be blank"}
+ end
+ end
+ end
+ end
+ end
+
+ context "/memberships/:id" do
+ context "GET" do
+ context "xml" do
+ should "return the membership" do
+ get '/memberships/2.xml', {}, credentials('jsmith')
+
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag :tag => 'membership',
+ :child => {
+ :tag => 'id',
+ :content => '2',
+ :sibling => {
+ :tag => 'user',
+ :attributes => {:id => '3', :name => 'Dave Lopper'},
+ :sibling => {
+ :tag => 'roles',
+ :child => {
+ :tag => 'role',
+ :attributes => {:id => '2', :name => 'Developer'}
+ }
+ }
+ }
+ }
+ end
+ end
+
+ context "json" do
+ should "return the membership" do
+ get '/memberships/2.json', {}, credentials('jsmith')
+
+ assert_response :success
+ assert_equal 'application/json', @response.content_type
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_equal(
+ {"membership" => {
+ "id" => 2,
+ "project" => {"name"=>"eCookbook", "id"=>1},
+ "roles" => [{"name"=>"Developer", "id"=>2}],
+ "user" => {"name"=>"Dave Lopper", "id"=>3}}
+ },
+ json)
+ end
+ end
+ end
+
+ context "PUT" do
+ context "xml" do
+ should "update membership" do
+ assert_not_equal [1,2], Member.find(2).role_ids.sort
+ assert_no_difference 'Member.count' do
+ put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [1,2]}}, credentials('jsmith')
+
+ assert_response :ok
+ assert_equal '', @response.body
+ end
+ member = Member.find(2)
+ assert_equal [1,2], member.role_ids.sort
+ end
+
+ should "return errors on failure" do
+ put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [99]}}, credentials('jsmith')
+
+ assert_response :unprocessable_entity
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'errors', :child => {:tag => 'error', :content => /member_roles is invalid/}
+ end
+ end
+ end
+
+ context "DELETE" do
+ context "xml" do
+ should "destroy membership" do
+ assert_difference 'Member.count', -1 do
+ delete '/memberships/2.xml', {}, credentials('jsmith')
+
+ assert_response :ok
+ assert_equal '', @response.body
+ end
+ assert_nil Member.find_by_id(2)
+ end
+
+ should "respond with 422 on failure" do
+ assert_no_difference 'Member.count' do
+ # A membership with an inherited role can't be deleted
+ Member.find(2).member_roles.first.update_attribute :inherited_from, 99
+ delete '/memberships/2.xml', {}, credentials('jsmith')
+
+ assert_response :unprocessable_entity
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/test/integration/api_test/news_test.rb b/test/integration/api_test/news_test.rb
new file mode 100644
index 000000000..41be26260
--- /dev/null
+++ b/test/integration/api_test/news_test.rb
@@ -0,0 +1,97 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::NewsTest < Redmine::ApiTest::Base
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules,
+ :news
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "GET /news" do
+ context ".xml" do
+ should "return news" do
+ get '/news.xml'
+
+ assert_tag :tag => 'news',
+ :attributes => {:type => 'array'},
+ :child => {
+ :tag => 'news',
+ :child => {
+ :tag => 'id',
+ :content => '2'
+ }
+ }
+ end
+ end
+
+ context ".json" do
+ should "return news" do
+ get '/news.json'
+
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_kind_of Hash, json
+ assert_kind_of Array, json['news']
+ assert_kind_of Hash, json['news'].first
+ assert_equal 2, json['news'].first['id']
+ end
+ end
+ end
+
+ context "GET /projects/:project_id/news" do
+ context ".xml" do
+ should_allow_api_authentication(:get, "/projects/onlinestore/news.xml")
+
+ should "return news" do
+ get '/projects/ecookbook/news.xml'
+
+ assert_tag :tag => 'news',
+ :attributes => {:type => 'array'},
+ :child => {
+ :tag => 'news',
+ :child => {
+ :tag => 'id',
+ :content => '2'
+ }
+ }
+ end
+ end
+
+ context ".json" do
+ should_allow_api_authentication(:get, "/projects/onlinestore/news.json")
+
+ should "return news" do
+ get '/projects/ecookbook/news.json'
+
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_kind_of Hash, json
+ assert_kind_of Array, json['news']
+ assert_kind_of Hash, json['news'].first
+ assert_equal 2, json['news'].first['id']
+ end
+ end
+ end
+end
diff --git a/test/integration/api_test/projects_test.rb b/test/integration/api_test/projects_test.rb
new file mode 100644
index 000000000..a37af3d37
--- /dev/null
+++ b/test/integration/api_test/projects_test.rb
@@ -0,0 +1,297 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::ProjectsTest < Redmine::ApiTest::Base
+ fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
+ :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
+ :attachments, :custom_fields, :custom_values, :time_entries, :issue_categories
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ set_tmp_attachments_directory
+ end
+
+ context "GET /projects" do
+ context ".xml" do
+ should "return projects" do
+ get '/projects.xml'
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+
+ assert_tag :tag => 'projects',
+ :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}}
+ end
+ end
+
+ context ".json" do
+ should "return projects" do
+ get '/projects.json'
+ assert_response :success
+ assert_equal 'application/json', @response.content_type
+
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_kind_of Hash, json
+ assert_kind_of Array, json['projects']
+ assert_kind_of Hash, json['projects'].first
+ assert json['projects'].first.has_key?('id')
+ end
+ end
+ end
+
+ context "GET /projects/:id" do
+ context ".xml" do
+ # TODO: A private project is needed because should_allow_api_authentication
+ # actually tests that authentication is *required*, not just allowed
+ should_allow_api_authentication(:get, "/projects/2.xml")
+
+ should "return requested project" do
+ get '/projects/1.xml'
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+
+ assert_tag :tag => 'project',
+ :child => {:tag => 'id', :content => '1'}
+ assert_tag :tag => 'custom_field',
+ :attributes => {:name => 'Development status'}, :content => 'Stable'
+
+ assert_no_tag 'trackers'
+ assert_no_tag 'issue_categories'
+ end
+
+ context "with hidden custom fields" do
+ setup do
+ ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
+ end
+
+ should "not display hidden custom fields" do
+ get '/projects/1.xml'
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+
+ assert_no_tag 'custom_field',
+ :attributes => {:name => 'Development status'}
+ end
+ end
+
+ should "return categories with include=issue_categories" do
+ get '/projects/1.xml?include=issue_categories'
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+
+ assert_tag 'issue_categories',
+ :attributes => {:type => 'array'},
+ :child => {
+ :tag => 'issue_category',
+ :attributes => {
+ :id => '2',
+ :name => 'Recipes'
+ }
+ }
+ end
+
+ should "return trackers with include=trackers" do
+ get '/projects/1.xml?include=trackers'
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+
+ assert_tag 'trackers',
+ :attributes => {:type => 'array'},
+ :child => {
+ :tag => 'tracker',
+ :attributes => {
+ :id => '2',
+ :name => 'Feature request'
+ }
+ }
+ end
+ end
+
+ context ".json" do
+ should_allow_api_authentication(:get, "/projects/2.json")
+
+ should "return requested project" do
+ get '/projects/1.json'
+
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_kind_of Hash, json
+ assert_kind_of Hash, json['project']
+ assert_equal 1, json['project']['id']
+ end
+ end
+ end
+
+ context "POST /projects" do
+ context "with valid parameters" do
+ setup do
+ Setting.default_projects_modules = ['issue_tracking', 'repository']
+ @parameters = {:project => {:name => 'API test', :identifier => 'api-test'}}
+ end
+
+ context ".xml" do
+ should_allow_api_authentication(:post,
+ '/projects.xml',
+ {:project => {:name => 'API test', :identifier => 'api-test'}},
+ {:success_code => :created})
+
+
+ should "create a project with the attributes" do
+ assert_difference('Project.count') do
+ post '/projects.xml', @parameters, credentials('admin')
+ end
+
+ project = Project.first(:order => 'id DESC')
+ assert_equal 'API test', project.name
+ assert_equal 'api-test', project.identifier
+ assert_equal ['issue_tracking', 'repository'], project.enabled_module_names.sort
+ assert_equal Tracker.all.size, project.trackers.size
+
+ assert_response :created
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s}
+ end
+
+ should "accept enabled_module_names attribute" do
+ @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']})
+
+ assert_difference('Project.count') do
+ post '/projects.xml', @parameters, credentials('admin')
+ end
+
+ project = Project.first(:order => 'id DESC')
+ assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort
+ end
+
+ should "accept tracker_ids attribute" do
+ @parameters[:project].merge!({:tracker_ids => [1, 3]})
+
+ assert_difference('Project.count') do
+ post '/projects.xml', @parameters, credentials('admin')
+ end
+
+ project = Project.first(:order => 'id DESC')
+ assert_equal [1, 3], project.trackers.map(&:id).sort
+ end
+ end
+ end
+
+ context "with invalid parameters" do
+ setup do
+ @parameters = {:project => {:name => 'API test'}}
+ end
+
+ context ".xml" do
+ should "return errors" do
+ assert_no_difference('Project.count') do
+ post '/projects.xml', @parameters, credentials('admin')
+ end
+
+ assert_response :unprocessable_entity
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"}
+ end
+ end
+ end
+ end
+
+ context "PUT /projects/:id" do
+ context "with valid parameters" do
+ setup do
+ @parameters = {:project => {:name => 'API update'}}
+ end
+
+ context ".xml" do
+ should_allow_api_authentication(:put,
+ '/projects/2.xml',
+ {:project => {:name => 'API update'}},
+ {:success_code => :ok})
+
+ should "update the project" do
+ assert_no_difference 'Project.count' do
+ put '/projects/2.xml', @parameters, credentials('jsmith')
+ end
+ assert_response :ok
+ assert_equal '', @response.body
+ assert_equal 'application/xml', @response.content_type
+ project = Project.find(2)
+ assert_equal 'API update', project.name
+ end
+
+ should "accept enabled_module_names attribute" do
+ @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']})
+
+ assert_no_difference 'Project.count' do
+ put '/projects/2.xml', @parameters, credentials('admin')
+ end
+ assert_response :ok
+ assert_equal '', @response.body
+ project = Project.find(2)
+ assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort
+ end
+
+ should "accept tracker_ids attribute" do
+ @parameters[:project].merge!({:tracker_ids => [1, 3]})
+
+ assert_no_difference 'Project.count' do
+ put '/projects/2.xml', @parameters, credentials('admin')
+ end
+ assert_response :ok
+ assert_equal '', @response.body
+ project = Project.find(2)
+ assert_equal [1, 3], project.trackers.map(&:id).sort
+ end
+ end
+ end
+
+ context "with invalid parameters" do
+ setup do
+ @parameters = {:project => {:name => ''}}
+ end
+
+ context ".xml" do
+ should "return errors" do
+ assert_no_difference('Project.count') do
+ put '/projects/2.xml', @parameters, credentials('admin')
+ end
+
+ assert_response :unprocessable_entity
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
+ end
+ end
+ end
+ end
+
+ context "DELETE /projects/:id" do
+ context ".xml" do
+ should_allow_api_authentication(:delete,
+ '/projects/2.xml',
+ {},
+ {:success_code => :ok})
+
+ should "delete the project" do
+ assert_difference('Project.count',-1) do
+ delete '/projects/2.xml', {}, credentials('admin')
+ end
+ assert_response :ok
+ assert_equal '', @response.body
+ assert_nil Project.find_by_id(2)
+ end
+ end
+ end
+end
diff --git a/test/integration/api_test/queries_test.rb b/test/integration/api_test/queries_test.rb
new file mode 100644
index 000000000..57e8f6659
--- /dev/null
+++ b/test/integration/api_test/queries_test.rb
@@ -0,0 +1,58 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::QueriesTest < Redmine::ApiTest::Base
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules,
+ :queries
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "/queries" do
+ context "GET" do
+
+ should "return queries" do
+ get '/queries.xml'
+
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag :tag => 'queries',
+ :attributes => {:type => 'array'},
+ :child => {
+ :tag => 'query',
+ :child => {
+ :tag => 'id',
+ :content => '4',
+ :sibling => {
+ :tag => 'name',
+ :content => 'Public query for all projects'
+ }
+ }
+ }
+ end
+ end
+ end
+end
diff --git a/test/integration/api_test/roles_test.rb b/test/integration/api_test/roles_test.rb
new file mode 100644
index 000000000..87c4a9a18
--- /dev/null
+++ b/test/integration/api_test/roles_test.rb
@@ -0,0 +1,90 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::RolesTest < Redmine::ApiTest::Base
+ fixtures :roles
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "/roles" do
+ context "GET" do
+ context "xml" do
+ should "return the roles" do
+ get '/roles.xml'
+
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_equal 3, assigns(:roles).size
+
+ assert_tag :tag => 'roles',
+ :attributes => {:type => 'array'},
+ :child => {
+ :tag => 'role',
+ :child => {
+ :tag => 'id',
+ :content => '2',
+ :sibling => {
+ :tag => 'name',
+ :content => 'Developer'
+ }
+ }
+ }
+ end
+ end
+
+ context "json" do
+ should "return the roles" do
+ get '/roles.json'
+
+ assert_response :success
+ assert_equal 'application/json', @response.content_type
+ assert_equal 3, assigns(:roles).size
+
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_kind_of Hash, json
+ assert_kind_of Array, json['roles']
+ assert_include({'id' => 2, 'name' => 'Developer'}, json['roles'])
+ end
+ end
+ end
+ end
+
+ context "/roles/:id" do
+ context "GET" do
+ context "xml" do
+ should "return the role" do
+ get '/roles/1.xml'
+
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+
+ assert_select 'role' do
+ assert_select 'name', :text => 'Manager'
+ assert_select 'role permissions[type=array]' do
+ assert_select 'permission', Role.find(1).permissions.size
+ assert_select 'permission', :text => 'view_issues'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/test/integration/api_test/time_entries_test.rb b/test/integration/api_test/time_entries_test.rb
new file mode 100644
index 000000000..4c87a57b6
--- /dev/null
+++ b/test/integration/api_test/time_entries_test.rb
@@ -0,0 +1,164 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::TimeEntriesTest < Redmine::ApiTest::Base
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules,
+ :time_entries
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "GET /time_entries.xml" do
+ should "return time entries" do
+ get '/time_entries.xml', {}, credentials('jsmith')
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag :tag => 'time_entries',
+ :child => {:tag => 'time_entry', :child => {:tag => 'id', :content => '2'}}
+ end
+
+ context "with limit" do
+ should "return limited results" do
+ get '/time_entries.xml?limit=2', {}, credentials('jsmith')
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag :tag => 'time_entries',
+ :children => {:count => 2}
+ end
+ end
+ end
+
+ context "GET /time_entries/2.xml" do
+ should "return requested time entry" do
+ get '/time_entries/2.xml', {}, credentials('jsmith')
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag :tag => 'time_entry',
+ :child => {:tag => 'id', :content => '2'}
+ end
+ end
+
+ context "POST /time_entries.xml" do
+ context "with issue_id" do
+ should "return create time entry" do
+ assert_difference 'TimeEntry.count' do
+ post '/time_entries.xml', {:time_entry => {:issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith')
+ end
+ assert_response :created
+ assert_equal 'application/xml', @response.content_type
+
+ entry = TimeEntry.first(:order => 'id DESC')
+ assert_equal 'jsmith', entry.user.login
+ assert_equal Issue.find(1), entry.issue
+ assert_equal Project.find(1), entry.project
+ assert_equal Date.parse('2010-12-02'), entry.spent_on
+ assert_equal 3.5, entry.hours
+ assert_equal TimeEntryActivity.find(11), entry.activity
+ end
+
+ should "accept custom fields" do
+ field = TimeEntryCustomField.create!(:name => 'Test', :field_format => 'string')
+
+ assert_difference 'TimeEntry.count' do
+ post '/time_entries.xml', {:time_entry => {
+ :issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11', :custom_fields => [{:id => field.id.to_s, :value => 'accepted'}]
+ }}, credentials('jsmith')
+ end
+ assert_response :created
+ assert_equal 'application/xml', @response.content_type
+
+ entry = TimeEntry.first(:order => 'id DESC')
+ assert_equal 'accepted', entry.custom_field_value(field)
+ end
+ end
+
+ context "with project_id" do
+ should "return create time entry" do
+ assert_difference 'TimeEntry.count' do
+ post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith')
+ end
+ assert_response :created
+ assert_equal 'application/xml', @response.content_type
+
+ entry = TimeEntry.first(:order => 'id DESC')
+ assert_equal 'jsmith', entry.user.login
+ assert_nil entry.issue
+ assert_equal Project.find(1), entry.project
+ assert_equal Date.parse('2010-12-02'), entry.spent_on
+ assert_equal 3.5, entry.hours
+ assert_equal TimeEntryActivity.find(11), entry.activity
+ end
+ end
+
+ context "with invalid parameters" do
+ should "return errors" do
+ assert_no_difference 'TimeEntry.count' do
+ post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :activity_id => '11'}}, credentials('jsmith')
+ end
+ assert_response :unprocessable_entity
+ assert_equal 'application/xml', @response.content_type
+
+ assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"}
+ end
+ end
+ end
+
+ context "PUT /time_entries/2.xml" do
+ context "with valid parameters" do
+ should "update time entry" do
+ assert_no_difference 'TimeEntry.count' do
+ put '/time_entries/2.xml', {:time_entry => {:comments => 'API Update'}}, credentials('jsmith')
+ end
+ assert_response :ok
+ assert_equal '', @response.body
+ assert_equal 'API Update', TimeEntry.find(2).comments
+ end
+ end
+
+ context "with invalid parameters" do
+ should "return errors" do
+ assert_no_difference 'TimeEntry.count' do
+ put '/time_entries/2.xml', {:time_entry => {:hours => '', :comments => 'API Update'}}, credentials('jsmith')
+ end
+ assert_response :unprocessable_entity
+ assert_equal 'application/xml', @response.content_type
+
+ assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"}
+ end
+ end
+ end
+
+ context "DELETE /time_entries/2.xml" do
+ should "destroy time entry" do
+ assert_difference 'TimeEntry.count', -1 do
+ delete '/time_entries/2.xml', {}, credentials('jsmith')
+ end
+ assert_response :ok
+ assert_equal '', @response.body
+ assert_nil TimeEntry.find_by_id(2)
+ end
+ end
+end
diff --git a/test/integration/api_test/token_authentication_test.rb b/test/integration/api_test/token_authentication_test.rb
new file mode 100644
index 000000000..a20f1bbb6
--- /dev/null
+++ b/test/integration/api_test/token_authentication_test.rb
@@ -0,0 +1,49 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::TokenAuthenticationTest < Redmine::ApiTest::Base
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ Setting.login_required = '1'
+ end
+
+ def teardown
+ Setting.rest_api_enabled = '0'
+ Setting.login_required = '0'
+ end
+
+ # Using the NewsController because it's a simple API.
+ context "get /news" do
+ context "in :xml format" do
+ should_allow_key_based_auth(:get, "/news.xml")
+ end
+
+ context "in :json format" do
+ should_allow_key_based_auth(:get, "/news.json")
+ end
+ end
+end
diff --git a/test/integration/api_test/trackers_test.rb b/test/integration/api_test/trackers_test.rb
new file mode 100644
index 000000000..8141c952e
--- /dev/null
+++ b/test/integration/api_test/trackers_test.rb
@@ -0,0 +1,51 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::TrackersTest < Redmine::ApiTest::Base
+ fixtures :trackers
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "/trackers" do
+ context "GET" do
+
+ should "return trackers" do
+ get '/trackers.xml'
+
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag :tag => 'trackers',
+ :attributes => {:type => 'array'},
+ :child => {
+ :tag => 'tracker',
+ :child => {
+ :tag => 'id',
+ :content => '2',
+ :sibling => {
+ :tag => 'name',
+ :content => 'Feature request'
+ }
+ }
+ }
+ end
+ end
+ end
+end
diff --git a/test/integration/api_test/users_test.rb b/test/integration/api_test/users_test.rb
new file mode 100644
index 000000000..7f72872a2
--- /dev/null
+++ b/test/integration/api_test/users_test.rb
@@ -0,0 +1,371 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::UsersTest < Redmine::ApiTest::Base
+ fixtures :users, :members, :member_roles, :roles, :projects
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "GET /users" do
+ should_allow_api_authentication(:get, "/users.xml")
+ should_allow_api_authentication(:get, "/users.json")
+ end
+
+ context "GET /users/2" do
+ context ".xml" do
+ should "return requested user" do
+ get '/users/2.xml'
+
+ assert_response :success
+ assert_tag :tag => 'user',
+ :child => {:tag => 'id', :content => '2'}
+ end
+
+ context "with include=memberships" do
+ should "include memberships" do
+ get '/users/2.xml?include=memberships'
+
+ assert_response :success
+ assert_tag :tag => 'memberships',
+ :parent => {:tag => 'user'},
+ :children => {:count => 1}
+ end
+ end
+ end
+
+ context ".json" do
+ should "return requested user" do
+ get '/users/2.json'
+
+ assert_response :success
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_kind_of Hash, json
+ assert_kind_of Hash, json['user']
+ assert_equal 2, json['user']['id']
+ end
+
+ context "with include=memberships" do
+ should "include memberships" do
+ get '/users/2.json?include=memberships'
+
+ assert_response :success
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_kind_of Array, json['user']['memberships']
+ assert_equal [{
+ "id"=>1,
+ "project"=>{"name"=>"eCookbook", "id"=>1},
+ "roles"=>[{"name"=>"Manager", "id"=>1}]
+ }], json['user']['memberships']
+ end
+ end
+ end
+ end
+
+ context "GET /users/current" do
+ context ".xml" do
+ should "require authentication" do
+ get '/users/current.xml'
+
+ assert_response 401
+ end
+
+ should "return current user" do
+ get '/users/current.xml', {}, credentials('jsmith')
+
+ assert_tag :tag => 'user',
+ :child => {:tag => 'id', :content => '2'}
+ end
+ end
+ end
+
+ test "GET /users/:id should not return login for other user" do
+ get '/users/3.xml', {}, credentials('jsmith')
+ assert_response :success
+ assert_no_tag 'user', :child => {:tag => 'login'}
+ end
+
+ test "GET /users/:id should return login for current user" do
+ get '/users/2.xml', {}, credentials('jsmith')
+ assert_response :success
+ assert_tag 'user', :child => {:tag => 'login', :content => 'jsmith'}
+ end
+
+ test "GET /users/:id should not return api_key for other user" do
+ get '/users/3.xml', {}, credentials('jsmith')
+ assert_response :success
+ assert_no_tag 'user', :child => {:tag => 'api_key'}
+ end
+
+ test "GET /users/:id should return api_key for current user" do
+ get '/users/2.xml', {}, credentials('jsmith')
+ assert_response :success
+ assert_tag 'user', :child => {:tag => 'api_key', :content => User.find(2).api_key}
+ end
+
+ context "POST /users" do
+ context "with valid parameters" do
+ setup do
+ @parameters = {
+ :user => {
+ :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname',
+ :mail => 'foo@example.net', :password => 'secret123',
+ :mail_notification => 'only_assigned'
+ }
+ }
+ end
+
+ context ".xml" do
+ should_allow_api_authentication(:post,
+ '/users.xml',
+ {:user => {
+ :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname',
+ :mail => 'foo@example.net', :password => 'secret123'
+ }},
+ {:success_code => :created})
+
+ should "create a user with the attributes" do
+ assert_difference('User.count') do
+ post '/users.xml', @parameters, credentials('admin')
+ end
+
+ user = User.first(:order => 'id DESC')
+ assert_equal 'foo', user.login
+ assert_equal 'Firstname', user.firstname
+ assert_equal 'Lastname', user.lastname
+ assert_equal 'foo@example.net', user.mail
+ assert_equal 'only_assigned', user.mail_notification
+ assert !user.admin?
+ assert user.check_password?('secret123')
+
+ assert_response :created
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'user', :child => {:tag => 'id', :content => user.id.to_s}
+ end
+ end
+
+ context ".json" do
+ should_allow_api_authentication(:post,
+ '/users.json',
+ {:user => {
+ :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname',
+ :mail => 'foo@example.net'
+ }},
+ {:success_code => :created})
+
+ should "create a user with the attributes" do
+ assert_difference('User.count') do
+ post '/users.json', @parameters, credentials('admin')
+ end
+
+ user = User.first(:order => 'id DESC')
+ assert_equal 'foo', user.login
+ assert_equal 'Firstname', user.firstname
+ assert_equal 'Lastname', user.lastname
+ assert_equal 'foo@example.net', user.mail
+ assert !user.admin?
+
+ assert_response :created
+ assert_equal 'application/json', @response.content_type
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_kind_of Hash, json
+ assert_kind_of Hash, json['user']
+ assert_equal user.id, json['user']['id']
+ end
+ end
+ end
+
+ context "with invalid parameters" do
+ setup do
+ @parameters = {:user => {:login => 'foo', :lastname => 'Lastname', :mail => 'foo'}}
+ end
+
+ context ".xml" do
+ should "return errors" do
+ assert_no_difference('User.count') do
+ post '/users.xml', @parameters, credentials('admin')
+ end
+
+ assert_response :unprocessable_entity
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'errors', :child => {
+ :tag => 'error',
+ :content => "First name can't be blank"
+ }
+ end
+ end
+
+ context ".json" do
+ should "return errors" do
+ assert_no_difference('User.count') do
+ post '/users.json', @parameters, credentials('admin')
+ end
+
+ assert_response :unprocessable_entity
+ assert_equal 'application/json', @response.content_type
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_kind_of Hash, json
+ assert json.has_key?('errors')
+ assert_kind_of Array, json['errors']
+ end
+ end
+ end
+ end
+
+ context "PUT /users/2" do
+ context "with valid parameters" do
+ setup do
+ @parameters = {
+ :user => {
+ :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed',
+ :mail => 'jsmith@somenet.foo'
+ }
+ }
+ end
+
+ context ".xml" do
+ should_allow_api_authentication(:put,
+ '/users/2.xml',
+ {:user => {
+ :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed',
+ :mail => 'jsmith@somenet.foo'
+ }},
+ {:success_code => :ok})
+
+ should "update user with the attributes" do
+ assert_no_difference('User.count') do
+ put '/users/2.xml', @parameters, credentials('admin')
+ end
+
+ user = User.find(2)
+ assert_equal 'jsmith', user.login
+ assert_equal 'John', user.firstname
+ assert_equal 'Renamed', user.lastname
+ assert_equal 'jsmith@somenet.foo', user.mail
+ assert !user.admin?
+
+ assert_response :ok
+ assert_equal '', @response.body
+ end
+ end
+
+ context ".json" do
+ should_allow_api_authentication(:put,
+ '/users/2.json',
+ {:user => {
+ :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed',
+ :mail => 'jsmith@somenet.foo'
+ }},
+ {:success_code => :ok})
+
+ should "update user with the attributes" do
+ assert_no_difference('User.count') do
+ put '/users/2.json', @parameters, credentials('admin')
+ end
+
+ user = User.find(2)
+ assert_equal 'jsmith', user.login
+ assert_equal 'John', user.firstname
+ assert_equal 'Renamed', user.lastname
+ assert_equal 'jsmith@somenet.foo', user.mail
+ assert !user.admin?
+
+ assert_response :ok
+ assert_equal '', @response.body
+ end
+ end
+ end
+
+ context "with invalid parameters" do
+ setup do
+ @parameters = {
+ :user => {
+ :login => 'jsmith', :firstname => '', :lastname => 'Lastname',
+ :mail => 'foo'
+ }
+ }
+ end
+
+ context ".xml" do
+ should "return errors" do
+ assert_no_difference('User.count') do
+ put '/users/2.xml', @parameters, credentials('admin')
+ end
+
+ assert_response :unprocessable_entity
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'errors', :child => {
+ :tag => 'error',
+ :content => "First name can't be blank"
+ }
+ end
+ end
+
+ context ".json" do
+ should "return errors" do
+ assert_no_difference('User.count') do
+ put '/users/2.json', @parameters, credentials('admin')
+ end
+
+ assert_response :unprocessable_entity
+ assert_equal 'application/json', @response.content_type
+ json = ActiveSupport::JSON.decode(response.body)
+ assert_kind_of Hash, json
+ assert json.has_key?('errors')
+ assert_kind_of Array, json['errors']
+ end
+ end
+ end
+ end
+
+ context "DELETE /users/2" do
+ context ".xml" do
+ should_allow_api_authentication(:delete,
+ '/users/2.xml',
+ {},
+ {:success_code => :ok})
+
+ should "delete user" do
+ assert_difference('User.count', -1) do
+ delete '/users/2.xml', {}, credentials('admin')
+ end
+
+ assert_response :ok
+ assert_equal '', @response.body
+ end
+ end
+
+ context ".json" do
+ should_allow_api_authentication(:delete,
+ '/users/2.xml',
+ {},
+ {:success_code => :ok})
+
+ should "delete user" do
+ assert_difference('User.count', -1) do
+ delete '/users/2.json', {}, credentials('admin')
+ end
+
+ assert_response :ok
+ assert_equal '', @response.body
+ end
+ end
+ end
+end
diff --git a/test/integration/api_test/versions_test.rb b/test/integration/api_test/versions_test.rb
new file mode 100644
index 000000000..51f8e2b33
--- /dev/null
+++ b/test/integration/api_test/versions_test.rb
@@ -0,0 +1,158 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::VersionsTest < Redmine::ApiTest::Base
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules,
+ :versions
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ context "/projects/:project_id/versions" do
+ context "GET" do
+ should "return project versions" do
+ get '/projects/1/versions.xml'
+
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_tag :tag => 'versions',
+ :attributes => {:type => 'array'},
+ :child => {
+ :tag => 'version',
+ :child => {
+ :tag => 'id',
+ :content => '2',
+ :sibling => {
+ :tag => 'name',
+ :content => '1.0'
+ }
+ }
+ }
+ end
+ end
+
+ context "POST" do
+ should "create the version" do
+ assert_difference 'Version.count' do
+ post '/projects/1/versions.xml', {:version => {:name => 'API test'}}, credentials('jsmith')
+ end
+
+ version = Version.first(:order => 'id DESC')
+ assert_equal 'API test', version.name
+
+ assert_response :created
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s}
+ end
+
+ should "create the version with due date" do
+ assert_difference 'Version.count' do
+ post '/projects/1/versions.xml', {:version => {:name => 'API test', :due_date => '2012-01-24'}}, credentials('jsmith')
+ end
+
+ version = Version.first(:order => 'id DESC')
+ assert_equal 'API test', version.name
+ assert_equal Date.parse('2012-01-24'), version.due_date
+
+ assert_response :created
+ assert_equal 'application/xml', @response.content_type
+ assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s}
+ end
+
+ should "create the version with custom fields" do
+ field = VersionCustomField.generate!
+
+ assert_difference 'Version.count' do
+ post '/projects/1/versions.xml', {
+ :version => {
+ :name => 'API test',
+ :custom_fields => [
+ {'id' => field.id.to_s, 'value' => 'Some value'}
+ ]
+ }
+ }, credentials('jsmith')
+ end
+
+ version = Version.first(:order => 'id DESC')
+ assert_equal 'API test', version.name
+ assert_equal 'Some value', version.custom_field_value(field)
+
+ assert_response :created
+ assert_equal 'application/xml', @response.content_type
+ assert_select 'version>custom_fields>custom_field[id=?]>value', field.id.to_s, 'Some value'
+ end
+
+ context "with failure" do
+ should "return the errors" do
+ assert_no_difference('Version.count') do
+ post '/projects/1/versions.xml', {:version => {:name => ''}}, credentials('jsmith')
+ end
+
+ assert_response :unprocessable_entity
+ assert_tag :errors, :child => {:tag => 'error', :content => "Name can't be blank"}
+ end
+ end
+ end
+ end
+
+ context "/versions/:id" do
+ context "GET" do
+ should "return the version" do
+ get '/versions/2.xml'
+
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ assert_select 'version' do
+ assert_select 'id', :text => '2'
+ assert_select 'name', :text => '1.0'
+ assert_select 'sharing', :text => 'none'
+ end
+ end
+ end
+
+ context "PUT" do
+ should "update the version" do
+ put '/versions/2.xml', {:version => {:name => 'API update'}}, credentials('jsmith')
+
+ assert_response :ok
+ assert_equal '', @response.body
+ assert_equal 'API update', Version.find(2).name
+ end
+ end
+
+ context "DELETE" do
+ should "destroy the version" do
+ assert_difference 'Version.count', -1 do
+ delete '/versions/3.xml', {}, credentials('jsmith')
+ end
+
+ assert_response :ok
+ assert_equal '', @response.body
+ assert_nil Version.find_by_id(3)
+ end
+ end
+ end
+end
diff --git a/test/integration/api_test/wiki_pages_test.rb b/test/integration/api_test/wiki_pages_test.rb
new file mode 100644
index 000000000..02c764fa7
--- /dev/null
+++ b/test/integration/api_test/wiki_pages_test.rb
@@ -0,0 +1,193 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class Redmine::ApiTest::WikiPagesTest < Redmine::ApiTest::Base
+ fixtures :projects, :users, :roles, :members, :member_roles,
+ :enabled_modules, :wikis, :wiki_pages, :wiki_contents,
+ :wiki_content_versions, :attachments
+
+ def setup
+ Setting.rest_api_enabled = '1'
+ end
+
+ test "GET /projects/:project_id/wiki/index.xml should return wiki pages" do
+ get '/projects/ecookbook/wiki/index.xml'
+ assert_response 200
+ assert_equal 'application/xml', response.content_type
+ assert_select 'wiki_pages[type=array]' do
+ assert_select 'wiki_page', :count => Wiki.find(1).pages.count
+ assert_select 'wiki_page' do
+ assert_select 'title', :text => 'CookBook_documentation'
+ assert_select 'version', :text => '3'
+ assert_select 'created_on'
+ assert_select 'updated_on'
+ end
+ assert_select 'wiki_page' do
+ assert_select 'title', :text => 'Page_with_an_inline_image'
+ assert_select 'parent[title=?]', 'CookBook_documentation'
+ end
+ end
+ end
+
+ test "GET /projects/:project_id/wiki/:title.xml should return wiki page" do
+ get '/projects/ecookbook/wiki/CookBook_documentation.xml'
+ assert_response 200
+ assert_equal 'application/xml', response.content_type
+ assert_select 'wiki_page' do
+ assert_select 'title', :text => 'CookBook_documentation'
+ assert_select 'version', :text => '3'
+ assert_select 'text'
+ assert_select 'author'
+ assert_select 'comments'
+ assert_select 'created_on'
+ assert_select 'updated_on'
+ end
+ end
+
+ test "GET /projects/:project_id/wiki/:title.xml?include=attachments should include attachments" do
+ get '/projects/ecookbook/wiki/Page_with_an_inline_image.xml?include=attachments'
+ assert_response 200
+ assert_equal 'application/xml', response.content_type
+ assert_select 'wiki_page' do
+ assert_select 'title', :text => 'Page_with_an_inline_image'
+ assert_select 'attachments[type=array]' do
+ assert_select 'attachment' do
+ assert_select 'id', :text => '3'
+ assert_select 'filename', :text => 'logo.gif'
+ end
+ end
+ end
+ end
+
+ test "GET /projects/:project_id/wiki/:title.xml with unknown title and edit permission should respond with 404" do
+ get '/projects/ecookbook/wiki/Invalid_Page.xml', {}, credentials('jsmith')
+ assert_response 404
+ assert_equal 'application/xml', response.content_type
+ end
+
+ test "GET /projects/:project_id/wiki/:title/:version.xml should return wiki page version" do
+ get '/projects/ecookbook/wiki/CookBook_documentation/2.xml'
+ assert_response 200
+ assert_equal 'application/xml', response.content_type
+ assert_select 'wiki_page' do
+ assert_select 'title', :text => 'CookBook_documentation'
+ assert_select 'version', :text => '2'
+ assert_select 'text'
+ assert_select 'author'
+ assert_select 'created_on'
+ assert_select 'updated_on'
+ end
+ end
+
+ test "GET /projects/:project_id/wiki/:title/:version.xml without permission should be denied" do
+ Role.anonymous.remove_permission! :view_wiki_edits
+
+ get '/projects/ecookbook/wiki/CookBook_documentation/2.xml'
+ assert_response 401
+ assert_equal 'application/xml', response.content_type
+ end
+
+ test "PUT /projects/:project_id/wiki/:title.xml should update wiki page" do
+ assert_no_difference 'WikiPage.count' do
+ assert_difference 'WikiContent::Version.count' do
+ put '/projects/ecookbook/wiki/CookBook_documentation.xml',
+ {:wiki_page => {:text => 'New content from API', :comments => 'API update'}},
+ credentials('jsmith')
+ assert_response 200
+ end
+ end
+
+ page = WikiPage.find(1)
+ assert_equal 'New content from API', page.content.text
+ assert_equal 4, page.content.version
+ assert_equal 'API update', page.content.comments
+ assert_equal 'jsmith', page.content.author.login
+ end
+
+ test "PUT /projects/:project_id/wiki/:title.xml with current versino should update wiki page" do
+ assert_no_difference 'WikiPage.count' do
+ assert_difference 'WikiContent::Version.count' do
+ put '/projects/ecookbook/wiki/CookBook_documentation.xml',
+ {:wiki_page => {:text => 'New content from API', :comments => 'API update', :version => '3'}},
+ credentials('jsmith')
+ assert_response 200
+ end
+ end
+
+ page = WikiPage.find(1)
+ assert_equal 'New content from API', page.content.text
+ assert_equal 4, page.content.version
+ assert_equal 'API update', page.content.comments
+ assert_equal 'jsmith', page.content.author.login
+ end
+
+ test "PUT /projects/:project_id/wiki/:title.xml with stale version should respond with 409" do
+ assert_no_difference 'WikiPage.count' do
+ assert_no_difference 'WikiContent::Version.count' do
+ put '/projects/ecookbook/wiki/CookBook_documentation.xml',
+ {:wiki_page => {:text => 'New content from API', :comments => 'API update', :version => '2'}},
+ credentials('jsmith')
+ assert_response 409
+ end
+ end
+ end
+
+ test "PUT /projects/:project_id/wiki/:title.xml should create the page if it does not exist" do
+ assert_difference 'WikiPage.count' do
+ assert_difference 'WikiContent::Version.count' do
+ put '/projects/ecookbook/wiki/New_page_from_API.xml',
+ {:wiki_page => {:text => 'New content from API', :comments => 'API create'}},
+ credentials('jsmith')
+ assert_response 201
+ end
+ end
+
+ page = WikiPage.order('id DESC').first
+ assert_equal 'New_page_from_API', page.title
+ assert_equal 'New content from API', page.content.text
+ assert_equal 1, page.content.version
+ assert_equal 'API create', page.content.comments
+ assert_equal 'jsmith', page.content.author.login
+ assert_nil page.parent
+ end
+
+ test "PUT /projects/:project_id/wiki/:title.xml with parent" do
+ assert_difference 'WikiPage.count' do
+ assert_difference 'WikiContent::Version.count' do
+ put '/projects/ecookbook/wiki/New_subpage_from_API.xml',
+ {:wiki_page => {:parent_title => 'CookBook_documentation', :text => 'New content from API', :comments => 'API create'}},
+ credentials('jsmith')
+ assert_response 201
+ end
+ end
+
+ page = WikiPage.order('id DESC').first
+ assert_equal 'New_subpage_from_API', page.title
+ assert_equal WikiPage.find(1), page.parent
+ end
+
+ test "DELETE /projects/:project_id/wiki/:title.xml should destroy the page" do
+ assert_difference 'WikiPage.count', -1 do
+ delete '/projects/ecookbook/wiki/CookBook_documentation.xml', {}, credentials('jsmith')
+ assert_response 200
+ end
+
+ assert_nil WikiPage.find_by_id(1)
+ end
+end
diff --git a/test/integration/application_test.rb b/test/integration/application_test.rb
new file mode 100644
index 000000000..409dcfd22
--- /dev/null
+++ b/test/integration/application_test.rb
@@ -0,0 +1,67 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class ApplicationTest < ActionController::IntegrationTest
+ include Redmine::I18n
+
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules
+
+ def test_set_localization
+ Setting.default_language = 'en'
+
+ # a french user
+ get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'
+ assert_response :success
+ assert_tag :tag => 'h2', :content => 'Projets'
+ assert_equal :fr, current_language
+
+ # then an italien user
+ get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'it;q=0.8,en-us;q=0.5,en;q=0.3'
+ assert_response :success
+ assert_tag :tag => 'h2', :content => 'Progetti'
+ assert_equal :it, current_language
+
+ # not a supported language: default language should be used
+ get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'zz'
+ assert_response :success
+ assert_tag :tag => 'h2', :content => 'Projects'
+ end
+
+ def test_token_based_access_should_not_start_session
+ # issue of a private project
+ get 'issues/4.atom'
+ assert_response 302
+
+ rss_key = User.find(2).rss_key
+ get "issues/4.atom?key=#{rss_key}"
+ assert_response 200
+ assert_nil session[:user_id]
+ end
+
+ def test_missing_template_should_respond_with_404
+ get '/login.png'
+ assert_response 404
+ end
+end
diff --git a/test/integration/attachments_test.rb b/test/integration/attachments_test.rb
new file mode 100644
index 000000000..7c21c822d
--- /dev/null
+++ b/test/integration/attachments_test.rb
@@ -0,0 +1,132 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class AttachmentsTest < ActionController::IntegrationTest
+ fixtures :projects, :enabled_modules,
+ :users, :roles, :members, :member_roles,
+ :trackers, :projects_trackers,
+ :issue_statuses, :enumerations
+
+ def test_upload_as_js_and_attach_to_an_issue
+ log_user('jsmith', 'jsmith')
+
+ token = ajax_upload('myupload.txt', 'File content')
+
+ assert_difference 'Issue.count' do
+ post '/projects/ecookbook/issues', {
+ :issue => {:tracker_id => 1, :subject => 'Issue with upload'},
+ :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
+ }
+ assert_response 302
+ end
+
+ issue = Issue.order('id DESC').first
+ assert_equal 'Issue with upload', issue.subject
+ assert_equal 1, issue.attachments.count
+
+ attachment = issue.attachments.first
+ assert_equal 'myupload.txt', attachment.filename
+ assert_equal 'My uploaded file', attachment.description
+ assert_equal 'File content'.length, attachment.filesize
+ end
+
+ def test_upload_as_js_and_preview_as_inline_attachment
+ log_user('jsmith', 'jsmith')
+
+ token = ajax_upload('myupload.jpg', 'JPEG content')
+
+ post '/issues/preview/new/ecookbook', {
+ :issue => {:tracker_id => 1, :description => 'Inline upload: !myupload.jpg!'},
+ :attachments => {'1' => {:filename => 'myupload.jpg', :description => 'My uploaded file', :token => token}}
+ }
+ assert_response :success
+
+ attachment_path = response.body.match(%r{
{:tracker_id => 1, :subject => ''},
+ :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
+ }
+ assert_response :success
+ end
+ assert_select 'input[type=hidden][name=?][value=?]', 'attachments[p0][token]', token
+ assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'myupload.txt'
+ assert_select 'input[name=?][value=?]', 'attachments[p0][description]', 'My uploaded file'
+
+ assert_difference 'Issue.count' do
+ post '/projects/ecookbook/issues', {
+ :issue => {:tracker_id => 1, :subject => 'Issue with upload'},
+ :attachments => {'p0' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
+ }
+ assert_response 302
+ end
+
+ issue = Issue.order('id DESC').first
+ assert_equal 'Issue with upload', issue.subject
+ assert_equal 1, issue.attachments.count
+
+ attachment = issue.attachments.first
+ assert_equal 'myupload.txt', attachment.filename
+ assert_equal 'My uploaded file', attachment.description
+ assert_equal 'File content'.length, attachment.filesize
+ end
+
+ def test_upload_as_js_and_destroy
+ log_user('jsmith', 'jsmith')
+
+ token = ajax_upload('myupload.txt', 'File content')
+
+ attachment = Attachment.order('id DESC').first
+ attachment_path = "/attachments/#{attachment.id}.js?attachment_id=1"
+ assert_include "href: '#{attachment_path}'", response.body, "Path to attachment: #{attachment_path} not found in response:\n#{response.body}"
+
+ assert_difference 'Attachment.count', -1 do
+ delete attachment_path
+ assert_response :success
+ end
+
+ assert_include "$('#attachments_1').remove();", response.body
+ end
+
+ private
+
+ def ajax_upload(filename, content, attachment_id=1)
+ assert_difference 'Attachment.count' do
+ post "/uploads.js?attachment_id=#{attachment_id}&filename=#{filename}", content, {"CONTENT_TYPE" => 'application/octet-stream'}
+ assert_response :success
+ assert_equal 'text/javascript', response.content_type
+ end
+
+ token = response.body.match(/\.val\('(\d+\.[0-9a-f]+)'\)/)[1]
+ assert_not_nil token, "No upload token found in response:\n#{response.body}"
+ token
+ end
+end
diff --git a/test/integration/issues_test.rb b/test/integration/issues_test.rb
new file mode 100644
index 000000000..e0659b43c
--- /dev/null
+++ b/test/integration/issues_test.rb
@@ -0,0 +1,220 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class IssuesTest < ActionController::IntegrationTest
+ fixtures :projects,
+ :users,
+ :roles,
+ :members,
+ :member_roles,
+ :trackers,
+ :projects_trackers,
+ :enabled_modules,
+ :issue_statuses,
+ :issues,
+ :enumerations,
+ :custom_fields,
+ :custom_values,
+ :custom_fields_trackers
+
+ # create an issue
+ def test_add_issue
+ log_user('jsmith', 'jsmith')
+ get 'projects/1/issues/new', :tracker_id => '1'
+ assert_response :success
+ assert_template 'issues/new'
+
+ post 'projects/1/issues', :tracker_id => "1",
+ :issue => { :start_date => "2006-12-26",
+ :priority_id => "4",
+ :subject => "new test issue",
+ :category_id => "",
+ :description => "new issue",
+ :done_ratio => "0",
+ :due_date => "",
+ :assigned_to_id => "" },
+ :custom_fields => {'2' => 'Value for field 2'}
+ # find created issue
+ issue = Issue.find_by_subject("new test issue")
+ assert_kind_of Issue, issue
+
+ # check redirection
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
+ follow_redirect!
+ assert_equal issue, assigns(:issue)
+
+ # check issue attributes
+ assert_equal 'jsmith', issue.author.login
+ assert_equal 1, issue.project.id
+ assert_equal 1, issue.status.id
+ end
+
+ # add then remove 2 attachments to an issue
+ def test_issue_attachments
+ log_user('jsmith', 'jsmith')
+ set_tmp_attachments_directory
+
+ put 'issues/1',
+ :notes => 'Some notes',
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}}
+ assert_redirected_to "/issues/1"
+
+ # make sure attachment was saved
+ attachment = Issue.find(1).attachments.find_by_filename("testfile.txt")
+ assert_kind_of Attachment, attachment
+ assert_equal Issue.find(1), attachment.container
+ assert_equal 'This is an attachment', attachment.description
+ # verify the size of the attachment stored in db
+ #assert_equal file_data_1.length, attachment.filesize
+ # verify that the attachment was written to disk
+ assert File.exist?(attachment.diskfile)
+
+ # remove the attachments
+ Issue.find(1).attachments.each(&:destroy)
+ assert_equal 0, Issue.find(1).attachments.length
+ end
+
+ def test_other_formats_links_on_index
+ get '/projects/ecookbook/issues'
+
+ %w(Atom PDF CSV).each do |format|
+ assert_tag :a, :content => format,
+ :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
+ :rel => 'nofollow' }
+ end
+ end
+
+ def test_other_formats_links_on_index_without_project_id_in_url
+ get '/issues', :project_id => 'ecookbook'
+
+ %w(Atom PDF CSV).each do |format|
+ assert_tag :a, :content => format,
+ :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
+ :rel => 'nofollow' }
+ end
+ end
+
+ def test_pagination_links_on_index
+ Setting.per_page_options = '2'
+ get '/projects/ecookbook/issues'
+
+ assert_tag :a, :content => '2',
+ :attributes => { :href => '/projects/ecookbook/issues?page=2' }
+
+ end
+
+ def test_pagination_links_on_index_without_project_id_in_url
+ Setting.per_page_options = '2'
+ get '/issues', :project_id => 'ecookbook'
+
+ assert_tag :a, :content => '2',
+ :attributes => { :href => '/projects/ecookbook/issues?page=2' }
+
+ end
+
+ def test_issue_with_user_custom_field
+ @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true, :trackers => Tracker.all)
+ Role.anonymous.add_permission! :add_issues, :edit_issues
+ users = Project.find(1).users
+ tester = users.first
+
+ # Issue form
+ get '/projects/ecookbook/issues/new'
+ assert_response :success
+ assert_tag :select,
+ :attributes => {:name => "issue[custom_field_values][#{@field.id}]"},
+ :children => {:count => (users.size + 1)}, # +1 for blank value
+ :child => {
+ :tag => 'option',
+ :attributes => {:value => tester.id.to_s},
+ :content => tester.name
+ }
+
+ # Create issue
+ assert_difference 'Issue.count' do
+ post '/projects/ecookbook/issues',
+ :issue => {
+ :tracker_id => '1',
+ :priority_id => '4',
+ :subject => 'Issue with user custom field',
+ :custom_field_values => {@field.id.to_s => users.first.id.to_s}
+ }
+ end
+ issue = Issue.first(:order => 'id DESC')
+ assert_response 302
+
+ # Issue view
+ follow_redirect!
+ assert_tag :th,
+ :content => /Tester/,
+ :sibling => {
+ :tag => 'td',
+ :content => tester.name
+ }
+ assert_tag :select,
+ :attributes => {:name => "issue[custom_field_values][#{@field.id}]"},
+ :children => {:count => (users.size + 1)}, # +1 for blank value
+ :child => {
+ :tag => 'option',
+ :attributes => {:value => tester.id.to_s, :selected => 'selected'},
+ :content => tester.name
+ }
+
+ # Update issue
+ new_tester = users[1]
+ assert_difference 'Journal.count' do
+ put "/issues/#{issue.id}",
+ :notes => 'Updating custom field',
+ :issue => {
+ :custom_field_values => {@field.id.to_s => new_tester.id.to_s}
+ }
+ end
+ assert_response 302
+
+ # Issue view
+ follow_redirect!
+ assert_tag :content => 'Tester',
+ :ancestor => {:tag => 'ul', :attributes => {:class => /details/}},
+ :sibling => {
+ :content => tester.name,
+ :sibling => {
+ :content => new_tester.name
+ }
+ }
+ end
+
+ def test_update_using_invalid_http_verbs
+ subject = 'Updated by an invalid http verb'
+
+ get '/issues/update/1', {:issue => {:subject => subject}}, credentials('jsmith')
+ assert_response 404
+ assert_not_equal subject, Issue.find(1).subject
+
+ post '/issues/1', {:issue => {:subject => subject}}, credentials('jsmith')
+ assert_response 404
+ assert_not_equal subject, Issue.find(1).subject
+ end
+
+ def test_get_watch_should_be_invalid
+ assert_no_difference 'Watcher.count' do
+ get '/watchers/watch?object_type=issue&object_id=1', {}, credentials('jsmith')
+ assert_response 404
+ end
+ end
+end
diff --git a/test/integration/layout_test.rb b/test/integration/layout_test.rb
new file mode 100644
index 000000000..7f94b0d57
--- /dev/null
+++ b/test/integration/layout_test.rb
@@ -0,0 +1,119 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class LayoutTest < ActionController::IntegrationTest
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules
+
+ test "browsing to a missing page should render the base layout" do
+ get "/users/100000000"
+
+ assert_response :not_found
+
+ # UsersController uses the admin layout by default
+ assert_select "#admin-menu", :count => 0
+ end
+
+ test "browsing to an unauthorized page should render the base layout" do
+ change_user_password('miscuser9', 'test1234')
+
+ log_user('miscuser9','test1234')
+
+ get "/admin"
+ assert_response :forbidden
+ assert_select "#admin-menu", :count => 0
+ end
+
+ def test_top_menu_and_search_not_visible_when_login_required
+ with_settings :login_required => '1' do
+ get '/'
+ assert_select "#top-menu > ul", 0
+ assert_select "#quick-search", 0
+ end
+ end
+
+ def test_top_menu_and_search_visible_when_login_not_required
+ with_settings :login_required => '0' do
+ get '/'
+ assert_select "#top-menu > ul"
+ assert_select "#quick-search"
+ end
+ end
+
+ def test_wiki_formatter_header_tags
+ Role.anonymous.add_permission! :add_issues
+
+ get '/projects/ecookbook/issues/new'
+ assert_tag :script,
+ :attributes => {:src => %r{^/javascripts/jstoolbar/jstoolbar-textile.min.js}},
+ :parent => {:tag => 'head'}
+ end
+
+ def test_calendar_header_tags
+ with_settings :default_language => 'fr' do
+ get '/issues'
+ assert_include "/javascripts/i18n/jquery.ui.datepicker-fr.js", response.body
+ end
+
+ with_settings :default_language => 'en-GB' do
+ get '/issues'
+ assert_include "/javascripts/i18n/jquery.ui.datepicker-en-GB.js", response.body
+ end
+
+ with_settings :default_language => 'en' do
+ get '/issues'
+ assert_not_include "/javascripts/i18n/jquery.ui.datepicker", response.body
+ end
+
+ with_settings :default_language => 'zh' do
+ get '/issues'
+ assert_include "/javascripts/i18n/jquery.ui.datepicker-zh-CN.js", response.body
+ end
+
+ with_settings :default_language => 'zh-TW' do
+ get '/issues'
+ assert_include "/javascripts/i18n/jquery.ui.datepicker-zh-TW.js", response.body
+ end
+
+ with_settings :default_language => 'pt' do
+ get '/issues'
+ assert_include "/javascripts/i18n/jquery.ui.datepicker-pt.js", response.body
+ end
+
+ with_settings :default_language => 'pt-BR' do
+ get '/issues'
+ assert_include "/javascripts/i18n/jquery.ui.datepicker-pt-BR.js", response.body
+ end
+ end
+
+ def test_search_field_outside_project_should_link_to_global_search
+ get '/'
+ assert_select 'div#quick-search form[action=/search]'
+ end
+
+ def test_search_field_inside_project_should_link_to_project_search
+ get '/projects/ecookbook'
+ assert_select 'div#quick-search form[action=/projects/ecookbook/search]'
+ end
+end
diff --git a/test/integration/lib/redmine/hook_test.rb b/test/integration/lib/redmine/hook_test.rb
new file mode 100644
index 000000000..d2e79cbc8
--- /dev/null
+++ b/test/integration/lib/redmine/hook_test.rb
@@ -0,0 +1,89 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../../test_helper', __FILE__)
+
+class HookTest < ActionController::IntegrationTest
+ fixtures :users, :roles, :projects, :members, :member_roles
+
+ # Hooks that are manually registered later
+ class ProjectBasedTemplate < Redmine::Hook::ViewListener
+ def view_layouts_base_html_head(context)
+ # Adds a project stylesheet
+ stylesheet_link_tag(context[:project].identifier) if context[:project]
+ end
+ end
+
+ class SidebarContent < Redmine::Hook::ViewListener
+ def view_layouts_base_sidebar(context)
+ content_tag('p', 'Sidebar hook')
+ end
+ end
+
+ class ContentForInsideHook < Redmine::Hook::ViewListener
+ render_on :view_welcome_index_left, :inline => <<-VIEW
+<% content_for :header_tags do %>
+ <%= javascript_include_tag 'test_plugin.js', :plugin => 'test_plugin' %>
+ <%= stylesheet_link_tag 'test_plugin.css', :plugin => 'test_plugin' %>
+<% end %>
+
+ContentForInsideHook content
+VIEW
+ end
+
+ def setup
+ Redmine::Hook.clear_listeners
+ end
+
+ def teardown
+ Redmine::Hook.clear_listeners
+ end
+
+ def test_html_head_hook_response
+ Redmine::Hook.add_listener(ProjectBasedTemplate)
+
+ get '/projects/ecookbook'
+ assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
+ :parent => {:tag => 'head'}
+ end
+
+ def test_empty_sidebar_should_be_hidden
+ get '/'
+ assert_select 'div#main.nosidebar'
+ end
+
+ def test_sidebar_with_hook_content_should_not_be_hidden
+ Redmine::Hook.add_listener(SidebarContent)
+
+ get '/'
+ assert_select 'div#sidebar p', :text => 'Sidebar hook'
+ assert_select 'div#main'
+ assert_select 'div#main.nosidebar', 0
+ end
+
+ def test_hook_with_content_for_should_append_content
+ Redmine::Hook.add_listener(ContentForInsideHook)
+
+ get '/'
+ assert_response :success
+ assert_select 'p', :text => 'ContentForInsideHook content'
+ assert_select 'head' do
+ assert_select 'script[src=/plugin_assets/test_plugin/javascripts/test_plugin.js]'
+ assert_select 'link[href=/plugin_assets/test_plugin/stylesheets/test_plugin.css]'
+ end
+ end
+end
diff --git a/test/integration/lib/redmine/menu_manager_test.rb b/test/integration/lib/redmine/menu_manager_test.rb
new file mode 100644
index 000000000..a083b055a
--- /dev/null
+++ b/test/integration/lib/redmine/menu_manager_test.rb
@@ -0,0 +1,76 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../../test_helper', __FILE__)
+
+class MenuManagerTest < ActionController::IntegrationTest
+ include Redmine::I18n
+
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users, :issue_categories,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :enabled_modules
+
+ def test_project_menu_with_specific_locale
+ get 'projects/ecookbook/issues', { }, 'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'
+
+ assert_tag :div, :attributes => { :id => 'main-menu' },
+ :descendant => { :tag => 'li', :child => { :tag => 'a', :content => ll('fr', :label_activity),
+ :attributes => { :href => '/projects/ecookbook/activity',
+ :class => 'activity' } } }
+ assert_tag :div, :attributes => { :id => 'main-menu' },
+ :descendant => { :tag => 'li', :child => { :tag => 'a', :content => ll('fr', :label_issue_plural),
+ :attributes => { :href => '/projects/ecookbook/issues',
+ :class => 'issues selected' } } }
+ end
+
+ def test_project_menu_with_additional_menu_items
+ Setting.default_language = 'en'
+ assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do
+ Redmine::MenuManager.map :project_menu do |menu|
+ menu.push :foo, { :controller => 'projects', :action => 'show' }, :caption => 'Foo'
+ menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity
+ menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar
+ end
+
+ get 'projects/ecookbook'
+ assert_tag :div, :attributes => { :id => 'main-menu' },
+ :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo',
+ :attributes => { :class => 'foo' } } }
+
+ assert_tag :div, :attributes => { :id => 'main-menu' },
+ :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar',
+ :attributes => { :class => 'bar' } },
+ :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } }
+
+ assert_tag :div, :attributes => { :id => 'main-menu' },
+ :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK',
+ :attributes => { :class => 'hello' } },
+ :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } }
+
+ # Remove the menu items
+ Redmine::MenuManager.map :project_menu do |menu|
+ menu.delete :foo
+ menu.delete :bar
+ menu.delete :hello
+ end
+ end
+ end
+end
diff --git a/test/integration/lib/redmine/themes_test.rb b/test/integration/lib/redmine/themes_test.rb
new file mode 100644
index 000000000..6d286ef10
--- /dev/null
+++ b/test/integration/lib/redmine/themes_test.rb
@@ -0,0 +1,74 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../../test_helper', __FILE__)
+
+class ThemesTest < ActionController::IntegrationTest
+
+ def setup
+ @theme = Redmine::Themes.themes.last
+ Setting.ui_theme = @theme.id
+ end
+
+ def teardown
+ Setting.ui_theme = ''
+ end
+
+ def test_application_css
+ get '/'
+
+ assert_response :success
+ assert_tag :tag => 'link',
+ :attributes => {:href => %r{^/themes/#{@theme.dir}/stylesheets/application.css}}
+ end
+
+ def test_without_theme_js
+ get '/'
+
+ assert_response :success
+ assert_no_tag :tag => 'script',
+ :attributes => {:src => %r{^/themes/#{@theme.dir}/javascripts/theme.js}}
+ end
+
+ def test_with_theme_js
+ # Simulates a theme.js
+ @theme.javascripts << 'theme'
+ get '/'
+
+ assert_response :success
+ assert_tag :tag => 'script',
+ :attributes => {:src => %r{^/themes/#{@theme.dir}/javascripts/theme.js}}
+
+ ensure
+ @theme.javascripts.delete 'theme'
+ end
+
+ def test_with_sub_uri
+ Redmine::Utils.relative_url_root = '/foo'
+ @theme.javascripts << 'theme'
+ get '/'
+
+ assert_response :success
+ assert_tag :tag => 'link',
+ :attributes => {:href => %r{^/foo/themes/#{@theme.dir}/stylesheets/application.css}}
+ assert_tag :tag => 'script',
+ :attributes => {:src => %r{^/foo/themes/#{@theme.dir}/javascripts/theme.js}}
+
+ ensure
+ Redmine::Utils.relative_url_root = ''
+ end
+end
diff --git a/test/integration/projects_test.rb b/test/integration/projects_test.rb
new file mode 100644
index 000000000..87f28bb61
--- /dev/null
+++ b/test/integration/projects_test.rb
@@ -0,0 +1,51 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class ProjectsTest < ActionController::IntegrationTest
+ fixtures :projects, :users, :members, :enabled_modules
+
+ def test_archive_project
+ subproject = Project.find(1).children.first
+ log_user("admin", "admin")
+ get "admin/projects"
+ assert_response :success
+ assert_template "admin/projects"
+ post "projects/1/archive"
+ assert_redirected_to "/admin/projects"
+ assert !Project.find(1).active?
+
+ get 'projects/1'
+ assert_response 403
+ get "projects/#{subproject.id}"
+ assert_response 403
+
+ post "projects/1/unarchive"
+ assert_redirected_to "/admin/projects"
+ assert Project.find(1).active?
+ get "projects/1"
+ assert_response :success
+ end
+
+ def test_modules_should_not_allow_get
+ assert_no_difference 'EnabledModule.count' do
+ get '/projects/1/modules', {:enabled_module_names => ['']}, credentials('jsmith')
+ assert_response 404
+ end
+ end
+end
diff --git a/test/integration/repositories_git_test.rb b/test/integration/repositories_git_test.rb
new file mode 100644
index 000000000..d88a86305
--- /dev/null
+++ b/test/integration/repositories_git_test.rb
@@ -0,0 +1,50 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class RepositoriesGitTest < ActionController::IntegrationTest
+ fixtures :projects, :users, :roles, :members, :member_roles,
+ :repositories, :enabled_modules
+
+ REPOSITORY_PATH = Rails.root.join('tmp/test/git_repository').to_s
+ REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
+ PRJ_ID = 3
+
+ def setup
+ User.current = nil
+ @project = Project.find(PRJ_ID)
+ @repository = Repository::Git.create(
+ :project => @project,
+ :url => REPOSITORY_PATH,
+ :path_encoding => 'ISO-8859-1'
+ )
+ assert @repository
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_index
+ get '/projects/subproject1/repository/'
+ assert_response :success
+ end
+
+ def test_diff_two_revs
+ get '/projects/subproject1/repository/diff?rev=61b685fbe&rev_to=2f9c0091'
+ assert_response :success
+ end
+ end
+end
diff --git a/test/integration/users_test.rb b/test/integration/users_test.rb
new file mode 100644
index 000000000..ef6e4d1de
--- /dev/null
+++ b/test/integration/users_test.rb
@@ -0,0 +1,29 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class UsersTest < ActionController::IntegrationTest
+ fixtures :users
+
+ def test_destroy_should_not_accept_get_requests
+ assert_no_difference 'User.count' do
+ get '/users/destroy/2', {}, credentials('admin')
+ assert_response 404
+ end
+ end
+end
diff --git a/test/unit/activity_test.rb b/test/unit/activity_test.rb
new file mode 100644
index 000000000..f0bdef850
--- /dev/null
+++ b/test/unit/activity_test.rb
@@ -0,0 +1,129 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class ActivityTest < ActiveSupport::TestCase
+ fixtures :projects, :versions, :attachments, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
+ :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages, :time_entries,
+ :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
+
+ def setup
+ @project = Project.find(1)
+ end
+
+ def test_activity_without_subprojects
+ events = find_events(User.anonymous, :project => @project)
+ assert_not_nil events
+
+ assert events.include?(Issue.find(1))
+ assert !events.include?(Issue.find(4))
+ # subproject issue
+ assert !events.include?(Issue.find(5))
+ end
+
+ def test_activity_with_subprojects
+ events = find_events(User.anonymous, :project => @project, :with_subprojects => 1)
+ assert_not_nil events
+
+ assert events.include?(Issue.find(1))
+ # subproject issue
+ assert events.include?(Issue.find(5))
+ end
+
+ def test_global_activity_anonymous
+ events = find_events(User.anonymous)
+ assert_not_nil events
+
+ assert events.include?(Issue.find(1))
+ assert events.include?(Message.find(5))
+ # Issue of a private project
+ assert !events.include?(Issue.find(4))
+ # Private issue and comment
+ assert !events.include?(Issue.find(14))
+ assert !events.include?(Journal.find(5))
+ end
+
+ def test_global_activity_logged_user
+ events = find_events(User.find(2)) # manager
+ assert_not_nil events
+
+ assert events.include?(Issue.find(1))
+ # Issue of a private project the user belongs to
+ assert events.include?(Issue.find(4))
+ end
+
+ def test_user_activity
+ user = User.find(2)
+ events = Redmine::Activity::Fetcher.new(User.anonymous, :author => user).events(nil, nil, :limit => 10)
+
+ assert(events.size > 0)
+ assert(events.size <= 10)
+ assert_nil(events.detect {|e| e.event_author != user})
+ end
+
+ def test_files_activity
+ f = Redmine::Activity::Fetcher.new(User.anonymous, :project => Project.find(1))
+ f.scope = ['files']
+ events = f.events
+
+ assert_kind_of Array, events
+ assert events.include?(Attachment.find_by_container_type_and_container_id('Project', 1))
+ assert events.include?(Attachment.find_by_container_type_and_container_id('Version', 1))
+ assert_equal [Attachment], events.collect(&:class).uniq
+ assert_equal %w(Project Version), events.collect(&:container_type).uniq.sort
+ end
+
+ def test_event_group_for_issue
+ issue = Issue.find(1)
+ assert_equal issue, issue.event_group
+ end
+
+ def test_event_group_for_journal
+ issue = Issue.find(1)
+ journal = issue.journals.first
+ assert_equal issue, journal.event_group
+ end
+
+ def test_event_group_for_issue_time_entry
+ time = TimeEntry.where(:issue_id => 1).first
+ assert_equal time.issue, time.event_group
+ end
+
+ def test_event_group_for_project_time_entry
+ time = TimeEntry.where(:issue_id => nil).first
+ assert_equal time, time.event_group
+ end
+
+ def test_event_group_for_message
+ message = Message.find(1)
+ reply = message.children.first
+ assert_equal message, message.event_group
+ assert_equal message, reply.event_group
+ end
+
+ def test_event_group_for_wiki_content_version
+ content = WikiContent::Version.find(1)
+ assert_equal content.page, content.event_group
+ end
+
+ private
+
+ def find_events(user, options={})
+ Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1)
+ end
+end
diff --git a/test/unit/attachment_test.rb b/test/unit/attachment_test.rb
new file mode 100644
index 000000000..e32754b7d
--- /dev/null
+++ b/test/unit/attachment_test.rb
@@ -0,0 +1,283 @@
+# encoding: utf-8
+#
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class AttachmentTest < ActiveSupport::TestCase
+ fixtures :users, :projects, :roles, :members, :member_roles,
+ :enabled_modules, :issues, :trackers, :attachments
+
+ class MockFile
+ attr_reader :original_filename, :content_type, :content, :size
+
+ def initialize(attributes)
+ @original_filename = attributes[:original_filename]
+ @content_type = attributes[:content_type]
+ @content = attributes[:content] || "Content"
+ @size = content.size
+ end
+ end
+
+ def setup
+ set_tmp_attachments_directory
+ end
+
+ def test_container_for_new_attachment_should_be_nil
+ assert_nil Attachment.new.container
+ end
+
+ def test_create
+ a = Attachment.new(:container => Issue.find(1),
+ :file => uploaded_test_file("testfile.txt", "text/plain"),
+ :author => User.find(1))
+ assert a.save
+ assert_equal 'testfile.txt', a.filename
+ assert_equal 59, a.filesize
+ assert_equal 'text/plain', a.content_type
+ assert_equal 0, a.downloads
+ assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
+
+ assert a.disk_directory
+ assert_match %r{\A\d{4}/\d{2}\z}, a.disk_directory
+
+ assert File.exist?(a.diskfile)
+ assert_equal 59, File.size(a.diskfile)
+ end
+
+ def test_copy_should_preserve_attributes
+ a = Attachment.find(1)
+ copy = a.copy
+
+ assert_save copy
+ copy = Attachment.order('id DESC').first
+ %w(filename filesize content_type author_id created_on description digest disk_filename disk_directory diskfile).each do |attribute|
+ assert_equal a.send(attribute), copy.send(attribute), "#{attribute} was different"
+ end
+ end
+
+ def test_size_should_be_validated_for_new_file
+ with_settings :attachment_max_size => 0 do
+ a = Attachment.new(:container => Issue.find(1),
+ :file => uploaded_test_file("testfile.txt", "text/plain"),
+ :author => User.find(1))
+ assert !a.save
+ end
+ end
+
+ def test_size_should_not_be_validated_when_copying
+ a = Attachment.create!(:container => Issue.find(1),
+ :file => uploaded_test_file("testfile.txt", "text/plain"),
+ :author => User.find(1))
+ with_settings :attachment_max_size => 0 do
+ copy = a.copy
+ assert copy.save
+ end
+ end
+
+ def test_description_length_should_be_validated
+ a = Attachment.new(:description => 'a' * 300)
+ assert !a.save
+ assert_not_nil a.errors[:description]
+ end
+
+ def test_destroy
+ a = Attachment.new(:container => Issue.find(1),
+ :file => uploaded_test_file("testfile.txt", "text/plain"),
+ :author => User.find(1))
+ assert a.save
+ assert_equal 'testfile.txt', a.filename
+ assert_equal 59, a.filesize
+ assert_equal 'text/plain', a.content_type
+ assert_equal 0, a.downloads
+ assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
+ diskfile = a.diskfile
+ assert File.exist?(diskfile)
+ assert_equal 59, File.size(a.diskfile)
+ assert a.destroy
+ assert !File.exist?(diskfile)
+ end
+
+ def test_destroy_should_not_delete_file_referenced_by_other_attachment
+ a = Attachment.create!(:container => Issue.find(1),
+ :file => uploaded_test_file("testfile.txt", "text/plain"),
+ :author => User.find(1))
+ diskfile = a.diskfile
+
+ copy = a.copy
+ copy.save!
+
+ assert File.exists?(diskfile)
+ a.destroy
+ assert File.exists?(diskfile)
+ copy.destroy
+ assert !File.exists?(diskfile)
+ end
+
+ def test_create_should_auto_assign_content_type
+ a = Attachment.new(:container => Issue.find(1),
+ :file => uploaded_test_file("testfile.txt", ""),
+ :author => User.find(1))
+ assert a.save
+ assert_equal 'text/plain', a.content_type
+ end
+
+ def test_identical_attachments_at_the_same_time_should_not_overwrite
+ a1 = Attachment.create!(:container => Issue.find(1),
+ :file => uploaded_test_file("testfile.txt", ""),
+ :author => User.find(1))
+ a2 = Attachment.create!(:container => Issue.find(1),
+ :file => uploaded_test_file("testfile.txt", ""),
+ :author => User.find(1))
+ assert a1.disk_filename != a2.disk_filename
+ end
+
+ def test_filename_should_be_basenamed
+ a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file"))
+ assert_equal 'file', a.filename
+ end
+
+ def test_filename_should_be_sanitized
+ a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars"))
+ assert_equal 'valid_[] invalid_chars', a.filename
+ end
+
+ def test_diskfilename
+ assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
+ assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
+ assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentué.txt")[13..-1]
+ assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentué")[13..-1]
+ assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentué.ça")[13..-1]
+ end
+
+ def test_title
+ a = Attachment.new(:filename => "test.png")
+ assert_equal "test.png", a.title
+
+ a = Attachment.new(:filename => "test.png", :description => "Cool image")
+ assert_equal "test.png (Cool image)", a.title
+ end
+
+ def test_prune_should_destroy_old_unattached_attachments
+ Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
+ Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
+ Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1)
+
+ assert_difference 'Attachment.count', -2 do
+ Attachment.prune
+ end
+ end
+
+ def test_move_from_root_to_target_directory_should_move_root_files
+ a = Attachment.find(20)
+ assert a.disk_directory.blank?
+ # Create a real file for this fixture
+ File.open(a.diskfile, "w") do |f|
+ f.write "test file at the root of files directory"
+ end
+ assert a.readable?
+ Attachment.move_from_root_to_target_directory
+
+ a.reload
+ assert_equal '2012/05', a.disk_directory
+ assert a.readable?
+ end
+
+ test "Attachmnet.attach_files should attach the file" do
+ issue = Issue.first
+ assert_difference 'Attachment.count' do
+ Attachment.attach_files(issue,
+ '1' => {
+ 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
+ 'description' => 'test'
+ })
+ end
+
+ attachment = Attachment.first(:order => 'id DESC')
+ assert_equal issue, attachment.container
+ assert_equal 'testfile.txt', attachment.filename
+ assert_equal 59, attachment.filesize
+ assert_equal 'test', attachment.description
+ assert_equal 'text/plain', attachment.content_type
+ assert File.exists?(attachment.diskfile)
+ assert_equal 59, File.size(attachment.diskfile)
+ end
+
+ test "Attachmnet.attach_files should add unsaved files to the object as unsaved attachments" do
+ # Max size of 0 to force Attachment creation failures
+ with_settings(:attachment_max_size => 0) do
+ @project = Project.find(1)
+ response = Attachment.attach_files(@project, {
+ '1' => {'file' => mock_file, 'description' => 'test'},
+ '2' => {'file' => mock_file, 'description' => 'test'}
+ })
+
+ assert response[:unsaved].present?
+ assert_equal 2, response[:unsaved].length
+ assert response[:unsaved].first.new_record?
+ assert response[:unsaved].second.new_record?
+ assert_equal response[:unsaved], @project.unsaved_attachments
+ end
+ end
+
+ def test_latest_attach
+ set_fixtures_attachments_directory
+ a1 = Attachment.find(16)
+ assert_equal "testfile.png", a1.filename
+ assert a1.readable?
+ assert (! a1.visible?(User.anonymous))
+ assert a1.visible?(User.find(2))
+ a2 = Attachment.find(17)
+ assert_equal "testfile.PNG", a2.filename
+ assert a2.readable?
+ assert (! a2.visible?(User.anonymous))
+ assert a2.visible?(User.find(2))
+ assert a1.created_on < a2.created_on
+
+ la1 = Attachment.latest_attach([a1, a2], "testfile.png")
+ assert_equal 17, la1.id
+ la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG")
+ assert_equal 17, la2.id
+
+ set_tmp_attachments_directory
+ end
+
+ def test_thumbnailable_should_be_true_for_images
+ assert_equal true, Attachment.new(:filename => 'test.jpg').thumbnailable?
+ end
+
+ def test_thumbnailable_should_be_true_for_non_images
+ assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable?
+ end
+
+ if convert_installed?
+ def test_thumbnail_should_generate_the_thumbnail
+ set_fixtures_attachments_directory
+ attachment = Attachment.find(16)
+ Attachment.clear_thumbnails
+
+ assert_difference "Dir.glob(File.join(Attachment.thumbnails_storage_path, '*.thumb')).size" do
+ thumbnail = attachment.thumbnail
+ assert_equal "16_8e0294de2441577c529f170b6fb8f638_100.thumb", File.basename(thumbnail)
+ assert File.exists?(thumbnail)
+ end
+ end
+ else
+ puts '(ImageMagick convert not available)'
+ end
+end
diff --git a/test/unit/auth_source_ldap_test.rb b/test/unit/auth_source_ldap_test.rb
new file mode 100644
index 000000000..29c528f35
--- /dev/null
+++ b/test/unit/auth_source_ldap_test.rb
@@ -0,0 +1,141 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class AuthSourceLdapTest < ActiveSupport::TestCase
+ include Redmine::I18n
+ fixtures :auth_sources
+
+ def setup
+ end
+
+ def test_create
+ a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName')
+ assert a.save
+ end
+
+ def test_should_strip_ldap_attributes
+ a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName',
+ :attr_firstname => 'givenName ')
+ assert a.save
+ assert_equal 'givenName', a.reload.attr_firstname
+ end
+
+ def test_replace_port_zero_to_389
+ a = AuthSourceLdap.new(
+ :name => 'My LDAP', :host => 'ldap.example.net', :port => 0,
+ :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName',
+ :attr_firstname => 'givenName ')
+ assert a.save
+ assert_equal 389, a.port
+ end
+
+ def test_filter_should_be_validated
+ set_language_if_valid 'en'
+
+ a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :attr_login => 'sn')
+ a.filter = "(mail=*@redmine.org"
+ assert !a.valid?
+ assert_include "LDAP filter is invalid", a.errors.full_messages
+
+ a.filter = "(mail=*@redmine.org)"
+ assert a.valid?
+ end
+
+ if ldap_configured?
+ test '#authenticate with a valid LDAP user should return the user attributes' do
+ auth = AuthSourceLdap.find(1)
+ auth.update_attribute :onthefly_register, true
+
+ attributes = auth.authenticate('example1','123456')
+ assert attributes.is_a?(Hash), "An hash was not returned"
+ assert_equal 'Example', attributes[:firstname]
+ assert_equal 'One', attributes[:lastname]
+ assert_equal 'example1@redmine.org', attributes[:mail]
+ assert_equal auth.id, attributes[:auth_source_id]
+ attributes.keys.each do |attribute|
+ assert User.new.respond_to?("#{attribute}="), "Unexpected :#{attribute} attribute returned"
+ end
+ end
+
+ test '#authenticate with an invalid LDAP user should return nil' do
+ auth = AuthSourceLdap.find(1)
+ assert_equal nil, auth.authenticate('nouser','123456')
+ end
+
+ test '#authenticate without a login should return nil' do
+ auth = AuthSourceLdap.find(1)
+ assert_equal nil, auth.authenticate('','123456')
+ end
+
+ test '#authenticate without a password should return nil' do
+ auth = AuthSourceLdap.find(1)
+ assert_equal nil, auth.authenticate('edavis','')
+ end
+
+ test '#authenticate without filter should return any user' do
+ auth = AuthSourceLdap.find(1)
+ assert auth.authenticate('example1','123456')
+ assert auth.authenticate('edavis', '123456')
+ end
+
+ test '#authenticate with filter should return user who matches the filter only' do
+ auth = AuthSourceLdap.find(1)
+ auth.filter = "(mail=*@redmine.org)"
+
+ assert auth.authenticate('example1','123456')
+ assert_nil auth.authenticate('edavis', '123456')
+ end
+
+ def test_authenticate_should_timeout
+ auth_source = AuthSourceLdap.find(1)
+ auth_source.timeout = 1
+ def auth_source.initialize_ldap_con(*args); sleep(5); end
+
+ assert_raise AuthSourceTimeoutException do
+ auth_source.authenticate 'example1', '123456'
+ end
+ end
+
+ def test_search_should_return_matching_entries
+ results = AuthSource.search("exa")
+ assert_equal 1, results.size
+ result = results.first
+ assert_kind_of Hash, result
+ assert_equal "example1", result[:login]
+ assert_equal "Example", result[:firstname]
+ assert_equal "One", result[:lastname]
+ assert_equal "example1@redmine.org", result[:mail]
+ assert_equal 1, result[:auth_source_id]
+ end
+
+ def test_search_with_no_match_should_return_an_empty_array
+ results = AuthSource.search("wro")
+ assert_equal [], results
+ end
+
+ def test_search_with_exception_should_return_an_empty_array
+ Net::LDAP.stubs(:new).raises(Net::LDAP::LdapError, 'Cannot connect')
+
+ results = AuthSource.search("exa")
+ assert_equal [], results
+ end
+ else
+ puts '(Test LDAP server not configured)'
+ end
+end
diff --git a/test/unit/board_test.rb b/test/unit/board_test.rb
new file mode 100644
index 000000000..ff4779081
--- /dev/null
+++ b/test/unit/board_test.rb
@@ -0,0 +1,116 @@
+# encoding: utf-8
+#
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class BoardTest < ActiveSupport::TestCase
+ fixtures :projects, :boards, :messages, :attachments, :watchers
+
+ include Redmine::I18n
+
+ def setup
+ @project = Project.find(1)
+ end
+
+ def test_create
+ board = Board.new(:project => @project, :name => 'Test board', :description => 'Test board description')
+ assert board.save
+ board.reload
+ assert_equal 'Test board', board.name
+ assert_equal 'Test board description', board.description
+ assert_equal @project, board.project
+ assert_equal 0, board.topics_count
+ assert_equal 0, board.messages_count
+ assert_nil board.last_message
+ # last position
+ assert_equal @project.boards.size, board.position
+ end
+
+ def test_parent_should_be_in_same_project
+ set_language_if_valid 'en'
+ board = Board.new(:project_id => 3, :name => 'Test', :description => 'Test', :parent_id => 1)
+ assert !board.save
+ assert_include "Parent forum is invalid", board.errors.full_messages
+ end
+
+ def test_valid_parents_should_not_include_self_nor_a_descendant
+ board1 = Board.generate!(:project_id => 3)
+ board2 = Board.generate!(:project_id => 3, :parent => board1)
+ board3 = Board.generate!(:project_id => 3, :parent => board2)
+ board4 = Board.generate!(:project_id => 3)
+
+ assert_equal [board4], board1.reload.valid_parents.sort_by(&:id)
+ assert_equal [board1, board4], board2.reload.valid_parents.sort_by(&:id)
+ assert_equal [board1, board2, board4], board3.reload.valid_parents.sort_by(&:id)
+ assert_equal [board1, board2, board3], board4.reload.valid_parents.sort_by(&:id)
+ end
+
+ def test_position_should_be_assigned_with_parent_scope
+ parent1 = Board.generate!(:project_id => 3)
+ parent2 = Board.generate!(:project_id => 3)
+ child1 = Board.generate!(:project_id => 3, :parent => parent1)
+ child2 = Board.generate!(:project_id => 3, :parent => parent1)
+
+ assert_equal 1, parent1.reload.position
+ assert_equal 1, child1.reload.position
+ assert_equal 2, child2.reload.position
+ assert_equal 2, parent2.reload.position
+ end
+
+ def test_board_tree_should_yield_boards_with_level
+ parent1 = Board.generate!(:project_id => 3)
+ parent2 = Board.generate!(:project_id => 3)
+ child1 = Board.generate!(:project_id => 3, :parent => parent1)
+ child2 = Board.generate!(:project_id => 3, :parent => parent1)
+ child3 = Board.generate!(:project_id => 3, :parent => child1)
+
+ tree = Board.board_tree(Project.find(3).boards)
+
+ assert_equal [
+ [parent1, 0],
+ [child1, 1],
+ [child3, 2],
+ [child2, 1],
+ [parent2, 0]
+ ], tree
+ end
+
+ def test_destroy
+ board = Board.find(1)
+ assert_difference 'Message.count', -6 do
+ assert_difference 'Attachment.count', -1 do
+ assert_difference 'Watcher.count', -1 do
+ assert board.destroy
+ end
+ end
+ end
+ assert_equal 0, Message.count(:conditions => {:board_id => 1})
+ end
+
+ def test_destroy_should_nullify_children
+ parent = Board.generate!(:project => @project)
+ child = Board.generate!(:project => @project, :parent => parent)
+ assert_equal parent, child.parent
+
+ assert parent.destroy
+ child.reload
+ assert_nil child.parent
+ assert_nil child.parent_id
+ end
+end
diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb
new file mode 100644
index 000000000..a652c9810
--- /dev/null
+++ b/test/unit/changeset_test.rb
@@ -0,0 +1,476 @@
+# encoding: utf-8
+#
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class ChangesetTest < ActiveSupport::TestCase
+ fixtures :projects, :repositories,
+ :issues, :issue_statuses, :issue_categories,
+ :changesets, :changes,
+ :enumerations,
+ :custom_fields, :custom_values,
+ :users, :members, :member_roles, :trackers,
+ :enabled_modules, :roles
+
+ def test_ref_keywords_any
+ ActionMailer::Base.deliveries.clear
+ Setting.commit_fix_status_id = IssueStatus.find(
+ :first, :conditions => ["is_closed = ?", true]).id
+ Setting.commit_fix_done_ratio = '90'
+ Setting.commit_ref_keywords = '*'
+ Setting.commit_fix_keywords = 'fixes , closes'
+
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => 'New commit (#2). Fixes #1',
+ :revision => '12345')
+ assert c.save
+ assert_equal [1, 2], c.issue_ids.sort
+ fixed = Issue.find(1)
+ assert fixed.closed?
+ assert_equal 90, fixed.done_ratio
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_ref_keywords
+ Setting.commit_ref_keywords = 'refs'
+ Setting.commit_fix_keywords = ''
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => 'Ignores #2. Refs #1',
+ :revision => '12345')
+ assert c.save
+ assert_equal [1], c.issue_ids.sort
+ end
+
+ def test_ref_keywords_any_only
+ Setting.commit_ref_keywords = '*'
+ Setting.commit_fix_keywords = ''
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => 'Ignores #2. Refs #1',
+ :revision => '12345')
+ assert c.save
+ assert_equal [1, 2], c.issue_ids.sort
+ end
+
+ def test_ref_keywords_any_with_timelog
+ Setting.commit_ref_keywords = '*'
+ Setting.commit_logtime_enabled = '1'
+
+ {
+ '2' => 2.0,
+ '2h' => 2.0,
+ '2hours' => 2.0,
+ '15m' => 0.25,
+ '15min' => 0.25,
+ '3h15' => 3.25,
+ '3h15m' => 3.25,
+ '3h15min' => 3.25,
+ '3:15' => 3.25,
+ '3.25' => 3.25,
+ '3.25h' => 3.25,
+ '3,25' => 3.25,
+ '3,25h' => 3.25,
+ }.each do |syntax, expected_hours|
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => 24.hours.ago,
+ :comments => "Worked on this issue #1 @#{syntax}",
+ :revision => '520',
+ :user => User.find(2))
+ assert_difference 'TimeEntry.count' do
+ c.scan_comment_for_issue_ids
+ end
+ assert_equal [1], c.issue_ids.sort
+
+ time = TimeEntry.first(:order => 'id desc')
+ assert_equal 1, time.issue_id
+ assert_equal 1, time.project_id
+ assert_equal 2, time.user_id
+ assert_equal expected_hours, time.hours,
+ "@#{syntax} should be logged as #{expected_hours} hours but was #{time.hours}"
+ assert_equal Date.yesterday, time.spent_on
+ assert time.activity.is_default?
+ assert time.comments.include?('r520'),
+ "r520 was expected in time_entry comments: #{time.comments}"
+ end
+ end
+
+ def test_ref_keywords_closing_with_timelog
+ Setting.commit_fix_status_id = IssueStatus.find(
+ :first, :conditions => ["is_closed = ?", true]).id
+ Setting.commit_ref_keywords = '*'
+ Setting.commit_fix_keywords = 'fixes , closes'
+ Setting.commit_logtime_enabled = '1'
+
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => 'This is a comment. Fixes #1 @4.5, #2 @1',
+ :user => User.find(2))
+ assert_difference 'TimeEntry.count', 2 do
+ c.scan_comment_for_issue_ids
+ end
+
+ assert_equal [1, 2], c.issue_ids.sort
+ assert Issue.find(1).closed?
+ assert Issue.find(2).closed?
+
+ times = TimeEntry.all(:order => 'id desc', :limit => 2)
+ assert_equal [1, 2], times.collect(&:issue_id).sort
+ end
+
+ def test_ref_keywords_any_line_start
+ Setting.commit_ref_keywords = '*'
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => '#1 is the reason of this commit',
+ :revision => '12345')
+ assert c.save
+ assert_equal [1], c.issue_ids.sort
+ end
+
+ def test_ref_keywords_allow_brackets_around_a_issue_number
+ Setting.commit_ref_keywords = '*'
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => '[#1] Worked on this issue',
+ :revision => '12345')
+ assert c.save
+ assert_equal [1], c.issue_ids.sort
+ end
+
+ def test_ref_keywords_allow_brackets_around_multiple_issue_numbers
+ Setting.commit_ref_keywords = '*'
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => '[#1 #2, #3] Worked on these',
+ :revision => '12345')
+ assert c.save
+ assert_equal [1,2,3], c.issue_ids.sort
+ end
+
+ def test_commit_referencing_a_subproject_issue
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => 'refs #5, a subproject issue',
+ :revision => '12345')
+ assert c.save
+ assert_equal [5], c.issue_ids.sort
+ assert c.issues.first.project != c.project
+ end
+
+ def test_commit_closing_a_subproject_issue
+ with_settings :commit_fix_status_id => 5, :commit_fix_keywords => 'closes',
+ :default_language => 'en' do
+ issue = Issue.find(5)
+ assert !issue.closed?
+ assert_difference 'Journal.count' do
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => 'closes #5, a subproject issue',
+ :revision => '12345')
+ assert c.save
+ end
+ assert issue.reload.closed?
+ journal = Journal.first(:order => 'id DESC')
+ assert_equal issue, journal.issue
+ assert_include "Applied in changeset ecookbook:r12345.", journal.notes
+ end
+ end
+
+ def test_commit_referencing_a_parent_project_issue
+ # repository of child project
+ r = Repository::Subversion.create!(
+ :project => Project.find(3),
+ :url => 'svn://localhost/test')
+ c = Changeset.new(:repository => r,
+ :committed_on => Time.now,
+ :comments => 'refs #2, an issue of a parent project',
+ :revision => '12345')
+ assert c.save
+ assert_equal [2], c.issue_ids.sort
+ assert c.issues.first.project != c.project
+ end
+
+ def test_commit_referencing_a_project_with_commit_cross_project_ref_disabled
+ r = Repository::Subversion.create!(
+ :project => Project.find(3),
+ :url => 'svn://localhost/test')
+
+ with_settings :commit_cross_project_ref => '0' do
+ c = Changeset.new(:repository => r,
+ :committed_on => Time.now,
+ :comments => 'refs #4, an issue of a different project',
+ :revision => '12345')
+ assert c.save
+ assert_equal [], c.issue_ids
+ end
+ end
+
+ def test_commit_referencing_a_project_with_commit_cross_project_ref_enabled
+ r = Repository::Subversion.create!(
+ :project => Project.find(3),
+ :url => 'svn://localhost/test')
+
+ with_settings :commit_cross_project_ref => '1' do
+ c = Changeset.new(:repository => r,
+ :committed_on => Time.now,
+ :comments => 'refs #4, an issue of a different project',
+ :revision => '12345')
+ assert c.save
+ assert_equal [4], c.issue_ids
+ end
+ end
+
+ def test_text_tag_revision
+ c = Changeset.new(:revision => '520')
+ assert_equal 'r520', c.text_tag
+ end
+
+ def test_text_tag_revision_with_same_project
+ c = Changeset.new(:revision => '520', :repository => Project.find(1).repository)
+ assert_equal 'r520', c.text_tag(Project.find(1))
+ end
+
+ def test_text_tag_revision_with_different_project
+ c = Changeset.new(:revision => '520', :repository => Project.find(1).repository)
+ assert_equal 'ecookbook:r520', c.text_tag(Project.find(2))
+ end
+
+ def test_text_tag_revision_with_repository_identifier
+ r = Repository::Subversion.create!(
+ :project_id => 1,
+ :url => 'svn://localhost/test',
+ :identifier => 'documents')
+
+ c = Changeset.new(:revision => '520', :repository => r)
+ assert_equal 'documents|r520', c.text_tag
+ assert_equal 'ecookbook:documents|r520', c.text_tag(Project.find(2))
+ end
+
+ def test_text_tag_hash
+ c = Changeset.new(
+ :scmid => '7234cb2750b63f47bff735edc50a1c0a433c2518',
+ :revision => '7234cb2750b63f47bff735edc50a1c0a433c2518')
+ assert_equal 'commit:7234cb2750b63f47bff735edc50a1c0a433c2518', c.text_tag
+ end
+
+ def test_text_tag_hash_with_same_project
+ c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository)
+ assert_equal 'commit:7234cb27', c.text_tag(Project.find(1))
+ end
+
+ def test_text_tag_hash_with_different_project
+ c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository)
+ assert_equal 'ecookbook:commit:7234cb27', c.text_tag(Project.find(2))
+ end
+
+ def test_text_tag_hash_all_number
+ c = Changeset.new(:scmid => '0123456789', :revision => '0123456789')
+ assert_equal 'commit:0123456789', c.text_tag
+ end
+
+ def test_previous
+ changeset = Changeset.find_by_revision('3')
+ assert_equal Changeset.find_by_revision('2'), changeset.previous
+ end
+
+ def test_previous_nil
+ changeset = Changeset.find_by_revision('1')
+ assert_nil changeset.previous
+ end
+
+ def test_next
+ changeset = Changeset.find_by_revision('2')
+ assert_equal Changeset.find_by_revision('3'), changeset.next
+ end
+
+ def test_next_nil
+ changeset = Changeset.find_by_revision('10')
+ assert_nil changeset.next
+ end
+
+ def test_comments_should_be_converted_to_utf8
+ proj = Project.find(3)
+ # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt")
+ str = "Texte encod\xe9 en ISO-8859-1."
+ str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
+ r = Repository::Bazaar.create!(
+ :project => proj,
+ :url => '/tmp/test/bazaar',
+ :log_encoding => 'ISO-8859-1' )
+ assert r
+ c = Changeset.new(:repository => r,
+ :committed_on => Time.now,
+ :revision => '123',
+ :scmid => '12345',
+ :comments => str)
+ assert( c.save )
+ str_utf8 = "Texte encod\xc3\xa9 en ISO-8859-1."
+ str_utf8.force_encoding("UTF-8") if str_utf8.respond_to?(:force_encoding)
+ assert_equal str_utf8, c.comments
+ end
+
+ def test_invalid_utf8_sequences_in_comments_should_be_replaced_latin1
+ proj = Project.find(3)
+ # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt")
+ str1 = "Texte encod\xe9 en ISO-8859-1."
+ str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test"
+ str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding)
+ str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding)
+ r = Repository::Bazaar.create!(
+ :project => proj,
+ :url => '/tmp/test/bazaar',
+ :log_encoding => 'UTF-8' )
+ assert r
+ c = Changeset.new(:repository => r,
+ :committed_on => Time.now,
+ :revision => '123',
+ :scmid => '12345',
+ :comments => str1,
+ :committer => str2)
+ assert( c.save )
+ assert_equal "Texte encod? en ISO-8859-1.", c.comments
+ assert_equal "?a?b?c?d?e test", c.committer
+ end
+
+ def test_invalid_utf8_sequences_in_comments_should_be_replaced_ja_jis
+ proj = Project.find(3)
+ str = "test\xb5\xfetest\xb5\xfe"
+ if str.respond_to?(:force_encoding)
+ str.force_encoding('ASCII-8BIT')
+ end
+ r = Repository::Bazaar.create!(
+ :project => proj,
+ :url => '/tmp/test/bazaar',
+ :log_encoding => 'ISO-2022-JP' )
+ assert r
+ c = Changeset.new(:repository => r,
+ :committed_on => Time.now,
+ :revision => '123',
+ :scmid => '12345',
+ :comments => str)
+ assert( c.save )
+ assert_equal "test??test??", c.comments
+ end
+
+ def test_comments_should_be_converted_all_latin1_to_utf8
+ s1 = "\xC2\x80"
+ s2 = "\xc3\x82\xc2\x80"
+ s4 = s2.dup
+ if s1.respond_to?(:force_encoding)
+ s3 = s1.dup
+ s1.force_encoding('ASCII-8BIT')
+ s2.force_encoding('ASCII-8BIT')
+ s3.force_encoding('ISO-8859-1')
+ s4.force_encoding('UTF-8')
+ assert_equal s3.encode('UTF-8'), s4
+ end
+ proj = Project.find(3)
+ r = Repository::Bazaar.create!(
+ :project => proj,
+ :url => '/tmp/test/bazaar',
+ :log_encoding => 'ISO-8859-1' )
+ assert r
+ c = Changeset.new(:repository => r,
+ :committed_on => Time.now,
+ :revision => '123',
+ :scmid => '12345',
+ :comments => s1)
+ assert( c.save )
+ assert_equal s4, c.comments
+ end
+
+ def test_invalid_utf8_sequences_in_paths_should_be_replaced
+ proj = Project.find(3)
+ str1 = "Texte encod\xe9 en ISO-8859-1"
+ str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test"
+ str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding)
+ str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding)
+ r = Repository::Bazaar.create!(
+ :project => proj,
+ :url => '/tmp/test/bazaar',
+ :log_encoding => 'UTF-8' )
+ assert r
+ cs = Changeset.new(
+ :repository => r,
+ :committed_on => Time.now,
+ :revision => '123',
+ :scmid => '12345',
+ :comments => "test")
+ assert(cs.save)
+ ch = Change.new(
+ :changeset => cs,
+ :action => "A",
+ :path => str1,
+ :from_path => str2,
+ :from_revision => "345")
+ assert(ch.save)
+ assert_equal "Texte encod? en ISO-8859-1", ch.path
+ assert_equal "?a?b?c?d?e test", ch.from_path
+ end
+
+ def test_comments_nil
+ proj = Project.find(3)
+ r = Repository::Bazaar.create!(
+ :project => proj,
+ :url => '/tmp/test/bazaar',
+ :log_encoding => 'ISO-8859-1' )
+ assert r
+ c = Changeset.new(:repository => r,
+ :committed_on => Time.now,
+ :revision => '123',
+ :scmid => '12345',
+ :comments => nil,
+ :committer => nil)
+ assert( c.save )
+ assert_equal "", c.comments
+ assert_equal nil, c.committer
+ if c.comments.respond_to?(:force_encoding)
+ assert_equal "UTF-8", c.comments.encoding.to_s
+ end
+ end
+
+ def test_comments_empty
+ proj = Project.find(3)
+ r = Repository::Bazaar.create!(
+ :project => proj,
+ :url => '/tmp/test/bazaar',
+ :log_encoding => 'ISO-8859-1' )
+ assert r
+ c = Changeset.new(:repository => r,
+ :committed_on => Time.now,
+ :revision => '123',
+ :scmid => '12345',
+ :comments => "",
+ :committer => "")
+ assert( c.save )
+ assert_equal "", c.comments
+ assert_equal "", c.committer
+ if c.comments.respond_to?(:force_encoding)
+ assert_equal "UTF-8", c.comments.encoding.to_s
+ assert_equal "UTF-8", c.committer.encoding.to_s
+ end
+ end
+
+ def test_identifier
+ c = Changeset.find_by_revision('1')
+ assert_equal c.revision, c.identifier
+ end
+end
diff --git a/test/unit/comment_test.rb b/test/unit/comment_test.rb
new file mode 100644
index 000000000..325dac4ee
--- /dev/null
+++ b/test/unit/comment_test.rb
@@ -0,0 +1,57 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class CommentTest < ActiveSupport::TestCase
+ fixtures :users, :news, :comments, :projects, :enabled_modules
+
+ def setup
+ @jsmith = User.find(2)
+ @news = News.find(1)
+ end
+
+ def test_create
+ comment = Comment.new(:commented => @news, :author => @jsmith, :comments => "my comment")
+ assert comment.save
+ @news.reload
+ assert_equal 2, @news.comments_count
+ end
+
+ def test_create_should_send_notification
+ Watcher.create!(:watchable => @news, :user => @jsmith)
+
+ with_settings :notified_events => %w(news_comment_added) do
+ assert_difference 'ActionMailer::Base.deliveries.size' do
+ Comment.create!(:commented => @news, :author => @jsmith, :comments => "my comment")
+ end
+ end
+ end
+
+ def test_validate
+ comment = Comment.new(:commented => @news)
+ assert !comment.save
+ assert_equal 2, comment.errors.count
+ end
+
+ def test_destroy
+ comment = Comment.find(1)
+ assert comment.destroy
+ @news.reload
+ assert_equal 0, @news.comments_count
+ end
+end
diff --git a/test/unit/custom_field_test.rb b/test/unit/custom_field_test.rb
new file mode 100644
index 000000000..17a0041c0
--- /dev/null
+++ b/test/unit/custom_field_test.rb
@@ -0,0 +1,244 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class CustomFieldTest < ActiveSupport::TestCase
+ fixtures :custom_fields
+
+ def test_create
+ field = UserCustomField.new(:name => 'Money money money', :field_format => 'float')
+ assert field.save
+ end
+
+ def test_before_validation
+ field = CustomField.new(:name => 'test_before_validation', :field_format => 'int')
+ field.searchable = true
+ assert field.save
+ assert_equal false, field.searchable
+ field.searchable = true
+ assert field.save
+ assert_equal false, field.searchable
+ end
+
+ def test_regexp_validation
+ field = IssueCustomField.new(:name => 'regexp', :field_format => 'text', :regexp => '[a-z0-9')
+ assert !field.save
+ assert_include I18n.t('activerecord.errors.messages.invalid'),
+ field.errors[:regexp]
+ field.regexp = '[a-z0-9]'
+ assert field.save
+ end
+
+ def test_default_value_should_be_validated
+ field = CustomField.new(:name => 'Test', :field_format => 'int')
+ field.default_value = 'abc'
+ assert !field.valid?
+ field.default_value = '6'
+ assert field.valid?
+ end
+
+ def test_default_value_should_not_be_validated_when_blank
+ field = CustomField.new(:name => 'Test', :field_format => 'list', :possible_values => ['a', 'b'], :is_required => true, :default_value => '')
+ assert field.valid?
+ end
+
+ def test_should_not_change_field_format_of_existing_custom_field
+ field = CustomField.find(1)
+ field.field_format = 'int'
+ assert_equal 'list', field.field_format
+ end
+
+ def test_possible_values_should_accept_an_array
+ field = CustomField.new
+ field.possible_values = ["One value", ""]
+ assert_equal ["One value"], field.possible_values
+ end
+
+ def test_possible_values_should_accept_a_string
+ field = CustomField.new
+ field.possible_values = "One value"
+ assert_equal ["One value"], field.possible_values
+ end
+
+ def test_possible_values_should_accept_a_multiline_string
+ field = CustomField.new
+ field.possible_values = "One value\nAnd another one \r\n \n"
+ assert_equal ["One value", "And another one"], field.possible_values
+ end
+
+ if "string".respond_to?(:encoding)
+ def test_possible_values_stored_as_binary_should_be_utf8_encoded
+ field = CustomField.find(11)
+ assert_kind_of Array, field.possible_values
+ assert field.possible_values.size > 0
+ field.possible_values.each do |value|
+ assert_equal "UTF-8", value.encoding.name
+ end
+ end
+ end
+
+ def test_destroy
+ field = CustomField.find(1)
+ assert field.destroy
+ end
+
+ def test_new_subclass_instance_should_return_an_instance
+ f = CustomField.new_subclass_instance('IssueCustomField')
+ assert_kind_of IssueCustomField, f
+ end
+
+ def test_new_subclass_instance_should_set_attributes
+ f = CustomField.new_subclass_instance('IssueCustomField', :name => 'Test')
+ assert_kind_of IssueCustomField, f
+ assert_equal 'Test', f.name
+ end
+
+ def test_new_subclass_instance_with_invalid_class_name_should_return_nil
+ assert_nil CustomField.new_subclass_instance('WrongClassName')
+ end
+
+ def test_new_subclass_instance_with_non_subclass_name_should_return_nil
+ assert_nil CustomField.new_subclass_instance('Project')
+ end
+
+ def test_string_field_validation_with_blank_value
+ f = CustomField.new(:field_format => 'string')
+
+ assert f.valid_field_value?(nil)
+ assert f.valid_field_value?('')
+
+ f.is_required = true
+ assert !f.valid_field_value?(nil)
+ assert !f.valid_field_value?('')
+ end
+
+ def test_string_field_validation_with_min_and_max_lengths
+ f = CustomField.new(:field_format => 'string', :min_length => 2, :max_length => 5)
+
+ assert f.valid_field_value?(nil)
+ assert f.valid_field_value?('')
+ assert f.valid_field_value?('a' * 2)
+ assert !f.valid_field_value?('a')
+ assert !f.valid_field_value?('a' * 6)
+ end
+
+ def test_string_field_validation_with_regexp
+ f = CustomField.new(:field_format => 'string', :regexp => '^[A-Z0-9]*$')
+
+ assert f.valid_field_value?(nil)
+ assert f.valid_field_value?('')
+ assert f.valid_field_value?('ABC')
+ assert !f.valid_field_value?('abc')
+ end
+
+ def test_date_field_validation
+ f = CustomField.new(:field_format => 'date')
+
+ assert f.valid_field_value?(nil)
+ assert f.valid_field_value?('')
+ assert f.valid_field_value?('1975-07-14')
+ assert !f.valid_field_value?('1975-07-33')
+ assert !f.valid_field_value?('abc')
+ end
+
+ def test_list_field_validation
+ f = CustomField.new(:field_format => 'list', :possible_values => ['value1', 'value2'])
+
+ assert f.valid_field_value?(nil)
+ assert f.valid_field_value?('')
+ assert f.valid_field_value?('value2')
+ assert !f.valid_field_value?('abc')
+ end
+
+ def test_int_field_validation
+ f = CustomField.new(:field_format => 'int')
+
+ assert f.valid_field_value?(nil)
+ assert f.valid_field_value?('')
+ assert f.valid_field_value?('123')
+ assert f.valid_field_value?('+123')
+ assert f.valid_field_value?('-123')
+ assert !f.valid_field_value?('6abc')
+ end
+
+ def test_float_field_validation
+ f = CustomField.new(:field_format => 'float')
+
+ assert f.valid_field_value?(nil)
+ assert f.valid_field_value?('')
+ assert f.valid_field_value?('11.2')
+ assert f.valid_field_value?('-6.250')
+ assert f.valid_field_value?('5')
+ assert !f.valid_field_value?('6abc')
+ end
+
+ def test_multi_field_validation
+ f = CustomField.new(:field_format => 'list', :multiple => 'true', :possible_values => ['value1', 'value2'])
+
+ assert f.valid_field_value?(nil)
+ assert f.valid_field_value?('')
+ assert f.valid_field_value?([])
+ assert f.valid_field_value?([nil])
+ assert f.valid_field_value?([''])
+
+ assert f.valid_field_value?('value2')
+ assert !f.valid_field_value?('abc')
+
+ assert f.valid_field_value?(['value2'])
+ assert !f.valid_field_value?(['abc'])
+
+ assert f.valid_field_value?(['', 'value2'])
+ assert !f.valid_field_value?(['', 'abc'])
+
+ assert f.valid_field_value?(['value1', 'value2'])
+ assert !f.valid_field_value?(['value1', 'abc'])
+ end
+
+ def test_changing_multiple_to_false_should_delete_multiple_values
+ field = ProjectCustomField.create!(:name => 'field', :field_format => 'list', :multiple => 'true', :possible_values => ['field1', 'field2'])
+ other = ProjectCustomField.create!(:name => 'other', :field_format => 'list', :multiple => 'true', :possible_values => ['other1', 'other2'])
+
+ item_with_multiple_values = Project.generate!(:custom_field_values => {field.id => ['field1', 'field2'], other.id => ['other1', 'other2']})
+ item_with_single_values = Project.generate!(:custom_field_values => {field.id => ['field1'], other.id => ['other2']})
+
+ assert_difference 'CustomValue.count', -1 do
+ field.multiple = false
+ field.save!
+ end
+
+ item_with_multiple_values = Project.find(item_with_multiple_values.id)
+ assert_kind_of String, item_with_multiple_values.custom_field_value(field)
+ assert_kind_of Array, item_with_multiple_values.custom_field_value(other)
+ assert_equal 2, item_with_multiple_values.custom_field_value(other).size
+ end
+
+ def test_value_class_should_return_the_class_used_for_fields_values
+ assert_equal User, CustomField.new(:field_format => 'user').value_class
+ assert_equal Version, CustomField.new(:field_format => 'version').value_class
+ end
+
+ def test_value_class_should_return_nil_for_other_fields
+ assert_nil CustomField.new(:field_format => 'text').value_class
+ assert_nil CustomField.new.value_class
+ end
+
+ def test_value_from_keyword_for_list_custom_field
+ field = CustomField.find(1)
+ assert_equal 'PostgreSQL', field.value_from_keyword('postgresql', Issue.find(1))
+ end
+end
diff --git a/test/unit/custom_field_user_format_test.rb b/test/unit/custom_field_user_format_test.rb
new file mode 100644
index 000000000..489769011
--- /dev/null
+++ b/test/unit/custom_field_user_format_test.rb
@@ -0,0 +1,77 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class CustomFieldUserFormatTest < ActiveSupport::TestCase
+ fixtures :custom_fields, :projects, :members, :users, :member_roles, :trackers, :issues
+
+ def setup
+ @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user')
+ end
+
+ def test_possible_values_with_no_arguments
+ assert_equal [], @field.possible_values
+ assert_equal [], @field.possible_values(nil)
+ end
+
+ def test_possible_values_with_project_resource
+ project = Project.find(1)
+ possible_values = @field.possible_values(project.issues.first)
+ assert possible_values.any?
+ assert_equal project.users.sort.collect(&:id).map(&:to_s), possible_values
+ end
+
+ def test_possible_values_with_nil_project_resource
+ project = Project.find(1)
+ assert_equal [], @field.possible_values(Issue.new)
+ end
+
+ def test_possible_values_options_with_no_arguments
+ assert_equal [], @field.possible_values_options
+ assert_equal [], @field.possible_values_options(nil)
+ end
+
+ def test_possible_values_options_with_project_resource
+ project = Project.find(1)
+ possible_values_options = @field.possible_values_options(project.issues.first)
+ assert possible_values_options.any?
+ assert_equal project.users.sort.map {|u| [u.name, u.id.to_s]}, possible_values_options
+ end
+
+ def test_possible_values_options_with_array
+ projects = Project.find([1, 2])
+ possible_values_options = @field.possible_values_options(projects)
+ assert possible_values_options.any?
+ assert_equal (projects.first.users & projects.last.users).sort.map {|u| [u.name, u.id.to_s]}, possible_values_options
+ end
+
+ def test_cast_blank_value
+ assert_equal nil, @field.cast_value(nil)
+ assert_equal nil, @field.cast_value("")
+ end
+
+ def test_cast_valid_value
+ user = @field.cast_value("2")
+ assert_kind_of User, user
+ assert_equal User.find(2), user
+ end
+
+ def test_cast_invalid_value
+ assert_equal nil, @field.cast_value("187")
+ end
+end
diff --git a/test/unit/custom_field_version_format_test.rb b/test/unit/custom_field_version_format_test.rb
new file mode 100644
index 000000000..a4a790c12
--- /dev/null
+++ b/test/unit/custom_field_version_format_test.rb
@@ -0,0 +1,76 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class CustomFieldVersionFormatTest < ActiveSupport::TestCase
+ fixtures :custom_fields, :projects, :members, :users, :member_roles, :trackers, :issues, :versions
+
+ def setup
+ @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'version')
+ end
+
+ def test_possible_values_with_no_arguments
+ assert_equal [], @field.possible_values
+ assert_equal [], @field.possible_values(nil)
+ end
+
+ def test_possible_values_with_project_resource
+ project = Project.find(1)
+ possible_values = @field.possible_values(project.issues.first)
+ assert possible_values.any?
+ assert_equal project.shared_versions.sort.collect(&:id).map(&:to_s), possible_values
+ end
+
+ def test_possible_values_with_nil_project_resource
+ assert_equal [], @field.possible_values(Issue.new)
+ end
+
+ def test_possible_values_options_with_no_arguments
+ assert_equal [], @field.possible_values_options
+ assert_equal [], @field.possible_values_options(nil)
+ end
+
+ def test_possible_values_options_with_project_resource
+ project = Project.find(1)
+ possible_values_options = @field.possible_values_options(project.issues.first)
+ assert possible_values_options.any?
+ assert_equal project.shared_versions.sort.map {|u| [u.name, u.id.to_s]}, possible_values_options
+ end
+
+ def test_possible_values_options_with_array
+ projects = Project.find([1, 2])
+ possible_values_options = @field.possible_values_options(projects)
+ assert possible_values_options.any?
+ assert_equal (projects.first.shared_versions & projects.last.shared_versions).sort.map {|u| [u.name, u.id.to_s]}, possible_values_options
+ end
+
+ def test_cast_blank_value
+ assert_equal nil, @field.cast_value(nil)
+ assert_equal nil, @field.cast_value("")
+ end
+
+ def test_cast_valid_value
+ version = @field.cast_value("2")
+ assert_kind_of Version, version
+ assert_equal Version.find(2), version
+ end
+
+ def test_cast_invalid_value
+ assert_equal nil, @field.cast_value("187")
+ end
+end
diff --git a/test/unit/custom_value_test.rb b/test/unit/custom_value_test.rb
new file mode 100644
index 000000000..bc2f426a0
--- /dev/null
+++ b/test/unit/custom_value_test.rb
@@ -0,0 +1,39 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class CustomValueTest < ActiveSupport::TestCase
+ fixtures :custom_fields, :custom_values, :users
+
+ def test_default_value
+ field = CustomField.find_by_default_value('Default string')
+ assert_not_nil field
+
+ v = CustomValue.new(:custom_field => field)
+ assert_equal 'Default string', v.value
+
+ v = CustomValue.new(:custom_field => field, :value => 'Not empty')
+ assert_equal 'Not empty', v.value
+ end
+
+ def test_sti_polymorphic_association
+ # Rails uses top level sti class for polymorphic association. See #3978.
+ assert !User.find(4).custom_values.empty?
+ assert !CustomValue.find(2).customized.nil?
+ end
+end
diff --git a/test/unit/default_data_test.rb b/test/unit/default_data_test.rb
new file mode 100644
index 000000000..f97804dcb
--- /dev/null
+++ b/test/unit/default_data_test.rb
@@ -0,0 +1,49 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class DefaultDataTest < ActiveSupport::TestCase
+ include Redmine::I18n
+ fixtures :roles
+
+ def test_no_data
+ assert !Redmine::DefaultData::Loader::no_data?
+ Role.delete_all("builtin = 0")
+ Tracker.delete_all
+ IssueStatus.delete_all
+ Enumeration.delete_all
+ assert Redmine::DefaultData::Loader::no_data?
+ end
+
+ def test_load
+ valid_languages.each do |lang|
+ begin
+ Role.delete_all("builtin = 0")
+ Tracker.delete_all
+ IssueStatus.delete_all
+ Enumeration.delete_all
+ assert Redmine::DefaultData::Loader::load(lang)
+ assert_not_nil DocumentCategory.first
+ assert_not_nil IssuePriority.first
+ assert_not_nil TimeEntryActivity.first
+ rescue ActiveRecord::RecordInvalid => e
+ assert false, ":#{lang} default data is invalid (#{e.message})."
+ end
+ end
+ end
+end
diff --git a/test/unit/document_category_test.rb b/test/unit/document_category_test.rb
new file mode 100644
index 000000000..e2c95f997
--- /dev/null
+++ b/test/unit/document_category_test.rb
@@ -0,0 +1,47 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class DocumentCategoryTest < ActiveSupport::TestCase
+ fixtures :enumerations, :documents, :issues
+
+ def test_should_be_an_enumeration
+ assert DocumentCategory.ancestors.include?(Enumeration)
+ end
+
+ def test_objects_count
+ assert_equal 2, DocumentCategory.find_by_name("Uncategorized").objects_count
+ assert_equal 0, DocumentCategory.find_by_name("User documentation").objects_count
+ end
+
+ def test_option_name
+ assert_equal :enumeration_doc_categories, DocumentCategory.new.option_name
+ end
+
+ def test_default
+ assert_nil DocumentCategory.where(:is_default => true).first
+ e = Enumeration.find_by_name('Technical documentation')
+ e.update_attributes(:is_default => true)
+ assert_equal 3, DocumentCategory.default.id
+ end
+
+ def test_force_default
+ assert_nil DocumentCategory.where(:is_default => true).first
+ assert_equal 1, DocumentCategory.default.id
+ end
+end
diff --git a/test/unit/document_test.rb b/test/unit/document_test.rb
new file mode 100644
index 000000000..d37f88e27
--- /dev/null
+++ b/test/unit/document_test.rb
@@ -0,0 +1,62 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class DocumentTest < ActiveSupport::TestCase
+ fixtures :projects, :enumerations, :documents, :attachments,
+ :enabled_modules,
+ :users, :members, :member_roles, :roles,
+ :groups_users
+
+ def test_create
+ doc = Document.new(:project => Project.find(1), :title => 'New document', :category => Enumeration.find_by_name('User documentation'))
+ assert doc.save
+ end
+
+ def test_create_should_send_email_notification
+ ActionMailer::Base.deliveries.clear
+
+ with_settings :notified_events => %w(document_added) do
+ doc = Document.new(:project => Project.find(1), :title => 'New document', :category => Enumeration.find_by_name('User documentation'))
+ assert doc.save
+ end
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_create_with_default_category
+ # Sets a default category
+ e = Enumeration.find_by_name('Technical documentation')
+ e.update_attributes(:is_default => true)
+
+ doc = Document.new(:project => Project.find(1), :title => 'New document')
+ assert_equal e, doc.category
+ assert doc.save
+ end
+
+ def test_updated_on_with_attachments
+ d = Document.find(1)
+ assert d.attachments.any?
+ assert_equal d.attachments.map(&:created_on).max, d.updated_on
+ end
+
+ def test_updated_on_without_attachments
+ d = Document.find(2)
+ assert d.attachments.empty?
+ assert_equal d.created_on, d.updated_on
+ end
+end
diff --git a/test/unit/enabled_module_test.rb b/test/unit/enabled_module_test.rb
new file mode 100644
index 000000000..df48cee21
--- /dev/null
+++ b/test/unit/enabled_module_test.rb
@@ -0,0 +1,43 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class EnabledModuleTest < ActiveSupport::TestCase
+ fixtures :projects, :wikis
+
+ def test_enabling_wiki_should_create_a_wiki
+ CustomField.delete_all
+ project = Project.create!(:name => 'Project with wiki', :identifier => 'wikiproject')
+ assert_nil project.wiki
+ project.enabled_module_names = ['wiki']
+ project.reload
+ assert_not_nil project.wiki
+ assert_equal 'Wiki', project.wiki.start_page
+ end
+
+ def test_reenabling_wiki_should_not_create_another_wiki
+ project = Project.find(1)
+ assert_not_nil project.wiki
+ project.enabled_module_names = []
+ project.reload
+ assert_no_difference 'Wiki.count' do
+ project.enabled_module_names = ['wiki']
+ end
+ assert_not_nil project.wiki
+ end
+end
diff --git a/test/unit/enumeration_test.rb b/test/unit/enumeration_test.rb
new file mode 100644
index 000000000..a8eb7860a
--- /dev/null
+++ b/test/unit/enumeration_test.rb
@@ -0,0 +1,130 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class EnumerationTest < ActiveSupport::TestCase
+ fixtures :enumerations, :issues, :custom_fields, :custom_values
+
+ def test_objects_count
+ # low priority
+ assert_equal 6, Enumeration.find(4).objects_count
+ # urgent
+ assert_equal 0, Enumeration.find(7).objects_count
+ end
+
+ def test_in_use
+ # low priority
+ assert Enumeration.find(4).in_use?
+ # urgent
+ assert !Enumeration.find(7).in_use?
+ end
+
+ def test_default
+ e = Enumeration.default
+ assert e.is_a?(Enumeration)
+ assert e.is_default?
+ assert e.active?
+ assert_equal 'Default Enumeration', e.name
+ end
+
+ def test_default_non_active
+ e = Enumeration.find(12)
+ assert e.is_a?(Enumeration)
+ assert e.is_default?
+ assert e.active?
+ e.update_attributes(:active => false)
+ assert e.is_default?
+ assert !e.active?
+ end
+
+ def test_create
+ e = Enumeration.new(:name => 'Not default', :is_default => false)
+ e.type = 'Enumeration'
+ assert e.save
+ assert_equal 'Default Enumeration', Enumeration.default.name
+ end
+
+ def test_create_as_default
+ e = Enumeration.new(:name => 'Very urgent', :is_default => true)
+ e.type = 'Enumeration'
+ assert e.save
+ assert_equal e, Enumeration.default
+ end
+
+ def test_update_default
+ e = Enumeration.default
+ e.update_attributes(:name => 'Changed', :is_default => true)
+ assert_equal e, Enumeration.default
+ end
+
+ def test_update_default_to_non_default
+ e = Enumeration.default
+ e.update_attributes(:name => 'Changed', :is_default => false)
+ assert_nil Enumeration.default
+ end
+
+ def test_change_default
+ e = Enumeration.find_by_name('Default Enumeration')
+ e.update_attributes(:name => 'Changed Enumeration', :is_default => true)
+ assert_equal e, Enumeration.default
+ end
+
+ def test_destroy_with_reassign
+ Enumeration.find(4).destroy(Enumeration.find(6))
+ assert_nil Issue.where(:priority_id => 4).first
+ assert_equal 6, Enumeration.find(6).objects_count
+ end
+
+ def test_should_be_customizable
+ assert Enumeration.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
+ end
+
+ def test_should_belong_to_a_project
+ association = Enumeration.reflect_on_association(:project)
+ assert association, "No Project association found"
+ assert_equal :belongs_to, association.macro
+ end
+
+ def test_should_act_as_tree
+ enumeration = Enumeration.find(4)
+
+ assert enumeration.respond_to?(:parent)
+ assert enumeration.respond_to?(:children)
+ end
+
+ def test_is_override
+ # Defaults to off
+ enumeration = Enumeration.find(4)
+ assert !enumeration.is_override?
+
+ # Setup as an override
+ enumeration.parent = Enumeration.find(5)
+ assert enumeration.is_override?
+ end
+
+ def test_get_subclasses
+ classes = Enumeration.get_subclasses
+ assert_include IssuePriority, classes
+ assert_include DocumentCategory, classes
+ assert_include TimeEntryActivity, classes
+
+ classes.each do |klass|
+ assert_equal Enumeration, klass.superclass
+ end
+ end
+end
diff --git a/test/unit/group_test.rb b/test/unit/group_test.rb
new file mode 100644
index 000000000..53f1603e2
--- /dev/null
+++ b/test/unit/group_test.rb
@@ -0,0 +1,136 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class GroupTest < ActiveSupport::TestCase
+ fixtures :projects, :trackers, :issue_statuses, :issues,
+ :enumerations, :users,
+ :projects_trackers,
+ :roles,
+ :member_roles,
+ :members,
+ :groups_users
+
+ include Redmine::I18n
+
+ def test_create
+ g = Group.new(:name => 'New group')
+ assert g.save
+ g.reload
+ assert_equal 'New group', g.name
+ end
+
+ def test_name_should_accept_255_characters
+ name = 'a' * 255
+ g = Group.new(:name => name)
+ assert g.save
+ g.reload
+ assert_equal name, g.name
+ end
+
+ def test_blank_name_error_message
+ set_language_if_valid 'en'
+ g = Group.new
+ assert !g.save
+ assert_include "Name can't be blank", g.errors.full_messages
+ end
+
+ def test_blank_name_error_message_fr
+ set_language_if_valid 'fr'
+ str = "Nom doit \xc3\xaatre renseign\xc3\xa9(e)"
+ str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
+ g = Group.new
+ assert !g.save
+ assert_include str, g.errors.full_messages
+ end
+
+ def test_group_roles_should_be_given_to_added_user
+ group = Group.find(11)
+ user = User.find(9)
+ project = Project.first
+
+ Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
+ group.users << user
+ assert user.member_of?(project)
+ end
+
+ def test_new_roles_should_be_given_to_existing_user
+ group = Group.find(11)
+ user = User.find(9)
+ project = Project.first
+
+ group.users << user
+ m = Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
+ assert user.member_of?(project)
+ end
+
+ def test_user_roles_should_updated_when_updating_user_ids
+ group = Group.find(11)
+ user = User.find(9)
+ project = Project.first
+
+ Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
+ group.user_ids = [user.id]
+ group.save!
+ assert User.find(9).member_of?(project)
+
+ group.user_ids = [1]
+ group.save!
+ assert !User.find(9).member_of?(project)
+ end
+
+ def test_user_roles_should_updated_when_updating_group_roles
+ group = Group.find(11)
+ user = User.find(9)
+ project = Project.first
+ group.users << user
+ m = Member.create!(:principal => group, :project => project, :role_ids => [1])
+ assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort
+
+ m.role_ids = [1, 2]
+ assert_equal [1, 2], user.reload.roles_for_project(project).collect(&:id).sort
+
+ m.role_ids = [2]
+ assert_equal [2], user.reload.roles_for_project(project).collect(&:id).sort
+
+ m.role_ids = [1]
+ assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort
+ end
+
+ def test_user_memberships_should_be_removed_when_removing_group_membership
+ assert User.find(8).member_of?(Project.find(5))
+ Member.find_by_project_id_and_user_id(5, 10).destroy
+ assert !User.find(8).member_of?(Project.find(5))
+ end
+
+ def test_user_roles_should_be_removed_when_removing_user_from_group
+ assert User.find(8).member_of?(Project.find(5))
+ User.find(8).groups = []
+ assert !User.find(8).member_of?(Project.find(5))
+ end
+
+ def test_destroy_should_unassign_issues
+ group = Group.first
+ Issue.update_all(["assigned_to_id = ?", group.id], 'id = 1')
+
+ assert group.destroy
+ assert group.destroyed?
+
+ assert_equal nil, Issue.find(1).assigned_to_id
+ end
+end
diff --git a/test/unit/helpers/activities_helper_test.rb b/test/unit/helpers/activities_helper_test.rb
new file mode 100644
index 000000000..e7785dde4
--- /dev/null
+++ b/test/unit/helpers/activities_helper_test.rb
@@ -0,0 +1,101 @@
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class ActivitiesHelperTest < ActionView::TestCase
+ include ActivitiesHelper
+
+ class MockEvent
+ attr_reader :event_datetime, :event_group, :name
+
+ def initialize(group=nil)
+ @@count ||= 0
+ @name = "e#{@@count}"
+ @event_datetime = Time.now + @@count.hours
+ @event_group = group || self
+ @@count += 1
+ end
+
+ def self.clear
+ @@count = 0
+ end
+ end
+
+ def setup
+ MockEvent.clear
+ end
+
+ def test_sort_activity_events_should_sort_by_datetime
+ events = []
+ events << MockEvent.new
+ events << MockEvent.new
+ events << MockEvent.new
+
+ assert_equal [
+ ['e2', false],
+ ['e1', false],
+ ['e0', false]
+ ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]}
+ end
+
+ def test_sort_activity_events_should_group_events
+ events = []
+ events << MockEvent.new
+ events << MockEvent.new(events[0])
+ events << MockEvent.new(events[0])
+
+ assert_equal [
+ ['e2', false],
+ ['e1', true],
+ ['e0', true]
+ ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]}
+ end
+
+ def test_sort_activity_events_with_group_not_in_set_should_group_events
+ e = MockEvent.new
+ events = []
+ events << MockEvent.new(e)
+ events << MockEvent.new(e)
+
+ assert_equal [
+ ['e2', false],
+ ['e1', true]
+ ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]}
+ end
+
+ def test_sort_activity_events_should_sort_by_datetime_and_group
+ events = []
+ events << MockEvent.new
+ events << MockEvent.new
+ events << MockEvent.new
+ events << MockEvent.new(events[1])
+ events << MockEvent.new(events[2])
+ events << MockEvent.new
+ events << MockEvent.new(events[2])
+
+ assert_equal [
+ ['e6', false],
+ ['e4', true],
+ ['e2', true],
+ ['e5', false],
+ ['e3', false],
+ ['e1', true],
+ ['e0', false]
+ ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]}
+ end
+end
diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb
new file mode 100644
index 000000000..e14e60148
--- /dev/null
+++ b/test/unit/helpers/application_helper_test.rb
@@ -0,0 +1,1223 @@
+# encoding: utf-8
+#
+# Redmine - project management software
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+#
+# 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.
+
+require File.expand_path('../../../test_helper', __FILE__)
+
+class ApplicationHelperTest < ActionView::TestCase
+ include ERB::Util
+ include Rails.application.routes.url_helpers
+
+ fixtures :projects, :roles, :enabled_modules, :users,
+ :repositories, :changesets,
+ :trackers, :issue_statuses, :issues, :versions, :documents,
+ :wikis, :wiki_pages, :wiki_contents,
+ :boards, :messages, :news,
+ :attachments, :enumerations
+
+ def setup
+ super
+ set_tmp_attachments_directory
+ end
+
+ context "#link_to_if_authorized" do
+ context "authorized user" do
+ should "be tested"
+ end
+
+ context "unauthorized user" do
+ should "be tested"
+ end
+
+ should "allow using the :controller and :action for the target link" do
+ User.current = User.find_by_login('admin')
+
+ @project = Issue.first.project # Used by helper
+ response = link_to_if_authorized("By controller/action",
+ {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
+ assert_match /href/, response
+ end
+
+ end
+
+ def test_auto_links
+ to_test = {
+ 'http://foo.bar' => 'http://foo.bar',
+ 'http://foo.bar/~user' => 'http://foo.bar/~user',
+ 'http://foo.bar.' => 'http://foo.bar.',
+ 'https://foo.bar.' => 'https://foo.bar.',
+ 'This is a link: http://foo.bar.' => 'This is a link: http://foo.bar.',
+ 'A link (eg. http://foo.bar).' => 'A link (eg. http://foo.bar).',
+ 'http://foo.bar/foo.bar#foo.bar.' => 'http://foo.bar/foo.bar#foo.bar.',
+ 'http://www.foo.bar/Test_(foobar)' => 'http://www.foo.bar/Test_(foobar)',
+ '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : http://www.foo.bar/Test_(foobar))',
+ '(see inline link : http://www.foo.bar/Test)' => '(see inline link : http://www.foo.bar/Test)',
+ '(see inline link : http://www.foo.bar/Test).' => '(see inline link : http://www.foo.bar/Test).',
+ '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see inline link)',
+ '(see "inline link":http://www.foo.bar/Test)' => '(see inline link)',
+ '(see "inline link":http://www.foo.bar/Test).' => '(see inline link).',
+ 'www.foo.bar' => 'www.foo.bar',
+ 'http://foo.bar/page?p=1&t=z&s=' => 'http://foo.bar/page?p=1&t=z&s=',
+ 'http://foo.bar/page#125' => 'http://foo.bar/page#125',
+ 'http://foo@www.bar.com' => 'http://foo@www.bar.com',
+ 'http://foo:bar@www.bar.com' => 'http://foo:bar@www.bar.com',
+ 'ftp://foo.bar' => 'ftp://foo.bar',
+ 'ftps://foo.bar' => 'ftps://foo.bar',
+ 'sftp://foo.bar' => 'sftp://foo.bar',
+ # two exclamation marks
+ 'http://example.net/path!602815048C7B5C20!302.html' => 'http://example.net/path!602815048C7B5C20!302.html',
+ # escaping
+ 'http://foo"bar' => 'http://foo"bar',
+ # wrap in angle brackets
+ '' => '<http://foo.bar>',
+ # invalid urls
+ 'http://' => 'http://',
+ 'www.' => 'www.',
+ 'test-www.bar.com' => 'test-www.bar.com',
+ }
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text) }
+ end
+
+ if 'ruby'.respond_to?(:encoding)
+ def test_auto_links_with_non_ascii_characters
+ to_test = {
+ 'http://foo.bar/тест' => 'http://foo.bar/тест'
+ }
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text) }
+ end
+ else
+ puts 'Skipping test_auto_links_with_non_ascii_characters, unsupported ruby version'
+ end
+
+ def test_auto_mailto
+ to_test = {
+ 'test@foo.bar' => 'test@foo.bar',
+ 'test@www.foo.bar' => 'test@www.foo.bar',
+ }
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text) }
+ end
+
+ def test_inline_images
+ to_test = {
+ '!http://foo.bar/image.jpg!' => '
',
+ 'floating !>http://foo.bar/image.jpg!' => 'floating ',
+ 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class
',
+ 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style
',
+ 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title
',
+ 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title
',
+ }
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text) }
+ end
+
+ def test_inline_images_inside_tags
+ raw = <<-RAW
+h1. !foo.png! Heading
+
+Centered image:
+
+p=. !bar.gif!
+RAW
+
+ assert textilizable(raw).include?('
')
+ assert textilizable(raw).include?('
')
+ end
+
+ def test_attached_images
+ to_test = {
+ 'Inline image: !logo.gif!' => 'Inline image:
',
+ 'Inline image: !logo.GIF!' => 'Inline image:
',
+ 'No match: !ogo.gif!' => 'No match:
',
+ 'No match: !ogo.GIF!' => 'No match:
',
+ # link image
+ '!logo.gif!:http://foo.bar/' => '
',
+ }
+ attachments = Attachment.all
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text, :attachments => attachments) }
+ end
+
+ def test_attached_images_filename_extension
+ set_tmp_attachments_directory
+ a1 = Attachment.new(
+ :container => Issue.find(1),
+ :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
+ :author => User.find(1))
+ assert a1.save
+ assert_equal "testtest.JPG", a1.filename
+ assert_equal "image/jpeg", a1.content_type
+ assert a1.image?
+
+ a2 = Attachment.new(
+ :container => Issue.find(1),
+ :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
+ :author => User.find(1))
+ assert a2.save
+ assert_equal "testtest.jpeg", a2.filename
+ assert_equal "image/jpeg", a2.content_type
+ assert a2.image?
+
+ a3 = Attachment.new(
+ :container => Issue.find(1),
+ :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
+ :author => User.find(1))
+ assert a3.save
+ assert_equal "testtest.JPE", a3.filename
+ assert_equal "image/jpeg", a3.content_type
+ assert a3.image?
+
+ a4 = Attachment.new(
+ :container => Issue.find(1),
+ :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
+ :author => User.find(1))
+ assert a4.save
+ assert_equal "Testtest.BMP", a4.filename
+ assert_equal "image/x-ms-bmp", a4.content_type
+ assert a4.image?
+
+ to_test = {
+ 'Inline image: !testtest.jpg!' =>
+ 'Inline image:
',
+ 'Inline image: !testtest.jpeg!' =>
+ 'Inline image:
',
+ 'Inline image: !testtest.jpe!' =>
+ 'Inline image:
',
+ 'Inline image: !testtest.bmp!' =>
+ 'Inline image:
',
+ }
+
+ attachments = [a1, a2, a3, a4]
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text, :attachments => attachments) }
+ end
+
+ def test_attached_images_should_read_later
+ set_fixtures_attachments_directory
+ a1 = Attachment.find(16)
+ assert_equal "testfile.png", a1.filename
+ assert a1.readable?
+ assert (! a1.visible?(User.anonymous))
+ assert a1.visible?(User.find(2))
+ a2 = Attachment.find(17)
+ assert_equal "testfile.PNG", a2.filename
+ assert a2.readable?
+ assert (! a2.visible?(User.anonymous))
+ assert a2.visible?(User.find(2))
+ assert a1.created_on < a2.created_on
+
+ to_test = {
+ 'Inline image: !testfile.png!' =>
+ 'Inline image:
',
+ 'Inline image: !Testfile.PNG!' =>
+ 'Inline image:
',
+ }
+ attachments = [a1, a2]
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text, :attachments => attachments) }
+ set_tmp_attachments_directory
+ end
+
+ def test_textile_external_links
+ to_test = {
+ 'This is a "link":http://foo.bar' => 'This is a link',
+ 'This is an intern "link":/foo/bar' => 'This is an intern link',
+ '"link (Link title)":http://foo.bar' => 'link',
+ '"link (Link title with "double-quotes")":http://foo.bar' => 'link',
+ "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":
\n\n\n\tAnother paragraph",
+ # no multiline link text
+ "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line
and another on a second line\":test",
+ # mailto link
+ "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "system administrator",
+ # two exclamation marks
+ '"a link":http://example.net/path!602815048C7B5C20!302.html' => 'a link',
+ # escaping
+ '"test":http://foo"bar' => 'test',
+ }
+ to_test.each { |text, result| assert_equal "
#{result}
", textilizable(text) }
+ end
+
+ if 'ruby'.respond_to?(:encoding)
+ def test_textile_external_links_with_non_ascii_characters
+ to_test = {
+ 'This is a "link":http://foo.bar/тест' => 'This is a link'
+ }
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text) }
+ end
+ else
+ puts 'Skipping test_textile_external_links_with_non_ascii_characters, unsupported ruby version'
+ end
+
+ def test_redmine_links
+ issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
+ :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
+ note_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
+ :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
+
+ revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
+ :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
+ revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
+ :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
+
+ changeset_link2 = link_to('691322a8eb01e11fd7',
+ {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
+ :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
+
+ document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
+ :class => 'document')
+
+ version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
+ :class => 'version')
+
+ board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
+
+ message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
+
+ news_url = {:controller => 'news', :action => 'show', :id => 1}
+
+ project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
+
+ source_url = '/projects/ecookbook/repository/entry/some/file'
+ source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
+ source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
+ source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
+ source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
+
+ export_url = '/projects/ecookbook/repository/raw/some/file'
+ export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
+ export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
+ export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
+ export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
+
+ to_test = {
+ # tickets
+ '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
+ # ticket notes
+ '#3-14' => note_link,
+ '#3#note-14' => note_link,
+ # should not ignore leading zero
+ '#03' => '#03',
+ # changesets
+ 'r1' => revision_link,
+ 'r1.' => "#{revision_link}.",
+ 'r1, r2' => "#{revision_link}, #{revision_link2}",
+ 'r1,r2' => "#{revision_link},#{revision_link2}",
+ 'commit:691322a8eb01e11fd7' => changeset_link2,
+ # documents
+ 'document#1' => document_link,
+ 'document:"Test document"' => document_link,
+ # versions
+ 'version#2' => version_link,
+ 'version:1.0' => version_link,
+ 'version:"1.0"' => version_link,
+ # source
+ 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
+ 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
+ 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
+ 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
+ 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
+ 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
+ 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
+ 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
+ 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
+ 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
+ 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
+ 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
+ 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
+ # export
+ 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
+ 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
+ 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
+ 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
+ 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
+ # forum
+ 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
+ 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
+ # message
+ 'message#4' => link_to('Post 2', message_url, :class => 'message'),
+ 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
+ # news
+ 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
+ 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
+ # project
+ 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
+ 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
+ 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
+ # not found
+ '#0123456789' => '#0123456789',
+ # invalid expressions
+ 'source:' => 'source:',
+ # url hash
+ "http://foo.bar/FAQ#3" => 'http://foo.bar/FAQ#3',
+ }
+ @project = Project.find(1)
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text), "#{text} failed" }
+ end
+
+ def test_redmine_links_with_a_different_project_before_current_project
+ vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
+ vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
+
+ @project = Project.find(3)
+ assert_equal %(1.4.4 1.4.4
),
+ textilizable("ecookbook:version:1.4.4 version:1.4.4")
+ end
+
+ def test_escaped_redmine_links_should_not_be_parsed
+ to_test = [
+ '#3.',
+ '#3-14.',
+ '#3#-note14.',
+ 'r1',
+ 'document#1',
+ 'document:"Test document"',
+ 'version#2',
+ 'version:1.0',
+ 'version:"1.0"',
+ 'source:/some/file'
+ ]
+ @project = Project.find(1)
+ to_test.each { |text| assert_equal "#{text}
", textilizable("!" + text), "#{text} failed" }
+ end
+
+ def test_cross_project_redmine_links
+ source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
+ :class => 'source')
+
+ changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
+ :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
+
+ to_test = {
+ # documents
+ 'document:"Test document"' => 'document:"Test document"',
+ 'ecookbook:document:"Test document"' => 'Test document',
+ 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
+ # versions
+ 'version:"1.0"' => 'version:"1.0"',
+ 'ecookbook:version:"1.0"' => '1.0',
+ 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
+ # changeset
+ 'r2' => 'r2',
+ 'ecookbook:r2' => changeset_link,
+ 'invalid:r2' => 'invalid:r2',
+ # source
+ 'source:/some/file' => 'source:/some/file',
+ 'ecookbook:source:/some/file' => source_link,
+ 'invalid:source:/some/file' => 'invalid:source:/some/file',
+ }
+ @project = Project.find(3)
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text), "#{text} failed" }
+ end
+
+ def test_multiple_repositories_redmine_links
+ svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
+ Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
+ hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
+ Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
+
+ changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
+ :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
+ svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
+ :class => 'changeset', :title => '')
+ hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
+ :class => 'changeset', :title => '')
+
+ source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
+ hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
+
+ to_test = {
+ 'r2' => changeset_link,
+ 'svn_repo-1|r123' => svn_changeset_link,
+ 'invalid|r123' => 'invalid|r123',
+ 'commit:hg1|abcd' => hg_changeset_link,
+ 'commit:invalid|abcd' => 'commit:invalid|abcd',
+ # source
+ 'source:some/file' => source_link,
+ 'source:hg1|some/file' => hg_source_link,
+ 'source:invalid|some/file' => 'source:invalid|some/file',
+ }
+
+ @project = Project.find(1)
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text), "#{text} failed" }
+ end
+
+ def test_cross_project_multiple_repositories_redmine_links
+ svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
+ Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
+ hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
+ Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
+
+ changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
+ :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
+ svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
+ :class => 'changeset', :title => '')
+ hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
+ :class => 'changeset', :title => '')
+
+ source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
+ hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
+
+ to_test = {
+ 'ecookbook:r2' => changeset_link,
+ 'ecookbook:svn1|r123' => svn_changeset_link,
+ 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
+ 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
+ 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
+ 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
+ # source
+ 'ecookbook:source:some/file' => source_link,
+ 'ecookbook:source:hg1|some/file' => hg_source_link,
+ 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
+ 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
+ }
+
+ @project = Project.find(3)
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text), "#{text} failed" }
+ end
+
+ def test_redmine_links_git_commit
+ changeset_link = link_to('abcd',
+ {
+ :controller => 'repositories',
+ :action => 'revision',
+ :id => 'subproject1',
+ :rev => 'abcd',
+ },
+ :class => 'changeset', :title => 'test commit')
+ to_test = {
+ 'commit:abcd' => changeset_link,
+ }
+ @project = Project.find(3)
+ r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
+ assert r
+ c = Changeset.new(:repository => r,
+ :committed_on => Time.now,
+ :revision => 'abcd',
+ :scmid => 'abcd',
+ :comments => 'test commit')
+ assert( c.save )
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text) }
+ end
+
+ # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
+ def test_redmine_links_darcs_commit
+ changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
+ {
+ :controller => 'repositories',
+ :action => 'revision',
+ :id => 'subproject1',
+ :rev => '123',
+ },
+ :class => 'changeset', :title => 'test commit')
+ to_test = {
+ 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
+ }
+ @project = Project.find(3)
+ r = Repository::Darcs.create!(
+ :project => @project, :url => '/tmp/test/darcs',
+ :log_encoding => 'UTF-8')
+ assert r
+ c = Changeset.new(:repository => r,
+ :committed_on => Time.now,
+ :revision => '123',
+ :scmid => '20080308225258-98289-abcd456efg.gz',
+ :comments => 'test commit')
+ assert( c.save )
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text) }
+ end
+
+ def test_redmine_links_mercurial_commit
+ changeset_link_rev = link_to('r123',
+ {
+ :controller => 'repositories',
+ :action => 'revision',
+ :id => 'subproject1',
+ :rev => '123' ,
+ },
+ :class => 'changeset', :title => 'test commit')
+ changeset_link_commit = link_to('abcd',
+ {
+ :controller => 'repositories',
+ :action => 'revision',
+ :id => 'subproject1',
+ :rev => 'abcd' ,
+ },
+ :class => 'changeset', :title => 'test commit')
+ to_test = {
+ 'r123' => changeset_link_rev,
+ 'commit:abcd' => changeset_link_commit,
+ }
+ @project = Project.find(3)
+ r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
+ assert r
+ c = Changeset.new(:repository => r,
+ :committed_on => Time.now,
+ :revision => '123',
+ :scmid => 'abcd',
+ :comments => 'test commit')
+ assert( c.save )
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text) }
+ end
+
+ def test_attachment_links
+ to_test = {
+ 'attachment:error281.txt' => 'error281.txt'
+ }
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
+ end
+
+ def test_attachment_link_should_link_to_latest_attachment
+ set_tmp_attachments_directory
+ a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
+ a2 = Attachment.generate!(:filename => "test.txt")
+
+ assert_equal %(test.txt
),
+ textilizable('attachment:test.txt', :attachments => [a1, a2])
+ end
+
+ def test_wiki_links
+ to_test = {
+ '[[CookBook documentation]]' => 'CookBook documentation',
+ '[[Another page|Page]]' => 'Page',
+ # title content should be formatted
+ '[[Another page|With _styled_ *title*]]' => 'With styled title',
+ '[[Another page|With title containing HTML entities & markups]]' => 'With title containing <strong>HTML entities & markups</strong>',
+ # link with anchor
+ '[[CookBook documentation#One-section]]' => 'CookBook documentation',
+ '[[Another page#anchor|Page]]' => 'Page',
+ # UTF8 anchor
+ '[[Another_page#Тест|Тест]]' => %|Тест|,
+ # page that doesn't exist
+ '[[Unknown page]]' => 'Unknown page',
+ '[[Unknown page|404]]' => '404',
+ # link to another project wiki
+ '[[onlinestore:]]' => 'onlinestore',
+ '[[onlinestore:|Wiki]]' => 'Wiki',
+ '[[onlinestore:Start page]]' => 'Start page',
+ '[[onlinestore:Start page|Text]]' => 'Text',
+ '[[onlinestore:Unknown page]]' => 'Unknown page',
+ # striked through link
+ '-[[Another page|Page]]-' => 'Page',
+ '-[[Another page|Page]] link-' => 'Page link',
+ # escaping
+ '![[Another page|Page]]' => '[[Another page|Page]]',
+ # project does not exist
+ '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
+ '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
+ }
+
+ @project = Project.find(1)
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text) }
+ end
+
+ def test_wiki_links_within_local_file_generation_context
+
+ to_test = {
+ # link to a page
+ '[[CookBook documentation]]' => 'CookBook documentation',
+ '[[CookBook documentation|documentation]]' => 'documentation',
+ '[[CookBook documentation#One-section]]' => 'CookBook documentation',
+ '[[CookBook documentation#One-section|documentation]]' => 'documentation',
+ # page that doesn't exist
+ '[[Unknown page]]' => 'Unknown page',
+ '[[Unknown page|404]]' => '404',
+ '[[Unknown page#anchor]]' => 'Unknown page',
+ '[[Unknown page#anchor|404]]' => '404',
+ }
+
+ @project = Project.find(1)
+
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text, :wiki_links => :local) }
+ end
+
+ def test_wiki_links_within_wiki_page_context
+
+ page = WikiPage.find_by_title('Another_page' )
+
+ to_test = {
+ # link to another page
+ '[[CookBook documentation]]' => 'CookBook documentation',
+ '[[CookBook documentation|documentation]]' => 'documentation',
+ '[[CookBook documentation#One-section]]' => 'CookBook documentation',
+ '[[CookBook documentation#One-section|documentation]]' => 'documentation',
+ # link to the current page
+ '[[Another page]]' => 'Another page',
+ '[[Another page|Page]]' => 'Page',
+ '[[Another page#anchor]]' => 'Another page',
+ '[[Another page#anchor|Page]]' => 'Page',
+ # page that doesn't exist
+ '[[Unknown page]]' => 'Unknown page',
+ '[[Unknown page|404]]' => '404',
+ '[[Unknown page#anchor]]' => 'Unknown page',
+ '[[Unknown page#anchor|404]]' => '404',
+ }
+
+ @project = Project.find(1)
+
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(WikiContent.new( :text => text, :page => page ), :text) }
+ end
+
+ def test_wiki_links_anchor_option_should_prepend_page_title_to_href
+
+ to_test = {
+ # link to a page
+ '[[CookBook documentation]]' => 'CookBook documentation',
+ '[[CookBook documentation|documentation]]' => 'documentation',
+ '[[CookBook documentation#One-section]]' => 'CookBook documentation',
+ '[[CookBook documentation#One-section|documentation]]' => 'documentation',
+ # page that doesn't exist
+ '[[Unknown page]]' => 'Unknown page',
+ '[[Unknown page|404]]' => '404',
+ '[[Unknown page#anchor]]' => 'Unknown page',
+ '[[Unknown page#anchor|404]]' => '404',
+ }
+
+ @project = Project.find(1)
+
+ to_test.each { |text, result| assert_equal "#{result}
", textilizable(text, :wiki_links => :anchor) }
+ end
+
+ def test_html_tags
+ to_test = {
+ "content
" => "<div>content</div>
",
+ "content
" => "<div class=\"bold\">content</div>
",
+ "" => "<script>some script;</script>
",
+ # do not escape pre/code tags
+ "\nline 1\nline2
" => "\nline 1\nline2
",
+ "\nline 1\nline2
" => "\nline 1\nline2
",
+ "content
" => "<div>content</div>
",
+ "HTML comment: " => "HTML comment: <!-- no comments -->
",
+ "