123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- require 'rails_helper'
- 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
- subject(:channel) { create(:twitter_channel, custom_options: { sync: { search: nil } }) }
- let(:twitter_helper) do
- RSpecTwitter::Helper.new(auth_data_search_app)
- end
- let(:twitter_helper_channel) do
- RSpecTwitter::Helper.new(auth_data_channel_app)
- end
- let(:channel_attributes) do
- {
- 'status_in' => 'ok',
- 'last_log_in' => '',
- 'status_out' => nil,
- 'last_log_out' => nil,
- }
- end
- def auth_data_channel_app
- {
- consumer_key: ENV['TWITTER_CONSUMER_KEY'],
- consumer_secret: ENV['TWITTER_CONSUMER_SECRET'],
- oauth_token: ENV['TWITTER_OAUTH_TOKEN'],
- oauth_token_secret: ENV['TWITTER_OAUTH_TOKEN_SECRET'],
- }
- end
- def auth_data_search_app
- {
- consumer_key: ENV['TWITTER_SEARCH_CONSUMER_KEY'],
- consumer_secret: ENV['TWITTER_SEARCH_CONSUMER_SECRET'],
- oauth_token: ENV['TWITTER_SEARCH_OAUTH_TOKEN'],
- oauth_token_secret: ENV['TWITTER_SEARCH_OAUTH_TOKEN_SECRET'],
- }
- end
- before :all do # rubocop:disable RSpec/BeforeAfterAll
- # TODO: Remove this once we have working apps for the Twitter API again.
- if VCR.configuration.allow_http_connections_when_no_cassette?
- skip 'This test is currently not working in the live mode due to having suspended apps for the Twitter API.'
- end
- if %w[1 true].include?(ENV['CI_IGNORE_CASSETTES'])
- RSpecTwitter::Helper.new(auth_data_search_app).delete_old_tweets
- RSpecTwitter::Helper.new(auth_data_channel_app).delete_old_tweets
- end
- end
- it 'sets successful status attributes' do
- expect { channel.fetch }
- .to change { channel.reload.attributes }
- .to hash_including(channel_attributes)
- end
- context 'with search term configured' do
- subject(:channel) { create(:twitter_channel, custom_options: { sync: { search: [ { term: identifier, group_id: Group.first.id } ] } }) }
- let(:identifier) do
- random_number = %w[1 true].include?(ENV['CI_IGNORE_CASSETTES']) ? SecureRandom.uuid.delete('-') : '0509d41afd66476fa52a1c3892f669eb'
- "zammad_testing_#{random_number}"
- end
- let(:ticket_title) { "Come and join our team to bring Zammad even further forward! #{identifier}" }
- after do
- twitter_helper.delete_all_tweets(identifier)
- twitter_helper_channel.delete_all_tweets(identifier)
- end
- context 'with recent tweets' do
- before do
- twitter_helper.create_tweet(ticket_title)
- twitter_helper.create_tweet("dummy API activity test! #{identifier}")
- twitter_helper_channel.ensure_tweet_availability(identifier, 2)
- end
- let(:expected_ticket_attributes) do
- {
- 'title' => ticket_title.size > 80 ? "#{ticket_title[0..79]}..." : ticket_title,
- 'preferences' => {
- 'channel_id' => channel.id,
- 'channel_screen_name' => channel.options[:user][:screen_name]
- },
- }
- end
- it 'creates an article for each recent tweet', :aggregate_failures do
- expect { channel.fetch }.to change(Ticket, :count).by(2)
- expect(Ticket.last.attributes).to include(expected_ticket_attributes)
- end
- end
- context 'with responses to other tweets' do
- before do
- parent_tweet = twitter_helper.create_tweet('Parent tweet without identifier')
- twitter_helper.create_tweet("Response test! #{identifier}", in_reply_to_status_id: parent_tweet.id)
- twitter_helper_channel.ensure_tweet_availability(identifier, 1)
- end
- let(:ticket_articles) { Ticket.last.articles }
- it 'creates articles for parent tweets as well', :aggregate_failures do
- expect { channel.fetch }.to change(Ticket, :count).by(1)
- expect(ticket_articles.first.body).not_to include(identifier) # parent tweet
- expect(ticket_articles.last.body).to include(identifier) # search result
- end
- end
- context 'with "track_retweets" option' do
- before do
- tweet = twitter_helper_channel.create_tweet("Zammad is amazing! #{identifier}")
- twitter_helper.create_retweet(tweet.id)
- twitter_helper_channel.ensure_tweet_availability(identifier, 2)
- end
- context 'when set to false' do
- it 'skips retweets' do
- expect { channel.fetch }
- .not_to change { Ticket.where('title LIKE ?', 'RT @%').count }.from(0)
- end
- end
- context 'when set to true' do
- subject(:channel) { create(:twitter_channel, custom_options: { sync: { track_retweets: true, search: [ { term: identifier, group_id: Group.first.id } ] } }) }
- it 'creates an article for each recent tweet/retweet' do
- expect { channel.fetch }
- .to change { Ticket.where('title LIKE ?', 'RT @%').count }.by(1)
- .and change(Ticket, :count).by(1)
- end
- end
- end
- context 'with "import_older_tweets" option' do
- before do
- twitter_helper.create_tweet("Zammad is amazing! #{identifier}")
- twitter_helper.create_tweet("Such. A. Beautiful. Helpdesk. Tool. #{identifier}")
- twitter_helper.create_tweet("Need a helpdesk tool? Zammad <3 #{identifier}")
- twitter_helper_channel.ensure_tweet_availability(identifier, 3)
- travel 16.days
- channel.update!(created_at: Time.zone.now.utc)
- travel_back
- end
- context 'when false (default)' do
- it 'skips tweets 15+ days older than channel itself' do
- expect { channel.fetch }.not_to change(Ticket, :count)
- end
- end
- context 'when true' do
- subject(:channel) { create(:twitter_channel, custom_options: { sync: { import_older_tweets: true, search: [ { term: identifier, group_id: Group.first.id } ] } }) }
- it 'creates an article for each tweet' do
- expect { channel.fetch }.to change(Ticket, :count).by(3)
- end
- end
- end
- context 'when fetched tweets have already been imported' do
- before do
- tweet_ids = []
- 3.times do |index|
- tweet = twitter_helper.create_tweet("Tweet #{index}! #{identifier}")
- tweet_ids << tweet.id
- end
- twitter_helper_channel.ensure_tweet_availability(identifier, 3)
- tweet_ids.each { |tweet_id| create(:ticket_article, message_id: tweet_id) }
- end
- it 'does not import duplicates' do
- expect { channel.fetch }.not_to change(Ticket::Article, :count)
- end
- end
- context 'with a very common search term' do
- subject(:channel) { create(:twitter_channel, custom_options: { sync: { search: [ { term: 'corona', group_id: Group.first.id } ] } }) }
- let(:twitter_articles) { Ticket::Article.joins(:type).where(ticket_article_types: { name: 'twitter status' }) }
- before do
- stub_const('TwitterSync::MAX_TWEETS_PER_IMPORT', 10)
- end
- # Note that this rate limiting is partially duplicated
- # in #fetchable?, which prevents #fetch from running
- # more than once in a 20-minute period.
- it 'imports max. ~120 articles every 15 minutes', :aggregate_failures do
- freeze_time
- channel.fetch
- expect((twitter_articles - Ticket.last.articles).count).to be <= 10
- expect(twitter_articles.count).to be > 10
- travel(10.minutes)
- expect { channel.fetch }.not_to change(Ticket::Article, :count)
- travel(6.minutes)
- expect { channel.fetch }.to change(Ticket::Article, :count)
- travel_back
- end
- end
- end
- end
|