channels_twitter_spec.rb 4.8 KB

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