Commit c31adf01aea67dfc598882c2692b157e00a2f037

Authored by Daniel
1 parent 3ffb1506
Exists in colab and in 2 other branches master, stable

Implement Webhook support library

It can abstract different webhook formats for different services, and
have verification of request, repository address and branch information.
lib/webhooks.rb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +require 'webhooks/base'
  2 +require 'webhooks/gitlab'
  3 +
  4 +module Webhooks
  5 +end
... ...
lib/webhooks/base.rb 0 → 100644
... ... @@ -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
... ...
lib/webhooks/gitlab.rb 0 → 100644
... ... @@ -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
... ...
spec/factories/webhooks.rb 0 → 100644
... ... @@ -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
... ...
spec/lib/webhooks/base_spec.rb 0 → 100644
... ... @@ -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
... ...
spec/lib/webhooks/gitlab_spec.rb 0 → 100644
... ... @@ -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
... ...