Testing: Complete coverage for Channel::Driver::Twitter#fetch

This commit is contained in:
Ryan Lue 2020-02-25 16:55:34 +08:00 committed by Thorsten Eckel
parent 165c44c056
commit 2c0b858312
12 changed files with 104516 additions and 36 deletions

View file

@ -25,6 +25,7 @@ FactoryBot.define do
association :ticket, factory: :twitter_ticket
message_id { '775410014383026176' }
body { Faker::Lorem.sentence }
sender_name { 'Agent' }
trait :reply do
in_reply_to { Faker::Number.number(19) }

View file

@ -808,7 +808,7 @@ RSpec.describe Channel::Driver::Twitter do
describe 'Twitter API activity' do
it 'sets successful status attributes' do
expect { channel.fetch(true) }
expect { channel.fetch }
.to change { channel.reload.attributes }
.to hash_including(
'status_in' => 'ok',
@ -818,50 +818,181 @@ RSpec.describe Channel::Driver::Twitter do
)
end
it 'adds tickets based on .options[:sync][:search] parameters' do
expect { channel.fetch(true) }
.to change(Ticket, :count).by(8)
expect(Ticket.last.attributes).to include(
'title' => "Come and join our team to bring Zammad even further forward! It's gonna be ama...",
'preferences' => { 'channel_id' => channel.id,
'channel_screen_name' => channel.options[:user][:screen_name] },
'customer_id' => User.find_by(firstname: 'Mr.Generation', lastname: '').id
)
end
it 'skips tweets more than 15 days older than channel itself'
context 'and "track_retweets" option' do
subject(:channel) { create(:twitter_channel, custom_options: { sync: { track_retweets: true } }) }
it 'adds tickets based on .options[:sync][:search] parameters' do
expect { channel.fetch(true) }
.to change(Ticket, :count).by(21)
context 'with search term configured (at .options[:sync][:search])' do
it 'creates an article for each recent tweet' do
expect { channel.fetch }
.to change(Ticket, :count).by(8)
expect(Ticket.last.attributes).to include(
'title' => 'RT @BarackObama: Kobe was a legend on the court and just getting started in what...',
'title' => "Come and join our team to bring Zammad even further forward! It's gonna be ama...",
'preferences' => { 'channel_id' => channel.id,
'channel_screen_name' => channel.options[:user][:screen_name] },
'customer_id' => User.find_by(firstname: 'Zammad', lastname: 'Ali').id
'customer_id' => User.find_by(firstname: 'Mr.Generation', lastname: '').id
)
end
end
context 'and legacy "import_older_tweets" option' do
subject(:channel) { create(:twitter_channel, :legacy) }
it 'skips retweets' do
expect { channel.fetch }
.not_to change { Ticket.where('title LIKE ?', 'RT @%').count }.from(0)
end
it 'adds tickets based on .options[:sync][:search] parameters' do
expect { channel.fetch(true) }
.to change(Ticket, :count).by(21)
it 'skips tweets 15+ days older than channel itself' do
expect { channel.fetch }
.not_to change { Ticket.where('title LIKE ?', 'GitHub Trending Archive, 2_ Nov 2018, Ruby. %').count }.from(0)
end
expect(Ticket.last.attributes).to include(
'title' => 'Wir haben unsere DMs deaktiviert. ' \
'Leider können wir dank der neuen Twitter API k...',
'preferences' => { 'channel_id' => channel.id,
'channel_screen_name' => channel.options[:user][:screen_name] },
'customer_id' => User.find_by(firstname: 'Ccc', lastname: 'Event Logistics').id
)
context 'when fetched tweets have already been imported' do
before do
tweet_ids.each { |tweet_id| create(:ticket_article, message_id: tweet_id) }
end
let(:tweet_ids) do
[1224440380881428480,
1224426978557800449,
1224427517869809666,
1224427776654135297,
1224428510225354753,
1223188240078909440,
1223273797987508227,
1223103807283810304,
1223121619561799682,
1222872891320143878,
1222881209384161283,
1222896407524212736,
1222237955588227075,
1222108036795334657,
1222126386334388225,
1222109934923460608]
end
it 'does not import duplicates' do
expect { channel.fetch }.not_to change(Ticket::Article, :count)
end
end
context 'for very common search terms' do
subject(:channel) { create(:twitter_channel, custom_options: custom_options) }
let(:custom_options) do
{
sync: {
search: [
{
term: 'coronavirus',
group_id: Group.first.id
}
]
}
}
end
let(:twitter_articles) { Ticket::Article.joins(:type).where(ticket_article_types: { name: 'twitter status' }) }
it 'stops importing threads after 120 new articles' do
channel.fetch
expect((twitter_articles - Ticket.last.articles).count).to be < 120
expect(twitter_articles.count).to be >= 120
end
it 'refuses to import any other tweets for the next 15 minutes' do
channel.fetch
travel(14.minutes)
expect { create(:twitter_channel).fetch }
.not_to change(Ticket::Article, :count)
end
it 'resumes importing again after 15 minutes' do
channel.fetch
travel(15.minutes)
expect { create(:twitter_channel).fetch }
.to change(Ticket::Article, :count)
end
end
context 'and "track_retweets" option' do
subject(:channel) { create(:twitter_channel, custom_options: { sync: { track_retweets: true } }) }
it 'creates an article for each recent tweet/retweet' do
expect { channel.fetch }
.to change { Ticket.where('title LIKE ?', 'RT @%').count }
.and change(Ticket, :count).by(21)
end
end
context 'and "import_older_tweets" option (legacy)' do
subject(:channel) { create(:twitter_channel, :legacy) }
it 'creates an article for each tweet' do
expect { channel.fetch }
.to change { Ticket.where('title LIKE ?', 'GitHub Trending Archive, 2_ Nov 2018, Ruby. %').count }
.and change(Ticket, :count).by(21)
end
end
describe 'Race condition: when #fetch finds a half-processed, outgoing tweet' do
subject!(:channel) { create(:twitter_channel, custom_options: custom_options) }
let(:custom_options) do
{
user: {
# Must match outgoing tweet author Twitter user ID
id: '1205290247124217856',
},
sync: {
search: [
{
term: 'zammadzammadzammad',
group_id: Group.first.id
}
]
}
}
end
let!(:tweet) { create(:twitter_article, body: 'zammadzammadzammad') }
context '(i.e., after the BG job has posted the article to Twitter…' do
# NOTE: This context block cannot be set up programmatically.
# Instead, the tweet was posted, fetched, recorded into a VCR cassette,
# and then manually copied into the existing VCR cassette for this example.
context '…but before the BG job has "synced" article.message_id with tweet.id)' do
let(:twitter_job) { Delayed::Job.find_by(handler: <<~YML) }
--- !ruby/object:Observer::Ticket::Article::CommunicateTwitter::BackgroundJob
article_id: #{tweet.id}
YML
around do |example|
# This test case requires the use_vcr: :time_sensitive option
# to travel_to(when the VCR cassette was recorded).
#
# This ensures that #fetch doesn't ignore
# the "older" tweets stored in the VCR cassette,
# but it also freezes time,
# which breaks this race condition handling logic:
#
# break if Delayed::Job.where('created_at < ?', Time.current).none?
#
# So, we unfreeze time here.
travel_back
# Run BG job (Why not use Scheduler.worker?
# It led to hangs & failures elsewhere in test suite.)
Thread.new do
sleep 5 # simulate other bg jobs holding up the queue
twitter_job.invoke_job
end.tap { example.run }.join
end
it 'does not import the duplicate tweet (waits up to 60s for BG job to finish)' do
expect { channel.fetch }
.to not_change(Ticket::Article, :count)
end
end
end
end
end
end

View file

@ -0,0 +1,245 @@
---
http_interactions:
- request:
method: post
uri: https://api.twitter.com/1.1/statuses/update.json
body:
encoding: UTF-8
string: in_reply_to_status_id&status=zammadzammadzammad
headers:
User-Agent:
- TwitterRubyGem/6.2.0
Authorization:
- OAuth oauth_consumer_key="REDACTED", oauth_nonce="bd6dd2182d97c3db65b31354e57e4898",
oauth_signature="CB1L6ABVnKvvX6MYA%2FgAwEO2bJk%3D", oauth_signature_method="HMAC-SHA1",
oauth_timestamp="1583840885", oauth_token="REDACTED",
oauth_version="1.0"
Connection:
- close
Content-Type:
- application/x-www-form-urlencoded
Host:
- api.twitter.com
response:
status:
code: 200
message: OK
headers:
Cache-Control:
- no-cache, no-store, must-revalidate, pre-check=0, post-check=0
Connection:
- close
Content-Disposition:
- attachment; filename=json.json
Content-Length:
- '1875'
Content-Type:
- application/json;charset=utf-8
Date:
- Tue, 10 Mar 2020 11:48:05 GMT
Expires:
- Tue, 31 Mar 1981 05:00:00 GMT
Last-Modified:
- Tue, 10 Mar 2020 11:48:05 GMT
Pragma:
- no-cache
Server:
- tsa_m
Set-Cookie:
- guest_id=v1%3A158384088580519536; Max-Age=63072000; Expires=Thu, 10 Mar 2022
11:48:05 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
- lang=en; Path=/
- personalization_id="v1_C0O4/lMYKslqdCLoRaa92g=="; Max-Age=63072000; Expires=Thu,
10 Mar 2022 11:48:05 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
Status:
- 200 OK
Strict-Transport-Security:
- max-age=631138519
X-Access-Level:
- read-write-directmessages
X-Connection-Hash:
- 17fe1248d1e3052bdc237af4f58d515a
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Response-Time:
- '164'
X-Transaction:
- 00a2bc2500997791
X-Tsa-Request-Body-Time:
- '0'
X-Twitter-Response-Tags:
- BouncerCompliant
X-Xss-Protection:
- '0'
body:
encoding: UTF-8
string: '{"created_at":"Tue Mar 10 11:48:05 +0000 2020","id":1237344473199153152,"id_str":"1237344473199153152","text":"zammadzammadzammad","truncated":false,"entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[]},"source":"\u003ca
href=\"https:\/\/zammad.com\/\" rel=\"nofollow\"\u003ezammad\u003c\/a\u003e","in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":1205290247124217856,"id_str":"1205290247124217856","name":"pennbrooke","screen_name":"pennbrooke1","location":"","description":"","url":null,"entities":{"description":{"urls":[]}},"protected":false,"followers_count":0,"friends_count":1,"listed_count":0,"created_at":"Fri
Dec 13 00:56:10 +0000 2019","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":19,"lang":null,"contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"F5F8FA","profile_background_image_url":null,"profile_background_image_url_https":null,"profile_background_tile":false,"profile_image_url":"http:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_normal.png","profile_image_url_https":"https:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_normal.png","profile_link_color":"1DA1F2","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"has_extended_profile":false,"default_profile":true,"default_profile_image":true,"following":false,"follow_request_sent":false,"notifications":false,"translator_type":"none"},"geo":null,"coordinates":null,"place":null,"contributors":null,"is_quote_status":false,"retweet_count":0,"favorite_count":0,"favorited":false,"retweeted":false,"lang":"lv"}'
http_version:
recorded_at: Tue, 10 Mar 2020 11:48:06 GMT
- request:
method: get
uri: https://api.twitter.com/1.1/search/tweets.json?count=100&q=zammadzammadzammad&result_type=mixed
body:
encoding: UTF-8
string: ''
headers:
User-Agent:
- TwitterRubyGem/6.2.0
Authorization:
- OAuth oauth_consumer_key="REDACTED", oauth_nonce="37876a49fa0c474bd7732dea70083056",
oauth_signature="f159trPoyOipHcp%2BPlL33Kh2nO4%3D", oauth_signature_method="HMAC-SHA1",
oauth_timestamp="1583840887", oauth_token="REDACTED",
oauth_version="1.0"
Connection:
- close
Host:
- api.twitter.com
response:
status:
code: 200
message: OK
headers:
Cache-Control:
- no-cache, no-store, must-revalidate, pre-check=0, post-check=0
Connection:
- close
Content-Disposition:
- attachment; filename=json.json
Content-Length:
- '2346'
Content-Type:
- application/json;charset=utf-8
Date:
- Tue, 10 Mar 2020 11:48:07 GMT
Expires:
- Tue, 31 Mar 1981 05:00:00 GMT
Last-Modified:
- Tue, 10 Mar 2020 11:48:07 GMT
Pragma:
- no-cache
Server:
- tsa_m
Set-Cookie:
- guest_id=v1%3A158384088754038008; Max-Age=63072000; Expires=Thu, 10 Mar 2022
11:48:07 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
- lang=en; Path=/
- personalization_id="v1_N5oIKIuBSmkhNAVvuM7dWQ=="; Max-Age=63072000; Expires=Thu,
10 Mar 2022 11:48:07 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
Status:
- 200 OK
Strict-Transport-Security:
- max-age=631138519
X-Access-Level:
- read-write-directmessages
X-Connection-Hash:
- a615db05b45fdf48368de9e967c599c2
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Rate-Limit-Limit:
- '180'
X-Rate-Limit-Remaining:
- '177'
X-Rate-Limit-Reset:
- '1583841131'
X-Response-Time:
- '134'
X-Transaction:
- '004926c000957504'
X-Twitter-Response-Tags:
- BouncerCompliant
X-Xss-Protection:
- '0'
body:
encoding: UTF-8
string: '{"statuses":[{"created_at":"Tue Mar 10 11:48:05 +0000 2020","id":1237344473199153152,"id_str":"1237344473199153152","text":"zammadzammadzammad","truncated":false,"entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[]},"metadata":{"iso_language_code":"lv","result_type":"recent"},"source":"\u003ca
href=\"https:\/\/zammad.com\/\" rel=\"nofollow\"\u003ezammad\u003c\/a\u003e","in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":1205290247124217856,"id_str":"1205290247124217856","name":"pennbrooke","screen_name":"pennbrooke1","location":"","description":"","url":null,"entities":{"description":{"urls":[]}},"protected":false,"followers_count":0,"friends_count":1,"listed_count":0,"created_at":"Fri
Dec 13 00:56:10 +0000 2019","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":19,"lang":null,"contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"F5F8FA","profile_background_image_url":null,"profile_background_image_url_https":null,"profile_background_tile":false,"profile_image_url":"http:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_normal.png","profile_image_url_https":"https:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_normal.png","profile_link_color":"1DA1F2","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"has_extended_profile":false,"default_profile":true,"default_profile_image":true,"following":false,"follow_request_sent":false,"notifications":false,"translator_type":"none"},"geo":null,"coordinates":null,"place":null,"contributors":null,"is_quote_status":false,"retweet_count":0,"favorite_count":0,"favorited":false,"retweeted":false,"lang":"lv"}],"search_metadata":{"completed_in":0.017,"max_id":1237344473199153152,"max_id_str":"1237344473199153152","next_results":"?max_id=1237344473199153151&q=zammadzammadzammad&count=100&include_entities=1&result_type=mixed","query":"zammadzammadzammad","refresh_url":"?since_id=1237344473199153152&q=zammadzammadzammad&result_type=mixed&include_entities=1","count":100,"since_id":0,"since_id_str":"0"}}'
http_version:
recorded_at: Tue, 10 Mar 2020 11:48:07 GMT
- request:
method: get
uri: https://api.twitter.com/1.1/search/tweets.json?count=100&include_entities=1&max_id=1237344473199153151&q=zammadzammadzammad&result_type=mixed
body:
encoding: UTF-8
string: ''
headers:
User-Agent:
- TwitterRubyGem/6.2.0
Authorization:
- OAuth oauth_consumer_key="REDACTED", oauth_nonce="75815ceef5c89fbf52da222e32a10c88",
oauth_signature="BVaaa8THWe1R6DZEOr%2F8FAFm2v8%3D", oauth_signature_method="HMAC-SHA1",
oauth_timestamp="1583840902", oauth_token="REDACTED",
oauth_version="1.0"
Connection:
- close
Host:
- api.twitter.com
response:
status:
code: 200
message: OK
headers:
Cache-Control:
- no-cache, no-store, must-revalidate, pre-check=0, post-check=0
Connection:
- close
Content-Disposition:
- attachment; filename=json.json
Content-Length:
- '297'
Content-Type:
- application/json;charset=utf-8
Date:
- Tue, 10 Mar 2020 11:48:23 GMT
Expires:
- Tue, 31 Mar 1981 05:00:00 GMT
Last-Modified:
- Tue, 10 Mar 2020 11:48:23 GMT
Pragma:
- no-cache
Server:
- tsa_m
Set-Cookie:
- guest_id=v1%3A158384090303371642; Max-Age=63072000; Expires=Thu, 10 Mar 2022
11:48:23 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
- lang=en; Path=/
- personalization_id="v1_j9baN1VbzoK0Aak9kVoDHQ=="; Max-Age=63072000; Expires=Thu,
10 Mar 2022 11:48:23 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
Status:
- 200 OK
Strict-Transport-Security:
- max-age=631138519
X-Access-Level:
- read-write-directmessages
X-Connection-Hash:
- b2aecb9288e6367f9f95d89e76d342bc
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Rate-Limit-Limit:
- '180'
X-Rate-Limit-Remaining:
- '176'
X-Rate-Limit-Reset:
- '1583841131'
X-Response-Time:
- '123'
X-Transaction:
- '0087d22d00c9bd19'
X-Twitter-Response-Tags:
- BouncerCompliant
X-Xss-Protection:
- '0'
body:
encoding: UTF-8
string: '{"statuses":[],"search_metadata":{"completed_in":0.012,"max_id":1237344473199153151,"max_id_str":"1237344473199153151","query":"zammadzammadzammad","refresh_url":"?since_id=1237344473199153151&q=zammadzammadzammad&result_type=mixed&include_entities=1","count":100,"since_id":0,"since_id_str":"0"}}'
http_version:
recorded_at: Tue, 10 Mar 2020 11:48:23 GMT
recorded_with: VCR 4.0.0