diff options
Diffstat (limited to 'test')
45 files changed, 2064 insertions, 90 deletions
diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index 4a6fd0d30..38d69e7c8 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -43,6 +43,11 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase driver_option.add_preference 'download.default_directory', DOWNLOADS_PATH.gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR) driver_option.add_preference 'download.prompt_for_download', false driver_option.add_preference 'plugins.plugins_disabled', ["Chrome PDF Viewer"] + # Disable "Change your password" popup shown after login due to leak detection + driver_option.add_preference 'profile.password_manager_leak_detection', false + # Disable password saving prompts + driver_option.add_preference 'profile.password_manager_enabled', false + driver_option.add_preference 'credentials_enable_service', false end setup do @@ -68,13 +73,13 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase # using default browser locale which depend on system locale for "real" browsers drivers def log_user(login, password) visit '/my/page' - assert_equal '/login', current_path + assert_current_path '/login', :ignore_query => true within('#login-form form') do fill_in 'username', :with => login fill_in 'password', :with => password find('input[name=login]').click end - assert_equal '/my/page', current_path + assert_current_path '/my/page', :ignore_query => true end def wait_for_ajax diff --git a/test/fixtures/reactions.yml b/test/fixtures/reactions.yml new file mode 100644 index 000000000..d8fcbfc1b --- /dev/null +++ b/test/fixtures/reactions.yml @@ -0,0 +1,51 @@ +--- +reaction_001: + id: 1 + reactable_type: Issue + reactable_id: 1 + user_id: 1 +reaction_002: + id: 2 + reactable_type: Issue + reactable_id: 1 + user_id: 2 +reaction_003: + id: 3 + reactable_type: Issue + reactable_id: 1 + user_id: 3 +reaction_004: + id: 4 + reactable_type: Journal + reactable_id: 1 + user_id: 2 +reaction_005: + id: 5 + reactable_type: Issue + reactable_id: 6 + user_id: 2 +reaction_006: + id: 6 + reactable_type: Journal + reactable_id: 4 + user_id: 2 +reaction_007: + id: 7 + reactable_type: News + reactable_id: 1 + user_id: 1 +reaction_008: + id: 8 + reactable_type: Comment + reactable_id: 1 + user_id: 2 +reaction_009: + id: 9 + reactable_type: Message + reactable_id: 7 + user_id: 2 +reaction_010: + id: 10 + reactable_type: News + reactable_id: 3 + user_id: 2 diff --git a/test/functional/attachments_controller_test.rb b/test/functional/attachments_controller_test.rb index 04fdb15d2..c2e7e2f7b 100644 --- a/test/functional/attachments_controller_test.rb +++ b/test/functional/attachments_controller_test.rb @@ -42,7 +42,7 @@ class AttachmentsControllerTest < Redmine::ControllerTest assert_response :success assert_equal 'text/html', @response.media_type - assert_select 'th.filename', :text => /issues_controller.rb\t\(révision 1484\)/ + assert_select 'th.filename', :text => /issues_controller\.rb \(révision 1484\)/ assert_select 'td.line-code', :text => /Demande créée avec succès/ end end @@ -61,7 +61,7 @@ class AttachmentsControllerTest < Redmine::ControllerTest assert_response :success assert_equal 'text/html', @response.media_type - assert_select 'th.filename', :text => /issues_controller.rb\t\(r\?vision 1484\)/ + assert_select 'th.filename', :text => /issues_controller\.rb \(r\?vision 1484\)/ assert_select 'td.line-code', :text => /Demande cr\?\?e avec succ\?s/ end end @@ -81,7 +81,7 @@ class AttachmentsControllerTest < Redmine::ControllerTest assert_response :success assert_equal 'text/html', @response.media_type - assert_select 'th.filename', :text => /issues_controller.rb\t\(révision 1484\)/ + assert_select 'th.filename', :text => /issues_controller\.rb \(révision 1484\)/ assert_select 'td.line-code', :text => /Demande créée avec succès/ end end diff --git a/test/functional/calendars_controller_test.rb b/test/functional/calendars_controller_test.rb index 227919435..202c076ac 100644 --- a/test/functional/calendars_controller_test.rb +++ b/test/functional/calendars_controller_test.rb @@ -57,7 +57,7 @@ class CalendarsControllerTest < Redmine::ControllerTest ) do assert_select 'a.issue[href=?]', '/issues/2', :text => 'Feature request #2' assert_select 'span.tip' do - assert_select 'img[class="gravatar"]' + assert_select 'img[class="gravatar avatar"]' end assert_select 'input[name=?][type=?][value=?]', 'ids[]', 'checkbox', '2' end diff --git a/test/functional/custom_fields_controller_test.rb b/test/functional/custom_fields_controller_test.rb index 0706a2eda..fdd2a4148 100644 --- a/test/functional/custom_fields_controller_test.rb +++ b/test/functional/custom_fields_controller_test.rb @@ -282,6 +282,23 @@ class CustomFieldsControllerTest < Redmine::ControllerTest assert_select '[name=?]', 'custom_field[full_width_layout]', 0 end + def test_setting_ratio_interval_should_be_present_only_for_progressbar_format + get( + :new, + :params => { + :type => 'IssueCustomField', + :custom_field => { + :field_format => 'progressbar' + } + } + ) + assert_response :success + assert_select '[name=?]', 'custom_field[ratio_interval]' do + assert_select 'option[value=?]', '5' + assert_select 'option[value=?][selected=?]', '10', 'selected' + end + end + def test_new_js get( :new, diff --git a/test/functional/documents_controller_test.rb b/test/functional/documents_controller_test.rb index b59ecdc81..944f0b30f 100644 --- a/test/functional/documents_controller_test.rb +++ b/test/functional/documents_controller_test.rb @@ -113,9 +113,9 @@ class DocumentsControllerTest < Redmine::ControllerTest # adds a long description to the first document doc = documents(:documents_001) doc.update(:description => <<~LOREM) - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut egestas, mi vehicula varius varius, ipsum massa fermentum orci, eget tristique ante sem vel mi. Nulla facilisi. Donec enim libero, luctus ac sagittis sit amet, vehicula sagittis magna. Duis ultrices molestie ante, eget scelerisque sem iaculis vitae. Etiam fermentum mauris vitae metus pharetra condimentum fermentum est pretium. Proin sollicitudin elementum quam quis pharetra. Aenean facilisis nunc quis elit volutpat mollis. Aenean eleifend varius euismod. Ut dolor est, congue eget dapibus eget, elementum eu odio. Integer et lectus neque, nec scelerisque nisi. EndOfLineHere + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut egestas, mi vehicula varius varius, ipsum massa fermentum orci, eget tristique ante sem vel mi. Nulla facilisi. Donec enim libero, luctus ac sagittis sit amet, vehicula sagittis magna. Duis ultrices molestie ante, eget scelerisque sem iaculis vitae. Etiam fermentum mauris vitae metus pharetra condimentum fermentum est pretium. Proin sollicitudin elementum quam quis pharetra. Aenean facilisis nunc quis elit volutpat mollis. Aenean eleifend varius euismod. Ut dolor est, congue eget dapibus eget, elementum eu odio. Integer et lectus neque, nec scelerisque nisi. EndOfLineHere - Vestibulum non velit mi. Aliquam scelerisque libero ut nulla fringilla a sollicitudin magna rhoncus. Praesent a nunc lorem, ac porttitor eros. Sed ac diam nec neque interdum adipiscing quis quis justo. Donec arcu nunc, fringilla eu dictum at, venenatis ac sem. Vestibulum quis elit urna, ac mattis sapien. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Vestibulum non velit mi. Aliquam scelerisque libero ut nulla fringilla a sollicitudin magna rhoncus. Praesent a nunc lorem, ac porttitor eros. Sed ac diam nec neque interdum adipiscing quis quis justo. Donec arcu nunc, fringilla eu dictum at, venenatis ac sem. Vestibulum quis elit urna, ac mattis sapien. Lorem ipsum dolor sit amet, consectetur adipiscing elit. LOREM get(:index, :params => {:project_id => 'ecookbook'}) assert_response :success diff --git a/test/functional/gantts_controller_test.rb b/test/functional/gantts_controller_test.rb index daba816b8..73a74ba65 100644 --- a/test/functional/gantts_controller_test.rb +++ b/test/functional/gantts_controller_test.rb @@ -58,7 +58,7 @@ class GanttsControllerTest < Redmine::ControllerTest # Assert context menu on issues subject and gantt bar assert_select 'div[class=?]', 'issue-subject hascontextmenu' assert_select 'div.tooltip.hascontextmenu' do - assert_select 'img[class="gravatar"]' + assert_select 'img[class="gravatar avatar"]' end assert_select "form[data-cm-url=?]", '/issues/context_menu' diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index a6aff636f..4b9b44537 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -1737,7 +1737,7 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'td.last_notes[colspan="4"]', :text => 'Some notes with Redmine links: #2, r2.' assert_select( 'td.last_notes[colspan="4"]', - :text => 'A comment with inline image: and a reference to #1 and r2.' + :text => 'A comment with inline image: and a reference to #1 and r2.' ) get( :index, @@ -2485,7 +2485,7 @@ class IssuesControllerTest < Redmine::ControllerTest end assert_select 'div#tab-content-history' do assert_select 'div[id=?]', "change-#{Issue.find(1).journals.last.id}" do - assert_select 'ul.details', :text => "Subtask ##{issue.id} added" + assert_select 'ul.journal-details', :text => "Subtask ##{issue.id} added" end end end @@ -2816,7 +2816,7 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'h3', {text: /Watchers \(\d*\)/, count: 0} end - def test_show_should_display_watchers_with_gravatars + def test_show_should_display_watchers_with_avatars @request.session[:user_id] = 2 issue = Issue.find(1) issue.add_watcher User.find(2) @@ -2824,9 +2824,10 @@ class IssuesControllerTest < Redmine::ControllerTest with_settings :gravatar_enabled => '1' do get(:show, :params => {:id => 1}) end + assert_select 'div#watchers ul' do assert_select 'li.user-2' do - assert_select 'img.gravatar[title=?]', 'John Smith' + assert_select '.avatar[title=?]', 'John Smith' assert_select 'a[href="/users/2"]' assert_select 'a[class*=delete]' end @@ -3305,7 +3306,7 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'a[title=?][href=?]', 'Edit', '/time_entries/3/edit' assert_select 'a[title=?][href=?]', 'Delete', '/time_entries/3' - assert_select 'ul[class=?]', 'details', :text => /1.00 h/ + assert_select 'ul[class=?]', 'journal-details', :text => /1.00 h/ end end @@ -3331,6 +3332,42 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'span.badge.badge-private', text: 'Private' end + def test_show_should_display_reactions + current_user = User.generate! + + User.add_to_project(current_user, projects(:projects_001), + Role.generate!(users_visibility: 'members_of_visible_projects', permissions: [:view_issues])) + + @request.session[:user_id] = current_user.id + + get :show, params: { id: 1 } + + assert_response :success + + assert_select 'span[data-reaction-button-id=reaction_issue_1]' do + # The current_user can only see members who belong to projects that the current_user has access to. + # Since the Redmine Admin user does not belong to any projects visible to the current_user, + # the Redmine Admin user's name is not displayed in the reaction user list. Instead, "1 other" is shown. + assert_select 'a.reaction-button[title=?]', 'Dave Lopper and John Smith' do + assert_select 'span.icon-label', '2' + end + end + + assert_select 'span[data-reaction-button-id=reaction_journal_1]' do + assert_select 'a.reaction-button[title=?]', 'John Smith' + end + assert_select 'span[data-reaction-button-id=reaction_journal_2] a.reaction-button' + end + + def test_should_not_display_reactions_when_reactions_feature_is_disabled + with_settings reactions_enabled: '0' do + get :show, params: { id: 1 } + + assert_response :success + assert_select 'span[data-reaction-button-id]', false + end + end + def test_show_should_not_display_edit_attachment_icon_for_user_without_edit_issue_permission_on_tracker role = Role.find(2) role.set_permission_trackers 'edit_issues', [2, 3] @@ -5996,6 +6033,16 @@ class IssuesControllerTest < Redmine::ControllerTest end end + def test_get_edit_with_custom_field_progress_bar + cf = IssueCustomField.generate!(:tracker_ids => [1], :is_for_all => true, :field_format => 'progressbar') + + @request.session[:user_id] = 1 + get(:edit, :params => {:id => 1}) + assert_response :success + + assert_select "select[id=?]", "issue_custom_field_values_#{cf.id}", 1 + end + def test_get_edit_with_me_assigned_to_id @request.session[:user_id] = 2 get( @@ -8651,7 +8698,7 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'div#tab-content-history' do assert_select 'div[id=?]', "change-#{parent.journals.last.id}" do - assert_select 'ul.details', :text => "Subtask deleted (##{child.id})" + assert_select 'ul.journal-details', :text => "Subtask deleted (##{child.id})" end end end @@ -8740,31 +8787,27 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'a[href=?][onclick=?]', "/issues/1", "", :text => 'Cancel' end - def test_show_should_display_author_gravatar_only_when_not_assigned + def test_show_should_display_author_avatar_only_when_not_assigned issue = Issue.find(1) assert_nil issue.assigned_to_id @request.session[:user_id] = 1 - with_settings :gravatar_enabled => '1' do - get :show, :params => {:id => issue.id} - assert_select 'div.gravatar-with-child' do - assert_select 'img.gravatar', 1 - end + get :show, :params => {:id => issue.id} + assert_select 'div.avatar-with-child' do + assert_select '.avatar', 1 end end - def test_show_should_display_author_and_assignee_gravatars_when_assigned + def test_show_should_display_author_and_assignee_avatars_when_assigned issue = Issue.find(1) issue.assigned_to_id = 2 issue.save! @request.session[:user_id] = 1 - with_settings :gravatar_enabled => '1' do - get :show, :params => {:id => issue.id} - assert_select 'div.gravatar-with-child' do - assert_select 'img.gravatar', 2 - assert_select 'img.gravatar-child', 1 - end + get :show, :params => {:id => issue.id} + assert_select 'div.avatar-with-child' do + assert_select '.avatar', 2 + assert_select '.avatar-child', 1 end end diff --git a/test/functional/issues_custom_fields_visibility_test.rb b/test/functional/issues_custom_fields_visibility_test.rb index 3955ef861..6bcf2ad4f 100644 --- a/test/functional/issues_custom_fields_visibility_test.rb +++ b/test/functional/issues_custom_fields_visibility_test.rb @@ -101,9 +101,9 @@ class IssuesCustomFieldsVisibilityTest < Redmine::ControllerTest get(:show, :params => {:id => @issue.id}) @fields.each_with_index do |field, i| if fields.include?(field) - assert_select 'ul.details i', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} change" + assert_select 'ul.journal-details i', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} change" else - assert_select 'ul.details i', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name} change" + assert_select 'ul.journal-details i', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name} change" end end end diff --git a/test/functional/messages_controller_test.rb b/test/functional/messages_controller_test.rb index 74b9a3070..cec58ff5f 100644 --- a/test/functional/messages_controller_test.rb +++ b/test/functional/messages_controller_test.rb @@ -28,7 +28,7 @@ class MessagesControllerTest < Redmine::ControllerTest get(:show, :params => {:board_id => 1, :id => 1}) assert_response :success - assert_select 'h2', :text => 'First post' + assert_select 'h2', :text => "RAFirst post" end def test_show_should_contain_reply_field_tags_for_quoting @@ -123,6 +123,27 @@ class MessagesControllerTest < Redmine::ControllerTest assert_select 'h3', {text: /Watchers \(\d*\)/, count: 0} end + def test_show_should_display_reactions + @request.session[:user_id] = 2 + + get :show, params: { board_id: 1, id: 4 } + + assert_response :success + assert_select 'span[data-reaction-button-id=reaction_message_4] a.reaction-button' do + assert_select 'svg use[href*=thumb-up]' + end + assert_select 'span[data-reaction-button-id=reaction_message_5] a.reaction-button' + assert_select 'span[data-reaction-button-id=reaction_message_6] a.reaction-button' + + # Should not display reactions when reactions feature is disabled. + with_settings reactions_enabled: '0' do + get :show, params: { board_id: 1, id: 4 } + + assert_response :success + assert_select 'span[data-reaction-button-id]', false + end + end + def test_get_new @request.session[:user_id] = 2 get(:new, :params => {:board_id => 1}) diff --git a/test/functional/news_controller_test.rb b/test/functional/news_controller_test.rb index f1ddfff71..686fada25 100644 --- a/test/functional/news_controller_test.rb +++ b/test/functional/news_controller_test.rb @@ -75,7 +75,7 @@ class NewsControllerTest < Redmine::ControllerTest get(:show, :params => {:id => 1}) assert_response :success assert_select 'p.breadcrumb a[href=?]', '/projects/ecookbook/news', :text => 'News' - assert_select 'h2', :text => 'eCookbook first release !' + assert_select 'h2', :text => 'JS eCookbook first release !' end def test_show_should_show_attachments @@ -106,6 +106,23 @@ class NewsControllerTest < Redmine::ControllerTest assert_response :not_found end + def test_show_should_display_reactions + @request.session[:user_id] = 1 + + get :show, params: { id: 1 } + assert_response :success + assert_select 'span[data-reaction-button-id=reaction_news_1] a.reaction-button.reacted' + assert_select 'span[data-reaction-button-id=reaction_comment_1] a.reaction-button' + + # Should not display reactions when reactions feature is disabled. + with_settings reactions_enabled: '0' do + get :show, params: { id: 1 } + + assert_response :success + assert_select 'span[data-reaction-button-id]', false + end + end + def test_get_new_with_project_id @request.session[:user_id] = 2 get(:new, :params => {:project_id => 1}) diff --git a/test/functional/reactions_controller_test.rb b/test/functional/reactions_controller_test.rb new file mode 100644 index 000000000..b65794969 --- /dev/null +++ b/test/functional/reactions_controller_test.rb @@ -0,0 +1,394 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- 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_relative '../test_helper' + +class ReactionsControllerTest < Redmine::ControllerTest + setup do + Setting.reactions_enabled = '1' + # jsmith + @request.session[:user_id] = users(:users_002).id + end + + teardown do + Setting.clear_cache + end + + test 'create for issue' do + issue = issues(:issues_002) + + assert_difference( + ->{ Reaction.count } => 1, + ->{ issue.reactions.by(users(:users_002)).count } => 1 + ) do + post :create, params: { + object_type: 'Issue', + object_id: issue.id + }, xhr: true + end + + assert_response :success + end + + test 'create for journal' do + journal = journals(:journals_005) + + assert_difference( + ->{ Reaction.count } => 1, + ->{ journal.reactions.by(users(:users_002)).count } => 1 + ) do + post :create, params: { + object_type: 'Journal', + object_id: journal.id + }, xhr: true + end + + assert_response :success + end + + test 'create for news' do + news = news(:news_002) + + assert_difference( + ->{ Reaction.count } => 1, + ->{ news.reactions.by(users(:users_002)).count } => 1 + ) do + post :create, params: { + object_type: 'News', + object_id: news.id + }, xhr: true + end + + assert_response :success + end + + test 'create reaction for comment' do + comment = comments(:comments_002) + + assert_difference( + ->{ Reaction.count } => 1, + ->{ comment.reactions.by(users(:users_002)).count } => 1 + ) do + post :create, params: { + object_type: 'Comment', + object_id: comment.id + }, xhr: true + end + + assert_response :success + end + + test 'create for message' do + message = messages(:messages_001) + + assert_difference( + ->{ Reaction.count } => 1, + ->{ message.reactions.by(users(:users_002)).count } => 1 + ) do + post :create, params: { + object_type: 'Message', + object_id: message.id + }, xhr: true + end + + assert_response :success + end + + test 'destroy for issue' do + reaction = reactions(:reaction_005) + + assert_difference 'Reaction.count', -1 do + delete :destroy, params: { + id: reaction.id, + # Issue (id=6) + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + assert_not Reaction.exists?(reaction.id) + end + + test 'destroy for journal' do + reaction = reactions(:reaction_006) + + assert_difference 'Reaction.count', -1 do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + assert_not Reaction.exists?(reaction.id) + end + + test 'destroy for news' do + # For News(id=3) + reaction = reactions(:reaction_010) + + assert_difference 'Reaction.count', -1 do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + assert_not Reaction.exists?(reaction.id) + end + + test 'destroy for comment' do + # For Comment(id=1) + reaction = reactions(:reaction_008) + + assert_difference 'Reaction.count', -1 do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + assert_not Reaction.exists?(reaction.id) + end + + test 'destroy for message' do + reaction = reactions(:reaction_009) + + assert_difference 'Reaction.count', -1 do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + assert_not Reaction.exists?(reaction.id) + end + + test 'create should respond with 403 when feature is disabled' do + Setting.reactions_enabled = '0' + # admin + @request.session[:user_id] = users(:users_001).id + + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Issue', + object_id: issues(:issues_002).id + }, xhr: true + end + assert_response :forbidden + end + + test 'destroy should respond with 403 when feature is disabled' do + Setting.reactions_enabled = '0' + # admin + @request.session[:user_id] = users(:users_001).id + + reaction = reactions(:reaction_001) + assert_no_difference 'Reaction.count' do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + assert_response :forbidden + end + + test 'create by anonymou user should respond with 401 when feature is disabled' do + Setting.reactions_enabled = '0' + @request.session[:user_id] = nil + + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Issue', + object_id: issues(:issues_002).id + }, xhr: true + end + assert_response :unauthorized + end + + test 'create by anonymous user should respond with 401' do + @request.session[:user_id] = nil + + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Issue', + # Issue(id=1) is an issue in a public project + object_id: issues(:issues_001).id + }, xhr: true + end + + assert_response :unauthorized + end + + test 'destroy by anonymous user should respond with 401' do + @request.session[:user_id] = nil + + reaction = reactions(:reaction_002) + assert_no_difference 'Reaction.count' do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :unauthorized + end + + test 'create when reaction already exists should not create a new reaction and succeed' do + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Comment', + # user(jsmith) has already reacted to Comment(id=1) + object_id: comments(:comments_001).id + }, xhr: true + end + + assert_response :success + end + + test 'destroy another user reaction should not destroy the reaction and succeed' do + # admin user's reaction + reaction = reactions(:reaction_001) + + assert_no_difference 'Reaction.count' do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + end + + test 'destroy nonexistent reaction' do + # For Journal(id=4) + reaction = reactions(:reaction_006) + reaction.destroy! + + assert_not Reaction.exists?(reaction.id) + + assert_no_difference 'Reaction.count' do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + end + + test 'create with invalid object type should respond with 403' do + # admin + @request.session[:user_id] = users(:users_001).id + + post :create, params: { + object_type: 'InvalidType', + object_id: 1 + }, xhr: true + + assert_response :forbidden + end + + test 'create without permission to view should respond with 403' do + # dlopper + @request.session[:user_id] = users(:users_003).id + + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Issue', + # dlopper is not a member of the project where the issue (id=4) belongs. + object_id: issues(:issues_004).id + }, xhr: true + end + + assert_response :forbidden + end + + test 'destroy without permission to view should respond with 403' do + # dlopper + @request.session[:user_id] = users(:users_003).id + + # For Issue(id=6) + reaction = reactions(:reaction_005) + + assert_no_difference 'Reaction.count' do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :forbidden + end + + test 'create should respond with 404 for non-JS requests' do + issue = issues(:issues_002) + + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Issue', + object_id: issue.id + } # Sending an HTML request by omitting xhr: true + end + + assert_response :not_found + end + + test 'create should respond with 403 when project is closed' do + issue = issues(:issues_010) + issue.project.update!(status: Project::STATUS_CLOSED) + + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Issue', + object_id: issue.id + }, xhr: true + end + + assert_response :forbidden + end + + test 'destroy should respond with 403 when project is closed' do + reaction = reactions(:reaction_005) + reaction.reactable.project.update!(status: Project::STATUS_CLOSED) + + assert_no_difference 'Reaction.count' do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :forbidden + end +end diff --git a/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb index 7de85a78d..0a430317f 100644 --- a/test/functional/repositories_subversion_controller_test.rb +++ b/test/functional/repositories_subversion_controller_test.rb @@ -358,6 +358,27 @@ class RepositoriesSubversionControllerTest < Redmine::RepositoryControllerTest assert_equal "attachment; filename=\"helloworld.c\"; filename*=UTF-8''helloworld.c", @response.headers['Content-Disposition'] end + def test_entry_should_return_text_plain_for_js_files + # JavaScript files should be served as 'text/plain' instead of + # 'application/javascript' to avoid + # ActionController::InvalidCrossOriginRequest exception + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get( + :raw, + :params => { + :id => PRJ_ID, + :repository_id => @repository.id, + :path => repository_path_hash(['subversion_test', 'foo.js'])[:param] + } + ) + assert_response :success + assert_equal 'text/plain', @response.media_type + assert_match /attachment/, @response.headers['Content-Disposition'] + end + def test_directory_entry assert_equal 0, @repository.changesets.count @repository.fetch_changesets diff --git a/test/functional/workflows_controller_test.rb b/test/functional/workflows_controller_test.rb index b30559d80..dcdc8d5bb 100644 --- a/test/functional/workflows_controller_test.rb +++ b/test/functional/workflows_controller_test.rb @@ -211,6 +211,45 @@ class WorkflowsControllerTest < Redmine::ControllerTest assert w.assignee end + def test_post_edit_with_large_number_of_statuses + # This test ensures that workflows with many statuses can be saved. + # Without setting `ENV['RACK_QUERY_PARSER_PARAMS_LIMIT']`, this raises + # ActionController::BadRequest exception due to exceeding the default + # query parameter limit of 4096. + WorkflowTransition.delete_all + + num_statuses = 40 + transitions_data = {} + + # Allowed statuses for a new issue (status_id = 0) + transitions_data['0'] = {} + (1..num_statuses).each do |status_id| + transitions_data['0'][status_id.to_s] = {'always' => '1'} + end + + # Status transitions between statuses + (1..num_statuses).each do |status_id_from| # rubocop:disable RuboCopStyle/CombinableLoops + transitions_data[status_id_from.to_s] = {} + (1..num_statuses).each do |status_id_to| + # skip self-transitions + next if status_id_from == status_id_to + + transitions_data[status_id_from.to_s][status_id_to.to_s] = { + 'always' => '1', 'author' => '1', 'assignee' => '1' + } + end + end + + assert_nothing_raised do + patch :update, :params => { + :role_id => 2, + :tracker_id => 1, + :transitions => transitions_data + } + end + assert_response :found + end + def test_get_permissions get :permissions diff --git a/test/generators/controller_generator_test.rb b/test/generators/controller_generator_test.rb new file mode 100644 index 000000000..46aeac88e --- /dev/null +++ b/test/generators/controller_generator_test.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- 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_relative '../test_helper' +require_relative '../../lib/generators/redmine_plugin_controller/redmine_plugin_controller_generator' + +class ControllerGeneratorTest < Rails::Generators::TestCase + TMP_DIR = Rails.root / 'tmp/test/generators' + + tests RedminePluginControllerGenerator + destination TMP_DIR + setup :prepare_destination + + setup do + @plugin_directory = Redmine::Plugin.directory + Redmine::Plugin.directory = TMP_DIR + end + + teardown do + Redmine::Plugin.directory = @plugin_directory + end + + def test_generates_files_from_templates + g = generator ['ControllerDemo', 'Todo'] + + assert_name g, 'Todo', :controller + + capture(:stdout) do + g.copy_templates + end + + controller_path_names = (Redmine::Plugin.directory / 'controller_demo/app/controllers') + .glob('*.rb') + assert_equal 1, controller_path_names.count + assert_equal 'todo_controller.rb', controller_path_names.first.basename.to_s + + helper_path_names = (Redmine::Plugin.directory / 'controller_demo/app/helpers') + .glob('*.rb') + assert_equal 1, helper_path_names.count + assert_equal 'todo_helper.rb', helper_path_names.first.basename.to_s + + test_path_names = (Redmine::Plugin.directory / 'controller_demo/test/functional') + .glob('*.rb') + assert_equal 1, test_path_names.count + assert_equal 'todo_controller_test.rb', test_path_names.first.basename.to_s + end + + private + + def assert_name(generator, value, method) + assert_equal value, generator.send(method) + end +end diff --git a/test/generators/model_generator_test.rb b/test/generators/model_generator_test.rb new file mode 100644 index 000000000..f030de6f7 --- /dev/null +++ b/test/generators/model_generator_test.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- 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_relative '../test_helper' +require_relative '../../lib/generators/redmine_plugin_model/redmine_plugin_model_generator' + +class ModelGeneratorTest < Rails::Generators::TestCase + TMP_DIR = Rails.root / 'tmp/test/generators' + + tests RedminePluginModelGenerator + destination TMP_DIR + setup :prepare_destination + + setup do + @plugin_directory = Redmine::Plugin.directory + Redmine::Plugin.directory = TMP_DIR + end + + teardown do + Redmine::Plugin.directory = @plugin_directory + end + + def test_generates_files_from_templates + g = generator ['ModelDemo', 'TodoModel'] + + assert_name g, 'TodoModel', :model + + capture(:stdout) do + g.copy_templates + end + + model_path_names = (Redmine::Plugin.directory / 'model_demo/app/models') + .glob('*.rb') + assert_equal 1, model_path_names.count + assert_equal 'todo_model.rb', model_path_names.first.basename.to_s + + test_path_names = (Redmine::Plugin.directory / 'model_demo/test/unit') + .glob('*.rb') + assert_equal 1, test_path_names.count + assert_equal 'todo_model_test.rb', test_path_names.first.basename.to_s + + migration_path_names = (Redmine::Plugin.directory / 'model_demo/db/migrate') + .glob('*.rb') + assert_equal 1, migration_path_names.count + assert_match(/\d+_create_todo_models\.rb/, migration_path_names.first.basename.to_s) + end + + private + + def assert_name(generator, value, method) + assert_equal value, generator.send(method) + end +end diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb index f959744e2..1f60bbbe2 100644 --- a/test/helpers/application_helper_test.rb +++ b/test/helpers/application_helper_test.rb @@ -1732,6 +1732,46 @@ class ApplicationHelperTest < Redmine::HelperTest end end + def test_section_edit_links_with_multiline_heading + raw = <<~RAW + # Wiki + + ## `Foo` Bar + + The heading above generates multiline HTML. + Don't assume heading tags are always single-line. + + ``` + <h2> + <code>Foo</code> Bar</h2> + ``` + RAW + @project = Project.find(1) + set_language_if_valid 'en' + with_settings :text_formatting => 'common_mark' do + result = + textilizable( + raw, + :edit_section_links => + {:controller => 'wiki', :action => 'edit', + :project_id => '1', :id => 'Test'} + ).delete("\n") + + assert_match( + Regexp.new( + '<div class="contextual heading-2" title="Edit this section" id="section-2">' \ + '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=2">' \ + '<svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-.*\.svg#icon--edit"></use></svg>' \ + '<span class="icon-label">Edit this section</span>' \ + '</a></div>' \ + '<a name="Foo-Bar"></a>' \ + '<h2 ><code>Foo</code> Bar<a href="#Foo-Bar" class="wiki-anchor">¶</a></h2>' + ), + result + ) + end + end + def test_default_formatter with_settings :text_formatting => 'unknown' do text = 'a *link*: http://www.example.net/' @@ -2013,20 +2053,6 @@ class ApplicationHelperTest < Redmine::HelperTest end end - def test_principals_check_box_tag_without_avatar - principals = [User.find(1), Group.find(10)] - Setting.gravatar_enabled = '1' - avatar_tags = principals.collect{|p| avatar(p, :size => 16)} - - with_settings :gravatar_enabled => '0' do - tags = principals_check_box_tags(name, principals) - principals.each_with_index do |principal, i| - assert_not_include avatar_tags[i], tags - assert_include content_tag('span', principal_icon(principal), :class => "name icon icon-#{principal.class.name.downcase}"), tags - end - end - end - def test_principals_options_for_select_with_users User.current = nil users = [User.find(2), User.find(4)] @@ -2363,6 +2389,14 @@ class ApplicationHelperTest < Redmine::HelperTest assert_equal expected, format_activity_description(text) end + def test_render_flash_messages_should_ignore_non_string_values + flash[:array_value] = ['1', '2'] + flash[:hash_value] = { foo: 'bar' } + + result = render_flash_messages + assert_equal '', result + end + private def wiki_links_with_special_characters diff --git a/test/helpers/avatars_helper_test.rb b/test/helpers/avatars_helper_test.rb index f407ae09e..6b426bc98 100644 --- a/test/helpers/avatars_helper_test.rb +++ b/test/helpers/avatars_helper_test.rb @@ -63,14 +63,26 @@ class AvatarsHelperTest < Redmine::HelperTest end def test_avatar_css_class - # The default class of the img tag should be gravatar - assert_include 'class="gravatar"', avatar('jsmith <jsmith@somenet.foo>') - assert_include 'class="gravatar picture"', avatar('jsmith <jsmith@somenet.foo>', :class => 'picture') + # The default classes of the img tag should be gravatar and avatar + assert_include 'class="gravatar avatar"', avatar('jsmith <jsmith@somenet.foo>') + assert_include 'class="gravatar avatar picture"', avatar('jsmith <jsmith@somenet.foo>', :class => 'picture') end - def test_avatar_disabled + def test_avatar_with_initials + with_settings :gravatar_default => 'initials' do + assert_include 'initials="RA"', avatar(User.find(1)) + end + end + + def test_avatar_should_reject_initials_if_default_is_not_initials + with_settings :gravatar_default => 'identicon' do + assert_not_include 'initials="RA"', avatar(User.find(1)) + end + end + + def test_avatar_disabled_should_display_user_initials with_settings :gravatar_enabled => '0' do - assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo')) + assert_equal "<span role=\"img\" class=\"avatar-color-2 avatar s24\">JS</span>", avatar(User.find_by_mail('jsmith@somenet.foo')) end end diff --git a/test/helpers/icons_helper_test.rb b/test/helpers/icons_helper_test.rb index ab0b58db4..7ef071f86 100644 --- a/test/helpers/icons_helper_test.rb +++ b/test/helpers/icons_helper_test.rb @@ -71,6 +71,13 @@ class IconsHelperTest < Redmine::HelperTest assert_match expected, icon end + def test_sprite_icon_should_return_svg_with_filled_class_when_style_is_filled + expected = %r{<svg class="s18 icon-svg icon-svg-filled" aria-hidden="true"><use href="/assets/icons-\w+.svg#icon--edit"></use></svg>$} + icon = sprite_icon('edit', style: :filled) + + assert_match expected, icon + end + def test_file_icon_should_return_folder_icon_for_directory entry = stub(:is_dir? => true) expected = %r{<svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-\w+.svg#icon--folder"></use></svg><span class="icon-label">folder_name</span>} diff --git a/test/helpers/journals_helper_test.rb b/test/helpers/journals_helper_test.rb index 355d5ec6f..5c78761ef 100644 --- a/test/helpers/journals_helper_test.rb +++ b/test/helpers/journals_helper_test.rb @@ -47,10 +47,27 @@ class JournalsHelperTest < Redmine::HelperTest journals = issue.visible_journals_with_index # add indice journal_actions = render_journal_actions(issue, journals.first, {reply_links: true}) - assert_select_in journal_actions, 'a[title=?][class="icon icon-comment"]', 'Quote' + assert_select_in journal_actions, 'a[title=?][class="icon-only icon-quote"]', 'Quote' assert_select_in journal_actions, 'a[title=?][class="icon-only icon-edit"]', 'Edit' assert_select_in journal_actions, 'div[class="drdn-items"] a[class="icon icon-del"]' assert_select_in journal_actions, 'div[class="drdn-items"] a[class="icon icon-copy-link"]' + assert_select_in journal_actions, 'span.reaction-button-wrapper' + end + + def test_render_journal_actions_with_journal_without_notes + User.current = User.find(1) + issue = Issue.find(1) + issue.journals.first.update!(notes: '') + + journals = issue.visible_journals_with_index + + journal_actions = render_journal_actions(issue, journals.first, reply_links: true) + + assert_select_in journal_actions, 'span.reaction-button-wrapper' + assert_select_in journal_actions, 'span.drdn' + + assert_select_in journal_actions, 'a[class="icon-only icon-quote"]', false + assert_select_in journal_actions, 'a[class="icon-only icon-edit"]', false end def test_journal_thumbnail_attachments_should_be_in_the_same_order_as_the_journal_details diff --git a/test/helpers/reactions_helper_test.rb b/test/helpers/reactions_helper_test.rb new file mode 100644 index 000000000..1c5c82418 --- /dev/null +++ b/test/helpers/reactions_helper_test.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- 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_relative '../test_helper' + +class ReactionsHelperTest < ActionView::TestCase + include ReactionsHelper + + setup do + User.current = users(:users_002) + Setting.reactions_enabled = '1' + end + + teardown do + Setting.clear_cache + end + + test 'reaction_id_for generates a DOM id' do + assert_equal "reaction_issue_1", reaction_id_for(issues(:issues_001)) + end + + test 'reaction_button returns nil when feature is disabled' do + Setting.reactions_enabled = '0' + + assert_nil reaction_button(issues(:issues_004)) + end + + test 'reaction_button returns nil when object not visible' do + User.current = users(:users_003) + + assert_nil reaction_button(issues(:issues_004)) + end + + test 'reaction_button for anonymous users shows readonly button' do + User.current = nil + + result = reaction_button(journals(:journals_001)) + + assert_select_in result, 'span.reaction-button.readonly[title=?]', 'John Smith' + assert_select_in result, 'a.reaction-button', false + end + + test 'reaction_button for inactive projects shows readonly button' do + issue6 = issues(:issues_006) + issue6.project.update!(status: Project::STATUS_CLOSED) + + result = reaction_button(issue6) + + assert_select_in result, 'span.reaction-button.readonly[title=?]', 'John Smith' + assert_select_in result, 'a.reaction-button', false + end + + test 'reaction_button includes no tooltip when the object has no reactions' do + issue = issues(:issues_002) # Issue without reactions + result = reaction_button(issue) + + assert_select_in result, 'a.reaction-button[title]', false + end + + test 'reaction_button includes tooltip with all usernames when reactions are 10 or fewer' do + issue = issues(:issues_002) + + reactions = build_reactions(10) + issue.reactions += reactions + + result = with_locale 'en' do + reaction_button(issue) + end + + # The tooltip should display usernames in order of newest reactions. + expected_tooltip = 'Bob9 Doe, Bob8 Doe, Bob7 Doe, Bob6 Doe, Bob5 Doe, ' \ + 'Bob4 Doe, Bob3 Doe, Bob2 Doe, Bob1 Doe, and Bob0 Doe' + + assert_select_in result, 'a.reaction-button[title=?]', expected_tooltip + end + + test 'reaction_button includes tooltip with 10 usernames and others count when reactions exceed 10' do + issue = issues(:issues_002) + + reactions = build_reactions(11) + issue.reactions += reactions + + result = with_locale 'en' do + reaction_button(issue) + end + + expected_tooltip = 'Bob10 Doe, Bob9 Doe, Bob8 Doe, Bob7 Doe, Bob6 Doe, ' \ + 'Bob5 Doe, Bob4 Doe, Bob3 Doe, Bob2 Doe, Bob1 Doe, and 1 other' + + assert_select_in result, 'a.reaction-button[title=?]', expected_tooltip + end + + test 'reaction_button should be label less when no reactions' do + issue = issues(:issues_002) + + result = with_locale('en') do + reaction_button(issue) + end + assert_select_in result, 'a.reaction-button' do + assert_select 'span.icon-label', false + end + + # readonly + User.current = nil + result = with_locale('en') do + reaction_button(issue) + end + assert_select_in result, 'span.reaction-button.readonly' do + assert_select 'span.icon-label', false + end + end + + test 'reaction_button should not count and display non-visible users' do + issue2 = issues(:issues_002) + + issue2.reaction_detail = Reaction::Detail.new( + visible_users: users(:users_002, :users_003) + ) + + result = with_locale('en') do + reaction_button(issue2) + end + + assert_select_in result, 'a.reaction-button[title=?]', 'John Smith and Dave Lopper' + + # When all users are non-visible users + issue2.reaction_detail = Reaction::Detail.new( + visible_users: [] + ) + + result = with_locale('en') do + reaction_button(issue2) + end + + assert_select_in result, 'a.reaction-button[title]', false + assert_select_in result, 'a.reaction-button' do + assert_select 'span.icon-label', false + end + end + + test 'reaction_button formats the tooltip content based on the support.array settings of each locale' do + result = with_locale('ja') do + reaction_button(issues(:issues_001)) + end + + assert_select_in result, 'a.reaction-button[title=?]', 'Dave Lopper、John Smith、Redmine Admin' + end + + test 'reaction_button for reacted object' do + User.current = users(:users_002) + + issue = issues(:issues_001) + + result = with_locale('en') do + reaction_button(issue) + end + tooltip = 'Dave Lopper, John Smith, and Redmine Admin' + + assert_select_in result, 'span.reaction-button-wrapper[data-reaction-button-id=?]', 'reaction_issue_1' do + href = reaction_path(issue.reaction_detail.user_reaction, object_type: 'Issue', object_id: 1) + + assert_select 'a.icon.reaction-button.reacted[href=?]', href do + assert_select 'use[href*=?]', 'thumb-up-filled' + assert_select 'span.icon-label', '3' + end + + assert_select 'span.reaction-button', false + end + end + + test 'reaction_button for non-reacted object' do + User.current = users(:users_004) + + issue = issues(:issues_001) + + result = with_locale('en') do + reaction_button(issue) + end + tooltip = 'Dave Lopper, John Smith, and Redmine Admin' + + assert_select_in result, 'span.reaction-button-wrapper[data-reaction-button-id=?]', 'reaction_issue_1' do + href = reactions_path(object_type: 'Issue', object_id: 1) + + assert_select 'a.icon.reaction-button[href=?]', href do + assert_select 'use[href*=?]', 'thumb-up' + assert_select 'span.icon-label', '3' + end + + assert_select 'span.reaction-button', false + end + end + + private + + def build_reactions(count) + Array.new(count) do |i| + Reaction.new(user: User.generate!(firstname: "Bob#{i}")) + end + end +end diff --git a/test/integration/api_test/authentication_test.rb b/test/integration/api_test/authentication_test.rb index 23641b53a..4145fb969 100644 --- a/test/integration/api_test/authentication_test.rb +++ b/test/integration/api_test/authentication_test.rb @@ -127,20 +127,23 @@ class Redmine::ApiTest::AuthenticationTest < Redmine::ApiTest::Base assert_response :unauthorized end + # TODO: check why this test does not use the API endpoint def test_api_should_accept_switch_user_header_for_admin_user user = User.find(1) su = User.find(4) get '/users/current', :headers => {'X-Redmine-API-Key' => user.api_key, 'X-Redmine-Switch-User' => su.login} assert_response :success - assert_select 'h2', :text => su.name + assert_select 'h2', :text => "#{su.initials} #{su.name}" end + # TODO: check why this test does not use the API endpoint def test_api_should_respond_with_412_when_trying_to_switch_to_a_invalid_user get '/users/current', :headers => {'X-Redmine-API-Key' => User.find(1).api_key, 'X-Redmine-Switch-User' => 'foobar'} assert_response :precondition_failed end + # TODO: check why this test does not use the API endpoint def test_api_should_respond_with_412_when_trying_to_switch_to_a_locked_user user = User.find(5) assert user.locked? @@ -149,12 +152,13 @@ class Redmine::ApiTest::AuthenticationTest < Redmine::ApiTest::Base assert_response :precondition_failed end + # TODO: check why this test does not use the API endpoint def test_api_should_not_accept_switch_user_header_for_non_admin_user user = User.find(2) su = User.find(4) get '/users/current', :headers => {'X-Redmine-API-Key' => user.api_key, 'X-Redmine-Switch-User' => su.login} assert_response :success - assert_select 'h2', :text => user.name + assert_select 'h2', :text => "#{user.initials} #{user.name}" end end diff --git a/test/integration/api_test/news_test.rb b/test/integration/api_test/news_test.rb index bd9f2bb6d..399b2b347 100644 --- a/test/integration/api_test/news_test.rb +++ b/test/integration/api_test/news_test.rb @@ -62,7 +62,7 @@ class Redmine::ApiTest::NewsTest < Redmine::ApiTest::Base assert_select "author[id=2][name=\"John Smith\"]" assert_select 'title', 'eCookbook first release !' assert_select 'summary', 'First version was released...' - assert_select 'description', "eCookbook 1.0 has been released.\n\nVisit http://ecookbook.somenet.foo/" + assert_select 'description', 'eCookbook 1.0 has been released. Visit http://ecookbook.somenet.foo/' assert_select 'created_on', News.find(1).created_on.iso8601 end end diff --git a/test/integration/issues_test.rb b/test/integration/issues_test.rb index ac8d432f1..d0763091e 100644 --- a/test/integration/issues_test.rb +++ b/test/integration/issues_test.rb @@ -355,7 +355,7 @@ class IssuesTest < Redmine::IntegrationTest end # Issue view follow_redirect! - assert_select 'ul.details li', :text => "Tester changed from #{tester} to #{new_tester}" + assert_select 'ul.journal-details li', :text => "Tester changed from #{tester} to #{new_tester}" end end diff --git a/test/system/issues_reply_test.rb b/test/system/issues_reply_test.rb index 86cd21f16..76ed485a5 100644 --- a/test/system/issues_reply_test.rb +++ b/test/system/issues_reply_test.rb @@ -93,7 +93,7 @@ class IssuesReplyTest < ApplicationSystemTestCase # Select the entire details of the note#1 and the part of the note#1's text. page.execute_script <<-JS const range = document.createRange(); - range.setStartBefore(document.querySelector('#change-1 .details')); + range.setStartBefore(document.querySelector('#change-1 .journal-details')); // Select only the text "Journal" from the text "Journal notes" in the note-1. range.setEnd(document.querySelector('#change-1 .wiki > p').childNodes[0], 7); diff --git a/test/system/issues_test.rb b/test/system/issues_test.rb index 80ef25e0c..c161538e7 100644 --- a/test/system/issues_test.rb +++ b/test/system/issues_test.rb @@ -34,6 +34,8 @@ class IssuesSystemTest < ApplicationSystemTestCase find('input[name=commit]').click end + assert_text /Issue #\d+ created./ + # find created issue issue = Issue.find_by_subject("new test issue") assert_kind_of Issue, issue @@ -86,6 +88,7 @@ class IssuesSystemTest < ApplicationSystemTestCase fill_in field2.name, :with => 'CF2 value' assert_difference 'Issue.count' do page.first(:button, 'Create').click + assert_text /Issue #\d+ created./ end issue = Issue.order('id desc').first @@ -125,6 +128,7 @@ class IssuesSystemTest < ApplicationSystemTestCase end assert_difference 'Issue.count' do find('input[name=commit]').click + assert_text /Issue #\d+ created./ end issue = Issue.order('id desc').first @@ -141,6 +145,7 @@ class IssuesSystemTest < ApplicationSystemTestCase attach_file 'attachments[dummy][file]', Rails.root.join('test/fixtures/files/testfile.txt') fill_in 'attachments[1][description]', :with => 'Some description' click_on 'Create' + assert_text /Issue #\d+ created./ end assert_equal 1, issue.attachments.count assert_equal 'Some description', issue.attachments.first.description @@ -163,6 +168,7 @@ class IssuesSystemTest < ApplicationSystemTestCase attach_file 'attachments[dummy][file]', Rails.root.join('test/fixtures/files/testfile.txt') fill_in 'attachments[1][description]', :with => 'Some description' click_on 'Create' + assert_text /Issue #\d+ created./ end assert_equal 1, issue.attachments.count assert_equal 'Some description', issue.attachments.first.description @@ -181,6 +187,7 @@ class IssuesSystemTest < ApplicationSystemTestCase click_on 'Create' end click_on 'Create' + assert_text /Issue #\d+ created./ end end @@ -200,6 +207,7 @@ class IssuesSystemTest < ApplicationSystemTestCase end assert_difference 'Issue.count' do click_button('Create') + assert_text /Issue #\d+ created./ end issue = Issue.order('id desc').first @@ -230,6 +238,7 @@ class IssuesSystemTest < ApplicationSystemTestCase fill_in 'Form update CF', :with => 'CF value' assert_no_difference 'Issue.count' do page.first(:button, 'Submit').click + assert_text 'Successful update.' end assert page.has_css?('#flash_notice') issue = Issue.find(1) @@ -245,6 +254,7 @@ class IssuesSystemTest < ApplicationSystemTestCase page.find("#issue_status_id").select("Closed") assert_no_difference 'Issue.count' do page.first(:button, 'Submit').click + assert_text 'Successful update.' end assert page.has_css?('#flash_notice') assert_equal 5, issue.reload.status.id @@ -267,6 +277,7 @@ class IssuesSystemTest < ApplicationSystemTestCase click_on 'Submit' + assert_text 'Successful update.' assert_equal 3, Issue.find(2).attachments.count end diff --git a/test/system/messages_test.rb b/test/system/messages_test.rb index 66a29a3a7..ac074ac1a 100644 --- a/test/system/messages_test.rb +++ b/test/system/messages_test.rb @@ -22,7 +22,7 @@ require_relative '../application_system_test_case' class MessagesTest < ApplicationSystemTestCase def test_reply_to_topic_message with_text_formatting 'common_mark' do - within '#content > .contextual' do + within '#content > [data-controller="quote-reply"]' do click_link 'Quote' end @@ -64,7 +64,7 @@ class MessagesTest < ApplicationSystemTestCase window.getSelection().addRange(range); JS - within '#content > .contextual' do + within '#content > [data-controller="quote-reply"]' do click_link 'Quote' end diff --git a/test/system/oauth_provider_test.rb b/test/system/oauth_provider_test.rb new file mode 100644 index 000000000..364ed4c94 --- /dev/null +++ b/test/system/oauth_provider_test.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require_relative '../application_system_test_case' +require 'oauth2' +require 'rack' +require 'puma' + +class OauthProviderSystemTest < ApplicationSystemTestCase + fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles, + :trackers, :projects_trackers, :enabled_modules, :issue_statuses, :issues, + :enumerations, :custom_fields, :custom_values, :custom_fields_trackers, + :watchers, :journals, :journal_details, :versions, + :workflows + + test 'application creation and authorization' do + # + # admin creates the application, granting permissions and generating a uuid + # and secret. + # + log_user 'admin', 'admin' + with_settings rest_api_enabled: 1 do + visit '/admin' + within 'div#admin-menu ul' do + click_link 'Applications' + end + click_link 'New Application' + fill_in 'Name', with: 'Oauth Test' + + # as per https://tools.ietf.org/html/rfc8252#section-7.3, the port can be + # anything when the redirect URI's host is 127.0.0.1. + fill_in 'Redirect URI', with: 'http://127.0.0.1' + + check 'View Issues' + click_button 'Create' + + assert_text "Application created." + end + + assert app = Doorkeeper::Application.find_by_name('Oauth Test') + + find 'h2', visible: true, text: /Oauth Test/ + find 'p code', visible: true, text: app.uid + find 'p strong', visible: true, text: /will not be shown again/ + find 'p code', visible: true, text: /View Issues/ + + # scrape the clear text secret from the page + app_secret = all(:css, 'p code')[1].text + + click_link 'Sign out' + + # + # regular user authorizes the application + # + client = OAuth2::Client.new(app.uid, app_secret, site: "http://127.0.0.1:#{test_port}/") + + # set up a dummy http listener to handle the redirect + port = rand 10000..20000 + redirect_uri = "http://127.0.0.1:#{port}" + # the request handler below will set this to the auth token + token = nil + + # launches webrick, listening for the redirect with the auth code. + launch_client_app(port: port) do |req, res| + # get access code from code url param + if code = req.params['code'].presence + # exchange it for token + token = client.auth_code.get_token(code, redirect_uri: redirect_uri) + res.body = ["<html><body><p>Authorization succeeded, you may close this window now.</p></body></html>"] + end + end + + log_user 'jsmith', 'jsmith' + with_settings rest_api_enabled: 1 do + visit '/my/account' + click_link 'Authorized applications' + find 'p.nodata', visible: true + + # an oauth client would send the user to this url to request permission + url = client.auth_code.authorize_url redirect_uri: redirect_uri, scope: 'view_issues view_project' + uri = URI.parse url + visit uri.path + '?' + uri.query + + find 'h2', visible: true, text: 'Authorization required' + find 'p', visible: true, text: /Authorize Oauth Test/ + find '.oauth-permissions', visible: true, text: /View Issues/ + find '.oauth-permissions', visible: true, text: /View project/ + + click_button 'Authorize' + + assert grant = app.access_grants.last + assert_equal 'view_issues view_project', grant.scopes.to_s + + # check for output defined above in the request handler + find 'p', visible: true, text: /Authorization succeeded/ + assert token.present? + + visit '/my/account' + click_link 'Authorized applications' + find 'td', visible: true, text: /Oauth Test/ + click_link 'Sign out' + + # Now, use the token for some API requests + assert_raise(RestClient::Unauthorized) do + RestClient.get "http://localhost:#{test_port}/projects/onlinestore/issues.json" + end + + headers = { 'Authorization' => "Bearer #{token.token}" } + r = RestClient.get "http://localhost:#{test_port}/projects/onlinestore/issues.json", headers + issues = JSON.parse(r.body)['issues'] + assert issues.any? + + # time entries access is not part of the granted scopes + assert_raise(RestClient::Forbidden) do + RestClient.get "http://localhost:#{test_port}/projects/onlinestore/time_entries.json", headers + end + end + end + + private + + def launch_client_app(port: 12345, path: '/', &block) + app = ->(env) do + req = Rack::Request.new(env) + res = Rack::Response.new + yield(req, res) + res.finish + end + + server = Puma::Server.new app + server.add_tcp_listener '127.0.0.1', port + Thread.new { server.run } + end + + def test_port + Capybara.current_session.server.port + end +end diff --git a/test/system/reactions_test.rb b/test/system/reactions_test.rb new file mode 100644 index 000000000..96dd4cf81 --- /dev/null +++ b/test/system/reactions_test.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- 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_relative '../application_system_test_case' + +class ReactionsSystemTest < ApplicationSystemTestCase + def test_react_to_issue + log_user('jsmith', 'jsmith') + + issue = issues(:issues_002) + + with_settings(reactions_enabled: '1') do + visit '/issues/2' + reaction_button = find("div.issue.details [data-reaction-button-id=\"reaction_issue_#{issue.id}\"]") + assert_reaction_add_and_remove(reaction_button, issue) + end + end + + def test_react_to_journal + log_user('jsmith', 'jsmith') + + journal = journals(:journals_002) + + with_settings(reactions_enabled: '1') do + visit '/issues/1' + reaction_button = find("[data-reaction-button-id=\"reaction_journal_#{journal.id}\"]") + assert_reaction_add_and_remove(reaction_button, journal.reload) + end + end + + def test_react_to_forum_reply + log_user('jsmith', 'jsmith') + + reply_message = messages(:messages_002) # reply to message_001 + + with_settings(reactions_enabled: '1') do + visit 'boards/1/topics/1' + reaction_button = find("[data-reaction-button-id=\"reaction_message_#{reply_message.id}\"]") + assert_reaction_add_and_remove(reaction_button, reply_message) + end + end + + def test_react_to_forum_message + log_user('jsmith', 'jsmith') + + message = messages(:messages_001) + + with_settings(reactions_enabled: '1') do + visit 'boards/1/topics/1' + reaction_button = find("[data-reaction-button-id=\"reaction_message_#{message.id}\"]") + assert_reaction_add_and_remove(reaction_button, message) + end + end + + def test_react_to_news + log_user('jsmith', 'jsmith') + + with_settings(reactions_enabled: '1') do + visit '/news/2' + reaction_button = find("[data-reaction-button-id=\"reaction_news_2\"]") + assert_reaction_add_and_remove(reaction_button, news(:news_002)) + end + end + + def test_react_to_comment + log_user('jsmith', 'jsmith') + + comment = comments(:comments_002) + + with_settings(reactions_enabled: '1') do + visit '/news/1' + reaction_button = find("[data-reaction-button-id=\"reaction_comment_#{comment.id}\"]") + assert_reaction_add_and_remove(reaction_button, comment) + end + end + + def test_reactions_disabled + log_user('jsmith', 'jsmith') + + with_settings(reactions_enabled: '0') do + visit '/issues/1' + assert_no_selector('[data-reaction-button-id="reaction_issue_1"]') + end + end + + def test_reaction_button_is_visible_but_not_clickable_for_not_logged_in_user + with_settings(reactions_enabled: '1') do + visit '/issues/1' + + # visible + reaction_button = find('div.issue.details [data-reaction-button-id="reaction_issue_1"]') + within(reaction_button) { assert_selector('span.reaction-button') } + assert_equal "3", reaction_button.text + + # not clickable + within(reaction_button) { assert_no_selector('a.reaction-button') } + end + end + + def test_reaction_button_is_visible_on_property_changes_tab + # Create a journal with no notes + journal_without_notes = Journal.generate!(journalized: issues(:issues_001), notes: '', details: [JournalDetail.new]) + + log_user('jsmith', 'jsmith') + + visit '/issues/1?tab=properties' + + # Scroll to the history content + click_link '#1' + + assert_selector '#tab-properties.selected' + + within('#change-1') do + assert_selector 'a.reaction-button' + + assert_no_selector 'a.icon-quote' + assert_no_selector 'span.drdn' + end + within("#change-#{journal_without_notes.id}") do + assert_selector 'a.reaction-button' + + assert_no_selector '.drdn' + end + + click_link 'History' + + within('#change-1') do + assert_selector 'a.reaction-button' + + assert_selector 'a.icon-quote' + assert_selector 'span.drdn' + end + within("#change-#{journal_without_notes.id}") do + assert_selector 'a.reaction-button' + assert_selector 'span.drdn' + + assert_no_selector 'a.icon-quote' + end + end + + private + + def assert_reaction_add_and_remove(reaction_button, expected_subject) + # Add a reaction + within(reaction_button) { find('a.reaction-button').click } + find('body').hover # Hide tooltip + within(reaction_button) { assert_selector('a.reaction-button.reacted[title="John Smith"]') } + assert_equal "1", reaction_button.text + assert_equal 1, expected_subject.reactions.count + + # Remove the reaction + within(reaction_button) { find('a.reacted').click } + within(reaction_button) { assert_selector('a.reaction-button:not(.reacted)') } + assert_equal "", reaction_button.text + assert_equal 0, expected_subject.reactions.count + end +end diff --git a/test/system/sticky_issue_header_test.rb b/test/system/sticky_issue_header_test.rb new file mode 100644 index 000000000..5c67b36f3 --- /dev/null +++ b/test/system/sticky_issue_header_test.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- 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_relative '../application_system_test_case' +class StickyIssueHeaderSystemTest < ApplicationSystemTestCase + test "sticky issue header is hidden by default" do + issue = Issue.find(1) + visit issue_path(issue) + + assert_no_selector "#sticky-issue-header", text: issue.subject + end + + test "sticky issue header appears on scroll" do + issue = Issue.find(2) + visit issue_path(issue) + + page.execute_script("window.scrollTo(0, 1000)") + assert_selector "#sticky-issue-header.is-visible", text: issue.subject + + page.execute_script("window.scrollTo(0, 0)") + assert_no_selector "#sticky-issue-header", text: issue.subject + end +end diff --git a/test/system/sudo_mode_test.rb b/test/system/sudo_mode_test.rb index 73e755acd..307d465ff 100644 --- a/test/system/sudo_mode_test.rb +++ b/test/system/sudo_mode_test.rb @@ -48,7 +48,6 @@ class SudoModeSystemTest < ApplicationSystemTestCase find('input[name=commit]').click end - assert_equal '/users', current_path assert page.has_content?("Confirm your password to continue") assert page.has_css?('form#sudo-form') @@ -56,6 +55,8 @@ class SudoModeSystemTest < ApplicationSystemTestCase fill_in 'Password', :with => 'admin' click_button 'Submit' end + + assert_text /User johnpaul created./ end end diff --git a/test/system/timelog_test.rb b/test/system/timelog_test.rb index 57c521096..38c3ae19c 100644 --- a/test/system/timelog_test.rb +++ b/test/system/timelog_test.rb @@ -49,6 +49,8 @@ class TimelogTest < ApplicationSystemTestCase select 'QA', :from => 'Activity' page.first(:button, 'Submit').click + assert_text 'Successful update.' + entries = TimeEntry.where(:id => [1, 2, 3]).to_a assert entries.all? {|entry| entry.hours == 8.5} assert entries.all? {|entry| entry.activity.name == 'QA'} @@ -89,6 +91,7 @@ class TimelogTest < ApplicationSystemTestCase select 'Tracker', :from => 'Available Columns' page.first('input[type=button].move-right').click click_on 'Save' + assert_text 'Successful update.' # Display the list with updated settings visit '/time_entries' diff --git a/test/unit/lib/redmine/field_format/progressbar_format_test.rb b/test/unit/lib/redmine/field_format/progressbar_format_test.rb index d507f9ddb..6e0df724d 100644 --- a/test/unit/lib/redmine/field_format/progressbar_format_test.rb +++ b/test/unit/lib/redmine/field_format/progressbar_format_test.rb @@ -21,7 +21,7 @@ require_relative '../../../../test_helper' require 'redmine/field_format' module Redmine::FieldFormat - class ProgressbarFormatTest < ActiveSupport::TestCase + class ProgressbarFormatTest < ActionView::TestCase def setup @field = IssueCustomField.new(name: 'ProgressbarTest', field_format: 'progressbar') @format = Redmine::FieldFormat::ProgressbarFormat.instance @@ -81,5 +81,52 @@ module Redmine::FieldFormat options = @format.query_filter_options(@field, nil) assert_equal :integer, options[:type] end + + def test_default_ratio_interval_should_be_default_issue_done_ratio_interval + @field.save + assert_equal 10, @field.ratio_interval + end + + def test_ratio_interval + @field.update(ratio_interval: 5) + assert_equal 5, @field.ratio_interval + end + + def test_edit_tag_possible_values_with_ratio_interval + [5, 10].each do |ratio_interval| + @field.update(ratio_interval: ratio_interval) + value = CustomValue.new(custom_field: @field, value: '90') + + tag = @field.format.edit_tag(self, 'id', 'name', value) + assert_select_in tag, 'select' do + assert_select 'option', 100 / ratio_interval + 1 + end + end + end + + def test_bulk_edit_tag_possible_values_with_ratio_interval + [5, 10].each do |ratio_interval| + @field.update(ratio_interval: ratio_interval) + value = CustomValue.new(custom_field: @field, value: '90') + objects = [Issue.new, Issue.new] + + tag = @field.format.bulk_edit_tag(self, 'id', 'name', @field, objects, value) + assert_select_in tag, 'select' do |select| + assert_select select.first, 'option', 100 / ratio_interval + 2 + end + end + end + + def test_formatted_value_with_html_true + expected = progress_bar(50) + formatted = @format.formatted_value(self, @field, 50, Issue.new, true) + assert_equal expected, formatted + assert formatted.html_safe? + end + + def test_formatted_value_with_html_false + formatted = @format.formatted_value(self, @field, 50, Issue.new, false) + assert_equal '50', formatted + end end end diff --git a/test/unit/lib/redmine/quote_reply_helper_test.rb b/test/unit/lib/redmine/quote_reply_helper_test.rb index 43adb521b..d5d13d4f8 100644 --- a/test/unit/lib/redmine/quote_reply_helper_test.rb +++ b/test/unit/lib/redmine/quote_reply_helper_test.rb @@ -23,18 +23,18 @@ class QuoteReplyHelperTest < ActionView::TestCase include ERB::Util include Redmine::QuoteReply::Helper - def test_quote_reply + def test_quote_reply_button with_locale 'en' do url = quoted_issue_path(issues(:issues_001)) - a_tag = quote_reply(url, '#issue_description_wiki') - assert_includes a_tag, %|onclick="#{h "quoteReply('/issues/1/quoted', '#issue_description_wiki', 'common_mark'); return false;"}"| - assert_includes a_tag, %|class="icon icon-comment"| - assert_not_includes a_tag, 'title=' + html = quote_reply_button(url: url) + assert_select_in html, + 'a[data-quote-reply-url-param=?][data-quote-reply-text-formatting-param=?]:not([title])', + url, Setting.text_formatting # When icon_only is true - a_tag = quote_reply(url, '#issue_description_wiki', icon_only: true) - assert_includes a_tag, %|title="Quote"| + html = quote_reply_button(url: url, icon_only: true) + assert_select_in html, 'a.icon-only.icon-quote[title=?]', 'Quote' end end end diff --git a/test/unit/lib/redmine/reaction_test.rb b/test/unit/lib/redmine/reaction_test.rb new file mode 100644 index 000000000..f3228a3bd --- /dev/null +++ b/test/unit/lib/redmine/reaction_test.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- 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_relative '../../../test_helper' + +class Redmine::ReactionTest < ActiveSupport::TestCase + setup do + @user = users(:users_002) + @issue = issues(:issues_007) + Setting.reactions_enabled = '1' + end + + teardown do + Setting.clear_cache + end + + test 'preload_reaction_details preloads ReactionDetail for all objects in the collection' do + User.current = users(:users_002) + + issue1 = issues(:issues_001) + issue2 = issues(:issues_002) + + assert_nil issue1.instance_variable_get(:@reaction_detail) + assert_nil issue2.instance_variable_get(:@reaction_detail) + + Issue.preload_reaction_details([issue1, issue2]) + + expected_issue1_reaction_detail = Reaction::Detail.new( + visible_users: [users(:users_003), users(:users_002), users(:users_001)], + user_reaction: reactions(:reaction_002) + ) + + # ReactionDetail is already preloaded, so calling reaction_detail does not execute any query. + assert_no_queries do + assert_equal expected_issue1_reaction_detail, issue1.reaction_detail + + # Even when an object has no reactions, an empty ReactionDetail is set. + assert_equal Reaction::Detail.new( + visible_users: [], + user_reaction: nil + ), issue2.reaction_detail + end + end + + test 'visible_users in ReactionDetail preloaded by preload_reaction_details does not include non-visible users' do + current_user = User.current = User.generate! + visible_user = users(:users_002) + non_visible_user = User.generate! + + project = Project.generate! + role = Role.generate!(users_visibility: 'members_of_visible_projects') + + User.add_to_project(current_user, project, role) + User.add_to_project(visible_user, project, roles(:roles_001)) + + issue = Issue.generate!(project: project) + + [current_user, visible_user, non_visible_user].each do |user| + issue.reactions.create!(user: user) + end + + Issue.preload_reaction_details([issue]) + + # non_visible_user is not visible to current_user because they do not belong to any project. + assert_equal [visible_user, current_user], issue.reaction_detail.visible_users + end + + test 'preload_reaction_details does nothing when the reaction feature is disabled' do + Setting.reactions_enabled = '0' + + User.current = users(:users_002) + news1 = news(:news_001) + + # Stub the Setting to avoid executing queries for retrieving settings, + # making it easier to confirm no queries are executed by preload_reaction_details(). + Setting.stubs(:reactions_enabled?).returns(false) + + assert_no_queries do + News.preload_reaction_details([news1]) + end + + assert_nil news1.instance_variable_get(:@reaction_detail) + end + + test 'reaction_detail loads and returns ReactionDetail if it is not preloaded' do + message7 = messages(:messages_007) + + User.current = users(:users_002) + assert_nil message7.instance_variable_get(:@reaction_detail) + + assert_equal Reaction::Detail.new( + visible_users: [users(:users_002)], + user_reaction: reactions(:reaction_009) + ), message7.reaction_detail + end + + test 'load_reaction_detail loads ReactionDetail for the object itself' do + comment1 = comments(:comments_001) + + User.current = users(:users_001) + assert_nil comment1.instance_variable_get(:@reaction_detail) + + comment1.load_reaction_detail + + assert_equal Reaction::Detail.new( + visible_users: [users(:users_002)], + user_reaction: nil + ), comment1.reaction_detail + end + + test 'visible? returns true when reactions are enabled and object is visible to user' do + object = issues(:issues_007) + user = users(:users_002) + + assert Redmine::Reaction.visible?(object, user) + end + + test 'visible? returns false when reactions are disabled' do + Setting.reactions_enabled = '0' + + object = issues(:issues_007) + user = users(:users_002) + + assert_not Redmine::Reaction.visible?(object, user) + end + + test 'visible? returns false when object is not visible to user' do + object = issues(:issues_007) + user = users(:users_002) + + object.expects(:visible?).with(user).returns(false) + + assert_not Redmine::Reaction.visible?(object, user) + end + + test 'editable? returns true for various reactable objects when user is logged in, object is visible, and project is active' do + reactable_objects = { + issue: issues(:issues_007), + message: messages(:messages_001), + news: news(:news_001), + journal: journals(:journals_001), + comment: comments(:comments_002) + } + user = users(:users_002) + + reactable_objects.each do |type, object| + assert Redmine::Reaction.editable?(object, user), "Expected editable? to return true for #{type}" + end + end + + test 'editable? returns false when user is not logged in' do + object = issues(:issues_007) + user = User.anonymous + + assert_not Redmine::Reaction.editable?(object, user) + end + + test 'editable? returns false when project is inactive' do + object = issues(:issues_007) + user = users(:users_002) + object.project.update!(status: Project::STATUS_ARCHIVED) + + assert_not Redmine::Reaction.editable?(object, user) + end + + test 'editable? returns false when project is closed' do + object = issues(:issues_007) + user = users(:users_002) + object.project.update!(status: Project::STATUS_CLOSED) + + assert_not Redmine::Reaction.editable?(object, user) + end +end diff --git a/test/unit/lib/redmine/wiki_formatting/common_mark/formatter_test.rb b/test/unit/lib/redmine/wiki_formatting/common_mark/formatter_test.rb index 11d5913ce..bb0c5d450 100644 --- a/test/unit/lib/redmine/wiki_formatting/common_mark/formatter_test.rb +++ b/test/unit/lib/redmine/wiki_formatting/common_mark/formatter_test.rb @@ -163,39 +163,39 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase # 0 <<~STR.chomp, # Title - + Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero. STR # 1 <<~STR.chomp, ## Heading 2 - + ~~~ruby def foo end ~~~ - + Morbi facilisis accumsan orci non pharetra. - + ~~~ ruby def foo end ~~~ - + ``` Pre Content: - + ## Inside pre - + <tag> inside pre block - + Morbi facilisis accumsan orci non pharetra. ``` STR # 2 <<~STR.chomp, ### Heading 3 - + Nulla nunc nisi, egestas in ornare vel, posuere ac libero. STR ] @@ -226,7 +226,7 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase text = STR_WITH_PRE.join("\n\n") replacement = "New text" - assert_equal [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"), + assert_equal [STR_WITH_PRE[0..1], "New text"].join("\n\n"), @formatter.new(text).update_section(3, replacement) end @@ -287,7 +287,7 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase expected = <<~EXPECTED <p>Task list:</p> - <ul class="task-list"> + <ul class="contains-task-list"> <li class="task-list-item"> <input type="checkbox" class="task-list-item-checkbox" disabled> Task 1 </li> @@ -299,6 +299,49 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase assert_equal expected.gsub(%r{[\r\n\t]}, ''), to_html(text).gsub(%r{[\r\n\t]}, '').rstrip end + def test_should_render_alert_blocks + text = <<~MD + > [!note] + > This is a note. + + > [!tip] + > This is a tip. + + > [!warning] + > This is a warning. + + > [!caution] + > This is a caution. + + > [!important] + > This is a important. + MD + + html = to_html(text) + %w[note tip warning caution important].each do |alert| + icon = Redmine::WikiFormatting::CommonMark::ALERT_TYPE_TO_ICON_NAME[alert] + # rubocop:disable Layout/LineLength + expected = %r{<div class="markdown-alert markdown-alert-#{alert}">\n<p class="markdown-alert-title"><svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-\w+.svg\#icon--#{icon}"></use></svg><span class="icon-label">#{alert.capitalize}</span></p>\n<p>This is a #{alert}.</p>\n</div>} + # rubocop:enable Layout/LineLength + assert_match expected, html + end + end + + def test_should_not_render_unknown_alert_type + text = <<~MD + > [!unknown] + > This should not become an alert. + MD + + html = to_html(text) + + assert_include "<blockquote>", html + assert_include "[!unknown]", html + assert_include "This should not become an alert.", html + + assert_not_include 'markdown-alert', html + end + private def assert_section_with_hash(expected, text, index) diff --git a/test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb b/test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb index 4c0282f2d..b2d19eab9 100644 --- a/test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb +++ b/test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb @@ -47,10 +47,14 @@ if Object.const_defined?(:Commonmarker) end def test_should_support_footnotes - input = %(<a href="#fn-1" id="fnref-1">foo</a>) - assert_equal input, filter(input) - input = %(<ol><li id="fn-1">footnote</li></ol>) - assert_equal input, filter(input) + [ + %(<a href="#fn-1" id="fnref-1">foo</a>), + %(<a href="#fn-1" id="fnref-1-2">foo</a>), + %(<ol><li id="fn-1">footnote</li></ol>), + ].each do |input| + assert_equal input, filter(input) + assert_equal input, filter(input) + end end def test_should_remove_invalid_ids @@ -71,6 +75,32 @@ if Object.const_defined?(:Commonmarker) assert_equal %(<code>foo</code>), filter(input) end + def test_should_allow_valid_alert_div_and_p_classes + html = <<~HTML + <div class="markdown-alert markdown-alert-tip"> + <p class="markdown-alert-title">Tip</p> + <p>Useful tip.</p> + </div> + HTML + + sanitized = filter(html) + + assert_include 'class="markdown-alert markdown-alert-tip"', sanitized + assert_include 'class="markdown-alert-title"', sanitized + end + + def test_should_remove_invalid_div_class + html = '<div class="bad-class">Text</div>' + sanitized = filter(html) + assert_not_includes 'bad-class', sanitized + end + + def test_should_remove_invalid_p_class + html = '<p class="bad-class">Text</p>' + sanitized = filter(html) + assert_not_include 'bad-class', sanitized + end + def test_should_allow_links_with_safe_url_schemes %w(http https ftp ssh foo).each do |scheme| input = %(<a href="#{scheme}://example.org/">foo</a>) diff --git a/test/unit/lib/redmine/wiki_formatting/html_sanitizer_test.rb b/test/unit/lib/redmine/wiki_formatting/html_sanitizer_test.rb index 11dddb5f8..f8793cf9f 100644 --- a/test/unit/lib/redmine/wiki_formatting/html_sanitizer_test.rb +++ b/test/unit/lib/redmine/wiki_formatting/html_sanitizer_test.rb @@ -35,4 +35,24 @@ class Redmine::WikiFormatting::HtmlSanitizerTest < ActiveSupport::TestCase input = %(<a href="javascript:alert('hello');">foo</a>) assert_equal "<a>foo</a>", @sanitizer.call(input) end + + def test_should_be_strict_with_task_list_items + to_test = { + %(<input type="checkbox" class="">) => "", + %(<input type="checkbox" class="task-list-item-checkbox other">) => "", + %(<input type="checkbox" class="task-list-item-checkbox" id="item1">) => %(<input type="checkbox" class="task-list-item-checkbox">), + %(<input type="text" class="">) => "", + %(<input />) => "", + %(<ul class="other"></ul) => "<ul></ul>", + %(<ul class="contains-task-list"></ul) => "<ul class=\"contains-task-list\"></ul>", + %(<ul class="contains-task-list" id="list1"></ul) => "<ul class=\"contains-task-list\"></ul>", + %(<li class="other"></li>) => "", + %(<li id="other"></li>) => "", + %(<li class="task-list-item"></li>) => "", + %(<li class="task-list-item">Item 1</li>) => "Item 1", + } + to_test.each do |input, result| + assert_equal result, @sanitizer.call(input) + end + end end diff --git a/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb b/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb index 32280cfdf..678d4c6b2 100644 --- a/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb +++ b/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb @@ -466,19 +466,19 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase replacement = "New text" assert_equal( - [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"), + [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement) ) assert_equal( - [STR_WITHOUT_PRE[0..1], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), + [STR_WITHOUT_PRE[0..1], replacement, STR_WITHOUT_PRE[4]].join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(3, replacement) ) assert_equal( - [STR_WITHOUT_PRE[0..2], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), + [STR_WITHOUT_PRE[0..2], replacement, STR_WITHOUT_PRE[4]].join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(5, replacement) ) assert_equal( - [STR_WITHOUT_PRE[0..3], replacement].flatten.join("\n\n"), + [STR_WITHOUT_PRE[0..3], replacement].join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(6, replacement) ) assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(0, replacement) @@ -488,7 +488,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase def test_update_section_with_hash_should_update_the_requested_section replacement = "New text" assert_equal( - [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"), + [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE). update_section(2, replacement, ActiveSupport::Digest.hexdigest(STR_WITHOUT_PRE[1])) ) @@ -552,7 +552,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase text = STR_WITH_PRE.join("\n\n") replacement = "New text" assert_equal( - [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"), + [STR_WITH_PRE[0..1], "New text"].join("\n\n"), @formatter.new(text).update_section(3, replacement) ) end diff --git a/test/unit/member_test.rb b/test/unit/member_test.rb index 42fba4783..df9088027 100644 --- a/test/unit/member_test.rb +++ b/test/unit/member_test.rb @@ -108,7 +108,7 @@ class MemberTest < ActiveSupport::TestCase assert !member.save assert_include I18n.translate('activerecord.errors.messages.empty'), member.errors[:role] assert_equal 'Rôle doit être renseigné(e)', - [member.errors.full_messages].flatten.join + [member.errors.full_messages].join end def test_validate_member_role diff --git a/test/unit/principal_test.rb b/test/unit/principal_test.rb index 9eef5bbf3..61f775014 100644 --- a/test/unit/principal_test.rb +++ b/test/unit/principal_test.rb @@ -166,4 +166,8 @@ class PrincipalTest < ActiveSupport::TestCase r = Principal.like('vi_ci') assert_include user, r end + + def test_initials_should_return_nil + assert_nil Group.first.initials + end end diff --git a/test/unit/reaction_test.rb b/test/unit/reaction_test.rb new file mode 100644 index 000000000..9b3da0738 --- /dev/null +++ b/test/unit/reaction_test.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- 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_relative '../test_helper' + +class ReactionTest < ActiveSupport::TestCase + test 'validates :inclusion of reactable_type' do + %w(Issue Journal News Comment Message).each do |type| + reaction = Reaction.new(reactable_type: type, user: User.new) + assert reaction.valid? + end + + assert_not Reaction.new(reactable_type: 'InvalidType', user: User.new).valid? + end + + test 'scope: by' do + user2_reactions = issues(:issues_001).reactions.by(users(:users_002)) + + assert_equal [reactions(:reaction_002)], user2_reactions + end + + test "should prevent duplicate reactions with unique constraint under concurrent creation" do + user = users(:users_001) + issue = issues(:issues_004) + + threads = [] + results = [] + + # Ensure both threads start at the same time + barrier = Concurrent::CyclicBarrier.new(2) + + # Create two threads to simulate concurrent creation + 2.times do + threads << Thread.new do + barrier.wait # Wait for both threads to be ready + begin + reaction = Reaction.create( + reactable: issue, + user: user + ) + results << reaction.persisted? + rescue ActiveRecord::RecordNotUnique + results << false + end + end + end + + # Wait for both threads to finish + threads.each(&:join) + + # Ensure only one reaction was created + assert_equal 1, Reaction.where(reactable: issue, user: user).count + assert_includes results, true + assert_equal 1, results.count(true) + end + + test 'build_detail_map_for generates a detail map for reactable objects' do + result = Reaction.build_detail_map_for([issues(:issues_001), issues(:issues_006)], users(:users_003)) + + expected = { + 1 => Reaction::Detail.new( + visible_users: [users(:users_003), users(:users_002), users(:users_001)], + user_reaction: reactions(:reaction_003) + ), + 6 => Reaction::Detail.new( + visible_users: [users(:users_002)], + user_reaction: nil + ) + } + assert_equal expected, result + + # When an object have no reactions, the result should be empty. + result = Reaction.build_detail_map_for([journals(:journals_002)], users(:users_002)) + + assert_empty result + end + + test 'build_detail_map_for filters users based on visibility' do + current_user = User.generate! + visible_user = users(:users_002) + non_visible_user = User.generate! + + project = Project.generate! + role = Role.generate!(users_visibility: 'members_of_visible_projects') + + User.add_to_project(current_user, project, role) + User.add_to_project(visible_user, project, roles(:roles_001)) + + issue = Issue.generate!(project: project) + + [current_user, visible_user, non_visible_user].each do |user| + issue.reactions.create!(user: user) + end + + result = Reaction.build_detail_map_for([issue], current_user) + + assert_equal( + [current_user, visible_user].sort_by(&:id), + result[issue.id].visible_users.sort_by(&:id) + ) + end +end diff --git a/test/unit/role_test.rb b/test/unit/role_test.rb index 21103919f..1d0d39d7e 100644 --- a/test/unit/role_test.rb +++ b/test/unit/role_test.rb @@ -175,6 +175,32 @@ class RoleTest < ActiveSupport::TestCase assert_equal false, role.permissions_tracker_ids?(:view_issues, 1) end + def test_allowed_to_with_symbol + role = Role.create!(:name => 'Test', :permissions => [:view_issues]) + assert_equal true, role.allowed_to?(:view_issues) + assert_equal false, role.allowed_to?(:add_issues) + end + + def test_allowed_to_with_symbol_and_scope + role = Role.create!(:name => 'Test', :permissions => [:view_issues, :delete_issues]) + assert_equal true, role.allowed_to?(:view_issues, [:view_issues, :add_issues]) + assert_equal false, role.allowed_to?(:add_issues, [:view_issues, :add_issues]) + assert_equal false, role.allowed_to?(:delete_issues, [:view_issues, :add_issues]) + end + + def test_allowed_to_with_hash + role = Role.create!(:name => 'Test', :permissions => [:view_issues]) + assert_equal true, role.allowed_to?(:controller => 'issues', :action => 'show') + assert_equal false, role.allowed_to?(:controller => 'issues', :action => 'create') + end + + def test_allowed_to_with_hash_and_scope + role = Role.create!(:name => 'Test', :permissions => [:view_issues, :delete_issues]) + assert_equal true, role.allowed_to?({:controller => 'issues', :action => 'show'}, [:view_issues, :add_issues]) + assert_equal false, role.allowed_to?({:controller => 'issues', :action => 'create'}, [:view_issues, :add_issues]) + assert_equal false, role.allowed_to?({:controller => 'issues', :action => 'destroy'}, [:view_issues, :add_issues]) + end + def test_has_permission_without_permissions role = Role.create!(:name => 'Test') assert_equal false, role.has_permission?(:delete_issues) diff --git a/test/unit/setting_test.rb b/test/unit/setting_test.rb index 4ae07cebb..cbfabbb02 100644 --- a/test/unit/setting_test.rb +++ b/test/unit/setting_test.rb @@ -147,4 +147,8 @@ class SettingTest < ActiveSupport::TestCase def test_default_text_formatting_for_new_installations_is_common_mark assert_equal 'common_mark', Setting.text_formatting end + + def test_default_wiki_tablesort_enabled_for_new_installations_is_disabled + assert_equal "0", Setting.wiki_tablesort_enabled + end end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index ede12e1ce..967771c87 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -589,6 +589,27 @@ class UserTest < ActiveSupport::TestCase end end + def test_initials_format + assert_equal 'JS', @jsmith.initials(:firstname_lastinitial) + assert_equal 'SJ', @jsmith.initials(:lastname_comma_firstname) + assert_equal 'SJ', @jsmith.initials(:lastname_firstname) + assert_equal 'JS', @jsmith.initials(:firstinitial_lastname) + assert_equal 'JL', User.new(:firstname => 'Jean-Philippe', :lastname => 'Lang').initials(:firstinitial_lastname) + assert_equal 'JS', @jsmith.initials(:undefined_format) + end + + def test_initials_should_use_setting_as_default_format + with_settings :user_format => :firstname_lastname do + assert_equal 'JS', @jsmith.reload.initials + end + with_settings :user_format => :username do + assert_equal 'JS', @jsmith.reload.initials + end + with_settings :user_format => :lastname do + assert_equal 'SM', @jsmith.reload.initials + end + end + def test_lastname_should_accept_255_characters u = User.first u.lastname = 'a' * 255 @@ -1376,4 +1397,77 @@ class UserTest < ActiveSupport::TestCase User.prune(7) end end + + def test_should_recognize_authorized_by_oauth + u = User.find 2 + assert_not u.authorized_by_oauth? + u.oauth_scope = [:add_issues, :view_issues] + assert u.authorized_by_oauth? + end + + def test_admin_should_be_limited_by_oauth_scope + u = User.find_by_admin(true) + assert u.admin? + + u.oauth_scope = [:add_issues, :view_issues] + assert_not u.admin? + + u.oauth_scope = [:add_issues, :view_issues, :admin] + assert u.admin? + + u = User.find_by_admin(false) + assert_not u.admin? + u.oauth_scope = [:add_issues, :view_issues, :admin] + assert_not u.admin? + end + + def test_oauth_scope_should_limit_global_user_permissions + admin = User.find 1 + user = User.find 2 + [admin, user].each do |u| + assert u.allowed_to?(:add_issues, nil, global: true) + assert u.allowed_to?(:view_issues, nil, global: true) + u.oauth_scope = [:view_issues] + assert_not u.allowed_to?(:add_issues, nil, global: true) + assert u.allowed_to?(:view_issues, nil, global: true) + end + end + + def test_oauth_scope_should_limit_project_user_permissions + admin = User.find 1 + project = Project.find 5 + assert admin.allowed_to?(:add_issues, project) + assert admin.allowed_to?(:view_issues, project) + admin.oauth_scope = [:view_issues] + assert_not admin.allowed_to?(:add_issues, project) + assert admin.allowed_to?(:view_issues, project) + + admin.oauth_scope = [:view_issues, :admin] + assert admin.allowed_to?(:add_issues, project) + assert admin.allowed_to?(:view_issues, project) + + user = User.find 2 + project = Project.find 1 + assert user.allowed_to?(:add_issues, project) + assert user.allowed_to?(:view_issues, project) + user.oauth_scope = [:view_issues] + assert_not user.allowed_to?(:add_issues, project) + assert user.allowed_to?(:view_issues, project) + + user.oauth_scope = [:view_issues, :admin] + assert_not user.allowed_to?(:add_issues, project) + assert user.allowed_to?(:view_issues, project) + end + + def test_destroy_should_delete_associated_reactions + users(:users_004).reactions.create!( + [ + {reactable: issues(:issues_001)}, + {reactable: issues(:issues_002)} + ] + ) + assert_difference 'Reaction.count', -2 do + users(:users_004).destroy + end + end end |