Request-spec controller actions for Twitter API webhook

This is the first of many commits
to add missing test coverage for Zammad's Twitter functionality.
The testing process begins from the broadest scope possible,
and will proceed in future commits
to more granular, fine-grained behaviors.

(Request specs provide integration testing at the controller level,
and webhook controller actions are the main point of entry
for incoming Twitter API functionality.)
This commit is contained in:
Ryan Lue 2019-12-04 14:37:30 +08:00 committed by Thorsten Eckel
parent 889dd92aea
commit 9409d17de6
2 changed files with 132 additions and 4 deletions

View file

@ -9,10 +9,10 @@ FactoryBot.define do
name { 'twitter' }
credentials do
{ consumer_key: 123,
consumer_secret: 123,
oauth_token: 123,
oauth_token_secret: 123 }
{ consumer_key: '123',
consumer_secret: '123',
oauth_token: '123',
oauth_token_secret: '123' }
end
end
end

View file

@ -0,0 +1,128 @@
require 'rails_helper'
RSpec.describe 'Twitter channel API endpoints', type: :request do
let!(:twitter_channel) { create(:twitter_channel) }
let!(:twitter_credential) { create(:twitter_credential) }
let(:hash_signature) { %(sha256=#{Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', consumer_secret, payload))}) }
let(:consumer_secret) { twitter_credential.credentials[:consumer_secret] }
# What's this all about? See the "Challenge-Response Checks" section of this article:
# https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/guides/securing-webhooks
describe 'GET /api/v1/channels_twitter_webhook' do
let(:payload) { params[:crc_token] }
let(:params) { { crc_token: 'foo' } }
context 'with consumer secret and "crc_token" param' do
it 'responds with { response_token: <hash_signature> }' do
get '/api/v1/channels_twitter_webhook', params: params, as: :json
expect(json_response).to eq('response_token' => hash_signature)
end
end
context 'without valid twitter credentials in the DB' do
let!(:twitter_credential) { create(:twitter_credential, credentials: { foo: 'bar' }) }
it 'responds 422 Unprocessable Entity' do
get '/api/v1/channels_twitter_webhook', params: params, as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'without "crc_token" param' do
let(:params) { {} }
it 'responds 422 Unprocessable Entity' do
get '/api/v1/channels_twitter_webhook', params: params, as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe 'POST /api/v1/channels_twitter_webhook' do
let(:payload) { params.stringify_keys.to_s.gsub(/=>/, ':').tr(' ', '') }
let(:headers) { { 'x-twitter-webhooks-signature': hash_signature } }
let(:params) { { foo: 'bar', for_user_id: user_id } }
let(:user_id) { twitter_channel.options[:user][:id] }
# What's this all about? See the "Optional signature header validation" section of this article:
# https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/guides/securing-webhooks
describe 'hash signature validation' do
context 'with valid params and headers (i.e., not one of the failure cases below)' do
it 'responds 200 OK' do
post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
expect(response).to have_http_status(:ok)
end
end
describe '"x-twitter-webhooks-signature" header' do
context 'when absent' do
let(:headers) { {} }
it 'responds 422 Unprocessable Entity' do
post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'when payload doesnt match' do
let(:headers) { { 'x-twitter-webhooks-signature': 'Not actually a signature' } }
it 'responds 401 Not Authorized' do
post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
describe '"for_user_id" param' do
context 'when absent' do
let(:params) { { foo: 'bar' } }
it 'responds 422 Unprocessable Entity' do
post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'without corresponding Channel' do
let(:params) { { foo: 'bar', for_user_id: 'no_such_user' } }
it 'responds 422 Unprocessable Entity' do
post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end
describe 'core behavior' do
before do
allow(TwitterSync).to receive(:new).and_return(twitter_sync)
allow(twitter_sync).to receive(:process_webhook)
end
let(:twitter_sync) { instance_double('TwitterSync') }
it 'delegates to TwitterSync#process_webhook' do
post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
expect(twitter_sync).to have_received(:process_webhook).with(twitter_channel)
end
it 'responds with an empty hash' do
post '/api/v1/channels_twitter_webhook', params: params, headers: headers, as: :json
expect(json_response).to eq({})
end
end
end
end