From: Milan Cvetkovic Date: Mon, 11 Mar 2024 13:14:04 +0000 (+0000) Subject: Add Messages API X-Git-Tag: live~276^2 X-Git-Url: https://git.openstreetmap.org./rails.git/commitdiff_plain/0db47f3f76a33418d88bbdf249b1b5ed1d6fc4bf Add Messages API as discussed in [Issue #4509](https://wiki.openstreetmap.org/w/index.php?title=Messaging_API_proposal) and documented in [Messaging API reference](https://wiki.openstreetmap.org/w/index.php?title=Messaging_API_proposal) --- diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6fe5b2e57..3b18f7246 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -71,7 +71,7 @@ Metrics/ClassLength: # Offense count: 59 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: - Max: 29 + Max: 31 # Offense count: 753 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. @@ -86,7 +86,7 @@ Metrics/ParameterLists: # Offense count: 56 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: - Max: 30 + Max: 32 # Offense count: 2394 # This cop supports safe autocorrection (--autocorrect). @@ -95,7 +95,7 @@ Minitest/EmptyLineBeforeAssertionMethods: # Offense count: 565 Minitest/MultipleAssertions: - Max: 54 + Max: 60 # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). diff --git a/app/abilities/api_capability.rb b/app/abilities/api_capability.rb index f27dd2e63..44e676345 100644 --- a/app/abilities/api_capability.rb +++ b/app/abilities/api_capability.rb @@ -19,6 +19,8 @@ class ApiCapability can [:gpx_files], User if scope?(token, :read_gpx) can [:index, :show], UserPreference if scope?(token, :read_prefs) can [:update, :update_all, :destroy], UserPreference if scope?(token, :write_prefs) + can [:inbox, :outbox, :show, :update, :destroy], Message if scope?(token, :consume_messages) + can [:create], Message if scope?(token, :send_messages) if user.terms_agreed? can [:create, :update, :upload, :close, :subscribe, :unsubscribe], Changeset if scope?(token, :write_api) diff --git a/app/controllers/api/messages_controller.rb b/app/controllers/api/messages_controller.rb new file mode 100644 index 000000000..074f87398 --- /dev/null +++ b/app/controllers/api/messages_controller.rb @@ -0,0 +1,149 @@ +# The MessagesController is the RESTful interface to Message objects + +module Api + class MessagesController < ApiController + before_action :authorize + + before_action :check_api_writable, :only => [:create, :update, :destroy] + before_action :check_api_readable, :except => [:create, :update, :destroy] + + authorize_resource + + around_action :api_call_handle_error, :api_call_timeout + + before_action :set_request_formats + + def inbox + @skip_body = true + @messages = Message.includes(:sender, :recipient).where(:to_user_id => current_user.id) + + show_messages + end + + def outbox + @skip_body = true + @messages = Message.includes(:sender, :recipient).where(:from_user_id => current_user.id) + + show_messages + end + + # Dump the details on a message given in params[:id] + def show + @message = Message.includes(:sender, :recipient).find(params[:id]) + + raise OSM::APIAccessDenied if current_user.id != @message.from_user_id && current_user.id != @message.to_user_id + + # Render the result + respond_to do |format| + format.xml + format.json + end + end + + # Create a new message from current user + def create + # Check the arguments are sane + raise OSM::APIBadUserInput, "No title was given" if params[:title].blank? + raise OSM::APIBadUserInput, "No body was given" if params[:body].blank? + + # Extract the arguments + if params[:recipient_id] + recipient_id = params[:recipient_id].to_i + recipient = User.find(recipient_id) + elsif params[:recipient] + recipient_display_name = params[:recipient] + recipient = User.find_by(:display_name => recipient_display_name) + else + raise OSM::APIBadUserInput, "No recipient was given" + end + + raise OSM::APIRateLimitExceeded if current_user.sent_messages.where(:sent_on => Time.now.utc - 1.hour..).count >= current_user.max_messages_per_hour + + @message = Message.new(:sender => current_user, + :recipient => recipient, + :sent_on => Time.now.utc, + :title => params[:title], + :body => params[:body], + :body_format => "markdown") + @message.save! + + UserMailer.message_notification(@message).deliver_later if @message.notify_recipient? + + # Return a copy of the new message + respond_to do |format| + format.xml { render :action => :show } + format.json { render :action => :show } + end + end + + # Update read status of a message + def update + @message = Message.find(params[:id]) + read_status_idx = %w[true false].index params[:read_status] + + raise OSM::APIBadUserInput, "Invalid value of `read_status` was given" if read_status_idx.nil? + raise OSM::APIAccessDenied unless current_user.id == @message.to_user_id + + @message.message_read = read_status_idx.zero? + @message.save! + + # Return a copy of the message + respond_to do |format| + format.xml { render :action => :show } + format.json { render :action => :show } + end + end + + # Delete message by marking it as not visible for the current user + def destroy + @message = Message.find(params[:id]) + if current_user.id == @message.from_user_id + @message.from_user_visible = false + elsif current_user.id == @message.to_user_id + @message.to_user_visible = false + else + raise OSM::APIAccessDenied + end + + @message.save! + + # Return a copy of the message + respond_to do |format| + format.xml { render :action => :show } + format.json { render :action => :show } + end + end + + private + + def show_messages + @messages = @messages.where(:muted => false) + if params[:order].nil? || params[:order] == "newest" + @messages = @messages.where(:id => ..params[:from_id]) unless params[:from_id].nil? + @messages = @messages.order(:id => :desc) + elsif params[:order] == "oldest" + @messages = @messages.where(:id => params[:from_id]..) unless params[:from_id].nil? + @messages = @messages.order(:id => :asc) + else + raise OSM::APIBadUserInput, "Invalid order specified" + end + + limit = params[:limit] + if !limit + limit = Settings.default_message_query_limit + elsif !limit.to_i.positive? || limit.to_i > Settings.max_message_query_limit + raise OSM::APIBadUserInput, "Messages limit must be between 1 and #{Settings.max_message_query_limit}" + else + limit = limit.to_i + end + + @messages = @messages.limit(limit) + + # Render the result + respond_to do |format| + format.xml + format.json + end + end + end +end diff --git a/app/views/api/messages/_message.json.jbuilder b/app/views/api/messages/_message.json.jbuilder new file mode 100644 index 000000000..a04295d31 --- /dev/null +++ b/app/views/api/messages/_message.json.jbuilder @@ -0,0 +1,17 @@ +json.id message.id +json.from_user_id message.from_user_id +json.from_display_name message.sender.display_name +json.to_user_id message.to_user_id +json.to_display_name message.recipient.display_name +json.title message.title +json.sent_on message.sent_on.xmlschema + +if current_user.id == message.from_user_id + json.deleted !message.from_user_visible +elsif current_user.id == message.to_user_id + json.message_read message.message_read + json.deleted !message.to_user_visible +end + +json.body_format message.body_format +json.body message.body unless @skip_body diff --git a/app/views/api/messages/_message.xml.builder b/app/views/api/messages/_message.xml.builder new file mode 100644 index 000000000..64ac9e3bc --- /dev/null +++ b/app/views/api/messages/_message.xml.builder @@ -0,0 +1,21 @@ +attrs = { + "id" => message.id, + "from_user_id" => message.from_user_id, + "from_display_name" => message.sender.display_name, + "to_user_id" => message.to_user_id, + "to_display_name" => message.recipient.display_name, + "sent_on" => message.sent_on.xmlschema, + "body_format" => message.body_format +} + +if current_user.id == message.from_user_id + attrs["deleted"] = !message.from_user_visible +elsif current_user.id == message.to_user_id + attrs["message_read"] = message.message_read + attrs["deleted"] = !message.to_user_visible +end + +xml.message(attrs) do |nd| + nd.title(message.title) + nd.body(message.body) unless @skip_body +end diff --git a/app/views/api/messages/inbox.json.jbuilder b/app/views/api/messages/inbox.json.jbuilder new file mode 100644 index 000000000..524006ded --- /dev/null +++ b/app/views/api/messages/inbox.json.jbuilder @@ -0,0 +1,5 @@ +json.partial! "api/root_attributes" + +json.messages(@messages) do |message| + json.partial! message +end diff --git a/app/views/api/messages/inbox.xml.builder b/app/views/api/messages/inbox.xml.builder new file mode 100644 index 000000000..0ef9003a9 --- /dev/null +++ b/app/views/api/messages/inbox.xml.builder @@ -0,0 +1,7 @@ +xml.instruct! + +xml.osm(OSM::API.new.xml_root_attributes) do |osm| + xml.tag! "messages" do + osm << (render(@messages) || "") + end +end diff --git a/app/views/api/messages/outbox.json.jbuilder b/app/views/api/messages/outbox.json.jbuilder new file mode 100644 index 000000000..524006ded --- /dev/null +++ b/app/views/api/messages/outbox.json.jbuilder @@ -0,0 +1,5 @@ +json.partial! "api/root_attributes" + +json.messages(@messages) do |message| + json.partial! message +end diff --git a/app/views/api/messages/outbox.xml.builder b/app/views/api/messages/outbox.xml.builder new file mode 100644 index 000000000..440e3429b --- /dev/null +++ b/app/views/api/messages/outbox.xml.builder @@ -0,0 +1,5 @@ +xml.instruct! + +xml.osm(OSM::API.new.xml_root_attributes) do |osm| + osm << (render(@messages) || "") +end diff --git a/app/views/api/messages/show.json.jbuilder b/app/views/api/messages/show.json.jbuilder new file mode 100644 index 000000000..d8f24e958 --- /dev/null +++ b/app/views/api/messages/show.json.jbuilder @@ -0,0 +1,5 @@ +json.partial! "api/root_attributes" + +json.message do + json.partial! @message +end diff --git a/app/views/api/messages/show.xml.builder b/app/views/api/messages/show.xml.builder new file mode 100644 index 000000000..008d59249 --- /dev/null +++ b/app/views/api/messages/show.xml.builder @@ -0,0 +1,5 @@ +xml.instruct! :xml, :version => "1.0" + +xml.osm(OSM::API.new.xml_root_attributes) do |osm| + osm << render(@message) +end diff --git a/config/locales/en.yml b/config/locales/en.yml index dc7f1a1c0..3f2e8a93b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2641,6 +2641,8 @@ en: write_notes: Modify notes write_redactions: Redact map data read_email: Read user email address + consume_messages: Read, update status and delete user messages + send_messages: Send private messages to other users skip_authorization: Auto approve application for_roles: moderator: This permission is for actions available only to moderators diff --git a/config/routes.rb b/config/routes.rb index c832cbb35..650818d6f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -78,6 +78,15 @@ OpenStreetMap::Application.routes.draw do end end + resources :messages, :path => "user/messages", :constraints => { :id => /\d+/ }, :only => [:create, :show, :destroy], :controller => "messages", :as => :api_messages do + collection do + get "inbox" + get "outbox" + end + end + + post "/user/messages/:id" => "messages#update", :as => :api_message_update + post "gpx/create" => "traces#create" get "gpx/:id" => "traces#show", :as => :api_trace, :id => /\d+/ put "gpx/:id" => "traces#update", :id => /\d+/ diff --git a/config/settings.yml b/config/settings.yml index fa7207721..71df9ad3d 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -59,6 +59,10 @@ user_block_periods: [0, 1, 3, 6, 12, 24, 48, 96, 168, 336, 731, 4383, 8766, 8766 user_account_deletion_delay: null # Rate limit for message sending max_messages_per_hour: 60 +# Default limit on the number of messages returned by inbox and outbox message api +default_message_query_limit: 100 +# Maximum number of messages returned by inbox and outbox message api +max_message_query_limit: 100 # Rate limit for friending max_friends_per_hour: 60 # Rate limit for changeset comments diff --git a/lib/oauth.rb b/lib/oauth.rb index 88db38eb4..a8f497621 100644 --- a/lib/oauth.rb +++ b/lib/oauth.rb @@ -2,7 +2,7 @@ module Oauth SCOPES = %w[read_prefs write_prefs write_diary write_api read_gpx write_gpx write_notes].freeze PRIVILEGED_SCOPES = %w[read_email skip_authorization].freeze MODERATOR_SCOPES = %w[write_redactions].freeze - OAUTH2_SCOPES = %w[write_redactions openid].freeze + OAUTH2_SCOPES = %w[write_redactions consume_messages send_messages openid].freeze class Scope attr_reader :name diff --git a/test/controllers/api/messages_controller_test.rb b/test/controllers/api/messages_controller_test.rb new file mode 100644 index 000000000..0b54be4dc --- /dev/null +++ b/test/controllers/api/messages_controller_test.rb @@ -0,0 +1,611 @@ +require "test_helper" + +module Api + class MessagesControllerTest < ActionDispatch::IntegrationTest + ## + # test all routes which lead to this controller + def test_routes + assert_routing( + { :path => "/api/0.6/user/messages/inbox", :method => :get }, + { :controller => "api/messages", :action => "inbox" } + ) + assert_routing( + { :path => "/api/0.6/user/messages/inbox.xml", :method => :get }, + { :controller => "api/messages", :action => "inbox", :format => "xml" } + ) + assert_routing( + { :path => "/api/0.6/user/messages/inbox.json", :method => :get }, + { :controller => "api/messages", :action => "inbox", :format => "json" } + ) + assert_routing( + { :path => "/api/0.6/user/messages/outbox", :method => :get }, + { :controller => "api/messages", :action => "outbox" } + ) + assert_routing( + { :path => "/api/0.6/user/messages/outbox.xml", :method => :get }, + { :controller => "api/messages", :action => "outbox", :format => "xml" } + ) + assert_routing( + { :path => "/api/0.6/user/messages/outbox.json", :method => :get }, + { :controller => "api/messages", :action => "outbox", :format => "json" } + ) + assert_routing( + { :path => "/api/0.6/user/messages/1", :method => :get }, + { :controller => "api/messages", :action => "show", :id => "1" } + ) + assert_routing( + { :path => "/api/0.6/user/messages/1.xml", :method => :get }, + { :controller => "api/messages", :action => "show", :id => "1", :format => "xml" } + ) + assert_routing( + { :path => "/api/0.6/user/messages/1.json", :method => :get }, + { :controller => "api/messages", :action => "show", :id => "1", :format => "json" } + ) + assert_routing( + { :path => "/api/0.6/user/messages", :method => :post }, + { :controller => "api/messages", :action => "create" } + ) + assert_routing( + { :path => "/api/0.6/user/messages/1", :method => :post }, + { :controller => "api/messages", :action => "update", :id => "1" } + ) + assert_routing( + { :path => "/api/0.6/user/messages/1", :method => :delete }, + { :controller => "api/messages", :action => "destroy", :id => "1" } + ) + end + + def test_create_success + recipient = create(:user) + sender = create(:user) + + sender_token = create(:oauth_access_token, + :resource_owner_id => sender.id, + :scopes => %w[send_messages consume_messages]) + sender_auth = bearer_authorization_header(sender_token.token) + + msg = build(:message) + + assert_difference "Message.count", 1 do + assert_difference "ActionMailer::Base.deliveries.size", 1 do + perform_enqueued_jobs do + post api_messages_path, + :params => { :title => msg.title, + :recipient_id => recipient.id, + :body => msg.body, + :format => "json" }, + :headers => sender_auth + assert_response :success + end + end + end + + assert_equal "application/json", response.media_type + js = ActiveSupport::JSON.decode(@response.body) + jsm = js["message"] + assert_not_nil jsm + assert_not_nil jsm["id"] + assert_equal sender.id, jsm["from_user_id"] + assert_equal sender.display_name, jsm["from_display_name"] + assert_equal recipient.id, jsm["to_user_id"] + assert_equal recipient.display_name, jsm["to_display_name"] + assert_equal msg.title, jsm["title"] + assert_not_nil jsm["sent_on"] + assert_equal !msg.from_user_visible, jsm["deleted"] + assert_not jsm.key?("message_read") + assert_equal "markdown", jsm["body_format"] + assert_equal msg.body, jsm["body"] + end + + def test_create_fail + recipient = create(:user) + + sender = create(:user) + sender_token = create(:oauth_access_token, + :resource_owner_id => sender.id, + :scopes => %w[send_messages consume_messages]) + sender_auth = bearer_authorization_header(sender_token.token) + + assert_no_difference "Message.count" do + assert_no_difference "ActionMailer::Base.deliveries.size" do + perform_enqueued_jobs do + post api_messages_path, + :params => { :title => "Title", + :recipient_id => recipient.id, + :body => "body" } + end + end + end + assert_response :unauthorized + + assert_no_difference "Message.count" do + assert_no_difference "ActionMailer::Base.deliveries.size" do + perform_enqueued_jobs do + post api_messages_path, + :params => { :recipient_id => recipient.id, + :body => "body" }, + :headers => sender_auth + end + end + end + assert_response :bad_request + + assert_no_difference "Message.count" do + assert_no_difference "ActionMailer::Base.deliveries.size" do + perform_enqueued_jobs do + post api_messages_path, + :params => { :title => "Title", + :body => "body" }, + :headers => sender_auth + end + end + end + assert_response :bad_request + + assert_no_difference "Message.count" do + assert_no_difference "ActionMailer::Base.deliveries.size" do + perform_enqueued_jobs do + post api_messages_path, + :params => { :title => "Title", + :recipient_id => recipient.id }, + :headers => sender_auth + end + end + end + assert_response :bad_request + end + + def test_show + recipient = create(:user) + sender = create(:user) + user3 = create(:user) + + sender_token = create(:oauth_access_token, + :resource_owner_id => sender.id, + :scopes => %w[consume_messages]) + sender_auth = bearer_authorization_header(sender_token.token) + + recipient_token = create(:oauth_access_token, + :resource_owner_id => recipient.id, + :scopes => %w[consume_messages]) + recipient_auth = bearer_authorization_header(recipient_token.token) + + user3_token = create(:oauth_access_token, + :resource_owner_id => user3.id, + :scopes => %w[send_messages consume_messages]) + user3_auth = bearer_authorization_header(user3_token.token) + + msg = create(:message, :unread, :sender => sender, :recipient => recipient) + + # fail if not authorized + get api_message_path(:id => msg.id) + assert_response :unauthorized + + # only recipient and sender can read the message + get api_message_path(:id => msg.id), :headers => user3_auth + assert_response :forbidden + + # message does not exist + get api_message_path(:id => 99999), :headers => user3_auth + assert_response :not_found + + # verify xml output + get api_message_path(:id => msg.id), :headers => recipient_auth + assert_equal "application/xml", response.media_type + assert_select "message", :count => 1 do + assert_select "[id='#{msg.id}']" + assert_select "[from_user_id='#{sender.id}']" + assert_select "[from_display_name='#{sender.display_name}']" + assert_select "[to_user_id='#{recipient.id}']" + assert_select "[to_display_name='#{recipient.display_name}']" + assert_select "[sent_on]" + assert_select "[deleted='#{!msg.to_user_visible}']" + assert_select "[message_read='#{msg.message_read}']" + assert_select "[body_format='markdown']" + assert_select "title", msg.title + assert_select "body", msg.body + end + + # verify json output + get api_message_path(:id => msg.id, :format => "json"), :headers => recipient_auth + assert_equal "application/json", response.media_type + js = ActiveSupport::JSON.decode(@response.body) + jsm = js["message"] + assert_not_nil jsm + assert_equal msg.id, jsm["id"] + assert_equal sender.id, jsm["from_user_id"] + assert_equal sender.display_name, jsm["from_display_name"] + assert_equal recipient.id, jsm["to_user_id"] + assert_equal recipient.display_name, jsm["to_display_name"] + assert_equal msg.title, jsm["title"] + assert_not_nil jsm["sent_on"] + assert_equal msg.message_read, jsm["message_read"] + assert_equal !msg.to_user_visible, jsm["deleted"] + assert_equal "markdown", jsm["body_format"] + assert_equal msg.body, jsm["body"] + + get api_message_path(:id => msg.id), :headers => sender_auth + assert_equal "application/xml", response.media_type + assert_select "message", :count => 1 do + assert_select "[id='#{msg.id}']" + assert_select "[from_user_id='#{sender.id}']" + assert_select "[from_display_name='#{sender.display_name}']" + assert_select "[to_user_id='#{recipient.id}']" + assert_select "[to_display_name='#{recipient.display_name}']" + assert_select "[sent_on]" + assert_select "[deleted='#{!msg.from_user_visible}']" + assert_select "[message_read='#{msg.message_read}']", 0 + assert_select "[body_format='markdown']" + assert_select "title", msg.title + assert_select "body", msg.body + end + + # verify json output + get api_message_path(:id => msg.id, :format => "json"), :headers => sender_auth + assert_equal "application/json", response.media_type + js = ActiveSupport::JSON.decode(@response.body) + jsm = js["message"] + assert_not_nil jsm + assert_equal msg.id, jsm["id"] + assert_equal sender.id, jsm["from_user_id"] + assert_equal sender.display_name, jsm["from_display_name"] + assert_equal recipient.id, jsm["to_user_id"] + assert_equal recipient.display_name, jsm["to_display_name"] + assert_equal msg.title, jsm["title"] + assert_not_nil jsm["sent_on"] + assert_equal !msg.from_user_visible, jsm["deleted"] + assert_not jsm.key?("message_read") + assert_equal "markdown", jsm["body_format"] + assert_equal msg.body, jsm["body"] + end + + def test_update_status + recipient = create(:user) + sender = create(:user) + user3 = create(:user) + + recipient_token = create(:oauth_access_token, + :resource_owner_id => recipient.id, + :scopes => %w[consume_messages]) + recipient_auth = bearer_authorization_header(recipient_token.token) + + user3_token = create(:oauth_access_token, + :resource_owner_id => user3.id, + :scopes => %w[send_messages consume_messages]) + user3_auth = bearer_authorization_header(user3_token.token) + + msg = create(:message, :unread, :sender => sender, :recipient => recipient) + + # attempt to mark message as read by recipient, not authenticated + post api_message_path(:id => msg.id), :params => { :read_status => true } + assert_response :unauthorized + + # attempt to mark message as read by recipient, not allowed + post api_message_path(:id => msg.id), :params => { :read_status => true }, :headers => user3_auth + assert_response :forbidden + + # missing parameter + post api_message_path(:id => msg.id), :headers => recipient_auth + assert_response :bad_request + + # wrong type of parameter + post api_message_path(:id => msg.id), + :params => { :read_status => "not a boolean" }, + :headers => recipient_auth + assert_response :bad_request + + # mark message as read by recipient + post api_message_path(:id => msg.id, :format => "json"), + :params => { :read_status => true }, + :headers => recipient_auth + assert_response :success + assert_equal "application/json", response.media_type + js = ActiveSupport::JSON.decode(@response.body) + jsm = js["message"] + assert_not_nil jsm + assert_equal msg.id, jsm["id"] + assert_equal sender.id, jsm["from_user_id"] + assert_equal sender.display_name, jsm["from_display_name"] + assert_equal recipient.id, jsm["to_user_id"] + assert_equal recipient.display_name, jsm["to_display_name"] + assert_equal msg.title, jsm["title"] + assert_not_nil jsm["sent_on"] + assert jsm["message_read"] + assert_equal !msg.to_user_visible, jsm["deleted"] + assert_equal "markdown", jsm["body_format"] + assert_equal msg.body, jsm["body"] + + # mark message as unread by recipient + post api_message_path(:id => msg.id, :format => "json"), + :params => { :read_status => false }, + :headers => recipient_auth + assert_response :success + assert_equal "application/json", response.media_type + js = ActiveSupport::JSON.decode(@response.body) + jsm = js["message"] + assert_not_nil jsm + assert_equal msg.id, jsm["id"] + assert_equal sender.id, jsm["from_user_id"] + assert_equal sender.display_name, jsm["from_display_name"] + assert_equal recipient.id, jsm["to_user_id"] + assert_equal recipient.display_name, jsm["to_display_name"] + assert_equal msg.title, jsm["title"] + assert_not_nil jsm["sent_on"] + assert_not jsm["message_read"] + assert_equal !msg.to_user_visible, jsm["deleted"] + assert_equal "markdown", jsm["body_format"] + assert_equal msg.body, jsm["body"] + end + + def test_delete + recipient = create(:user) + recipient_token = create(:oauth_access_token, + :resource_owner_id => recipient.id, + :scopes => %w[consume_messages]) + recipient_auth = bearer_authorization_header(recipient_token.token) + + sender = create(:user) + sender_token = create(:oauth_access_token, + :resource_owner_id => sender.id, + :scopes => %w[send_messages consume_messages]) + sender_auth = bearer_authorization_header(sender_token.token) + + user3 = create(:user) + user3_token = create(:oauth_access_token, + :resource_owner_id => user3.id, + :scopes => %w[send_messages consume_messages]) + user3_auth = bearer_authorization_header(user3_token.token) + + msg = create(:message, :read, :sender => sender, :recipient => recipient) + + # attempt to delete message, not authenticated + delete api_message_path(:id => msg.id) + assert_response :unauthorized + + # attempt to delete message, by user3 + delete api_message_path(:id => msg.id), :headers => user3_auth + assert_response :forbidden + + # delete message by recipient + delete api_message_path(:id => msg.id, :format => "json"), :headers => recipient_auth + assert_response :success + assert_equal "application/json", response.media_type + js = ActiveSupport::JSON.decode(@response.body) + jsm = js["message"] + assert_not_nil jsm + assert_equal msg.id, jsm["id"] + assert_equal sender.id, jsm["from_user_id"] + assert_equal sender.display_name, jsm["from_display_name"] + assert_equal recipient.id, jsm["to_user_id"] + assert_equal recipient.display_name, jsm["to_display_name"] + assert_equal msg.title, jsm["title"] + assert_not_nil jsm["sent_on"] + assert_equal msg.message_read, jsm["message_read"] + assert jsm["deleted"] + assert_equal "markdown", jsm["body_format"] + assert_equal msg.body, jsm["body"] + + # delete message by sender + delete api_message_path(:id => msg.id, :format => "json"), :headers => sender_auth + assert_response :success + assert_equal "application/json", response.media_type + js = ActiveSupport::JSON.decode(@response.body) + jsm = js["message"] + assert_not_nil jsm + assert_equal msg.id, jsm["id"] + assert_equal sender.id, jsm["from_user_id"] + assert_equal sender.display_name, jsm["from_display_name"] + assert_equal recipient.id, jsm["to_user_id"] + assert_equal recipient.display_name, jsm["to_display_name"] + assert_equal msg.title, jsm["title"] + assert_not_nil jsm["sent_on"] + assert jsm["deleted"] + assert_not jsm.key?("message_read") + assert_equal "markdown", jsm["body_format"] + assert_equal msg.body, jsm["body"] + end + + def test_list_messages + user1 = create(:user) + user1_token = create(:oauth_access_token, + :resource_owner_id => user1.id, + :scopes => %w[send_messages consume_messages]) + user1_auth = bearer_authorization_header(user1_token.token) + + user2 = create(:user) + user2_token = create(:oauth_access_token, + :resource_owner_id => user2.id, + :scopes => %w[send_messages consume_messages]) + user2_auth = bearer_authorization_header(user2_token.token) + + user3 = create(:user) + user3_token = create(:oauth_access_token, + :resource_owner_id => user3.id, + :scopes => %w[send_messages consume_messages]) + user3_auth = bearer_authorization_header(user3_token.token) + + # create some messages between users + # user | inbox | outbox + # 1 | 0 | 3 + # 2 | 2 | 1 + # 3 | 2 | 0 + create(:message, :unread, :sender => user1, :recipient => user2) + create(:message, :unread, :sender => user1, :recipient => user2) + create(:message, :unread, :sender => user1, :recipient => user3) + create(:message, :unread, :sender => user2, :recipient => user3) + + # only authorized users + get inbox_api_messages_path + assert_response :unauthorized + get outbox_api_messages_path + assert_response :unauthorized + + # no messages in user1.inbox + get inbox_api_messages_path, :headers => user1_auth + assert_response :success + assert_equal "application/xml", response.media_type + assert_select "message", :count => 0 + + # 3 messages in user1.outbox + get outbox_api_messages_path, :headers => user1_auth + assert_response :success + assert_equal "application/xml", response.media_type + assert_select "message", :count => 3 do + assert_select "[from_user_id='#{user1.id}']" + assert_select "[from_display_name='#{user1.display_name}']" + assert_select "[to_user_id]" + assert_select "[to_display_name]" + assert_select "[sent_on]" + assert_select "[message_read]", 0 + assert_select "[deleted='false']" + assert_select "[body_format]" + assert_select "body", false + assert_select "title" + end + + # 2 messages in user2.inbox + get inbox_api_messages_path, :headers => user2_auth + assert_response :success + assert_equal "application/xml", response.media_type + assert_select "message", :count => 2 do + assert_select "[from_user_id]" + assert_select "[from_display_name]" + assert_select "[to_user_id='#{user2.id}']" + assert_select "[to_display_name='#{user2.display_name}']" + assert_select "[sent_on]" + assert_select "[message_read='false']" + assert_select "[deleted='false']" + assert_select "[body_format]" + assert_select "body", false + assert_select "title" + end + + # 1 message in user2.outbox + get outbox_api_messages_path, :headers => user2_auth + assert_response :success + assert_equal "application/xml", response.media_type + assert_select "message", :count => 1 do + assert_select "[from_user_id='#{user2.id}']" + assert_select "[from_display_name='#{user2.display_name}']" + assert_select "[to_user_id]" + assert_select "[to_display_name]" + assert_select "[sent_on]" + assert_select "[deleted='false']" + assert_select "[message_read]", 0 + assert_select "[body_format]" + assert_select "body", false + assert_select "title" + end + + # 2 messages in user3.inbox + get inbox_api_messages_path, :headers => user3_auth + assert_response :success + assert_equal "application/xml", response.media_type + assert_select "message", :count => 2 do + assert_select "[from_user_id]" + assert_select "[from_display_name]" + assert_select "[to_user_id='#{user3.id}']" + assert_select "[to_display_name='#{user3.display_name}']" + assert_select "[sent_on]" + assert_select "[message_read='false']" + assert_select "[deleted='false']" + assert_select "[body_format]" + assert_select "body", false + assert_select "title" + end + + # 0 messages in user3.outbox + get outbox_api_messages_path, :headers => user3_auth + assert_response :success + assert_equal "application/xml", response.media_type + assert_select "message", :count => 0 + end + + def test_paged_list_messages_asc + recipient = create(:user) + recipient_token = create(:oauth_access_token, + :resource_owner_id => recipient.id, + :scopes => %w[consume_messages]) + recipient_auth = bearer_authorization_header(recipient_token.token) + + sender = create(:user) + + create_list(:message, 100, :unread, :sender => sender, :recipient => recipient) + + msgs_read = {} + params = { :order => "oldest", :limit => 20 } + 10.times do + get inbox_api_messages_path(:format => "json"), + :params => params, + :headers => recipient_auth + assert_response :success + assert_equal "application/json", response.media_type + js = ActiveSupport::JSON.decode(@response.body) + jsm = js["messages"] + assert_operator jsm.count, :<=, 20 + + break if jsm.nil? || jsm.count.zero? + + assert_operator(jsm[0]["id"], :>=, params[:from_id]) unless params[:from_id].nil? + # ensure ascending order + (0..jsm.count - 1).each do |i| + assert_operator(jsm[i]["id"], :<, jsm[i + 1]["id"]) unless i == jsm.count - 1 + msgs_read[jsm[i]["id"]] = jsm[i] + end + params[:from_id] = jsm[jsm.count - 1]["id"] + end + assert_equal 100, msgs_read.count + end + + def test_paged_list_messages_desc + recipient = create(:user) + recipient_token = create(:oauth_access_token, + :resource_owner_id => recipient.id, + :scopes => %w[consume_messages]) + recipient_auth = bearer_authorization_header(recipient_token.token) + + sender = create(:user) + + create_list(:message, 100, :unread, :sender => sender, :recipient => recipient) + + real_max_id = -1 + msgs_read = {} + params = { :order => "newest", :limit => 20 } + 10.times do + get inbox_api_messages_path(:format => "json"), + :params => params, + :headers => recipient_auth + assert_response :success + assert_equal "application/json", response.media_type + js = ActiveSupport::JSON.decode(@response.body) + jsm = js["messages"] + assert_operator jsm.count, :<=, 20 + + break if jsm.nil? || jsm.count.zero? + + if params[:from_id].nil? + real_max_id = jsm[0]["id"] + else + assert_operator jsm[0]["id"], :<=, params[:from_id] + end + # ensure descending order + (0..jsm.count - 1).each do |i| + assert_operator(jsm[i]["id"], :>, jsm[i + 1]["id"]) unless i == jsm.count - 1 + msgs_read[jsm[i]["id"]] = jsm[i] + end + params[:from_id] = jsm[jsm.count - 1]["id"] + end + assert_equal 100, msgs_read.count + assert_not_equal(-1, real_max_id) + + # invoke without min_id/max_id parameters, verify that we get the last batch + get inbox_api_messages_path(:format => "json"), :params => { :limit => 20 }, :headers => recipient_auth + assert_response :success + assert_equal "application/json", response.media_type + js = ActiveSupport::JSON.decode(@response.body) + jsm = js["messages"] + assert_not_nil jsm + assert_equal real_max_id, jsm[0]["id"] + end + end +end