channels_twitter_spec.rb 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. require 'rails_helper'
  2. RSpec.describe 'Twitter channel API endpoints', type: :request do
  3. let!(:twitter_channel) { create(:twitter_channel) }
  4. let(:twitter_credential) { ExternalCredential.find(twitter_channel.options[:auth][:external_credential_id]) }
  5. let(:hash_signature) { %(sha256=#{Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', consumer_secret, payload))}) }
  6. let(:consumer_secret) { twitter_credential.credentials[:consumer_secret] }
  7. # What's this all about? See the "Challenge-Response Checks" section of this article:
  8. # https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/guides/securing-webhooks
  9. describe 'GET /api/v1/channels_twitter_webhook' do
  10. let(:payload) { params[:crc_token] }
  11. let(:params) { { crc_token: 'foo' } }
  12. context 'with consumer secret and "crc_token" param' do
  13. it 'responds with { response_token: <hash_signature> }' do
  14. get '/api/v1/channels_twitter_webhook', params: params, as: :json
  15. expect(json_response).to eq('response_token' => hash_signature)
  16. end
  17. end
  18. context 'without valid twitter credentials in the DB' do
  19. before do
  20. twitter_credential.credentials.delete(:consumer_secret)
  21. twitter_credential.save!
  22. end
  23. it 'responds 422 Unprocessable Entity' do
  24. get '/api/v1/channels_twitter_webhook', params: params, as: :json
  25. expect(response).to have_http_status(:unprocessable_entity)
  26. end
  27. end
  28. context 'without "crc_token" param' do
  29. before { params.delete(:crc_token) }
  30. it 'responds 422 Unprocessable Entity' do
  31. get '/api/v1/channels_twitter_webhook', params: params, as: :json
  32. expect(response).to have_http_status(:unprocessable_entity)
  33. end
  34. end
  35. end
  36. describe 'POST /api/v1/channels_twitter_webhook' do
  37. let(:payload) { params.stringify_keys.to_s.gsub(%r{=>}, ':').delete(' ') }
  38. let(:headers) { { 'x-twitter-webhooks-signature': hash_signature } }
  39. let(:params) { { foo: 'bar', for_user_id: twitter_channel.options[:user][:id] } }
  40. # What's this all about? See the "Optional signature header validation" section of this article:
  41. # https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/guides/securing-webhooks
  42. describe 'hash signature validation' do
  43. context 'with valid params and headers (i.e., not one of the failure cases below)' do
  44. it 'responds 200 OK' do
  45. post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
  46. expect(response).to have_http_status(:ok)
  47. end
  48. end
  49. describe '"x-twitter-webhooks-signature" header' do
  50. context 'when absent' do
  51. let(:headers) { {} }
  52. it 'responds 422 Unprocessable Entity' do
  53. post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
  54. expect(response).to have_http_status(:unprocessable_entity)
  55. end
  56. end
  57. context 'when invalid (not based on consumer secret + payload)' do
  58. let(:headers) { { 'x-twitter-webhooks-signature': 'Not a valid signature' } }
  59. it 'responds 401 Not Authorized' do
  60. post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
  61. expect(response).to have_http_status(:unauthorized)
  62. end
  63. end
  64. end
  65. describe '"for_user_id" param' do
  66. context 'when absent' do
  67. let(:params) { { foo: 'bar' } }
  68. it 'responds 422 Unprocessable Entity' do
  69. post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
  70. expect(response).to have_http_status(:unprocessable_entity)
  71. end
  72. end
  73. context 'without corresponding Channel' do
  74. let(:params) { { foo: 'bar', for_user_id: 'no_such_user' } }
  75. it 'responds 422 Unprocessable Entity' do
  76. post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
  77. expect(response).to have_http_status(:unprocessable_entity)
  78. end
  79. end
  80. end
  81. end
  82. describe 'core behavior' do
  83. before do
  84. allow(TwitterSync).to receive(:new).and_return(twitter_sync)
  85. allow(twitter_sync).to receive(:process_webhook)
  86. end
  87. let(:twitter_sync) { instance_double('TwitterSync') }
  88. it 'delegates to TwitterSync#process_webhook' do
  89. post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
  90. expect(twitter_sync).to have_received(:process_webhook).with(twitter_channel)
  91. end
  92. it 'responds with an empty hash' do
  93. post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
  94. expect(json_response).to eq({})
  95. end
  96. end
  97. end
  98. end