From 20c8a19277ade80efe2f606253b1383f19381674 Mon Sep 17 00:00:00 2001 From: MAEDA Go Date: Thu, 15 Oct 2026 00:03:48 +0900 Subject: [PATCH 1/3] Allow users to set their avatar in Redmine --- app/assets/stylesheets/application.css | 2 +- app/assets/stylesheets/responsive.css | 2 +- app/controllers/avatars_controller.rb | 54 +++++++++++++ app/helpers/avatars_helper.rb | 24 ++++++ app/models/attachment.rb | 6 +- app/models/user.rb | 56 +++++++++++++ app/views/settings/_display.html.erb | 5 ++ app/views/users/index.api.rsb | 3 +- app/views/users/show.api.rsb | 3 +- config/locales/en.yml | 3 + config/routes.rb | 1 + config/settings.yml | 2 + .../20261015000046_add_avatar_id_to_users.rb | 5 ++ lib/redmine/thumbnail.rb | 6 +- test/functional/avatars_controller_test.rb | 68 ++++++++++++++++ test/helpers/avatars_helper_test.rb | 19 +++++ test/integration/api_test/users_test.rb | 11 +++ test/integration/routing/users_test.rb | 2 + test/unit/lib/redmine/thumbnail_test.rb | 13 +++ test/unit/user_test.rb | 80 +++++++++++++++++++ 20 files changed, 358 insertions(+), 7 deletions(-) create mode 100644 app/controllers/avatars_controller.rb create mode 100644 db/migrate/20261015000046_add_avatar_id_to_users.rb create mode 100644 test/functional/avatars_controller_test.rb diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index d9b598341..3dfb0efb0 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -2364,7 +2364,7 @@ div.avatar-with-child > .avatar:nth-child(2).group-avatar { } h2 .avatar, h3 .avatar {margin-inline-end: 4px;} -h4 img.gravatar {margin-block: -2px -4px; margin-inline: 0 4px;} +h4 img.avatar {margin-block: -2px -4px; margin-inline: 0 4px;} #activity dt .avatar { margin-block: 0; margin-inline: 0 1em; diff --git a/app/assets/stylesheets/responsive.css b/app/assets/stylesheets/responsive.css index 38bb19061..50ad9c085 100644 --- a/app/assets/stylesheets/responsive.css +++ b/app/assets/stylesheets/responsive.css @@ -494,7 +494,7 @@ border-block-start: 1px solid rgba(255,255,255,.1); } - .flyout-menu__avatar img.gravatar { + .flyout-menu__avatar img.avatar { inline-size: 40px; block-size: 40px; padding: 0; diff --git a/app/controllers/avatars_controller.rb b/app/controllers/avatars_controller.rb new file mode 100644 index 000000000..4c1a28ccf --- /dev/null +++ b/app/controllers/avatars_controller.rb @@ -0,0 +1,54 @@ +# 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. + +class AvatarsController < ApplicationController + before_action :find_avatar + before_action :check_avatar_available + + def show + # The original image is never served since it may contain metadata + # such as the location where it was taken + file = @avatar.thumbnail(:size => params[:size], :crop => true) + unless file + render_404 + return + end + + expires_in 1.day, :public => !Setting.login_required? + if stale?(:etag => file, :template => false) + send_file( + file, + :filename => filename_for_content_disposition(@avatar.filename), + :type => @avatar.content_type, + :disposition => 'inline') + end + end + + private + + def find_avatar + user = User.find_by(:id => params[:user_id], :avatar_id => params[:id]) + @avatar = user&.avatar + render_404 unless @avatar&.readable? + end + + def check_avatar_available + render_404 unless User.avatar_available? + end +end diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index e5875e9ba..47a70bd1a 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -34,6 +34,15 @@ module AvatarsHelper avatar(user, options).to_s.html_safe end + # Returns the full URL of the avatar image of +user+ for the API, or nil + def avatar_image_url(user) + if local_avatar?(user) + user_avatar_url(user, user.avatar_id) + elsif user.mail && Setting.gravatar_enabled? + gravatar_url(user.mail, {rating: nil, size: nil, default: Setting.gravatar_default}) + end + end + # Returns the avatar image tag for the given +user+ if avatars are enabled # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe ') def avatar(user, options = {}) @@ -46,6 +55,8 @@ module AvatarsHelper anonymous_avatar(options) elsif user.is_a?(Group) group_avatar(options) + elsif local_avatar?(user) + local_avatar_tag(user, options) elsif Setting.gravatar_enabled? gravatar_avatar_tag(user, options) elsif user.respond_to?(:initials) @@ -75,6 +86,19 @@ module AvatarsHelper private + def local_avatar?(user) + user.is_a?(User) && user.avatar_id && User.avatar_available? + end + + def local_avatar_tag(user, options) + size = (options.delete(:size) || GravatarHelper::DEFAULT_OPTIONS[:size]).to_i + options[:title] ||= user.name + options[:srcset] = "#{user_avatar_path(user, user.avatar_id, :size => size * 2)} 2x" + image_tag( + user_avatar_path(user, user.avatar_id, :size => size), + GravatarHelper::DEFAULT_OPTIONS.slice(:alt).merge(options).merge(:width => size, :height => size)) + end + def gravatar_avatar_tag(user, options) options[:default] = Setting.gravatar_default options[:class] = [GravatarHelper::DEFAULT_OPTIONS[:class], options[:class]].compact.join(' ') diff --git a/app/models/attachment.rb b/app/models/attachment.rb index eeeb16910..3a8d422af 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -262,6 +262,7 @@ class Attachment < ApplicationRecord # Returns the full path the attachment thumbnail, or nil # if the thumbnail cannot be generated. + # Pass :crop => true to get a square thumbnail cropped around the center. def thumbnail(options={}) if thumbnailable? && readable? size = options[:size].to_i @@ -274,11 +275,12 @@ class Attachment < ApplicationRecord size = Setting.thumbnails_size.to_i end size = 100 unless size > 0 - target = thumbnail_path(size) + crop = !!options[:crop] + target = thumbnail_path(crop ? "#{size}_crop" : size) begin # TODO: Stop passing the deprecated is_pdf flag in Redmine 7.0 - Redmine::Thumbnail.generate(self.diskfile, target, size, is_pdf?) + Redmine::Thumbnail.generate(self.diskfile, target, size, is_pdf?, :crop => crop) rescue => e if logger logger.error( diff --git a/app/models/user.rb b/app/models/user.rb index dad8213a5..723921db5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -109,6 +109,7 @@ class User < Principal :foreign_key => :resource_owner_id, :dependent => :delete_all belongs_to :auth_source + belongs_to :avatar, :class_name => 'Attachment', :dependent => :destroy scope :logged, lambda {where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}")} scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i})} @@ -120,6 +121,8 @@ class User < Principal attr_accessor :remote_ip attr_writer :oauth_scope + AVATAR_CONTENT_TYPES = %w(image/avif image/gif image/jpeg image/png image/webp).freeze + LOGIN_LENGTH_LIMIT = 60 MAIL_LENGTH_LIMIT = 254 @@ -425,6 +428,52 @@ class User < Principal self.preference ||= UserPreference.new(:user => self) end + # Avatars are served only as thumbnails, which require ImageMagick + def self.avatar_available? + Setting.avatar_enabled? && Redmine::Thumbnail.convert_available? + end + + # Replaces the avatar with the uploaded +file+ and returns true, or returns + # false with errors if the file cannot be used as an avatar + def update_avatar(file) + attachment = Attachment.new(:file => file, :author => User.current, :container => self) + # Avatars are served only as thumbnails + unless User.avatar_available? && AVATAR_CONTENT_TYPES.include?(Marcel::MimeType.for(file)) && attachment.thumbnailable? + errors.add(:avatar, :invalid) + return false + end + + unless attachment.valid? + attachment.errors.full_messages.each {|message| errors.add(:base, message)} + return false + end + + transaction do + attachment.save! + replace_avatar(attachment) + end + true + end + + def delete_avatar + replace_avatar(nil) + end + + # Unlike the thumbnails, the original image of the avatar may contain + # metadata such as the location where it was taken + def attachments_visible?(user=User.current) + user == self || user.admin? + end + + # Avatars can only be changed through AvatarsController + def attachments_editable?(user=User.current) + false + end + + def attachments_deletable?(user=User.current) + false + end + def time_zone @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone]) end @@ -975,6 +1024,13 @@ class User < Principal private + def replace_avatar(attachment) + old_avatar = avatar + self.avatar = attachment + update_column(:avatar_id, avatar_id) + old_avatar&.destroy + end + def generate_password_if_needed if generate_password? && auth_source.nil? length = [Setting.password_min_length.to_i + 2, 10].max diff --git a/app/views/settings/_display.html.erb b/app/views/settings/_display.html.erb index 9f7fbc881..56912f854 100644 --- a/app/views/settings/_display.html.erb +++ b/app/views/settings/_display.html.erb @@ -19,6 +19,11 @@

<%= setting_select :user_format, @options[:user_format] %>

+

<%= setting_check_box :avatar_enabled %> +<% unless Redmine::Thumbnail.convert_available? %> +<%= sprite_icon('warning', l(:text_setting_avatar_enabled_imagemagick_unavailable)) %> +<% end %>

+

<%= setting_check_box :gravatar_enabled, :data => {:enables => '#settings_gravatar_default'} %> <%= t(:text_avatar_server_config_html, :url => Redmine::Configuration['avatar_server_url']) %>

diff --git a/app/views/users/index.api.rsb b/app/views/users/index.api.rsb index 8dd376e25..a17c2653e 100644 --- a/app/views/users/index.api.rsb +++ b/app/views/users/index.api.rsb @@ -11,7 +11,8 @@ api.array :users, api_meta(:total_count => @user_count, :offset => @offset, :lim api.updated_on user.updated_on api.last_login_on user.last_login_on api.passwd_changed_on user.passwd_changed_on - api.avatar_url gravatar_url(user.mail, {rating: nil, size: nil, default: Setting.gravatar_default}) if Setting.gravatar_enabled? + avatar_url = avatar_image_url(user) + api.avatar_url avatar_url if avatar_url api.twofa_scheme user.twofa_scheme api.status user.status diff --git a/app/views/users/show.api.rsb b/app/views/users/show.api.rsb index 0681903b8..982693c18 100644 --- a/app/views/users/show.api.rsb +++ b/app/views/users/show.api.rsb @@ -9,7 +9,8 @@ api.user do api.updated_on @user.updated_on api.last_login_on @user.last_login_on api.passwd_changed_on @user.passwd_changed_on - api.avatar_url gravatar_url(@user.mail, {rating: nil, size: nil, default: Setting.gravatar_default}) if @user.mail && Setting.gravatar_enabled? + avatar_url = avatar_image_url(@user) + api.avatar_url avatar_url if avatar_url api.twofa_scheme @user.twofa_scheme if User.current.admin? || (User.current == @user) api.api_key @user.api_key if (User.current.admin? || (User.current == @user && !User.current.authorized_by_oauth?)) api.status @user.status if User.current.admin? diff --git a/config/locales/en.yml b/config/locales/en.yml index 279539b88..4c5e402b8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -357,6 +357,7 @@ en: field_start_date: Start date field_done_ratio: "% Done" field_auth_source: Authentication mode + field_avatar: Avatar field_hide_mail: Hide my email address field_comments: Comment field_url: URL @@ -479,6 +480,7 @@ en: setting_mail_handler_preferred_body_part: Preferred part of multipart (HTML) emails setting_sys_api_key: Repository management WS API key setting_sequential_project_identifiers: Generate sequential project identifiers + setting_avatar_enabled: Allow users to set their avatar setting_gravatar_enabled: Use Gravatar user icons setting_gravatar_default: Default Gravatar image setting_diff_max_lines_displayed: Maximum number of diff lines displayed @@ -1398,6 +1400,7 @@ en: text_allowed_queries_to_select: Public (to any users) queries only selectable text_setting_config_change: You can configure the behaviour in config/configuration.yml. Please restart the application after editing it. text_setting_gravatar_default_initials_html: Users' initials are sent to %{url} to generate their avatars. + text_setting_avatar_enabled_imagemagick_unavailable: ImageMagick is not available, so users cannot set their avatar. text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, this application will be able to read and write all data and impersonate other users. text_oauth_admin_permission_info: This application requests full administrative access. If you are an Administrator (or become one in the future), it will be able to read and write all data and impersonate other users on your behalf. If you want to avoid this, authorize it as a user without Administrator privileges instead. text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown again. diff --git a/config/routes.rb b/config/routes.rb index bd6f39fcc..66de196e1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -129,6 +129,7 @@ Rails.application.routes.draw do resources :memberships, :controller => 'principal_memberships' resources :email_addresses, :only => [:index, :create, :update, :destroy] end + get 'users/:user_id/avatar/:id(/:size)', :to => 'avatars#show', :user_id => /\d+/, :id => /\d+/, :size => /\d+/, :as => 'user_avatar' post 'watchers/watch', :to => 'watchers#watch', :as => 'watch' delete 'watchers/watch', :to => 'watchers#unwatch' diff --git a/config/settings.yml b/config/settings.yml index 667a4427f..c9b2c6282 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -328,6 +328,8 @@ emails_footer: default: |- You have received this notification because you have either subscribed to it, or are involved in it. To change your notification preferences, please click here: http://hostname/my/account +avatar_enabled: + default: 1 gravatar_enabled: default: 0 gravatar_default: diff --git a/db/migrate/20261015000046_add_avatar_id_to_users.rb b/db/migrate/20261015000046_add_avatar_id_to_users.rb new file mode 100644 index 000000000..a0e4f0bec --- /dev/null +++ b/db/migrate/20261015000046_add_avatar_id_to_users.rb @@ -0,0 +1,5 @@ +class AddAvatarIdToUsers < ActiveRecord::Migration[8.1] + def change + add_column :users, :avatar_id, :integer + end +end diff --git a/lib/redmine/thumbnail.rb b/lib/redmine/thumbnail.rb index 7d585437f..99da8a5d1 100644 --- a/lib/redmine/thumbnail.rb +++ b/lib/redmine/thumbnail.rb @@ -33,8 +33,9 @@ module Redmine ALLOWED_TYPES = %w(image/avif image/bmp image/gif image/jpeg image/png image/webp application/pdf) # Generates a thumbnail for the source image to target + # If crop is true, the image is cropped to a square around its center # TODO: Remove the deprecated _is_pdf parameter in Redmine 7.0 - def self.generate(source, target, size, _is_pdf = nil) + def self.generate(source, target, size, _is_pdf = nil, crop: false) return nil unless convert_available? unless File.exist?(target) @@ -54,6 +55,9 @@ module Redmine return nil unless valid_pdf_magic?(source) cmd = "#{shell_quote CONVERT_BIN} #{shell_quote "#{source}[0]"} -thumbnail #{shell_quote size_option} #{shell_quote "png:#{output}"}" + elsif crop + cmd = "#{shell_quote CONVERT_BIN} #{shell_quote source} -auto-orient -thumbnail #{shell_quote "#{size}x#{size}^"} " \ + "-gravity center -extent #{shell_quote "#{size}x#{size}"} #{shell_quote output}" else cmd = "#{shell_quote CONVERT_BIN} #{shell_quote source} -auto-orient -thumbnail #{shell_quote size_option} #{shell_quote output}" end diff --git a/test/functional/avatars_controller_test.rb b/test/functional/avatars_controller_test.rb new file mode 100644 index 000000000..170d3b0ee --- /dev/null +++ b/test/functional/avatars_controller_test.rb @@ -0,0 +1,68 @@ +# 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 AvatarsControllerTest < Redmine::ControllerTest + def setup + User.current = nil + set_fixtures_attachments_directory + User.find(2).update_column(:avatar_id, 16) + end + + def teardown + set_tmp_attachments_directory + end + + def test_show + skip unless convert_installed? + + get :show, :params => {:user_id => 2, :id => 16, :size => 48} + assert_response :success + assert_equal 'image/png', response.media_type + assert_match /\Ainline/, response.headers['Content-Disposition'] + assert_equal "max-age=#{1.day.to_i}, public", response.headers['Cache-Control'] + end + + def test_show_should_not_serve_original_image_if_thumbnail_cannot_be_generated + Attachment.any_instance.stubs(:thumbnail).returns(nil) + get :show, :params => {:user_id => 2, :id => 16} + assert_response :not_found + end + + def test_show_should_return_404_with_avatars_disabled + Redmine::Thumbnail.stubs(:convert_available?).returns(true) + with_settings :avatar_enabled => '0' do + get :show, :params => {:user_id => 2, :id => 16} + end + assert_response :not_found + end + + def test_show_should_return_404_for_replaced_avatar + get :show, :params => {:user_id => 2, :id => 15} + assert_response :not_found + end + + def test_show_should_require_login_if_login_is_required + with_settings :login_required => '1' do + get :show, :params => {:user_id => 2, :id => 16} + assert_response :redirect + end + end +end diff --git a/test/helpers/avatars_helper_test.rb b/test/helpers/avatars_helper_test.rb index edc58dc5e..8354efc0f 100644 --- a/test/helpers/avatars_helper_test.rb +++ b/test/helpers/avatars_helper_test.rb @@ -31,6 +31,25 @@ class AvatarsHelperTest < Redmine::HelperTest assert_include Digest::SHA256.hexdigest('jsmith@somenet.foo'), avatar(User.find_by_mail('jsmith@somenet.foo')) end + def test_avatar_with_user_having_local_avatar + Redmine::Thumbnail.stubs(:convert_available?).returns(true) + user = User.find(2) + user.avatar_id = 16 + + avatar_tag = avatar(user, :size => 24) + assert_include 'src="/users/2/avatar/16/24"', avatar_tag + assert_include 'srcset="/users/2/avatar/16/48 2x"', avatar_tag + assert_include 'width="24" height="24"', avatar_tag + end + + def test_avatar_should_ignore_local_avatar_without_imagemagick + Redmine::Thumbnail.stubs(:convert_available?).returns(false) + user = User.find(2) + user.avatar_id = 16 + + assert_include Digest::SHA256.hexdigest('jsmith@somenet.foo'), avatar(user) + end + def test_avatar_with_email_string assert_include Digest::SHA256.hexdigest('jsmith@somenet.foo'), avatar('jsmith ') end diff --git a/test/integration/api_test/users_test.rb b/test/integration/api_test/users_test.rb index 8cf5194d8..5f4afbd6e 100644 --- a/test/integration/api_test/users_test.rb +++ b/test/integration/api_test/users_test.rb @@ -168,6 +168,17 @@ class Redmine::ApiTest::UsersTest < Redmine::ApiTest::Base assert_select 'user avatar_url', :text => %r|\Ahttps://gravatar.com/avatar/\h{64}\?default=robohash| end + test "GET /users/:id.xml should return the URL of the uploaded avatar" do + Redmine::Thumbnail.stubs(:convert_available?).returns(true) + User.find(2).update_column(:avatar_id, 16) + with_settings :gravatar_enabled => '1' do + get '/users/2.xml' + end + + assert_response :success + assert_select 'user avatar_url', :text => 'http://www.example.com/users/2/avatar/16' + end + test "GET /users/:id.xml should not return avatar_url when not set email address" do user = User.find(2) user.email_addresses.delete_all diff --git a/test/integration/routing/users_test.rb b/test/integration/routing/users_test.rb index bc976e1c9..6ca38a758 100644 --- a/test/integration/routing/users_test.rb +++ b/test/integration/routing/users_test.rb @@ -34,5 +34,7 @@ class RoutingUsersTest < Redmine::RoutingTest should_route 'DELETE /users/bulk_destroy' => 'users#bulk_destroy' should_route 'POST /users/bulk_lock' => 'users#bulk_lock' should_route 'POST /users/bulk_unlock' => 'users#bulk_unlock' + + should_route 'GET /users/44/avatar/2/48' => 'avatars#show', :user_id => '44', :id => '2', :size => '48' end end diff --git a/test/unit/lib/redmine/thumbnail_test.rb b/test/unit/lib/redmine/thumbnail_test.rb index 67fd72123..0e7b08195 100644 --- a/test/unit/lib/redmine/thumbnail_test.rb +++ b/test/unit/lib/redmine/thumbnail_test.rb @@ -68,4 +68,17 @@ class Redmine::ThumbnailTest < ActiveSupport::TestCase ensure FileUtils.rm_f(target) if target end + + def test_thumbnail_with_crop_should_be_square + skip unless Redmine::Thumbnail.convert_available? + + set_tmp_attachments_directory + file = file_fixture('2010/11/101123161450_testfile_1.png') + target = File.join(Attachment.storage_path, "#{SecureRandom.hex(8)}.thumb") + Redmine::Thumbnail.generate(file.to_s, target, 50, :crop => true) + # Width and height in the IHDR chunk of the PNG file + assert_equal [50, 50], File.binread(target, 8, 16).unpack('NN') + ensure + FileUtils.rm_f(target) if target + end end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 3b19aabe2..2b6b19687 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -1568,4 +1568,84 @@ class UserTest < ActiveSupport::TestCase users(:users_004).destroy end end + + def test_update_avatar_should_save_image_as_attachment_of_user + set_tmp_attachments_directory + assert update_avatar(@jsmith, '2010/11/101123161450_testfile_1.png') + + avatar = @jsmith.reload.avatar + assert_equal @jsmith, avatar.container + assert_equal 'image/png', avatar.content_type + end + + def test_avatar_attachment_should_be_visible_only_to_user_and_administrators + set_tmp_attachments_directory + update_avatar(@jsmith, '2010/11/101123161450_testfile_1.png') + + avatar = @jsmith.reload.avatar + assert avatar.visible?(@jsmith) + assert avatar.visible?(@admin) + assert_not avatar.visible?(@dlopper) + end + + def test_avatar_available_should_require_setting_and_imagemagick + Redmine::Thumbnail.stubs(:convert_available?).returns(true) + assert User.avatar_available? + with_settings :avatar_enabled => '0' do + assert_not User.avatar_available? + end + + Redmine::Thumbnail.stubs(:convert_available?).returns(false) + assert_not User.avatar_available? + end + + def test_update_avatar_should_reject_images_without_imagemagick + Redmine::Thumbnail.stubs(:convert_available?).returns(false) + assert_no_difference 'Attachment.count' do + assert_not @jsmith.update_avatar(uploaded_test_file('2010/11/101123161450_testfile_1.png', 'image/png')) + end + end + + def test_update_avatar_should_reject_non_raster_images + assert_no_difference 'Attachment.count' do + assert_not update_avatar(@jsmith, 'testfile.svg') + end + assert_include :avatar, @jsmith.errors.attribute_names + end + + def test_update_avatar_should_replace_existing_avatar + set_tmp_attachments_directory + update_avatar(@jsmith, '2010/11/101123161450_testfile_1.png') + old_avatar_id = @jsmith.avatar_id + + update_avatar(@jsmith, '2010/12/101223161450_testfile_2.png') + assert_not_equal old_avatar_id, @jsmith.reload.avatar_id + assert_not Attachment.exists?(old_avatar_id) + end + + def test_delete_avatar_should_destroy_avatar + set_tmp_attachments_directory + update_avatar(@jsmith, '2010/11/101123161450_testfile_1.png') + avatar_id = @jsmith.avatar_id + + @jsmith.delete_avatar + assert_nil @jsmith.reload.avatar_id + assert_not Attachment.exists?(avatar_id) + end + + def test_destroy_should_destroy_avatar + set_tmp_attachments_directory + update_avatar(@jsmith, '2010/11/101123161450_testfile_1.png') + + assert_difference 'Attachment.count', -1 do + @jsmith.destroy + end + end + + private + + def update_avatar(user, name) + Redmine::Thumbnail.stubs(:convert_available?).returns(true) + user.update_avatar(uploaded_test_file(name, Redmine::MimeType.of(name))) + end end -- 2.55.0