##// END OF EJS Templates
Merged r10992 from trunk (#12400)....
Jean-Philippe Lang -
r10777:82c7dc11d203
parent child
Show More
@@ -1,320 +1,326
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class CustomField < ActiveRecord::Base
19 19 include Redmine::SubclassFactory
20 20
21 21 has_many :custom_values, :dependent => :delete_all
22 22 acts_as_list :scope => 'type = \'#{self.class}\''
23 23 serialize :possible_values
24 24
25 25 validates_presence_of :name, :field_format
26 26 validates_uniqueness_of :name, :scope => :type
27 27 validates_length_of :name, :maximum => 30
28 28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
29 29
30 30 validate :validate_custom_field
31 31 before_validation :set_searchable
32 32
33 33 CUSTOM_FIELDS_TABS = [
34 34 {:name => 'IssueCustomField', :partial => 'custom_fields/index',
35 35 :label => :label_issue_plural},
36 36 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index',
37 37 :label => :label_spent_time},
38 38 {:name => 'ProjectCustomField', :partial => 'custom_fields/index',
39 39 :label => :label_project_plural},
40 40 {:name => 'VersionCustomField', :partial => 'custom_fields/index',
41 41 :label => :label_version_plural},
42 42 {:name => 'UserCustomField', :partial => 'custom_fields/index',
43 43 :label => :label_user_plural},
44 44 {:name => 'GroupCustomField', :partial => 'custom_fields/index',
45 45 :label => :label_group_plural},
46 46 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index',
47 47 :label => TimeEntryActivity::OptionName},
48 48 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index',
49 49 :label => IssuePriority::OptionName},
50 50 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index',
51 51 :label => DocumentCategory::OptionName}
52 52 ]
53 53
54 54 CUSTOM_FIELDS_NAMES = CUSTOM_FIELDS_TABS.collect{|v| v[:name]}
55 55
56 56 def field_format=(arg)
57 57 # cannot change format of a saved custom field
58 58 super if new_record?
59 59 end
60 60
61 61 def set_searchable
62 62 # make sure these fields are not searchable
63 63 self.searchable = false if %w(int float date bool).include?(field_format)
64 64 # make sure only these fields can have multiple values
65 65 self.multiple = false unless %w(list user version).include?(field_format)
66 66 true
67 67 end
68 68
69 69 def validate_custom_field
70 70 if self.field_format == "list"
71 71 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
72 72 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
73 73 end
74 74
75 75 if regexp.present?
76 76 begin
77 77 Regexp.new(regexp)
78 78 rescue
79 79 errors.add(:regexp, :invalid)
80 80 end
81 81 end
82 82
83 83 if default_value.present? && !valid_field_value?(default_value)
84 84 errors.add(:default_value, :invalid)
85 85 end
86 86 end
87 87
88 88 def possible_values_options(obj=nil)
89 89 case field_format
90 90 when 'user', 'version'
91 91 if obj.respond_to?(:project) && obj.project
92 92 case field_format
93 93 when 'user'
94 94 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
95 95 when 'version'
96 96 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
97 97 end
98 98 elsif obj.is_a?(Array)
99 99 obj.collect {|o| possible_values_options(o)}.reduce(:&)
100 100 else
101 101 []
102 102 end
103 103 when 'bool'
104 104 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
105 105 else
106 106 possible_values || []
107 107 end
108 108 end
109 109
110 110 def possible_values(obj=nil)
111 111 case field_format
112 112 when 'user', 'version'
113 113 possible_values_options(obj).collect(&:last)
114 114 when 'bool'
115 115 ['1', '0']
116 116 else
117 117 values = super()
118 118 if values.is_a?(Array)
119 119 values.each do |value|
120 120 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
121 121 end
122 122 end
123 123 values || []
124 124 end
125 125 end
126 126
127 127 # Makes possible_values accept a multiline string
128 128 def possible_values=(arg)
129 129 if arg.is_a?(Array)
130 130 super(arg.compact.collect(&:strip).select {|v| !v.blank?})
131 131 else
132 132 self.possible_values = arg.to_s.split(/[\n\r]+/)
133 133 end
134 134 end
135 135
136 136 def cast_value(value)
137 137 casted = nil
138 138 unless value.blank?
139 139 case field_format
140 140 when 'string', 'text', 'list'
141 141 casted = value
142 142 when 'date'
143 143 casted = begin; value.to_date; rescue; nil end
144 144 when 'bool'
145 145 casted = (value == '1' ? true : false)
146 146 when 'int'
147 147 casted = value.to_i
148 148 when 'float'
149 149 casted = value.to_f
150 150 when 'user', 'version'
151 151 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
152 152 end
153 153 end
154 154 casted
155 155 end
156 156
157 157 def value_from_keyword(keyword, customized)
158 158 possible_values_options = possible_values_options(customized)
159 159 if possible_values_options.present?
160 160 keyword = keyword.to_s.downcase
161 possible_values_options.detect {|text, id| text.downcase == keyword}.try(:last)
161 if v = possible_values_options.detect {|text, id| text.downcase == keyword}
162 if v.is_a?(Array)
163 v.last
164 else
165 v
166 end
167 end
162 168 else
163 169 keyword
164 170 end
165 171 end
166 172
167 173 # Returns a ORDER BY clause that can used to sort customized
168 174 # objects by their value of the custom field.
169 175 # Returns nil if the custom field can not be used for sorting.
170 176 def order_statement
171 177 return nil if multiple?
172 178 case field_format
173 179 when 'string', 'text', 'list', 'date', 'bool'
174 180 # COALESCE is here to make sure that blank and NULL values are sorted equally
175 181 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
176 182 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
177 183 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
178 184 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
179 185 when 'int', 'float'
180 186 # Make the database cast values into numeric
181 187 # Postgresql will raise an error if a value can not be casted!
182 188 # CustomValue validations should ensure that it doesn't occur
183 189 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
184 190 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
185 191 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
186 192 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
187 193 when 'user', 'version'
188 194 value_class.fields_for_order_statement(value_join_alias)
189 195 else
190 196 nil
191 197 end
192 198 end
193 199
194 200 # Returns a GROUP BY clause that can used to group by custom value
195 201 # Returns nil if the custom field can not be used for grouping.
196 202 def group_statement
197 203 return nil if multiple?
198 204 case field_format
199 205 when 'list', 'date', 'bool', 'int'
200 206 order_statement
201 207 when 'user', 'version'
202 208 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
203 209 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
204 210 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
205 211 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
206 212 else
207 213 nil
208 214 end
209 215 end
210 216
211 217 def join_for_order_statement
212 218 case field_format
213 219 when 'user', 'version'
214 220 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
215 221 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
216 222 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
217 223 " AND #{join_alias}.custom_field_id = #{id}" +
218 224 " AND #{join_alias}.value <> ''" +
219 225 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
220 226 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
221 227 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
222 228 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" +
223 229 " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" +
224 230 " ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id"
225 231 else
226 232 nil
227 233 end
228 234 end
229 235
230 236 def join_alias
231 237 "cf_#{id}"
232 238 end
233 239
234 240 def value_join_alias
235 241 join_alias + "_" + field_format
236 242 end
237 243
238 244 def <=>(field)
239 245 position <=> field.position
240 246 end
241 247
242 248 # Returns the class that values represent
243 249 def value_class
244 250 case field_format
245 251 when 'user', 'version'
246 252 field_format.classify.constantize
247 253 else
248 254 nil
249 255 end
250 256 end
251 257
252 258 def self.customized_class
253 259 self.name =~ /^(.+)CustomField$/
254 260 begin; $1.constantize; rescue nil; end
255 261 end
256 262
257 263 # to move in project_custom_field
258 264 def self.for_all
259 265 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
260 266 end
261 267
262 268 def type_name
263 269 nil
264 270 end
265 271
266 272 # Returns the error messages for the given value
267 273 # or an empty array if value is a valid value for the custom field
268 274 def validate_field_value(value)
269 275 errs = []
270 276 if value.is_a?(Array)
271 277 if !multiple?
272 278 errs << ::I18n.t('activerecord.errors.messages.invalid')
273 279 end
274 280 if is_required? && value.detect(&:present?).nil?
275 281 errs << ::I18n.t('activerecord.errors.messages.blank')
276 282 end
277 283 value.each {|v| errs += validate_field_value_format(v)}
278 284 else
279 285 if is_required? && value.blank?
280 286 errs << ::I18n.t('activerecord.errors.messages.blank')
281 287 end
282 288 errs += validate_field_value_format(value)
283 289 end
284 290 errs
285 291 end
286 292
287 293 # Returns true if value is a valid value for the custom field
288 294 def valid_field_value?(value)
289 295 validate_field_value(value).empty?
290 296 end
291 297
292 298 def format_in?(*args)
293 299 args.include?(field_format)
294 300 end
295 301
296 302 protected
297 303
298 304 # Returns the error message for the given value regarding its format
299 305 def validate_field_value_format(value)
300 306 errs = []
301 307 if value.present?
302 308 errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
303 309 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
304 310 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
305 311
306 312 # Format specific validations
307 313 case field_format
308 314 when 'int'
309 315 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
310 316 when 'float'
311 317 begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
312 318 when 'date'
313 319 errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
314 320 when 'list'
315 321 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
316 322 end
317 323 end
318 324 errs
319 325 end
320 326 end
@@ -1,41 +1,42
1 1 Return-Path: <jsmith@somenet.foo>
2 2 Received: from osiris ([127.0.0.1])
3 3 by OSIRIS
4 4 with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
5 5 Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
6 6 From: "John Smith" <jsmith@somenet.foo>
7 7 To: <redmine@somenet.foo>
8 8 Subject: New ticket with custom field values
9 9 Date: Sun, 22 Jun 2008 12:28:07 +0200
10 10 MIME-Version: 1.0
11 11 Content-Type: text/plain;
12 12 format=flowed;
13 13 charset="iso-8859-1";
14 14 reply-type=original
15 15 Content-Transfer-Encoding: 7bit
16 16 X-Priority: 3
17 17 X-MSMail-Priority: Normal
18 18 X-Mailer: Microsoft Outlook Express 6.00.2900.2869
19 19 X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
20 20
21 21 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
22 22 turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
23 23 blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
24 24 sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
25 25 in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
26 26 sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
27 27 id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
28 28 eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
29 29 sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
30 30 malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
31 31 platea dictumst.
32 32
33 33 Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
34 34 sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
35 35 Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
36 36 dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
37 37 massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
38 38 pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
39 39
40 40 category: Stock management
41 41 searchable field: Value for a custom field
42 Database: postgresql
@@ -1,221 +1,226
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class CustomFieldTest < ActiveSupport::TestCase
21 21 fixtures :custom_fields
22 22
23 23 def test_create
24 24 field = UserCustomField.new(:name => 'Money money money', :field_format => 'float')
25 25 assert field.save
26 26 end
27 27
28 28 def test_before_validation
29 29 field = CustomField.new(:name => 'test_before_validation', :field_format => 'int')
30 30 field.searchable = true
31 31 assert field.save
32 32 assert_equal false, field.searchable
33 33 field.searchable = true
34 34 assert field.save
35 35 assert_equal false, field.searchable
36 36 end
37 37
38 38 def test_regexp_validation
39 39 field = IssueCustomField.new(:name => 'regexp', :field_format => 'text', :regexp => '[a-z0-9')
40 40 assert !field.save
41 41 assert_include I18n.t('activerecord.errors.messages.invalid'),
42 42 field.errors[:regexp]
43 43 field.regexp = '[a-z0-9]'
44 44 assert field.save
45 45 end
46 46
47 47 def test_default_value_should_be_validated
48 48 field = CustomField.new(:name => 'Test', :field_format => 'int')
49 49 field.default_value = 'abc'
50 50 assert !field.valid?
51 51 field.default_value = '6'
52 52 assert field.valid?
53 53 end
54 54
55 55 def test_default_value_should_not_be_validated_when_blank
56 56 field = CustomField.new(:name => 'Test', :field_format => 'list', :possible_values => ['a', 'b'], :is_required => true, :default_value => '')
57 57 assert field.valid?
58 58 end
59 59
60 60 def test_should_not_change_field_format_of_existing_custom_field
61 61 field = CustomField.find(1)
62 62 field.field_format = 'int'
63 63 assert_equal 'list', field.field_format
64 64 end
65 65
66 66 def test_possible_values_should_accept_an_array
67 67 field = CustomField.new
68 68 field.possible_values = ["One value", ""]
69 69 assert_equal ["One value"], field.possible_values
70 70 end
71 71
72 72 def test_possible_values_should_accept_a_string
73 73 field = CustomField.new
74 74 field.possible_values = "One value"
75 75 assert_equal ["One value"], field.possible_values
76 76 end
77 77
78 78 def test_possible_values_should_accept_a_multiline_string
79 79 field = CustomField.new
80 80 field.possible_values = "One value\nAnd another one \r\n \n"
81 81 assert_equal ["One value", "And another one"], field.possible_values
82 82 end
83 83
84 84 if "string".respond_to?(:encoding)
85 85 def test_possible_values_stored_as_binary_should_be_utf8_encoded
86 86 field = CustomField.find(11)
87 87 assert_kind_of Array, field.possible_values
88 88 assert field.possible_values.size > 0
89 89 field.possible_values.each do |value|
90 90 assert_equal "UTF-8", value.encoding.name
91 91 end
92 92 end
93 93 end
94 94
95 95 def test_destroy
96 96 field = CustomField.find(1)
97 97 assert field.destroy
98 98 end
99 99
100 100 def test_new_subclass_instance_should_return_an_instance
101 101 f = CustomField.new_subclass_instance('IssueCustomField')
102 102 assert_kind_of IssueCustomField, f
103 103 end
104 104
105 105 def test_new_subclass_instance_should_set_attributes
106 106 f = CustomField.new_subclass_instance('IssueCustomField', :name => 'Test')
107 107 assert_kind_of IssueCustomField, f
108 108 assert_equal 'Test', f.name
109 109 end
110 110
111 111 def test_new_subclass_instance_with_invalid_class_name_should_return_nil
112 112 assert_nil CustomField.new_subclass_instance('WrongClassName')
113 113 end
114 114
115 115 def test_new_subclass_instance_with_non_subclass_name_should_return_nil
116 116 assert_nil CustomField.new_subclass_instance('Project')
117 117 end
118 118
119 119 def test_string_field_validation_with_blank_value
120 120 f = CustomField.new(:field_format => 'string')
121 121
122 122 assert f.valid_field_value?(nil)
123 123 assert f.valid_field_value?('')
124 124
125 125 f.is_required = true
126 126 assert !f.valid_field_value?(nil)
127 127 assert !f.valid_field_value?('')
128 128 end
129 129
130 130 def test_string_field_validation_with_min_and_max_lengths
131 131 f = CustomField.new(:field_format => 'string', :min_length => 2, :max_length => 5)
132 132
133 133 assert f.valid_field_value?(nil)
134 134 assert f.valid_field_value?('')
135 135 assert f.valid_field_value?('a' * 2)
136 136 assert !f.valid_field_value?('a')
137 137 assert !f.valid_field_value?('a' * 6)
138 138 end
139 139
140 140 def test_string_field_validation_with_regexp
141 141 f = CustomField.new(:field_format => 'string', :regexp => '^[A-Z0-9]*$')
142 142
143 143 assert f.valid_field_value?(nil)
144 144 assert f.valid_field_value?('')
145 145 assert f.valid_field_value?('ABC')
146 146 assert !f.valid_field_value?('abc')
147 147 end
148 148
149 149 def test_date_field_validation
150 150 f = CustomField.new(:field_format => 'date')
151 151
152 152 assert f.valid_field_value?(nil)
153 153 assert f.valid_field_value?('')
154 154 assert f.valid_field_value?('1975-07-14')
155 155 assert !f.valid_field_value?('1975-07-33')
156 156 assert !f.valid_field_value?('abc')
157 157 end
158 158
159 159 def test_list_field_validation
160 160 f = CustomField.new(:field_format => 'list', :possible_values => ['value1', 'value2'])
161 161
162 162 assert f.valid_field_value?(nil)
163 163 assert f.valid_field_value?('')
164 164 assert f.valid_field_value?('value2')
165 165 assert !f.valid_field_value?('abc')
166 166 end
167 167
168 168 def test_int_field_validation
169 169 f = CustomField.new(:field_format => 'int')
170 170
171 171 assert f.valid_field_value?(nil)
172 172 assert f.valid_field_value?('')
173 173 assert f.valid_field_value?('123')
174 174 assert f.valid_field_value?('+123')
175 175 assert f.valid_field_value?('-123')
176 176 assert !f.valid_field_value?('6abc')
177 177 end
178 178
179 179 def test_float_field_validation
180 180 f = CustomField.new(:field_format => 'float')
181 181
182 182 assert f.valid_field_value?(nil)
183 183 assert f.valid_field_value?('')
184 184 assert f.valid_field_value?('11.2')
185 185 assert f.valid_field_value?('-6.250')
186 186 assert f.valid_field_value?('5')
187 187 assert !f.valid_field_value?('6abc')
188 188 end
189 189
190 190 def test_multi_field_validation
191 191 f = CustomField.new(:field_format => 'list', :multiple => 'true', :possible_values => ['value1', 'value2'])
192 192
193 193 assert f.valid_field_value?(nil)
194 194 assert f.valid_field_value?('')
195 195 assert f.valid_field_value?([])
196 196 assert f.valid_field_value?([nil])
197 197 assert f.valid_field_value?([''])
198 198
199 199 assert f.valid_field_value?('value2')
200 200 assert !f.valid_field_value?('abc')
201 201
202 202 assert f.valid_field_value?(['value2'])
203 203 assert !f.valid_field_value?(['abc'])
204 204
205 205 assert f.valid_field_value?(['', 'value2'])
206 206 assert !f.valid_field_value?(['', 'abc'])
207 207
208 208 assert f.valid_field_value?(['value1', 'value2'])
209 209 assert !f.valid_field_value?(['value1', 'abc'])
210 210 end
211 211
212 212 def test_value_class_should_return_the_class_used_for_fields_values
213 213 assert_equal User, CustomField.new(:field_format => 'user').value_class
214 214 assert_equal Version, CustomField.new(:field_format => 'version').value_class
215 215 end
216 216
217 217 def test_value_class_should_return_nil_for_other_fields
218 218 assert_nil CustomField.new(:field_format => 'text').value_class
219 219 assert_nil CustomField.new.value_class
220 220 end
221
222 def test_value_from_keyword_for_list_custom_field
223 field = CustomField.find(1)
224 assert_equal 'PostgreSQL', field.value_from_keyword('postgresql', Issue.find(1))
225 end
221 226 end
@@ -1,794 +1,794
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../test_helper', __FILE__)
21 21
22 22 class MailHandlerTest < ActiveSupport::TestCase
23 23 fixtures :users, :projects, :enabled_modules, :roles,
24 24 :members, :member_roles, :users,
25 25 :issues, :issue_statuses,
26 26 :workflows, :trackers, :projects_trackers,
27 27 :versions, :enumerations, :issue_categories,
28 28 :custom_fields, :custom_fields_trackers, :custom_fields_projects,
29 29 :boards, :messages
30 30
31 31 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
32 32
33 33 def setup
34 34 ActionMailer::Base.deliveries.clear
35 35 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
36 36 end
37 37
38 38 def teardown
39 39 Setting.clear_cache
40 40 end
41 41
42 42 def test_add_issue
43 43 ActionMailer::Base.deliveries.clear
44 44 # This email contains: 'Project: onlinestore'
45 45 issue = submit_email('ticket_on_given_project.eml')
46 46 assert issue.is_a?(Issue)
47 47 assert !issue.new_record?
48 48 issue.reload
49 49 assert_equal Project.find(2), issue.project
50 50 assert_equal issue.project.trackers.first, issue.tracker
51 51 assert_equal 'New ticket on a given project', issue.subject
52 52 assert_equal User.find_by_login('jsmith'), issue.author
53 53 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
54 54 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
55 55 assert_equal '2010-01-01', issue.start_date.to_s
56 56 assert_equal '2010-12-31', issue.due_date.to_s
57 57 assert_equal User.find_by_login('jsmith'), issue.assigned_to
58 58 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
59 59 assert_equal 2.5, issue.estimated_hours
60 60 assert_equal 30, issue.done_ratio
61 61 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
62 62 # keywords should be removed from the email body
63 63 assert !issue.description.match(/^Project:/i)
64 64 assert !issue.description.match(/^Status:/i)
65 65 assert !issue.description.match(/^Start Date:/i)
66 66 # Email notification should be sent
67 67 mail = ActionMailer::Base.deliveries.last
68 68 assert_not_nil mail
69 69 assert mail.subject.include?('New ticket on a given project')
70 70 end
71 71
72 72 def test_add_issue_with_default_tracker
73 73 # This email contains: 'Project: onlinestore'
74 74 issue = submit_email(
75 75 'ticket_on_given_project.eml',
76 76 :issue => {:tracker => 'Support request'}
77 77 )
78 78 assert issue.is_a?(Issue)
79 79 assert !issue.new_record?
80 80 issue.reload
81 81 assert_equal 'Support request', issue.tracker.name
82 82 end
83 83
84 84 def test_add_issue_with_status
85 85 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
86 86 issue = submit_email('ticket_on_given_project.eml')
87 87 assert issue.is_a?(Issue)
88 88 assert !issue.new_record?
89 89 issue.reload
90 90 assert_equal Project.find(2), issue.project
91 91 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
92 92 end
93 93
94 94 def test_add_issue_with_attributes_override
95 95 issue = submit_email(
96 96 'ticket_with_attributes.eml',
97 97 :allow_override => 'tracker,category,priority'
98 98 )
99 99 assert issue.is_a?(Issue)
100 100 assert !issue.new_record?
101 101 issue.reload
102 102 assert_equal 'New ticket on a given project', issue.subject
103 103 assert_equal User.find_by_login('jsmith'), issue.author
104 104 assert_equal Project.find(2), issue.project
105 105 assert_equal 'Feature request', issue.tracker.to_s
106 106 assert_equal 'Stock management', issue.category.to_s
107 107 assert_equal 'Urgent', issue.priority.to_s
108 108 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
109 109 end
110 110
111 111 def test_add_issue_with_group_assignment
112 112 with_settings :issue_group_assignment => '1' do
113 113 issue = submit_email('ticket_on_given_project.eml') do |email|
114 114 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
115 115 end
116 116 assert issue.is_a?(Issue)
117 117 assert !issue.new_record?
118 118 issue.reload
119 119 assert_equal Group.find(11), issue.assigned_to
120 120 end
121 121 end
122 122
123 123 def test_add_issue_with_partial_attributes_override
124 124 issue = submit_email(
125 125 'ticket_with_attributes.eml',
126 126 :issue => {:priority => 'High'},
127 127 :allow_override => ['tracker']
128 128 )
129 129 assert issue.is_a?(Issue)
130 130 assert !issue.new_record?
131 131 issue.reload
132 132 assert_equal 'New ticket on a given project', issue.subject
133 133 assert_equal User.find_by_login('jsmith'), issue.author
134 134 assert_equal Project.find(2), issue.project
135 135 assert_equal 'Feature request', issue.tracker.to_s
136 136 assert_nil issue.category
137 137 assert_equal 'High', issue.priority.to_s
138 138 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
139 139 end
140 140
141 141 def test_add_issue_with_spaces_between_attribute_and_separator
142 142 issue = submit_email(
143 143 'ticket_with_spaces_between_attribute_and_separator.eml',
144 144 :allow_override => 'tracker,category,priority'
145 145 )
146 146 assert issue.is_a?(Issue)
147 147 assert !issue.new_record?
148 148 issue.reload
149 149 assert_equal 'New ticket on a given project', issue.subject
150 150 assert_equal User.find_by_login('jsmith'), issue.author
151 151 assert_equal Project.find(2), issue.project
152 152 assert_equal 'Feature request', issue.tracker.to_s
153 153 assert_equal 'Stock management', issue.category.to_s
154 154 assert_equal 'Urgent', issue.priority.to_s
155 155 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
156 156 end
157 157
158 158 def test_add_issue_with_attachment_to_specific_project
159 159 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
160 160 assert issue.is_a?(Issue)
161 161 assert !issue.new_record?
162 162 issue.reload
163 163 assert_equal 'Ticket created by email with attachment', issue.subject
164 164 assert_equal User.find_by_login('jsmith'), issue.author
165 165 assert_equal Project.find(2), issue.project
166 166 assert_equal 'This is a new ticket with attachments', issue.description
167 167 # Attachment properties
168 168 assert_equal 1, issue.attachments.size
169 169 assert_equal 'Paella.jpg', issue.attachments.first.filename
170 170 assert_equal 'image/jpeg', issue.attachments.first.content_type
171 171 assert_equal 10790, issue.attachments.first.filesize
172 172 end
173 173
174 174 def test_add_issue_with_custom_fields
175 175 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
176 176 assert issue.is_a?(Issue)
177 177 assert !issue.new_record?
178 178 issue.reload
179 179 assert_equal 'New ticket with custom field values', issue.subject
180 assert_equal 'Value for a custom field',
181 issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
180 assert_equal 'PostgreSQL', issue.custom_field_value(1)
181 assert_equal 'Value for a custom field', issue.custom_field_value(2)
182 182 assert !issue.description.match(/^searchable field:/i)
183 183 end
184 184
185 185 def test_add_issue_with_version_custom_fields
186 186 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3])
187 187
188 188 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email|
189 189 email << "Affected version: 1.0\n"
190 190 end
191 191 assert issue.is_a?(Issue)
192 192 assert !issue.new_record?
193 193 issue.reload
194 194 assert_equal '2', issue.custom_field_value(field)
195 195 end
196 196
197 197 def test_add_issue_should_match_assignee_on_display_name
198 198 user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz')
199 199 User.add_to_project(user, Project.find(2))
200 200 issue = submit_email('ticket_on_given_project.eml') do |email|
201 201 email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz')
202 202 end
203 203 assert issue.is_a?(Issue)
204 204 assert_equal user, issue.assigned_to
205 205 end
206 206
207 207 def test_add_issue_with_cc
208 208 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
209 209 assert issue.is_a?(Issue)
210 210 assert !issue.new_record?
211 211 issue.reload
212 212 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
213 213 assert_equal 1, issue.watcher_user_ids.size
214 214 end
215 215
216 216 def test_add_issue_by_unknown_user
217 217 assert_no_difference 'User.count' do
218 218 assert_equal false,
219 219 submit_email(
220 220 'ticket_by_unknown_user.eml',
221 221 :issue => {:project => 'ecookbook'}
222 222 )
223 223 end
224 224 end
225 225
226 226 def test_add_issue_by_anonymous_user
227 227 Role.anonymous.add_permission!(:add_issues)
228 228 assert_no_difference 'User.count' do
229 229 issue = submit_email(
230 230 'ticket_by_unknown_user.eml',
231 231 :issue => {:project => 'ecookbook'},
232 232 :unknown_user => 'accept'
233 233 )
234 234 assert issue.is_a?(Issue)
235 235 assert issue.author.anonymous?
236 236 end
237 237 end
238 238
239 239 def test_add_issue_by_anonymous_user_with_no_from_address
240 240 Role.anonymous.add_permission!(:add_issues)
241 241 assert_no_difference 'User.count' do
242 242 issue = submit_email(
243 243 'ticket_by_empty_user.eml',
244 244 :issue => {:project => 'ecookbook'},
245 245 :unknown_user => 'accept'
246 246 )
247 247 assert issue.is_a?(Issue)
248 248 assert issue.author.anonymous?
249 249 end
250 250 end
251 251
252 252 def test_add_issue_by_anonymous_user_on_private_project
253 253 Role.anonymous.add_permission!(:add_issues)
254 254 assert_no_difference 'User.count' do
255 255 assert_no_difference 'Issue.count' do
256 256 assert_equal false,
257 257 submit_email(
258 258 'ticket_by_unknown_user.eml',
259 259 :issue => {:project => 'onlinestore'},
260 260 :unknown_user => 'accept'
261 261 )
262 262 end
263 263 end
264 264 end
265 265
266 266 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
267 267 assert_no_difference 'User.count' do
268 268 assert_difference 'Issue.count' do
269 269 issue = submit_email(
270 270 'ticket_by_unknown_user.eml',
271 271 :issue => {:project => 'onlinestore'},
272 272 :no_permission_check => '1',
273 273 :unknown_user => 'accept'
274 274 )
275 275 assert issue.is_a?(Issue)
276 276 assert issue.author.anonymous?
277 277 assert !issue.project.is_public?
278 278 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
279 279 end
280 280 end
281 281 end
282 282
283 283 def test_add_issue_by_created_user
284 284 Setting.default_language = 'en'
285 285 assert_difference 'User.count' do
286 286 issue = submit_email(
287 287 'ticket_by_unknown_user.eml',
288 288 :issue => {:project => 'ecookbook'},
289 289 :unknown_user => 'create'
290 290 )
291 291 assert issue.is_a?(Issue)
292 292 assert issue.author.active?
293 293 assert_equal 'john.doe@somenet.foo', issue.author.mail
294 294 assert_equal 'John', issue.author.firstname
295 295 assert_equal 'Doe', issue.author.lastname
296 296
297 297 # account information
298 298 email = ActionMailer::Base.deliveries.first
299 299 assert_not_nil email
300 300 assert email.subject.include?('account activation')
301 301 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
302 302 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
303 303 assert_equal issue.author, User.try_to_login(login, password)
304 304 end
305 305 end
306 306
307 307 def test_add_issue_without_from_header
308 308 Role.anonymous.add_permission!(:add_issues)
309 309 assert_equal false, submit_email('ticket_without_from_header.eml')
310 310 end
311 311
312 312 def test_add_issue_with_invalid_attributes
313 313 issue = submit_email(
314 314 'ticket_with_invalid_attributes.eml',
315 315 :allow_override => 'tracker,category,priority'
316 316 )
317 317 assert issue.is_a?(Issue)
318 318 assert !issue.new_record?
319 319 issue.reload
320 320 assert_nil issue.assigned_to
321 321 assert_nil issue.start_date
322 322 assert_nil issue.due_date
323 323 assert_equal 0, issue.done_ratio
324 324 assert_equal 'Normal', issue.priority.to_s
325 325 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
326 326 end
327 327
328 328 def test_add_issue_with_localized_attributes
329 329 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
330 330 issue = submit_email(
331 331 'ticket_with_localized_attributes.eml',
332 332 :allow_override => 'tracker,category,priority'
333 333 )
334 334 assert issue.is_a?(Issue)
335 335 assert !issue.new_record?
336 336 issue.reload
337 337 assert_equal 'New ticket on a given project', issue.subject
338 338 assert_equal User.find_by_login('jsmith'), issue.author
339 339 assert_equal Project.find(2), issue.project
340 340 assert_equal 'Feature request', issue.tracker.to_s
341 341 assert_equal 'Stock management', issue.category.to_s
342 342 assert_equal 'Urgent', issue.priority.to_s
343 343 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
344 344 end
345 345
346 346 def test_add_issue_with_japanese_keywords
347 347 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
348 348 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
349 349 tracker = Tracker.create!(:name => ja_dev)
350 350 Project.find(1).trackers << tracker
351 351 issue = submit_email(
352 352 'japanese_keywords_iso_2022_jp.eml',
353 353 :issue => {:project => 'ecookbook'},
354 354 :allow_override => 'tracker'
355 355 )
356 356 assert_kind_of Issue, issue
357 357 assert_equal tracker, issue.tracker
358 358 end
359 359
360 360 def test_add_issue_from_apple_mail
361 361 issue = submit_email(
362 362 'apple_mail_with_attachment.eml',
363 363 :issue => {:project => 'ecookbook'}
364 364 )
365 365 assert_kind_of Issue, issue
366 366 assert_equal 1, issue.attachments.size
367 367
368 368 attachment = issue.attachments.first
369 369 assert_equal 'paella.jpg', attachment.filename
370 370 assert_equal 10790, attachment.filesize
371 371 assert File.exist?(attachment.diskfile)
372 372 assert_equal 10790, File.size(attachment.diskfile)
373 373 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
374 374 end
375 375
376 376 def test_thunderbird_with_attachment_ja
377 377 issue = submit_email(
378 378 'thunderbird_with_attachment_ja.eml',
379 379 :issue => {:project => 'ecookbook'}
380 380 )
381 381 assert_kind_of Issue, issue
382 382 assert_equal 1, issue.attachments.size
383 383 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
384 384 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
385 385 attachment = issue.attachments.first
386 386 assert_equal ja, attachment.filename
387 387 assert_equal 5, attachment.filesize
388 388 assert File.exist?(attachment.diskfile)
389 389 assert_equal 5, File.size(attachment.diskfile)
390 390 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
391 391 end
392 392
393 393 def test_gmail_with_attachment_ja
394 394 issue = submit_email(
395 395 'gmail_with_attachment_ja.eml',
396 396 :issue => {:project => 'ecookbook'}
397 397 )
398 398 assert_kind_of Issue, issue
399 399 assert_equal 1, issue.attachments.size
400 400 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
401 401 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
402 402 attachment = issue.attachments.first
403 403 assert_equal ja, attachment.filename
404 404 assert_equal 5, attachment.filesize
405 405 assert File.exist?(attachment.diskfile)
406 406 assert_equal 5, File.size(attachment.diskfile)
407 407 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
408 408 end
409 409
410 410 def test_thunderbird_with_attachment_latin1
411 411 issue = submit_email(
412 412 'thunderbird_with_attachment_iso-8859-1.eml',
413 413 :issue => {:project => 'ecookbook'}
414 414 )
415 415 assert_kind_of Issue, issue
416 416 assert_equal 1, issue.attachments.size
417 417 u = ""
418 418 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
419 419 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
420 420 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
421 421 11.times { u << u1 }
422 422 attachment = issue.attachments.first
423 423 assert_equal "#{u}.png", attachment.filename
424 424 assert_equal 130, attachment.filesize
425 425 assert File.exist?(attachment.diskfile)
426 426 assert_equal 130, File.size(attachment.diskfile)
427 427 assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest
428 428 end
429 429
430 430 def test_gmail_with_attachment_latin1
431 431 issue = submit_email(
432 432 'gmail_with_attachment_iso-8859-1.eml',
433 433 :issue => {:project => 'ecookbook'}
434 434 )
435 435 assert_kind_of Issue, issue
436 436 assert_equal 1, issue.attachments.size
437 437 u = ""
438 438 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
439 439 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
440 440 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
441 441 11.times { u << u1 }
442 442 attachment = issue.attachments.first
443 443 assert_equal "#{u}.txt", attachment.filename
444 444 assert_equal 5, attachment.filesize
445 445 assert File.exist?(attachment.diskfile)
446 446 assert_equal 5, File.size(attachment.diskfile)
447 447 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
448 448 end
449 449
450 450 def test_add_issue_with_iso_8859_1_subject
451 451 issue = submit_email(
452 452 'subject_as_iso-8859-1.eml',
453 453 :issue => {:project => 'ecookbook'}
454 454 )
455 455 str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc..."
456 456 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
457 457 assert_kind_of Issue, issue
458 458 assert_equal str, issue.subject
459 459 end
460 460
461 461 def test_add_issue_with_japanese_subject
462 462 issue = submit_email(
463 463 'subject_japanese_1.eml',
464 464 :issue => {:project => 'ecookbook'}
465 465 )
466 466 assert_kind_of Issue, issue
467 467 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
468 468 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
469 469 assert_equal ja, issue.subject
470 470 end
471 471
472 472 def test_add_issue_with_no_subject_header
473 473 issue = submit_email(
474 474 'no_subject_header.eml',
475 475 :issue => {:project => 'ecookbook'}
476 476 )
477 477 assert_kind_of Issue, issue
478 478 assert_equal '(no subject)', issue.subject
479 479 end
480 480
481 481 def test_add_issue_with_mixed_japanese_subject
482 482 issue = submit_email(
483 483 'subject_japanese_2.eml',
484 484 :issue => {:project => 'ecookbook'}
485 485 )
486 486 assert_kind_of Issue, issue
487 487 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
488 488 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
489 489 assert_equal ja, issue.subject
490 490 end
491 491
492 492 def test_should_ignore_emails_from_locked_users
493 493 User.find(2).lock!
494 494
495 495 MailHandler.any_instance.expects(:dispatch).never
496 496 assert_no_difference 'Issue.count' do
497 497 assert_equal false, submit_email('ticket_on_given_project.eml')
498 498 end
499 499 end
500 500
501 501 def test_should_ignore_emails_from_emission_address
502 502 Role.anonymous.add_permission!(:add_issues)
503 503 assert_no_difference 'User.count' do
504 504 assert_equal false,
505 505 submit_email(
506 506 'ticket_from_emission_address.eml',
507 507 :issue => {:project => 'ecookbook'},
508 508 :unknown_user => 'create'
509 509 )
510 510 end
511 511 end
512 512
513 513 def test_should_ignore_auto_replied_emails
514 514 MailHandler.any_instance.expects(:dispatch).never
515 515 [
516 516 "X-Auto-Response-Suppress: OOF",
517 517 "Auto-Submitted: auto-replied",
518 518 "Auto-Submitted: Auto-Replied",
519 519 "Auto-Submitted: auto-generated"
520 520 ].each do |header|
521 521 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
522 522 raw = header + "\n" + raw
523 523
524 524 assert_no_difference 'Issue.count' do
525 525 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
526 526 end
527 527 end
528 528 end
529 529
530 530 def test_add_issue_should_send_email_notification
531 531 Setting.notified_events = ['issue_added']
532 532 ActionMailer::Base.deliveries.clear
533 533 # This email contains: 'Project: onlinestore'
534 534 issue = submit_email('ticket_on_given_project.eml')
535 535 assert issue.is_a?(Issue)
536 536 assert_equal 1, ActionMailer::Base.deliveries.size
537 537 end
538 538
539 539 def test_update_issue
540 540 journal = submit_email('ticket_reply.eml')
541 541 assert journal.is_a?(Journal)
542 542 assert_equal User.find_by_login('jsmith'), journal.user
543 543 assert_equal Issue.find(2), journal.journalized
544 544 assert_match /This is reply/, journal.notes
545 545 assert_equal false, journal.private_notes
546 546 assert_equal 'Feature request', journal.issue.tracker.name
547 547 end
548 548
549 549 def test_update_issue_with_attribute_changes
550 550 # This email contains: 'Status: Resolved'
551 551 journal = submit_email('ticket_reply_with_status.eml')
552 552 assert journal.is_a?(Journal)
553 553 issue = Issue.find(journal.issue.id)
554 554 assert_equal User.find_by_login('jsmith'), journal.user
555 555 assert_equal Issue.find(2), journal.journalized
556 556 assert_match /This is reply/, journal.notes
557 557 assert_equal 'Feature request', journal.issue.tracker.name
558 558 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
559 559 assert_equal '2010-01-01', issue.start_date.to_s
560 560 assert_equal '2010-12-31', issue.due_date.to_s
561 561 assert_equal User.find_by_login('jsmith'), issue.assigned_to
562 562 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
563 563 # keywords should be removed from the email body
564 564 assert !journal.notes.match(/^Status:/i)
565 565 assert !journal.notes.match(/^Start Date:/i)
566 566 end
567 567
568 568 def test_update_issue_with_attachment
569 569 assert_difference 'Journal.count' do
570 570 assert_difference 'JournalDetail.count' do
571 571 assert_difference 'Attachment.count' do
572 572 assert_no_difference 'Issue.count' do
573 573 journal = submit_email('ticket_with_attachment.eml') do |raw|
574 574 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
575 575 end
576 576 end
577 577 end
578 578 end
579 579 end
580 580 journal = Journal.first(:order => 'id DESC')
581 581 assert_equal Issue.find(2), journal.journalized
582 582 assert_equal 1, journal.details.size
583 583
584 584 detail = journal.details.first
585 585 assert_equal 'attachment', detail.property
586 586 assert_equal 'Paella.jpg', detail.value
587 587 end
588 588
589 589 def test_update_issue_should_send_email_notification
590 590 ActionMailer::Base.deliveries.clear
591 591 journal = submit_email('ticket_reply.eml')
592 592 assert journal.is_a?(Journal)
593 593 assert_equal 1, ActionMailer::Base.deliveries.size
594 594 end
595 595
596 596 def test_update_issue_should_not_set_defaults
597 597 journal = submit_email(
598 598 'ticket_reply.eml',
599 599 :issue => {:tracker => 'Support request', :priority => 'High'}
600 600 )
601 601 assert journal.is_a?(Journal)
602 602 assert_match /This is reply/, journal.notes
603 603 assert_equal 'Feature request', journal.issue.tracker.name
604 604 assert_equal 'Normal', journal.issue.priority.name
605 605 end
606 606
607 607 def test_replying_to_a_private_note_should_add_reply_as_private
608 608 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
609 609
610 610 assert_difference 'Journal.count' do
611 611 journal = submit_email('ticket_reply.eml') do |email|
612 612 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
613 613 end
614 614
615 615 assert_kind_of Journal, journal
616 616 assert_match /This is reply/, journal.notes
617 617 assert_equal true, journal.private_notes
618 618 end
619 619 end
620 620
621 621 def test_reply_to_a_message
622 622 m = submit_email('message_reply.eml')
623 623 assert m.is_a?(Message)
624 624 assert !m.new_record?
625 625 m.reload
626 626 assert_equal 'Reply via email', m.subject
627 627 # The email replies to message #2 which is part of the thread of message #1
628 628 assert_equal Message.find(1), m.parent
629 629 end
630 630
631 631 def test_reply_to_a_message_by_subject
632 632 m = submit_email('message_reply_by_subject.eml')
633 633 assert m.is_a?(Message)
634 634 assert !m.new_record?
635 635 m.reload
636 636 assert_equal 'Reply to the first post', m.subject
637 637 assert_equal Message.find(1), m.parent
638 638 end
639 639
640 640 def test_should_strip_tags_of_html_only_emails
641 641 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
642 642 assert issue.is_a?(Issue)
643 643 assert !issue.new_record?
644 644 issue.reload
645 645 assert_equal 'HTML email', issue.subject
646 646 assert_equal 'This is a html-only email.', issue.description
647 647 end
648 648
649 649 context "truncate emails based on the Setting" do
650 650 context "with no setting" do
651 651 setup do
652 652 Setting.mail_handler_body_delimiters = ''
653 653 end
654 654
655 655 should "add the entire email into the issue" do
656 656 issue = submit_email('ticket_on_given_project.eml')
657 657 assert_issue_created(issue)
658 658 assert issue.description.include?('---')
659 659 assert issue.description.include?('This paragraph is after the delimiter')
660 660 end
661 661 end
662 662
663 663 context "with a single string" do
664 664 setup do
665 665 Setting.mail_handler_body_delimiters = '---'
666 666 end
667 667 should "truncate the email at the delimiter for the issue" do
668 668 issue = submit_email('ticket_on_given_project.eml')
669 669 assert_issue_created(issue)
670 670 assert issue.description.include?('This paragraph is before delimiters')
671 671 assert issue.description.include?('--- This line starts with a delimiter')
672 672 assert !issue.description.match(/^---$/)
673 673 assert !issue.description.include?('This paragraph is after the delimiter')
674 674 end
675 675 end
676 676
677 677 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
678 678 setup do
679 679 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
680 680 end
681 681 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
682 682 journal = submit_email('issue_update_with_quoted_reply_above.eml')
683 683 assert journal.is_a?(Journal)
684 684 assert journal.notes.include?('An update to the issue by the sender.')
685 685 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
686 686 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
687 687 end
688 688 end
689 689
690 690 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
691 691 setup do
692 692 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
693 693 end
694 694 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
695 695 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
696 696 assert journal.is_a?(Journal)
697 697 assert journal.notes.include?('An update to the issue by the sender.')
698 698 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
699 699 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
700 700 end
701 701 end
702 702
703 703 context "with multiple strings" do
704 704 setup do
705 705 Setting.mail_handler_body_delimiters = "---\nBREAK"
706 706 end
707 707 should "truncate the email at the first delimiter found (BREAK)" do
708 708 issue = submit_email('ticket_on_given_project.eml')
709 709 assert_issue_created(issue)
710 710 assert issue.description.include?('This paragraph is before delimiters')
711 711 assert !issue.description.include?('BREAK')
712 712 assert !issue.description.include?('This paragraph is between delimiters')
713 713 assert !issue.description.match(/^---$/)
714 714 assert !issue.description.include?('This paragraph is after the delimiter')
715 715 end
716 716 end
717 717 end
718 718
719 719 def test_email_with_long_subject_line
720 720 issue = submit_email('ticket_with_long_subject.eml')
721 721 assert issue.is_a?(Issue)
722 722 assert_equal issue.subject, 'New ticket on a given project with a very long subject line which exceeds 255 chars and should not be ignored but chopped off. And if the subject line is still not long enough, we just add more text. And more text. Wow, this is really annoying. Especially, if you have nothing to say...'[0,255]
723 723 end
724 724
725 725 def test_new_user_from_attributes_should_return_valid_user
726 726 to_test = {
727 727 # [address, name] => [login, firstname, lastname]
728 728 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
729 729 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
730 730 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
731 731 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
732 732 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
733 733 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
734 734 }
735 735
736 736 to_test.each do |attrs, expected|
737 737 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
738 738
739 739 assert user.valid?, user.errors.full_messages.to_s
740 740 assert_equal attrs.first, user.mail
741 741 assert_equal expected[0], user.login
742 742 assert_equal expected[1], user.firstname
743 743 assert_equal expected[2], user.lastname
744 744 end
745 745 end
746 746
747 747 def test_new_user_from_attributes_should_respect_minimum_password_length
748 748 with_settings :password_min_length => 15 do
749 749 user = MailHandler.new_user_from_attributes('jsmith@example.net')
750 750 assert user.valid?
751 751 assert user.password.length >= 15
752 752 end
753 753 end
754 754
755 755 def test_new_user_from_attributes_should_use_default_login_if_invalid
756 756 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
757 757 assert user.valid?
758 758 assert user.login =~ /^user[a-f0-9]+$/
759 759 assert_equal 'foo+bar@example.net', user.mail
760 760 end
761 761
762 762 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
763 763 assert_difference 'User.count' do
764 764 issue = submit_email(
765 765 'fullname_of_sender_as_utf8_encoded.eml',
766 766 :issue => {:project => 'ecookbook'},
767 767 :unknown_user => 'create'
768 768 )
769 769 end
770 770
771 771 user = User.first(:order => 'id DESC')
772 772 assert_equal "foo@example.org", user.mail
773 773 str1 = "\xc3\x84\xc3\xa4"
774 774 str2 = "\xc3\x96\xc3\xb6"
775 775 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
776 776 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
777 777 assert_equal str1, user.firstname
778 778 assert_equal str2, user.lastname
779 779 end
780 780
781 781 private
782 782
783 783 def submit_email(filename, options={})
784 784 raw = IO.read(File.join(FIXTURES_PATH, filename))
785 785 yield raw if block_given?
786 786 MailHandler.receive(raw, options)
787 787 end
788 788
789 789 def assert_issue_created(issue)
790 790 assert issue.is_a?(Issue)
791 791 assert !issue.new_record?
792 792 issue.reload
793 793 end
794 794 end
General Comments 0
You need to be logged in to leave comments. Login now