##// END OF EJS Templates
Makes the API accepts the X-Redmine-API-Key header to hold the API key....
Jean-Philippe Lang -
r4459:07fe46e9dfc1
parent child
Show More
@@ -1,473 +1,482
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 ApplicationController < ActionController::Base
21 class ApplicationController < ActionController::Base
22 include Redmine::I18n
22 include Redmine::I18n
23
23
24 layout 'base'
24 layout 'base'
25 exempt_from_layout 'builder', 'rsb'
25 exempt_from_layout 'builder', 'rsb'
26
26
27 # Remove broken cookie after upgrade from 0.8.x (#4292)
27 # Remove broken cookie after upgrade from 0.8.x (#4292)
28 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
28 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
29 # TODO: remove it when Rails is fixed
29 # TODO: remove it when Rails is fixed
30 before_filter :delete_broken_cookies
30 before_filter :delete_broken_cookies
31 def delete_broken_cookies
31 def delete_broken_cookies
32 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
32 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
33 cookies.delete '_redmine_session'
33 cookies.delete '_redmine_session'
34 redirect_to home_path
34 redirect_to home_path
35 return false
35 return false
36 end
36 end
37 end
37 end
38
38
39 before_filter :user_setup, :check_if_login_required, :set_localization
39 before_filter :user_setup, :check_if_login_required, :set_localization
40 filter_parameter_logging :password
40 filter_parameter_logging :password
41 protect_from_forgery
41 protect_from_forgery
42
42
43 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
43 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
44
44
45 include Redmine::Search::Controller
45 include Redmine::Search::Controller
46 include Redmine::MenuManager::MenuController
46 include Redmine::MenuManager::MenuController
47 helper Redmine::MenuManager::MenuHelper
47 helper Redmine::MenuManager::MenuHelper
48
48
49 Redmine::Scm::Base.all.each do |scm|
49 Redmine::Scm::Base.all.each do |scm|
50 require_dependency "repository/#{scm.underscore}"
50 require_dependency "repository/#{scm.underscore}"
51 end
51 end
52
52
53 def user_setup
53 def user_setup
54 # Check the settings cache for each request
54 # Check the settings cache for each request
55 Setting.check_cache
55 Setting.check_cache
56 # Find the current user
56 # Find the current user
57 User.current = find_current_user
57 User.current = find_current_user
58 end
58 end
59
59
60 # Returns the current user or nil if no user is logged in
60 # Returns the current user or nil if no user is logged in
61 # and starts a session if needed
61 # and starts a session if needed
62 def find_current_user
62 def find_current_user
63 if session[:user_id]
63 if session[:user_id]
64 # existing session
64 # existing session
65 (User.active.find(session[:user_id]) rescue nil)
65 (User.active.find(session[:user_id]) rescue nil)
66 elsif cookies[:autologin] && Setting.autologin?
66 elsif cookies[:autologin] && Setting.autologin?
67 # auto-login feature starts a new session
67 # auto-login feature starts a new session
68 user = User.try_to_autologin(cookies[:autologin])
68 user = User.try_to_autologin(cookies[:autologin])
69 session[:user_id] = user.id if user
69 session[:user_id] = user.id if user
70 user
70 user
71 elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
71 elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
72 # RSS key authentication does not start a session
72 # RSS key authentication does not start a session
73 User.find_by_rss_key(params[:key])
73 User.find_by_rss_key(params[:key])
74 elsif Setting.rest_api_enabled? && ['xml', 'json'].include?(params[:format])
74 elsif Setting.rest_api_enabled? && api_request?
75 if params[:key].present? && accept_key_auth_actions.include?(params[:action])
75 if (key = api_key_from_request) && accept_key_auth_actions.include?(params[:action])
76 # Use API key
76 # Use API key
77 User.find_by_api_key(params[:key])
77 User.find_by_api_key(key)
78 else
78 else
79 # HTTP Basic, either username/password or API key/random
79 # HTTP Basic, either username/password or API key/random
80 authenticate_with_http_basic do |username, password|
80 authenticate_with_http_basic do |username, password|
81 User.try_to_login(username, password) || User.find_by_api_key(username)
81 User.try_to_login(username, password) || User.find_by_api_key(username)
82 end
82 end
83 end
83 end
84 end
84 end
85 end
85 end
86
86
87 # Sets the logged in user
87 # Sets the logged in user
88 def logged_user=(user)
88 def logged_user=(user)
89 reset_session
89 reset_session
90 if user && user.is_a?(User)
90 if user && user.is_a?(User)
91 User.current = user
91 User.current = user
92 session[:user_id] = user.id
92 session[:user_id] = user.id
93 else
93 else
94 User.current = User.anonymous
94 User.current = User.anonymous
95 end
95 end
96 end
96 end
97
97
98 # check if login is globally required to access the application
98 # check if login is globally required to access the application
99 def check_if_login_required
99 def check_if_login_required
100 # no check needed if user is already logged in
100 # no check needed if user is already logged in
101 return true if User.current.logged?
101 return true if User.current.logged?
102 require_login if Setting.login_required?
102 require_login if Setting.login_required?
103 end
103 end
104
104
105 def set_localization
105 def set_localization
106 lang = nil
106 lang = nil
107 if User.current.logged?
107 if User.current.logged?
108 lang = find_language(User.current.language)
108 lang = find_language(User.current.language)
109 end
109 end
110 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
110 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
111 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
111 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
112 if !accept_lang.blank?
112 if !accept_lang.blank?
113 accept_lang = accept_lang.downcase
113 accept_lang = accept_lang.downcase
114 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
114 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
115 end
115 end
116 end
116 end
117 lang ||= Setting.default_language
117 lang ||= Setting.default_language
118 set_language_if_valid(lang)
118 set_language_if_valid(lang)
119 end
119 end
120
120
121 def require_login
121 def require_login
122 if !User.current.logged?
122 if !User.current.logged?
123 # Extract only the basic url parameters on non-GET requests
123 # Extract only the basic url parameters on non-GET requests
124 if request.get?
124 if request.get?
125 url = url_for(params)
125 url = url_for(params)
126 else
126 else
127 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
127 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
128 end
128 end
129 respond_to do |format|
129 respond_to do |format|
130 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
130 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
131 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
131 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
132 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
132 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
133 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
133 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
134 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
134 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
135 end
135 end
136 return false
136 return false
137 end
137 end
138 true
138 true
139 end
139 end
140
140
141 def require_admin
141 def require_admin
142 return unless require_login
142 return unless require_login
143 if !User.current.admin?
143 if !User.current.admin?
144 render_403
144 render_403
145 return false
145 return false
146 end
146 end
147 true
147 true
148 end
148 end
149
149
150 def deny_access
150 def deny_access
151 User.current.logged? ? render_403 : require_login
151 User.current.logged? ? render_403 : require_login
152 end
152 end
153
153
154 # Authorize the user for the requested action
154 # Authorize the user for the requested action
155 def authorize(ctrl = params[:controller], action = params[:action], global = false)
155 def authorize(ctrl = params[:controller], action = params[:action], global = false)
156 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
156 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
157 if allowed
157 if allowed
158 true
158 true
159 else
159 else
160 if @project && @project.archived?
160 if @project && @project.archived?
161 render_403 :message => :notice_not_authorized_archived_project
161 render_403 :message => :notice_not_authorized_archived_project
162 else
162 else
163 deny_access
163 deny_access
164 end
164 end
165 end
165 end
166 end
166 end
167
167
168 # Authorize the user for the requested action outside a project
168 # Authorize the user for the requested action outside a project
169 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
169 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
170 authorize(ctrl, action, global)
170 authorize(ctrl, action, global)
171 end
171 end
172
172
173 # Find project of id params[:id]
173 # Find project of id params[:id]
174 def find_project
174 def find_project
175 @project = Project.find(params[:id])
175 @project = Project.find(params[:id])
176 rescue ActiveRecord::RecordNotFound
176 rescue ActiveRecord::RecordNotFound
177 render_404
177 render_404
178 end
178 end
179
179
180 # Find project of id params[:project_id]
180 # Find project of id params[:project_id]
181 def find_project_by_project_id
181 def find_project_by_project_id
182 @project = Project.find(params[:project_id])
182 @project = Project.find(params[:project_id])
183 rescue ActiveRecord::RecordNotFound
183 rescue ActiveRecord::RecordNotFound
184 render_404
184 render_404
185 end
185 end
186
186
187 # Find a project based on params[:project_id]
187 # Find a project based on params[:project_id]
188 # TODO: some subclasses override this, see about merging their logic
188 # TODO: some subclasses override this, see about merging their logic
189 def find_optional_project
189 def find_optional_project
190 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
190 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
191 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
191 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
192 allowed ? true : deny_access
192 allowed ? true : deny_access
193 rescue ActiveRecord::RecordNotFound
193 rescue ActiveRecord::RecordNotFound
194 render_404
194 render_404
195 end
195 end
196
196
197 # Finds and sets @project based on @object.project
197 # Finds and sets @project based on @object.project
198 def find_project_from_association
198 def find_project_from_association
199 render_404 unless @object.present?
199 render_404 unless @object.present?
200
200
201 @project = @object.project
201 @project = @object.project
202 rescue ActiveRecord::RecordNotFound
202 rescue ActiveRecord::RecordNotFound
203 render_404
203 render_404
204 end
204 end
205
205
206 def find_model_object
206 def find_model_object
207 model = self.class.read_inheritable_attribute('model_object')
207 model = self.class.read_inheritable_attribute('model_object')
208 if model
208 if model
209 @object = model.find(params[:id])
209 @object = model.find(params[:id])
210 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
210 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
211 end
211 end
212 rescue ActiveRecord::RecordNotFound
212 rescue ActiveRecord::RecordNotFound
213 render_404
213 render_404
214 end
214 end
215
215
216 def self.model_object(model)
216 def self.model_object(model)
217 write_inheritable_attribute('model_object', model)
217 write_inheritable_attribute('model_object', model)
218 end
218 end
219
219
220 # Filter for bulk issue operations
220 # Filter for bulk issue operations
221 def find_issues
221 def find_issues
222 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
222 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
223 raise ActiveRecord::RecordNotFound if @issues.empty?
223 raise ActiveRecord::RecordNotFound if @issues.empty?
224 @projects = @issues.collect(&:project).compact.uniq
224 @projects = @issues.collect(&:project).compact.uniq
225 @project = @projects.first if @projects.size == 1
225 @project = @projects.first if @projects.size == 1
226 rescue ActiveRecord::RecordNotFound
226 rescue ActiveRecord::RecordNotFound
227 render_404
227 render_404
228 end
228 end
229
229
230 # Check if project is unique before bulk operations
230 # Check if project is unique before bulk operations
231 def check_project_uniqueness
231 def check_project_uniqueness
232 unless @project
232 unless @project
233 # TODO: let users bulk edit/move/destroy issues from different projects
233 # TODO: let users bulk edit/move/destroy issues from different projects
234 render_error 'Can not bulk edit/move/destroy issues from different projects'
234 render_error 'Can not bulk edit/move/destroy issues from different projects'
235 return false
235 return false
236 end
236 end
237 end
237 end
238
238
239 # make sure that the user is a member of the project (or admin) if project is private
239 # make sure that the user is a member of the project (or admin) if project is private
240 # used as a before_filter for actions that do not require any particular permission on the project
240 # used as a before_filter for actions that do not require any particular permission on the project
241 def check_project_privacy
241 def check_project_privacy
242 if @project && @project.active?
242 if @project && @project.active?
243 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
243 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
244 true
244 true
245 else
245 else
246 User.current.logged? ? render_403 : require_login
246 User.current.logged? ? render_403 : require_login
247 end
247 end
248 else
248 else
249 @project = nil
249 @project = nil
250 render_404
250 render_404
251 false
251 false
252 end
252 end
253 end
253 end
254
254
255 def back_url
255 def back_url
256 params[:back_url] || request.env['HTTP_REFERER']
256 params[:back_url] || request.env['HTTP_REFERER']
257 end
257 end
258
258
259 def redirect_back_or_default(default)
259 def redirect_back_or_default(default)
260 back_url = CGI.unescape(params[:back_url].to_s)
260 back_url = CGI.unescape(params[:back_url].to_s)
261 if !back_url.blank?
261 if !back_url.blank?
262 begin
262 begin
263 uri = URI.parse(back_url)
263 uri = URI.parse(back_url)
264 # do not redirect user to another host or to the login or register page
264 # do not redirect user to another host or to the login or register page
265 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
265 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
266 redirect_to(back_url)
266 redirect_to(back_url)
267 return
267 return
268 end
268 end
269 rescue URI::InvalidURIError
269 rescue URI::InvalidURIError
270 # redirect to default
270 # redirect to default
271 end
271 end
272 end
272 end
273 redirect_to default
273 redirect_to default
274 end
274 end
275
275
276 def render_403(options={})
276 def render_403(options={})
277 @project = nil
277 @project = nil
278 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
278 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
279 return false
279 return false
280 end
280 end
281
281
282 def render_404(options={})
282 def render_404(options={})
283 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
283 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
284 return false
284 return false
285 end
285 end
286
286
287 # Renders an error response
287 # Renders an error response
288 def render_error(arg)
288 def render_error(arg)
289 arg = {:message => arg} unless arg.is_a?(Hash)
289 arg = {:message => arg} unless arg.is_a?(Hash)
290
290
291 @message = arg[:message]
291 @message = arg[:message]
292 @message = l(@message) if @message.is_a?(Symbol)
292 @message = l(@message) if @message.is_a?(Symbol)
293 @status = arg[:status] || 500
293 @status = arg[:status] || 500
294
294
295 respond_to do |format|
295 respond_to do |format|
296 format.html {
296 format.html {
297 render :template => 'common/error', :layout => use_layout, :status => @status
297 render :template => 'common/error', :layout => use_layout, :status => @status
298 }
298 }
299 format.atom { head @status }
299 format.atom { head @status }
300 format.xml { head @status }
300 format.xml { head @status }
301 format.js { head @status }
301 format.js { head @status }
302 format.json { head @status }
302 format.json { head @status }
303 end
303 end
304 end
304 end
305
305
306 # Picks which layout to use based on the request
306 # Picks which layout to use based on the request
307 #
307 #
308 # @return [boolean, string] name of the layout to use or false for no layout
308 # @return [boolean, string] name of the layout to use or false for no layout
309 def use_layout
309 def use_layout
310 request.xhr? ? false : 'base'
310 request.xhr? ? false : 'base'
311 end
311 end
312
312
313 def invalid_authenticity_token
313 def invalid_authenticity_token
314 if api_request?
314 if api_request?
315 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
315 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
316 end
316 end
317 render_error "Invalid form authenticity token."
317 render_error "Invalid form authenticity token."
318 end
318 end
319
319
320 def render_feed(items, options={})
320 def render_feed(items, options={})
321 @items = items || []
321 @items = items || []
322 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
322 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
323 @items = @items.slice(0, Setting.feeds_limit.to_i)
323 @items = @items.slice(0, Setting.feeds_limit.to_i)
324 @title = options[:title] || Setting.app_title
324 @title = options[:title] || Setting.app_title
325 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
325 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
326 end
326 end
327
327
328 def self.accept_key_auth(*actions)
328 def self.accept_key_auth(*actions)
329 actions = actions.flatten.map(&:to_s)
329 actions = actions.flatten.map(&:to_s)
330 write_inheritable_attribute('accept_key_auth_actions', actions)
330 write_inheritable_attribute('accept_key_auth_actions', actions)
331 end
331 end
332
332
333 def accept_key_auth_actions
333 def accept_key_auth_actions
334 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
334 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
335 end
335 end
336
336
337 # Returns the number of objects that should be displayed
337 # Returns the number of objects that should be displayed
338 # on the paginated list
338 # on the paginated list
339 def per_page_option
339 def per_page_option
340 per_page = nil
340 per_page = nil
341 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
341 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
342 per_page = params[:per_page].to_s.to_i
342 per_page = params[:per_page].to_s.to_i
343 session[:per_page] = per_page
343 session[:per_page] = per_page
344 elsif session[:per_page]
344 elsif session[:per_page]
345 per_page = session[:per_page]
345 per_page = session[:per_page]
346 else
346 else
347 per_page = Setting.per_page_options_array.first || 25
347 per_page = Setting.per_page_options_array.first || 25
348 end
348 end
349 per_page
349 per_page
350 end
350 end
351
351
352 # Returns offset and limit used to retrieve objects
352 # Returns offset and limit used to retrieve objects
353 # for an API response based on offset, limit and page parameters
353 # for an API response based on offset, limit and page parameters
354 def api_offset_and_limit(options=params)
354 def api_offset_and_limit(options=params)
355 if options[:offset].present?
355 if options[:offset].present?
356 offset = options[:offset].to_i
356 offset = options[:offset].to_i
357 if offset < 0
357 if offset < 0
358 offset = 0
358 offset = 0
359 end
359 end
360 end
360 end
361 limit = options[:limit].to_i
361 limit = options[:limit].to_i
362 if limit < 1
362 if limit < 1
363 limit = 25
363 limit = 25
364 elsif limit > 100
364 elsif limit > 100
365 limit = 100
365 limit = 100
366 end
366 end
367 if offset.nil? && options[:page].present?
367 if offset.nil? && options[:page].present?
368 offset = (options[:page].to_i - 1) * limit
368 offset = (options[:page].to_i - 1) * limit
369 offset = 0 if offset < 0
369 offset = 0 if offset < 0
370 end
370 end
371 offset ||= 0
371 offset ||= 0
372
372
373 [offset, limit]
373 [offset, limit]
374 end
374 end
375
375
376 # qvalues http header parser
376 # qvalues http header parser
377 # code taken from webrick
377 # code taken from webrick
378 def parse_qvalues(value)
378 def parse_qvalues(value)
379 tmp = []
379 tmp = []
380 if value
380 if value
381 parts = value.split(/,\s*/)
381 parts = value.split(/,\s*/)
382 parts.each {|part|
382 parts.each {|part|
383 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
383 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
384 val = m[1]
384 val = m[1]
385 q = (m[2] or 1).to_f
385 q = (m[2] or 1).to_f
386 tmp.push([val, q])
386 tmp.push([val, q])
387 end
387 end
388 }
388 }
389 tmp = tmp.sort_by{|val, q| -q}
389 tmp = tmp.sort_by{|val, q| -q}
390 tmp.collect!{|val, q| val}
390 tmp.collect!{|val, q| val}
391 end
391 end
392 return tmp
392 return tmp
393 rescue
393 rescue
394 nil
394 nil
395 end
395 end
396
396
397 # Returns a string that can be used as filename value in Content-Disposition header
397 # Returns a string that can be used as filename value in Content-Disposition header
398 def filename_for_content_disposition(name)
398 def filename_for_content_disposition(name)
399 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
399 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
400 end
400 end
401
401
402 def api_request?
402 def api_request?
403 %w(xml json).include? params[:format]
403 %w(xml json).include? params[:format]
404 end
404 end
405
406 # Returns the API key present in the request
407 def api_key_from_request
408 if params[:key].present?
409 params[:key]
410 elsif request.headers["X-Redmine-API-Key"].present?
411 request.headers["X-Redmine-API-Key"]
412 end
413 end
405
414
406 # Renders a warning flash if obj has unsaved attachments
415 # Renders a warning flash if obj has unsaved attachments
407 def render_attachment_warning_if_needed(obj)
416 def render_attachment_warning_if_needed(obj)
408 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
417 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
409 end
418 end
410
419
411 # Sets the `flash` notice or error based the number of issues that did not save
420 # Sets the `flash` notice or error based the number of issues that did not save
412 #
421 #
413 # @param [Array, Issue] issues all of the saved and unsaved Issues
422 # @param [Array, Issue] issues all of the saved and unsaved Issues
414 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
423 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
415 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
424 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
416 if unsaved_issue_ids.empty?
425 if unsaved_issue_ids.empty?
417 flash[:notice] = l(:notice_successful_update) unless issues.empty?
426 flash[:notice] = l(:notice_successful_update) unless issues.empty?
418 else
427 else
419 flash[:error] = l(:notice_failed_to_save_issues,
428 flash[:error] = l(:notice_failed_to_save_issues,
420 :count => unsaved_issue_ids.size,
429 :count => unsaved_issue_ids.size,
421 :total => issues.size,
430 :total => issues.size,
422 :ids => '#' + unsaved_issue_ids.join(', #'))
431 :ids => '#' + unsaved_issue_ids.join(', #'))
423 end
432 end
424 end
433 end
425
434
426 # Rescues an invalid query statement. Just in case...
435 # Rescues an invalid query statement. Just in case...
427 def query_statement_invalid(exception)
436 def query_statement_invalid(exception)
428 logger.error "Query::StatementInvalid: #{exception.message}" if logger
437 logger.error "Query::StatementInvalid: #{exception.message}" if logger
429 session.delete(:query)
438 session.delete(:query)
430 sort_clear if respond_to?(:sort_clear)
439 sort_clear if respond_to?(:sort_clear)
431 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
440 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
432 end
441 end
433
442
434 # Converts the errors on an ActiveRecord object into a common JSON format
443 # Converts the errors on an ActiveRecord object into a common JSON format
435 def object_errors_to_json(object)
444 def object_errors_to_json(object)
436 object.errors.collect do |attribute, error|
445 object.errors.collect do |attribute, error|
437 { attribute => error }
446 { attribute => error }
438 end.to_json
447 end.to_json
439 end
448 end
440
449
441 # Renders API response on validation failure
450 # Renders API response on validation failure
442 def render_validation_errors(object)
451 def render_validation_errors(object)
443 options = { :status => :unprocessable_entity, :layout => false }
452 options = { :status => :unprocessable_entity, :layout => false }
444 options.merge!(case params[:format]
453 options.merge!(case params[:format]
445 when 'xml'; { :xml => object.errors }
454 when 'xml'; { :xml => object.errors }
446 when 'json'; { :json => {'errors' => object.errors} } # ActiveResource client compliance
455 when 'json'; { :json => {'errors' => object.errors} } # ActiveResource client compliance
447 else
456 else
448 raise "Unknown format #{params[:format]} in #render_validation_errors"
457 raise "Unknown format #{params[:format]} in #render_validation_errors"
449 end
458 end
450 )
459 )
451 render options
460 render options
452 end
461 end
453
462
454 # Overrides #default_template so that the api template
463 # Overrides #default_template so that the api template
455 # is used automatically if it exists
464 # is used automatically if it exists
456 def default_template(action_name = self.action_name)
465 def default_template(action_name = self.action_name)
457 if api_request?
466 if api_request?
458 begin
467 begin
459 return self.view_paths.find_template(default_template_name(action_name), 'api')
468 return self.view_paths.find_template(default_template_name(action_name), 'api')
460 rescue ::ActionView::MissingTemplate
469 rescue ::ActionView::MissingTemplate
461 # the api template was not found
470 # the api template was not found
462 # fallback to the default behaviour
471 # fallback to the default behaviour
463 end
472 end
464 end
473 end
465 super
474 super
466 end
475 end
467
476
468 # Overrides #pick_layout so that #render with no arguments
477 # Overrides #pick_layout so that #render with no arguments
469 # doesn't use the layout for api requests
478 # doesn't use the layout for api requests
470 def pick_layout(*args)
479 def pick_layout(*args)
471 api_request? ? nil : super
480 api_request? ? nil : super
472 end
481 end
473 end
482 end
@@ -1,420 +1,434
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 ENV["RAILS_ENV"] = "test"
18 ENV["RAILS_ENV"] = "test"
19 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
19 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
20 require 'test_help'
20 require 'test_help'
21 require File.expand_path(File.dirname(__FILE__) + '/helper_testcase')
21 require File.expand_path(File.dirname(__FILE__) + '/helper_testcase')
22 require File.join(RAILS_ROOT,'test', 'mocks', 'open_id_authentication_mock.rb')
22 require File.join(RAILS_ROOT,'test', 'mocks', 'open_id_authentication_mock.rb')
23
23
24 require File.expand_path(File.dirname(__FILE__) + '/object_daddy_helpers')
24 require File.expand_path(File.dirname(__FILE__) + '/object_daddy_helpers')
25 include ObjectDaddyHelpers
25 include ObjectDaddyHelpers
26
26
27 class ActiveSupport::TestCase
27 class ActiveSupport::TestCase
28 # Transactional fixtures accelerate your tests by wrapping each test method
28 # Transactional fixtures accelerate your tests by wrapping each test method
29 # in a transaction that's rolled back on completion. This ensures that the
29 # in a transaction that's rolled back on completion. This ensures that the
30 # test database remains unchanged so your fixtures don't have to be reloaded
30 # test database remains unchanged so your fixtures don't have to be reloaded
31 # between every test method. Fewer database queries means faster tests.
31 # between every test method. Fewer database queries means faster tests.
32 #
32 #
33 # Read Mike Clark's excellent walkthrough at
33 # Read Mike Clark's excellent walkthrough at
34 # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
34 # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
35 #
35 #
36 # Every Active Record database supports transactions except MyISAM tables
36 # Every Active Record database supports transactions except MyISAM tables
37 # in MySQL. Turn off transactional fixtures in this case; however, if you
37 # in MySQL. Turn off transactional fixtures in this case; however, if you
38 # don't care one way or the other, switching from MyISAM to InnoDB tables
38 # don't care one way or the other, switching from MyISAM to InnoDB tables
39 # is recommended.
39 # is recommended.
40 self.use_transactional_fixtures = true
40 self.use_transactional_fixtures = true
41
41
42 # Instantiated fixtures are slow, but give you @david where otherwise you
42 # Instantiated fixtures are slow, but give you @david where otherwise you
43 # would need people(:david). If you don't want to migrate your existing
43 # would need people(:david). If you don't want to migrate your existing
44 # test cases which use the @david style and don't mind the speed hit (each
44 # test cases which use the @david style and don't mind the speed hit (each
45 # instantiated fixtures translates to a database query per test method),
45 # instantiated fixtures translates to a database query per test method),
46 # then set this back to true.
46 # then set this back to true.
47 self.use_instantiated_fixtures = false
47 self.use_instantiated_fixtures = false
48
48
49 # Add more helper methods to be used by all tests here...
49 # Add more helper methods to be used by all tests here...
50
50
51 def log_user(login, password)
51 def log_user(login, password)
52 User.anonymous
52 User.anonymous
53 get "/login"
53 get "/login"
54 assert_equal nil, session[:user_id]
54 assert_equal nil, session[:user_id]
55 assert_response :success
55 assert_response :success
56 assert_template "account/login"
56 assert_template "account/login"
57 post "/login", :username => login, :password => password
57 post "/login", :username => login, :password => password
58 assert_equal login, User.find(session[:user_id]).login
58 assert_equal login, User.find(session[:user_id]).login
59 end
59 end
60
60
61 def uploaded_test_file(name, mime)
61 def uploaded_test_file(name, mime)
62 ActionController::TestUploadedFile.new(ActiveSupport::TestCase.fixture_path + "/files/#{name}", mime)
62 ActionController::TestUploadedFile.new(ActiveSupport::TestCase.fixture_path + "/files/#{name}", mime)
63 end
63 end
64
64
65 # Mock out a file
65 # Mock out a file
66 def self.mock_file
66 def self.mock_file
67 file = 'a_file.png'
67 file = 'a_file.png'
68 file.stubs(:size).returns(32)
68 file.stubs(:size).returns(32)
69 file.stubs(:original_filename).returns('a_file.png')
69 file.stubs(:original_filename).returns('a_file.png')
70 file.stubs(:content_type).returns('image/png')
70 file.stubs(:content_type).returns('image/png')
71 file.stubs(:read).returns(false)
71 file.stubs(:read).returns(false)
72 file
72 file
73 end
73 end
74
74
75 def mock_file
75 def mock_file
76 self.class.mock_file
76 self.class.mock_file
77 end
77 end
78
78
79 # Use a temporary directory for attachment related tests
79 # Use a temporary directory for attachment related tests
80 def set_tmp_attachments_directory
80 def set_tmp_attachments_directory
81 Dir.mkdir "#{RAILS_ROOT}/tmp/test" unless File.directory?("#{RAILS_ROOT}/tmp/test")
81 Dir.mkdir "#{RAILS_ROOT}/tmp/test" unless File.directory?("#{RAILS_ROOT}/tmp/test")
82 Dir.mkdir "#{RAILS_ROOT}/tmp/test/attachments" unless File.directory?("#{RAILS_ROOT}/tmp/test/attachments")
82 Dir.mkdir "#{RAILS_ROOT}/tmp/test/attachments" unless File.directory?("#{RAILS_ROOT}/tmp/test/attachments")
83 Attachment.storage_path = "#{RAILS_ROOT}/tmp/test/attachments"
83 Attachment.storage_path = "#{RAILS_ROOT}/tmp/test/attachments"
84 end
84 end
85
85
86 def with_settings(options, &block)
86 def with_settings(options, &block)
87 saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].dup; h}
87 saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].dup; h}
88 options.each {|k, v| Setting[k] = v}
88 options.each {|k, v| Setting[k] = v}
89 yield
89 yield
90 saved_settings.each {|k, v| Setting[k] = v}
90 saved_settings.each {|k, v| Setting[k] = v}
91 end
91 end
92
92
93 def change_user_password(login, new_password)
93 def change_user_password(login, new_password)
94 user = User.first(:conditions => {:login => login})
94 user = User.first(:conditions => {:login => login})
95 user.password, user.password_confirmation = new_password, new_password
95 user.password, user.password_confirmation = new_password, new_password
96 user.save!
96 user.save!
97 end
97 end
98
98
99 def self.ldap_configured?
99 def self.ldap_configured?
100 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
100 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
101 return @test_ldap.bind
101 return @test_ldap.bind
102 rescue Exception => e
102 rescue Exception => e
103 # LDAP is not listening
103 # LDAP is not listening
104 return nil
104 return nil
105 end
105 end
106
106
107 # Returns the path to the test +vendor+ repository
107 # Returns the path to the test +vendor+ repository
108 def self.repository_path(vendor)
108 def self.repository_path(vendor)
109 File.join(RAILS_ROOT.gsub(%r{config\/\.\.}, ''), "/tmp/test/#{vendor.downcase}_repository")
109 File.join(RAILS_ROOT.gsub(%r{config\/\.\.}, ''), "/tmp/test/#{vendor.downcase}_repository")
110 end
110 end
111
111
112 # Returns true if the +vendor+ test repository is configured
112 # Returns true if the +vendor+ test repository is configured
113 def self.repository_configured?(vendor)
113 def self.repository_configured?(vendor)
114 File.directory?(repository_path(vendor))
114 File.directory?(repository_path(vendor))
115 end
115 end
116
116
117 def assert_error_tag(options={})
117 def assert_error_tag(options={})
118 assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options))
118 assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options))
119 end
119 end
120
120
121 # Shoulda macros
121 # Shoulda macros
122 def self.should_render_404
122 def self.should_render_404
123 should_respond_with :not_found
123 should_respond_with :not_found
124 should_render_template 'common/error'
124 should_render_template 'common/error'
125 end
125 end
126
126
127 def self.should_have_before_filter(expected_method, options = {})
127 def self.should_have_before_filter(expected_method, options = {})
128 should_have_filter('before', expected_method, options)
128 should_have_filter('before', expected_method, options)
129 end
129 end
130
130
131 def self.should_have_after_filter(expected_method, options = {})
131 def self.should_have_after_filter(expected_method, options = {})
132 should_have_filter('after', expected_method, options)
132 should_have_filter('after', expected_method, options)
133 end
133 end
134
134
135 def self.should_have_filter(filter_type, expected_method, options)
135 def self.should_have_filter(filter_type, expected_method, options)
136 description = "have #{filter_type}_filter :#{expected_method}"
136 description = "have #{filter_type}_filter :#{expected_method}"
137 description << " with #{options.inspect}" unless options.empty?
137 description << " with #{options.inspect}" unless options.empty?
138
138
139 should description do
139 should description do
140 klass = "action_controller/filters/#{filter_type}_filter".classify.constantize
140 klass = "action_controller/filters/#{filter_type}_filter".classify.constantize
141 expected = klass.new(:filter, expected_method.to_sym, options)
141 expected = klass.new(:filter, expected_method.to_sym, options)
142 assert_equal 1, @controller.class.filter_chain.select { |filter|
142 assert_equal 1, @controller.class.filter_chain.select { |filter|
143 filter.method == expected.method && filter.kind == expected.kind &&
143 filter.method == expected.method && filter.kind == expected.kind &&
144 filter.options == expected.options && filter.class == expected.class
144 filter.options == expected.options && filter.class == expected.class
145 }.size
145 }.size
146 end
146 end
147 end
147 end
148
148
149 def self.should_show_the_old_and_new_values_for(prop_key, model, &block)
149 def self.should_show_the_old_and_new_values_for(prop_key, model, &block)
150 context "" do
150 context "" do
151 setup do
151 setup do
152 if block_given?
152 if block_given?
153 instance_eval &block
153 instance_eval &block
154 else
154 else
155 @old_value = model.generate!
155 @old_value = model.generate!
156 @new_value = model.generate!
156 @new_value = model.generate!
157 end
157 end
158 end
158 end
159
159
160 should "use the new value's name" do
160 should "use the new value's name" do
161 @detail = JournalDetail.generate!(:property => 'attr',
161 @detail = JournalDetail.generate!(:property => 'attr',
162 :old_value => @old_value.id,
162 :old_value => @old_value.id,
163 :value => @new_value.id,
163 :value => @new_value.id,
164 :prop_key => prop_key)
164 :prop_key => prop_key)
165
165
166 assert_match @new_value.name, show_detail(@detail, true)
166 assert_match @new_value.name, show_detail(@detail, true)
167 end
167 end
168
168
169 should "use the old value's name" do
169 should "use the old value's name" do
170 @detail = JournalDetail.generate!(:property => 'attr',
170 @detail = JournalDetail.generate!(:property => 'attr',
171 :old_value => @old_value.id,
171 :old_value => @old_value.id,
172 :value => @new_value.id,
172 :value => @new_value.id,
173 :prop_key => prop_key)
173 :prop_key => prop_key)
174
174
175 assert_match @old_value.name, show_detail(@detail, true)
175 assert_match @old_value.name, show_detail(@detail, true)
176 end
176 end
177 end
177 end
178 end
178 end
179
179
180 def self.should_create_a_new_user(&block)
180 def self.should_create_a_new_user(&block)
181 should "create a new user" do
181 should "create a new user" do
182 user = instance_eval &block
182 user = instance_eval &block
183 assert user
183 assert user
184 assert_kind_of User, user
184 assert_kind_of User, user
185 assert !user.new_record?
185 assert !user.new_record?
186 end
186 end
187 end
187 end
188
188
189 # Test that a request allows the three types of API authentication
189 # Test that a request allows the three types of API authentication
190 #
190 #
191 # * HTTP Basic with username and password
191 # * HTTP Basic with username and password
192 # * HTTP Basic with an api key for the username
192 # * HTTP Basic with an api key for the username
193 # * Key based with the key=X parameter
193 # * Key based with the key=X parameter
194 #
194 #
195 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
195 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
196 # @param [String] url the request url
196 # @param [String] url the request url
197 # @param [optional, Hash] parameters additional request parameters
197 # @param [optional, Hash] parameters additional request parameters
198 # @param [optional, Hash] options additional options
198 # @param [optional, Hash] options additional options
199 # @option options [Symbol] :success_code Successful response code (:success)
199 # @option options [Symbol] :success_code Successful response code (:success)
200 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
200 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
201 def self.should_allow_api_authentication(http_method, url, parameters={}, options={})
201 def self.should_allow_api_authentication(http_method, url, parameters={}, options={})
202 should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options)
202 should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options)
203 should_allow_http_basic_auth_with_key(http_method, url, parameters, options)
203 should_allow_http_basic_auth_with_key(http_method, url, parameters, options)
204 should_allow_key_based_auth(http_method, url, parameters, options)
204 should_allow_key_based_auth(http_method, url, parameters, options)
205 end
205 end
206
206
207 # Test that a request allows the username and password for HTTP BASIC
207 # Test that a request allows the username and password for HTTP BASIC
208 #
208 #
209 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
209 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
210 # @param [String] url the request url
210 # @param [String] url the request url
211 # @param [optional, Hash] parameters additional request parameters
211 # @param [optional, Hash] parameters additional request parameters
212 # @param [optional, Hash] options additional options
212 # @param [optional, Hash] options additional options
213 # @option options [Symbol] :success_code Successful response code (:success)
213 # @option options [Symbol] :success_code Successful response code (:success)
214 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
214 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
215 def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={})
215 def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={})
216 success_code = options[:success_code] || :success
216 success_code = options[:success_code] || :success
217 failure_code = options[:failure_code] || :unauthorized
217 failure_code = options[:failure_code] || :unauthorized
218
218
219 context "should allow http basic auth using a username and password for #{http_method} #{url}" do
219 context "should allow http basic auth using a username and password for #{http_method} #{url}" do
220 context "with a valid HTTP authentication" do
220 context "with a valid HTTP authentication" do
221 setup do
221 setup do
222 @user = User.generate_with_protected!(:password => 'my_password', :password_confirmation => 'my_password', :admin => true) # Admin so they can access the project
222 @user = User.generate_with_protected!(:password => 'my_password', :password_confirmation => 'my_password', :admin => true) # Admin so they can access the project
223 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@user.login, 'my_password')
223 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@user.login, 'my_password')
224 send(http_method, url, parameters, {:authorization => @authorization})
224 send(http_method, url, parameters, {:authorization => @authorization})
225 end
225 end
226
226
227 should_respond_with success_code
227 should_respond_with success_code
228 should_respond_with_content_type_based_on_url(url)
228 should_respond_with_content_type_based_on_url(url)
229 should "login as the user" do
229 should "login as the user" do
230 assert_equal @user, User.current
230 assert_equal @user, User.current
231 end
231 end
232 end
232 end
233
233
234 context "with an invalid HTTP authentication" do
234 context "with an invalid HTTP authentication" do
235 setup do
235 setup do
236 @user = User.generate_with_protected!
236 @user = User.generate_with_protected!
237 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@user.login, 'wrong_password')
237 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@user.login, 'wrong_password')
238 send(http_method, url, parameters, {:authorization => @authorization})
238 send(http_method, url, parameters, {:authorization => @authorization})
239 end
239 end
240
240
241 should_respond_with failure_code
241 should_respond_with failure_code
242 should_respond_with_content_type_based_on_url(url)
242 should_respond_with_content_type_based_on_url(url)
243 should "not login as the user" do
243 should "not login as the user" do
244 assert_equal User.anonymous, User.current
244 assert_equal User.anonymous, User.current
245 end
245 end
246 end
246 end
247
247
248 context "without credentials" do
248 context "without credentials" do
249 setup do
249 setup do
250 send(http_method, url, parameters, {:authorization => ''})
250 send(http_method, url, parameters, {:authorization => ''})
251 end
251 end
252
252
253 should_respond_with failure_code
253 should_respond_with failure_code
254 should_respond_with_content_type_based_on_url(url)
254 should_respond_with_content_type_based_on_url(url)
255 should "include_www_authenticate_header" do
255 should "include_www_authenticate_header" do
256 assert @controller.response.headers.has_key?('WWW-Authenticate')
256 assert @controller.response.headers.has_key?('WWW-Authenticate')
257 end
257 end
258 end
258 end
259 end
259 end
260
260
261 end
261 end
262
262
263 # Test that a request allows the API key with HTTP BASIC
263 # Test that a request allows the API key with HTTP BASIC
264 #
264 #
265 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
265 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
266 # @param [String] url the request url
266 # @param [String] url the request url
267 # @param [optional, Hash] parameters additional request parameters
267 # @param [optional, Hash] parameters additional request parameters
268 # @param [optional, Hash] options additional options
268 # @param [optional, Hash] options additional options
269 # @option options [Symbol] :success_code Successful response code (:success)
269 # @option options [Symbol] :success_code Successful response code (:success)
270 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
270 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
271 def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={})
271 def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={})
272 success_code = options[:success_code] || :success
272 success_code = options[:success_code] || :success
273 failure_code = options[:failure_code] || :unauthorized
273 failure_code = options[:failure_code] || :unauthorized
274
274
275 context "should allow http basic auth with a key for #{http_method} #{url}" do
275 context "should allow http basic auth with a key for #{http_method} #{url}" do
276 context "with a valid HTTP authentication using the API token" do
276 context "with a valid HTTP authentication using the API token" do
277 setup do
277 setup do
278 @user = User.generate_with_protected!(:admin => true)
278 @user = User.generate_with_protected!(:admin => true)
279 @token = Token.generate!(:user => @user, :action => 'api')
279 @token = Token.generate!(:user => @user, :action => 'api')
280 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@token.value, 'X')
280 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@token.value, 'X')
281 send(http_method, url, parameters, {:authorization => @authorization})
281 send(http_method, url, parameters, {:authorization => @authorization})
282 end
282 end
283
283
284 should_respond_with success_code
284 should_respond_with success_code
285 should_respond_with_content_type_based_on_url(url)
285 should_respond_with_content_type_based_on_url(url)
286 should_be_a_valid_response_string_based_on_url(url)
286 should_be_a_valid_response_string_based_on_url(url)
287 should "login as the user" do
287 should "login as the user" do
288 assert_equal @user, User.current
288 assert_equal @user, User.current
289 end
289 end
290 end
290 end
291
291
292 context "with an invalid HTTP authentication" do
292 context "with an invalid HTTP authentication" do
293 setup do
293 setup do
294 @user = User.generate_with_protected!
294 @user = User.generate_with_protected!
295 @token = Token.generate!(:user => @user, :action => 'feeds')
295 @token = Token.generate!(:user => @user, :action => 'feeds')
296 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@token.value, 'X')
296 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@token.value, 'X')
297 send(http_method, url, parameters, {:authorization => @authorization})
297 send(http_method, url, parameters, {:authorization => @authorization})
298 end
298 end
299
299
300 should_respond_with failure_code
300 should_respond_with failure_code
301 should_respond_with_content_type_based_on_url(url)
301 should_respond_with_content_type_based_on_url(url)
302 should "not login as the user" do
302 should "not login as the user" do
303 assert_equal User.anonymous, User.current
303 assert_equal User.anonymous, User.current
304 end
304 end
305 end
305 end
306 end
306 end
307 end
307 end
308
308
309 # Test that a request allows full key authentication
309 # Test that a request allows full key authentication
310 #
310 #
311 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
311 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
312 # @param [String] url the request url, without the key=ZXY parameter
312 # @param [String] url the request url, without the key=ZXY parameter
313 # @param [optional, Hash] parameters additional request parameters
313 # @param [optional, Hash] parameters additional request parameters
314 # @param [optional, Hash] options additional options
314 # @param [optional, Hash] options additional options
315 # @option options [Symbol] :success_code Successful response code (:success)
315 # @option options [Symbol] :success_code Successful response code (:success)
316 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
316 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
317 def self.should_allow_key_based_auth(http_method, url, parameters={}, options={})
317 def self.should_allow_key_based_auth(http_method, url, parameters={}, options={})
318 success_code = options[:success_code] || :success
318 success_code = options[:success_code] || :success
319 failure_code = options[:failure_code] || :unauthorized
319 failure_code = options[:failure_code] || :unauthorized
320
320
321 context "should allow key based auth using key=X for #{http_method} #{url}" do
321 context "should allow key based auth using key=X for #{http_method} #{url}" do
322 context "with a valid api token" do
322 context "with a valid api token" do
323 setup do
323 setup do
324 @user = User.generate_with_protected!(:admin => true)
324 @user = User.generate_with_protected!(:admin => true)
325 @token = Token.generate!(:user => @user, :action => 'api')
325 @token = Token.generate!(:user => @user, :action => 'api')
326 # Simple url parse to add on ?key= or &key=
326 # Simple url parse to add on ?key= or &key=
327 request_url = if url.match(/\?/)
327 request_url = if url.match(/\?/)
328 url + "&key=#{@token.value}"
328 url + "&key=#{@token.value}"
329 else
329 else
330 url + "?key=#{@token.value}"
330 url + "?key=#{@token.value}"
331 end
331 end
332 send(http_method, request_url, parameters)
332 send(http_method, request_url, parameters)
333 end
333 end
334
334
335 should_respond_with success_code
335 should_respond_with success_code
336 should_respond_with_content_type_based_on_url(url)
336 should_respond_with_content_type_based_on_url(url)
337 should_be_a_valid_response_string_based_on_url(url)
337 should_be_a_valid_response_string_based_on_url(url)
338 should "login as the user" do
338 should "login as the user" do
339 assert_equal @user, User.current
339 assert_equal @user, User.current
340 end
340 end
341 end
341 end
342
342
343 context "with an invalid api token" do
343 context "with an invalid api token" do
344 setup do
344 setup do
345 @user = User.generate_with_protected!
345 @user = User.generate_with_protected!
346 @token = Token.generate!(:user => @user, :action => 'feeds')
346 @token = Token.generate!(:user => @user, :action => 'feeds')
347 # Simple url parse to add on ?key= or &key=
347 # Simple url parse to add on ?key= or &key=
348 request_url = if url.match(/\?/)
348 request_url = if url.match(/\?/)
349 url + "&key=#{@token.value}"
349 url + "&key=#{@token.value}"
350 else
350 else
351 url + "?key=#{@token.value}"
351 url + "?key=#{@token.value}"
352 end
352 end
353 send(http_method, request_url, parameters)
353 send(http_method, request_url, parameters)
354 end
354 end
355
355
356 should_respond_with failure_code
356 should_respond_with failure_code
357 should_respond_with_content_type_based_on_url(url)
357 should_respond_with_content_type_based_on_url(url)
358 should "not login as the user" do
358 should "not login as the user" do
359 assert_equal User.anonymous, User.current
359 assert_equal User.anonymous, User.current
360 end
360 end
361 end
361 end
362 end
362 end
363
363
364 context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do
365 setup do
366 @user = User.generate_with_protected!(:admin => true)
367 @token = Token.generate!(:user => @user, :action => 'api')
368 send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s})
369 end
370
371 should_respond_with success_code
372 should_respond_with_content_type_based_on_url(url)
373 should_be_a_valid_response_string_based_on_url(url)
374 should "login as the user" do
375 assert_equal @user, User.current
376 end
377 end
364 end
378 end
365
379
366 # Uses should_respond_with_content_type based on what's in the url:
380 # Uses should_respond_with_content_type based on what's in the url:
367 #
381 #
368 # '/project/issues.xml' => should_respond_with_content_type :xml
382 # '/project/issues.xml' => should_respond_with_content_type :xml
369 # '/project/issues.json' => should_respond_with_content_type :json
383 # '/project/issues.json' => should_respond_with_content_type :json
370 #
384 #
371 # @param [String] url Request
385 # @param [String] url Request
372 def self.should_respond_with_content_type_based_on_url(url)
386 def self.should_respond_with_content_type_based_on_url(url)
373 case
387 case
374 when url.match(/xml/i)
388 when url.match(/xml/i)
375 should_respond_with_content_type :xml
389 should_respond_with_content_type :xml
376 when url.match(/json/i)
390 when url.match(/json/i)
377 should_respond_with_content_type :json
391 should_respond_with_content_type :json
378 else
392 else
379 raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}"
393 raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}"
380 end
394 end
381
395
382 end
396 end
383
397
384 # Uses the url to assert which format the response should be in
398 # Uses the url to assert which format the response should be in
385 #
399 #
386 # '/project/issues.xml' => should_be_a_valid_xml_string
400 # '/project/issues.xml' => should_be_a_valid_xml_string
387 # '/project/issues.json' => should_be_a_valid_json_string
401 # '/project/issues.json' => should_be_a_valid_json_string
388 #
402 #
389 # @param [String] url Request
403 # @param [String] url Request
390 def self.should_be_a_valid_response_string_based_on_url(url)
404 def self.should_be_a_valid_response_string_based_on_url(url)
391 case
405 case
392 when url.match(/xml/i)
406 when url.match(/xml/i)
393 should_be_a_valid_xml_string
407 should_be_a_valid_xml_string
394 when url.match(/json/i)
408 when url.match(/json/i)
395 should_be_a_valid_json_string
409 should_be_a_valid_json_string
396 else
410 else
397 raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}"
411 raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}"
398 end
412 end
399
413
400 end
414 end
401
415
402 # Checks that the response is a valid JSON string
416 # Checks that the response is a valid JSON string
403 def self.should_be_a_valid_json_string
417 def self.should_be_a_valid_json_string
404 should "be a valid JSON string (or empty)" do
418 should "be a valid JSON string (or empty)" do
405 assert (response.body.blank? || ActiveSupport::JSON.decode(response.body))
419 assert (response.body.blank? || ActiveSupport::JSON.decode(response.body))
406 end
420 end
407 end
421 end
408
422
409 # Checks that the response is a valid XML string
423 # Checks that the response is a valid XML string
410 def self.should_be_a_valid_xml_string
424 def self.should_be_a_valid_xml_string
411 should "be a valid XML string" do
425 should "be a valid XML string" do
412 assert REXML::Document.new(response.body)
426 assert REXML::Document.new(response.body)
413 end
427 end
414 end
428 end
415
429
416 end
430 end
417
431
418 # Simple module to "namespace" all of the API tests
432 # Simple module to "namespace" all of the API tests
419 module ApiTest
433 module ApiTest
420 end
434 end
General Comments 0
You need to be logged in to leave comments. Login now