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