summaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/fixtures/reactions.yml51
-rw-r--r--test/functional/issues_controller_test.rb36
-rw-r--r--test/functional/messages_controller_test.rb21
-rw-r--r--test/functional/news_controller_test.rb17
-rw-r--r--test/functional/reactions_controller_test.rb394
-rw-r--r--test/helpers/reactions_helper_test.rb196
-rw-r--r--test/system/issues_test.rb11
-rw-r--r--test/system/reactions_test.rb132
-rw-r--r--test/system/sudo_mode_test.rb3
-rw-r--r--test/system/timelog_test.rb3
-rw-r--r--test/unit/lib/redmine/reaction_test.rb193
-rw-r--r--test/unit/reaction_test.rb120
-rw-r--r--test/unit/user_test.rb12
13 files changed, 1188 insertions, 1 deletions
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/issues_controller_test.rb b/test/functional/issues_controller_test.rb
index b5180fcff..b7e0321d4 100644
--- a/test/functional/issues_controller_test.rb
+++ b/test/functional/issues_controller_test.rb
@@ -3331,6 +3331,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, John Smith, and 1 other' do
+ assert_select 'span.icon-label', '3'
+ 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]
diff --git a/test/functional/messages_controller_test.rb b/test/functional/messages_controller_test.rb
index 74b9a3070..997b2263a 100644
--- a/test/functional/messages_controller_test.rb
+++ b/test/functional/messages_controller_test.rb
@@ -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..536814c9d 100644
--- a/test/functional/news_controller_test.rb
+++ b/test/functional/news_controller_test.rb
@@ -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/helpers/reactions_helper_test.rb b/test/helpers/reactions_helper_test.rb
new file mode 100644
index 000000000..f3a4e38d8
--- /dev/null
+++ b/test/helpers/reactions_helper_test.rb
@@ -0,0 +1,196 @@
+# 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 displays non-visible users as "X other" in the tooltip' do
+ issue2 = issues(:issues_002)
+
+ issue2.reaction_detail = Reaction::Detail.new(
+ # The remaining 3 users are non-visible users
+ reaction_count: 5,
+ 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, Dave Lopper, and 3 others'
+
+ # When all users are non-visible users
+ issue2.reaction_detail = Reaction::Detail.new(
+ reaction_count: 2,
+ visible_users: []
+ )
+
+ result = with_locale('en') do
+ reaction_button(issue2)
+ end
+
+ assert_select_in result, 'a.reaction-button[title=?]', '2 others'
+ 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[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[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/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/reactions_test.rb b/test/system/reactions_test.rb
new file mode 100644
index 000000000..01ba76832
--- /dev/null
+++ b/test/system/reactions_test.rb
@@ -0,0 +1,132 @@
+# 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
+
+ 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 "0", reaction_button.text
+ assert_equal 0, expected_subject.reactions.count
+ 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/reaction_test.rb b/test/unit/lib/redmine/reaction_test.rb
new file mode 100644
index 000000000..bed4600d0
--- /dev/null
+++ b/test/unit/lib/redmine/reaction_test.rb
@@ -0,0 +1,193 @@
+# 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(
+ reaction_count: 3,
+ 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(
+ reaction_count: 0,
+ 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(
+ reaction_count: 1,
+ 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(
+ reaction_count: 1,
+ 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 'writable? 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.writable?(object, user), "Expected writable? to return true for #{type}"
+ end
+ end
+
+ test 'writable? returns false when user is not logged in' do
+ object = issues(:issues_007)
+ user = User.anonymous
+
+ assert_not Redmine::Reaction.writable?(object, user)
+ end
+
+ test 'writable? 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.writable?(object, user)
+ end
+
+ test 'writable? 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.writable?(object, user)
+ end
+end
diff --git a/test/unit/reaction_test.rb b/test/unit/reaction_test.rb
new file mode 100644
index 000000000..2690da351
--- /dev/null
+++ b/test/unit/reaction_test.rb
@@ -0,0 +1,120 @@
+# 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(
+ reaction_count: 3,
+ visible_users: [users(:users_003), users(:users_002), users(:users_001)],
+ user_reaction: reactions(:reaction_003)
+ ),
+ 6 => Reaction::Detail.new(
+ reaction_count: 1,
+ 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/user_test.rb b/test/unit/user_test.rb
index ede12e1ce..aeae62df8 100644
--- a/test/unit/user_test.rb
+++ b/test/unit/user_test.rb
@@ -1376,4 +1376,16 @@ class UserTest < ActiveSupport::TestCase
User.prune(7)
end
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