Commit c31adf01aea67dfc598882c2692b157e00a2f037
1 parent
3ffb1506
Exists in
colab
and in
2 other branches
Implement Webhook support library
It can abstract different webhook formats for different services, and have verification of request, repository address and branch information.
Showing
6 changed files
with
242 additions
and
0 deletions
Show diff stats
| @@ -0,0 +1,44 @@ | @@ -0,0 +1,44 @@ | ||
| 1 | +module Webhooks | ||
| 2 | + class Base | ||
| 3 | + attr_reader :request, :repository | ||
| 4 | + | ||
| 5 | + def initialize(request, repository) | ||
| 6 | + @request = request | ||
| 7 | + @repository = repository | ||
| 8 | + end | ||
| 9 | + | ||
| 10 | + # Returns true if and only if the remote addresses in the request and repository match. | ||
| 11 | + def valid_address? | ||
| 12 | + repository.address == webhook_address | ||
| 13 | + end | ||
| 14 | + | ||
| 15 | + # Returns true if and only if the branch in the request and repository match. | ||
| 16 | + def valid_branch? | ||
| 17 | + repository.branch == webhook_branch | ||
| 18 | + end | ||
| 19 | + | ||
| 20 | + # Returns true if the request parameters, as determined by the particular hook service, are valid | ||
| 21 | + # It will usually check headers, IP ranges, signatures, or any other relevant information. | ||
| 22 | + def valid_request?; raise NotImplementedError; end | ||
| 23 | + | ||
| 24 | + # Extracts the remote address from the webhook request. | ||
| 25 | + def webhook_address; raise NotImplementedError; end | ||
| 26 | + | ||
| 27 | + # Extracts the branch from the webhook request. | ||
| 28 | + def webhook_branch; raise NotImplementedError; end | ||
| 29 | + | ||
| 30 | + protected | ||
| 31 | + | ||
| 32 | + # Converts a git ref name to a branch. This is an utility function for webhook formats that only provide the ref | ||
| 33 | + # updated in their information. Returns nil if the passed parameter is not a valid ref. | ||
| 34 | + # The expected format is 'refs/heads/#{branch_name}'. Anything else will be rejected. | ||
| 35 | + def branch_from_ref(ref) | ||
| 36 | + return nil if !ref | ||
| 37 | + | ||
| 38 | + ref = ref.split('/') | ||
| 39 | + return nil if ref.size != 3 || ref[0..1] != ['refs', 'heads'] | ||
| 40 | + | ||
| 41 | + ref.last | ||
| 42 | + end | ||
| 43 | + end | ||
| 44 | +end |
| @@ -0,0 +1,19 @@ | @@ -0,0 +1,19 @@ | ||
| 1 | +module Webhooks | ||
| 2 | + class GitLab < Base | ||
| 3 | + def valid_request? | ||
| 4 | + request.headers['X-Gitlab-Event'] == 'Push Hook' | ||
| 5 | + end | ||
| 6 | + | ||
| 7 | + def webhook_address | ||
| 8 | + begin | ||
| 9 | + request.params.fetch(:project).fetch(:git_http_url) | ||
| 10 | + rescue KeyError | ||
| 11 | + return nil | ||
| 12 | + end | ||
| 13 | + end | ||
| 14 | + | ||
| 15 | + def webhook_branch | ||
| 16 | + branch_from_ref(request.params[:ref]) | ||
| 17 | + end | ||
| 18 | + end | ||
| 19 | +end |
| @@ -0,0 +1,40 @@ | @@ -0,0 +1,40 @@ | ||
| 1 | +require 'action_controller/test_case' | ||
| 2 | + | ||
| 3 | +class RequestInfo | ||
| 4 | + attr_accessor :headers, :params | ||
| 5 | + | ||
| 6 | + def request | ||
| 7 | + req = ActionController::TestRequest.new | ||
| 8 | + headers.each { |k, v| req.headers[k] = v } | ||
| 9 | + params.each { |k, v| req.update_param(k, v) } | ||
| 10 | + req | ||
| 11 | + end | ||
| 12 | +end | ||
| 13 | + | ||
| 14 | +FactoryGirl.define do | ||
| 15 | + factory :request, class: RequestInfo do | ||
| 16 | + headers {} | ||
| 17 | + params {} | ||
| 18 | + end | ||
| 19 | + | ||
| 20 | + factory :gitlab_webhook_request, parent: :request do | ||
| 21 | + headers { { 'X-Gitlab-Event' => 'Push Hook' } } | ||
| 22 | + | ||
| 23 | + # Excerpt From http://doc.gitlab.com/ee/web_hooks/web_hooks.html | ||
| 24 | + params { { | ||
| 25 | + "object_kind" => "push", | ||
| 26 | + "ref" => "refs/heads/master", | ||
| 27 | + "project" => { | ||
| 28 | + "name" => "Kalibro Client", | ||
| 29 | + "description" => "", | ||
| 30 | + "git_ssh_url" => "git@example.com:mezuro/kalibro_client.git", | ||
| 31 | + "git_http_url" => "https://gitlab.com/mezuro/kalibro_client.git", | ||
| 32 | + "path_with_namespace" => "mezuro/kalibro_client", | ||
| 33 | + "default_branch" => "master", | ||
| 34 | + "url" => "git@example.com:mezuro/kalibro_client.git", | ||
| 35 | + "ssh_url" => "git@example.com:mezuro/kalibro_client.git", | ||
| 36 | + "http_url" => "https://gitlab.com/mezuro/kalibro_client.git" | ||
| 37 | + } | ||
| 38 | + } } | ||
| 39 | + end | ||
| 40 | +end |
| @@ -0,0 +1,74 @@ | @@ -0,0 +1,74 @@ | ||
| 1 | +require 'rails_helper' | ||
| 2 | +require 'webhooks' | ||
| 3 | + | ||
| 4 | +RSpec.describe Webhooks::Base do | ||
| 5 | + let(:request) { ActionController::TestRequest.new } | ||
| 6 | + let(:repository) { FactoryGirl.build(:repository) } | ||
| 7 | + | ||
| 8 | + subject { described_class.new(request, repository) } | ||
| 9 | + | ||
| 10 | + describe 'initialize' do | ||
| 11 | + it 'is expected to initialize request and repository' do | ||
| 12 | + expect(subject.request).to eq(request) | ||
| 13 | + expect(subject.repository).to eq(repository) | ||
| 14 | + end | ||
| 15 | + end | ||
| 16 | + | ||
| 17 | + describe 'valid_request?' do | ||
| 18 | + it 'is expected to not be implemented' do | ||
| 19 | + expect { subject.valid_request? }.to raise_error(NotImplementedError) | ||
| 20 | + end | ||
| 21 | + end | ||
| 22 | + | ||
| 23 | + describe 'webhook_address' do | ||
| 24 | + it 'is expected to not be implemented' do | ||
| 25 | + expect { subject.webhook_address }.to raise_error(NotImplementedError) | ||
| 26 | + end | ||
| 27 | + end | ||
| 28 | + | ||
| 29 | + describe 'webhook_branch' do | ||
| 30 | + it 'is expected to not be implemented' do | ||
| 31 | + expect { subject.webhook_branch }.to raise_error(NotImplementedError) | ||
| 32 | + end | ||
| 33 | + end | ||
| 34 | + | ||
| 35 | + describe 'valid_address?' do | ||
| 36 | + it 'is expected to return true if the repository and webhook addresses match' do | ||
| 37 | + subject.expects(:webhook_address).returns(repository.address) | ||
| 38 | + expect(subject.valid_address?).to eq(true) | ||
| 39 | + end | ||
| 40 | + | ||
| 41 | + it "is expected to return false if the repository and webhook addresses don't match" do | ||
| 42 | + subject.expects(:webhook_address).returns('test') | ||
| 43 | + expect(subject.valid_address?).to eq(false) | ||
| 44 | + end | ||
| 45 | + end | ||
| 46 | + | ||
| 47 | + describe 'valid_branch?' do | ||
| 48 | + it 'is expected to return true if the repository and webhook branches match' do | ||
| 49 | + subject.expects(:webhook_branch).returns(repository.branch) | ||
| 50 | + expect(subject.valid_branch?).to eq(true) | ||
| 51 | + end | ||
| 52 | + | ||
| 53 | + it "is expected to return false if the repository and webhook addresses don't match" do | ||
| 54 | + subject.expects(:webhook_branch).returns('test') | ||
| 55 | + expect(subject.valid_branch?).to eq(false) | ||
| 56 | + end | ||
| 57 | + end | ||
| 58 | + | ||
| 59 | + describe 'branch_from_ref' do | ||
| 60 | + cases = { | ||
| 61 | + nil => nil, | ||
| 62 | + 'refs/heads/master' => 'master', | ||
| 63 | + 'refs/tags/test' => nil, | ||
| 64 | + 'test' => nil, | ||
| 65 | + '' => nil | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + cases.each do |input, output| | ||
| 69 | + it "is expected to return #{output.inspect} for #{input.inspect}" do | ||
| 70 | + expect(subject.send(:branch_from_ref, input)).to eq(output) | ||
| 71 | + end | ||
| 72 | + end | ||
| 73 | + end | ||
| 74 | +end |
| @@ -0,0 +1,60 @@ | @@ -0,0 +1,60 @@ | ||
| 1 | +require 'rails_helper' | ||
| 2 | +require 'webhooks' | ||
| 3 | + | ||
| 4 | +RSpec.describe Webhooks::GitLab do | ||
| 5 | + let(:request) { FactoryGirl.build(:gitlab_webhook_request).request } | ||
| 6 | + let(:repository) { FactoryGirl.build(:repository) } | ||
| 7 | + subject { described_class.new(request, repository) } | ||
| 8 | + | ||
| 9 | + describe 'valid_request?' do | ||
| 10 | + context 'with the correct Gitlab header value' do | ||
| 11 | + it 'is expected to return true' do | ||
| 12 | + expect(subject.valid_request?).to be(true) | ||
| 13 | + end | ||
| 14 | + end | ||
| 15 | + | ||
| 16 | + context 'with an incorrect Gitlab header value' do | ||
| 17 | + let(:request) { FactoryGirl.build(:gitlab_webhook_request, headers: { 'X-Gitlab-Event' => 'Merge Hook' }).request } | ||
| 18 | + it 'is expected to return false' do | ||
| 19 | + expect(subject.valid_request?).to be(false) | ||
| 20 | + end | ||
| 21 | + end | ||
| 22 | + | ||
| 23 | + context 'without a GitLab header' do | ||
| 24 | + let(:request) { FactoryGirl.build(:gitlab_webhook_request, headers: {}).request } | ||
| 25 | + it 'is expected to return false' do | ||
| 26 | + expect(subject.valid_request?).to be(false) | ||
| 27 | + end | ||
| 28 | + end | ||
| 29 | + end | ||
| 30 | + | ||
| 31 | + describe 'webhook_address' do | ||
| 32 | + context 'the git URL is present' do | ||
| 33 | + it 'is expected to return the URL' do | ||
| 34 | + expect(subject.webhook_address).to eq(request.params[:project][:git_http_url]) | ||
| 35 | + end | ||
| 36 | + end | ||
| 37 | + | ||
| 38 | + context'the git URL is not present' do | ||
| 39 | + it 'is expected to return nil' do | ||
| 40 | + request.expects(:params).returns({}) | ||
| 41 | + expect(subject.webhook_address).to be_nil | ||
| 42 | + end | ||
| 43 | + end | ||
| 44 | + end | ||
| 45 | + | ||
| 46 | + describe 'webhook_branch' do | ||
| 47 | + context 'the git ref is present' do | ||
| 48 | + it 'is expected to return the branch from the ref' do | ||
| 49 | + expect(subject.webhook_branch).to eq('master') | ||
| 50 | + end | ||
| 51 | + end | ||
| 52 | + | ||
| 53 | + context 'the git ref is not present' do | ||
| 54 | + it 'is expected to return nil' do | ||
| 55 | + request.expects(:params).returns({}) | ||
| 56 | + expect(subject.webhook_branch).to be_nil | ||
| 57 | + end | ||
| 58 | + end | ||
| 59 | + end | ||
| 60 | +end |