background_job_spec.rb 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. require 'rails_helper'
  2. RSpec.describe Observer::Ticket::Article::CommunicateTwitter::BackgroundJob, type: :job do
  3. let(:article) { create(:twitter_article, **(try(:factory_options) || {})) }
  4. describe 'core behavior', :use_vcr do
  5. # This job runs automatically whenever an article is created.
  6. # We disable this auto-execution so we can invoke it manually in the tests below.
  7. around do |example|
  8. ActiveRecord::Base.observers.disable('observer::_ticket::_article::_communicate_twitter')
  9. example.run
  10. ActiveRecord::Base.observers.enable('observer::_ticket::_article::_communicate_twitter')
  11. end
  12. context 'for tweets' do
  13. let(:tweet_attributes) do
  14. {
  15. 'mention_ids' => [],
  16. 'geo' => {},
  17. 'retweeted' => false,
  18. 'possibly_sensitive' => false,
  19. 'in_reply_to_user_id' => nil,
  20. 'place' => {},
  21. 'retweet_count' => 0,
  22. 'source' => '<a href="https://zammad.com/" rel="nofollow">zammad</a>',
  23. 'favorited' => false,
  24. 'truncated' => false,
  25. }
  26. end
  27. let(:links_array) do
  28. [{
  29. url: 'https://twitter.com/_/status/1244937367435108360',
  30. target: '_blank',
  31. name: 'on Twitter',
  32. }]
  33. end
  34. it 'increments the "delivery_retry" preference' do
  35. expect { described_class.new(article.id).perform }
  36. .to change { article.reload.preferences[:delivery_retry] }.to(1)
  37. end
  38. it 'dispatches the tweet' do
  39. described_class.new(article.id).perform
  40. expect(WebMock)
  41. .to have_requested(:post, 'https://api.twitter.com/1.1/statuses/update.json')
  42. .with(body: "in_reply_to_status_id&status=#{CGI.escape(article.body)}" )
  43. end
  44. it 'updates the article with tweet attributes' do
  45. expect { described_class.new(article.id).perform }
  46. .to change { article.reload.message_id }.to('1244937367435108360')
  47. .and change { article.reload.preferences[:twitter] }.to(hash_including(tweet_attributes))
  48. .and change { article.reload.preferences[:links] }.to(links_array)
  49. end
  50. it 'sets the appropriate delivery status attributes' do
  51. expect { described_class.new(article.id).perform }
  52. .to change { article.reload.preferences[:delivery_status] }.to('success')
  53. .and change { article.reload.preferences[:delivery_status_date] }.to(an_instance_of(ActiveSupport::TimeWithZone))
  54. .and not_change { article.reload.preferences[:delivery_status_message] }.from(nil)
  55. end
  56. context 'with a user mention' do
  57. let(:factory_options) { { body: '@twitter @twitterlive Don’t mind me, just testing the API' } }
  58. it 'updates the article with tweet recipients' do
  59. expect { described_class.new(article.id).perform }
  60. .to change { article.reload.to }.to('@Twitter @TwitterLive')
  61. end
  62. end
  63. end
  64. context 'for DMs' do
  65. let(:article) { create(:twitter_dm_article, :pending_delivery, recipient: recipient, body: 'Please ignore this message.') }
  66. let(:recipient) { create(:twitter_authorization, uid: '2688148651', username: 'AirbnbHelp') }
  67. let(:request_body) do
  68. {
  69. event: {
  70. type: 'message_create',
  71. message_create: {
  72. target: { recipient_id: recipient.uid },
  73. message_data: { text: article.body }
  74. }
  75. }
  76. }.to_json
  77. end
  78. let(:dm_attributes) do
  79. {
  80. 'recipient_id' => recipient.uid,
  81. 'sender_id' => '1205290247124217856',
  82. }
  83. end
  84. let(:links_array) do
  85. [{
  86. url: "https://twitter.com/messages/#{recipient.uid}-1205290247124217856",
  87. target: '_blank',
  88. name: 'on Twitter',
  89. }]
  90. end
  91. it 'increments the "delivery_retry" preference' do
  92. expect { described_class.new(article.id).perform }
  93. .to change { article.reload.preferences[:delivery_retry] }.to(1)
  94. end
  95. it 'dispatches the DM' do
  96. described_class.new(article.id).perform
  97. expect(WebMock)
  98. .to have_requested(:post, 'https://api.twitter.com/1.1/direct_messages/events/new.json')
  99. .with(body: request_body)
  100. end
  101. it 'updates the article with DM attributes' do
  102. expect { described_class.new(article.id).perform }
  103. .to change { article.reload.message_id }.to('1244953398509617156')
  104. .and change { article.reload.preferences[:twitter] }.to(hash_including(dm_attributes))
  105. .and change { article.reload.preferences[:links] }.to(links_array)
  106. end
  107. it 'sets the appropriate delivery status attributes' do
  108. expect { described_class.new(article.id).perform }
  109. .to change { article.reload.preferences[:delivery_status] }.to('success')
  110. .and change { article.reload.preferences[:delivery_status_date] }.to(an_instance_of(ActiveSupport::TimeWithZone))
  111. .and not_change { article.reload.preferences[:delivery_status_message] }.from(nil)
  112. end
  113. end
  114. describe 'failure cases' do
  115. shared_examples 'for failure cases' do
  116. it 'raises an error and sets the appropriate delivery status messages' do
  117. expect { described_class.new(article.id).perform }
  118. .to raise_error(error_message)
  119. .and change { article.reload.preferences[:delivery_status] }.to('fail')
  120. .and change { article.reload.preferences[:delivery_status_date] }.to(an_instance_of(ActiveSupport::TimeWithZone))
  121. .and change { article.reload.preferences[:delivery_status_message] }.to(error_message)
  122. end
  123. end
  124. context 'when article.ticket.preferences["channel_id"] is nil' do
  125. before do
  126. article.ticket.preferences.delete(:channel_id)
  127. article.ticket.save
  128. end
  129. let(:error_message) { "Can't find ticket.preferences['channel_id'] for Ticket.find(#{article.ticket_id})" }
  130. include_examples 'for failure cases'
  131. end
  132. context 'if article.ticket.preferences["channel_id"] has been removed' do
  133. before { channel.destroy }
  134. let(:channel) { Channel.find(article.ticket.preferences[:channel_id]) }
  135. let(:error_message) { "No such channel id #{article.ticket.preferences['channel_id']}" }
  136. include_examples 'for failure cases'
  137. context 'and another suitable channel exists (matching on ticket.preferences[:channel_screen_name])' do
  138. let!(:new_channel) { create(:twitter_channel, custom_options: { user: { screen_name: channel.options[:user][:screen_name] } }) }
  139. it 'uses that channel' do
  140. described_class.new(article.id).perform
  141. expect(WebMock)
  142. .to have_requested(:post, 'https://api.twitter.com/1.1/statuses/update.json')
  143. .with(body: "in_reply_to_status_id&status=#{CGI.escape(article.body)}" )
  144. end
  145. end
  146. end
  147. context 'if article.ticket.preferences["channel_id"] isn’t actually a twitter channel' do
  148. before do
  149. article.ticket.preferences[:channel_id] = create(:email_channel).id
  150. article.ticket.save
  151. end
  152. let(:error_message) { "Channel.find(#{article.ticket.preferences[:channel_id]}) isn't a twitter channel!" }
  153. include_examples 'for failure cases'
  154. end
  155. context 'when tweet dispatch fails (e.g., due to authentication error)' do
  156. before do
  157. article.ticket.preferences[:channel_id] = create(:twitter_channel, :invalid).id
  158. article.ticket.save
  159. end
  160. let(:error_message) { "Can't use Channel::Driver::Twitter: #<Twitter::Error::Unauthorized: Invalid or expired token.>" }
  161. include_examples 'for failure cases'
  162. end
  163. context 'when tweet comes back nil' do
  164. before do
  165. allow(Twitter::REST::Client).to receive(:new).with(any_args).and_return(client_double)
  166. allow(client_double).to receive(:update).with(any_args).and_return(nil)
  167. end
  168. let(:client_double) { double('Twitter::REST::Client') }
  169. let(:error_message) { 'Got no tweet!' }
  170. include_examples 'for failure cases'
  171. end
  172. context 'on the fourth time it fails' do
  173. before { Channel.find(article.ticket.preferences[:channel_id]).destroy }
  174. let(:error_message) { "No such channel id #{article.ticket.preferences['channel_id']}" }
  175. let(:factory_options) { { preferences: { delivery_retry: 3 } } }
  176. it 'adds a delivery failure note (article) to the ticket' do
  177. expect { described_class.new(article.id).perform }
  178. .to raise_error(error_message)
  179. .and change { article.ticket.reload.articles.count }.by(1)
  180. expect(Ticket::Article.last.attributes).to include(
  181. 'content_type' => 'text/plain',
  182. 'body' => "Unable to send tweet: #{error_message}",
  183. 'internal' => true,
  184. 'sender_id' => Ticket::Article::Sender.find_by(name: 'System').id,
  185. 'type_id' => Ticket::Article::Type.find_by(name: 'note').id,
  186. 'preferences' => {
  187. 'delivery_article_id_related' => article.id,
  188. 'delivery_message' => true,
  189. },
  190. )
  191. end
  192. end
  193. end
  194. end
  195. end