communicate_twitter_job_spec.rb 8.7 KB

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