diff --git a/lib/webhooks.rb b/lib/webhooks.rb new file mode 100644 index 0000000..b1fd3a0 --- /dev/null +++ b/lib/webhooks.rb @@ -0,0 +1,5 @@ +require 'webhooks/base' +require 'webhooks/gitlab' + +module Webhooks +end diff --git a/lib/webhooks/base.rb b/lib/webhooks/base.rb new file mode 100644 index 0000000..7ecc90a --- /dev/null +++ b/lib/webhooks/base.rb @@ -0,0 +1,44 @@ +module Webhooks + class Base + attr_reader :request, :repository + + def initialize(request, repository) + @request = request + @repository = repository + end + + # Returns true if and only if the remote addresses in the request and repository match. + def valid_address? + repository.address == webhook_address + end + + # Returns true if and only if the branch in the request and repository match. + def valid_branch? + repository.branch == webhook_branch + end + + # Returns true if the request parameters, as determined by the particular hook service, are valid + # It will usually check headers, IP ranges, signatures, or any other relevant information. + def valid_request?; raise NotImplementedError; end + + # Extracts the remote address from the webhook request. + def webhook_address; raise NotImplementedError; end + + # Extracts the branch from the webhook request. + def webhook_branch; raise NotImplementedError; end + + protected + + # Converts a git ref name to a branch. This is an utility function for webhook formats that only provide the ref + # updated in their information. Returns nil if the passed parameter is not a valid ref. + # The expected format is 'refs/heads/#{branch_name}'. Anything else will be rejected. + def branch_from_ref(ref) + return nil if !ref + + ref = ref.split('/') + return nil if ref.size != 3 || ref[0..1] != ['refs', 'heads'] + + ref.last + end + end +end diff --git a/lib/webhooks/gitlab.rb b/lib/webhooks/gitlab.rb new file mode 100644 index 0000000..b740f51 --- /dev/null +++ b/lib/webhooks/gitlab.rb @@ -0,0 +1,19 @@ +module Webhooks + class GitLab < Base + def valid_request? + request.headers['X-Gitlab-Event'] == 'Push Hook' + end + + def webhook_address + begin + request.params.fetch(:project).fetch(:git_http_url) + rescue KeyError + return nil + end + end + + def webhook_branch + branch_from_ref(request.params[:ref]) + end + end +end diff --git a/spec/factories/webhooks.rb b/spec/factories/webhooks.rb new file mode 100644 index 0000000..d63a712 --- /dev/null +++ b/spec/factories/webhooks.rb @@ -0,0 +1,40 @@ +require 'action_controller/test_case' + +class RequestInfo + attr_accessor :headers, :params + + def request + req = ActionController::TestRequest.new + headers.each { |k, v| req.headers[k] = v } + params.each { |k, v| req.update_param(k, v) } + req + end +end + +FactoryGirl.define do + factory :request, class: RequestInfo do + headers {} + params {} + end + + factory :gitlab_webhook_request, parent: :request do + headers { { 'X-Gitlab-Event' => 'Push Hook' } } + + # Excerpt From http://doc.gitlab.com/ee/web_hooks/web_hooks.html + params { { + "object_kind" => "push", + "ref" => "refs/heads/master", + "project" => { + "name" => "Kalibro Client", + "description" => "", + "git_ssh_url" => "git@example.com:mezuro/kalibro_client.git", + "git_http_url" => "https://gitlab.com/mezuro/kalibro_client.git", + "path_with_namespace" => "mezuro/kalibro_client", + "default_branch" => "master", + "url" => "git@example.com:mezuro/kalibro_client.git", + "ssh_url" => "git@example.com:mezuro/kalibro_client.git", + "http_url" => "https://gitlab.com/mezuro/kalibro_client.git" + } + } } + end +end diff --git a/spec/lib/webhooks/base_spec.rb b/spec/lib/webhooks/base_spec.rb new file mode 100644 index 0000000..3cc6ef7 --- /dev/null +++ b/spec/lib/webhooks/base_spec.rb @@ -0,0 +1,74 @@ +require 'rails_helper' +require 'webhooks' + +RSpec.describe Webhooks::Base do + let(:request) { ActionController::TestRequest.new } + let(:repository) { FactoryGirl.build(:repository) } + + subject { described_class.new(request, repository) } + + describe 'initialize' do + it 'is expected to initialize request and repository' do + expect(subject.request).to eq(request) + expect(subject.repository).to eq(repository) + end + end + + describe 'valid_request?' do + it 'is expected to not be implemented' do + expect { subject.valid_request? }.to raise_error(NotImplementedError) + end + end + + describe 'webhook_address' do + it 'is expected to not be implemented' do + expect { subject.webhook_address }.to raise_error(NotImplementedError) + end + end + + describe 'webhook_branch' do + it 'is expected to not be implemented' do + expect { subject.webhook_branch }.to raise_error(NotImplementedError) + end + end + + describe 'valid_address?' do + it 'is expected to return true if the repository and webhook addresses match' do + subject.expects(:webhook_address).returns(repository.address) + expect(subject.valid_address?).to eq(true) + end + + it "is expected to return false if the repository and webhook addresses don't match" do + subject.expects(:webhook_address).returns('test') + expect(subject.valid_address?).to eq(false) + end + end + + describe 'valid_branch?' do + it 'is expected to return true if the repository and webhook branches match' do + subject.expects(:webhook_branch).returns(repository.branch) + expect(subject.valid_branch?).to eq(true) + end + + it "is expected to return false if the repository and webhook addresses don't match" do + subject.expects(:webhook_branch).returns('test') + expect(subject.valid_branch?).to eq(false) + end + end + + describe 'branch_from_ref' do + cases = { + nil => nil, + 'refs/heads/master' => 'master', + 'refs/tags/test' => nil, + 'test' => nil, + '' => nil + } + + cases.each do |input, output| + it "is expected to return #{output.inspect} for #{input.inspect}" do + expect(subject.send(:branch_from_ref, input)).to eq(output) + end + end + end +end diff --git a/spec/lib/webhooks/gitlab_spec.rb b/spec/lib/webhooks/gitlab_spec.rb new file mode 100644 index 0000000..efeb8bb --- /dev/null +++ b/spec/lib/webhooks/gitlab_spec.rb @@ -0,0 +1,60 @@ +require 'rails_helper' +require 'webhooks' + +RSpec.describe Webhooks::GitLab do + let(:request) { FactoryGirl.build(:gitlab_webhook_request).request } + let(:repository) { FactoryGirl.build(:repository) } + subject { described_class.new(request, repository) } + + describe 'valid_request?' do + context 'with the correct Gitlab header value' do + it 'is expected to return true' do + expect(subject.valid_request?).to be(true) + end + end + + context 'with an incorrect Gitlab header value' do + let(:request) { FactoryGirl.build(:gitlab_webhook_request, headers: { 'X-Gitlab-Event' => 'Merge Hook' }).request } + it 'is expected to return false' do + expect(subject.valid_request?).to be(false) + end + end + + context 'without a GitLab header' do + let(:request) { FactoryGirl.build(:gitlab_webhook_request, headers: {}).request } + it 'is expected to return false' do + expect(subject.valid_request?).to be(false) + end + end + end + + describe 'webhook_address' do + context 'the git URL is present' do + it 'is expected to return the URL' do + expect(subject.webhook_address).to eq(request.params[:project][:git_http_url]) + end + end + + context'the git URL is not present' do + it 'is expected to return nil' do + request.expects(:params).returns({}) + expect(subject.webhook_address).to be_nil + end + end + end + + describe 'webhook_branch' do + context 'the git ref is present' do + it 'is expected to return the branch from the ref' do + expect(subject.webhook_branch).to eq('master') + end + end + + context 'the git ref is not present' do + it 'is expected to return nil' do + request.expects(:params).returns({}) + expect(subject.webhook_branch).to be_nil + end + end + end +end -- libgit2 0.21.2