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