diff --git a/spec/models/observer/ticket/article/communicate_twitter/background_job_spec.rb b/spec/models/observer/ticket/article/communicate_twitter/background_job_spec.rb
new file mode 100644
index 000000000..c8c3d20be
--- /dev/null
+++ b/spec/models/observer/ticket/article/communicate_twitter/background_job_spec.rb
@@ -0,0 +1,238 @@
+require 'rails_helper'
+
+RSpec.describe Observer::Ticket::Article::CommunicateTwitter::BackgroundJob, type: :job do
+ let(:article) { create(:twitter_article, **(try(:factory_options) || {})) }
+
+ describe 'core behavior', :use_vcr do
+ # This job runs automatically whenever an article is created.
+ # We disable this auto-execution so we can invoke it manually in the tests below.
+ around do |example|
+ ActiveRecord::Base.observers.disable('observer::_ticket::_article::_communicate_twitter')
+ example.run
+ ActiveRecord::Base.observers.enable('observer::_ticket::_article::_communicate_twitter')
+ end
+
+ context 'for tweets' do
+ let(:tweet_attributes) do
+ {
+ 'mention_ids' => [],
+ 'geo' => {},
+ 'retweeted' => false,
+ 'possibly_sensitive' => false,
+ 'in_reply_to_user_id' => nil,
+ 'place' => {},
+ 'retweet_count' => 0,
+ 'source' => 'zammad',
+ 'favorited' => false,
+ 'truncated' => false,
+ }
+ end
+
+ let(:links_array) do
+ [{
+ url: 'https://twitter.com/_/status/1244937367435108360',
+ target: '_blank',
+ name: 'on Twitter',
+ }]
+ end
+
+ it 'increments the "delivery_retry" preference' do
+ expect { described_class.new(article.id).perform }
+ .to change { article.reload.preferences[:delivery_retry] }.to(1)
+ end
+
+ it 'dispatches the tweet' do
+ described_class.new(article.id).perform
+
+ expect(WebMock)
+ .to have_requested(:post, 'https://api.twitter.com/1.1/statuses/update.json')
+ .with(body: "in_reply_to_status_id&status=#{CGI.escape(article.body)}" )
+ end
+
+ it 'updates the article with tweet attributes' do
+ expect { described_class.new(article.id).perform }
+ .to change { article.reload.message_id }.to('1244937367435108360')
+ .and change { article.reload.preferences[:twitter] }.to(hash_including(tweet_attributes))
+ .and change { article.reload.preferences[:links] }.to(links_array)
+ end
+
+ it 'sets the appropriate delivery status attributes' do
+ expect { described_class.new(article.id).perform }
+ .to change { article.reload.preferences[:delivery_status] }.to('success')
+ .and change { article.reload.preferences[:delivery_status_date] }.to(an_instance_of(ActiveSupport::TimeWithZone))
+ .and not_change { article.reload.preferences[:delivery_status_message] }.from(nil)
+ end
+
+ context 'with a user mention' do
+ let(:factory_options) { { body: '@twitter @twitterlive Don’t mind me, just testing the API' } }
+
+ it 'updates the article with tweet recipients' do
+ expect { described_class.new(article.id).perform }
+ .to change { article.reload.to }.to('@Twitter @TwitterLive')
+ end
+ end
+ end
+
+ context 'for DMs' do
+ let(:article) { create(:twitter_dm_article, :pending_delivery, recipient: recipient, body: 'Please ignore this message.') }
+ let(:recipient) { create(:twitter_authorization, uid: '2688148651', username: 'AirbnbHelp') }
+
+ let(:request_body) do
+ {
+ event: {
+ type: 'message_create',
+ message_create: {
+ target: { recipient_id: recipient.uid },
+ message_data: { text: article.body }
+ }
+ }
+ }.to_json
+ end
+
+ let(:dm_attributes) do
+ {
+ 'recipient_id' => recipient.uid,
+ 'sender_id' => '1205290247124217856',
+ }
+ end
+
+ let(:links_array) do
+ [{
+ url: "https://twitter.com/messages/#{recipient.uid}-1205290247124217856",
+ target: '_blank',
+ name: 'on Twitter',
+ }]
+ end
+
+ it 'increments the "delivery_retry" preference' do
+ expect { described_class.new(article.id).perform }
+ .to change { article.reload.preferences[:delivery_retry] }.to(1)
+ end
+
+ it 'dispatches the DM' do
+ described_class.new(article.id).perform
+
+ expect(WebMock)
+ .to have_requested(:post, 'https://api.twitter.com/1.1/direct_messages/events/new.json')
+ .with(body: request_body)
+ end
+
+ it 'updates the article with DM attributes' do
+ expect { described_class.new(article.id).perform }
+ .to change { article.reload.message_id }.to('1244953398509617156')
+ .and change { article.reload.preferences[:twitter] }.to(hash_including(dm_attributes))
+ .and change { article.reload.preferences[:links] }.to(links_array)
+ end
+
+ it 'sets the appropriate delivery status attributes' do
+ expect { described_class.new(article.id).perform }
+ .to change { article.reload.preferences[:delivery_status] }.to('success')
+ .and change { article.reload.preferences[:delivery_status_date] }.to(an_instance_of(ActiveSupport::TimeWithZone))
+ .and not_change { article.reload.preferences[:delivery_status_message] }.from(nil)
+ end
+ end
+
+ describe 'failure cases' do
+ shared_examples 'for failure cases' do
+ it 'raises an error and sets the appropriate delivery status messages' do
+ expect { described_class.new(article.id).perform }
+ .to raise_error(error_message)
+ .and change { article.reload.preferences[:delivery_status] }.to('fail')
+ .and change { article.reload.preferences[:delivery_status_date] }.to(an_instance_of(ActiveSupport::TimeWithZone))
+ .and change { article.reload.preferences[:delivery_status_message] }.to(error_message)
+ end
+ end
+
+ context 'when article.ticket.preferences["channel_id"] is nil' do
+ before do
+ article.ticket.preferences.delete(:channel_id)
+ article.ticket.save
+ end
+
+ let(:error_message) { "Can't find ticket.preferences['channel_id'] for Ticket.find(#{article.ticket_id})" }
+
+ include_examples 'for failure cases'
+ end
+
+ context 'if article.ticket.preferences["channel_id"] has been removed' do
+ before { channel.destroy }
+
+ let(:channel) { Channel.find(article.ticket.preferences[:channel_id]) }
+ let(:error_message) { "No such channel id #{article.ticket.preferences['channel_id']}" }
+
+ include_examples 'for failure cases'
+
+ context 'and another suitable channel exists (matching on ticket.preferences[:channel_screen_name])' do
+ let!(:new_channel) { create(:twitter_channel, custom_options: { user: { screen_name: channel.options[:user][:screen_name] } }) }
+
+ it 'uses that channel' do
+ described_class.new(article.id).perform
+
+ expect(WebMock)
+ .to have_requested(:post, 'https://api.twitter.com/1.1/statuses/update.json')
+ .with(body: "in_reply_to_status_id&status=#{CGI.escape(article.body)}" )
+ end
+ end
+ end
+
+ context 'if article.ticket.preferences["channel_id"] isn’t actually a twitter channel' do
+ before do
+ article.ticket.preferences[:channel_id] = create(:email_channel).id
+ article.ticket.save
+ end
+
+ let(:error_message) { "Channel.find(#{article.ticket.preferences[:channel_id]}) isn't a twitter channel!" }
+
+ include_examples 'for failure cases'
+ end
+
+ context 'when tweet dispatch fails (e.g., due to authentication error)' do
+ before do
+ article.ticket.preferences[:channel_id] = create(:twitter_channel, :invalid).id
+ article.ticket.save
+ end
+
+ let(:error_message) { "Can't use Channel::Driver::Twitter: #" }
+
+ include_examples 'for failure cases'
+ end
+
+ context 'when tweet comes back nil' do
+ before do
+ allow(Twitter::REST::Client).to receive(:new).with(any_args).and_return(client_double)
+ allow(client_double).to receive(:update).with(any_args).and_return(nil)
+ end
+
+ let(:client_double) { double('Twitter::REST::Client') }
+ let(:error_message) { 'Got no tweet!' }
+
+ include_examples 'for failure cases'
+ end
+
+ context 'on the fourth time it fails' do
+ before { Channel.find(article.ticket.preferences[:channel_id]).destroy }
+
+ let(:error_message) { "No such channel id #{article.ticket.preferences['channel_id']}" }
+ let(:factory_options) { { preferences: { delivery_retry: 3 } } }
+
+ it 'adds a delivery failure note (article) to the ticket' do
+ expect { described_class.new(article.id).perform }
+ .to raise_error(error_message)
+ .and change { article.ticket.reload.articles.count }.by(1)
+
+ expect(Ticket::Article.last.attributes).to include(
+ 'content_type' => 'text/plain',
+ 'body' => "Unable to send tweet: #{error_message}",
+ 'internal' => true,
+ 'sender_id' => Ticket::Article::Sender.find_by(name: 'System').id,
+ 'type_id' => Ticket::Article::Type.find_by(name: 'note').id,
+ 'preferences' => {
+ 'delivery_article_id_related' => article.id,
+ 'delivery_message' => true,
+ },
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/and_another_suitable_channel_exists_matching_on_ticket_preferences_channel_screen_name_uses_that_channel.yml b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/and_another_suitable_channel_exists_matching_on_ticket_preferences_channel_screen_name_uses_that_channel.yml
new file mode 100644
index 000000000..4ba503d22
--- /dev/null
+++ b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/and_another_suitable_channel_exists_matching_on_ticket_preferences_channel_screen_name_uses_that_channel.yml
@@ -0,0 +1,84 @@
+---
+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=Consequatur+deserunt+sapiente+rerum.
+ headers:
+ User-Agent:
+ - TwitterRubyGem/6.2.0
+ Authorization:
+ - OAuth oauth_consumer_key="REDACTED", oauth_nonce="c5c00913ad671a7bc4dd721564fa1ddb",
+ oauth_signature="RAol4gCi%2FtCzZDvdXB%2FMzAsVIBY%3D", oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="1585658007", 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:
+ - '1893'
+ Content-Type:
+ - application/json;charset=utf-8
+ Date:
+ - Tue, 31 Mar 2020 12:33:27 GMT
+ Expires:
+ - Tue, 31 Mar 1981 05:00:00 GMT
+ Last-Modified:
+ - Tue, 31 Mar 2020 12:33:27 GMT
+ Pragma:
+ - no-cache
+ Server:
+ - tsa_m
+ Set-Cookie:
+ - guest_id=v1%3A158565800758345505; Max-Age=63072000; Expires=Thu, 31 Mar 2022
+ 12:33:27 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
+ - lang=en; Path=/
+ - personalization_id="v1_JWzIkRlgfSAhdmWPMCfNQw=="; Max-Age=63072000; Expires=Thu,
+ 31 Mar 2022 12:33:27 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:
+ - 7b19dfd8d697ce9c159a74d199c390d6
+ X-Content-Type-Options:
+ - nosniff
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Response-Time:
+ - '153'
+ X-Transaction:
+ - '009b8a42003fa36c'
+ X-Tsa-Request-Body-Time:
+ - '0'
+ X-Twitter-Response-Tags:
+ - BouncerCompliant
+ X-Xss-Protection:
+ - '0'
+ body:
+ encoding: UTF-8
+ string: '{"created_at":"Tue Mar 31 12:33:27 +0000 2020","id":1244966034336968704,"id_str":"1244966034336968704","text":"Consequatur
+ deserunt sapiente rerum.","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":25,"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":"de"}'
+ http_version:
+ recorded_at: Tue, 31 Mar 2020 12:33:27 GMT
+recorded_with: VCR 4.0.0
diff --git a/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_dms_dispatches_the_dm.yml b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_dms_dispatches_the_dm.yml
new file mode 100644
index 000000000..84248173d
--- /dev/null
+++ b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_dms_dispatches_the_dm.yml
@@ -0,0 +1,83 @@
+---
+http_interactions:
+- request:
+ method: post
+ uri: https://api.twitter.com/1.1/direct_messages/events/new.json
+ body:
+ encoding: UTF-8
+ string: '{"event":{"type":"message_create","message_create":{"target":{"recipient_id":"2688148651"},"message_data":{"text":"Please
+ ignore this message."}}}}'
+ headers:
+ User-Agent:
+ - TwitterRubyGem/6.2.0
+ Authorization:
+ - OAuth oauth_consumer_key="REDACTED", oauth_nonce="89f9fb3a4a61083d5a43108631b561fb",
+ oauth_signature="9Kmi04TwHkzb1Qg1kc4UcFaAuzc%3D", oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="1585654994", oauth_token="REDACTED",
+ oauth_version="1.0"
+ Connection:
+ - close
+ Content-Type:
+ - application/json; charset=UTF-8
+ 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:
+ - '313'
+ Content-Type:
+ - application/json;charset=utf-8
+ Date:
+ - Tue, 31 Mar 2020 11:43:15 GMT
+ Expires:
+ - Tue, 31 Mar 1981 05:00:00 GMT
+ Last-Modified:
+ - Tue, 31 Mar 2020 11:43:15 GMT
+ Pragma:
+ - no-cache
+ Server:
+ - tsa_m
+ Set-Cookie:
+ - guest_id=v1%3A158565499496785911; Max-Age=63072000; Expires=Thu, 31 Mar 2022
+ 11:43:15 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
+ - lang=en; Path=/
+ - personalization_id="v1_ixvQSJNpQwDdhsQnnFjMbg=="; Max-Age=63072000; Expires=Thu,
+ 31 Mar 2022 11:43:15 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:
+ - 89771d0fa3e3da3329775711776b1a24
+ X-Content-Type-Options:
+ - nosniff
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Response-Time:
+ - '215'
+ X-Transaction:
+ - 00e3834000e91bc3
+ X-Tsa-Request-Body-Time:
+ - '0'
+ X-Twitter-Response-Tags:
+ - BouncerCompliant
+ X-Xss-Protection:
+ - '0'
+ body:
+ encoding: UTF-8
+ string: '{"event":{"type":"message_create","id":"1244953398509617156","created_timestamp":"1585654994977","message_create":{"target":{"recipient_id":"2688148651"},"sender_id":"1205290247124217856","message_data":{"text":"Please
+ ignore this message.","entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[]}}}}}'
+ http_version:
+ recorded_at: Tue, 31 Mar 2020 11:43:15 GMT
+recorded_with: VCR 4.0.0
diff --git a/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_dms_increments_the_delivery_retry_preference.yml b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_dms_increments_the_delivery_retry_preference.yml
new file mode 100644
index 000000000..84248173d
--- /dev/null
+++ b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_dms_increments_the_delivery_retry_preference.yml
@@ -0,0 +1,83 @@
+---
+http_interactions:
+- request:
+ method: post
+ uri: https://api.twitter.com/1.1/direct_messages/events/new.json
+ body:
+ encoding: UTF-8
+ string: '{"event":{"type":"message_create","message_create":{"target":{"recipient_id":"2688148651"},"message_data":{"text":"Please
+ ignore this message."}}}}'
+ headers:
+ User-Agent:
+ - TwitterRubyGem/6.2.0
+ Authorization:
+ - OAuth oauth_consumer_key="REDACTED", oauth_nonce="89f9fb3a4a61083d5a43108631b561fb",
+ oauth_signature="9Kmi04TwHkzb1Qg1kc4UcFaAuzc%3D", oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="1585654994", oauth_token="REDACTED",
+ oauth_version="1.0"
+ Connection:
+ - close
+ Content-Type:
+ - application/json; charset=UTF-8
+ 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:
+ - '313'
+ Content-Type:
+ - application/json;charset=utf-8
+ Date:
+ - Tue, 31 Mar 2020 11:43:15 GMT
+ Expires:
+ - Tue, 31 Mar 1981 05:00:00 GMT
+ Last-Modified:
+ - Tue, 31 Mar 2020 11:43:15 GMT
+ Pragma:
+ - no-cache
+ Server:
+ - tsa_m
+ Set-Cookie:
+ - guest_id=v1%3A158565499496785911; Max-Age=63072000; Expires=Thu, 31 Mar 2022
+ 11:43:15 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
+ - lang=en; Path=/
+ - personalization_id="v1_ixvQSJNpQwDdhsQnnFjMbg=="; Max-Age=63072000; Expires=Thu,
+ 31 Mar 2022 11:43:15 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:
+ - 89771d0fa3e3da3329775711776b1a24
+ X-Content-Type-Options:
+ - nosniff
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Response-Time:
+ - '215'
+ X-Transaction:
+ - 00e3834000e91bc3
+ X-Tsa-Request-Body-Time:
+ - '0'
+ X-Twitter-Response-Tags:
+ - BouncerCompliant
+ X-Xss-Protection:
+ - '0'
+ body:
+ encoding: UTF-8
+ string: '{"event":{"type":"message_create","id":"1244953398509617156","created_timestamp":"1585654994977","message_create":{"target":{"recipient_id":"2688148651"},"sender_id":"1205290247124217856","message_data":{"text":"Please
+ ignore this message.","entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[]}}}}}'
+ http_version:
+ recorded_at: Tue, 31 Mar 2020 11:43:15 GMT
+recorded_with: VCR 4.0.0
diff --git a/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_dms_sets_the_appropriate_delivery_status_attributes.yml b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_dms_sets_the_appropriate_delivery_status_attributes.yml
new file mode 100644
index 000000000..84248173d
--- /dev/null
+++ b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_dms_sets_the_appropriate_delivery_status_attributes.yml
@@ -0,0 +1,83 @@
+---
+http_interactions:
+- request:
+ method: post
+ uri: https://api.twitter.com/1.1/direct_messages/events/new.json
+ body:
+ encoding: UTF-8
+ string: '{"event":{"type":"message_create","message_create":{"target":{"recipient_id":"2688148651"},"message_data":{"text":"Please
+ ignore this message."}}}}'
+ headers:
+ User-Agent:
+ - TwitterRubyGem/6.2.0
+ Authorization:
+ - OAuth oauth_consumer_key="REDACTED", oauth_nonce="89f9fb3a4a61083d5a43108631b561fb",
+ oauth_signature="9Kmi04TwHkzb1Qg1kc4UcFaAuzc%3D", oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="1585654994", oauth_token="REDACTED",
+ oauth_version="1.0"
+ Connection:
+ - close
+ Content-Type:
+ - application/json; charset=UTF-8
+ 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:
+ - '313'
+ Content-Type:
+ - application/json;charset=utf-8
+ Date:
+ - Tue, 31 Mar 2020 11:43:15 GMT
+ Expires:
+ - Tue, 31 Mar 1981 05:00:00 GMT
+ Last-Modified:
+ - Tue, 31 Mar 2020 11:43:15 GMT
+ Pragma:
+ - no-cache
+ Server:
+ - tsa_m
+ Set-Cookie:
+ - guest_id=v1%3A158565499496785911; Max-Age=63072000; Expires=Thu, 31 Mar 2022
+ 11:43:15 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
+ - lang=en; Path=/
+ - personalization_id="v1_ixvQSJNpQwDdhsQnnFjMbg=="; Max-Age=63072000; Expires=Thu,
+ 31 Mar 2022 11:43:15 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:
+ - 89771d0fa3e3da3329775711776b1a24
+ X-Content-Type-Options:
+ - nosniff
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Response-Time:
+ - '215'
+ X-Transaction:
+ - 00e3834000e91bc3
+ X-Tsa-Request-Body-Time:
+ - '0'
+ X-Twitter-Response-Tags:
+ - BouncerCompliant
+ X-Xss-Protection:
+ - '0'
+ body:
+ encoding: UTF-8
+ string: '{"event":{"type":"message_create","id":"1244953398509617156","created_timestamp":"1585654994977","message_create":{"target":{"recipient_id":"2688148651"},"sender_id":"1205290247124217856","message_data":{"text":"Please
+ ignore this message.","entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[]}}}}}'
+ http_version:
+ recorded_at: Tue, 31 Mar 2020 11:43:15 GMT
+recorded_with: VCR 4.0.0
diff --git a/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_dms_updates_the_article_with_dm_attributes.yml b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_dms_updates_the_article_with_dm_attributes.yml
new file mode 100644
index 000000000..84248173d
--- /dev/null
+++ b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_dms_updates_the_article_with_dm_attributes.yml
@@ -0,0 +1,83 @@
+---
+http_interactions:
+- request:
+ method: post
+ uri: https://api.twitter.com/1.1/direct_messages/events/new.json
+ body:
+ encoding: UTF-8
+ string: '{"event":{"type":"message_create","message_create":{"target":{"recipient_id":"2688148651"},"message_data":{"text":"Please
+ ignore this message."}}}}'
+ headers:
+ User-Agent:
+ - TwitterRubyGem/6.2.0
+ Authorization:
+ - OAuth oauth_consumer_key="REDACTED", oauth_nonce="89f9fb3a4a61083d5a43108631b561fb",
+ oauth_signature="9Kmi04TwHkzb1Qg1kc4UcFaAuzc%3D", oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="1585654994", oauth_token="REDACTED",
+ oauth_version="1.0"
+ Connection:
+ - close
+ Content-Type:
+ - application/json; charset=UTF-8
+ 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:
+ - '313'
+ Content-Type:
+ - application/json;charset=utf-8
+ Date:
+ - Tue, 31 Mar 2020 11:43:15 GMT
+ Expires:
+ - Tue, 31 Mar 1981 05:00:00 GMT
+ Last-Modified:
+ - Tue, 31 Mar 2020 11:43:15 GMT
+ Pragma:
+ - no-cache
+ Server:
+ - tsa_m
+ Set-Cookie:
+ - guest_id=v1%3A158565499496785911; Max-Age=63072000; Expires=Thu, 31 Mar 2022
+ 11:43:15 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
+ - lang=en; Path=/
+ - personalization_id="v1_ixvQSJNpQwDdhsQnnFjMbg=="; Max-Age=63072000; Expires=Thu,
+ 31 Mar 2022 11:43:15 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:
+ - 89771d0fa3e3da3329775711776b1a24
+ X-Content-Type-Options:
+ - nosniff
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Response-Time:
+ - '215'
+ X-Transaction:
+ - 00e3834000e91bc3
+ X-Tsa-Request-Body-Time:
+ - '0'
+ X-Twitter-Response-Tags:
+ - BouncerCompliant
+ X-Xss-Protection:
+ - '0'
+ body:
+ encoding: UTF-8
+ string: '{"event":{"type":"message_create","id":"1244953398509617156","created_timestamp":"1585654994977","message_create":{"target":{"recipient_id":"2688148651"},"sender_id":"1205290247124217856","message_data":{"text":"Please
+ ignore this message.","entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[]}}}}}'
+ http_version:
+ recorded_at: Tue, 31 Mar 2020 11:43:15 GMT
+recorded_with: VCR 4.0.0
diff --git a/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_dispatches_the_tweet.yml b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_dispatches_the_tweet.yml
new file mode 100644
index 000000000..70d453ae2
--- /dev/null
+++ b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_dispatches_the_tweet.yml
@@ -0,0 +1,84 @@
+---
+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=Quos+nulla+asperiores+ut.
+ headers:
+ User-Agent:
+ - TwitterRubyGem/6.2.0
+ Authorization:
+ - OAuth oauth_consumer_key="REDACTED", oauth_nonce="00732df7fa44269cd335acf38d6abe42",
+ oauth_signature="Qna858PdKKMrFjGZEYdm8oU8g6k%3D", oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="1585651172", 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:
+ - '1882'
+ Content-Type:
+ - application/json;charset=utf-8
+ Date:
+ - Tue, 31 Mar 2020 10:39:32 GMT
+ Expires:
+ - Tue, 31 Mar 1981 05:00:00 GMT
+ Last-Modified:
+ - Tue, 31 Mar 2020 10:39:32 GMT
+ Pragma:
+ - no-cache
+ Server:
+ - tsa_m
+ Set-Cookie:
+ - guest_id=v1%3A158565117286241661; Max-Age=63072000; Expires=Thu, 31 Mar 2022
+ 10:39:32 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
+ - lang=en; Path=/
+ - personalization_id="v1_VZPA3OsrdQH41ZancPvXAg=="; Max-Age=63072000; Expires=Thu,
+ 31 Mar 2022 10:39:32 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:
+ - 2d2380f4b7cda58563ac424cc1897507
+ X-Content-Type-Options:
+ - nosniff
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Response-Time:
+ - '175'
+ X-Transaction:
+ - 0024cb7500e82636
+ X-Tsa-Request-Body-Time:
+ - '0'
+ X-Twitter-Response-Tags:
+ - BouncerCompliant
+ X-Xss-Protection:
+ - '0'
+ body:
+ encoding: UTF-8
+ string: '{"created_at":"Tue Mar 31 10:39:32 +0000 2020","id":1244937367435108360,"id_str":"1244937367435108360","text":"Quos
+ nulla asperiores ut.","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":21,"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":"ca"}'
+ http_version:
+ recorded_at: Tue, 31 Mar 2020 10:39:33 GMT
+recorded_with: VCR 4.0.0
diff --git a/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_increments_the_delivery_retry_preference.yml b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_increments_the_delivery_retry_preference.yml
new file mode 100644
index 000000000..70d453ae2
--- /dev/null
+++ b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_increments_the_delivery_retry_preference.yml
@@ -0,0 +1,84 @@
+---
+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=Quos+nulla+asperiores+ut.
+ headers:
+ User-Agent:
+ - TwitterRubyGem/6.2.0
+ Authorization:
+ - OAuth oauth_consumer_key="REDACTED", oauth_nonce="00732df7fa44269cd335acf38d6abe42",
+ oauth_signature="Qna858PdKKMrFjGZEYdm8oU8g6k%3D", oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="1585651172", 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:
+ - '1882'
+ Content-Type:
+ - application/json;charset=utf-8
+ Date:
+ - Tue, 31 Mar 2020 10:39:32 GMT
+ Expires:
+ - Tue, 31 Mar 1981 05:00:00 GMT
+ Last-Modified:
+ - Tue, 31 Mar 2020 10:39:32 GMT
+ Pragma:
+ - no-cache
+ Server:
+ - tsa_m
+ Set-Cookie:
+ - guest_id=v1%3A158565117286241661; Max-Age=63072000; Expires=Thu, 31 Mar 2022
+ 10:39:32 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
+ - lang=en; Path=/
+ - personalization_id="v1_VZPA3OsrdQH41ZancPvXAg=="; Max-Age=63072000; Expires=Thu,
+ 31 Mar 2022 10:39:32 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:
+ - 2d2380f4b7cda58563ac424cc1897507
+ X-Content-Type-Options:
+ - nosniff
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Response-Time:
+ - '175'
+ X-Transaction:
+ - 0024cb7500e82636
+ X-Tsa-Request-Body-Time:
+ - '0'
+ X-Twitter-Response-Tags:
+ - BouncerCompliant
+ X-Xss-Protection:
+ - '0'
+ body:
+ encoding: UTF-8
+ string: '{"created_at":"Tue Mar 31 10:39:32 +0000 2020","id":1244937367435108360,"id_str":"1244937367435108360","text":"Quos
+ nulla asperiores ut.","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":21,"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":"ca"}'
+ http_version:
+ recorded_at: Tue, 31 Mar 2020 10:39:33 GMT
+recorded_with: VCR 4.0.0
diff --git a/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_sets_the_appropriate_delivery_status_attributes.yml b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_sets_the_appropriate_delivery_status_attributes.yml
new file mode 100644
index 000000000..f7965cbb2
--- /dev/null
+++ b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_sets_the_appropriate_delivery_status_attributes.yml
@@ -0,0 +1,84 @@
+---
+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=Nobis+consequatur+et+deleniti.
+ headers:
+ User-Agent:
+ - TwitterRubyGem/6.2.0
+ Authorization:
+ - OAuth oauth_consumer_key="REDACTED", oauth_nonce="ca8e21d95937e7af2b7e1e758342bd3d",
+ oauth_signature="gGxlgbtogX4cd4VS5Dlso8XPNpg%3D", oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="1585653843", 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:
+ - '1887'
+ Content-Type:
+ - application/json;charset=utf-8
+ Date:
+ - Tue, 31 Mar 2020 11:24:04 GMT
+ Expires:
+ - Tue, 31 Mar 1981 05:00:00 GMT
+ Last-Modified:
+ - Tue, 31 Mar 2020 11:24:04 GMT
+ Pragma:
+ - no-cache
+ Server:
+ - tsa_m
+ Set-Cookie:
+ - guest_id=v1%3A158565384450024079; Max-Age=63072000; Expires=Thu, 31 Mar 2022
+ 11:24:04 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
+ - lang=en; Path=/
+ - personalization_id="v1_aMVHuwyntSCutxT7HYN0tg=="; Max-Age=63072000; Expires=Thu,
+ 31 Mar 2022 11:24:04 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:
+ - 457eca514c649e33a27f1274228e3968
+ X-Content-Type-Options:
+ - nosniff
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Response-Time:
+ - '151'
+ X-Transaction:
+ - '00843cce00439a0a'
+ X-Tsa-Request-Body-Time:
+ - '0'
+ X-Twitter-Response-Tags:
+ - BouncerCompliant
+ X-Xss-Protection:
+ - '0'
+ body:
+ encoding: UTF-8
+ string: '{"created_at":"Tue Mar 31 11:24:04 +0000 2020","id":1244948573092896770,"id_str":"1244948573092896770","text":"Nobis
+ consequatur et deleniti.","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":23,"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":"ca"}'
+ http_version:
+ recorded_at: Tue, 31 Mar 2020 11:24:04 GMT
+recorded_with: VCR 4.0.0
diff --git a/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_sets_the_appropriate_delivery_status_message.yml b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_sets_the_appropriate_delivery_status_message.yml
new file mode 100644
index 000000000..70d453ae2
--- /dev/null
+++ b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_sets_the_appropriate_delivery_status_message.yml
@@ -0,0 +1,84 @@
+---
+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=Quos+nulla+asperiores+ut.
+ headers:
+ User-Agent:
+ - TwitterRubyGem/6.2.0
+ Authorization:
+ - OAuth oauth_consumer_key="REDACTED", oauth_nonce="00732df7fa44269cd335acf38d6abe42",
+ oauth_signature="Qna858PdKKMrFjGZEYdm8oU8g6k%3D", oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="1585651172", 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:
+ - '1882'
+ Content-Type:
+ - application/json;charset=utf-8
+ Date:
+ - Tue, 31 Mar 2020 10:39:32 GMT
+ Expires:
+ - Tue, 31 Mar 1981 05:00:00 GMT
+ Last-Modified:
+ - Tue, 31 Mar 2020 10:39:32 GMT
+ Pragma:
+ - no-cache
+ Server:
+ - tsa_m
+ Set-Cookie:
+ - guest_id=v1%3A158565117286241661; Max-Age=63072000; Expires=Thu, 31 Mar 2022
+ 10:39:32 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
+ - lang=en; Path=/
+ - personalization_id="v1_VZPA3OsrdQH41ZancPvXAg=="; Max-Age=63072000; Expires=Thu,
+ 31 Mar 2022 10:39:32 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:
+ - 2d2380f4b7cda58563ac424cc1897507
+ X-Content-Type-Options:
+ - nosniff
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Response-Time:
+ - '175'
+ X-Transaction:
+ - 0024cb7500e82636
+ X-Tsa-Request-Body-Time:
+ - '0'
+ X-Twitter-Response-Tags:
+ - BouncerCompliant
+ X-Xss-Protection:
+ - '0'
+ body:
+ encoding: UTF-8
+ string: '{"created_at":"Tue Mar 31 10:39:32 +0000 2020","id":1244937367435108360,"id_str":"1244937367435108360","text":"Quos
+ nulla asperiores ut.","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":21,"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":"ca"}'
+ http_version:
+ recorded_at: Tue, 31 Mar 2020 10:39:33 GMT
+recorded_with: VCR 4.0.0
diff --git a/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_updates_the_article_with_tweet_attributes.yml b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_updates_the_article_with_tweet_attributes.yml
new file mode 100644
index 000000000..70d453ae2
--- /dev/null
+++ b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/for_tweets_updates_the_article_with_tweet_attributes.yml
@@ -0,0 +1,84 @@
+---
+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=Quos+nulla+asperiores+ut.
+ headers:
+ User-Agent:
+ - TwitterRubyGem/6.2.0
+ Authorization:
+ - OAuth oauth_consumer_key="REDACTED", oauth_nonce="00732df7fa44269cd335acf38d6abe42",
+ oauth_signature="Qna858PdKKMrFjGZEYdm8oU8g6k%3D", oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="1585651172", 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:
+ - '1882'
+ Content-Type:
+ - application/json;charset=utf-8
+ Date:
+ - Tue, 31 Mar 2020 10:39:32 GMT
+ Expires:
+ - Tue, 31 Mar 1981 05:00:00 GMT
+ Last-Modified:
+ - Tue, 31 Mar 2020 10:39:32 GMT
+ Pragma:
+ - no-cache
+ Server:
+ - tsa_m
+ Set-Cookie:
+ - guest_id=v1%3A158565117286241661; Max-Age=63072000; Expires=Thu, 31 Mar 2022
+ 10:39:32 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
+ - lang=en; Path=/
+ - personalization_id="v1_VZPA3OsrdQH41ZancPvXAg=="; Max-Age=63072000; Expires=Thu,
+ 31 Mar 2022 10:39:32 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:
+ - 2d2380f4b7cda58563ac424cc1897507
+ X-Content-Type-Options:
+ - nosniff
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Response-Time:
+ - '175'
+ X-Transaction:
+ - 0024cb7500e82636
+ X-Tsa-Request-Body-Time:
+ - '0'
+ X-Twitter-Response-Tags:
+ - BouncerCompliant
+ X-Xss-Protection:
+ - '0'
+ body:
+ encoding: UTF-8
+ string: '{"created_at":"Tue Mar 31 10:39:32 +0000 2020","id":1244937367435108360,"id_str":"1244937367435108360","text":"Quos
+ nulla asperiores ut.","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":21,"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":"ca"}'
+ http_version:
+ recorded_at: Tue, 31 Mar 2020 10:39:33 GMT
+recorded_with: VCR 4.0.0
diff --git a/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/when_tweet_dispatch_fails_e_g__due_to_authentication_error_raises_an_error_and_sets_the_appropriate_delivery_status_messages.yml b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/when_tweet_dispatch_fails_e_g__due_to_authentication_error_raises_an_error_and_sets_the_appropriate_delivery_status_messages.yml
new file mode 100644
index 000000000..2d146b9a6
--- /dev/null
+++ b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/when_tweet_dispatch_fails_e_g__due_to_authentication_error_raises_an_error_and_sets_the_appropriate_delivery_status_messages.yml
@@ -0,0 +1,56 @@
+---
+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=Officia+qui+quo+laudantium.
+ headers:
+ User-Agent:
+ - TwitterRubyGem/6.2.0
+ Authorization:
+ - OAuth oauth_consumer_key="q7K8GEkhyCHs9jHLtkmD9Kod4", oauth_nonce="03c07d843276ea5be9e32934e277cd07",
+ oauth_signature="etY6LuIgozpV2JPPFCnnSRykVf4%3D", oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="1585657668", oauth_token="7783712304-H9s75r2d532diPmJYK6JrvUWxu9gTDZ6ocjfToL",
+ oauth_version="1.0"
+ Connection:
+ - close
+ Content-Type:
+ - application/x-www-form-urlencoded
+ Host:
+ - api.twitter.com
+ response:
+ status:
+ code: 401
+ message: Unauthorized
+ headers:
+ Connection:
+ - close
+ Content-Length:
+ - '62'
+ Content-Type:
+ - application/json; charset=utf-8
+ Date:
+ - Tue, 31 Mar 2020 12:27:49 GMT
+ Server:
+ - tsa_m
+ Set-Cookie:
+ - guest_id=v1%3A158565766914726723; Max-Age=63072000; Expires=Thu, 31 Mar 2022
+ 12:27:49 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
+ - personalization_id="v1_Pk8vk94r2BPsy5pPrAyZiw=="; Max-Age=63072000; Expires=Thu,
+ 31 Mar 2022 12:27:49 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
+ Strict-Transport-Security:
+ - max-age=631138519
+ X-Connection-Hash:
+ - 43a5b1d7e5d707d650f1cf02cdef086c
+ X-Response-Time:
+ - '107'
+ X-Tsa-Request-Body-Time:
+ - '0'
+ body:
+ encoding: UTF-8
+ string: '{"errors":[{"code":89,"message":"Invalid or expired token."}]}'
+ http_version:
+ recorded_at: Tue, 31 Mar 2020 12:27:49 GMT
+recorded_with: VCR 4.0.0
diff --git a/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/with_a_user_mention_updates_the_article_with_tweet_recipients.yml b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/with_a_user_mention_updates_the_article_with_tweet_recipients.yml
new file mode 100644
index 000000000..bbf785a9a
--- /dev/null
+++ b/test/data/vcr_cassettes/models/observer/ticket/article/communicate_twitter/background_job/with_a_user_mention_updates_the_article_with_tweet_recipients.yml
@@ -0,0 +1,85 @@
+---
+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=%40twitter+%40twitterlive+Don%E2%80%99t+mind+me%2C+just+testing+the+API
+ headers:
+ User-Agent:
+ - TwitterRubyGem/6.2.0
+ Authorization:
+ - OAuth oauth_consumer_key="REDACTED", oauth_nonce="c2b2e0a690db19f551b76a53addd61dc",
+ oauth_signature="%2BldEo0cWQxcCIfGQcVATbUTcKiY%3D", oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="1585652887", 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:
+ - '2123'
+ Content-Type:
+ - application/json;charset=utf-8
+ Date:
+ - Tue, 31 Mar 2020 11:08:08 GMT
+ Expires:
+ - Tue, 31 Mar 1981 05:00:00 GMT
+ Last-Modified:
+ - Tue, 31 Mar 2020 11:08:08 GMT
+ Pragma:
+ - no-cache
+ Server:
+ - tsa_m
+ Set-Cookie:
+ - guest_id=v1%3A158565288818297882; Max-Age=63072000; Expires=Thu, 31 Mar 2022
+ 11:08:08 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
+ - lang=en; Path=/
+ - personalization_id="v1_gHl+MuEHs7qIWs/N81Nw5g=="; Max-Age=63072000; Expires=Thu,
+ 31 Mar 2022 11:08:08 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:
+ - b9ab8422ca7a75076ed48a672f8bd60f
+ X-Content-Type-Options:
+ - nosniff
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Response-Time:
+ - '165'
+ X-Transaction:
+ - 0044032300c7ecc7
+ X-Tsa-Request-Body-Time:
+ - '0'
+ X-Twitter-Response-Tags:
+ - BouncerCompliant
+ X-Xss-Protection:
+ - '0'
+ body:
+ encoding: UTF-8
+ string: '{"created_at":"Tue Mar 31 11:08:08 +0000 2020","id":1244944562004447232,"id_str":"1244944562004447232","text":"@twitter
+ @twitterlive Don\u2019t mind me, just testing the API","truncated":false,"entities":{"hashtags":[],"symbols":[],"user_mentions":[{"screen_name":"Twitter","name":"Twitter","id":783214,"id_str":"783214","indices":[0,8]},{"screen_name":"TwitterLive","name":"Twitter
+ Live","id":130649891,"id_str":"130649891","indices":[9,21]}],"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":783214,"in_reply_to_user_id_str":"783214","in_reply_to_screen_name":"Twitter","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":22,"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":"en"}'
+ http_version:
+ recorded_at: Tue, 31 Mar 2020 11:08:08 GMT
+recorded_with: VCR 4.0.0