##// END OF EJS Templates
Respond with errors and appropriate content type on /issues API calls with invalid query params (#8883)....
Jean-Philippe Lang -
r6189:fdd5367ebab2
parent child
Show More
@@ -1,336 +1,338
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssuesController < ApplicationController
19 19 menu_item :new_issue, :only => [:new, :create]
20 20 default_search_scope :issues
21 21
22 22 before_filter :find_issue, :only => [:show, :edit, :update]
23 23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
25 25 before_filter :find_project, :only => [:new, :create]
26 26 before_filter :authorize, :except => [:index]
27 27 before_filter :find_optional_project, :only => [:index]
28 28 before_filter :check_for_default_issue_status, :only => [:new, :create]
29 29 before_filter :build_new_issue_from_params, :only => [:new, :create]
30 30 accept_rss_auth :index, :show
31 31 accept_api_auth :index, :show, :create, :update, :destroy
32 32
33 33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
34 34
35 35 helper :journals
36 36 helper :projects
37 37 include ProjectsHelper
38 38 helper :custom_fields
39 39 include CustomFieldsHelper
40 40 helper :issue_relations
41 41 include IssueRelationsHelper
42 42 helper :watchers
43 43 include WatchersHelper
44 44 helper :attachments
45 45 include AttachmentsHelper
46 46 helper :queries
47 47 include QueriesHelper
48 48 helper :repositories
49 49 include RepositoriesHelper
50 50 helper :sort
51 51 include SortHelper
52 52 include IssuesHelper
53 53 helper :timelog
54 54 helper :gantt
55 55 include Redmine::Export::PDF
56 56
57 57 verify :method => [:post, :delete],
58 58 :only => :destroy,
59 59 :render => { :nothing => true, :status => :method_not_allowed }
60 60
61 61 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
62 62 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
63 63 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
64 64
65 65 def index
66 66 retrieve_query
67 67 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
68 68 sort_update(@query.sortable_columns)
69 69
70 70 if @query.valid?
71 71 case params[:format]
72 72 when 'csv', 'pdf'
73 73 @limit = Setting.issues_export_limit.to_i
74 74 when 'atom'
75 75 @limit = Setting.feeds_limit.to_i
76 76 when 'xml', 'json'
77 77 @offset, @limit = api_offset_and_limit
78 78 else
79 79 @limit = per_page_option
80 80 end
81 81
82 82 @issue_count = @query.issue_count
83 83 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
84 84 @offset ||= @issue_pages.current.offset
85 85 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
86 86 :order => sort_clause,
87 87 :offset => @offset,
88 88 :limit => @limit)
89 89 @issue_count_by_group = @query.issue_count_by_group
90 90
91 91 respond_to do |format|
92 92 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
93 93 format.api
94 94 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
95 95 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
96 96 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
97 97 end
98 98 else
99 # Send html if the query is not valid
100 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
99 respond_to do |format|
100 format.any(:html, :atom, :csv, :pdf) { render(:template => 'issues/index.rhtml', :layout => !request.xhr?) }
101 format.api { render_validation_errors(@query) }
102 end
101 103 end
102 104 rescue ActiveRecord::RecordNotFound
103 105 render_404
104 106 end
105 107
106 108 def show
107 109 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
108 110 @journals.each_with_index {|j,i| j.indice = i+1}
109 111 @journals.reverse! if User.current.wants_comments_in_reverse_order?
110 112
111 113 if User.current.allowed_to?(:view_changesets, @project)
112 114 @changesets = @issue.changesets.visible.all
113 115 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
114 116 end
115 117
116 118 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
117 119 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
118 120 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
119 121 @priorities = IssuePriority.active
120 122 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
121 123 respond_to do |format|
122 124 format.html { render :template => 'issues/show.rhtml' }
123 125 format.api
124 126 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
125 127 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
126 128 end
127 129 end
128 130
129 131 # Add a new issue
130 132 # The new issue will be created from an existing one if copy_from parameter is given
131 133 def new
132 134 respond_to do |format|
133 135 format.html { render :action => 'new', :layout => !request.xhr? }
134 136 format.js { render :partial => 'attributes' }
135 137 end
136 138 end
137 139
138 140 def create
139 141 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
140 142 if @issue.save
141 143 attachments = Attachment.attach_files(@issue, params[:attachments])
142 144 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
143 145 respond_to do |format|
144 146 format.html {
145 147 render_attachment_warning_if_needed(@issue)
146 148 flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
147 149 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?} } :
148 150 { :action => 'show', :id => @issue })
149 151 }
150 152 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
151 153 end
152 154 return
153 155 else
154 156 respond_to do |format|
155 157 format.html { render :action => 'new' }
156 158 format.api { render_validation_errors(@issue) }
157 159 end
158 160 end
159 161 end
160 162
161 163 def edit
162 164 update_issue_from_params
163 165
164 166 @journal = @issue.current_journal
165 167
166 168 respond_to do |format|
167 169 format.html { }
168 170 format.xml { }
169 171 end
170 172 end
171 173
172 174 def update
173 175 update_issue_from_params
174 176
175 177 if @issue.save_issue_with_child_records(params, @time_entry)
176 178 render_attachment_warning_if_needed(@issue)
177 179 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
178 180
179 181 respond_to do |format|
180 182 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
181 183 format.api { head :ok }
182 184 end
183 185 else
184 186 render_attachment_warning_if_needed(@issue)
185 187 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
186 188 @journal = @issue.current_journal
187 189
188 190 respond_to do |format|
189 191 format.html { render :action => 'edit' }
190 192 format.api { render_validation_errors(@issue) }
191 193 end
192 194 end
193 195 end
194 196
195 197 # Bulk edit a set of issues
196 198 def bulk_edit
197 199 @issues.sort!
198 200 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
199 201 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
200 202 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
201 203 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
202 204 end
203 205
204 206 def bulk_update
205 207 @issues.sort!
206 208 attributes = parse_params_for_bulk_issue_attributes(params)
207 209
208 210 unsaved_issue_ids = []
209 211 @issues.each do |issue|
210 212 issue.reload
211 213 journal = issue.init_journal(User.current, params[:notes])
212 214 issue.safe_attributes = attributes
213 215 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
214 216 unless issue.save
215 217 # Keep unsaved issue ids to display them in flash error
216 218 unsaved_issue_ids << issue.id
217 219 end
218 220 end
219 221 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
220 222 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
221 223 end
222 224
223 225 def destroy
224 226 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
225 227 if @hours > 0
226 228 case params[:todo]
227 229 when 'destroy'
228 230 # nothing to do
229 231 when 'nullify'
230 232 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
231 233 when 'reassign'
232 234 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
233 235 if reassign_to.nil?
234 236 flash.now[:error] = l(:error_issue_not_found_in_project)
235 237 return
236 238 else
237 239 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
238 240 end
239 241 else
240 242 # display the destroy form if it's a user request
241 243 return unless api_request?
242 244 end
243 245 end
244 246 @issues.each do |issue|
245 247 begin
246 248 issue.reload.destroy
247 249 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
248 250 # nothing to do, issue was already deleted (eg. by a parent)
249 251 end
250 252 end
251 253 respond_to do |format|
252 254 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
253 255 format.api { head :ok }
254 256 end
255 257 end
256 258
257 259 private
258 260 def find_issue
259 261 # Issue.visible.find(...) can not be used to redirect user to the login form
260 262 # if the issue actually exists but requires authentication
261 263 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
262 264 unless @issue.visible?
263 265 deny_access
264 266 return
265 267 end
266 268 @project = @issue.project
267 269 rescue ActiveRecord::RecordNotFound
268 270 render_404
269 271 end
270 272
271 273 def find_project
272 274 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
273 275 @project = Project.find(project_id)
274 276 rescue ActiveRecord::RecordNotFound
275 277 render_404
276 278 end
277 279
278 280 # Used by #edit and #update to set some common instance variables
279 281 # from the params
280 282 # TODO: Refactor, not everything in here is needed by #edit
281 283 def update_issue_from_params
282 284 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
283 285 @priorities = IssuePriority.active
284 286 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
285 287 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
286 288 @time_entry.attributes = params[:time_entry]
287 289
288 290 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
289 291 @issue.init_journal(User.current, @notes)
290 292 @issue.safe_attributes = params[:issue]
291 293 end
292 294
293 295 # TODO: Refactor, lots of extra code in here
294 296 # TODO: Changing tracker on an existing issue should not trigger this
295 297 def build_new_issue_from_params
296 298 if params[:id].blank?
297 299 @issue = Issue.new
298 300 @issue.copy_from(params[:copy_from]) if params[:copy_from]
299 301 @issue.project = @project
300 302 else
301 303 @issue = @project.issues.visible.find(params[:id])
302 304 end
303 305
304 306 @issue.project = @project
305 307 @issue.author = User.current
306 308 # Tracker must be set before custom field values
307 309 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
308 310 if @issue.tracker.nil?
309 311 render_error l(:error_no_tracker_in_project)
310 312 return false
311 313 end
312 314 @issue.start_date ||= Date.today
313 315 if params[:issue].is_a?(Hash)
314 316 @issue.safe_attributes = params[:issue]
315 317 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
316 318 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
317 319 end
318 320 end
319 321 @priorities = IssuePriority.active
320 322 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
321 323 end
322 324
323 325 def check_for_default_issue_status
324 326 if IssueStatus.default.nil?
325 327 render_error l(:error_no_default_issue_status)
326 328 return false
327 329 end
328 330 end
329 331
330 332 def parse_params_for_bulk_issue_attributes(params)
331 333 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
332 334 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
333 335 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
334 336 attributes
335 337 end
336 338 end
@@ -1,523 +1,533
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../test_helper', __FILE__)
19 19
20 20 class ApiTest::IssuesTest < ActionController::IntegrationTest
21 21 fixtures :projects,
22 22 :users,
23 23 :roles,
24 24 :members,
25 25 :member_roles,
26 26 :issues,
27 27 :issue_statuses,
28 28 :versions,
29 29 :trackers,
30 30 :projects_trackers,
31 31 :issue_categories,
32 32 :enabled_modules,
33 33 :enumerations,
34 34 :attachments,
35 35 :workflows,
36 36 :custom_fields,
37 37 :custom_values,
38 38 :custom_fields_projects,
39 39 :custom_fields_trackers,
40 40 :time_entries,
41 41 :journals,
42 42 :journal_details,
43 43 :queries,
44 44 :attachments
45 45
46 46 def setup
47 47 Setting.rest_api_enabled = '1'
48 48 end
49 49
50 50 context "/index.xml" do
51 51 # Use a private project to make sure auth is really working and not just
52 52 # only showing public issues.
53 53 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
54 54
55 55 should "contain metadata" do
56 56 get '/issues.xml'
57 57
58 58 assert_tag :tag => 'issues',
59 59 :attributes => {
60 60 :type => 'array',
61 61 :total_count => assigns(:issue_count),
62 62 :limit => 25,
63 63 :offset => 0
64 64 }
65 65 end
66 66
67 67 context "with offset and limit" do
68 68 should "use the params" do
69 69 get '/issues.xml?offset=2&limit=3'
70 70
71 71 assert_equal 3, assigns(:limit)
72 72 assert_equal 2, assigns(:offset)
73 73 assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}}
74 74 end
75 75 end
76 76
77 77 context "with nometa param" do
78 78 should "not contain metadata" do
79 79 get '/issues.xml?nometa=1'
80 80
81 81 assert_tag :tag => 'issues',
82 82 :attributes => {
83 83 :type => 'array',
84 84 :total_count => nil,
85 85 :limit => nil,
86 86 :offset => nil
87 87 }
88 88 end
89 89 end
90 90
91 91 context "with nometa header" do
92 92 should "not contain metadata" do
93 93 get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'}
94 94
95 95 assert_tag :tag => 'issues',
96 96 :attributes => {
97 97 :type => 'array',
98 98 :total_count => nil,
99 99 :limit => nil,
100 100 :offset => nil
101 101 }
102 102 end
103 103 end
104
105 context "with invalid query params" do
106 should "return errors" do
107 get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}}
108
109 assert_response :unprocessable_entity
110 assert_equal 'application/xml', @response.content_type
111 assert_tag 'errors', :child => {:tag => 'error', :content => "Start date can't be blank"}
112 end
113 end
104 114 end
105 115
106 116 context "/index.json" do
107 117 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
108 118 end
109 119
110 120 context "/index.xml with filter" do
111 121 should "show only issues with the status_id" do
112 122 get '/issues.xml?status_id=5'
113 123 assert_tag :tag => 'issues',
114 124 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
115 125 :only => { :tag => 'issue' } }
116 126 end
117 127 end
118 128
119 129 context "/index.json with filter" do
120 130 should "show only issues with the status_id" do
121 131 get '/issues.json?status_id=5'
122 132
123 133 json = ActiveSupport::JSON.decode(response.body)
124 134 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
125 135 assert_equal 3, status_ids_used.length
126 136 assert status_ids_used.all? {|id| id == 5 }
127 137 end
128 138
129 139 end
130 140
131 141 # Issue 6 is on a private project
132 142 context "/issues/6.xml" do
133 143 should_allow_api_authentication(:get, "/issues/6.xml")
134 144 end
135 145
136 146 context "/issues/6.json" do
137 147 should_allow_api_authentication(:get, "/issues/6.json")
138 148 end
139 149
140 150 context "GET /issues/:id" do
141 151 context "with journals" do
142 152 context ".xml" do
143 153 should "display journals" do
144 154 get '/issues/1.xml?include=journals'
145 155
146 156 assert_tag :tag => 'issue',
147 157 :child => {
148 158 :tag => 'journals',
149 159 :attributes => { :type => 'array' },
150 160 :child => {
151 161 :tag => 'journal',
152 162 :attributes => { :id => '1'},
153 163 :child => {
154 164 :tag => 'details',
155 165 :attributes => { :type => 'array' },
156 166 :child => {
157 167 :tag => 'detail',
158 168 :attributes => { :name => 'status_id' },
159 169 :child => {
160 170 :tag => 'old_value',
161 171 :content => '1',
162 172 :sibling => {
163 173 :tag => 'new_value',
164 174 :content => '2'
165 175 }
166 176 }
167 177 }
168 178 }
169 179 }
170 180 }
171 181 end
172 182 end
173 183 end
174 184
175 185 context "with custom fields" do
176 186 context ".xml" do
177 187 should "display custom fields" do
178 188 get '/issues/3.xml'
179 189
180 190 assert_tag :tag => 'issue',
181 191 :child => {
182 192 :tag => 'custom_fields',
183 193 :attributes => { :type => 'array' },
184 194 :child => {
185 195 :tag => 'custom_field',
186 196 :attributes => { :id => '1'},
187 197 :child => {
188 198 :tag => 'value',
189 199 :content => 'MySQL'
190 200 }
191 201 }
192 202 }
193 203
194 204 assert_nothing_raised do
195 205 Hash.from_xml(response.body).to_xml
196 206 end
197 207 end
198 208 end
199 209 end
200 210
201 211 context "with attachments" do
202 212 context ".xml" do
203 213 should "display attachments" do
204 214 get '/issues/3.xml?include=attachments'
205 215
206 216 assert_tag :tag => 'issue',
207 217 :child => {
208 218 :tag => 'attachments',
209 219 :children => {:count => 5},
210 220 :child => {
211 221 :tag => 'attachment',
212 222 :child => {
213 223 :tag => 'filename',
214 224 :content => 'source.rb',
215 225 :sibling => {
216 226 :tag => 'content_url',
217 227 :content => 'http://www.example.com/attachments/download/4/source.rb'
218 228 }
219 229 }
220 230 }
221 231 }
222 232 end
223 233 end
224 234 end
225 235
226 236 context "with subtasks" do
227 237 setup do
228 238 @c1 = Issue.generate!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
229 239 @c2 = Issue.generate!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
230 240 @c3 = Issue.generate!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :parent_issue_id => @c1.id)
231 241 end
232 242
233 243 context ".xml" do
234 244 should "display children" do
235 245 get '/issues/1.xml?include=children'
236 246
237 247 assert_tag :tag => 'issue',
238 248 :child => {
239 249 :tag => 'children',
240 250 :children => {:count => 2},
241 251 :child => {
242 252 :tag => 'issue',
243 253 :attributes => {:id => @c1.id.to_s},
244 254 :child => {
245 255 :tag => 'subject',
246 256 :content => 'child c1',
247 257 :sibling => {
248 258 :tag => 'children',
249 259 :children => {:count => 1},
250 260 :child => {
251 261 :tag => 'issue',
252 262 :attributes => {:id => @c3.id.to_s}
253 263 }
254 264 }
255 265 }
256 266 }
257 267 }
258 268 end
259 269
260 270 context ".json" do
261 271 should "display children" do
262 272 get '/issues/1.json?include=children'
263 273
264 274 json = ActiveSupport::JSON.decode(response.body)
265 275 assert_equal([
266 276 {
267 277 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
268 278 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
269 279 },
270 280 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
271 281 ],
272 282 json['issue']['children'])
273 283 end
274 284 end
275 285 end
276 286 end
277 287 end
278 288
279 289 context "POST /issues.xml" do
280 290 should_allow_api_authentication(:post,
281 291 '/issues.xml',
282 292 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
283 293 {:success_code => :created})
284 294
285 295 should "create an issue with the attributes" do
286 296 assert_difference('Issue.count') do
287 297 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
288 298 end
289 299
290 300 issue = Issue.first(:order => 'id DESC')
291 301 assert_equal 1, issue.project_id
292 302 assert_equal 2, issue.tracker_id
293 303 assert_equal 3, issue.status_id
294 304 assert_equal 'API test', issue.subject
295 305
296 306 assert_response :created
297 307 assert_equal 'application/xml', @response.content_type
298 308 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
299 309 end
300 310 end
301 311
302 312 context "POST /issues.xml with failure" do
303 313 should "have an errors tag" do
304 314 assert_no_difference('Issue.count') do
305 315 post '/issues.xml', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
306 316 end
307 317
308 318 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
309 319 end
310 320 end
311 321
312 322 context "POST /issues.json" do
313 323 should_allow_api_authentication(:post,
314 324 '/issues.json',
315 325 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
316 326 {:success_code => :created})
317 327
318 328 should "create an issue with the attributes" do
319 329 assert_difference('Issue.count') do
320 330 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
321 331 end
322 332
323 333 issue = Issue.first(:order => 'id DESC')
324 334 assert_equal 1, issue.project_id
325 335 assert_equal 2, issue.tracker_id
326 336 assert_equal 3, issue.status_id
327 337 assert_equal 'API test', issue.subject
328 338 end
329 339
330 340 end
331 341
332 342 context "POST /issues.json with failure" do
333 343 should "have an errors element" do
334 344 assert_no_difference('Issue.count') do
335 345 post '/issues.json', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
336 346 end
337 347
338 348 json = ActiveSupport::JSON.decode(response.body)
339 349 assert json['errors'].include?(['subject', "can't be blank"])
340 350 end
341 351 end
342 352
343 353 # Issue 6 is on a private project
344 354 context "PUT /issues/6.xml" do
345 355 setup do
346 356 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
347 357 @headers = { :authorization => credentials('jsmith') }
348 358 end
349 359
350 360 should_allow_api_authentication(:put,
351 361 '/issues/6.xml',
352 362 {:issue => {:subject => 'API update', :notes => 'A new note'}},
353 363 {:success_code => :ok})
354 364
355 365 should "not create a new issue" do
356 366 assert_no_difference('Issue.count') do
357 367 put '/issues/6.xml', @parameters, @headers
358 368 end
359 369 end
360 370
361 371 should "create a new journal" do
362 372 assert_difference('Journal.count') do
363 373 put '/issues/6.xml', @parameters, @headers
364 374 end
365 375 end
366 376
367 377 should "add the note to the journal" do
368 378 put '/issues/6.xml', @parameters, @headers
369 379
370 380 journal = Journal.last
371 381 assert_equal "A new note", journal.notes
372 382 end
373 383
374 384 should "update the issue" do
375 385 put '/issues/6.xml', @parameters, @headers
376 386
377 387 issue = Issue.find(6)
378 388 assert_equal "API update", issue.subject
379 389 end
380 390
381 391 end
382 392
383 393 context "PUT /issues/3.xml with custom fields" do
384 394 setup do
385 395 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, {'id' => '2', 'value' => '150'}]}}
386 396 @headers = { :authorization => credentials('jsmith') }
387 397 end
388 398
389 399 should "update custom fields" do
390 400 assert_no_difference('Issue.count') do
391 401 put '/issues/3.xml', @parameters, @headers
392 402 end
393 403
394 404 issue = Issue.find(3)
395 405 assert_equal '150', issue.custom_value_for(2).value
396 406 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
397 407 end
398 408 end
399 409
400 410 context "PUT /issues/6.xml with failed update" do
401 411 setup do
402 412 @parameters = {:issue => {:subject => ''}}
403 413 @headers = { :authorization => credentials('jsmith') }
404 414 end
405 415
406 416 should "not create a new issue" do
407 417 assert_no_difference('Issue.count') do
408 418 put '/issues/6.xml', @parameters, @headers
409 419 end
410 420 end
411 421
412 422 should "not create a new journal" do
413 423 assert_no_difference('Journal.count') do
414 424 put '/issues/6.xml', @parameters, @headers
415 425 end
416 426 end
417 427
418 428 should "have an errors tag" do
419 429 put '/issues/6.xml', @parameters, @headers
420 430
421 431 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
422 432 end
423 433 end
424 434
425 435 context "PUT /issues/6.json" do
426 436 setup do
427 437 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
428 438 @headers = { :authorization => credentials('jsmith') }
429 439 end
430 440
431 441 should_allow_api_authentication(:put,
432 442 '/issues/6.json',
433 443 {:issue => {:subject => 'API update', :notes => 'A new note'}},
434 444 {:success_code => :ok})
435 445
436 446 should "not create a new issue" do
437 447 assert_no_difference('Issue.count') do
438 448 put '/issues/6.json', @parameters, @headers
439 449 end
440 450 end
441 451
442 452 should "create a new journal" do
443 453 assert_difference('Journal.count') do
444 454 put '/issues/6.json', @parameters, @headers
445 455 end
446 456 end
447 457
448 458 should "add the note to the journal" do
449 459 put '/issues/6.json', @parameters, @headers
450 460
451 461 journal = Journal.last
452 462 assert_equal "A new note", journal.notes
453 463 end
454 464
455 465 should "update the issue" do
456 466 put '/issues/6.json', @parameters, @headers
457 467
458 468 issue = Issue.find(6)
459 469 assert_equal "API update", issue.subject
460 470 end
461 471
462 472 end
463 473
464 474 context "PUT /issues/6.json with failed update" do
465 475 setup do
466 476 @parameters = {:issue => {:subject => ''}}
467 477 @headers = { :authorization => credentials('jsmith') }
468 478 end
469 479
470 480 should "not create a new issue" do
471 481 assert_no_difference('Issue.count') do
472 482 put '/issues/6.json', @parameters, @headers
473 483 end
474 484 end
475 485
476 486 should "not create a new journal" do
477 487 assert_no_difference('Journal.count') do
478 488 put '/issues/6.json', @parameters, @headers
479 489 end
480 490 end
481 491
482 492 should "have an errors attribute" do
483 493 put '/issues/6.json', @parameters, @headers
484 494
485 495 json = ActiveSupport::JSON.decode(response.body)
486 496 assert json['errors'].include?(['subject', "can't be blank"])
487 497 end
488 498 end
489 499
490 500 context "DELETE /issues/1.xml" do
491 501 should_allow_api_authentication(:delete,
492 502 '/issues/6.xml',
493 503 {},
494 504 {:success_code => :ok})
495 505
496 506 should "delete the issue" do
497 507 assert_difference('Issue.count',-1) do
498 508 delete '/issues/6.xml', {}, :authorization => credentials('jsmith')
499 509 end
500 510
501 511 assert_nil Issue.find_by_id(6)
502 512 end
503 513 end
504 514
505 515 context "DELETE /issues/1.json" do
506 516 should_allow_api_authentication(:delete,
507 517 '/issues/6.json',
508 518 {},
509 519 {:success_code => :ok})
510 520
511 521 should "delete the issue" do
512 522 assert_difference('Issue.count',-1) do
513 523 delete '/issues/6.json', {}, :authorization => credentials('jsmith')
514 524 end
515 525
516 526 assert_nil Issue.find_by_id(6)
517 527 end
518 528 end
519 529
520 530 def credentials(user, password=nil)
521 531 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
522 532 end
523 533 end
General Comments 0
You need to be logged in to leave comments. Login now