diff options
66 files changed, 542 insertions, 127 deletions
diff --git a/.github/actions/setup-redmine/action.yml b/.github/actions/setup-redmine/action.yml new file mode 100644 index 000000000..d637914f0 --- /dev/null +++ b/.github/actions/setup-redmine/action.yml @@ -0,0 +1,68 @@ +name: Setup Redmine Test Environment +description: Composite action for setting up Redmine test environment + +inputs: + db-type: + description: 'Database type: postgresql, mysql2, or sqlite3. Note: postgresql and mysql2 require service containers to be defined in the workflow.' + required: true + ruby-version: + description: 'Ruby version to use' + required: true + +runs: + using: composite + steps: + - name: Install dependencies and configure environment + shell: bash + run: | + sudo apt-get update + sudo apt-get install --yes --quiet ghostscript gsfonts locales bzr cvs + sudo locale-gen en_US # for bazaar non ascii test + + - name: Allow imagemagick to read PDF files + shell: bash + run: | + echo '<policymap>' > policy.xml + echo '<policy domain="coder" rights="read | write" pattern="PDF" />' >> policy.xml + echo '</policymap>' >> policy.xml + sudo rm /etc/ImageMagick-6/policy.xml + sudo mv policy.xml /etc/ImageMagick-6/policy.xml + + - if: ${{ inputs.db-type == 'sqlite3' }} + name: Prepare test database for sqlite3 + shell: bash + run: | + cat > config/database.yml <<EOF + test: + adapter: sqlite3 + database: db/test.sqlite3 + EOF + + - if: ${{ inputs.db-type == 'mysql2' || inputs.db-type == 'postgresql' }} + name: Prepare test database for mysql2 and postgresql + shell: bash + run: | + cat > config/database.yml <<EOF + test: + adapter: ${{ inputs.db-type }} + database: redmine_test + username: root + password: root + host: 127.0.0.1 + EOF + + - name: Install Ruby and gems + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ inputs.ruby-version }} + bundler-cache: true + + - name: Run prepare test environment + shell: bash + env: + RAILS_ENV: test + SCMS: subversion,git,git_utf8,filesystem,bazaar,cvs + run: | + bundle exec rake ci:about + bundle exec rake ci:setup + bundle exec rake db:environment:set diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4d5e0e200..481cd38a1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,55 +46,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Install dependencies and configure environment - run: | - sudo apt-get update - sudo apt-get install --yes --quiet ghostscript gsfonts locales bzr cvs - sudo locale-gen en_US # for bazaar non ascii test - - - name: Allow imagemagick to read PDF files - run: | - echo '<policymap>' > policy.xml - echo '<policy domain="coder" rights="read | write" pattern="PDF" />' >> policy.xml - echo '</policymap>' >> policy.xml - sudo rm /etc/ImageMagick-6/policy.xml - sudo mv policy.xml /etc/ImageMagick-6/policy.xml - - - if: ${{ matrix.db == 'sqlite3' }} - name: Prepare test database for sqlite3 - run: | - cat > config/database.yml <<EOF - test: - adapter: sqlite3 - database: db/test.sqlite3 - EOF - - - if: ${{ matrix.db == 'mysql2' || matrix.db == 'postgresql' }} - name: Prepare test database for mysql2 and postgresql - run: | - cat > config/database.yml <<EOF - test: - adapter: ${{ matrix.db }} - database: redmine_test - username: root - password: root - host: 127.0.0.1 - EOF - - - name: Install Ruby and gems - uses: ruby/setup-ruby@v1 + - name: Setup Redmine test environment + uses: ./.github/actions/setup-redmine with: + db-type: ${{ matrix.db }} ruby-version: ${{ matrix.ruby }} - bundler-cache: true - - - name: Run prepare test environment - env: - RAILS_ENV: test - SCMS: subversion,git,git_utf8,filesystem,bazaar,cvs - run: | - bundle exec rake ci:about - bundle exec rake ci:setup - bundle exec rake db:environment:set - name: Run tests run: | @@ -106,3 +62,38 @@ jobs: LC_ALL: en_US.ISO8859-1 run: | bin/rails test test/unit/repository_bazaar_test.rb + + system-tests: + name: system test + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Redmine test environment + uses: ./.github/actions/setup-redmine + with: + db-type: sqlite3 + ruby-version: '3.2' + + # System tests use Chrome and ChromeDriver installed on the GitHub Actions Ubuntu image. + # They are generally updated to the latest stable versions. + - name: Display Chrome version + run: google-chrome --version + + - name: Run system tests + run: bin/rails test:system + env: + GOOGLE_CHROME_OPTS_ARGS: headless,disable-gpu,no-sandbox,disable-dev-shm-usage + # System tests might still be a bit unstable, so for now, even if a system test fails, + # output the results and consider the overall test as successful. + continue-on-error: true + + - name: Upload system test screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: system-test-screenshots + path: tmp/screenshots + if-no-files-found: ignore @@ -18,7 +18,7 @@ gem 'rubyzip', '~> 2.3.0' # Ruby Standard Gems gem 'csv', '~> 3.2.6' -gem 'net-imap', '~> 0.3.4' +gem 'net-imap', '~> 0.3.9' gem 'net-pop', '~> 0.1.2' gem 'net-smtp', '~> 0.3.3' gem 'rexml', require: false if Gem.ruby_version >= Gem::Version.new('3.0') @@ -101,7 +101,7 @@ group :development do end group :test do - gem "rails-dom-testing" + gem "rails-dom-testing", '>= 2.3.0' gem 'mocha', '>= 2.0.1' gem 'simplecov', '~> 0.22.0', :require => false gem "ffi", platforms: [:mingw, :x64_mingw, :mswin] @@ -116,6 +116,7 @@ group :test do end # RuboCop gem 'rubocop', '~> 1.57.0', require: false + gem 'rubocop-ast', '~> 1.40.0', require: false gem 'rubocop-performance', '~> 1.19.0', require: false gem 'rubocop-rails', '~> 2.22.1', require: false end diff --git a/app/controllers/auto_completes_controller.rb b/app/controllers/auto_completes_controller.rb index 2982447e9..77105c8e8 100644 --- a/app/controllers/auto_completes_controller.rb +++ b/app/controllers/auto_completes_controller.rb @@ -26,7 +26,7 @@ class AutoCompletesController < ApplicationController status = params[:status].to_s issue_id = params[:issue_id].to_s - scope = Issue.cross_project_scope(@project, params[:scope]).visible + scope = Issue.cross_project_scope(@project, params[:scope]).includes(:tracker).visible scope = scope.open(status == 'o') if status.present? scope = scope.where.not(:id => issue_id.to_i) if issue_id.present? if q.present? diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index a5ffb4451..7e129348a 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -156,7 +156,15 @@ class RepositoriesController < ApplicationController # Force the download send_opt = {:filename => filename_for_content_disposition(@path.split('/').last)} send_type = Redmine::MimeType.of(@path) - send_opt[:type] = send_type.to_s if send_type + case send_type + when nil + # No MIME type detected. Let Rails use the default type. + when 'application/javascript' + # Avoid ActionController::InvalidCrossOriginRequest exception by setting non-JS content type + send_opt[:type] = 'text/plain' + else + send_opt[:type] = send_type + end send_opt[:disposition] = disposition(@path) send_data @repository.cat(@path, @rev), send_opt else diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index 36b90da77..bcb3b0891 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -240,6 +240,7 @@ class WikiController < ApplicationController # don't load text @versions = @page.content.versions. select("id, author_id, comments, updated_on, version"). + preload(:author). reorder('version DESC'). limit(@version_pages.per_page + 1). offset(@version_pages.offset). diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 377765cbb..bebd72834 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -402,7 +402,7 @@ module ApplicationHelper def format_changeset_comments(changeset, options={}) method = options[:short] ? :short_comments : :comments - textilizable changeset, method, :formatting => Setting.commit_logs_formatting? + textilizable changeset, method, project: changeset.project, formatting: Setting.commit_logs_formatting? end def due_date_distance_in_words(date) @@ -736,7 +736,7 @@ module ApplicationHelper end def other_formats_links(&block) - concat('<p class="other-formats">'.html_safe + l(:label_export_to)) + concat('<p class="other-formats hide-when-print">'.html_safe + l(:label_export_to)) yield Redmine::Views::OtherFormatsBuilder.new(self) concat('</p>'.html_safe) end diff --git a/app/models/email_address.rb b/app/models/email_address.rb index 1e7a25ee7..909221f1a 100644 --- a/app/models/email_address.rb +++ b/app/models/email_address.rb @@ -66,7 +66,7 @@ class EmailAddress < ActiveRecord::Base # Returns true if domain belongs to domains list. def self.domain_in?(domain, domains) - domain = domain.downcase + domain = domain.to_s.downcase domains = domains.to_s.split(/[\s,]+/) unless domains.is_a?(Array) domains.reject(&:blank?).map(&:downcase).any? do |s| s.start_with?('.') ? domain.end_with?(s) : domain == s @@ -142,6 +142,10 @@ class EmailAddress < ActiveRecord::Base def validate_email_domain domain = address.partition('@').last + # Skip domain validation if the email does not contain a domain part, + # to avoid an incomplete error message like "domain not allowed ()" + return if domain.empty? + return if self.class.valid_domain?(domain) if User.current.logged? diff --git a/app/models/time_entry_query.rb b/app/models/time_entry_query.rb index 3d828d2bd..9c18f73d3 100644 --- a/app/models/time_entry_query.rb +++ b/app/models/time_entry_query.rb @@ -152,12 +152,19 @@ class TimeEntryQuery < Query end def base_scope - TimeEntry.visible. - joins(:project, :user). - includes(:activity). - references(:activity). - left_join_issue. - where(statement) + scope = TimeEntry.visible + .joins(:project, :user) + .includes(:activity) + .references(:activity) + .left_join_issue + .where(statement) + + if Redmine::Database.mysql? && ActiveRecord::Base.connection.supports_optimizer_hints? + # Provides MySQL with a hint to use a better join order and avoid slow response times + scope.optimizer_hints('JOIN_ORDER(time_entries, projects, users)') + else + scope + end end def results_scope(options={}) diff --git a/app/models/user.rb b/app/models/user.rb index d189898dd..0f18e39e5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -87,8 +87,8 @@ class User < Principal :after_remove => Proc.new {|user, group| group.user_removed(user)} has_many :changesets, :dependent => :nullify has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' - has_one :atom_token, lambda {where "action='feeds'"}, :class_name => 'Token' - has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token' + has_one :atom_token, lambda {where "#{table.name}.action='feeds'"}, :class_name => 'Token' + has_one :api_token, lambda {where "#{table.name}.action='api'"}, :class_name => 'Token' has_one :email_address, lambda {where :is_default => true}, :autosave => true has_many :email_addresses, :dependent => :delete_all belongs_to :auth_source diff --git a/app/views/custom_fields/index.api.rsb b/app/views/custom_fields/index.api.rsb index 9f46f89f2..d4b19d62b 100644 --- a/app/views/custom_fields/index.api.rsb +++ b/app/views/custom_fields/index.api.rsb @@ -15,6 +15,7 @@ api.array :custom_fields do api.multiple field.multiple? api.default_value field.default_value api.visible field.visible? + api.editable field.editable? values = field.possible_values_options if values.present? diff --git a/app/views/issues/_list.html.erb b/app/views/issues/_list.html.erb index 74b6c94f8..92a507054 100644 --- a/app/views/issues/_list.html.erb +++ b/app/views/issues/_list.html.erb @@ -15,7 +15,7 @@ <% query.inline_columns.each do |column| %> <%= column_header(query, column, query_options) %> <% end %> - <th class="buttons"></th> + <th class="buttons hide-when-print"></th> </tr> </thead> <tbody> @@ -36,7 +36,7 @@ <% query.inline_columns.each do |column| %> <%= content_tag('td', column_content(column, issue), :class => column.css_classes) %> <% end %> - <td class="buttons"><%= link_to_context_menu %></td> + <td class="buttons hide-when-print"><%= link_to_context_menu %></td> </tr> <% query.block_columns.each do |column| if (text = column_content(column, issue)) && text.present? -%> diff --git a/app/views/queries/_form.html.erb b/app/views/queries/_form.html.erb index 1c302fe76..75a20248a 100644 --- a/app/views/queries/_form.html.erb +++ b/app/views/queries/_form.html.erb @@ -30,7 +30,7 @@ <% unless @query.is_a?(ProjectQuery) %> <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label> - <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?, :class => (User.current.admin? ? '' : 'disable-unless-private') %></p> + <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?, :disabled => (!@query.new_record? && @query.project.nil?), :class => (User.current.admin? ? '' : 'disable-unless-private') %></p> <% end %> <% unless params[:calendar] %> diff --git a/app/views/timelog/_list.html.erb b/app/views/timelog/_list.html.erb index af8dd1fa5..5f22627b9 100644 --- a/app/views/timelog/_list.html.erb +++ b/app/views/timelog/_list.html.erb @@ -11,7 +11,7 @@ <% @query.inline_columns.each do |column| %> <%= column_header(@query, column) %> <% end %> - <th></th> + <th class="buttons hide-when-print"></th> </tr> </thead> <tbody> @@ -36,7 +36,7 @@ <% @query.inline_columns.each do |column| %> <%= content_tag('td', column_content(column, entry), :class => column.css_classes) %> <% end %> - <td class="buttons"> + <td class="buttons hide-when-print"> <% if entry.editable_by?(User.current) -%> <%= link_to l(:button_edit), edit_time_entry_path(entry), :title => l(:button_edit), diff --git a/app/views/versions/index.html.erb b/app/views/versions/index.html.erb index 382133e93..010dea7b3 100644 --- a/app/views/versions/index.html.erb +++ b/app/views/versions/index.html.erb @@ -37,7 +37,7 @@ <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td> <td class="assigned_to"><%= assignee_avatar(issue.assigned_to, :size => 16) %></td> <td class="subject"><%= link_to_issue(issue, :project => (@project != issue.project)) %></td> - <td class="buttons"><%= link_to_context_menu %></td> + <td class="buttons hide-when-print"><%= link_to_context_menu %></td> </tr> <% end -%> </table> diff --git a/app/views/versions/show.html.erb b/app/views/versions/show.html.erb index e17571075..3d689d093 100644 --- a/app/views/versions/show.html.erb +++ b/app/views/versions/show.html.erb @@ -47,7 +47,7 @@ <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td> <td class="assigned_to"><%= assignee_avatar(issue.assigned_to, :size => 16) %></td> <td class="subject"><%= link_to_issue(issue, :project => (@project != issue.project)) %></td> - <td class="buttons"><%= link_to_context_menu %></td> + <td class="buttons hide-when-print"><%= link_to_context_menu %></td> </tr> <% end %> </table> diff --git a/app/views/workflows/edit.html.erb b/app/views/workflows/edit.html.erb index df4507be2..adb1996cd 100644 --- a/app/views/workflows/edit.html.erb +++ b/app/views/workflows/edit.html.erb @@ -40,7 +40,7 @@ <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %> <fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;"> - <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_additional_workflow_transitions_for_author) %></legend> + <legend onclick="toggleFieldset(this);" class="icon icon-<%= @workflows['author'].present? ? "expanded" : "collapsed" %>"><%= l(:label_additional_workflow_transitions_for_author) %></legend> <div id="author_workflows" style="margin: 0.5em 0 0.5em 0;"> <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %> </div> @@ -48,7 +48,7 @@ <%= javascript_tag "hideFieldset($('#author_workflows'))" unless @workflows['author'].present? %> <fieldset class="collapsible" style="padding: 0;"> - <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_additional_workflow_transitions_for_assignee) %></legend> + <legend onclick="toggleFieldset(this);" class="icon icon-<%= @workflows['assignee'].present? ? "expanded" : "collapsed" %>"><%= l(:label_additional_workflow_transitions_for_assignee) %></legend> <div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;"> <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %> </div> diff --git a/config/boot.rb b/config/boot.rb index 89040bd90..75f2782ff 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,5 +1,21 @@ # frozen_string_literal: true +# Rack 3.1.14 or later sets default limits of 4MB for query string bytesize +# and 4096 for the number of query parameters. These limits are too low +# for Redmine and can cause the following issues: +# +# - The low bytesize limit prevents the mail handler from processing incoming +# emails larger than 4MB (https://www.redmine.org/issues/42962) +# - The low parameter limit prevents saving workflows with many statuses +# (https://www.redmine.org/issues/42875) +# +# See also: +# - https://github.com/rack/rack/blob/v3.1.16/README.md#configuration +# - https://github.com/rack/rack/blob/v3.1.16/lib/rack/query_parser.rb#L54 +# - https://github.com/rack/rack/blob/v3.1.16/lib/rack/query_parser.rb#L57 +ENV['RACK_QUERY_PARSER_BYTESIZE_LIMIT'] ||= '33554432' +ENV['RACK_QUERY_PARSER_PARAMS_LIMIT'] ||= '65536' + # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) diff --git a/config/routes.rb b/config/routes.rb index d5296b3c3..0457ff1ef 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -313,7 +313,7 @@ Rails.application.routes.draw do # additional routes for having the file name at the end of url get 'attachments/:id/:filename', :to => 'attachments#show', :id => /\d+/, :filename => /.*/, :as => 'named_attachment', :format => 'html' - get 'attachments/download/:id/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'download_named_attachment' + get 'attachments/download/:id/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'download_named_attachment', :format => 'html' get 'attachments/download/:id', :to => 'attachments#download', :id => /\d+/ get 'attachments/thumbnail/:id(/:size)', :to => 'attachments#thumbnail', :id => /\d+/, :size => /\d+/, :as => 'thumbnail' resources :attachments, :only => [:show, :update, :destroy] diff --git a/doc/CHANGELOG b/doc/CHANGELOG index ae7ac9aaf..9fc2bdd8d 100644 --- a/doc/CHANGELOG +++ b/doc/CHANGELOG @@ -4,6 +4,125 @@ Redmine - project management software Copyright (C) 2006- Jean-Philippe Lang https://www.redmine.org/ +== 2025-07-07 v5.1.9 + +=== [Code cleanup/refactoring] + +* Defect #42687: Fix random failures in several system tests with Chrome 133 and later +* Patch #42422: Use Capybara's assert_current_path in "log_user" steps to wait for page in ApplicationSystemTestCase +* Patch #42600: Suppress "Change your password" popup for stable system tests +* Patch #42756: Update tests for rails-dom-testing 2.3.0 whitespace collapsing + +=== [Database] + +* Defect #42622: Joining both atom_token and api_token on the User model causes an error due to the ambiguous column name "action" + +=== [Email receiving] + +* Defect #42962: Mail handler fails to create issues from emails over 4MB on Rack >= 3.1.14 + +=== [Gems support] + +* Defect #42606: RuboCop warning about deprecated `EnsureNode#body` with rubocop-ast >= 1.41 + +=== [I18n] + +* Defect #42815: Limit available locales to those defined by Redmine itself no longer works + +=== [Issues workflow] + +* Defect #42875: "Page not found" error when saving workflows with many statuses on Rack >= 3.1.14 + +=== [No category] + +* Patch #42688: Run system tests on GitHub CI + +=== [Performance] + +* Defect #42933: Fix N+1 query issue in Wiki history page when loading authors of Wiki content versions + +=== [SCM] + +* Defect #42839: Downloading .js files from the repository browser fails with a 422 error due to ActionController::InvalidCrossOriginRequest +* Patch #42597: Skip some Mercurial tests when using Mercurial 5.1 or later in Redmine 6.0 or 5.1 + +=== [Security] + +* Patch #42662: Require net-imap gem 0.2.5, 0.3.9, 0.4.20, 0.5.7, or later to address CVE-2025-43857 + +=== [Text formatting] + +* Defect #42648: Wiki/CommonMark: Broken references for multiple footnote usage + +=== [UI] + +* Defect #42640: Query totals overlaps query buttons when an RTL language is used +* Patch #42794: Hide irrelevant information when printing + +== 2025-04-20 v5.1.8 + +=== [Administration] + +* Defect #42584: NoMethodError when creating a user with an invalid email address and domain restrictions are enabled + +=== [Attachments] + +* Defect #42394: Inconsistent behaviour between attachment download routes with and without filename + +=== [Code cleanup/refactoring] + +* Patch #42562: Fix random test failure in ProjectAdminQueryTest due to missing language setting +* Patch #42572: Fix random test failure in MemberTest#test_update_roles_with_inherited_roles due to non-deterministic ordering + +=== [Custom fields] + +* Patch #41935: Add "editable" attribute in the custom fields API response + +=== [Gantt] + +* Defect #42145: MiniMagick (> 5) removed cli_path, result crash when supplied imagemagick_convert_command + +=== [Issues] + +* Defect #42458: "For all projects" checkbox should be disabled when editing an existing query in which the checkbox is already checked + +=== [Performance] + +* Defect #40728: Slow loading of global spent time list in MySQL +* Feature #42574: Optimize autocomplete issue listing triggered by typing "##" by eager loading trackers + +=== [Text formatting] + +* Defect #42545: Commit message in issue history might be rendered in incorrect context + +=== [UI] + +* Defect #41828: In mobile view, delete relation svg icon in 'Related Issues' on an issue page, overflow text +* Defect #41947: Collapse arrow shows the wrong direction at /workflows/edit +* Patch #42497: Adjust the position of the news comment delete button +* Patch #42596: Do not show user icon in add watchers modal when gravatar is disabled + +== 2025-03-10 v5.1.7 + +=== [Code cleanup/refactoring] + +* Defect #42200: InlineAutocompleteSystemTest login test fails randomly +* Patch #42244: Fix random failures in IssuesTest#test_bulk_copy due to StaleElementReferenceError + +=== [Gems support] + +* Defect #42245: 5.1-stable: Redmine fails to start with error: Unknown database adapter `"mysql2"` found in config/database.yml + +=== [No category] + +* Feature #30069: Use GitHub Actions as a secondary CI solution to run tests through the existing mirroring + +=== [Security] +* Defect #42326: Stored Cross-Site Scripting (XSS) in macros +* Defect #42352: ProjectQuery leaks details of private projects +* Defect #42194: /my/account does not correctly enforce sudo mode +* Patch #42333: Update Nokogiri to 1.18.3 + == 2025-01-29 v5.1.6 === [Code cleanup/refactoring] diff --git a/lib/redmine/helpers/gantt.rb b/lib/redmine/helpers/gantt.rb index b31486d5d..665930bf1 100644 --- a/lib/redmine/helpers/gantt.rb +++ b/lib/redmine/helpers/gantt.rb @@ -396,7 +396,15 @@ module Redmine Redmine::Configuration['rmagick_font_path'].presence img = MiniMagick::Image.create(".#{format}", false) if Redmine::Configuration['imagemagick_convert_command'].present? - MiniMagick.cli_path = File.dirname(Redmine::Configuration['imagemagick_convert_command']) + if MiniMagick.respond_to?(:cli_path) + MiniMagick.cli_path = File.dirname(Redmine::Configuration['imagemagick_convert_command']) + else + Rails.logger.warn( + 'imagemagick_convert_command option is ignored ' \ + 'because MiniMagick has removed the option to define a custom path for the binary. ' \ + 'Please ensure the convert binary is available in your PATH.' + ) + end end MiniMagick::Tool::Convert.new do |gc| gc.size('%dx%d' % [subject_width + g_width + 1, height]) diff --git a/lib/redmine/i18n.rb b/lib/redmine/i18n.rb index 0fe080f4b..b170d278c 100644 --- a/lib/redmine/i18n.rb +++ b/lib/redmine/i18n.rb @@ -162,13 +162,11 @@ module Redmine # Custom backend based on I18n::Backend::Simple with the following changes: # * available_locales are determined by looking at translation file names class Backend < ::I18n::Backend::Simple - module Implementation - # Get available locales from the translations filenames - def available_locales - @available_locales ||= begin - redmine_locales = Dir[Rails.root / 'config' / 'locales' / '*.yml'].map { |f| File.basename(f, '.yml').to_sym } - super & redmine_locales - end + # Get available locales from the translations filenames + def available_locales + @available_locales ||= begin + redmine_locales = Dir[Rails.root / 'config' / 'locales' / '*.yml'].map { |f| File.basename(f, '.yml').to_sym } + super & redmine_locales end end diff --git a/lib/redmine/version.rb b/lib/redmine/version.rb index 1c4dc4ae5..d58b83345 100644 --- a/lib/redmine/version.rb +++ b/lib/redmine/version.rb @@ -7,7 +7,7 @@ module Redmine module VERSION MAJOR = 5 MINOR = 1 - TINY = 6 + TINY = 9 # Branch values: # * official release: nil diff --git a/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb b/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb index c689f6d9b..09be3b01d 100644 --- a/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb +++ b/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb @@ -86,7 +86,7 @@ module Redmine node = env[:node] return unless node.name == "a" || node.name == "li" return unless node.has_attribute?("id") - return if node.name == "a" && node["id"] =~ /\Afnref-\d+\z/ + return if node.name == "a" && node["id"] =~ /\Afnref(-\d+){1,2}\z/ return if node.name == "li" && node["id"] =~ /\Afn-\d+\z/ node.remove_attribute("id") diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 009424993..48efa4bce 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -348,7 +348,7 @@ tr.version.closed, tr.version.closed a { color: #999; } tr.version td.name { padding-left: 20px; } tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; } -tr.member td.icon-user, #principals_for_new_member .icon-user {background:transparent;} +tr.member td.icon-user, #principals_for_new_member .icon-user, #users_for_watcher .icon-user {background:transparent;} tr.user td {width:13%;white-space: nowrap;} td.username, td.firstname, td.lastname, td.email {text-align:left !important;} @@ -476,7 +476,7 @@ div.square { .contextual {float:right; white-space: nowrap; line-height:1.4em;margin:5px 0px; padding-left: 10px; font-size:0.9em;} .contextual .icon {padding-top: 2px; padding-bottom: 3px;} .contextual input, .contextual select {font-size:0.9em;} -.message .contextual { margin-top: 0; } +.message .contextual, #comments .contextual { margin-top: 0; } .splitcontent {overflow: auto; display: flex; flex-wrap: wrap;} .splitcontentleft {flex: 1; margin-right: 5px;} diff --git a/public/stylesheets/responsive.css b/public/stylesheets/responsive.css index 482f73cf2..5180258a2 100644 --- a/public/stylesheets/responsive.css +++ b/public/stylesheets/responsive.css @@ -778,6 +778,10 @@ width: 100%; /* let subject have one full width column */ } + #issue_tree .issue:has(.buttons a) > td.subject, #relations .issue:has(.buttons a) > td.subject { + padding-right: 40px; + } + #issue_tree .issue > td:not(.subject), #relations .issue > td:not(.subject) { width: 20%; /* three columns for all cells that are not subject */ } diff --git a/public/stylesheets/rtl.css b/public/stylesheets/rtl.css index 9cae7d2eb..e0ce7dd9a 100644 --- a/public/stylesheets/rtl.css +++ b/public/stylesheets/rtl.css @@ -217,7 +217,7 @@ a.remove-upload {background: url(../images/delete.png) no-repeat right 1px top 5 div.thumbnails div {margin-right:0px; margin-left:2px;} -p.other-formats { text-align:left; } +p.other-formats, p.query-totals { text-align:left; } a.atom { background: url(../images/feed.png) no-repeat right 1px top 50%; padding: 2px 16px 3px 0; } diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index a788f337c..7ec621ab1 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -45,6 +45,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 @@ -70,13 +75,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/changesets.yml b/test/fixtures/changesets.yml index 247dda375..8eaca6788 100644 --- a/test/fixtures/changesets.yml +++ b/test/fixtures/changesets.yml @@ -102,3 +102,15 @@ changesets_010: user_id: 3 repository_id: 10 committer: dlopper +changesets_011: + commit_date: "2025-04-07" + comments: |- + This commit references an issue and a [[wiki]] page + Refs #2 + committed_on: 2025-04-07 19:00:00 + revision: "11" + id: 110 + scmid: + user_id: 3 + repository_id: 10 + committer: dlopper diff --git a/test/functional/attachments_controller_test.rb b/test/functional/attachments_controller_test.rb index ed12955d5..2d9599d01 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/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/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 0c687d90d..08de597d1 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -1736,7 +1736,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, @@ -3164,6 +3164,22 @@ class IssuesControllerTest < Redmine::ControllerTest end end + def test_show_render_changeset_comments_in_original_context + issue = Issue.find(9) + issue.changeset_ids = [110] + issue.save! + + @request.session[:user_id] = 2 + get :issue_tab, params: {id: issue.id, name: 'changesets', format: 'js'}, xhr: true + + assert_select 'div#changeset-110' do + # assert_select 'div.tabs a[id=?]', 'tab-changesets', text: 'unicorns' + assert_select 'div.changeset-comments' do + assert_select 'a[href=?]', '/projects/ecookbook/wiki/Wiki', text: 'wiki' + end + end + end + def test_show_should_display_spent_time_tab_for_issue_with_time_entries @request.session[:user_id] = 1 get :show, :params => {:id => 3} diff --git a/test/functional/queries_controller_test.rb b/test/functional/queries_controller_test.rb index 4ee2155ff..ab3dcf531 100644 --- a/test/functional/queries_controller_test.rb +++ b/test/functional/queries_controller_test.rb @@ -986,4 +986,44 @@ class QueriesControllerTest < Redmine::ControllerTest assert_include ["Development", "10"], json assert_include ["Inactive Activity", "14"], json end + + def test_new_query_is_for_all_checkbox_not_disabled + @request.session[:user_id] = 1 + get :new + assert_response :success + # Verify that the "For all projects" checkbox is not disabled when creating a new query + assert_select 'input[name=query_is_for_all][type=checkbox][checked]:not([disabled])' + end + + def test_new_project_query_is_for_all_checkbox_not_disabled + @request.session[:user_id] = 1 + get(:new, :params => {:project_id => 1}) + assert_response :success + # Verify that the checkbox is not disabled when creating a new query within a project + assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked]):not([disabled])' + end + + def test_edit_global_query_is_for_all_checkbox_disabled + @request.session[:user_id] = 1 + # Create a global query (project_id = nil) + query = IssueQuery.create!(:name => 'test_global_query', :user_id => 1, :project_id => nil) + + get(:edit, :params => {:id => query.id}) + assert_response :success + + # Verify that the "For all projects" checkbox is disabled when editing an existing global query + assert_select 'input[name=query_is_for_all][type=checkbox][checked][disabled]' + end + + def test_edit_project_query_is_for_all_checkbox_not_disabled + @request.session[:user_id] = 1 + # Create a project-specific query + query = IssueQuery.create!(:name => 'test_project_query', :user_id => 1, :project_id => 1) + + get(:edit, :params => {:id => query.id}) + assert_response :success + + # Verify that the checkbox is not disabled when editing a project-specific query + assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked]):not([disabled])' + end end diff --git a/test/functional/repositories_bazaar_controller_test.rb b/test/functional/repositories_bazaar_controller_test.rb index 076ad7748..11f75dd06 100644 --- a/test/functional/repositories_bazaar_controller_test.rb +++ b/test/functional/repositories_bazaar_controller_test.rb @@ -37,6 +37,7 @@ class RepositoriesBazaarControllerTest < Redmine::RepositoryControllerTest :log_encoding => 'UTF-8' ) assert @repository + skip "SCM command is unavailable" unless @repository.class.scm_available end if File.directory?(REPOSITORY_PATH) diff --git a/test/functional/repositories_controller_test.rb b/test/functional/repositories_controller_test.rb index 81cc28ce9..0e5786581 100644 --- a/test/functional/repositories_controller_test.rb +++ b/test/functional/repositories_controller_test.rb @@ -186,6 +186,7 @@ class RepositoriesControllerTest < Redmine::RepositoryControllerTest def test_show_without_main_repository_should_display_first_repository skip unless repository_configured?('subversion') + skip unless Repository::Subversion.scm_available project = Project.find(1) repos = project.repositories @@ -208,6 +209,7 @@ class RepositoriesControllerTest < Redmine::RepositoryControllerTest def test_show_should_show_diff_button_depending_on_browse_repository_permission skip unless repository_configured?('subversion') + skip unless Repository::Subversion.scm_available @request.session[:user_id] = 2 role = Role.find(1) diff --git a/test/functional/repositories_cvs_controller_test.rb b/test/functional/repositories_cvs_controller_test.rb index d14d62420..34d9e441c 100644 --- a/test/functional/repositories_cvs_controller_test.rb +++ b/test/functional/repositories_cvs_controller_test.rb @@ -40,6 +40,7 @@ class RepositoriesCvsControllerTest < Redmine::RepositoryControllerTest :url => MODULE_NAME, :log_encoding => 'UTF-8') assert @repository + skip "SCM command is unavailable" unless @repository.class.scm_available end if File.directory?(REPOSITORY_PATH) diff --git a/test/functional/repositories_git_controller_test.rb b/test/functional/repositories_git_controller_test.rb index 3de0672cc..8735d599a 100644 --- a/test/functional/repositories_git_controller_test.rb +++ b/test/functional/repositories_git_controller_test.rb @@ -41,6 +41,7 @@ class RepositoriesGitControllerTest < Redmine::RepositoryControllerTest :path_encoding => 'ISO-8859-1' ) assert @repository + skip "SCM command is unavailable" unless @repository.class.scm_available end def test_create_and_update diff --git a/test/functional/repositories_mercurial_controller_test.rb b/test/functional/repositories_mercurial_controller_test.rb index 96a54bde8..fb26b3193 100644 --- a/test/functional/repositories_mercurial_controller_test.rb +++ b/test/functional/repositories_mercurial_controller_test.rb @@ -37,6 +37,8 @@ class RepositoriesMercurialControllerTest < Redmine::RepositoryControllerTest :path_encoding => 'ISO-8859-1' ) assert @repository + skip "SCM command is unavailable" unless @repository.class.scm_available + @diff_c_support = true end diff --git a/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb index b5b4061b7..44cc0a0b4 100644 --- a/test/functional/repositories_subversion_controller_test.rb +++ b/test/functional/repositories_subversion_controller_test.rb @@ -34,6 +34,7 @@ class RepositoriesSubversionControllerTest < Redmine::RepositoryControllerTest @repository = Repository::Subversion.create(:project => @project, :url => self.class.subversion_repository_url) assert @repository + skip "SCM command is unavailable" unless @repository.class.scm_available end if repository_configured?('subversion') @@ -356,6 +357,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/search_controller_test.rb b/test/functional/search_controller_test.rb index 2cb785d70..04ec5a05d 100644 --- a/test/functional/search_controller_test.rb +++ b/test/functional/search_controller_test.rb @@ -66,16 +66,18 @@ class SearchControllerTest < Redmine::ControllerTest assert_response :success assert_select '#search-results' do - assert_select 'dt.issue a', :text => /Feature request #2/ + assert_select 'dt.issue a', :text => /Bug #1/ assert_select 'dt.issue a', :text => /Bug #5/ assert_select 'dt.changeset a', :text => /Revision 1/ - assert_select 'dt.issue a', :text => /Add ingredients categories/ - assert_select 'dd', :text => /should be classified by categories/ + assert_select 'dt.issue a', :text => /Cannot print recipes/ + assert_select 'dd', :text => /Unable to print/ end assert_select '#search-results-counts' do - assert_select 'a', :text => 'Changesets (5)' + assert_select 'a', :text => 'Changesets (6)' + assert_select 'a', :text => 'Issues (5)' + assert_select 'a', :text => 'Projects (4)' end end diff --git a/test/functional/workflows_controller_test.rb b/test/functional/workflows_controller_test.rb index 61bb3e63b..7b7803c81 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/integration/api_test/attachments_test.rb b/test/integration/api_test/attachments_test.rb index d07e22a66..32c08a067 100644 --- a/test/integration/api_test/attachments_test.rb +++ b/test/integration/api_test/attachments_test.rb @@ -63,7 +63,7 @@ class Redmine::ApiTest::AttachmentsTest < Redmine::ApiTest::Base test "GET /attachments/download/:id/:filename should deny access without credentials" do get '/attachments/download/7/archive.zip' - assert_response 401 + assert_response 302 end test "GET /attachments/thumbnail/:id should return the thumbnail" do diff --git a/test/integration/api_test/custom_fields_test.rb b/test/integration/api_test/custom_fields_test.rb index 0df56e59a..4fb06636e 100644 --- a/test/integration/api_test/custom_fields_test.rb +++ b/test/integration/api_test/custom_fields_test.rb @@ -37,6 +37,8 @@ class Redmine::ApiTest::CustomFieldsTest < Redmine::ApiTest::Base end assert_select 'trackers[type=array]' assert_select 'roles[type=array]' + assert_select 'visible', :text => 'true' + assert_select 'editable', :text => 'true' end end end diff --git a/test/integration/api_test/news_test.rb b/test/integration/api_test/news_test.rb index 485af2fc1..96c3d0742 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/attachments_test.rb b/test/integration/attachments_test.rb index fc64df3ee..e0a78ca9b 100644 --- a/test/integration/attachments_test.rb +++ b/test/integration/attachments_test.rb @@ -267,6 +267,16 @@ class AttachmentsTest < Redmine::IntegrationTest end end + def test_unauthorized_named_download_link_should_redirect_to_login + with_settings login_required: '1' do + get "/attachments/download/1" + assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fattachments%2Fdownload%2F1" + + get "/attachments/download/1/error281.txt" + assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fattachments%2Fdownload%2F1%2Ferror281.txt" + end + end + private def ajax_upload(filename, content, attachment_id=1) diff --git a/test/integration/repositories_git_test.rb b/test/integration/repositories_git_test.rb index 20d643449..793b49458 100644 --- a/test/integration/repositories_git_test.rb +++ b/test/integration/repositories_git_test.rb @@ -35,6 +35,7 @@ class RepositoriesGitTest < Redmine::IntegrationTest :path_encoding => 'ISO-8859-1' ) assert @repository + skip "SCM command is unavailable" unless @repository.class.scm_available end if File.directory?(REPOSITORY_PATH) diff --git a/test/integration/routing/attachments_test.rb b/test/integration/routing/attachments_test.rb index 15e61635b..18b411f99 100644 --- a/test/integration/routing/attachments_test.rb +++ b/test/integration/routing/attachments_test.rb @@ -26,7 +26,7 @@ class RoutingAttachmentsTest < Redmine::RoutingTest should_route 'GET /attachments/1/filename.txt' => 'attachments#show', :id => '1', :filename => 'filename.txt', :format => 'html' should_route 'GET /attachments/download/1' => 'attachments#download', :id => '1' - should_route 'GET /attachments/download/1/filename.ext' => 'attachments#download', :id => '1', :filename => 'filename.ext' + should_route 'GET /attachments/download/1/filename.ext' => 'attachments#download', :id => '1', :filename => 'filename.ext', :format => 'html' should_route 'GET /attachments/thumbnail/1' => 'attachments#thumbnail', :id => '1' should_route 'GET /attachments/thumbnail/1/200' => 'attachments#thumbnail', :id => '1', :size => '200' diff --git a/test/system/issues_test.rb b/test/system/issues_test.rb index 1316c1fc9..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,7 +277,8 @@ class IssuesSystemTest < ApplicationSystemTestCase click_on 'Submit' - assert_equal 1, Issue.find(2).attachments.count + assert_text 'Successful update.' + assert_equal 3, Issue.find(2).attachments.count end test "removing issue shows confirm dialog" do 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 166bbe1f9..bf6f1b8d0 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/changeset_test.rb b/test/unit/changeset_test.rb index 3eac9ba8f..dff6459b4 100644 --- a/test/unit/changeset_test.rb +++ b/test/unit/changeset_test.rb @@ -479,7 +479,7 @@ class ChangesetTest < ActiveSupport::TestCase end def test_next_nil - changeset = Changeset.find_by_revision('10') + changeset = Changeset.find_by_revision('11') assert_nil changeset.next end diff --git a/test/unit/email_address_test.rb b/test/unit/email_address_test.rb index 8b47e9a1d..7576b2f4f 100644 --- a/test/unit/email_address_test.rb +++ b/test/unit/email_address_test.rb @@ -63,6 +63,12 @@ class EmailAddressTest < ActiveSupport::TestCase end end + def test_domain_in_should_not_raise_exception_when_domain_is_nil + assert_nothing_raised do + assert_not EmailAddress.domain_in?(nil, 'example.com') + end + end + def test_should_reject_invalid_email assert_not EmailAddress.new(address: 'invalid,email@example.com').valid? end diff --git a/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb b/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb index c0bff9b1f..9d6cd6b32 100644 --- a/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb +++ b/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb @@ -27,6 +27,7 @@ class BazaarAdapterTest < ActiveSupport::TestCase def setup @adapter = Redmine::Scm::Adapters::BazaarAdapter. new(File.join(REPOSITORY_PATH, "trunk")) + skip "SCM command is unavailable" unless @adapter.class.client_available end def test_scm_version diff --git a/test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb b/test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb index 2ed9dc618..3bfe24997 100644 --- a/test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb +++ b/test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb @@ -27,6 +27,7 @@ class CvsAdapterTest < ActiveSupport::TestCase if File.directory?(REPOSITORY_PATH) def setup @adapter = Redmine::Scm::Adapters::CvsAdapter.new(MODULE_NAME, REPOSITORY_PATH) + skip "SCM command is unavailable" unless @adapter.class.client_available end def test_scm_version diff --git a/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb b/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb index b7a95b635..9c48c2f2e 100644 --- a/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb +++ b/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb @@ -42,13 +42,6 @@ class GitAdapterTest < ActiveSupport::TestCase WINDOWS_SKIP_STR = "TODO: This test fails in Git for Windows above 1.7.10" def setup - adapter_class = Redmine::Scm::Adapters::GitAdapter - assert adapter_class - assert adapter_class.client_command - assert_equal true, adapter_class.client_available - assert_equal true, adapter_class.client_version_above?([1]) - assert_equal true, adapter_class.client_version_above?([1, 0]) - @adapter = Redmine::Scm::Adapters::GitAdapter. new( @@ -59,6 +52,8 @@ class GitAdapterTest < ActiveSupport::TestCase 'ISO-8859-1' ) assert @adapter + skip "SCM is unavailable" unless @adapter.class.client_available + @char_1 = 'Ü' @str_felix_hex = "Felix Sch\xC3\xA4fer".b end diff --git a/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb b/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb index d8347c6dd..3315f68ab 100644 --- a/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb +++ b/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb @@ -30,12 +30,6 @@ class MercurialAdapterTest < ActiveSupport::TestCase if File.directory?(REPOSITORY_PATH) def setup - adapter_class = Redmine::Scm::Adapters::MercurialAdapter - assert adapter_class - assert adapter_class.client_command - assert_equal true, adapter_class.client_available - assert_equal true, adapter_class.client_version_above?([0, 9, 5]) - @adapter = Redmine::Scm::Adapters::MercurialAdapter.new( REPOSITORY_PATH, @@ -44,6 +38,8 @@ class MercurialAdapterTest < ActiveSupport::TestCase nil, 'ISO-8859-1' ) + skip "SCM command is unavailable" unless @adapter.class.client_available + @diff_c_support = true @char_1 = 'Ü' @tag_char_1 = 'tag-Ü-00' diff --git a/test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb b/test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb index fe574a4ff..edc3541d1 100644 --- a/test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb +++ b/test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb @@ -23,6 +23,7 @@ class SubversionAdapterTest < ActiveSupport::TestCase if repository_configured?('subversion') def setup @adapter = Redmine::Scm::Adapters::SubversionAdapter.new(self.class.subversion_repository_url) + skip "SCM command is unavailable" unless @adapter.class.client_available end def test_client_version 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 d3956e802..bf7e5655f 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 diff --git a/test/unit/member_test.rb b/test/unit/member_test.rb index f92841b76..42fba4783 100644 --- a/test/unit/member_test.rb +++ b/test/unit/member_test.rb @@ -79,7 +79,7 @@ class MemberTest < ActiveSupport::TestCase [1, group_a_member.member_roles.find_by(role_id: 1).id], [1, group_b_member.member_roles.find_by(role_id: 1).id], [2, group_b_member.member_roles.find_by(role_id: 2).id], - ], test_user_member.member_roles.map{|r| [r.role_id, r.inherited_from]} + ].sort, test_user_member.member_roles.map{|r| [r.role_id, r.inherited_from]}.sort # Verify that a new non-inherited role is added and inherited roles are maintained test_user_member.set_editable_role_ids([3]) # Add Reporter role to test_user @@ -88,7 +88,7 @@ class MemberTest < ActiveSupport::TestCase [1, group_b_member.member_roles.find_by(role_id: 1).id], [2, group_b_member.member_roles.find_by(role_id: 2).id], [3, nil] - ], test_user_member.member_roles.map{|r| [r.role_id, r.inherited_from]} + ].sort, test_user_member.member_roles.map{|r| [r.role_id, r.inherited_from]}.sort end def test_validate diff --git a/test/unit/project_admin_query_test.rb b/test/unit/project_admin_query_test.rb index 7f3945fd1..e9efceaaf 100644 --- a/test/unit/project_admin_query_test.rb +++ b/test/unit/project_admin_query_test.rb @@ -95,6 +95,7 @@ class ProjectAdminQueryTest < ActiveSupport::TestCase end def test_project_statuses_values_should_return_all_statuses + set_language_if_valid 'en' q = ProjectAdminQuery.new assert_equal [ ["active", "1"], diff --git a/test/unit/repository_bazaar_test.rb b/test/unit/repository_bazaar_test.rb index 23f3ce48f..5fec37973 100644 --- a/test/unit/repository_bazaar_test.rb +++ b/test/unit/repository_bazaar_test.rb @@ -50,6 +50,7 @@ class RepositoryBazaarTest < ActiveSupport::TestCase :log_encoding => 'UTF-8' ) assert @repository + skip "SCM command is unavailable" unless @repository.class.scm_available end def test_blank_path_to_repository_error_message diff --git a/test/unit/repository_cvs_test.rb b/test/unit/repository_cvs_test.rb index af995eac0..84d0ed80b 100644 --- a/test/unit/repository_cvs_test.rb +++ b/test/unit/repository_cvs_test.rb @@ -36,6 +36,7 @@ class RepositoryCvsTest < ActiveSupport::TestCase :url => MODULE_NAME, :log_encoding => 'UTF-8') assert @repository + skip "SCM command is unavailable" unless @repository.class.scm_available end def test_blank_module_error_message diff --git a/test/unit/repository_git_test.rb b/test/unit/repository_git_test.rb index ec1ca5157..857be9442 100644 --- a/test/unit/repository_git_test.rb +++ b/test/unit/repository_git_test.rb @@ -41,6 +41,7 @@ class RepositoryGitTest < ActiveSupport::TestCase :path_encoding => 'ISO-8859-1' ) assert @repository + skip "SCM command is unavailable" unless @repository.class.scm_available end def test_nondefault_repo_with_blank_identifier_destruction diff --git a/test/unit/repository_mercurial_test.rb b/test/unit/repository_mercurial_test.rb index 861729bac..f9a18acb4 100644 --- a/test/unit/repository_mercurial_test.rb +++ b/test/unit/repository_mercurial_test.rb @@ -35,6 +35,7 @@ class RepositoryMercurialTest < ActiveSupport::TestCase :path_encoding => 'ISO-8859-1' ) assert @repository + skip "SCM command is unavailable" unless @repository.class.scm_available end def test_blank_path_to_repository_error_message @@ -164,6 +165,10 @@ class RepositoryMercurialTest < ActiveSupport::TestCase end def test_fetch_changesets_from_scratch + # This test fails when using Mercurial >= 5.1 due to a change in behavior. + # See https://repo.mercurial-scm.org/hg/rev/0c72eddb4be5 for details. + skip "Test skipped because Mercurial >= 5.1 is used" if @repository.scm.class.client_version_above?([5, 1]) + assert_equal 0, @repository.changesets.count @repository.fetch_changesets @project.reload @@ -247,6 +252,10 @@ class RepositoryMercurialTest < ActiveSupport::TestCase end def test_latest_changesets + # This test fails when using Mercurial >= 5.1 due to a change in behavior. + # See https://repo.mercurial-scm.org/hg/rev/0c72eddb4be5 for details. + skip "Test skipped because Mercurial >= 5.1 is used" if @repository.scm.class.client_version_above?([5, 1]) + assert_equal 0, @repository.changesets.count @repository.fetch_changesets @project.reload diff --git a/test/unit/repository_subversion_test.rb b/test/unit/repository_subversion_test.rb index 477b738d5..3794cfb05 100644 --- a/test/unit/repository_subversion_test.rb +++ b/test/unit/repository_subversion_test.rb @@ -30,6 +30,7 @@ class RepositorySubversionTest < ActiveSupport::TestCase @repository = Repository::Subversion.create(:project => @project, :url => self.class.subversion_repository_url) assert @repository + skip "SCM command is unavailable" unless @repository.class.scm_available end def test_invalid_url diff --git a/test/unit/repository_test.rb b/test/unit/repository_test.rb index a02e30271..d847e1f64 100644 --- a/test/unit/repository_test.rb +++ b/test/unit/repository_test.rb @@ -455,7 +455,7 @@ class RepositoryTest < ActiveSupport::TestCase def test_stats_by_author_reflect_changesets_and_changes repository = Repository.find(10) - expected = {"Dave Lopper"=>{:commits_count=>10, :changes_count=>3}} + expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>3}} assert_equal expected, repository.stats_by_author set = Changeset.create!( @@ -467,7 +467,7 @@ class RepositoryTest < ActiveSupport::TestCase ) Change.create!(:changeset => set, :action => 'A', :path => '/path/to/file1') Change.create!(:changeset => set, :action => 'A', :path => '/path/to/file2') - expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>5}} + expected = {"Dave Lopper"=>{:commits_count=>12, :changes_count=>5}} assert_equal expected, repository.stats_by_author end @@ -476,7 +476,7 @@ class RepositoryTest < ActiveSupport::TestCase # to ensure things are dynamically linked to Users User.find_by_login("dlopper").update_attribute(:firstname, "Dave's") repository = Repository.find(10) - expected = {"Dave's Lopper"=>{:commits_count=>10, :changes_count=>3}} + expected = {"Dave's Lopper"=>{:commits_count=>11, :changes_count=>3}} assert_equal expected, repository.stats_by_author end @@ -502,7 +502,7 @@ class RepositoryTest < ActiveSupport::TestCase # with committer="dlopper <dlopper@somefoo.net>" repository = Repository.find(10) - expected = {"Dave Lopper"=>{:commits_count=>10, :changes_count=>3}} + expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>3}} assert_equal expected, repository.stats_by_author set = Changeset.create!( @@ -513,7 +513,7 @@ class RepositoryTest < ActiveSupport::TestCase :comments => 'Another commit by foo.' ) - expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>3}} + expected = {"Dave Lopper"=>{:commits_count=>12, :changes_count=>3}} assert_equal expected, repository.stats_by_author end |