##// END OF EJS Templates
Adds a template for API error messages so that it does not depend on AR::Errors serialization....
Jean-Philippe Lang -
r8974:dc50edae5e27
parent child
Show More
@@ -0,0 +1,5
1 api.array :errors do
2 @error_messages.each do |message|
3 api.error message
4 end
5 end
@@ -1,541 +1,534
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 'uri'
19 19 require 'cgi'
20 20
21 21 class Unauthorized < Exception; end
22 22
23 23 class ApplicationController < ActionController::Base
24 24 include Redmine::I18n
25 25
26 26 layout 'base'
27 27 exempt_from_layout 'builder', 'rsb'
28 28
29 29 protect_from_forgery
30 30 def handle_unverified_request
31 31 super
32 32 cookies.delete(:autologin)
33 33 end
34 34 # Remove broken cookie after upgrade from 0.8.x (#4292)
35 35 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
36 36 # TODO: remove it when Rails is fixed
37 37 before_filter :delete_broken_cookies
38 38 def delete_broken_cookies
39 39 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
40 40 cookies.delete '_redmine_session'
41 41 redirect_to home_path
42 42 return false
43 43 end
44 44 end
45 45
46 46 # FIXME: Remove this when all of Rack and Rails have learned how to
47 47 # properly use encodings
48 48 before_filter :params_filter
49 49
50 50 def params_filter
51 51 if RUBY_VERSION >= '1.9' && defined?(Rails) && Rails::VERSION::MAJOR < 3
52 52 self.utf8nize!(params)
53 53 end
54 54 end
55 55
56 56 def utf8nize!(obj)
57 57 if obj.frozen?
58 58 obj
59 59 elsif obj.is_a? String
60 60 obj.respond_to?(:force_encoding) ? obj.force_encoding("UTF-8") : obj
61 61 elsif obj.is_a? Hash
62 62 obj.each {|k, v| obj[k] = self.utf8nize!(v)}
63 63 elsif obj.is_a? Array
64 64 obj.each {|v| self.utf8nize!(v)}
65 65 else
66 66 obj
67 67 end
68 68 end
69 69
70 70 before_filter :user_setup, :check_if_login_required, :set_localization
71 71 filter_parameter_logging :password
72 72
73 73 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
74 74 rescue_from ::Unauthorized, :with => :deny_access
75 75
76 76 include Redmine::Search::Controller
77 77 include Redmine::MenuManager::MenuController
78 78 helper Redmine::MenuManager::MenuHelper
79 79
80 80 Redmine::Scm::Base.all.each do |scm|
81 81 require_dependency "repository/#{scm.underscore}"
82 82 end
83 83
84 84 def user_setup
85 85 # Check the settings cache for each request
86 86 Setting.check_cache
87 87 # Find the current user
88 88 User.current = find_current_user
89 89 end
90 90
91 91 # Returns the current user or nil if no user is logged in
92 92 # and starts a session if needed
93 93 def find_current_user
94 94 if session[:user_id]
95 95 # existing session
96 96 (User.active.find(session[:user_id]) rescue nil)
97 97 elsif cookies[:autologin] && Setting.autologin?
98 98 # auto-login feature starts a new session
99 99 user = User.try_to_autologin(cookies[:autologin])
100 100 session[:user_id] = user.id if user
101 101 user
102 102 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
103 103 # RSS key authentication does not start a session
104 104 User.find_by_rss_key(params[:key])
105 105 elsif Setting.rest_api_enabled? && accept_api_auth?
106 106 if (key = api_key_from_request)
107 107 # Use API key
108 108 User.find_by_api_key(key)
109 109 else
110 110 # HTTP Basic, either username/password or API key/random
111 111 authenticate_with_http_basic do |username, password|
112 112 User.try_to_login(username, password) || User.find_by_api_key(username)
113 113 end
114 114 end
115 115 end
116 116 end
117 117
118 118 # Sets the logged in user
119 119 def logged_user=(user)
120 120 reset_session
121 121 if user && user.is_a?(User)
122 122 User.current = user
123 123 session[:user_id] = user.id
124 124 else
125 125 User.current = User.anonymous
126 126 end
127 127 end
128 128
129 129 # check if login is globally required to access the application
130 130 def check_if_login_required
131 131 # no check needed if user is already logged in
132 132 return true if User.current.logged?
133 133 require_login if Setting.login_required?
134 134 end
135 135
136 136 def set_localization
137 137 lang = nil
138 138 if User.current.logged?
139 139 lang = find_language(User.current.language)
140 140 end
141 141 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
142 142 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
143 143 if !accept_lang.blank?
144 144 accept_lang = accept_lang.downcase
145 145 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
146 146 end
147 147 end
148 148 lang ||= Setting.default_language
149 149 set_language_if_valid(lang)
150 150 end
151 151
152 152 def require_login
153 153 if !User.current.logged?
154 154 # Extract only the basic url parameters on non-GET requests
155 155 if request.get?
156 156 url = url_for(params)
157 157 else
158 158 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
159 159 end
160 160 respond_to do |format|
161 161 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
162 162 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
163 163 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
164 164 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
165 165 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
166 166 end
167 167 return false
168 168 end
169 169 true
170 170 end
171 171
172 172 def require_admin
173 173 return unless require_login
174 174 if !User.current.admin?
175 175 render_403
176 176 return false
177 177 end
178 178 true
179 179 end
180 180
181 181 def deny_access
182 182 User.current.logged? ? render_403 : require_login
183 183 end
184 184
185 185 # Authorize the user for the requested action
186 186 def authorize(ctrl = params[:controller], action = params[:action], global = false)
187 187 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
188 188 if allowed
189 189 true
190 190 else
191 191 if @project && @project.archived?
192 192 render_403 :message => :notice_not_authorized_archived_project
193 193 else
194 194 deny_access
195 195 end
196 196 end
197 197 end
198 198
199 199 # Authorize the user for the requested action outside a project
200 200 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
201 201 authorize(ctrl, action, global)
202 202 end
203 203
204 204 # Find project of id params[:id]
205 205 def find_project
206 206 @project = Project.find(params[:id])
207 207 rescue ActiveRecord::RecordNotFound
208 208 render_404
209 209 end
210 210
211 211 # Find project of id params[:project_id]
212 212 def find_project_by_project_id
213 213 @project = Project.find(params[:project_id])
214 214 rescue ActiveRecord::RecordNotFound
215 215 render_404
216 216 end
217 217
218 218 # Find a project based on params[:project_id]
219 219 # TODO: some subclasses override this, see about merging their logic
220 220 def find_optional_project
221 221 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
222 222 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
223 223 allowed ? true : deny_access
224 224 rescue ActiveRecord::RecordNotFound
225 225 render_404
226 226 end
227 227
228 228 # Finds and sets @project based on @object.project
229 229 def find_project_from_association
230 230 render_404 unless @object.present?
231 231
232 232 @project = @object.project
233 233 end
234 234
235 235 def find_model_object
236 236 model = self.class.read_inheritable_attribute('model_object')
237 237 if model
238 238 @object = model.find(params[:id])
239 239 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
240 240 end
241 241 rescue ActiveRecord::RecordNotFound
242 242 render_404
243 243 end
244 244
245 245 def self.model_object(model)
246 246 write_inheritable_attribute('model_object', model)
247 247 end
248 248
249 249 # Filter for bulk issue operations
250 250 def find_issues
251 251 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
252 252 raise ActiveRecord::RecordNotFound if @issues.empty?
253 253 if @issues.detect {|issue| !issue.visible?}
254 254 deny_access
255 255 return
256 256 end
257 257 @projects = @issues.collect(&:project).compact.uniq
258 258 @project = @projects.first if @projects.size == 1
259 259 rescue ActiveRecord::RecordNotFound
260 260 render_404
261 261 end
262 262
263 263 # make sure that the user is a member of the project (or admin) if project is private
264 264 # used as a before_filter for actions that do not require any particular permission on the project
265 265 def check_project_privacy
266 266 if @project && @project.active?
267 267 if @project.visible?
268 268 true
269 269 else
270 270 deny_access
271 271 end
272 272 else
273 273 @project = nil
274 274 render_404
275 275 false
276 276 end
277 277 end
278 278
279 279 def back_url
280 280 params[:back_url] || request.env['HTTP_REFERER']
281 281 end
282 282
283 283 def redirect_back_or_default(default)
284 284 back_url = CGI.unescape(params[:back_url].to_s)
285 285 if !back_url.blank?
286 286 begin
287 287 uri = URI.parse(back_url)
288 288 # do not redirect user to another host or to the login or register page
289 289 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
290 290 redirect_to(back_url)
291 291 return
292 292 end
293 293 rescue URI::InvalidURIError
294 294 # redirect to default
295 295 end
296 296 end
297 297 redirect_to default
298 298 false
299 299 end
300 300
301 301 def render_403(options={})
302 302 @project = nil
303 303 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
304 304 return false
305 305 end
306 306
307 307 def render_404(options={})
308 308 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
309 309 return false
310 310 end
311 311
312 312 # Renders an error response
313 313 def render_error(arg)
314 314 arg = {:message => arg} unless arg.is_a?(Hash)
315 315
316 316 @message = arg[:message]
317 317 @message = l(@message) if @message.is_a?(Symbol)
318 318 @status = arg[:status] || 500
319 319
320 320 respond_to do |format|
321 321 format.html {
322 322 render :template => 'common/error', :layout => use_layout, :status => @status
323 323 }
324 324 format.atom { head @status }
325 325 format.xml { head @status }
326 326 format.js { head @status }
327 327 format.json { head @status }
328 328 end
329 329 end
330 330
331 331 # Filter for actions that provide an API response
332 332 # but have no HTML representation for non admin users
333 333 def require_admin_or_api_request
334 334 return true if api_request?
335 335 if User.current.admin?
336 336 true
337 337 elsif User.current.logged?
338 338 render_error(:status => 406)
339 339 else
340 340 deny_access
341 341 end
342 342 end
343 343
344 344 # Picks which layout to use based on the request
345 345 #
346 346 # @return [boolean, string] name of the layout to use or false for no layout
347 347 def use_layout
348 348 request.xhr? ? false : 'base'
349 349 end
350 350
351 351 def invalid_authenticity_token
352 352 if api_request?
353 353 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
354 354 end
355 355 render_error "Invalid form authenticity token."
356 356 end
357 357
358 358 def render_feed(items, options={})
359 359 @items = items || []
360 360 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
361 361 @items = @items.slice(0, Setting.feeds_limit.to_i)
362 362 @title = options[:title] || Setting.app_title
363 363 render :template => "common/feed.atom", :layout => false,
364 364 :content_type => 'application/atom+xml'
365 365 end
366 366
367 367 # TODO: remove in Redmine 1.4
368 368 def self.accept_key_auth(*actions)
369 369 ActiveSupport::Deprecation.warn "ApplicationController.accept_key_auth is deprecated and will be removed in Redmine 1.4. Use accept_rss_auth (or accept_api_auth) instead."
370 370 accept_rss_auth(*actions)
371 371 end
372 372
373 373 # TODO: remove in Redmine 1.4
374 374 def accept_key_auth_actions
375 375 ActiveSupport::Deprecation.warn "ApplicationController.accept_key_auth_actions is deprecated and will be removed in Redmine 1.4. Use accept_rss_auth (or accept_api_auth) instead."
376 376 self.class.accept_rss_auth
377 377 end
378 378
379 379 def self.accept_rss_auth(*actions)
380 380 if actions.any?
381 381 write_inheritable_attribute('accept_rss_auth_actions', actions)
382 382 else
383 383 read_inheritable_attribute('accept_rss_auth_actions') || []
384 384 end
385 385 end
386 386
387 387 def accept_rss_auth?(action=action_name)
388 388 self.class.accept_rss_auth.include?(action.to_sym)
389 389 end
390 390
391 391 def self.accept_api_auth(*actions)
392 392 if actions.any?
393 393 write_inheritable_attribute('accept_api_auth_actions', actions)
394 394 else
395 395 read_inheritable_attribute('accept_api_auth_actions') || []
396 396 end
397 397 end
398 398
399 399 def accept_api_auth?(action=action_name)
400 400 self.class.accept_api_auth.include?(action.to_sym)
401 401 end
402 402
403 403 # Returns the number of objects that should be displayed
404 404 # on the paginated list
405 405 def per_page_option
406 406 per_page = nil
407 407 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
408 408 per_page = params[:per_page].to_s.to_i
409 409 session[:per_page] = per_page
410 410 elsif session[:per_page]
411 411 per_page = session[:per_page]
412 412 else
413 413 per_page = Setting.per_page_options_array.first || 25
414 414 end
415 415 per_page
416 416 end
417 417
418 418 # Returns offset and limit used to retrieve objects
419 419 # for an API response based on offset, limit and page parameters
420 420 def api_offset_and_limit(options=params)
421 421 if options[:offset].present?
422 422 offset = options[:offset].to_i
423 423 if offset < 0
424 424 offset = 0
425 425 end
426 426 end
427 427 limit = options[:limit].to_i
428 428 if limit < 1
429 429 limit = 25
430 430 elsif limit > 100
431 431 limit = 100
432 432 end
433 433 if offset.nil? && options[:page].present?
434 434 offset = (options[:page].to_i - 1) * limit
435 435 offset = 0 if offset < 0
436 436 end
437 437 offset ||= 0
438 438
439 439 [offset, limit]
440 440 end
441 441
442 442 # qvalues http header parser
443 443 # code taken from webrick
444 444 def parse_qvalues(value)
445 445 tmp = []
446 446 if value
447 447 parts = value.split(/,\s*/)
448 448 parts.each {|part|
449 449 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
450 450 val = m[1]
451 451 q = (m[2] or 1).to_f
452 452 tmp.push([val, q])
453 453 end
454 454 }
455 455 tmp = tmp.sort_by{|val, q| -q}
456 456 tmp.collect!{|val, q| val}
457 457 end
458 458 return tmp
459 459 rescue
460 460 nil
461 461 end
462 462
463 463 # Returns a string that can be used as filename value in Content-Disposition header
464 464 def filename_for_content_disposition(name)
465 465 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
466 466 end
467 467
468 468 def api_request?
469 469 %w(xml json).include? params[:format]
470 470 end
471 471
472 472 # Returns the API key present in the request
473 473 def api_key_from_request
474 474 if params[:key].present?
475 475 params[:key]
476 476 elsif request.headers["X-Redmine-API-Key"].present?
477 477 request.headers["X-Redmine-API-Key"]
478 478 end
479 479 end
480 480
481 481 # Renders a warning flash if obj has unsaved attachments
482 482 def render_attachment_warning_if_needed(obj)
483 483 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
484 484 end
485 485
486 486 # Sets the `flash` notice or error based the number of issues that did not save
487 487 #
488 488 # @param [Array, Issue] issues all of the saved and unsaved Issues
489 489 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
490 490 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
491 491 if unsaved_issue_ids.empty?
492 492 flash[:notice] = l(:notice_successful_update) unless issues.empty?
493 493 else
494 494 flash[:error] = l(:notice_failed_to_save_issues,
495 495 :count => unsaved_issue_ids.size,
496 496 :total => issues.size,
497 497 :ids => '#' + unsaved_issue_ids.join(', #'))
498 498 end
499 499 end
500 500
501 501 # Rescues an invalid query statement. Just in case...
502 502 def query_statement_invalid(exception)
503 503 logger.error "Query::StatementInvalid: #{exception.message}" if logger
504 504 session.delete(:query)
505 505 sort_clear if respond_to?(:sort_clear)
506 506 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
507 507 end
508 508
509 509 # Renders API response on validation failure
510 510 def render_validation_errors(object)
511 options = { :status => :unprocessable_entity, :layout => false }
512 options.merge!(case params[:format]
513 when 'xml'; { :xml => object.errors }
514 when 'json'; { :json => {'errors' => object.errors} } # ActiveResource client compliance
515 else
516 raise "Unknown format #{params[:format]} in #render_validation_errors"
517 end
518 )
519 render options
511 @error_messages = object.errors.full_messages
512 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => false
520 513 end
521 514
522 515 # Overrides #default_template so that the api template
523 516 # is used automatically if it exists
524 517 def default_template(action_name = self.action_name)
525 518 if api_request?
526 519 begin
527 520 return self.view_paths.find_template(default_template_name(action_name), 'api')
528 521 rescue ::ActionView::MissingTemplate
529 522 # the api template was not found
530 523 # fallback to the default behaviour
531 524 end
532 525 end
533 526 super
534 527 end
535 528
536 529 # Overrides #pick_layout so that #render with no arguments
537 530 # doesn't use the layout for api requests
538 531 def pick_layout(*args)
539 532 api_request? ? nil : super
540 533 end
541 534 end
@@ -1,778 +1,778
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 ApiTest::IssuesTest < ActionController::IntegrationTest
21 21 fixtures :projects,
22 22 :users,
23 23 :roles,
24 24 :members,
25 25 :member_roles,
26 26 :issues,
27 27 :issue_statuses,
28 28 :versions,
29 29 :trackers,
30 30 :projects_trackers,
31 31 :issue_categories,
32 32 :enabled_modules,
33 33 :enumerations,
34 34 :attachments,
35 35 :workflows,
36 36 :custom_fields,
37 37 :custom_values,
38 38 :custom_fields_projects,
39 39 :custom_fields_trackers,
40 40 :time_entries,
41 41 :journals,
42 42 :journal_details,
43 43 :queries,
44 44 :attachments
45 45
46 46 def setup
47 47 Setting.rest_api_enabled = '1'
48 48 end
49 49
50 50 context "/issues" do
51 51 # Use a private project to make sure auth is really working and not just
52 52 # only showing public issues.
53 53 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
54 54
55 55 should "contain metadata" do
56 56 get '/issues.xml'
57 57
58 58 assert_tag :tag => 'issues',
59 59 :attributes => {
60 60 :type => 'array',
61 61 :total_count => assigns(:issue_count),
62 62 :limit => 25,
63 63 :offset => 0
64 64 }
65 65 end
66 66
67 67 context "with offset and limit" do
68 68 should "use the params" do
69 69 get '/issues.xml?offset=2&limit=3'
70 70
71 71 assert_equal 3, assigns(:limit)
72 72 assert_equal 2, assigns(:offset)
73 73 assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}}
74 74 end
75 75 end
76 76
77 77 context "with nometa param" do
78 78 should "not contain metadata" do
79 79 get '/issues.xml?nometa=1'
80 80
81 81 assert_tag :tag => 'issues',
82 82 :attributes => {
83 83 :type => 'array',
84 84 :total_count => nil,
85 85 :limit => nil,
86 86 :offset => nil
87 87 }
88 88 end
89 89 end
90 90
91 91 context "with nometa header" do
92 92 should "not contain metadata" do
93 93 get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'}
94 94
95 95 assert_tag :tag => 'issues',
96 96 :attributes => {
97 97 :type => 'array',
98 98 :total_count => nil,
99 99 :limit => nil,
100 100 :offset => nil
101 101 }
102 102 end
103 103 end
104 104
105 105 context "with relations" do
106 106 should "display relations" do
107 107 get '/issues.xml?include=relations'
108 108
109 109 assert_response :success
110 110 assert_equal 'application/xml', @response.content_type
111 111 assert_tag 'relations',
112 112 :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '3'}},
113 113 :children => {:count => 1},
114 114 :child => {
115 115 :tag => 'relation',
116 116 :attributes => {:id => '2', :issue_id => '2', :issue_to_id => '3', :relation_type => 'relates'}
117 117 }
118 118 assert_tag 'relations',
119 119 :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '1'}},
120 120 :children => {:count => 0}
121 121 end
122 122 end
123 123
124 124 context "with invalid query params" do
125 125 should "return errors" do
126 126 get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}}
127 127
128 128 assert_response :unprocessable_entity
129 129 assert_equal 'application/xml', @response.content_type
130 130 assert_tag 'errors', :child => {:tag => 'error', :content => "Start date can't be blank"}
131 131 end
132 132 end
133 133
134 134 context "with custom field filter" do
135 135 should "show only issues with the custom field value" do
136 136 get '/issues.xml', { :set_filter => 1, :f => ['cf_1'], :op => {:cf_1 => '='}, :v => {:cf_1 => ['MySQL']}}
137 137
138 138 expected_ids = Issue.visible.all(
139 139 :include => :custom_values,
140 140 :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
141 141
142 142 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
143 143 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
144 144 end
145 145 end
146 146 end
147 147
148 148 context "with custom field filter (shorthand method)" do
149 149 should "show only issues with the custom field value" do
150 150 get '/issues.xml', { :cf_1 => 'MySQL' }
151 151
152 152 expected_ids = Issue.visible.all(
153 153 :include => :custom_values,
154 154 :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
155 155
156 156 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
157 157 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
158 158 end
159 159 end
160 160 end
161 161 end
162 162
163 163 context "/index.json" do
164 164 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
165 165 end
166 166
167 167 context "/index.xml with filter" do
168 168 should "show only issues with the status_id" do
169 169 get '/issues.xml?status_id=5'
170 170
171 171 expected_ids = Issue.visible.all(:conditions => {:status_id => 5}).map(&:id)
172 172
173 173 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
174 174 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
175 175 end
176 176 end
177 177 end
178 178
179 179 context "/index.json with filter" do
180 180 should "show only issues with the status_id" do
181 181 get '/issues.json?status_id=5'
182 182
183 183 json = ActiveSupport::JSON.decode(response.body)
184 184 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
185 185 assert_equal 3, status_ids_used.length
186 186 assert status_ids_used.all? {|id| id == 5 }
187 187 end
188 188
189 189 end
190 190
191 191 # Issue 6 is on a private project
192 192 context "/issues/6.xml" do
193 193 should_allow_api_authentication(:get, "/issues/6.xml")
194 194 end
195 195
196 196 context "/issues/6.json" do
197 197 should_allow_api_authentication(:get, "/issues/6.json")
198 198 end
199 199
200 200 context "GET /issues/:id" do
201 201 context "with journals" do
202 202 context ".xml" do
203 203 should "display journals" do
204 204 get '/issues/1.xml?include=journals'
205 205
206 206 assert_tag :tag => 'issue',
207 207 :child => {
208 208 :tag => 'journals',
209 209 :attributes => { :type => 'array' },
210 210 :child => {
211 211 :tag => 'journal',
212 212 :attributes => { :id => '1'},
213 213 :child => {
214 214 :tag => 'details',
215 215 :attributes => { :type => 'array' },
216 216 :child => {
217 217 :tag => 'detail',
218 218 :attributes => { :name => 'status_id' },
219 219 :child => {
220 220 :tag => 'old_value',
221 221 :content => '1',
222 222 :sibling => {
223 223 :tag => 'new_value',
224 224 :content => '2'
225 225 }
226 226 }
227 227 }
228 228 }
229 229 }
230 230 }
231 231 end
232 232 end
233 233 end
234 234
235 235 context "with custom fields" do
236 236 context ".xml" do
237 237 should "display custom fields" do
238 238 get '/issues/3.xml'
239 239
240 240 assert_tag :tag => 'issue',
241 241 :child => {
242 242 :tag => 'custom_fields',
243 243 :attributes => { :type => 'array' },
244 244 :child => {
245 245 :tag => 'custom_field',
246 246 :attributes => { :id => '1'},
247 247 :child => {
248 248 :tag => 'value',
249 249 :content => 'MySQL'
250 250 }
251 251 }
252 252 }
253 253
254 254 assert_nothing_raised do
255 255 Hash.from_xml(response.body).to_xml
256 256 end
257 257 end
258 258 end
259 259 end
260 260
261 261 context "with multi custom fields" do
262 262 setup do
263 263 field = CustomField.find(1)
264 264 field.update_attribute :multiple, true
265 265 issue = Issue.find(3)
266 266 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
267 267 issue.save!
268 268 end
269 269
270 270 context ".xml" do
271 271 should "display custom fields" do
272 272 get '/issues/3.xml'
273 273 assert_response :success
274 274 assert_tag :tag => 'issue',
275 275 :child => {
276 276 :tag => 'custom_fields',
277 277 :attributes => { :type => 'array' },
278 278 :child => {
279 279 :tag => 'custom_field',
280 280 :attributes => { :id => '1'},
281 281 :child => {
282 282 :tag => 'value',
283 283 :attributes => { :type => 'array' },
284 284 :children => { :count => 2 }
285 285 }
286 286 }
287 287 }
288 288
289 289 xml = Hash.from_xml(response.body)
290 290 custom_fields = xml['issue']['custom_fields']
291 291 assert_kind_of Array, custom_fields
292 292 field = custom_fields.detect {|f| f['id'] == '1'}
293 293 assert_kind_of Hash, field
294 294 assert_equal ['MySQL', 'Oracle'], field['value'].sort
295 295 end
296 296 end
297 297
298 298 context ".json" do
299 299 should "display custom fields" do
300 300 get '/issues/3.json'
301 301 assert_response :success
302 302 json = ActiveSupport::JSON.decode(response.body)
303 303 custom_fields = json['issue']['custom_fields']
304 304 assert_kind_of Array, custom_fields
305 305 field = custom_fields.detect {|f| f['id'] == 1}
306 306 assert_kind_of Hash, field
307 307 assert_equal ['MySQL', 'Oracle'], field['value'].sort
308 308 end
309 309 end
310 310 end
311 311
312 312 context "with empty value for multi custom field" do
313 313 setup do
314 314 field = CustomField.find(1)
315 315 field.update_attribute :multiple, true
316 316 issue = Issue.find(3)
317 317 issue.custom_field_values = {1 => ['']}
318 318 issue.save!
319 319 end
320 320
321 321 context ".xml" do
322 322 should "display custom fields" do
323 323 get '/issues/3.xml'
324 324 assert_response :success
325 325 assert_tag :tag => 'issue',
326 326 :child => {
327 327 :tag => 'custom_fields',
328 328 :attributes => { :type => 'array' },
329 329 :child => {
330 330 :tag => 'custom_field',
331 331 :attributes => { :id => '1'},
332 332 :child => {
333 333 :tag => 'value',
334 334 :attributes => { :type => 'array' },
335 335 :children => { :count => 0 }
336 336 }
337 337 }
338 338 }
339 339
340 340 xml = Hash.from_xml(response.body)
341 341 custom_fields = xml['issue']['custom_fields']
342 342 assert_kind_of Array, custom_fields
343 343 field = custom_fields.detect {|f| f['id'] == '1'}
344 344 assert_kind_of Hash, field
345 345 assert_equal [], field['value']
346 346 end
347 347 end
348 348
349 349 context ".json" do
350 350 should "display custom fields" do
351 351 get '/issues/3.json'
352 352 assert_response :success
353 353 json = ActiveSupport::JSON.decode(response.body)
354 354 custom_fields = json['issue']['custom_fields']
355 355 assert_kind_of Array, custom_fields
356 356 field = custom_fields.detect {|f| f['id'] == 1}
357 357 assert_kind_of Hash, field
358 358 assert_equal [], field['value'].sort
359 359 end
360 360 end
361 361 end
362 362
363 363 context "with attachments" do
364 364 context ".xml" do
365 365 should "display attachments" do
366 366 get '/issues/3.xml?include=attachments'
367 367
368 368 assert_tag :tag => 'issue',
369 369 :child => {
370 370 :tag => 'attachments',
371 371 :children => {:count => 5},
372 372 :child => {
373 373 :tag => 'attachment',
374 374 :child => {
375 375 :tag => 'filename',
376 376 :content => 'source.rb',
377 377 :sibling => {
378 378 :tag => 'content_url',
379 379 :content => 'http://www.example.com/attachments/download/4/source.rb'
380 380 }
381 381 }
382 382 }
383 383 }
384 384 end
385 385 end
386 386 end
387 387
388 388 context "with subtasks" do
389 389 setup do
390 390 @c1 = Issue.create!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :author_id => 1, :parent_issue_id => 1)
391 391 @c2 = Issue.create!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :author_id => 1, :parent_issue_id => 1)
392 392 @c3 = Issue.create!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :author_id => 1, :parent_issue_id => @c1.id)
393 393 end
394 394
395 395 context ".xml" do
396 396 should "display children" do
397 397 get '/issues/1.xml?include=children'
398 398
399 399 assert_tag :tag => 'issue',
400 400 :child => {
401 401 :tag => 'children',
402 402 :children => {:count => 2},
403 403 :child => {
404 404 :tag => 'issue',
405 405 :attributes => {:id => @c1.id.to_s},
406 406 :child => {
407 407 :tag => 'subject',
408 408 :content => 'child c1',
409 409 :sibling => {
410 410 :tag => 'children',
411 411 :children => {:count => 1},
412 412 :child => {
413 413 :tag => 'issue',
414 414 :attributes => {:id => @c3.id.to_s}
415 415 }
416 416 }
417 417 }
418 418 }
419 419 }
420 420 end
421 421
422 422 context ".json" do
423 423 should "display children" do
424 424 get '/issues/1.json?include=children'
425 425
426 426 json = ActiveSupport::JSON.decode(response.body)
427 427 assert_equal([
428 428 {
429 429 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
430 430 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
431 431 },
432 432 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
433 433 ],
434 434 json['issue']['children'])
435 435 end
436 436 end
437 437 end
438 438 end
439 439 end
440 440
441 441 context "POST /issues.xml" do
442 442 should_allow_api_authentication(:post,
443 443 '/issues.xml',
444 444 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
445 445 {:success_code => :created})
446 446
447 447 should "create an issue with the attributes" do
448 448 assert_difference('Issue.count') do
449 449 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, credentials('jsmith')
450 450 end
451 451
452 452 issue = Issue.first(:order => 'id DESC')
453 453 assert_equal 1, issue.project_id
454 454 assert_equal 2, issue.tracker_id
455 455 assert_equal 3, issue.status_id
456 456 assert_equal 'API test', issue.subject
457 457
458 458 assert_response :created
459 459 assert_equal 'application/xml', @response.content_type
460 460 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
461 461 end
462 462 end
463 463
464 464 context "POST /issues.xml with failure" do
465 465 should "have an errors tag" do
466 466 assert_no_difference('Issue.count') do
467 467 post '/issues.xml', {:issue => {:project_id => 1}}, credentials('jsmith')
468 468 end
469 469
470 470 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
471 471 end
472 472 end
473 473
474 474 context "POST /issues.json" do
475 475 should_allow_api_authentication(:post,
476 476 '/issues.json',
477 477 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
478 478 {:success_code => :created})
479 479
480 480 should "create an issue with the attributes" do
481 481 assert_difference('Issue.count') do
482 482 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, credentials('jsmith')
483 483 end
484 484
485 485 issue = Issue.first(:order => 'id DESC')
486 486 assert_equal 1, issue.project_id
487 487 assert_equal 2, issue.tracker_id
488 488 assert_equal 3, issue.status_id
489 489 assert_equal 'API test', issue.subject
490 490 end
491 491
492 492 end
493 493
494 494 context "POST /issues.json with failure" do
495 495 should "have an errors element" do
496 496 assert_no_difference('Issue.count') do
497 497 post '/issues.json', {:issue => {:project_id => 1}}, credentials('jsmith')
498 498 end
499 499
500 500 json = ActiveSupport::JSON.decode(response.body)
501 assert json['errors'].include?(['subject', "can't be blank"])
501 assert json['errors'].include?("Subject can't be blank")
502 502 end
503 503 end
504 504
505 505 # Issue 6 is on a private project
506 506 context "PUT /issues/6.xml" do
507 507 setup do
508 508 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
509 509 end
510 510
511 511 should_allow_api_authentication(:put,
512 512 '/issues/6.xml',
513 513 {:issue => {:subject => 'API update', :notes => 'A new note'}},
514 514 {:success_code => :ok})
515 515
516 516 should "not create a new issue" do
517 517 assert_no_difference('Issue.count') do
518 518 put '/issues/6.xml', @parameters, credentials('jsmith')
519 519 end
520 520 end
521 521
522 522 should "create a new journal" do
523 523 assert_difference('Journal.count') do
524 524 put '/issues/6.xml', @parameters, credentials('jsmith')
525 525 end
526 526 end
527 527
528 528 should "add the note to the journal" do
529 529 put '/issues/6.xml', @parameters, credentials('jsmith')
530 530
531 531 journal = Journal.last
532 532 assert_equal "A new note", journal.notes
533 533 end
534 534
535 535 should "update the issue" do
536 536 put '/issues/6.xml', @parameters, credentials('jsmith')
537 537
538 538 issue = Issue.find(6)
539 539 assert_equal "API update", issue.subject
540 540 end
541 541
542 542 end
543 543
544 544 context "PUT /issues/3.xml with custom fields" do
545 545 setup do
546 546 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, {'id' => '2', 'value' => '150'}]}}
547 547 end
548 548
549 549 should "update custom fields" do
550 550 assert_no_difference('Issue.count') do
551 551 put '/issues/3.xml', @parameters, credentials('jsmith')
552 552 end
553 553
554 554 issue = Issue.find(3)
555 555 assert_equal '150', issue.custom_value_for(2).value
556 556 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
557 557 end
558 558 end
559 559
560 560 context "PUT /issues/3.xml with multi custom fields" do
561 561 setup do
562 562 field = CustomField.find(1)
563 563 field.update_attribute :multiple, true
564 564 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => ['MySQL', 'PostgreSQL'] }, {'id' => '2', 'value' => '150'}]}}
565 565 end
566 566
567 567 should "update custom fields" do
568 568 assert_no_difference('Issue.count') do
569 569 put '/issues/3.xml', @parameters, credentials('jsmith')
570 570 end
571 571
572 572 issue = Issue.find(3)
573 573 assert_equal '150', issue.custom_value_for(2).value
574 574 assert_equal ['MySQL', 'PostgreSQL'], issue.custom_field_value(1).sort
575 575 end
576 576 end
577 577
578 578 context "PUT /issues/3.xml with project change" do
579 579 setup do
580 580 @parameters = {:issue => {:project_id => 2, :subject => 'Project changed'}}
581 581 end
582 582
583 583 should "update project" do
584 584 assert_no_difference('Issue.count') do
585 585 put '/issues/3.xml', @parameters, credentials('jsmith')
586 586 end
587 587
588 588 issue = Issue.find(3)
589 589 assert_equal 2, issue.project_id
590 590 assert_equal 'Project changed', issue.subject
591 591 end
592 592 end
593 593
594 594 context "PUT /issues/6.xml with failed update" do
595 595 setup do
596 596 @parameters = {:issue => {:subject => ''}}
597 597 end
598 598
599 599 should "not create a new issue" do
600 600 assert_no_difference('Issue.count') do
601 601 put '/issues/6.xml', @parameters, credentials('jsmith')
602 602 end
603 603 end
604 604
605 605 should "not create a new journal" do
606 606 assert_no_difference('Journal.count') do
607 607 put '/issues/6.xml', @parameters, credentials('jsmith')
608 608 end
609 609 end
610 610
611 611 should "have an errors tag" do
612 612 put '/issues/6.xml', @parameters, credentials('jsmith')
613 613
614 614 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
615 615 end
616 616 end
617 617
618 618 context "PUT /issues/6.json" do
619 619 setup do
620 620 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
621 621 end
622 622
623 623 should_allow_api_authentication(:put,
624 624 '/issues/6.json',
625 625 {:issue => {:subject => 'API update', :notes => 'A new note'}},
626 626 {:success_code => :ok})
627 627
628 628 should "not create a new issue" do
629 629 assert_no_difference('Issue.count') do
630 630 put '/issues/6.json', @parameters, credentials('jsmith')
631 631 end
632 632 end
633 633
634 634 should "create a new journal" do
635 635 assert_difference('Journal.count') do
636 636 put '/issues/6.json', @parameters, credentials('jsmith')
637 637 end
638 638 end
639 639
640 640 should "add the note to the journal" do
641 641 put '/issues/6.json', @parameters, credentials('jsmith')
642 642
643 643 journal = Journal.last
644 644 assert_equal "A new note", journal.notes
645 645 end
646 646
647 647 should "update the issue" do
648 648 put '/issues/6.json', @parameters, credentials('jsmith')
649 649
650 650 issue = Issue.find(6)
651 651 assert_equal "API update", issue.subject
652 652 end
653 653
654 654 end
655 655
656 656 context "PUT /issues/6.json with failed update" do
657 657 setup do
658 658 @parameters = {:issue => {:subject => ''}}
659 659 end
660 660
661 661 should "not create a new issue" do
662 662 assert_no_difference('Issue.count') do
663 663 put '/issues/6.json', @parameters, credentials('jsmith')
664 664 end
665 665 end
666 666
667 667 should "not create a new journal" do
668 668 assert_no_difference('Journal.count') do
669 669 put '/issues/6.json', @parameters, credentials('jsmith')
670 670 end
671 671 end
672 672
673 673 should "have an errors attribute" do
674 674 put '/issues/6.json', @parameters, credentials('jsmith')
675 675
676 676 json = ActiveSupport::JSON.decode(response.body)
677 assert json['errors'].include?(['subject', "can't be blank"])
677 assert json['errors'].include?("Subject can't be blank")
678 678 end
679 679 end
680 680
681 681 context "DELETE /issues/1.xml" do
682 682 should_allow_api_authentication(:delete,
683 683 '/issues/6.xml',
684 684 {},
685 685 {:success_code => :ok})
686 686
687 687 should "delete the issue" do
688 688 assert_difference('Issue.count',-1) do
689 689 delete '/issues/6.xml', {}, credentials('jsmith')
690 690 end
691 691
692 692 assert_nil Issue.find_by_id(6)
693 693 end
694 694 end
695 695
696 696 context "DELETE /issues/1.json" do
697 697 should_allow_api_authentication(:delete,
698 698 '/issues/6.json',
699 699 {},
700 700 {:success_code => :ok})
701 701
702 702 should "delete the issue" do
703 703 assert_difference('Issue.count',-1) do
704 704 delete '/issues/6.json', {}, credentials('jsmith')
705 705 end
706 706
707 707 assert_nil Issue.find_by_id(6)
708 708 end
709 709 end
710 710
711 711 def test_create_issue_with_uploaded_file
712 712 set_tmp_attachments_directory
713 713
714 714 # upload the file
715 715 assert_difference 'Attachment.count' do
716 716 post '/uploads.xml', 'test_create_with_upload', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith'))
717 717 assert_response :created
718 718 end
719 719 xml = Hash.from_xml(response.body)
720 720 token = xml['upload']['token']
721 721 attachment = Attachment.first(:order => 'id DESC')
722 722
723 723 # create the issue with the upload's token
724 724 assert_difference 'Issue.count' do
725 725 post '/issues.xml',
726 726 {:issue => {:project_id => 1, :subject => 'Uploaded file', :uploads => [{:token => token, :filename => 'test.txt', :content_type => 'text/plain'}]}},
727 727 credentials('jsmith')
728 728 assert_response :created
729 729 end
730 730 issue = Issue.first(:order => 'id DESC')
731 731 assert_equal 1, issue.attachments.count
732 732 assert_equal attachment, issue.attachments.first
733 733
734 734 attachment.reload
735 735 assert_equal 'test.txt', attachment.filename
736 736 assert_equal 'text/plain', attachment.content_type
737 737 assert_equal 'test_create_with_upload'.size, attachment.filesize
738 738 assert_equal 2, attachment.author_id
739 739
740 740 # get the issue with its attachments
741 741 get "/issues/#{issue.id}.xml", :include => 'attachments'
742 742 assert_response :success
743 743 xml = Hash.from_xml(response.body)
744 744 attachments = xml['issue']['attachments']
745 745 assert_kind_of Array, attachments
746 746 assert_equal 1, attachments.size
747 747 url = attachments.first['content_url']
748 748 assert_not_nil url
749 749
750 750 # download the attachment
751 751 get url
752 752 assert_response :success
753 753 end
754 754
755 755 def test_update_issue_with_uploaded_file
756 756 set_tmp_attachments_directory
757 757
758 758 # upload the file
759 759 assert_difference 'Attachment.count' do
760 760 post '/uploads.xml', 'test_upload_with_upload', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith'))
761 761 assert_response :created
762 762 end
763 763 xml = Hash.from_xml(response.body)
764 764 token = xml['upload']['token']
765 765 attachment = Attachment.first(:order => 'id DESC')
766 766
767 767 # update the issue with the upload's token
768 768 assert_difference 'Journal.count' do
769 769 put '/issues/1.xml',
770 770 {:issue => {:notes => 'Attachment added', :uploads => [{:token => token, :filename => 'test.txt', :content_type => 'text/plain'}]}},
771 771 credentials('jsmith')
772 772 assert_response :ok
773 773 end
774 774
775 775 issue = Issue.find(1)
776 776 assert_include attachment, issue.attachments
777 777 end
778 778 end
General Comments 0
You need to be logged in to leave comments. Login now