account_activity_api_spec.rb 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe 'Twitter > Account Activity API', integration: true, required_envs: %w[TWITTER_CONSUMER_KEY TWITTER_CONSUMER_SECRET TWITTER_OAUTH_TOKEN TWITTER_OAUTH_TOKEN_SECRET TWITTER_USER_ID TWITTER_DM_REAL_RECIPIENT TWITTER_SEARCH_CONSUMER_KEY TWITTER_SEARCH_CONSUMER_SECRET TWITTER_SEARCH_OAUTH_TOKEN TWITTER_SEARCH_OAUTH_TOKEN_SECRET TWITTER_SEARCH_USER_ID], use_vcr: :time_sensitive do # rubocop:disable RSpec/DescribeClass
  4. subject(:channel) { create(:twitter_channel, custom_options: { sync: { search: nil } }) }
  5. let(:twitter_helper) do
  6. RSpecTwitter::Helper.new(auth_data_search_app)
  7. end
  8. let(:twitter_helper_channel) do
  9. RSpecTwitter::Helper.new(auth_data_channel_app)
  10. end
  11. let(:channel_attributes) do
  12. {
  13. 'status_in' => 'ok',
  14. 'last_log_in' => '',
  15. 'status_out' => nil,
  16. 'last_log_out' => nil,
  17. }
  18. end
  19. def auth_data_channel_app
  20. {
  21. consumer_key: ENV['TWITTER_CONSUMER_KEY'],
  22. consumer_secret: ENV['TWITTER_CONSUMER_SECRET'],
  23. oauth_token: ENV['TWITTER_OAUTH_TOKEN'],
  24. oauth_token_secret: ENV['TWITTER_OAUTH_TOKEN_SECRET'],
  25. }
  26. end
  27. def auth_data_search_app
  28. {
  29. consumer_key: ENV['TWITTER_SEARCH_CONSUMER_KEY'],
  30. consumer_secret: ENV['TWITTER_SEARCH_CONSUMER_SECRET'],
  31. oauth_token: ENV['TWITTER_SEARCH_OAUTH_TOKEN'],
  32. oauth_token_secret: ENV['TWITTER_SEARCH_OAUTH_TOKEN_SECRET'],
  33. }
  34. end
  35. before :all do # rubocop:disable RSpec/BeforeAfterAll
  36. # TODO: Remove this once we have working apps for the Twitter API again.
  37. if VCR.configuration.allow_http_connections_when_no_cassette?
  38. skip 'This test is currently not working in the live mode due to having suspended apps for the Twitter API.'
  39. end
  40. if %w[1 true].include?(ENV['CI_IGNORE_CASSETTES'])
  41. RSpecTwitter::Helper.new(auth_data_search_app).delete_old_tweets
  42. RSpecTwitter::Helper.new(auth_data_channel_app).delete_old_tweets
  43. end
  44. end
  45. it 'sets successful status attributes' do
  46. expect { channel.fetch }
  47. .to change { channel.reload.attributes }
  48. .to hash_including(channel_attributes)
  49. end
  50. context 'with search term configured' do
  51. subject(:channel) { create(:twitter_channel, custom_options: { sync: { search: [ { term: identifier, group_id: Group.first.id } ] } }) }
  52. let(:identifier) do
  53. random_number = %w[1 true].include?(ENV['CI_IGNORE_CASSETTES']) ? SecureRandom.uuid.delete('-') : '0509d41afd66476fa52a1c3892f669eb'
  54. "zammad_testing_#{random_number}"
  55. end
  56. let(:ticket_title) { "Come and join our team to bring Zammad even further forward! #{identifier}" }
  57. after do
  58. twitter_helper.delete_all_tweets(identifier)
  59. twitter_helper_channel.delete_all_tweets(identifier)
  60. end
  61. context 'with recent tweets' do
  62. before do
  63. twitter_helper.create_tweet(ticket_title)
  64. twitter_helper.create_tweet("dummy API activity test! #{identifier}")
  65. twitter_helper_channel.ensure_tweet_availability(identifier, 2)
  66. end
  67. let(:expected_ticket_attributes) do
  68. {
  69. 'title' => ticket_title.size > 80 ? "#{ticket_title[0..79]}..." : ticket_title,
  70. 'preferences' => {
  71. 'channel_id' => channel.id,
  72. 'channel_screen_name' => channel.options[:user][:screen_name]
  73. },
  74. }
  75. end
  76. it 'creates an article for each recent tweet', :aggregate_failures do
  77. expect { channel.fetch }.to change(Ticket, :count).by(2)
  78. expect(Ticket.last.attributes).to include(expected_ticket_attributes)
  79. end
  80. end
  81. context 'with responses to other tweets' do
  82. before do
  83. parent_tweet = twitter_helper.create_tweet('Parent tweet without identifier')
  84. twitter_helper.create_tweet("Response test! #{identifier}", in_reply_to_status_id: parent_tweet.id)
  85. twitter_helper_channel.ensure_tweet_availability(identifier, 1)
  86. end
  87. let(:ticket_articles) { Ticket.last.articles }
  88. it 'creates articles for parent tweets as well', :aggregate_failures do
  89. expect { channel.fetch }.to change(Ticket, :count).by(1)
  90. expect(ticket_articles.first.body).not_to include(identifier) # parent tweet
  91. expect(ticket_articles.last.body).to include(identifier) # search result
  92. end
  93. end
  94. context 'with "track_retweets" option' do
  95. before do
  96. tweet = twitter_helper_channel.create_tweet("Zammad is amazing! #{identifier}")
  97. twitter_helper.create_retweet(tweet.id)
  98. twitter_helper_channel.ensure_tweet_availability(identifier, 2)
  99. end
  100. context 'when set to false' do
  101. it 'skips retweets' do
  102. expect { channel.fetch }
  103. .not_to change { Ticket.where('title LIKE ?', 'RT @%').count }.from(0)
  104. end
  105. end
  106. context 'when set to true' do
  107. subject(:channel) { create(:twitter_channel, custom_options: { sync: { track_retweets: true, search: [ { term: identifier, group_id: Group.first.id } ] } }) }
  108. it 'creates an article for each recent tweet/retweet' do
  109. expect { channel.fetch }
  110. .to change { Ticket.where('title LIKE ?', 'RT @%').count }.by(1)
  111. .and change(Ticket, :count).by(1)
  112. end
  113. end
  114. end
  115. context 'with "import_older_tweets" option' do
  116. before do
  117. twitter_helper.create_tweet("Zammad is amazing! #{identifier}")
  118. twitter_helper.create_tweet("Such. A. Beautiful. Helpdesk. Tool. #{identifier}")
  119. twitter_helper.create_tweet("Need a helpdesk tool? Zammad <3 #{identifier}")
  120. twitter_helper_channel.ensure_tweet_availability(identifier, 3)
  121. travel 16.days
  122. channel.update!(created_at: Time.zone.now.utc)
  123. travel_back
  124. end
  125. context 'when false (default)' do
  126. it 'skips tweets 15+ days older than channel itself' do
  127. expect { channel.fetch }.not_to change(Ticket, :count)
  128. end
  129. end
  130. context 'when true' do
  131. subject(:channel) { create(:twitter_channel, custom_options: { sync: { import_older_tweets: true, search: [ { term: identifier, group_id: Group.first.id } ] } }) }
  132. it 'creates an article for each tweet' do
  133. expect { channel.fetch }.to change(Ticket, :count).by(3)
  134. end
  135. end
  136. end
  137. context 'when fetched tweets have already been imported' do
  138. before do
  139. tweet_ids = []
  140. 3.times do |index|
  141. tweet = twitter_helper.create_tweet("Tweet #{index}! #{identifier}")
  142. tweet_ids << tweet.id
  143. end
  144. twitter_helper_channel.ensure_tweet_availability(identifier, 3)
  145. tweet_ids.each { |tweet_id| create(:ticket_article, message_id: tweet_id) }
  146. end
  147. it 'does not import duplicates' do
  148. expect { channel.fetch }.not_to change(Ticket::Article, :count)
  149. end
  150. end
  151. context 'with a very common search term' do
  152. subject(:channel) { create(:twitter_channel, custom_options: { sync: { search: [ { term: 'corona', group_id: Group.first.id } ] } }) }
  153. let(:twitter_articles) { Ticket::Article.joins(:type).where(ticket_article_types: { name: 'twitter status' }) }
  154. before do
  155. stub_const('TwitterSync::MAX_TWEETS_PER_IMPORT', 10)
  156. end
  157. # Note that this rate limiting is partially duplicated
  158. # in #fetchable?, which prevents #fetch from running
  159. # more than once in a 20-minute period.
  160. it 'imports max. ~120 articles every 15 minutes', :aggregate_failures do
  161. freeze_time
  162. channel.fetch
  163. expect((twitter_articles - Ticket.last.articles).count).to be <= 10
  164. expect(twitter_articles.count).to be > 10
  165. travel(10.minutes)
  166. expect { channel.fetch }.not_to change(Ticket::Article, :count)
  167. travel(6.minutes)
  168. expect { channel.fetch }.to change(Ticket::Article, :count)
  169. travel_back
  170. end
  171. end
  172. end
  173. end