@@ -1,468 +1,480 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2013 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | #require 'shoulda' |
|
19 | 19 | ENV["RAILS_ENV"] = "test" |
|
20 | 20 | require File.expand_path(File.dirname(__FILE__) + "/../config/environment") |
|
21 | 21 | require 'rails/test_help' |
|
22 | 22 | require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s |
|
23 | 23 | |
|
24 | 24 | require File.expand_path(File.dirname(__FILE__) + '/object_helpers') |
|
25 | 25 | include ObjectHelpers |
|
26 | 26 | |
|
27 | require 'awesome_nested_set/version' | |
|
28 | ||
|
27 | 29 | class ActiveSupport::TestCase |
|
28 | 30 | include ActionDispatch::TestProcess |
|
29 | 31 | |
|
30 | 32 | self.use_transactional_fixtures = true |
|
31 | 33 | self.use_instantiated_fixtures = false |
|
32 | 34 | |
|
33 | 35 | def log_user(login, password) |
|
34 | 36 | User.anonymous |
|
35 | 37 | get "/login" |
|
36 | 38 | assert_equal nil, session[:user_id] |
|
37 | 39 | assert_response :success |
|
38 | 40 | assert_template "account/login" |
|
39 | 41 | post "/login", :username => login, :password => password |
|
40 | 42 | assert_equal login, User.find(session[:user_id]).login |
|
41 | 43 | end |
|
42 | 44 | |
|
43 | 45 | def uploaded_test_file(name, mime) |
|
44 | 46 | fixture_file_upload("files/#{name}", mime, true) |
|
45 | 47 | end |
|
46 | 48 | |
|
47 | 49 | def credentials(user, password=nil) |
|
48 | 50 | {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)} |
|
49 | 51 | end |
|
50 | 52 | |
|
51 | 53 | # Mock out a file |
|
52 | 54 | def self.mock_file |
|
53 | 55 | file = 'a_file.png' |
|
54 | 56 | file.stubs(:size).returns(32) |
|
55 | 57 | file.stubs(:original_filename).returns('a_file.png') |
|
56 | 58 | file.stubs(:content_type).returns('image/png') |
|
57 | 59 | file.stubs(:read).returns(false) |
|
58 | 60 | file |
|
59 | 61 | end |
|
60 | 62 | |
|
61 | 63 | def mock_file |
|
62 | 64 | self.class.mock_file |
|
63 | 65 | end |
|
64 | 66 | |
|
65 | 67 | def mock_file_with_options(options={}) |
|
66 | 68 | file = '' |
|
67 | 69 | file.stubs(:size).returns(32) |
|
68 | 70 | original_filename = options[:original_filename] || nil |
|
69 | 71 | file.stubs(:original_filename).returns(original_filename) |
|
70 | 72 | content_type = options[:content_type] || nil |
|
71 | 73 | file.stubs(:content_type).returns(content_type) |
|
72 | 74 | file.stubs(:read).returns(false) |
|
73 | 75 | file |
|
74 | 76 | end |
|
75 | 77 | |
|
76 | 78 | # Use a temporary directory for attachment related tests |
|
77 | 79 | def set_tmp_attachments_directory |
|
78 | 80 | Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test") |
|
79 | 81 | unless File.directory?("#{Rails.root}/tmp/test/attachments") |
|
80 | 82 | Dir.mkdir "#{Rails.root}/tmp/test/attachments" |
|
81 | 83 | end |
|
82 | 84 | Attachment.storage_path = "#{Rails.root}/tmp/test/attachments" |
|
83 | 85 | end |
|
84 | 86 | |
|
85 | 87 | def set_fixtures_attachments_directory |
|
86 | 88 | Attachment.storage_path = "#{Rails.root}/test/fixtures/files" |
|
87 | 89 | end |
|
88 | 90 | |
|
89 | 91 | def with_settings(options, &block) |
|
90 | 92 | saved_settings = options.keys.inject({}) do |h, k| |
|
91 | 93 | h[k] = case Setting[k] |
|
92 | 94 | when Symbol, false, true, nil |
|
93 | 95 | Setting[k] |
|
94 | 96 | else |
|
95 | 97 | Setting[k].dup |
|
96 | 98 | end |
|
97 | 99 | h |
|
98 | 100 | end |
|
99 | 101 | options.each {|k, v| Setting[k] = v} |
|
100 | 102 | yield |
|
101 | 103 | ensure |
|
102 | 104 | saved_settings.each {|k, v| Setting[k] = v} if saved_settings |
|
103 | 105 | end |
|
104 | 106 | |
|
105 | 107 | # Yields the block with user as the current user |
|
106 | 108 | def with_current_user(user, &block) |
|
107 | 109 | saved_user = User.current |
|
108 | 110 | User.current = user |
|
109 | 111 | yield |
|
110 | 112 | ensure |
|
111 | 113 | User.current = saved_user |
|
112 | 114 | end |
|
113 | 115 | |
|
114 | 116 | def change_user_password(login, new_password) |
|
115 | 117 | user = User.where(:login => login).first |
|
116 | 118 | user.password, user.password_confirmation = new_password, new_password |
|
117 | 119 | user.save! |
|
118 | 120 | end |
|
119 | 121 | |
|
120 | 122 | def self.ldap_configured? |
|
121 | 123 | @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389) |
|
122 | 124 | return @test_ldap.bind |
|
123 | 125 | rescue Exception => e |
|
124 | 126 | # LDAP is not listening |
|
125 | 127 | return nil |
|
126 | 128 | end |
|
127 | 129 | |
|
128 | 130 | def self.convert_installed? |
|
129 | 131 | Redmine::Thumbnail.convert_available? |
|
130 | 132 | end |
|
131 | 133 | |
|
132 | 134 | # Returns the path to the test +vendor+ repository |
|
133 | 135 | def self.repository_path(vendor) |
|
134 | 136 | Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s |
|
135 | 137 | end |
|
136 | 138 | |
|
137 | 139 | # Returns the url of the subversion test repository |
|
138 | 140 | def self.subversion_repository_url |
|
139 | 141 | path = repository_path('subversion') |
|
140 | 142 | path = '/' + path unless path.starts_with?('/') |
|
141 | 143 | "file://#{path}" |
|
142 | 144 | end |
|
143 | 145 | |
|
144 | 146 | # Returns true if the +vendor+ test repository is configured |
|
145 | 147 | def self.repository_configured?(vendor) |
|
146 | 148 | File.directory?(repository_path(vendor)) |
|
147 | 149 | end |
|
148 | 150 | |
|
149 | 151 | def repository_path_hash(arr) |
|
150 | 152 | hs = {} |
|
151 | 153 | hs[:path] = arr.join("/") |
|
152 | 154 | hs[:param] = arr.join("/") |
|
153 | 155 | hs |
|
154 | 156 | end |
|
155 | 157 | |
|
156 | 158 | def assert_save(object) |
|
157 | 159 | saved = object.save |
|
158 | 160 | message = "#{object.class} could not be saved" |
|
159 | 161 | errors = object.errors.full_messages.map {|m| "- #{m}"} |
|
160 | 162 | message << ":\n#{errors.join("\n")}" if errors.any? |
|
161 | 163 | assert_equal true, saved, message |
|
162 | 164 | end |
|
163 | 165 | |
|
164 | 166 | def assert_error_tag(options={}) |
|
165 | 167 | assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options)) |
|
166 | 168 | end |
|
167 | 169 | |
|
168 | 170 | def assert_include(expected, s, message=nil) |
|
169 | 171 | assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"") |
|
170 | 172 | end |
|
171 | 173 | |
|
172 | 174 | def assert_not_include(expected, s, message=nil) |
|
173 | 175 | assert !s.include?(expected), (message || "\"#{expected}\" found in \"#{s}\"") |
|
174 | 176 | end |
|
175 | 177 | |
|
176 | 178 | def assert_select_in(text, *args, &block) |
|
177 | 179 | d = HTML::Document.new(CGI::unescapeHTML(String.new(text))).root |
|
178 | 180 | assert_select(d, *args, &block) |
|
179 | 181 | end |
|
180 | 182 | |
|
181 | 183 | def assert_mail_body_match(expected, mail, message=nil) |
|
182 | 184 | if expected.is_a?(String) |
|
183 | 185 | assert_include expected, mail_body(mail), message |
|
184 | 186 | else |
|
185 | 187 | assert_match expected, mail_body(mail), message |
|
186 | 188 | end |
|
187 | 189 | end |
|
188 | 190 | |
|
189 | 191 | def assert_mail_body_no_match(expected, mail, message=nil) |
|
190 | 192 | if expected.is_a?(String) |
|
191 | 193 | assert_not_include expected, mail_body(mail), message |
|
192 | 194 | else |
|
193 | 195 | assert_no_match expected, mail_body(mail), message |
|
194 | 196 | end |
|
195 | 197 | end |
|
196 | 198 | |
|
197 | 199 | def mail_body(mail) |
|
198 | 200 | mail.parts.first.body.encoded |
|
199 | 201 | end |
|
202 | ||
|
203 | # awesome_nested_set new node lft and rgt value changed this refactor revision. | |
|
204 | # https://github.com/collectiveidea/awesome_nested_set/commit/199fca9bb938e40200cd90714dc69247ef017c61 | |
|
205 | # The reason of behavior change is "self.class.base_class.unscoped" added this line. | |
|
206 | # https://github.com/collectiveidea/awesome_nested_set/commit/199fca9bb9#diff-f61b59a5e6319024e211b0ffdd0e4ef1R273 | |
|
207 | # It seems correct behavior because of this line comment. | |
|
208 | # https://github.com/collectiveidea/awesome_nested_set/blame/199fca9bb9/lib/awesome_nested_set/model.rb#L278 | |
|
209 | def new_issue_lft | |
|
210 | ::AwesomeNestedSet::VERSION > "2.1.6" ? Issue.maximum(:rgt) + 1 : 1 | |
|
211 | end | |
|
200 | 212 | end |
|
201 | 213 | |
|
202 | 214 | module Redmine |
|
203 | 215 | module ApiTest |
|
204 | 216 | # Base class for API tests |
|
205 | 217 | class Base < ActionDispatch::IntegrationTest |
|
206 | 218 | # Test that a request allows the three types of API authentication |
|
207 | 219 | # |
|
208 | 220 | # * HTTP Basic with username and password |
|
209 | 221 | # * HTTP Basic with an api key for the username |
|
210 | 222 | # * Key based with the key=X parameter |
|
211 | 223 | # |
|
212 | 224 | # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) |
|
213 | 225 | # @param [String] url the request url |
|
214 | 226 | # @param [optional, Hash] parameters additional request parameters |
|
215 | 227 | # @param [optional, Hash] options additional options |
|
216 | 228 | # @option options [Symbol] :success_code Successful response code (:success) |
|
217 | 229 | # @option options [Symbol] :failure_code Failure response code (:unauthorized) |
|
218 | 230 | def self.should_allow_api_authentication(http_method, url, parameters={}, options={}) |
|
219 | 231 | should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options) |
|
220 | 232 | should_allow_http_basic_auth_with_key(http_method, url, parameters, options) |
|
221 | 233 | should_allow_key_based_auth(http_method, url, parameters, options) |
|
222 | 234 | end |
|
223 | 235 | |
|
224 | 236 | # Test that a request allows the username and password for HTTP BASIC |
|
225 | 237 | # |
|
226 | 238 | # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) |
|
227 | 239 | # @param [String] url the request url |
|
228 | 240 | # @param [optional, Hash] parameters additional request parameters |
|
229 | 241 | # @param [optional, Hash] options additional options |
|
230 | 242 | # @option options [Symbol] :success_code Successful response code (:success) |
|
231 | 243 | # @option options [Symbol] :failure_code Failure response code (:unauthorized) |
|
232 | 244 | def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={}) |
|
233 | 245 | success_code = options[:success_code] || :success |
|
234 | 246 | failure_code = options[:failure_code] || :unauthorized |
|
235 | 247 | |
|
236 | 248 | context "should allow http basic auth using a username and password for #{http_method} #{url}" do |
|
237 | 249 | context "with a valid HTTP authentication" do |
|
238 | 250 | setup do |
|
239 | 251 | @user = User.generate! do |user| |
|
240 | 252 | user.admin = true |
|
241 | 253 | user.password = 'my_password' |
|
242 | 254 | end |
|
243 | 255 | send(http_method, url, parameters, credentials(@user.login, 'my_password')) |
|
244 | 256 | end |
|
245 | 257 | |
|
246 | 258 | should_respond_with success_code |
|
247 | 259 | should_respond_with_content_type_based_on_url(url) |
|
248 | 260 | should "login as the user" do |
|
249 | 261 | assert_equal @user, User.current |
|
250 | 262 | end |
|
251 | 263 | end |
|
252 | 264 | |
|
253 | 265 | context "with an invalid HTTP authentication" do |
|
254 | 266 | setup do |
|
255 | 267 | @user = User.generate! |
|
256 | 268 | send(http_method, url, parameters, credentials(@user.login, 'wrong_password')) |
|
257 | 269 | end |
|
258 | 270 | |
|
259 | 271 | should_respond_with failure_code |
|
260 | 272 | should_respond_with_content_type_based_on_url(url) |
|
261 | 273 | should "not login as the user" do |
|
262 | 274 | assert_equal User.anonymous, User.current |
|
263 | 275 | end |
|
264 | 276 | end |
|
265 | 277 | |
|
266 | 278 | context "without credentials" do |
|
267 | 279 | setup do |
|
268 | 280 | send(http_method, url, parameters) |
|
269 | 281 | end |
|
270 | 282 | |
|
271 | 283 | should_respond_with failure_code |
|
272 | 284 | should_respond_with_content_type_based_on_url(url) |
|
273 | 285 | should "include_www_authenticate_header" do |
|
274 | 286 | assert @controller.response.headers.has_key?('WWW-Authenticate') |
|
275 | 287 | end |
|
276 | 288 | end |
|
277 | 289 | end |
|
278 | 290 | end |
|
279 | 291 | |
|
280 | 292 | # Test that a request allows the API key with HTTP BASIC |
|
281 | 293 | # |
|
282 | 294 | # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) |
|
283 | 295 | # @param [String] url the request url |
|
284 | 296 | # @param [optional, Hash] parameters additional request parameters |
|
285 | 297 | # @param [optional, Hash] options additional options |
|
286 | 298 | # @option options [Symbol] :success_code Successful response code (:success) |
|
287 | 299 | # @option options [Symbol] :failure_code Failure response code (:unauthorized) |
|
288 | 300 | def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={}) |
|
289 | 301 | success_code = options[:success_code] || :success |
|
290 | 302 | failure_code = options[:failure_code] || :unauthorized |
|
291 | 303 | |
|
292 | 304 | context "should allow http basic auth with a key for #{http_method} #{url}" do |
|
293 | 305 | context "with a valid HTTP authentication using the API token" do |
|
294 | 306 | setup do |
|
295 | 307 | @user = User.generate! do |user| |
|
296 | 308 | user.admin = true |
|
297 | 309 | end |
|
298 | 310 | @token = Token.create!(:user => @user, :action => 'api') |
|
299 | 311 | send(http_method, url, parameters, credentials(@token.value, 'X')) |
|
300 | 312 | end |
|
301 | 313 | should_respond_with success_code |
|
302 | 314 | should_respond_with_content_type_based_on_url(url) |
|
303 | 315 | should_be_a_valid_response_string_based_on_url(url) |
|
304 | 316 | should "login as the user" do |
|
305 | 317 | assert_equal @user, User.current |
|
306 | 318 | end |
|
307 | 319 | end |
|
308 | 320 | |
|
309 | 321 | context "with an invalid HTTP authentication" do |
|
310 | 322 | setup do |
|
311 | 323 | @user = User.generate! |
|
312 | 324 | @token = Token.create!(:user => @user, :action => 'feeds') |
|
313 | 325 | send(http_method, url, parameters, credentials(@token.value, 'X')) |
|
314 | 326 | end |
|
315 | 327 | should_respond_with failure_code |
|
316 | 328 | should_respond_with_content_type_based_on_url(url) |
|
317 | 329 | should "not login as the user" do |
|
318 | 330 | assert_equal User.anonymous, User.current |
|
319 | 331 | end |
|
320 | 332 | end |
|
321 | 333 | end |
|
322 | 334 | end |
|
323 | 335 | |
|
324 | 336 | # Test that a request allows full key authentication |
|
325 | 337 | # |
|
326 | 338 | # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) |
|
327 | 339 | # @param [String] url the request url, without the key=ZXY parameter |
|
328 | 340 | # @param [optional, Hash] parameters additional request parameters |
|
329 | 341 | # @param [optional, Hash] options additional options |
|
330 | 342 | # @option options [Symbol] :success_code Successful response code (:success) |
|
331 | 343 | # @option options [Symbol] :failure_code Failure response code (:unauthorized) |
|
332 | 344 | def self.should_allow_key_based_auth(http_method, url, parameters={}, options={}) |
|
333 | 345 | success_code = options[:success_code] || :success |
|
334 | 346 | failure_code = options[:failure_code] || :unauthorized |
|
335 | 347 | |
|
336 | 348 | context "should allow key based auth using key=X for #{http_method} #{url}" do |
|
337 | 349 | context "with a valid api token" do |
|
338 | 350 | setup do |
|
339 | 351 | @user = User.generate! do |user| |
|
340 | 352 | user.admin = true |
|
341 | 353 | end |
|
342 | 354 | @token = Token.create!(:user => @user, :action => 'api') |
|
343 | 355 | # Simple url parse to add on ?key= or &key= |
|
344 | 356 | request_url = if url.match(/\?/) |
|
345 | 357 | url + "&key=#{@token.value}" |
|
346 | 358 | else |
|
347 | 359 | url + "?key=#{@token.value}" |
|
348 | 360 | end |
|
349 | 361 | send(http_method, request_url, parameters) |
|
350 | 362 | end |
|
351 | 363 | should_respond_with success_code |
|
352 | 364 | should_respond_with_content_type_based_on_url(url) |
|
353 | 365 | should_be_a_valid_response_string_based_on_url(url) |
|
354 | 366 | should "login as the user" do |
|
355 | 367 | assert_equal @user, User.current |
|
356 | 368 | end |
|
357 | 369 | end |
|
358 | 370 | |
|
359 | 371 | context "with an invalid api token" do |
|
360 | 372 | setup do |
|
361 | 373 | @user = User.generate! do |user| |
|
362 | 374 | user.admin = true |
|
363 | 375 | end |
|
364 | 376 | @token = Token.create!(:user => @user, :action => 'feeds') |
|
365 | 377 | # Simple url parse to add on ?key= or &key= |
|
366 | 378 | request_url = if url.match(/\?/) |
|
367 | 379 | url + "&key=#{@token.value}" |
|
368 | 380 | else |
|
369 | 381 | url + "?key=#{@token.value}" |
|
370 | 382 | end |
|
371 | 383 | send(http_method, request_url, parameters) |
|
372 | 384 | end |
|
373 | 385 | should_respond_with failure_code |
|
374 | 386 | should_respond_with_content_type_based_on_url(url) |
|
375 | 387 | should "not login as the user" do |
|
376 | 388 | assert_equal User.anonymous, User.current |
|
377 | 389 | end |
|
378 | 390 | end |
|
379 | 391 | end |
|
380 | 392 | |
|
381 | 393 | context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do |
|
382 | 394 | setup do |
|
383 | 395 | @user = User.generate! do |user| |
|
384 | 396 | user.admin = true |
|
385 | 397 | end |
|
386 | 398 | @token = Token.create!(:user => @user, :action => 'api') |
|
387 | 399 | send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s}) |
|
388 | 400 | end |
|
389 | 401 | should_respond_with success_code |
|
390 | 402 | should_respond_with_content_type_based_on_url(url) |
|
391 | 403 | should_be_a_valid_response_string_based_on_url(url) |
|
392 | 404 | should "login as the user" do |
|
393 | 405 | assert_equal @user, User.current |
|
394 | 406 | end |
|
395 | 407 | end |
|
396 | 408 | end |
|
397 | 409 | |
|
398 | 410 | # Uses should_respond_with_content_type based on what's in the url: |
|
399 | 411 | # |
|
400 | 412 | # '/project/issues.xml' => should_respond_with_content_type :xml |
|
401 | 413 | # '/project/issues.json' => should_respond_with_content_type :json |
|
402 | 414 | # |
|
403 | 415 | # @param [String] url Request |
|
404 | 416 | def self.should_respond_with_content_type_based_on_url(url) |
|
405 | 417 | case |
|
406 | 418 | when url.match(/xml/i) |
|
407 | 419 | should "respond with XML" do |
|
408 | 420 | assert_equal 'application/xml', @response.content_type |
|
409 | 421 | end |
|
410 | 422 | when url.match(/json/i) |
|
411 | 423 | should "respond with JSON" do |
|
412 | 424 | assert_equal 'application/json', @response.content_type |
|
413 | 425 | end |
|
414 | 426 | else |
|
415 | 427 | raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}" |
|
416 | 428 | end |
|
417 | 429 | end |
|
418 | 430 | |
|
419 | 431 | # Uses the url to assert which format the response should be in |
|
420 | 432 | # |
|
421 | 433 | # '/project/issues.xml' => should_be_a_valid_xml_string |
|
422 | 434 | # '/project/issues.json' => should_be_a_valid_json_string |
|
423 | 435 | # |
|
424 | 436 | # @param [String] url Request |
|
425 | 437 | def self.should_be_a_valid_response_string_based_on_url(url) |
|
426 | 438 | case |
|
427 | 439 | when url.match(/xml/i) |
|
428 | 440 | should_be_a_valid_xml_string |
|
429 | 441 | when url.match(/json/i) |
|
430 | 442 | should_be_a_valid_json_string |
|
431 | 443 | else |
|
432 | 444 | raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}" |
|
433 | 445 | end |
|
434 | 446 | end |
|
435 | 447 | |
|
436 | 448 | # Checks that the response is a valid JSON string |
|
437 | 449 | def self.should_be_a_valid_json_string |
|
438 | 450 | should "be a valid JSON string (or empty)" do |
|
439 | 451 | assert(response.body.blank? || ActiveSupport::JSON.decode(response.body)) |
|
440 | 452 | end |
|
441 | 453 | end |
|
442 | 454 | |
|
443 | 455 | # Checks that the response is a valid XML string |
|
444 | 456 | def self.should_be_a_valid_xml_string |
|
445 | 457 | should "be a valid XML string" do |
|
446 | 458 | assert REXML::Document.new(response.body) |
|
447 | 459 | end |
|
448 | 460 | end |
|
449 | 461 | |
|
450 | 462 | def self.should_respond_with(status) |
|
451 | 463 | should "respond with #{status}" do |
|
452 | 464 | assert_response status |
|
453 | 465 | end |
|
454 | 466 | end |
|
455 | 467 | end |
|
456 | 468 | end |
|
457 | 469 | end |
|
458 | 470 | |
|
459 | 471 | # URL helpers do not work with config.threadsafe! |
|
460 | 472 | # https://github.com/rspec/rspec-rails/issues/476#issuecomment-4705454 |
|
461 | 473 | ActionView::TestCase::TestController.instance_eval do |
|
462 | 474 | helper Rails.application.routes.url_helpers |
|
463 | 475 | end |
|
464 | 476 | ActionView::TestCase::TestController.class_eval do |
|
465 | 477 | def _routes |
|
466 | 478 | Rails.application.routes |
|
467 | 479 | end |
|
468 | 480 | end |
@@ -1,409 +1,402 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2013 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | |
|
20 | 20 | class IssueNestedSetTest < ActiveSupport::TestCase |
|
21 | 21 | fixtures :projects, :users, :roles, |
|
22 | 22 | :trackers, :projects_trackers, |
|
23 | 23 | :issue_statuses, :issue_categories, :issue_relations, |
|
24 | 24 | :enumerations, |
|
25 | 25 | :issues |
|
26 | 26 | |
|
27 | 27 | def test_new_record_is_leaf |
|
28 | 28 | i = Issue.new |
|
29 | 29 | assert i.leaf? |
|
30 | 30 | end |
|
31 | 31 | |
|
32 | 32 | def test_create_root_issue |
|
33 | lft1 = new_issue_lft | |
|
33 | 34 | issue1 = Issue.generate! |
|
35 | lft2 = new_issue_lft | |
|
34 | 36 | issue2 = Issue.generate! |
|
35 | 37 | issue1.reload |
|
36 | 38 | issue2.reload |
|
37 | ||
|
38 |
assert_equal [issue |
|
|
39 | assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt] | |
|
39 | assert_equal [issue1.id, nil, lft1, lft1 + 1], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt] | |
|
40 | assert_equal [issue2.id, nil, lft2, lft2 + 1], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt] | |
|
40 | 41 | end |
|
41 | 42 | |
|
42 | 43 | def test_create_child_issue |
|
44 | lft = new_issue_lft | |
|
43 | 45 | parent = Issue.generate! |
|
44 | 46 | child = Issue.generate!(:parent_issue_id => parent.id) |
|
45 | 47 | parent.reload |
|
46 | 48 | child.reload |
|
47 | ||
|
48 |
assert_equal [parent.id, |
|
|
49 | assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt] | |
|
49 | assert_equal [parent.id, nil, lft, lft + 3], [parent.root_id, parent.parent_id, parent.lft, parent.rgt] | |
|
50 | assert_equal [parent.id, parent.id, lft + 1, lft + 2], [child.root_id, child.parent_id, child.lft, child.rgt] | |
|
50 | 51 | end |
|
51 | 52 | |
|
52 | 53 | def test_creating_a_child_in_a_subproject_should_validate |
|
53 | 54 | issue = Issue.generate! |
|
54 | 55 | child = Issue.new(:project_id => 3, :tracker_id => 2, :author_id => 1, |
|
55 | 56 | :subject => 'child', :parent_issue_id => issue.id) |
|
56 | 57 | assert_save child |
|
57 | 58 | assert_equal issue, child.reload.parent |
|
58 | 59 | end |
|
59 | 60 | |
|
60 | 61 | def test_creating_a_child_in_an_invalid_project_should_not_validate |
|
61 | 62 | issue = Issue.generate! |
|
62 | 63 | child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1, |
|
63 | 64 | :subject => 'child', :parent_issue_id => issue.id) |
|
64 | 65 | assert !child.save |
|
65 | 66 | assert_not_equal [], child.errors[:parent_issue_id] |
|
66 | 67 | end |
|
67 | 68 | |
|
68 | 69 | def test_move_a_root_to_child |
|
70 | lft = new_issue_lft | |
|
69 | 71 | parent1 = Issue.generate! |
|
70 | 72 | parent2 = Issue.generate! |
|
71 | 73 | child = Issue.generate!(:parent_issue_id => parent1.id) |
|
72 | ||
|
73 | 74 | parent2.parent_issue_id = parent1.id |
|
74 | 75 | parent2.save! |
|
75 | 76 | child.reload |
|
76 | 77 | parent1.reload |
|
77 | 78 | parent2.reload |
|
78 | ||
|
79 |
assert_equal [parent1.id, |
|
|
80 |
assert_equal [parent1.id, |
|
|
81 | assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt] | |
|
79 | assert_equal [parent1.id, lft, lft + 5], [parent1.root_id, parent1.lft, parent1.rgt] | |
|
80 | assert_equal [parent1.id, lft + 3, lft + 4], [parent2.root_id, parent2.lft, parent2.rgt] | |
|
81 | assert_equal [parent1.id, lft + 1, lft + 2], [child.root_id, child.lft, child.rgt] | |
|
82 | 82 | end |
|
83 | 83 | |
|
84 | 84 | def test_move_a_child_to_root |
|
85 | lft1 = new_issue_lft | |
|
85 | 86 | parent1 = Issue.generate! |
|
87 | lft2 = new_issue_lft | |
|
86 | 88 | parent2 = Issue.generate! |
|
87 | 89 | child = Issue.generate!(:parent_issue_id => parent1.id) |
|
88 | ||
|
89 | 90 | child.parent_issue_id = nil |
|
90 | 91 | child.save! |
|
91 | 92 | child.reload |
|
92 | 93 | parent1.reload |
|
93 | 94 | parent2.reload |
|
94 | ||
|
95 |
assert_equal [parent |
|
|
96 | assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt] | |
|
95 | assert_equal [parent1.id, lft1, lft1 + 1], [parent1.root_id, parent1.lft, parent1.rgt] | |
|
96 | assert_equal [parent2.id, lft2, lft2 + 1], [parent2.root_id, parent2.lft, parent2.rgt] | |
|
97 | 97 | assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt] |
|
98 | 98 | end |
|
99 | 99 | |
|
100 | 100 | def test_move_a_child_to_another_issue |
|
101 | lft1 = new_issue_lft | |
|
101 | 102 | parent1 = Issue.generate! |
|
103 | lft2 = new_issue_lft | |
|
102 | 104 | parent2 = Issue.generate! |
|
103 | 105 | child = Issue.generate!(:parent_issue_id => parent1.id) |
|
104 | ||
|
105 | 106 | child.parent_issue_id = parent2.id |
|
106 | 107 | child.save! |
|
107 | 108 | child.reload |
|
108 | 109 | parent1.reload |
|
109 | 110 | parent2.reload |
|
110 | ||
|
111 |
assert_equal [parent |
|
|
112 |
assert_equal [parent2.id, 1, |
|
|
113 | assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt] | |
|
111 | assert_equal [parent1.id, lft1, lft1 + 1], [parent1.root_id, parent1.lft, parent1.rgt] | |
|
112 | assert_equal [parent2.id, lft2, lft2 + 3], [parent2.root_id, parent2.lft, parent2.rgt] | |
|
113 | assert_equal [parent2.id, lft2 + 1, lft2 + 2], [child.root_id, child.lft, child.rgt] | |
|
114 | 114 | end |
|
115 | 115 | |
|
116 | 116 | def test_move_a_child_with_descendants_to_another_issue |
|
117 | lft1 = new_issue_lft | |
|
117 | 118 | parent1 = Issue.generate! |
|
119 | lft2 = new_issue_lft | |
|
118 | 120 | parent2 = Issue.generate! |
|
119 | 121 | child = Issue.generate!(:parent_issue_id => parent1.id) |
|
120 | 122 | grandchild = Issue.generate!(:parent_issue_id => child.id) |
|
121 | ||
|
122 | 123 | parent1.reload |
|
123 | 124 | parent2.reload |
|
124 | 125 | child.reload |
|
125 | 126 | grandchild.reload |
|
126 | ||
|
127 |
assert_equal [parent |
|
|
128 |
assert_equal [parent |
|
|
129 |
assert_equal [parent1.id, 2, |
|
|
130 | assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt] | |
|
131 | ||
|
127 | assert_equal [parent1.id, lft1, lft1 + 5], [parent1.root_id, parent1.lft, parent1.rgt] | |
|
128 | assert_equal [parent2.id, lft2, lft2 + 1], [parent2.root_id, parent2.lft, parent2.rgt] | |
|
129 | assert_equal [parent1.id, lft1 + 1, lft1 + 4], [child.root_id, child.lft, child.rgt] | |
|
130 | assert_equal [parent1.id, lft1 + 2, lft1 + 3], [grandchild.root_id, grandchild.lft, grandchild.rgt] | |
|
132 | 131 | child.reload.parent_issue_id = parent2.id |
|
133 | 132 | child.save! |
|
134 | 133 | child.reload |
|
135 | 134 | grandchild.reload |
|
136 | 135 | parent1.reload |
|
137 | 136 | parent2.reload |
|
138 | ||
|
139 |
assert_equal [parent |
|
|
140 |
assert_equal [parent2.id, 1, |
|
|
141 |
assert_equal [parent2.id, 2, |
|
|
142 | assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt] | |
|
137 | assert_equal [parent1.id, lft1, lft1 + 1], [parent1.root_id, parent1.lft, parent1.rgt] | |
|
138 | assert_equal [parent2.id, lft2, lft2 + 5], [parent2.root_id, parent2.lft, parent2.rgt] | |
|
139 | assert_equal [parent2.id, lft2 + 1, lft2 + 4], [child.root_id, child.lft, child.rgt] | |
|
140 | assert_equal [parent2.id, lft2 + 2, lft2 + 3], [grandchild.root_id, grandchild.lft, grandchild.rgt] | |
|
143 | 141 | end |
|
144 | 142 | |
|
145 | 143 | def test_move_a_child_with_descendants_to_another_project |
|
144 | lft1 = new_issue_lft | |
|
146 | 145 | parent1 = Issue.generate! |
|
147 | 146 | child = Issue.generate!(:parent_issue_id => parent1.id) |
|
148 | 147 | grandchild = Issue.generate!(:parent_issue_id => child.id) |
|
149 | ||
|
150 | 148 | child.reload |
|
151 | 149 | child.project = Project.find(2) |
|
152 | 150 | assert child.save |
|
153 | 151 | child.reload |
|
154 | 152 | grandchild.reload |
|
155 | 153 | parent1.reload |
|
156 | ||
|
157 | assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] | |
|
154 | assert_equal [1, parent1.id, lft1, lft1 + 1], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] | |
|
158 | 155 | assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt] |
|
159 | 156 | assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt] |
|
160 | 157 | end |
|
161 | 158 | |
|
162 | 159 | def test_moving_an_issue_to_a_descendant_should_not_validate |
|
163 | 160 | parent1 = Issue.generate! |
|
164 | 161 | parent2 = Issue.generate! |
|
165 | 162 | child = Issue.generate!(:parent_issue_id => parent1.id) |
|
166 | 163 | grandchild = Issue.generate!(:parent_issue_id => child.id) |
|
167 | 164 | |
|
168 | 165 | child.reload |
|
169 | 166 | child.parent_issue_id = grandchild.id |
|
170 | 167 | assert !child.save |
|
171 | 168 | assert_not_equal [], child.errors[:parent_issue_id] |
|
172 | 169 | end |
|
173 | 170 | |
|
174 | 171 | def test_updating_a_root_issue_should_not_trigger_update_nested_set_attributes_on_parent_change |
|
175 | 172 | issue = Issue.find(Issue.generate!.id) |
|
176 | 173 | issue.parent_issue_id = "" |
|
177 | 174 | issue.expects(:update_nested_set_attributes_on_parent_change).never |
|
178 | 175 | issue.save! |
|
179 | 176 | end |
|
180 | 177 | |
|
181 | 178 | def test_updating_a_child_issue_should_not_trigger_update_nested_set_attributes_on_parent_change |
|
182 | 179 | issue = Issue.find(Issue.generate!(:parent_issue_id => 1).id) |
|
183 | 180 | issue.parent_issue_id = "1" |
|
184 | 181 | issue.expects(:update_nested_set_attributes_on_parent_change).never |
|
185 | 182 | issue.save! |
|
186 | 183 | end |
|
187 | 184 | |
|
188 | 185 | def test_moving_a_root_issue_should_trigger_update_nested_set_attributes_on_parent_change |
|
189 | 186 | issue = Issue.find(Issue.generate!.id) |
|
190 | 187 | issue.parent_issue_id = "1" |
|
191 | 188 | issue.expects(:update_nested_set_attributes_on_parent_change).once |
|
192 | 189 | issue.save! |
|
193 | 190 | end |
|
194 | 191 | |
|
195 | 192 | def test_moving_a_child_issue_to_another_parent_should_trigger_update_nested_set_attributes_on_parent_change |
|
196 | 193 | issue = Issue.find(Issue.generate!(:parent_issue_id => 1).id) |
|
197 | 194 | issue.parent_issue_id = "2" |
|
198 | 195 | issue.expects(:update_nested_set_attributes_on_parent_change).once |
|
199 | 196 | issue.save! |
|
200 | 197 | end |
|
201 | 198 | |
|
202 | 199 | def test_moving_a_child_issue_to_root_should_trigger_update_nested_set_attributes_on_parent_change |
|
203 | 200 | issue = Issue.find(Issue.generate!(:parent_issue_id => 1).id) |
|
204 | 201 | issue.parent_issue_id = "" |
|
205 | 202 | issue.expects(:update_nested_set_attributes_on_parent_change).once |
|
206 | 203 | issue.save! |
|
207 | 204 | end |
|
208 | 205 | |
|
209 | 206 | def test_destroy_should_destroy_children |
|
207 | lft1 = new_issue_lft | |
|
210 | 208 | issue1 = Issue.generate! |
|
211 | 209 | issue2 = Issue.generate! |
|
212 | 210 | issue3 = Issue.generate!(:parent_issue_id => issue2.id) |
|
213 | 211 | issue4 = Issue.generate!(:parent_issue_id => issue1.id) |
|
214 | ||
|
215 | 212 | issue3.init_journal(User.find(2)) |
|
216 | 213 | issue3.subject = 'child with journal' |
|
217 | 214 | issue3.save! |
|
218 | ||
|
219 | 215 | assert_difference 'Issue.count', -2 do |
|
220 | 216 | assert_difference 'Journal.count', -1 do |
|
221 | 217 | assert_difference 'JournalDetail.count', -1 do |
|
222 | 218 | Issue.find(issue2.id).destroy |
|
223 | 219 | end |
|
224 | 220 | end |
|
225 | 221 | end |
|
226 | ||
|
227 | 222 | issue1.reload |
|
228 | 223 | issue4.reload |
|
229 | 224 | assert !Issue.exists?(issue2.id) |
|
230 | 225 | assert !Issue.exists?(issue3.id) |
|
231 |
assert_equal [issue1.id, 1, |
|
|
232 |
assert_equal [issue1.id, |
|
|
226 | assert_equal [issue1.id, lft1, lft1 + 3], [issue1.root_id, issue1.lft, issue1.rgt] | |
|
227 | assert_equal [issue1.id, lft1 + 1, lft1 + 2], [issue4.root_id, issue4.lft, issue4.rgt] | |
|
233 | 228 | end |
|
234 | ||
|
229 | ||
|
235 | 230 | def test_destroy_child_should_update_parent |
|
231 | lft1 = new_issue_lft | |
|
236 | 232 | issue = Issue.generate! |
|
237 | 233 | child1 = Issue.generate!(:parent_issue_id => issue.id) |
|
238 | 234 | child2 = Issue.generate!(:parent_issue_id => issue.id) |
|
239 | ||
|
240 | 235 | issue.reload |
|
241 |
assert_equal [issue.id, 1, |
|
|
242 | ||
|
236 | assert_equal [issue.id, lft1, lft1 + 5], [issue.root_id, issue.lft, issue.rgt] | |
|
243 | 237 | child2.reload.destroy |
|
244 | ||
|
245 | 238 | issue.reload |
|
246 |
assert_equal [issue.id, 1, |
|
|
239 | assert_equal [issue.id, lft1, lft1 + 3], [issue.root_id, issue.lft, issue.rgt] | |
|
247 | 240 | end |
|
248 | 241 | |
|
249 | 242 | def test_destroy_parent_issue_updated_during_children_destroy |
|
250 | 243 | parent = Issue.generate! |
|
251 | 244 | Issue.generate!(:start_date => Date.today, :parent_issue_id => parent.id) |
|
252 | 245 | Issue.generate!(:start_date => 2.days.from_now, :parent_issue_id => parent.id) |
|
253 | 246 | |
|
254 | 247 | assert_difference 'Issue.count', -3 do |
|
255 | 248 | Issue.find(parent.id).destroy |
|
256 | 249 | end |
|
257 | 250 | end |
|
258 | 251 | |
|
259 | 252 | def test_destroy_child_issue_with_children |
|
260 | 253 | root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root') |
|
261 | 254 | child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id) |
|
262 | 255 | leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id) |
|
263 | 256 | leaf.init_journal(User.find(2)) |
|
264 | 257 | leaf.subject = 'leaf with journal' |
|
265 | 258 | leaf.save! |
|
266 | 259 | |
|
267 | 260 | assert_difference 'Issue.count', -2 do |
|
268 | 261 | assert_difference 'Journal.count', -1 do |
|
269 | 262 | assert_difference 'JournalDetail.count', -1 do |
|
270 | 263 | Issue.find(child.id).destroy |
|
271 | 264 | end |
|
272 | 265 | end |
|
273 | 266 | end |
|
274 | 267 | |
|
275 | 268 | root = Issue.find(root.id) |
|
276 | 269 | assert root.leaf?, "Root issue is not a leaf (lft: #{root.lft}, rgt: #{root.rgt})" |
|
277 | 270 | end |
|
278 | 271 | |
|
279 | 272 | def test_destroy_issue_with_grand_child |
|
273 | lft1 = new_issue_lft | |
|
280 | 274 | parent = Issue.generate! |
|
281 | 275 | issue = Issue.generate!(:parent_issue_id => parent.id) |
|
282 | 276 | child = Issue.generate!(:parent_issue_id => issue.id) |
|
283 | 277 | grandchild1 = Issue.generate!(:parent_issue_id => child.id) |
|
284 | 278 | grandchild2 = Issue.generate!(:parent_issue_id => child.id) |
|
285 | ||
|
286 | 279 | assert_difference 'Issue.count', -4 do |
|
287 | 280 | Issue.find(issue.id).destroy |
|
288 | 281 | parent.reload |
|
289 |
assert_equal [1, |
|
|
282 | assert_equal [lft1, lft1 + 1], [parent.lft, parent.rgt] | |
|
290 | 283 | end |
|
291 | 284 | end |
|
292 | 285 | |
|
293 | 286 | def test_parent_priority_should_be_the_highest_child_priority |
|
294 | 287 | parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal')) |
|
295 | 288 | # Create children |
|
296 | 289 | child1 = Issue.generate!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id) |
|
297 | 290 | assert_equal 'High', parent.reload.priority.name |
|
298 | 291 | child2 = Issue.generate!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id) |
|
299 | 292 | assert_equal 'Immediate', child1.reload.priority.name |
|
300 | 293 | assert_equal 'Immediate', parent.reload.priority.name |
|
301 | 294 | child3 = Issue.generate!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id) |
|
302 | 295 | assert_equal 'Immediate', parent.reload.priority.name |
|
303 | 296 | # Destroy a child |
|
304 | 297 | child1.destroy |
|
305 | 298 | assert_equal 'Low', parent.reload.priority.name |
|
306 | 299 | # Update a child |
|
307 | 300 | child3.reload.priority = IssuePriority.find_by_name('Normal') |
|
308 | 301 | child3.save! |
|
309 | 302 | assert_equal 'Normal', parent.reload.priority.name |
|
310 | 303 | end |
|
311 | 304 | |
|
312 | 305 | def test_parent_dates_should_be_lowest_start_and_highest_due_dates |
|
313 | 306 | parent = Issue.generate! |
|
314 | 307 | Issue.generate!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id) |
|
315 | 308 | Issue.generate!( :due_date => '2010-02-13', :parent_issue_id => parent.id) |
|
316 | 309 | Issue.generate!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id) |
|
317 | 310 | parent.reload |
|
318 | 311 | assert_equal Date.parse('2010-01-25'), parent.start_date |
|
319 | 312 | assert_equal Date.parse('2010-02-22'), parent.due_date |
|
320 | 313 | end |
|
321 | 314 | |
|
322 | 315 | def test_parent_done_ratio_should_be_average_done_ratio_of_leaves |
|
323 | 316 | parent = Issue.generate! |
|
324 | 317 | Issue.generate!(:done_ratio => 20, :parent_issue_id => parent.id) |
|
325 | 318 | assert_equal 20, parent.reload.done_ratio |
|
326 | 319 | Issue.generate!(:done_ratio => 70, :parent_issue_id => parent.id) |
|
327 | 320 | assert_equal 45, parent.reload.done_ratio |
|
328 | 321 | |
|
329 | 322 | child = Issue.generate!(:done_ratio => 0, :parent_issue_id => parent.id) |
|
330 | 323 | assert_equal 30, parent.reload.done_ratio |
|
331 | 324 | |
|
332 | 325 | Issue.generate!(:done_ratio => 30, :parent_issue_id => child.id) |
|
333 | 326 | assert_equal 30, child.reload.done_ratio |
|
334 | 327 | assert_equal 40, parent.reload.done_ratio |
|
335 | 328 | end |
|
336 | 329 | |
|
337 | 330 | def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any |
|
338 | 331 | parent = Issue.generate! |
|
339 | 332 | Issue.generate!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id) |
|
340 | 333 | assert_equal 20, parent.reload.done_ratio |
|
341 | 334 | Issue.generate!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id) |
|
342 | 335 | assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio |
|
343 | 336 | end |
|
344 | 337 | |
|
345 | 338 | def test_parent_done_ratio_with_child_estimate_to_0_should_reach_100 |
|
346 | 339 | parent = Issue.generate! |
|
347 | 340 | issue1 = Issue.generate!(:parent_issue_id => parent.id) |
|
348 | 341 | issue2 = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 0) |
|
349 | 342 | assert_equal 0, parent.reload.done_ratio |
|
350 | 343 | issue1.reload.update_attribute :status_id, 5 |
|
351 | 344 | assert_equal 50, parent.reload.done_ratio |
|
352 | 345 | issue2.reload.update_attribute :status_id, 5 |
|
353 | 346 | assert_equal 100, parent.reload.done_ratio |
|
354 | 347 | end |
|
355 | 348 | |
|
356 | 349 | def test_parent_estimate_should_be_sum_of_leaves |
|
357 | 350 | parent = Issue.generate! |
|
358 | 351 | Issue.generate!(:estimated_hours => nil, :parent_issue_id => parent.id) |
|
359 | 352 | assert_equal nil, parent.reload.estimated_hours |
|
360 | 353 | Issue.generate!(:estimated_hours => 5, :parent_issue_id => parent.id) |
|
361 | 354 | assert_equal 5, parent.reload.estimated_hours |
|
362 | 355 | Issue.generate!(:estimated_hours => 7, :parent_issue_id => parent.id) |
|
363 | 356 | assert_equal 12, parent.reload.estimated_hours |
|
364 | 357 | end |
|
365 | 358 | |
|
366 | 359 | def test_move_parent_updates_old_parent_attributes |
|
367 | 360 | first_parent = Issue.generate! |
|
368 | 361 | second_parent = Issue.generate! |
|
369 | 362 | child = Issue.generate!(:estimated_hours => 5, :parent_issue_id => first_parent.id) |
|
370 | 363 | assert_equal 5, first_parent.reload.estimated_hours |
|
371 | 364 | child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id) |
|
372 | 365 | assert_equal 7, second_parent.reload.estimated_hours |
|
373 | 366 | assert_nil first_parent.reload.estimated_hours |
|
374 | 367 | end |
|
375 | 368 | |
|
376 | 369 | def test_reschuling_a_parent_should_reschedule_subtasks |
|
377 | 370 | parent = Issue.generate! |
|
378 | 371 | c1 = Issue.generate!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id) |
|
379 | 372 | c2 = Issue.generate!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id) |
|
380 | 373 | parent.reload |
|
381 | 374 | parent.reschedule_on!(Date.parse('2010-06-02')) |
|
382 | 375 | c1.reload |
|
383 | 376 | assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date] |
|
384 | 377 | c2.reload |
|
385 | 378 | assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change |
|
386 | 379 | parent.reload |
|
387 | 380 | assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date] |
|
388 | 381 | end |
|
389 | 382 | |
|
390 | 383 | def test_project_copy_should_copy_issue_tree |
|
391 | 384 | p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2]) |
|
392 | 385 | i1 = Issue.generate!(:project => p, :subject => 'i1') |
|
393 | 386 | i2 = Issue.generate!(:project => p, :subject => 'i2', :parent_issue_id => i1.id) |
|
394 | 387 | i3 = Issue.generate!(:project => p, :subject => 'i3', :parent_issue_id => i1.id) |
|
395 | 388 | i4 = Issue.generate!(:project => p, :subject => 'i4', :parent_issue_id => i2.id) |
|
396 | 389 | i5 = Issue.generate!(:project => p, :subject => 'i5') |
|
397 | 390 | c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2]) |
|
398 | 391 | c.copy(p, :only => 'issues') |
|
399 | 392 | c.reload |
|
400 | 393 | |
|
401 | 394 | assert_equal 5, c.issues.count |
|
402 | 395 | ic1, ic2, ic3, ic4, ic5 = c.issues.order('subject').all |
|
403 | 396 | assert ic1.root? |
|
404 | 397 | assert_equal ic1, ic2.parent |
|
405 | 398 | assert_equal ic1, ic3.parent |
|
406 | 399 | assert_equal ic2, ic4.parent |
|
407 | 400 | assert ic5.root? |
|
408 | 401 | end |
|
409 | 402 | end |
@@ -1,54 +1,52 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2013 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | |
|
20 | 20 | class IssueTransactionTest < ActiveSupport::TestCase |
|
21 | 21 | fixtures :projects, :users, :members, :member_roles, :roles, |
|
22 | 22 | :trackers, :projects_trackers, |
|
23 | 23 | :versions, |
|
24 | 24 | :issue_statuses, :issue_categories, :issue_relations, :workflows, |
|
25 | 25 | :enumerations, |
|
26 | 26 | :issues, |
|
27 | 27 | :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values, |
|
28 | 28 | :time_entries |
|
29 | 29 | |
|
30 | 30 | self.use_transactional_fixtures = false |
|
31 | 31 | |
|
32 | 32 | def test_invalid_move_to_another_project |
|
33 | lft1 = new_issue_lft | |
|
33 | 34 | parent1 = Issue.generate! |
|
34 | 35 | child = Issue.generate!(:parent_issue_id => parent1.id) |
|
35 | 36 | grandchild = Issue.generate!(:parent_issue_id => child.id, :tracker_id => 2) |
|
36 | 37 | Project.find(2).tracker_ids = [1] |
|
37 | ||
|
38 | 38 | parent1.reload |
|
39 |
assert_equal [1, parent1.id, 1, |
|
|
40 | ||
|
39 | assert_equal [1, parent1.id, lft1, lft1 + 5], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] | |
|
41 | 40 | # child can not be moved to Project 2 because its child is on a disabled tracker |
|
42 | 41 | child = Issue.find(child.id) |
|
43 | 42 | child.project = Project.find(2) |
|
44 | 43 | assert !child.save |
|
45 | 44 | child.reload |
|
46 | 45 | grandchild.reload |
|
47 | 46 | parent1.reload |
|
48 | ||
|
49 | 47 | # no change |
|
50 |
assert_equal [1, parent1.id, 1, |
|
|
51 |
assert_equal [1, parent1.id, |
|
|
52 |
assert_equal [1, parent1.id, |
|
|
48 | assert_equal [1, parent1.id, lft1, lft1 + 5], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] | |
|
49 | assert_equal [1, parent1.id, lft1 + 1, lft1 + 4], [child.project_id, child.root_id, child.lft, child.rgt] | |
|
50 | assert_equal [1, parent1.id, lft1 + 2, lft1 + 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt] | |
|
53 | 51 | end |
|
54 | 52 | end |
@@ -1,885 +1,887 | |||
|
1 | 1 | # encoding: utf-8 |
|
2 | 2 | # |
|
3 | 3 | # Redmine - project management software |
|
4 | 4 | # Copyright (C) 2006-2013 Jean-Philippe Lang |
|
5 | 5 | # |
|
6 | 6 | # This program is free software; you can redistribute it and/or |
|
7 | 7 | # modify it under the terms of the GNU General Public License |
|
8 | 8 | # as published by the Free Software Foundation; either version 2 |
|
9 | 9 | # of the License, or (at your option) any later version. |
|
10 | 10 | # |
|
11 | 11 | # This program is distributed in the hope that it will be useful, |
|
12 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 | 14 | # GNU General Public License for more details. |
|
15 | 15 | # |
|
16 | 16 | # You should have received a copy of the GNU General Public License |
|
17 | 17 | # along with this program; if not, write to the Free Software |
|
18 | 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
19 | 19 | |
|
20 | 20 | require File.expand_path('../../test_helper', __FILE__) |
|
21 | 21 | |
|
22 | 22 | class MailHandlerTest < ActiveSupport::TestCase |
|
23 | 23 | fixtures :users, :projects, :enabled_modules, :roles, |
|
24 | 24 | :members, :member_roles, :users, |
|
25 | 25 | :issues, :issue_statuses, |
|
26 | 26 | :workflows, :trackers, :projects_trackers, |
|
27 | 27 | :versions, :enumerations, :issue_categories, |
|
28 | 28 | :custom_fields, :custom_fields_trackers, :custom_fields_projects, |
|
29 | 29 | :boards, :messages |
|
30 | 30 | |
|
31 | 31 | FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' |
|
32 | 32 | |
|
33 | 33 | def setup |
|
34 | 34 | ActionMailer::Base.deliveries.clear |
|
35 | 35 | Setting.notified_events = Redmine::Notifiable.all.collect(&:name) |
|
36 | 36 | end |
|
37 | 37 | |
|
38 | 38 | def teardown |
|
39 | 39 | Setting.clear_cache |
|
40 | 40 | end |
|
41 | 41 | |
|
42 | 42 | def test_add_issue |
|
43 | 43 | ActionMailer::Base.deliveries.clear |
|
44 | lft1 = new_issue_lft | |
|
44 | 45 | # This email contains: 'Project: onlinestore' |
|
45 | 46 | issue = submit_email('ticket_on_given_project.eml') |
|
46 | 47 | assert issue.is_a?(Issue) |
|
47 | 48 | assert !issue.new_record? |
|
48 | 49 | issue.reload |
|
49 | 50 | assert_equal Project.find(2), issue.project |
|
50 | 51 | assert_equal issue.project.trackers.first, issue.tracker |
|
51 | 52 | assert_equal 'New ticket on a given project', issue.subject |
|
52 | 53 | assert_equal User.find_by_login('jsmith'), issue.author |
|
53 | 54 | assert_equal IssueStatus.find_by_name('Resolved'), issue.status |
|
54 | 55 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
55 | 56 | assert_equal '2010-01-01', issue.start_date.to_s |
|
56 | 57 | assert_equal '2010-12-31', issue.due_date.to_s |
|
57 | 58 | assert_equal User.find_by_login('jsmith'), issue.assigned_to |
|
58 | 59 | assert_equal Version.find_by_name('Alpha'), issue.fixed_version |
|
59 | 60 | assert_equal 2.5, issue.estimated_hours |
|
60 | 61 | assert_equal 30, issue.done_ratio |
|
61 |
assert_equal [issue.id, 1, |
|
|
62 | assert_equal [issue.id, lft1, lft1 + 1], [issue.root_id, issue.lft, issue.rgt] | |
|
62 | 63 | # keywords should be removed from the email body |
|
63 | 64 | assert !issue.description.match(/^Project:/i) |
|
64 | 65 | assert !issue.description.match(/^Status:/i) |
|
65 | 66 | assert !issue.description.match(/^Start Date:/i) |
|
66 | 67 | # Email notification should be sent |
|
67 | 68 | mail = ActionMailer::Base.deliveries.last |
|
68 | 69 | assert_not_nil mail |
|
69 | 70 | assert mail.subject.include?('New ticket on a given project') |
|
70 | 71 | end |
|
71 | 72 | |
|
72 | 73 | def test_add_issue_with_default_tracker |
|
73 | 74 | # This email contains: 'Project: onlinestore' |
|
74 | 75 | issue = submit_email( |
|
75 | 76 | 'ticket_on_given_project.eml', |
|
76 | 77 | :issue => {:tracker => 'Support request'} |
|
77 | 78 | ) |
|
78 | 79 | assert issue.is_a?(Issue) |
|
79 | 80 | assert !issue.new_record? |
|
80 | 81 | issue.reload |
|
81 | 82 | assert_equal 'Support request', issue.tracker.name |
|
82 | 83 | end |
|
83 | 84 | |
|
84 | 85 | def test_add_issue_with_status |
|
85 | 86 | # This email contains: 'Project: onlinestore' and 'Status: Resolved' |
|
86 | 87 | issue = submit_email('ticket_on_given_project.eml') |
|
87 | 88 | assert issue.is_a?(Issue) |
|
88 | 89 | assert !issue.new_record? |
|
89 | 90 | issue.reload |
|
90 | 91 | assert_equal Project.find(2), issue.project |
|
91 | 92 | assert_equal IssueStatus.find_by_name("Resolved"), issue.status |
|
92 | 93 | end |
|
93 | 94 | |
|
94 | 95 | def test_add_issue_with_attributes_override |
|
95 | 96 | issue = submit_email( |
|
96 | 97 | 'ticket_with_attributes.eml', |
|
97 | 98 | :allow_override => 'tracker,category,priority' |
|
98 | 99 | ) |
|
99 | 100 | assert issue.is_a?(Issue) |
|
100 | 101 | assert !issue.new_record? |
|
101 | 102 | issue.reload |
|
102 | 103 | assert_equal 'New ticket on a given project', issue.subject |
|
103 | 104 | assert_equal User.find_by_login('jsmith'), issue.author |
|
104 | 105 | assert_equal Project.find(2), issue.project |
|
105 | 106 | assert_equal 'Feature request', issue.tracker.to_s |
|
106 | 107 | assert_equal 'Stock management', issue.category.to_s |
|
107 | 108 | assert_equal 'Urgent', issue.priority.to_s |
|
108 | 109 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
109 | 110 | end |
|
110 | 111 | |
|
111 | 112 | def test_add_issue_with_group_assignment |
|
112 | 113 | with_settings :issue_group_assignment => '1' do |
|
113 | 114 | issue = submit_email('ticket_on_given_project.eml') do |email| |
|
114 | 115 | email.gsub!('Assigned to: John Smith', 'Assigned to: B Team') |
|
115 | 116 | end |
|
116 | 117 | assert issue.is_a?(Issue) |
|
117 | 118 | assert !issue.new_record? |
|
118 | 119 | issue.reload |
|
119 | 120 | assert_equal Group.find(11), issue.assigned_to |
|
120 | 121 | end |
|
121 | 122 | end |
|
122 | 123 | |
|
123 | 124 | def test_add_issue_with_partial_attributes_override |
|
124 | 125 | issue = submit_email( |
|
125 | 126 | 'ticket_with_attributes.eml', |
|
126 | 127 | :issue => {:priority => 'High'}, |
|
127 | 128 | :allow_override => ['tracker'] |
|
128 | 129 | ) |
|
129 | 130 | assert issue.is_a?(Issue) |
|
130 | 131 | assert !issue.new_record? |
|
131 | 132 | issue.reload |
|
132 | 133 | assert_equal 'New ticket on a given project', issue.subject |
|
133 | 134 | assert_equal User.find_by_login('jsmith'), issue.author |
|
134 | 135 | assert_equal Project.find(2), issue.project |
|
135 | 136 | assert_equal 'Feature request', issue.tracker.to_s |
|
136 | 137 | assert_nil issue.category |
|
137 | 138 | assert_equal 'High', issue.priority.to_s |
|
138 | 139 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
139 | 140 | end |
|
140 | 141 | |
|
141 | 142 | def test_add_issue_with_spaces_between_attribute_and_separator |
|
142 | 143 | issue = submit_email( |
|
143 | 144 | 'ticket_with_spaces_between_attribute_and_separator.eml', |
|
144 | 145 | :allow_override => 'tracker,category,priority' |
|
145 | 146 | ) |
|
146 | 147 | assert issue.is_a?(Issue) |
|
147 | 148 | assert !issue.new_record? |
|
148 | 149 | issue.reload |
|
149 | 150 | assert_equal 'New ticket on a given project', issue.subject |
|
150 | 151 | assert_equal User.find_by_login('jsmith'), issue.author |
|
151 | 152 | assert_equal Project.find(2), issue.project |
|
152 | 153 | assert_equal 'Feature request', issue.tracker.to_s |
|
153 | 154 | assert_equal 'Stock management', issue.category.to_s |
|
154 | 155 | assert_equal 'Urgent', issue.priority.to_s |
|
155 | 156 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
156 | 157 | end |
|
157 | 158 | |
|
158 | 159 | def test_add_issue_with_attachment_to_specific_project |
|
159 | 160 | issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) |
|
160 | 161 | assert issue.is_a?(Issue) |
|
161 | 162 | assert !issue.new_record? |
|
162 | 163 | issue.reload |
|
163 | 164 | assert_equal 'Ticket created by email with attachment', issue.subject |
|
164 | 165 | assert_equal User.find_by_login('jsmith'), issue.author |
|
165 | 166 | assert_equal Project.find(2), issue.project |
|
166 | 167 | assert_equal 'This is a new ticket with attachments', issue.description |
|
167 | 168 | # Attachment properties |
|
168 | 169 | assert_equal 1, issue.attachments.size |
|
169 | 170 | assert_equal 'Paella.jpg', issue.attachments.first.filename |
|
170 | 171 | assert_equal 'image/jpeg', issue.attachments.first.content_type |
|
171 | 172 | assert_equal 10790, issue.attachments.first.filesize |
|
172 | 173 | end |
|
173 | 174 | |
|
174 | 175 | def test_add_issue_with_custom_fields |
|
175 | 176 | issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'}) |
|
176 | 177 | assert issue.is_a?(Issue) |
|
177 | 178 | assert !issue.new_record? |
|
178 | 179 | issue.reload |
|
179 | 180 | assert_equal 'New ticket with custom field values', issue.subject |
|
180 | 181 | assert_equal 'PostgreSQL', issue.custom_field_value(1) |
|
181 | 182 | assert_equal 'Value for a custom field', issue.custom_field_value(2) |
|
182 | 183 | assert !issue.description.match(/^searchable field:/i) |
|
183 | 184 | end |
|
184 | 185 | |
|
185 | 186 | def test_add_issue_with_version_custom_fields |
|
186 | 187 | field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3]) |
|
187 | 188 | |
|
188 | 189 | issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email| |
|
189 | 190 | email << "Affected version: 1.0\n" |
|
190 | 191 | end |
|
191 | 192 | assert issue.is_a?(Issue) |
|
192 | 193 | assert !issue.new_record? |
|
193 | 194 | issue.reload |
|
194 | 195 | assert_equal '2', issue.custom_field_value(field) |
|
195 | 196 | end |
|
196 | 197 | |
|
197 | 198 | def test_add_issue_should_match_assignee_on_display_name |
|
198 | 199 | user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz') |
|
199 | 200 | User.add_to_project(user, Project.find(2)) |
|
200 | 201 | issue = submit_email('ticket_on_given_project.eml') do |email| |
|
201 | 202 | email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz') |
|
202 | 203 | end |
|
203 | 204 | assert issue.is_a?(Issue) |
|
204 | 205 | assert_equal user, issue.assigned_to |
|
205 | 206 | end |
|
206 | 207 | |
|
207 | 208 | def test_add_issue_with_cc |
|
208 | 209 | issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'}) |
|
209 | 210 | assert issue.is_a?(Issue) |
|
210 | 211 | assert !issue.new_record? |
|
211 | 212 | issue.reload |
|
212 | 213 | assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo')) |
|
213 | 214 | assert_equal 1, issue.watcher_user_ids.size |
|
214 | 215 | end |
|
215 | 216 | |
|
216 | 217 | def test_add_issue_by_unknown_user |
|
217 | 218 | assert_no_difference 'User.count' do |
|
218 | 219 | assert_equal false, |
|
219 | 220 | submit_email( |
|
220 | 221 | 'ticket_by_unknown_user.eml', |
|
221 | 222 | :issue => {:project => 'ecookbook'} |
|
222 | 223 | ) |
|
223 | 224 | end |
|
224 | 225 | end |
|
225 | 226 | |
|
226 | 227 | def test_add_issue_by_anonymous_user |
|
227 | 228 | Role.anonymous.add_permission!(:add_issues) |
|
228 | 229 | assert_no_difference 'User.count' do |
|
229 | 230 | issue = submit_email( |
|
230 | 231 | 'ticket_by_unknown_user.eml', |
|
231 | 232 | :issue => {:project => 'ecookbook'}, |
|
232 | 233 | :unknown_user => 'accept' |
|
233 | 234 | ) |
|
234 | 235 | assert issue.is_a?(Issue) |
|
235 | 236 | assert issue.author.anonymous? |
|
236 | 237 | end |
|
237 | 238 | end |
|
238 | 239 | |
|
239 | 240 | def test_add_issue_by_anonymous_user_with_no_from_address |
|
240 | 241 | Role.anonymous.add_permission!(:add_issues) |
|
241 | 242 | assert_no_difference 'User.count' do |
|
242 | 243 | issue = submit_email( |
|
243 | 244 | 'ticket_by_empty_user.eml', |
|
244 | 245 | :issue => {:project => 'ecookbook'}, |
|
245 | 246 | :unknown_user => 'accept' |
|
246 | 247 | ) |
|
247 | 248 | assert issue.is_a?(Issue) |
|
248 | 249 | assert issue.author.anonymous? |
|
249 | 250 | end |
|
250 | 251 | end |
|
251 | 252 | |
|
252 | 253 | def test_add_issue_by_anonymous_user_on_private_project |
|
253 | 254 | Role.anonymous.add_permission!(:add_issues) |
|
254 | 255 | assert_no_difference 'User.count' do |
|
255 | 256 | assert_no_difference 'Issue.count' do |
|
256 | 257 | assert_equal false, |
|
257 | 258 | submit_email( |
|
258 | 259 | 'ticket_by_unknown_user.eml', |
|
259 | 260 | :issue => {:project => 'onlinestore'}, |
|
260 | 261 | :unknown_user => 'accept' |
|
261 | 262 | ) |
|
262 | 263 | end |
|
263 | 264 | end |
|
264 | 265 | end |
|
265 | 266 | |
|
266 | 267 | def test_add_issue_by_anonymous_user_on_private_project_without_permission_check |
|
268 | lft1 = new_issue_lft | |
|
267 | 269 | assert_no_difference 'User.count' do |
|
268 | 270 | assert_difference 'Issue.count' do |
|
269 | 271 | issue = submit_email( |
|
270 | 272 | 'ticket_by_unknown_user.eml', |
|
271 | 273 | :issue => {:project => 'onlinestore'}, |
|
272 | 274 | :no_permission_check => '1', |
|
273 | 275 | :unknown_user => 'accept' |
|
274 | 276 | ) |
|
275 | 277 | assert issue.is_a?(Issue) |
|
276 | 278 | assert issue.author.anonymous? |
|
277 | 279 | assert !issue.project.is_public? |
|
278 |
assert_equal [issue.id, 1, |
|
|
280 | assert_equal [issue.id, lft1, lft1 + 1], [issue.root_id, issue.lft, issue.rgt] | |
|
279 | 281 | end |
|
280 | 282 | end |
|
281 | 283 | end |
|
282 | 284 | |
|
283 | 285 | def test_add_issue_by_created_user |
|
284 | 286 | Setting.default_language = 'en' |
|
285 | 287 | assert_difference 'User.count' do |
|
286 | 288 | issue = submit_email( |
|
287 | 289 | 'ticket_by_unknown_user.eml', |
|
288 | 290 | :issue => {:project => 'ecookbook'}, |
|
289 | 291 | :unknown_user => 'create' |
|
290 | 292 | ) |
|
291 | 293 | assert issue.is_a?(Issue) |
|
292 | 294 | assert issue.author.active? |
|
293 | 295 | assert_equal 'john.doe@somenet.foo', issue.author.mail |
|
294 | 296 | assert_equal 'John', issue.author.firstname |
|
295 | 297 | assert_equal 'Doe', issue.author.lastname |
|
296 | 298 | |
|
297 | 299 | # account information |
|
298 | 300 | email = ActionMailer::Base.deliveries.first |
|
299 | 301 | assert_not_nil email |
|
300 | 302 | assert email.subject.include?('account activation') |
|
301 | 303 | login = mail_body(email).match(/\* Login: (.*)$/)[1].strip |
|
302 | 304 | password = mail_body(email).match(/\* Password: (.*)$/)[1].strip |
|
303 | 305 | assert_equal issue.author, User.try_to_login(login, password) |
|
304 | 306 | end |
|
305 | 307 | end |
|
306 | 308 | |
|
307 | 309 | def test_created_user_should_be_added_to_groups |
|
308 | 310 | group1 = Group.generate! |
|
309 | 311 | group2 = Group.generate! |
|
310 | 312 | |
|
311 | 313 | assert_difference 'User.count' do |
|
312 | 314 | submit_email( |
|
313 | 315 | 'ticket_by_unknown_user.eml', |
|
314 | 316 | :issue => {:project => 'ecookbook'}, |
|
315 | 317 | :unknown_user => 'create', |
|
316 | 318 | :default_group => "#{group1.name},#{group2.name}" |
|
317 | 319 | ) |
|
318 | 320 | end |
|
319 | 321 | user = User.order('id DESC').first |
|
320 | 322 | assert_same_elements [group1, group2], user.groups |
|
321 | 323 | end |
|
322 | 324 | |
|
323 | 325 | def test_created_user_should_not_receive_account_information_with_no_account_info_option |
|
324 | 326 | assert_difference 'User.count' do |
|
325 | 327 | submit_email( |
|
326 | 328 | 'ticket_by_unknown_user.eml', |
|
327 | 329 | :issue => {:project => 'ecookbook'}, |
|
328 | 330 | :unknown_user => 'create', |
|
329 | 331 | :no_account_notice => '1' |
|
330 | 332 | ) |
|
331 | 333 | end |
|
332 | 334 | |
|
333 | 335 | # only 1 email for the new issue notification |
|
334 | 336 | assert_equal 1, ActionMailer::Base.deliveries.size |
|
335 | 337 | email = ActionMailer::Base.deliveries.first |
|
336 | 338 | assert_include 'Ticket by unknown user', email.subject |
|
337 | 339 | end |
|
338 | 340 | |
|
339 | 341 | def test_created_user_should_have_mail_notification_to_none_with_no_notification_option |
|
340 | 342 | assert_difference 'User.count' do |
|
341 | 343 | submit_email( |
|
342 | 344 | 'ticket_by_unknown_user.eml', |
|
343 | 345 | :issue => {:project => 'ecookbook'}, |
|
344 | 346 | :unknown_user => 'create', |
|
345 | 347 | :no_notification => '1' |
|
346 | 348 | ) |
|
347 | 349 | end |
|
348 | 350 | user = User.order('id DESC').first |
|
349 | 351 | assert_equal 'none', user.mail_notification |
|
350 | 352 | end |
|
351 | 353 | |
|
352 | 354 | def test_add_issue_without_from_header |
|
353 | 355 | Role.anonymous.add_permission!(:add_issues) |
|
354 | 356 | assert_equal false, submit_email('ticket_without_from_header.eml') |
|
355 | 357 | end |
|
356 | 358 | |
|
357 | 359 | def test_add_issue_with_invalid_attributes |
|
358 | 360 | issue = submit_email( |
|
359 | 361 | 'ticket_with_invalid_attributes.eml', |
|
360 | 362 | :allow_override => 'tracker,category,priority' |
|
361 | 363 | ) |
|
362 | 364 | assert issue.is_a?(Issue) |
|
363 | 365 | assert !issue.new_record? |
|
364 | 366 | issue.reload |
|
365 | 367 | assert_nil issue.assigned_to |
|
366 | 368 | assert_nil issue.start_date |
|
367 | 369 | assert_nil issue.due_date |
|
368 | 370 | assert_equal 0, issue.done_ratio |
|
369 | 371 | assert_equal 'Normal', issue.priority.to_s |
|
370 | 372 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
371 | 373 | end |
|
372 | 374 | |
|
373 | 375 | def test_add_issue_with_invalid_project_should_be_assigned_to_default_project |
|
374 | 376 | issue = submit_email('ticket_on_given_project.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'project') do |email| |
|
375 | 377 | email.gsub!(/^Project:.+$/, 'Project: invalid') |
|
376 | 378 | end |
|
377 | 379 | assert issue.is_a?(Issue) |
|
378 | 380 | assert !issue.new_record? |
|
379 | 381 | assert_equal 'ecookbook', issue.project.identifier |
|
380 | 382 | end |
|
381 | 383 | |
|
382 | 384 | def test_add_issue_with_localized_attributes |
|
383 | 385 | User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr' |
|
384 | 386 | issue = submit_email( |
|
385 | 387 | 'ticket_with_localized_attributes.eml', |
|
386 | 388 | :allow_override => 'tracker,category,priority' |
|
387 | 389 | ) |
|
388 | 390 | assert issue.is_a?(Issue) |
|
389 | 391 | assert !issue.new_record? |
|
390 | 392 | issue.reload |
|
391 | 393 | assert_equal 'New ticket on a given project', issue.subject |
|
392 | 394 | assert_equal User.find_by_login('jsmith'), issue.author |
|
393 | 395 | assert_equal Project.find(2), issue.project |
|
394 | 396 | assert_equal 'Feature request', issue.tracker.to_s |
|
395 | 397 | assert_equal 'Stock management', issue.category.to_s |
|
396 | 398 | assert_equal 'Urgent', issue.priority.to_s |
|
397 | 399 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
398 | 400 | end |
|
399 | 401 | |
|
400 | 402 | def test_add_issue_with_japanese_keywords |
|
401 | 403 | ja_dev = "\xe9\x96\x8b\xe7\x99\xba" |
|
402 | 404 | ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding) |
|
403 | 405 | tracker = Tracker.create!(:name => ja_dev) |
|
404 | 406 | Project.find(1).trackers << tracker |
|
405 | 407 | issue = submit_email( |
|
406 | 408 | 'japanese_keywords_iso_2022_jp.eml', |
|
407 | 409 | :issue => {:project => 'ecookbook'}, |
|
408 | 410 | :allow_override => 'tracker' |
|
409 | 411 | ) |
|
410 | 412 | assert_kind_of Issue, issue |
|
411 | 413 | assert_equal tracker, issue.tracker |
|
412 | 414 | end |
|
413 | 415 | |
|
414 | 416 | def test_add_issue_from_apple_mail |
|
415 | 417 | issue = submit_email( |
|
416 | 418 | 'apple_mail_with_attachment.eml', |
|
417 | 419 | :issue => {:project => 'ecookbook'} |
|
418 | 420 | ) |
|
419 | 421 | assert_kind_of Issue, issue |
|
420 | 422 | assert_equal 1, issue.attachments.size |
|
421 | 423 | |
|
422 | 424 | attachment = issue.attachments.first |
|
423 | 425 | assert_equal 'paella.jpg', attachment.filename |
|
424 | 426 | assert_equal 10790, attachment.filesize |
|
425 | 427 | assert File.exist?(attachment.diskfile) |
|
426 | 428 | assert_equal 10790, File.size(attachment.diskfile) |
|
427 | 429 | assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest |
|
428 | 430 | end |
|
429 | 431 | |
|
430 | 432 | def test_thunderbird_with_attachment_ja |
|
431 | 433 | issue = submit_email( |
|
432 | 434 | 'thunderbird_with_attachment_ja.eml', |
|
433 | 435 | :issue => {:project => 'ecookbook'} |
|
434 | 436 | ) |
|
435 | 437 | assert_kind_of Issue, issue |
|
436 | 438 | assert_equal 1, issue.attachments.size |
|
437 | 439 | ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt" |
|
438 | 440 | ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) |
|
439 | 441 | attachment = issue.attachments.first |
|
440 | 442 | assert_equal ja, attachment.filename |
|
441 | 443 | assert_equal 5, attachment.filesize |
|
442 | 444 | assert File.exist?(attachment.diskfile) |
|
443 | 445 | assert_equal 5, File.size(attachment.diskfile) |
|
444 | 446 | assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest |
|
445 | 447 | end |
|
446 | 448 | |
|
447 | 449 | def test_gmail_with_attachment_ja |
|
448 | 450 | issue = submit_email( |
|
449 | 451 | 'gmail_with_attachment_ja.eml', |
|
450 | 452 | :issue => {:project => 'ecookbook'} |
|
451 | 453 | ) |
|
452 | 454 | assert_kind_of Issue, issue |
|
453 | 455 | assert_equal 1, issue.attachments.size |
|
454 | 456 | ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt" |
|
455 | 457 | ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) |
|
456 | 458 | attachment = issue.attachments.first |
|
457 | 459 | assert_equal ja, attachment.filename |
|
458 | 460 | assert_equal 5, attachment.filesize |
|
459 | 461 | assert File.exist?(attachment.diskfile) |
|
460 | 462 | assert_equal 5, File.size(attachment.diskfile) |
|
461 | 463 | assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest |
|
462 | 464 | end |
|
463 | 465 | |
|
464 | 466 | def test_thunderbird_with_attachment_latin1 |
|
465 | 467 | issue = submit_email( |
|
466 | 468 | 'thunderbird_with_attachment_iso-8859-1.eml', |
|
467 | 469 | :issue => {:project => 'ecookbook'} |
|
468 | 470 | ) |
|
469 | 471 | assert_kind_of Issue, issue |
|
470 | 472 | assert_equal 1, issue.attachments.size |
|
471 | 473 | u = "" |
|
472 | 474 | u.force_encoding('UTF-8') if u.respond_to?(:force_encoding) |
|
473 | 475 | u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc" |
|
474 | 476 | u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding) |
|
475 | 477 | 11.times { u << u1 } |
|
476 | 478 | attachment = issue.attachments.first |
|
477 | 479 | assert_equal "#{u}.png", attachment.filename |
|
478 | 480 | assert_equal 130, attachment.filesize |
|
479 | 481 | assert File.exist?(attachment.diskfile) |
|
480 | 482 | assert_equal 130, File.size(attachment.diskfile) |
|
481 | 483 | assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest |
|
482 | 484 | end |
|
483 | 485 | |
|
484 | 486 | def test_gmail_with_attachment_latin1 |
|
485 | 487 | issue = submit_email( |
|
486 | 488 | 'gmail_with_attachment_iso-8859-1.eml', |
|
487 | 489 | :issue => {:project => 'ecookbook'} |
|
488 | 490 | ) |
|
489 | 491 | assert_kind_of Issue, issue |
|
490 | 492 | assert_equal 1, issue.attachments.size |
|
491 | 493 | u = "" |
|
492 | 494 | u.force_encoding('UTF-8') if u.respond_to?(:force_encoding) |
|
493 | 495 | u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc" |
|
494 | 496 | u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding) |
|
495 | 497 | 11.times { u << u1 } |
|
496 | 498 | attachment = issue.attachments.first |
|
497 | 499 | assert_equal "#{u}.txt", attachment.filename |
|
498 | 500 | assert_equal 5, attachment.filesize |
|
499 | 501 | assert File.exist?(attachment.diskfile) |
|
500 | 502 | assert_equal 5, File.size(attachment.diskfile) |
|
501 | 503 | assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest |
|
502 | 504 | end |
|
503 | 505 | |
|
504 | 506 | def test_multiple_inline_text_parts_should_be_appended_to_issue_description |
|
505 | 507 | issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'}) |
|
506 | 508 | assert_include 'first', issue.description |
|
507 | 509 | assert_include 'second', issue.description |
|
508 | 510 | assert_include 'third', issue.description |
|
509 | 511 | end |
|
510 | 512 | |
|
511 | 513 | def test_attachment_text_part_should_be_added_as_issue_attachment |
|
512 | 514 | issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'}) |
|
513 | 515 | assert_not_include 'Plain text attachment', issue.description |
|
514 | 516 | attachment = issue.attachments.detect {|a| a.filename == 'textfile.txt'} |
|
515 | 517 | assert_not_nil attachment |
|
516 | 518 | assert_include 'Plain text attachment', File.read(attachment.diskfile) |
|
517 | 519 | end |
|
518 | 520 | |
|
519 | 521 | def test_add_issue_with_iso_8859_1_subject |
|
520 | 522 | issue = submit_email( |
|
521 | 523 | 'subject_as_iso-8859-1.eml', |
|
522 | 524 | :issue => {:project => 'ecookbook'} |
|
523 | 525 | ) |
|
524 | 526 | str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc..." |
|
525 | 527 | str.force_encoding('UTF-8') if str.respond_to?(:force_encoding) |
|
526 | 528 | assert_kind_of Issue, issue |
|
527 | 529 | assert_equal str, issue.subject |
|
528 | 530 | end |
|
529 | 531 | |
|
530 | 532 | def test_add_issue_with_japanese_subject |
|
531 | 533 | issue = submit_email( |
|
532 | 534 | 'subject_japanese_1.eml', |
|
533 | 535 | :issue => {:project => 'ecookbook'} |
|
534 | 536 | ) |
|
535 | 537 | assert_kind_of Issue, issue |
|
536 | 538 | ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88" |
|
537 | 539 | ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) |
|
538 | 540 | assert_equal ja, issue.subject |
|
539 | 541 | end |
|
540 | 542 | |
|
541 | 543 | def test_add_issue_with_korean_body |
|
542 | 544 | # Make sure mail bodies with a charset unknown to Ruby |
|
543 | 545 | # but known to the Mail gem 2.5.4 are handled correctly |
|
544 | 546 | kr = "\xEA\xB3\xA0\xEB\xA7\x99\xEC\x8A\xB5\xEB\x8B\x88\xEB\x8B\xA4." |
|
545 | 547 | if !kr.respond_to?(:force_encoding) |
|
546 | 548 | puts "\nOn Ruby 1.8, skip Korean encoding mail body test" |
|
547 | 549 | else |
|
548 | 550 | kr.force_encoding('UTF-8') |
|
549 | 551 | issue = submit_email( |
|
550 | 552 | 'body_ks_c_5601-1987.eml', |
|
551 | 553 | :issue => {:project => 'ecookbook'} |
|
552 | 554 | ) |
|
553 | 555 | assert_kind_of Issue, issue |
|
554 | 556 | assert_equal kr, issue.description |
|
555 | 557 | end |
|
556 | 558 | end |
|
557 | 559 | |
|
558 | 560 | def test_add_issue_with_no_subject_header |
|
559 | 561 | issue = submit_email( |
|
560 | 562 | 'no_subject_header.eml', |
|
561 | 563 | :issue => {:project => 'ecookbook'} |
|
562 | 564 | ) |
|
563 | 565 | assert_kind_of Issue, issue |
|
564 | 566 | assert_equal '(no subject)', issue.subject |
|
565 | 567 | end |
|
566 | 568 | |
|
567 | 569 | def test_add_issue_with_mixed_japanese_subject |
|
568 | 570 | issue = submit_email( |
|
569 | 571 | 'subject_japanese_2.eml', |
|
570 | 572 | :issue => {:project => 'ecookbook'} |
|
571 | 573 | ) |
|
572 | 574 | assert_kind_of Issue, issue |
|
573 | 575 | ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88" |
|
574 | 576 | ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) |
|
575 | 577 | assert_equal ja, issue.subject |
|
576 | 578 | end |
|
577 | 579 | |
|
578 | 580 | def test_should_ignore_emails_from_locked_users |
|
579 | 581 | User.find(2).lock! |
|
580 | 582 | |
|
581 | 583 | MailHandler.any_instance.expects(:dispatch).never |
|
582 | 584 | assert_no_difference 'Issue.count' do |
|
583 | 585 | assert_equal false, submit_email('ticket_on_given_project.eml') |
|
584 | 586 | end |
|
585 | 587 | end |
|
586 | 588 | |
|
587 | 589 | def test_should_ignore_emails_from_emission_address |
|
588 | 590 | Role.anonymous.add_permission!(:add_issues) |
|
589 | 591 | assert_no_difference 'User.count' do |
|
590 | 592 | assert_equal false, |
|
591 | 593 | submit_email( |
|
592 | 594 | 'ticket_from_emission_address.eml', |
|
593 | 595 | :issue => {:project => 'ecookbook'}, |
|
594 | 596 | :unknown_user => 'create' |
|
595 | 597 | ) |
|
596 | 598 | end |
|
597 | 599 | end |
|
598 | 600 | |
|
599 | 601 | def test_should_ignore_auto_replied_emails |
|
600 | 602 | MailHandler.any_instance.expects(:dispatch).never |
|
601 | 603 | [ |
|
602 | 604 | "X-Auto-Response-Suppress: OOF", |
|
603 | 605 | "Auto-Submitted: auto-replied", |
|
604 | 606 | "Auto-Submitted: Auto-Replied", |
|
605 | 607 | "Auto-Submitted: auto-generated" |
|
606 | 608 | ].each do |header| |
|
607 | 609 | raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) |
|
608 | 610 | raw = header + "\n" + raw |
|
609 | 611 | |
|
610 | 612 | assert_no_difference 'Issue.count' do |
|
611 | 613 | assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored" |
|
612 | 614 | end |
|
613 | 615 | end |
|
614 | 616 | end |
|
615 | 617 | |
|
616 | 618 | def test_add_issue_should_send_email_notification |
|
617 | 619 | Setting.notified_events = ['issue_added'] |
|
618 | 620 | ActionMailer::Base.deliveries.clear |
|
619 | 621 | # This email contains: 'Project: onlinestore' |
|
620 | 622 | issue = submit_email('ticket_on_given_project.eml') |
|
621 | 623 | assert issue.is_a?(Issue) |
|
622 | 624 | assert_equal 1, ActionMailer::Base.deliveries.size |
|
623 | 625 | end |
|
624 | 626 | |
|
625 | 627 | def test_update_issue |
|
626 | 628 | journal = submit_email('ticket_reply.eml') |
|
627 | 629 | assert journal.is_a?(Journal) |
|
628 | 630 | assert_equal User.find_by_login('jsmith'), journal.user |
|
629 | 631 | assert_equal Issue.find(2), journal.journalized |
|
630 | 632 | assert_match /This is reply/, journal.notes |
|
631 | 633 | assert_equal false, journal.private_notes |
|
632 | 634 | assert_equal 'Feature request', journal.issue.tracker.name |
|
633 | 635 | end |
|
634 | 636 | |
|
635 | 637 | def test_update_issue_with_attribute_changes |
|
636 | 638 | # This email contains: 'Status: Resolved' |
|
637 | 639 | journal = submit_email('ticket_reply_with_status.eml') |
|
638 | 640 | assert journal.is_a?(Journal) |
|
639 | 641 | issue = Issue.find(journal.issue.id) |
|
640 | 642 | assert_equal User.find_by_login('jsmith'), journal.user |
|
641 | 643 | assert_equal Issue.find(2), journal.journalized |
|
642 | 644 | assert_match /This is reply/, journal.notes |
|
643 | 645 | assert_equal 'Feature request', journal.issue.tracker.name |
|
644 | 646 | assert_equal IssueStatus.find_by_name("Resolved"), issue.status |
|
645 | 647 | assert_equal '2010-01-01', issue.start_date.to_s |
|
646 | 648 | assert_equal '2010-12-31', issue.due_date.to_s |
|
647 | 649 | assert_equal User.find_by_login('jsmith'), issue.assigned_to |
|
648 | 650 | assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value |
|
649 | 651 | # keywords should be removed from the email body |
|
650 | 652 | assert !journal.notes.match(/^Status:/i) |
|
651 | 653 | assert !journal.notes.match(/^Start Date:/i) |
|
652 | 654 | end |
|
653 | 655 | |
|
654 | 656 | def test_update_issue_with_attachment |
|
655 | 657 | assert_difference 'Journal.count' do |
|
656 | 658 | assert_difference 'JournalDetail.count' do |
|
657 | 659 | assert_difference 'Attachment.count' do |
|
658 | 660 | assert_no_difference 'Issue.count' do |
|
659 | 661 | journal = submit_email('ticket_with_attachment.eml') do |raw| |
|
660 | 662 | raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories' |
|
661 | 663 | end |
|
662 | 664 | end |
|
663 | 665 | end |
|
664 | 666 | end |
|
665 | 667 | end |
|
666 | 668 | journal = Journal.order('id DESC').first |
|
667 | 669 | assert_equal Issue.find(2), journal.journalized |
|
668 | 670 | assert_equal 1, journal.details.size |
|
669 | 671 | |
|
670 | 672 | detail = journal.details.first |
|
671 | 673 | assert_equal 'attachment', detail.property |
|
672 | 674 | assert_equal 'Paella.jpg', detail.value |
|
673 | 675 | end |
|
674 | 676 | |
|
675 | 677 | def test_update_issue_should_send_email_notification |
|
676 | 678 | ActionMailer::Base.deliveries.clear |
|
677 | 679 | journal = submit_email('ticket_reply.eml') |
|
678 | 680 | assert journal.is_a?(Journal) |
|
679 | 681 | assert_equal 1, ActionMailer::Base.deliveries.size |
|
680 | 682 | end |
|
681 | 683 | |
|
682 | 684 | def test_update_issue_should_not_set_defaults |
|
683 | 685 | journal = submit_email( |
|
684 | 686 | 'ticket_reply.eml', |
|
685 | 687 | :issue => {:tracker => 'Support request', :priority => 'High'} |
|
686 | 688 | ) |
|
687 | 689 | assert journal.is_a?(Journal) |
|
688 | 690 | assert_match /This is reply/, journal.notes |
|
689 | 691 | assert_equal 'Feature request', journal.issue.tracker.name |
|
690 | 692 | assert_equal 'Normal', journal.issue.priority.name |
|
691 | 693 | end |
|
692 | 694 | |
|
693 | 695 | def test_replying_to_a_private_note_should_add_reply_as_private |
|
694 | 696 | private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2) |
|
695 | 697 | |
|
696 | 698 | assert_difference 'Journal.count' do |
|
697 | 699 | journal = submit_email('ticket_reply.eml') do |email| |
|
698 | 700 | email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>" |
|
699 | 701 | end |
|
700 | 702 | |
|
701 | 703 | assert_kind_of Journal, journal |
|
702 | 704 | assert_match /This is reply/, journal.notes |
|
703 | 705 | assert_equal true, journal.private_notes |
|
704 | 706 | end |
|
705 | 707 | end |
|
706 | 708 | |
|
707 | 709 | def test_reply_to_a_message |
|
708 | 710 | m = submit_email('message_reply.eml') |
|
709 | 711 | assert m.is_a?(Message) |
|
710 | 712 | assert !m.new_record? |
|
711 | 713 | m.reload |
|
712 | 714 | assert_equal 'Reply via email', m.subject |
|
713 | 715 | # The email replies to message #2 which is part of the thread of message #1 |
|
714 | 716 | assert_equal Message.find(1), m.parent |
|
715 | 717 | end |
|
716 | 718 | |
|
717 | 719 | def test_reply_to_a_message_by_subject |
|
718 | 720 | m = submit_email('message_reply_by_subject.eml') |
|
719 | 721 | assert m.is_a?(Message) |
|
720 | 722 | assert !m.new_record? |
|
721 | 723 | m.reload |
|
722 | 724 | assert_equal 'Reply to the first post', m.subject |
|
723 | 725 | assert_equal Message.find(1), m.parent |
|
724 | 726 | end |
|
725 | 727 | |
|
726 | 728 | def test_should_strip_tags_of_html_only_emails |
|
727 | 729 | issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'}) |
|
728 | 730 | assert issue.is_a?(Issue) |
|
729 | 731 | assert !issue.new_record? |
|
730 | 732 | issue.reload |
|
731 | 733 | assert_equal 'HTML email', issue.subject |
|
732 | 734 | assert_equal 'This is a html-only email.', issue.description |
|
733 | 735 | end |
|
734 | 736 | |
|
735 | 737 | test "truncate emails with no setting should add the entire email into the issue" do |
|
736 | 738 | with_settings :mail_handler_body_delimiters => '' do |
|
737 | 739 | issue = submit_email('ticket_on_given_project.eml') |
|
738 | 740 | assert_issue_created(issue) |
|
739 | 741 | assert issue.description.include?('---') |
|
740 | 742 | assert issue.description.include?('This paragraph is after the delimiter') |
|
741 | 743 | end |
|
742 | 744 | end |
|
743 | 745 | |
|
744 | 746 | test "truncate emails with a single string should truncate the email at the delimiter for the issue" do |
|
745 | 747 | with_settings :mail_handler_body_delimiters => '---' do |
|
746 | 748 | issue = submit_email('ticket_on_given_project.eml') |
|
747 | 749 | assert_issue_created(issue) |
|
748 | 750 | assert issue.description.include?('This paragraph is before delimiters') |
|
749 | 751 | assert issue.description.include?('--- This line starts with a delimiter') |
|
750 | 752 | assert !issue.description.match(/^---$/) |
|
751 | 753 | assert !issue.description.include?('This paragraph is after the delimiter') |
|
752 | 754 | end |
|
753 | 755 | end |
|
754 | 756 | |
|
755 | 757 | test "truncate emails with a single quoted reply should truncate the email at the delimiter with the quoted reply symbols (>)" do |
|
756 | 758 | with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do |
|
757 | 759 | journal = submit_email('issue_update_with_quoted_reply_above.eml') |
|
758 | 760 | assert journal.is_a?(Journal) |
|
759 | 761 | assert journal.notes.include?('An update to the issue by the sender.') |
|
760 | 762 | assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---")) |
|
761 | 763 | assert !journal.notes.include?('Looks like the JSON api for projects was missed.') |
|
762 | 764 | end |
|
763 | 765 | end |
|
764 | 766 | |
|
765 | 767 | test "truncate emails with multiple quoted replies should truncate the email at the delimiter with the quoted reply symbols (>)" do |
|
766 | 768 | with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do |
|
767 | 769 | journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml') |
|
768 | 770 | assert journal.is_a?(Journal) |
|
769 | 771 | assert journal.notes.include?('An update to the issue by the sender.') |
|
770 | 772 | assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---")) |
|
771 | 773 | assert !journal.notes.include?('Looks like the JSON api for projects was missed.') |
|
772 | 774 | end |
|
773 | 775 | end |
|
774 | 776 | |
|
775 | 777 | test "truncate emails with multiple strings should truncate the email at the first delimiter found (BREAK)" do |
|
776 | 778 | with_settings :mail_handler_body_delimiters => "---\nBREAK" do |
|
777 | 779 | issue = submit_email('ticket_on_given_project.eml') |
|
778 | 780 | assert_issue_created(issue) |
|
779 | 781 | assert issue.description.include?('This paragraph is before delimiters') |
|
780 | 782 | assert !issue.description.include?('BREAK') |
|
781 | 783 | assert !issue.description.include?('This paragraph is between delimiters') |
|
782 | 784 | assert !issue.description.match(/^---$/) |
|
783 | 785 | assert !issue.description.include?('This paragraph is after the delimiter') |
|
784 | 786 | end |
|
785 | 787 | end |
|
786 | 788 | |
|
787 | 789 | def test_attachments_that_match_mail_handler_excluded_filenames_should_be_ignored |
|
788 | 790 | with_settings :mail_handler_excluded_filenames => '*.vcf, *.jpg' do |
|
789 | 791 | issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) |
|
790 | 792 | assert issue.is_a?(Issue) |
|
791 | 793 | assert !issue.new_record? |
|
792 | 794 | assert_equal 0, issue.reload.attachments.size |
|
793 | 795 | end |
|
794 | 796 | end |
|
795 | 797 | |
|
796 | 798 | def test_attachments_that_do_not_match_mail_handler_excluded_filenames_should_be_attached |
|
797 | 799 | with_settings :mail_handler_excluded_filenames => '*.vcf, *.gif' do |
|
798 | 800 | issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) |
|
799 | 801 | assert issue.is_a?(Issue) |
|
800 | 802 | assert !issue.new_record? |
|
801 | 803 | assert_equal 1, issue.reload.attachments.size |
|
802 | 804 | end |
|
803 | 805 | end |
|
804 | 806 | |
|
805 | 807 | def test_email_with_long_subject_line |
|
806 | 808 | issue = submit_email('ticket_with_long_subject.eml') |
|
807 | 809 | assert issue.is_a?(Issue) |
|
808 | 810 | 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] |
|
809 | 811 | end |
|
810 | 812 | |
|
811 | 813 | def test_new_user_from_attributes_should_return_valid_user |
|
812 | 814 | to_test = { |
|
813 | 815 | # [address, name] => [login, firstname, lastname] |
|
814 | 816 | ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'], |
|
815 | 817 | ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'], |
|
816 | 818 | ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'], |
|
817 | 819 | ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'], |
|
818 | 820 | ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'], |
|
819 | 821 | ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh'] |
|
820 | 822 | } |
|
821 | 823 | |
|
822 | 824 | to_test.each do |attrs, expected| |
|
823 | 825 | user = MailHandler.new_user_from_attributes(attrs.first, attrs.last) |
|
824 | 826 | |
|
825 | 827 | assert user.valid?, user.errors.full_messages.to_s |
|
826 | 828 | assert_equal attrs.first, user.mail |
|
827 | 829 | assert_equal expected[0], user.login |
|
828 | 830 | assert_equal expected[1], user.firstname |
|
829 | 831 | assert_equal expected[2], user.lastname |
|
830 | 832 | assert_equal 'only_my_events', user.mail_notification |
|
831 | 833 | end |
|
832 | 834 | end |
|
833 | 835 | |
|
834 | 836 | def test_new_user_from_attributes_should_use_default_login_if_invalid |
|
835 | 837 | user = MailHandler.new_user_from_attributes('foo+bar@example.net') |
|
836 | 838 | assert user.valid? |
|
837 | 839 | assert user.login =~ /^user[a-f0-9]+$/ |
|
838 | 840 | assert_equal 'foo+bar@example.net', user.mail |
|
839 | 841 | end |
|
840 | 842 | |
|
841 | 843 | def test_new_user_with_utf8_encoded_fullname_should_be_decoded |
|
842 | 844 | assert_difference 'User.count' do |
|
843 | 845 | issue = submit_email( |
|
844 | 846 | 'fullname_of_sender_as_utf8_encoded.eml', |
|
845 | 847 | :issue => {:project => 'ecookbook'}, |
|
846 | 848 | :unknown_user => 'create' |
|
847 | 849 | ) |
|
848 | 850 | end |
|
849 | 851 | user = User.order('id DESC').first |
|
850 | 852 | assert_equal "foo@example.org", user.mail |
|
851 | 853 | str1 = "\xc3\x84\xc3\xa4" |
|
852 | 854 | str2 = "\xc3\x96\xc3\xb6" |
|
853 | 855 | str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding) |
|
854 | 856 | str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding) |
|
855 | 857 | assert_equal str1, user.firstname |
|
856 | 858 | assert_equal str2, user.lastname |
|
857 | 859 | end |
|
858 | 860 | |
|
859 | 861 | def test_extract_options_from_env_should_return_options |
|
860 | 862 | options = MailHandler.extract_options_from_env({ |
|
861 | 863 | 'tracker' => 'defect', |
|
862 | 864 | 'project' => 'foo', |
|
863 | 865 | 'unknown_user' => 'create' |
|
864 | 866 | }) |
|
865 | 867 | |
|
866 | 868 | assert_equal({ |
|
867 | 869 | :issue => {:tracker => 'defect', :project => 'foo'}, |
|
868 | 870 | :unknown_user => 'create' |
|
869 | 871 | }, options) |
|
870 | 872 | end |
|
871 | 873 | |
|
872 | 874 | private |
|
873 | 875 | |
|
874 | 876 | def submit_email(filename, options={}) |
|
875 | 877 | raw = IO.read(File.join(FIXTURES_PATH, filename)) |
|
876 | 878 | yield raw if block_given? |
|
877 | 879 | MailHandler.receive(raw, options) |
|
878 | 880 | end |
|
879 | 881 | |
|
880 | 882 | def assert_issue_created(issue) |
|
881 | 883 | assert issue.is_a?(Issue) |
|
882 | 884 | assert !issue.new_record? |
|
883 | 885 | issue.reload |
|
884 | 886 | end |
|
885 | 887 | end |
General Comments 0
You need to be logged in to leave comments.
Login now