Project

General

Profile

Feature #44141 » 0002-Add-REST-API-for-forum-messages.patch

Go MAEDA, 2026-06-04 12:45

View differences:

app/controllers/messages_controller.rb
20 20
class MessagesController < ApplicationController
21 21
  menu_item :boards
22 22
  default_search_scope :messages
23
  before_action :find_board, :only => [:new, :preview]
23
  before_action :find_board, :only => [:index, :create, :new, :preview]
24
  before_action :find_topic, :only => [:replies, :create_reply]
24 25
  before_action :find_attachments, :only => [:preview]
25
  before_action :find_message, :except => [:new, :preview]
26
  before_action :authorize, :except => [:preview, :edit, :destroy]
26
  before_action :find_message, :only => [:show, :reply, :edit, :update, :destroy, :quote]
27
  before_action :authorize, :except => [:preview, :edit, :update, :destroy]
28
  accept_api_auth :index, :show, :create, :update, :destroy, :replies, :create_reply
27 29

  
28 30
  helper :boards
29 31
  helper :watchers
......
33 35

  
34 36
  REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE)
35 37

  
38
  # List topics of a board
39
  def index
40
    @offset, @limit = api_offset_and_limit
41
    scope = @board.topics
42
    @message_count = scope.count
43
    @messages = scope.
44
      reorder(:sticky => :desc, :id => :desc).
45
      includes(:author, :parent, {:last_reply => :author}, {:board => :project}).
46
      limit(@limit).
47
      offset(@offset).
48
      to_a
49

  
50
    respond_to do |format|
51
      format.html {render_404}
52
      format.api {render :action => 'topics'}
53
    end
54
  end
55

  
36 56
  # Show a topic and its replies
37 57
  def show
38
    page = params[:page]
39
    # Find the page of the requested reply
40
    if params[:r] && page.nil?
41
      offset = @topic.children.where("#{Message.table_name}.id < ?", params[:r].to_i).count
42
      page = 1 + offset / REPLIES_PER_PAGE
43
    end
58
    respond_to do |format|
59
      format.html do
60
        page = params[:page]
61
        # Find the page of the requested reply
62
        if params[:r] && page.nil?
63
          offset = @topic.children.where("#{Message.table_name}.id < ?", params[:r].to_i).count
64
          page = 1 + offset / REPLIES_PER_PAGE
65
        end
44 66

  
45
    @reply_count = @topic.children.count
46
    @reply_pages = Paginator.new @reply_count, REPLIES_PER_PAGE, page
47
    @replies =  @topic.children.
48
      includes(:author, :attachments, {:board => :project}).
49
      reorder("#{Message.table_name}.created_on ASC, #{Message.table_name}.id ASC").
50
      limit(@reply_pages.per_page).
51
      offset(@reply_pages.offset).
52
      to_a
67
        @reply_count = @topic.children.count
68
        @reply_pages = Paginator.new @reply_count, REPLIES_PER_PAGE, page
69
        @replies =  @topic.children.
70
          includes(:author, :attachments, {:board => :project}).
71
          reorder("#{Message.table_name}.created_on ASC, #{Message.table_name}.id ASC").
72
          limit(@reply_pages.per_page).
73
          offset(@reply_pages.offset).
74
          to_a
53 75

  
54
    Message.preload_reaction_details(@replies)
76
        Message.preload_reaction_details(@replies)
77

  
78
        @reply = Message.new(:subject => "RE: #{@message.subject}")
79
        render :action => "show", :layout => false if request.xhr?
80
      end
81
      format.api
82
    end
83
  end
55 84

  
56
    @reply = Message.new(:subject => "RE: #{@message.subject}")
57
    render :action => "show", :layout => false if request.xhr?
85
  # Create a new topic through the REST API
86
  def create
87
    @message = Message.new
88
    @message.author = User.current
89
    @message.board = @board
90
    @message.safe_attributes = params[:message]
91
    # Ignore board_id overrides from the request body for this scoped API.
92
    @message.board = @board
93
    @message.save_attachments(params[:attachments] || (params[:message] && params[:message][:uploads]))
94
    if @message.save
95
      call_hook(:controller_messages_new_after_save, {:params => params, :message => @message})
96
      render_attachment_warning_if_needed(@message)
97
      respond_to do |format|
98
        format.html {render_404}
99
        format.api do
100
          render(
101
            :action => 'show',
102
            :status => :created,
103
            :location => message_url(@message)
104
          )
105
        end
106
      end
107
    else
108
      respond_to do |format|
109
        format.html {render_404}
110
        format.api {render_validation_errors(@message)}
111
      end
112
    end
58 113
  end
59 114

  
60 115
  # Create a new topic
......
74 129
    end
75 130
  end
76 131

  
132
  # List replies of a topic
133
  def replies
134
    @offset, @limit = api_offset_and_limit
135
    scope = @topic.children
136
    @message_count = scope.count
137
    @messages = scope.
138
      includes(:author, :parent, {:last_reply => :author}, {:board => :project}).
139
      reorder("#{Message.table_name}.created_on ASC, #{Message.table_name}.id ASC").
140
      limit(@limit).
141
      offset(@offset).
142
      to_a
143

  
144
    respond_to do |format|
145
      format.html {render_404}
146
      format.api
147
    end
148
  end
149

  
77 150
  # Reply to a topic
78 151
  def reply
79 152
    @reply = Message.new
......
90 163
    redirect_to board_message_path(@board, @topic, :r => @reply)
91 164
  end
92 165

  
166
  # Reply to a topic through the REST API
167
  def create_reply
168
    @reply = Message.new(:subject => "RE: #{@topic.subject}")
169
    @reply.author = User.current
170
    @reply.board = @board
171
    @reply.safe_attributes = params[:reply]
172
    # Ignore board_id overrides from the request body for this scoped API.
173
    @reply.board = @board
174
    @reply.save_attachments(params[:attachments] || (params[:reply] && params[:reply][:uploads]))
175
    @topic.children << @reply
176
    if @reply.new_record?
177
      respond_to do |format|
178
        format.html {render_404}
179
        format.api {render_validation_errors(@reply)}
180
      end
181
    else
182
      call_hook(:controller_messages_reply_after_save, {:params => params, :message => @reply})
183
      render_attachment_warning_if_needed(@reply)
184
      @message = @reply
185
      respond_to do |format|
186
        format.html {render_404}
187
        format.api do
188
          render(
189
            :action => 'show',
190
            :status => :created,
191
            :location => message_url(@reply)
192
          )
193
        end
194
      end
195
    end
196
  end
197

  
93 198
  # Edit a message
94 199
  def edit
95 200
    (render_403; return false) unless @message.editable_by?(User.current)
......
105 210
    end
106 211
  end
107 212

  
213
  def update
214
    (render_403; return false) unless @message.editable_by?(User.current)
215
    project = @message.project
216
    @message.safe_attributes = params[:message]
217
    unless project.boards.exists?(@message.board_id)
218
      @message.errors.add(:board_id, :invalid)
219
      respond_to do |format|
220
        format.html {render_404}
221
        format.api {render_validation_errors(@message)}
222
      end
223
      return
224
    end
225
    @message.save_attachments(params[:attachments] || (params[:message] && params[:message][:uploads]))
226
    if @message.save
227
      render_attachment_warning_if_needed(@message)
228
      respond_to do |format|
229
        format.html {render_404}
230
        format.api {render_api_ok}
231
      end
232
    else
233
      respond_to do |format|
234
        format.html {render_404}
235
        format.api {render_validation_errors(@message)}
236
      end
237
    end
238
  end
239

  
108 240
  # Delete a messages
109 241
  def destroy
110 242
    (render_403; return false) unless @message.destroyable_by?(User.current)
111 243
    r = @message.to_param
112 244
    @message.destroy
113
    flash[:notice] = l(:notice_successful_delete)
114
    if @message.parent
115
      redirect_to board_message_path(@board, @message.parent, :r => r)
116
    else
117
      redirect_to project_board_path(@project, @board)
245
    respond_to do |format|
246
      format.html do
247
        flash[:notice] = l(:notice_successful_delete)
248
        if @message.parent
249
          redirect_to board_message_path(@board, @message.parent, :r => r)
250
        else
251
          redirect_to project_board_path(@project, @board)
252
        end
253
      end
254
      format.api {render_api_ok}
118 255
    end
119 256
  end
120 257

  
......
144 281
  private
145 282

  
146 283
  def find_message
147
    return unless find_board
284
    if params[:board_id]
285
      return unless find_board
148 286

  
149
    @message = @board.messages.includes(:parent).find(params[:id])
287
      @message = @board.messages.includes(:parent).find(params[:id])
288
    else
289
      @message = Message.visible.includes(:parent, :author, :attachments, {:board => :project}).find(params[:id])
290
      @board = @message.board
291
      @project = @board.project
292
    end
150 293
    @topic = @message.root
151 294
  rescue ActiveRecord::RecordNotFound
152 295
    render_404
153 296
  end
154 297

  
298
  def find_topic
299
    @topic = Message.visible.includes(:author, :parent, {:board => :project}).find(params[:topic_id])
300
    if @topic.parent
301
      render_404
302
      return false
303
    end
304
    @board = @topic.board
305
    @project = @board.project
306
  rescue ActiveRecord::RecordNotFound
307
    render_404
308
  end
309

  
155 310
  def find_board
156 311
    @board = Board.includes(:project).find(params[:board_id])
157 312
    @project = @board.project
app/helpers/messages_helper.rb
20 20
module MessagesHelper
21 21
  include Redmine::QuoteReply::Helper
22 22
  include ReactionsHelper
23

  
24
  def render_api_message(message, api)
25
    api.id message.id
26
    api.project(:id => message.project.id, :name => message.project.name)
27
    api.board(:id => message.board_id, :name => message.board.name)
28
    api.parent(:id => message.parent_id, :subject => message.parent.subject) unless message.parent.nil?
29
    api.root(:id => message.root.id, :subject => message.root.subject)
30
    api.author(:id => message.author_id, :name => message.author.name) unless message.author.nil?
31
    api.subject message.subject
32
    api.content message.content
33
    api.replies_count message.replies_count
34
    render_api_message_last_reply(message.last_reply, api) unless message.last_reply.nil?
35
    api.locked message.locked?
36
    api.sticky message.sticky?
37
    api.created_on message.created_on
38
    api.updated_on message.updated_on
39

  
40
    if include_in_api_response?('attachments')
41
      api.array :attachments do
42
        message.attachments.each do |attachment|
43
          render_api_attachment(attachment, api)
44
        end
45
      end
46
    end
47
  end
48

  
49
  def render_api_message_last_reply(message, api)
50
    api.last_reply(:id => message.id) do
51
      api.author(:id => message.author_id, :name => message.author.name) unless message.author.nil?
52
      api.subject message.subject
53
      api.created_on message.created_on
54
    end
55
  end
23 56
end
app/views/messages/replies.api.rsb
1
api.array :replies, api_meta(:total_count => @message_count, :offset => @offset, :limit => @limit) do
2
  @messages.each do |message|
3
    api.reply do
4
      render_api_message(message, api)
5
    end
6
  end
7
end
app/views/messages/show.api.rsb
1
api.message do
2
  render_api_message(@message, api)
3
end
app/views/messages/topics.api.rsb
1
api.array :topics, api_meta(:total_count => @message_count, :offset => @offset, :limit => @limit) do
2
  @messages.each do |message|
3
    api.topic do
4
      render_api_message(message, api)
5
    end
6
  end
7
end
config/routes.rb
43 43

  
44 44
  match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
45 45

  
46
  get 'boards/:board_id/topics.:format', :to => 'messages#index', :constraints => {:format => /xml|json/}
47
  post 'boards/:board_id/topics.:format', :to => 'messages#create', :constraints => {:format => /xml|json/}
46 48
  match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post], :as => 'new_board_message'
47 49
  get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
48 50
  match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
......
52 54
  post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
53 55
  post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
54 56
  post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
57
  get 'messages/:topic_id/replies.:format', :to => 'messages#replies', :constraints => {:format => /xml|json/}
58
  post 'messages/:topic_id/replies.:format', :to => 'messages#create_reply', :constraints => {:format => /xml|json/}
59
  get 'messages/:id.:format', :to => 'messages#show', :as => 'message', :constraints => {:format => /xml|json/}
60
  put 'messages/:id.:format', :to => 'messages#update', :constraints => {:format => /xml|json/}
61
  patch 'messages/:id.:format', :to => 'messages#update', :constraints => {:format => /xml|json/}
62
  delete 'messages/:id.:format', :to => 'messages#destroy', :constraints => {:format => /xml|json/}
55 63
  get 'boards/:id.:format', :to => 'boards#show', :as => 'board', :constraints => {:format => /xml|json/}
56 64
  put 'boards/:id.:format', :to => 'boards#update', :constraints => {:format => /xml|json/}
57 65
  patch 'boards/:id.:format', :to => 'boards#update', :constraints => {:format => /xml|json/}
lib/redmine/preparation.rb
144 144
        end
145 145

  
146 146
        map.project_module :boards do |map|
147
          map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :read => true
148
          map.permission :add_messages, {:messages => [:new, :reply, :quote], :attachments => :upload}
149
          map.permission :edit_messages, {:messages => :edit, :attachments => :upload}, :require => :member
150
          map.permission :edit_own_messages, {:messages => :edit, :attachments => :upload}, :require => :loggedin
147
          map.permission :view_messages, {:boards => [:index, :show], :messages => [:index, :show, :replies]}, :read => true
148
          map.permission :add_messages, {:messages => [:new, :create, :reply, :create_reply, :quote], :attachments => :upload}
149
          map.permission :edit_messages, {:messages => [:edit, :update], :attachments => :upload}, :require => :member
150
          map.permission :edit_own_messages, {:messages => [:edit, :update], :attachments => :upload}, :require => :loggedin
151 151
          map.permission :delete_messages, {:messages => :destroy}, :require => :member
152 152
          map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
153 153
          map.permission :view_message_watchers, {}, :read => true
test/integration/api_test/api_routing_test.rb
99 99
    should_route 'DELETE /issues/12/watchers/3' => 'watchers#destroy', :object_type => 'issue', :object_id => '12', :user_id => '3'
100 100
  end
101 101

  
102
  def test_messages
103
    should_route 'GET /boards/1/topics' => 'messages#index', :board_id => '1'
104
    should_route 'POST /boards/1/topics' => 'messages#create', :board_id => '1'
105

  
106
    should_route 'GET /messages/1/replies' => 'messages#replies', :topic_id => '1'
107
    should_route 'POST /messages/1/replies' => 'messages#create_reply', :topic_id => '1'
108

  
109
    should_route 'GET /messages/2' => 'messages#show', :id => '2'
110
    should_route 'PUT /messages/2' => 'messages#update', :id => '2'
111
    should_route 'DELETE /messages/2' => 'messages#destroy', :id => '2'
112
  end
113

  
102 114
  def test_memberships
103 115
    should_route 'GET /projects/5234/memberships' => 'members#index', :project_id => '5234'
104 116
    should_route 'POST /projects/5234/memberships' => 'members#create', :project_id => '5234'
test/integration/api_test/messages_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require_relative '../../test_helper'
21

  
22
class Redmine::ApiTest::MessagesTest < Redmine::ApiTest::Base
23
  test "GET /boards/:board_id/topics.xml should return the board topics" do
24
    get '/boards/1/topics.xml', :headers => credentials('jsmith')
25

  
26
    assert_response :success
27
    assert_equal 'application/xml', @response.media_type
28
    assert_select 'topics[type=array] topic id', :text => '4'
29
    assert_select 'topics[type=array] topic parent', 0
30
  end
31

  
32
  test "POST /boards/:board_id/topics.xml should create a topic" do
33
    assert_difference 'Message.count' do
34
      post(
35
        '/boards/1/topics.xml',
36
        :params => {:message => {:subject => 'API topic', :content => 'API topic body'}},
37
        :headers => credentials('jsmith'))
38
    end
39

  
40
    message = Message.order(id: :desc).first
41
    assert_response :created
42
    assert_equal 'application/xml', @response.media_type
43
    assert_nil message.parent
44
    assert_equal 'API topic', message.subject
45
    assert_equal 'API topic body', message.content
46
    assert_equal Board.find(1), message.board
47
    assert_select 'message id', :text => message.id.to_s
48
  end
49

  
50
  test "POST /boards/:board_id/topics.xml should ignore board_id parameter" do
51
    assert_difference 'Message.count' do
52
      post(
53
        '/boards/1/topics.xml',
54
        :params => {
55
          :message => {
56
            :subject => 'API topic',
57
            :content => 'API topic body',
58
            :board_id => 2
59
          }
60
        },
61
        :headers => credentials('jsmith'))
62
    end
63

  
64
    assert_response :created
65
    assert_equal Board.find(1), Message.order(id: :desc).first.board
66
  end
67

  
68
  test "POST /boards/:board_id/topics.xml with attachment should create a topic with attachment" do
69
    token = xml_upload('File content', credentials('jsmith'))
70
    attachment = Attachment.find_by_token(token)
71

  
72
    assert_difference 'Message.count' do
73
      post(
74
        '/boards/1/topics.xml',
75
        :params => {
76
          :message => {
77
            :subject => 'API topic with attachment',
78
            :content => 'API topic body'
79
          },
80
          :attachments => [
81
            {
82
              :token => token,
83
              :filename => 'message.txt',
84
              :content_type => 'text/plain'
85
            }
86
          ]
87
        },
88
        :headers => credentials('jsmith'))
89
    end
90

  
91
    message = Message.order(id: :desc).first
92
    assert_response :created
93
    assert_equal attachment, message.attachments.first
94

  
95
    attachment.reload
96
    assert_equal 'message.txt', attachment.filename
97
    assert_equal 'text/plain', attachment.content_type
98
    assert_equal 2, attachment.author_id
99
  end
100

  
101
  test "POST /boards/:board_id/topics.xml with invalid parameters should return errors" do
102
    assert_no_difference 'Message.count' do
103
      post(
104
        '/boards/1/topics.xml',
105
        :params => {:message => {:subject => '', :content => 'API topic body'}},
106
        :headers => credentials('jsmith'))
107
    end
108

  
109
    assert_response :unprocessable_content
110
    assert_equal 'application/xml', @response.media_type
111
    assert_select 'errors error', :text => 'Subject cannot be blank'
112
  end
113

  
114
  test "GET /messages/:topic_id/replies.xml should return replies" do
115
    get '/messages/1/replies.xml', :headers => credentials('jsmith')
116

  
117
    assert_response :success
118
    assert_equal 'application/xml', @response.media_type
119
    assert_select 'replies[type=array] reply id', :text => '2'
120
    assert_select 'replies[type=array] reply parent[id="1"][subject="First post"]'
121
  end
122

  
123
  test "POST /messages/:topic_id/replies.xml should create a reply" do
124
    assert_difference 'Message.count' do
125
      post(
126
        '/messages/1/replies.xml',
127
        :params => {:reply => {:content => 'API reply body'}},
128
        :headers => credentials('jsmith'))
129
    end
130

  
131
    reply = Message.order(id: :desc).first
132
    assert_response :created
133
    assert_equal 'application/xml', @response.media_type
134
    assert_equal Message.find(1), reply.parent
135
    assert_equal 'RE: First post', reply.subject
136
    assert_equal 'API reply body', reply.content
137
    assert_select 'message id', :text => reply.id.to_s
138
  end
139

  
140
  test "POST /messages/:topic_id/replies.xml should ignore board_id parameter" do
141
    assert_difference 'Message.count' do
142
      post(
143
        '/messages/1/replies.xml',
144
        :params => {:reply => {:content => 'API reply body', :board_id => 2}},
145
        :headers => credentials('jsmith'))
146
    end
147

  
148
    reply = Message.order(id: :desc).first
149
    assert_response :created
150
    assert_equal Message.find(1), reply.parent
151
    assert_equal Board.find(1), reply.board
152
  end
153

  
154
  test "POST /messages/:topic_id/replies.xml to locked topic should return errors" do
155
    Message.find(1).update!(:locked => true)
156

  
157
    assert_no_difference 'Message.count' do
158
      post(
159
        '/messages/1/replies.xml',
160
        :params => {:reply => {:content => 'API reply body'}},
161
        :headers => credentials('jsmith'))
162
    end
163

  
164
    assert_response :unprocessable_content
165
    assert_equal 'application/xml', @response.media_type
166
    assert_select 'errors error', :text => 'Topic is locked'
167
  end
168

  
169
  test "POST /messages/:topic_id/replies without API format should not create a reply" do
170
    assert_no_difference 'Message.count' do
171
      post(
172
        '/messages/1/replies',
173
        :params => {:reply => {:content => 'HTML reply body'}},
174
        :headers => credentials('jsmith'))
175
    end
176

  
177
    assert_response :not_found
178
  end
179

  
180
  test "GET /messages/:id.xml should return a topic by message id" do
181
    get '/messages/1.xml', :headers => credentials('jsmith')
182

  
183
    assert_response :success
184
    assert_equal 'application/xml', @response.media_type
185
    assert_select 'message' do
186
      assert_select 'id', :text => '1'
187
      assert_select 'project[id="1"][name="eCookbook"]'
188
      assert_select 'board[id="1"][name="Help"]'
189
      assert_select 'root[id="1"][subject="First post"]'
190
      assert_select 'subject', :text => 'First post'
191
      assert_select 'replies_count', :text => '2'
192
    end
193
  end
194

  
195
  test "GET /messages/:id.xml should return a reply by message id" do
196
    get '/messages/2.xml', :headers => credentials('jsmith')
197

  
198
    assert_response :success
199
    assert_equal 'application/xml', @response.media_type
200
    assert_select 'message' do
201
      assert_select 'id', :text => '2'
202
      assert_select 'parent[id="1"][subject="First post"]'
203
      assert_select 'root[id="1"][subject="First post"]'
204
      assert_select 'subject', :text => 'First reply'
205
    end
206
  end
207

  
208
  test "GET /messages/:id.xml without permission should return 404" do
209
    get '/messages/7.xml'
210

  
211
    assert_response :not_found
212
  end
213

  
214
  test "PUT /messages/:id.xml should update a message" do
215
    put(
216
      '/messages/1.xml',
217
      :params => {:message => {:subject => 'Updated topic', :content => 'Updated topic body'}},
218
      :headers => credentials('jsmith'))
219

  
220
    assert_response :no_content
221
    assert_equal '', @response.body
222
    assert_equal 'Updated topic', Message.find(1).subject
223
    assert_equal 'Updated topic body', Message.find(1).content
224
  end
225

  
226
  test "PUT /messages/:id.xml should allow moving a message to a board in the same project" do
227
    put(
228
      '/messages/1.xml',
229
      :params => {:message => {:board_id => 2}},
230
      :headers => credentials('jsmith'))
231

  
232
    assert_response :no_content
233
    assert_equal Board.find(2), Message.find(1).board
234
  end
235

  
236
  test "PUT /messages/:id.xml should not move a message to a board in another project" do
237
    put(
238
      '/messages/1.xml',
239
      :params => {:message => {:board_id => 3}},
240
      :headers => credentials('jsmith'))
241

  
242
    assert_response :unprocessable_content
243
    assert_equal Board.find(1), Message.find(1).board
244
  end
245

  
246
  test "DELETE /messages/:id.xml should destroy a message" do
247
    assert_difference 'Message.count', -1 do
248
      delete '/messages/2.xml', :headers => credentials('jsmith')
249
    end
250

  
251
    assert_response :no_content
252
    assert_equal '', @response.body
253
    assert_nil Message.find_by_id(2)
254
  end
255

  
256
  test "DELETE /messages/:id without API format should not destroy a message" do
257
    assert_no_difference 'Message.count' do
258
      delete '/messages/2', :headers => credentials('jsmith')
259
    end
260

  
261
    assert_response :not_found
262
    assert Message.exists?(2)
263
  end
264
end
(2-2/2)