##// END OF EJS Templates
Allow key authentication when updating issues (with tests) #6447...
Eric Davis -
r4252:7d934c984ae8
parent child
Show More
@@ -1,332 +1,332
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => [:new, :create]
19 menu_item :new_issue, :only => [:new, :create]
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :update]
22 before_filter :find_issue, :only => [:show, :edit, :update]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
25 before_filter :find_project, :only => [:new, :create]
25 before_filter :find_project, :only => [:new, :create]
26 before_filter :authorize, :except => [:index]
26 before_filter :authorize, :except => [:index]
27 before_filter :find_optional_project, :only => [:index]
27 before_filter :find_optional_project, :only => [:index]
28 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 before_filter :check_for_default_issue_status, :only => [:new, :create]
29 before_filter :build_new_issue_from_params, :only => [:new, :create]
29 before_filter :build_new_issue_from_params, :only => [:new, :create]
30 accept_key_auth :index, :show, :create
30 accept_key_auth :index, :show, :create, :update
31
31
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33
33
34 helper :journals
34 helper :journals
35 helper :projects
35 helper :projects
36 include ProjectsHelper
36 include ProjectsHelper
37 helper :custom_fields
37 helper :custom_fields
38 include CustomFieldsHelper
38 include CustomFieldsHelper
39 helper :issue_relations
39 helper :issue_relations
40 include IssueRelationsHelper
40 include IssueRelationsHelper
41 helper :watchers
41 helper :watchers
42 include WatchersHelper
42 include WatchersHelper
43 helper :attachments
43 helper :attachments
44 include AttachmentsHelper
44 include AttachmentsHelper
45 helper :queries
45 helper :queries
46 include QueriesHelper
46 include QueriesHelper
47 helper :sort
47 helper :sort
48 include SortHelper
48 include SortHelper
49 include IssuesHelper
49 include IssuesHelper
50 helper :timelog
50 helper :timelog
51 helper :gantt
51 helper :gantt
52 include Redmine::Export::PDF
52 include Redmine::Export::PDF
53
53
54 verify :method => [:post, :delete],
54 verify :method => [:post, :delete],
55 :only => :destroy,
55 :only => :destroy,
56 :render => { :nothing => true, :status => :method_not_allowed }
56 :render => { :nothing => true, :status => :method_not_allowed }
57
57
58 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
58 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
59 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
59 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
60 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
60 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
61
61
62 def index
62 def index
63 retrieve_query
63 retrieve_query
64 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
64 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
65 sort_update(@query.sortable_columns)
65 sort_update(@query.sortable_columns)
66
66
67 if @query.valid?
67 if @query.valid?
68 limit = case params[:format]
68 limit = case params[:format]
69 when 'csv', 'pdf'
69 when 'csv', 'pdf'
70 Setting.issues_export_limit.to_i
70 Setting.issues_export_limit.to_i
71 when 'atom'
71 when 'atom'
72 Setting.feeds_limit.to_i
72 Setting.feeds_limit.to_i
73 else
73 else
74 per_page_option
74 per_page_option
75 end
75 end
76
76
77 @issue_count = @query.issue_count
77 @issue_count = @query.issue_count
78 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
78 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
79 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
79 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
80 :order => sort_clause,
80 :order => sort_clause,
81 :offset => @issue_pages.current.offset,
81 :offset => @issue_pages.current.offset,
82 :limit => limit)
82 :limit => limit)
83 @issue_count_by_group = @query.issue_count_by_group
83 @issue_count_by_group = @query.issue_count_by_group
84
84
85 respond_to do |format|
85 respond_to do |format|
86 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
86 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
87 format.xml { render :layout => false }
87 format.xml { render :layout => false }
88 format.json { render :text => @issues.to_json, :layout => false }
88 format.json { render :text => @issues.to_json, :layout => false }
89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
90 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
90 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
91 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
91 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
92 end
92 end
93 else
93 else
94 # Send html if the query is not valid
94 # Send html if the query is not valid
95 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
95 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
96 end
96 end
97 rescue ActiveRecord::RecordNotFound
97 rescue ActiveRecord::RecordNotFound
98 render_404
98 render_404
99 end
99 end
100
100
101 def show
101 def show
102 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
102 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
103 @journals.each_with_index {|j,i| j.indice = i+1}
103 @journals.each_with_index {|j,i| j.indice = i+1}
104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
105 @changesets = @issue.changesets.visible.all
105 @changesets = @issue.changesets.visible.all
106 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
106 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
107 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
107 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
108 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
108 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
109 @priorities = IssuePriority.all
109 @priorities = IssuePriority.all
110 @time_entry = TimeEntry.new
110 @time_entry = TimeEntry.new
111 respond_to do |format|
111 respond_to do |format|
112 format.html { render :template => 'issues/show.rhtml' }
112 format.html { render :template => 'issues/show.rhtml' }
113 format.xml { render :layout => false }
113 format.xml { render :layout => false }
114 format.json { render :text => @issue.to_json, :layout => false }
114 format.json { render :text => @issue.to_json, :layout => false }
115 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
115 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
116 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
116 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
117 end
117 end
118 end
118 end
119
119
120 # Add a new issue
120 # Add a new issue
121 # The new issue will be created from an existing one if copy_from parameter is given
121 # The new issue will be created from an existing one if copy_from parameter is given
122 def new
122 def new
123 respond_to do |format|
123 respond_to do |format|
124 format.html { render :action => 'new', :layout => !request.xhr? }
124 format.html { render :action => 'new', :layout => !request.xhr? }
125 format.js { render :partial => 'attributes' }
125 format.js { render :partial => 'attributes' }
126 end
126 end
127 end
127 end
128
128
129 def create
129 def create
130 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
130 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
131 if @issue.save
131 if @issue.save
132 attachments = Attachment.attach_files(@issue, params[:attachments])
132 attachments = Attachment.attach_files(@issue, params[:attachments])
133 render_attachment_warning_if_needed(@issue)
133 render_attachment_warning_if_needed(@issue)
134 flash[:notice] = l(:notice_successful_create)
134 flash[:notice] = l(:notice_successful_create)
135 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
135 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
136 respond_to do |format|
136 respond_to do |format|
137 format.html {
137 format.html {
138 redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
138 redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
139 { :action => 'show', :id => @issue })
139 { :action => 'show', :id => @issue })
140 }
140 }
141 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
141 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
142 format.json { render :text => @issue.to_json, :status => :created, :location => url_for(:controller => 'issues', :action => 'show'), :layout => false }
142 format.json { render :text => @issue.to_json, :status => :created, :location => url_for(:controller => 'issues', :action => 'show'), :layout => false }
143 end
143 end
144 return
144 return
145 else
145 else
146 respond_to do |format|
146 respond_to do |format|
147 format.html { render :action => 'new' }
147 format.html { render :action => 'new' }
148 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
148 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
149 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
149 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
150 end
150 end
151 end
151 end
152 end
152 end
153
153
154 # Attributes that can be updated on workflow transition (without :edit permission)
154 # Attributes that can be updated on workflow transition (without :edit permission)
155 # TODO: make it configurable (at least per role)
155 # TODO: make it configurable (at least per role)
156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
157
157
158 def edit
158 def edit
159 update_issue_from_params
159 update_issue_from_params
160
160
161 @journal = @issue.current_journal
161 @journal = @issue.current_journal
162
162
163 respond_to do |format|
163 respond_to do |format|
164 format.html { }
164 format.html { }
165 format.xml { }
165 format.xml { }
166 end
166 end
167 end
167 end
168
168
169 def update
169 def update
170 update_issue_from_params
170 update_issue_from_params
171
171
172 if @issue.save_issue_with_child_records(params, @time_entry)
172 if @issue.save_issue_with_child_records(params, @time_entry)
173 render_attachment_warning_if_needed(@issue)
173 render_attachment_warning_if_needed(@issue)
174 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
174 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
175
175
176 respond_to do |format|
176 respond_to do |format|
177 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
177 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
178 format.xml { head :ok }
178 format.xml { head :ok }
179 format.json { head :ok }
179 format.json { head :ok }
180 end
180 end
181 else
181 else
182 render_attachment_warning_if_needed(@issue)
182 render_attachment_warning_if_needed(@issue)
183 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
183 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
184 @journal = @issue.current_journal
184 @journal = @issue.current_journal
185
185
186 respond_to do |format|
186 respond_to do |format|
187 format.html { render :action => 'edit' }
187 format.html { render :action => 'edit' }
188 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
188 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
189 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
189 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
190 end
190 end
191 end
191 end
192 end
192 end
193
193
194 # Bulk edit a set of issues
194 # Bulk edit a set of issues
195 def bulk_edit
195 def bulk_edit
196 @issues.sort!
196 @issues.sort!
197 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
197 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
198 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
198 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
199 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
199 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
200 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
200 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
201 end
201 end
202
202
203 def bulk_update
203 def bulk_update
204 @issues.sort!
204 @issues.sort!
205 attributes = parse_params_for_bulk_issue_attributes(params)
205 attributes = parse_params_for_bulk_issue_attributes(params)
206
206
207 unsaved_issue_ids = []
207 unsaved_issue_ids = []
208 @issues.each do |issue|
208 @issues.each do |issue|
209 issue.reload
209 issue.reload
210 journal = issue.init_journal(User.current, params[:notes])
210 journal = issue.init_journal(User.current, params[:notes])
211 issue.safe_attributes = attributes
211 issue.safe_attributes = attributes
212 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
212 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
213 unless issue.save
213 unless issue.save
214 # Keep unsaved issue ids to display them in flash error
214 # Keep unsaved issue ids to display them in flash error
215 unsaved_issue_ids << issue.id
215 unsaved_issue_ids << issue.id
216 end
216 end
217 end
217 end
218 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
218 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
219 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
219 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
220 end
220 end
221
221
222 def destroy
222 def destroy
223 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
223 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
224 if @hours > 0
224 if @hours > 0
225 case params[:todo]
225 case params[:todo]
226 when 'destroy'
226 when 'destroy'
227 # nothing to do
227 # nothing to do
228 when 'nullify'
228 when 'nullify'
229 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
229 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
230 when 'reassign'
230 when 'reassign'
231 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
231 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
232 if reassign_to.nil?
232 if reassign_to.nil?
233 flash.now[:error] = l(:error_issue_not_found_in_project)
233 flash.now[:error] = l(:error_issue_not_found_in_project)
234 return
234 return
235 else
235 else
236 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
236 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
237 end
237 end
238 else
238 else
239 unless params[:format] == 'xml' || params[:format] == 'json'
239 unless params[:format] == 'xml' || params[:format] == 'json'
240 # display the destroy form if it's a user request
240 # display the destroy form if it's a user request
241 return
241 return
242 end
242 end
243 end
243 end
244 end
244 end
245 @issues.each(&:destroy)
245 @issues.each(&:destroy)
246 respond_to do |format|
246 respond_to do |format|
247 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
247 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
248 format.xml { head :ok }
248 format.xml { head :ok }
249 format.json { head :ok }
249 format.json { head :ok }
250 end
250 end
251 end
251 end
252
252
253 private
253 private
254 def find_issue
254 def find_issue
255 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
255 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
256 @project = @issue.project
256 @project = @issue.project
257 rescue ActiveRecord::RecordNotFound
257 rescue ActiveRecord::RecordNotFound
258 render_404
258 render_404
259 end
259 end
260
260
261 def find_project
261 def find_project
262 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
262 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
263 @project = Project.find(project_id)
263 @project = Project.find(project_id)
264 rescue ActiveRecord::RecordNotFound
264 rescue ActiveRecord::RecordNotFound
265 render_404
265 render_404
266 end
266 end
267
267
268 # Used by #edit and #update to set some common instance variables
268 # Used by #edit and #update to set some common instance variables
269 # from the params
269 # from the params
270 # TODO: Refactor, not everything in here is needed by #edit
270 # TODO: Refactor, not everything in here is needed by #edit
271 def update_issue_from_params
271 def update_issue_from_params
272 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
272 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
273 @priorities = IssuePriority.all
273 @priorities = IssuePriority.all
274 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
274 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
275 @time_entry = TimeEntry.new
275 @time_entry = TimeEntry.new
276
276
277 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
277 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
278 @issue.init_journal(User.current, @notes)
278 @issue.init_journal(User.current, @notes)
279 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
279 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
280 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
280 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
281 attrs = params[:issue].dup
281 attrs = params[:issue].dup
282 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
282 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
283 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
283 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
284 @issue.safe_attributes = attrs
284 @issue.safe_attributes = attrs
285 end
285 end
286
286
287 end
287 end
288
288
289 # TODO: Refactor, lots of extra code in here
289 # TODO: Refactor, lots of extra code in here
290 # TODO: Changing tracker on an existing issue should not trigger this
290 # TODO: Changing tracker on an existing issue should not trigger this
291 def build_new_issue_from_params
291 def build_new_issue_from_params
292 if params[:id].blank?
292 if params[:id].blank?
293 @issue = Issue.new
293 @issue = Issue.new
294 @issue.copy_from(params[:copy_from]) if params[:copy_from]
294 @issue.copy_from(params[:copy_from]) if params[:copy_from]
295 @issue.project = @project
295 @issue.project = @project
296 else
296 else
297 @issue = @project.issues.visible.find(params[:id])
297 @issue = @project.issues.visible.find(params[:id])
298 end
298 end
299
299
300 @issue.project = @project
300 @issue.project = @project
301 # Tracker must be set before custom field values
301 # Tracker must be set before custom field values
302 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
302 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
303 if @issue.tracker.nil?
303 if @issue.tracker.nil?
304 render_error l(:error_no_tracker_in_project)
304 render_error l(:error_no_tracker_in_project)
305 return false
305 return false
306 end
306 end
307 if params[:issue].is_a?(Hash)
307 if params[:issue].is_a?(Hash)
308 @issue.safe_attributes = params[:issue]
308 @issue.safe_attributes = params[:issue]
309 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
309 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
310 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
310 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
311 end
311 end
312 end
312 end
313 @issue.author = User.current
313 @issue.author = User.current
314 @issue.start_date ||= Date.today
314 @issue.start_date ||= Date.today
315 @priorities = IssuePriority.all
315 @priorities = IssuePriority.all
316 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
316 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
317 end
317 end
318
318
319 def check_for_default_issue_status
319 def check_for_default_issue_status
320 if IssueStatus.default.nil?
320 if IssueStatus.default.nil?
321 render_error l(:error_no_default_issue_status)
321 render_error l(:error_no_default_issue_status)
322 return false
322 return false
323 end
323 end
324 end
324 end
325
325
326 def parse_params_for_bulk_issue_attributes(params)
326 def parse_params_for_bulk_issue_attributes(params)
327 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
327 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
328 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
328 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
329 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
329 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
330 attributes
330 attributes
331 end
331 end
332 end
332 end
@@ -1,315 +1,336
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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.dirname(__FILE__)}/../../test_helper"
18 require "#{File.dirname(__FILE__)}/../../test_helper"
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
44
45 def setup
45 def setup
46 Setting.rest_api_enabled = '1'
46 Setting.rest_api_enabled = '1'
47 end
47 end
48
48
49 # Use a private project to make sure auth is really working and not just
49 # Use a private project to make sure auth is really working and not just
50 # only showing public issues.
50 # only showing public issues.
51 context "/index.xml" do
51 context "/index.xml" do
52 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
52 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
53 end
53 end
54
54
55 context "/index.json" do
55 context "/index.json" do
56 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
56 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
57 end
57 end
58
58
59 context "/index.xml with filter" do
59 context "/index.xml with filter" do
60 should_allow_api_authentication(:get, "/projects/private-child/issues.xml?status_id=5")
60 should_allow_api_authentication(:get, "/projects/private-child/issues.xml?status_id=5")
61
61
62 should "show only issues with the status_id" do
62 should "show only issues with the status_id" do
63 get '/issues.xml?status_id=5'
63 get '/issues.xml?status_id=5'
64 assert_tag :tag => 'issues',
64 assert_tag :tag => 'issues',
65 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
65 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
66 :only => { :tag => 'issue' } }
66 :only => { :tag => 'issue' } }
67 end
67 end
68 end
68 end
69
69
70 context "/index.json with filter" do
70 context "/index.json with filter" do
71 should_allow_api_authentication(:get, "/projects/private-child/issues.json?status_id=5")
71 should_allow_api_authentication(:get, "/projects/private-child/issues.json?status_id=5")
72
72
73 should "show only issues with the status_id" do
73 should "show only issues with the status_id" do
74 get '/issues.json?status_id=5'
74 get '/issues.json?status_id=5'
75
75
76 json = ActiveSupport::JSON.decode(response.body)
76 json = ActiveSupport::JSON.decode(response.body)
77 status_ids_used = json.collect {|j| j['status_id'] }
77 status_ids_used = json.collect {|j| j['status_id'] }
78 assert_equal 3, status_ids_used.length
78 assert_equal 3, status_ids_used.length
79 assert status_ids_used.all? {|id| id == 5 }
79 assert status_ids_used.all? {|id| id == 5 }
80 end
80 end
81
81
82 end
82 end
83
83
84 # Issue 6 is on a private project
84 # Issue 6 is on a private project
85 context "/issues/6.xml" do
85 context "/issues/6.xml" do
86 should_allow_api_authentication(:get, "/issues/6.xml")
86 should_allow_api_authentication(:get, "/issues/6.xml")
87 end
87 end
88
88
89 context "/issues/6.json" do
89 context "/issues/6.json" do
90 should_allow_api_authentication(:get, "/issues/6.json")
90 should_allow_api_authentication(:get, "/issues/6.json")
91 end
91 end
92
92
93 context "POST /issues.xml" do
93 context "POST /issues.xml" do
94 should_allow_api_authentication(:post,
94 should_allow_api_authentication(:post,
95 '/issues.xml',
95 '/issues.xml',
96 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
96 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
97 {:success_code => :created})
97 {:success_code => :created})
98
98
99 should "create an issue with the attributes" do
99 should "create an issue with the attributes" do
100 assert_difference('Issue.count') do
100 assert_difference('Issue.count') do
101 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
101 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
102 end
102 end
103
103
104 issue = Issue.first(:order => 'id DESC')
104 issue = Issue.first(:order => 'id DESC')
105 assert_equal 1, issue.project_id
105 assert_equal 1, issue.project_id
106 assert_equal 2, issue.tracker_id
106 assert_equal 2, issue.tracker_id
107 assert_equal 3, issue.status_id
107 assert_equal 3, issue.status_id
108 assert_equal 'API test', issue.subject
108 assert_equal 'API test', issue.subject
109 end
109 end
110 end
110 end
111
111
112 context "POST /issues.xml with failure" do
112 context "POST /issues.xml with failure" do
113 should_allow_api_authentication(:post,
113 should_allow_api_authentication(:post,
114 '/issues.xml',
114 '/issues.xml',
115 {:issue => {:project_id => 1}},
115 {:issue => {:project_id => 1}},
116 {:success_code => :unprocessable_entity})
116 {:success_code => :unprocessable_entity})
117
117
118 should "have an errors tag" do
118 should "have an errors tag" do
119 assert_no_difference('Issue.count') do
119 assert_no_difference('Issue.count') do
120 post '/issues.xml', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
120 post '/issues.xml', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
121 end
121 end
122
122
123 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
123 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
124 end
124 end
125 end
125 end
126
126
127 context "POST /issues.json" do
127 context "POST /issues.json" do
128 should_allow_api_authentication(:post,
128 should_allow_api_authentication(:post,
129 '/issues.json',
129 '/issues.json',
130 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
130 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
131 {:success_code => :created})
131 {:success_code => :created})
132
132
133 should "create an issue with the attributes" do
133 should "create an issue with the attributes" do
134 assert_difference('Issue.count') do
134 assert_difference('Issue.count') do
135 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
135 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
136 end
136 end
137
137
138 issue = Issue.first(:order => 'id DESC')
138 issue = Issue.first(:order => 'id DESC')
139 assert_equal 1, issue.project_id
139 assert_equal 1, issue.project_id
140 assert_equal 2, issue.tracker_id
140 assert_equal 2, issue.tracker_id
141 assert_equal 3, issue.status_id
141 assert_equal 3, issue.status_id
142 assert_equal 'API test', issue.subject
142 assert_equal 'API test', issue.subject
143 end
143 end
144
144
145 end
145 end
146
146
147 context "POST /issues.json with failure" do
147 context "POST /issues.json with failure" do
148 should_allow_api_authentication(:post,
148 should_allow_api_authentication(:post,
149 '/issues.json',
149 '/issues.json',
150 {:issue => {:project_id => 1}},
150 {:issue => {:project_id => 1}},
151 {:success_code => :unprocessable_entity})
151 {:success_code => :unprocessable_entity})
152
152
153 should "have an errors element" do
153 should "have an errors element" do
154 assert_no_difference('Issue.count') do
154 assert_no_difference('Issue.count') do
155 post '/issues.json', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
155 post '/issues.json', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
156 end
156 end
157
157
158 json = ActiveSupport::JSON.decode(response.body)
158 json = ActiveSupport::JSON.decode(response.body)
159 assert_equal "can't be blank", json.first['subject']
159 assert_equal "can't be blank", json.first['subject']
160 end
160 end
161 end
161 end
162
162
163 context "PUT /issues/1.xml" do
163 # Issue 6 is on a private project
164 context "PUT /issues/6.xml" do
164 setup do
165 setup do
165 @issue_count = Issue.count
166 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
166 @journal_count = Journal.count
167 @headers = { :authorization => credentials('jsmith') }
167 @attributes = {:subject => 'API update', :notes => 'A new note'}
168
169 put '/issues/1.xml', {:issue => @attributes}, :authorization => credentials('jsmith')
170 end
168 end
171
169
172 should_respond_with :ok
170 should_allow_api_authentication(:put,
173 should_respond_with_content_type 'application/xml'
171 '/issues/6.xml',
172 {:issue => {:subject => 'API update', :notes => 'A new note'}},
173 {:success_code => :ok})
174
174
175 should "not create a new issue" do
175 should "not create a new issue" do
176 assert_equal Issue.count, @issue_count
176 assert_no_difference('Issue.count') do
177 put '/issues/6.xml', @parameters, @headers
178 end
177 end
179 end
178
180
179 should "create a new journal" do
181 should "create a new journal" do
180 assert_equal Journal.count, @journal_count + 1
182 assert_difference('Journal.count') do
183 put '/issues/6.xml', @parameters, @headers
184 end
181 end
185 end
182
186
183 should "add the note to the journal" do
187 should "add the note to the journal" do
188 put '/issues/6.xml', @parameters, @headers
189
184 journal = Journal.last
190 journal = Journal.last
185 assert_equal "A new note", journal.notes
191 assert_equal "A new note", journal.notes
186 end
192 end
187
193
188 should "update the issue" do
194 should "update the issue" do
189 issue = Issue.find(1)
195 put '/issues/6.xml', @parameters, @headers
190 @attributes.each do |attribute, value|
196
191 assert_equal value, issue.send(attribute) unless attribute == :notes
197 issue = Issue.find(6)
192 end
198 assert_equal "API update", issue.subject
193 end
199 end
194
200
195 end
201 end
196
202
197 context "PUT /issues/1.xml with failed update" do
203 context "PUT /issues/6.xml with failed update" do
198 setup do
204 setup do
199 @attributes = {:subject => ''}
205 @parameters = {:issue => {:subject => ''}}
200 @issue_count = Issue.count
206 @headers = { :authorization => credentials('jsmith') }
201 @journal_count = Journal.count
202
203 put '/issues/1.xml', {:issue => @attributes}, :authorization => credentials('jsmith')
204 end
207 end
205
208
206 should_respond_with :unprocessable_entity
209 should_allow_api_authentication(:put,
207 should_respond_with_content_type 'application/xml'
210 '/issues/6.xml',
208
211 {:issue => {:subject => ''}}, # Missing subject should fail
212 {:success_code => :unprocessable_entity})
213
209 should "not create a new issue" do
214 should "not create a new issue" do
210 assert_equal Issue.count, @issue_count
215 assert_no_difference('Issue.count') do
216 put '/issues/6.xml', @parameters, @headers
217 end
211 end
218 end
212
219
213 should "not create a new journal" do
220 should "not create a new journal" do
214 assert_equal Journal.count, @journal_count
221 assert_no_difference('Journal.count') do
222 put '/issues/6.xml', @parameters, @headers
223 end
215 end
224 end
216
225
217 should "have an errors tag" do
226 should "have an errors tag" do
227 put '/issues/6.xml', @parameters, @headers
228
218 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
229 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
219 end
230 end
220 end
231 end
221
232
222 context "PUT /issues/1.json" do
233 context "PUT /issues/6.json" do
223 setup do
234 setup do
224 @issue_count = Issue.count
235 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
225 @journal_count = Journal.count
236 @headers = { :authorization => credentials('jsmith') }
226 @attributes = {:subject => 'API update', :notes => 'A new note'}
227
228 put '/issues/1.json', {:issue => @attributes}, :authorization => credentials('jsmith')
229 end
237 end
230
238
231 should_respond_with :ok
239 should_allow_api_authentication(:put,
232 should_respond_with_content_type 'application/json'
240 '/issues/6.json',
241 {:issue => {:subject => 'API update', :notes => 'A new note'}},
242 {:success_code => :ok})
233
243
234 should "not create a new issue" do
244 should "not create a new issue" do
235 assert_equal Issue.count, @issue_count
245 assert_no_difference('Issue.count') do
246 put '/issues/6.json', @parameters, @headers
247 end
236 end
248 end
237
249
238 should "create a new journal" do
250 should "create a new journal" do
239 assert_equal Journal.count, @journal_count + 1
251 assert_difference('Journal.count') do
252 put '/issues/6.json', @parameters, @headers
253 end
240 end
254 end
241
255
242 should "add the note to the journal" do
256 should "add the note to the journal" do
257 put '/issues/6.json', @parameters, @headers
258
243 journal = Journal.last
259 journal = Journal.last
244 assert_equal "A new note", journal.notes
260 assert_equal "A new note", journal.notes
245 end
261 end
246
262
247 should "update the issue" do
263 should "update the issue" do
248 issue = Issue.find(1)
264 put '/issues/6.json', @parameters, @headers
249 @attributes.each do |attribute, value|
265
250 assert_equal value, issue.send(attribute) unless attribute == :notes
266 issue = Issue.find(6)
251 end
267 assert_equal "API update", issue.subject
252 end
268 end
253
269
254 end
270 end
255
271
256 context "PUT /issues/1.json with failed update" do
272 context "PUT /issues/6.json with failed update" do
257 setup do
273 setup do
258 @attributes = {:subject => ''}
274 @parameters = {:issue => {:subject => ''}}
259 @issue_count = Issue.count
275 @headers = { :authorization => credentials('jsmith') }
260 @journal_count = Journal.count
261
262 put '/issues/1.json', {:issue => @attributes}, :authorization => credentials('jsmith')
263 end
276 end
264
277
265 should_respond_with :unprocessable_entity
278 should_allow_api_authentication(:put,
266 should_respond_with_content_type 'application/json'
279 '/issues/6.json',
267
280 {:issue => {:subject => ''}}, # Missing subject should fail
281 {:success_code => :unprocessable_entity})
282
268 should "not create a new issue" do
283 should "not create a new issue" do
269 assert_equal Issue.count, @issue_count
284 assert_no_difference('Issue.count') do
285 put '/issues/6.json', @parameters, @headers
286 end
270 end
287 end
271
288
272 should "not create a new journal" do
289 should "not create a new journal" do
273 assert_equal Journal.count, @journal_count
290 assert_no_difference('Journal.count') do
291 put '/issues/6.json', @parameters, @headers
292 end
274 end
293 end
275
294
276 should "have an errors attribute" do
295 should "have an errors attribute" do
296 put '/issues/6.json', @parameters, @headers
297
277 json = ActiveSupport::JSON.decode(response.body)
298 json = ActiveSupport::JSON.decode(response.body)
278 assert_equal "can't be blank", json.first['subject']
299 assert_equal "can't be blank", json.first['subject']
279 end
300 end
280 end
301 end
281
302
282 context "DELETE /issues/1.xml" do
303 context "DELETE /issues/1.xml" do
283 setup do
304 setup do
284 @issue_count = Issue.count
305 @issue_count = Issue.count
285 delete '/issues/1.xml', {}, :authorization => credentials('jsmith')
306 delete '/issues/1.xml', {}, :authorization => credentials('jsmith')
286 end
307 end
287
308
288 should_respond_with :ok
309 should_respond_with :ok
289 should_respond_with_content_type 'application/xml'
310 should_respond_with_content_type 'application/xml'
290
311
291 should "delete the issue" do
312 should "delete the issue" do
292 assert_equal Issue.count, @issue_count -1
313 assert_equal Issue.count, @issue_count -1
293 assert_nil Issue.find_by_id(1)
314 assert_nil Issue.find_by_id(1)
294 end
315 end
295 end
316 end
296
317
297 context "DELETE /issues/1.json" do
318 context "DELETE /issues/1.json" do
298 setup do
319 setup do
299 @issue_count = Issue.count
320 @issue_count = Issue.count
300 delete '/issues/1.json', {}, :authorization => credentials('jsmith')
321 delete '/issues/1.json', {}, :authorization => credentials('jsmith')
301 end
322 end
302
323
303 should_respond_with :ok
324 should_respond_with :ok
304 should_respond_with_content_type 'application/json'
325 should_respond_with_content_type 'application/json'
305
326
306 should "delete the issue" do
327 should "delete the issue" do
307 assert_equal Issue.count, @issue_count -1
328 assert_equal Issue.count, @issue_count -1
308 assert_nil Issue.find_by_id(1)
329 assert_nil Issue.find_by_id(1)
309 end
330 end
310 end
331 end
311
332
312 def credentials(user, password=nil)
333 def credentials(user, password=nil)
313 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
334 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
314 end
335 end
315 end
336 end
@@ -1,420 +1,420
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({:tag => 'p', :attributes => { :id => 'errorExplanation' }}.merge(options))
118 assert_tag({:tag => 'p', :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 end
364 end
365
365
366 # Uses should_respond_with_content_type based on what's in the url:
366 # Uses should_respond_with_content_type based on what's in the url:
367 #
367 #
368 # '/project/issues.xml' => should_respond_with_content_type :xml
368 # '/project/issues.xml' => should_respond_with_content_type :xml
369 # '/project/issues.json' => should_respond_with_content_type :json
369 # '/project/issues.json' => should_respond_with_content_type :json
370 #
370 #
371 # @param [String] url Request
371 # @param [String] url Request
372 def self.should_respond_with_content_type_based_on_url(url)
372 def self.should_respond_with_content_type_based_on_url(url)
373 case
373 case
374 when url.match(/xml/i)
374 when url.match(/xml/i)
375 should_respond_with_content_type :xml
375 should_respond_with_content_type :xml
376 when url.match(/json/i)
376 when url.match(/json/i)
377 should_respond_with_content_type :json
377 should_respond_with_content_type :json
378 else
378 else
379 raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}"
379 raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}"
380 end
380 end
381
381
382 end
382 end
383
383
384 # Uses the url to assert which format the response should be in
384 # Uses the url to assert which format the response should be in
385 #
385 #
386 # '/project/issues.xml' => should_be_a_valid_xml_string
386 # '/project/issues.xml' => should_be_a_valid_xml_string
387 # '/project/issues.json' => should_be_a_valid_json_string
387 # '/project/issues.json' => should_be_a_valid_json_string
388 #
388 #
389 # @param [String] url Request
389 # @param [String] url Request
390 def self.should_be_a_valid_response_string_based_on_url(url)
390 def self.should_be_a_valid_response_string_based_on_url(url)
391 case
391 case
392 when url.match(/xml/i)
392 when url.match(/xml/i)
393 should_be_a_valid_xml_string
393 should_be_a_valid_xml_string
394 when url.match(/json/i)
394 when url.match(/json/i)
395 should_be_a_valid_json_string
395 should_be_a_valid_json_string
396 else
396 else
397 raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}"
397 raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}"
398 end
398 end
399
399
400 end
400 end
401
401
402 # Checks that the response is a valid JSON string
402 # Checks that the response is a valid JSON string
403 def self.should_be_a_valid_json_string
403 def self.should_be_a_valid_json_string
404 should "be a valid JSON string" do
404 should "be a valid JSON string (or empty)" do
405 assert ActiveSupport::JSON.decode(response.body)
405 assert (response.body.blank? || ActiveSupport::JSON.decode(response.body))
406 end
406 end
407 end
407 end
408
408
409 # Checks that the response is a valid XML string
409 # Checks that the response is a valid XML string
410 def self.should_be_a_valid_xml_string
410 def self.should_be_a_valid_xml_string
411 should "be a valid XML string" do
411 should "be a valid XML string" do
412 assert REXML::Document.new(response.body)
412 assert REXML::Document.new(response.body)
413 end
413 end
414 end
414 end
415
415
416 end
416 end
417
417
418 # Simple module to "namespace" all of the API tests
418 # Simple module to "namespace" all of the API tests
419 module ApiTest
419 module ApiTest
420 end
420 end
General Comments 0
You need to be logged in to leave comments. Login now