summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/custom_action_status.rb93
-rw-r--r--spec/custom_action_status_spec.rb140
-rw-r--r--spec/vcr_cassettes/custom-action-not-ok.yml40
-rw-r--r--spec/vcr_cassettes/custom-action-ok.yml99
4 files changed, 372 insertions, 0 deletions
diff --git a/lib/custom_action_status.rb b/lib/custom_action_status.rb
new file mode 100644
index 0000000..b251156
--- /dev/null
+++ b/lib/custom_action_status.rb
@@ -0,0 +1,93 @@
+require 'cgi'
+require 'json'
+
+require_relative 'access_status'
+require_relative 'gitlab_shell'
+require_relative 'http_helper'
+
+class CustomActionStatus < AccessStatus
+ include HTTPHelper
+
+ class MissingPayloadError < StandardError; end
+ class MissingAPIEndpointsError < StandardError; end
+ class MissingDataError < StandardError; end
+ class UnsuccessfulError < StandardError; end
+
+ DEFAULT_HEADERS = { 'Content-Type' => 'application/json' }.freeze
+
+ def initialize(status, message, payload)
+ @status = status
+ @message = message
+ @payload = payload
+ end
+
+ def self.create_from_json(json)
+ values = JSON.parse(json)
+ new(values['status'], values['message'], values['payload'])
+ end
+
+ def execute(key_id)
+ validate!
+
+ output = ''
+ resp = nil
+
+ api_endpoints.each do |endpoint|
+ url = "#{base_api_endpoint}/#{endpoint}"
+ json = { 'data' => data.merge('key_id' => key_id), 'output' => output }
+
+ resp = post(url, {}, headers: DEFAULT_HEADERS, options: { json: json })
+ return resp unless GitlabShell::HTTP_SUCCESS_CODES.include?(resp.code)
+
+ body = JSON.parse(resp.body)
+ print_flush(body['result'])
+
+ # In the context of the git push sequence of events, it's necessary to read
+ # stdin in order to capture output to pass onto subsequent commands
+ output = read_stdin
+ end
+
+ resp
+ end
+
+ private
+
+ attr_reader :payload
+
+ def api_endpoints
+ @api_endpoints ||= payload['api_endpoints']
+ end
+
+ def data
+ @data ||= payload['data']
+ end
+
+ def read_stdin
+ CGI.escape($stdin.read)
+ end
+
+ def print_flush(str)
+ return false unless str
+ print(CGI.unescape(str))
+ STDOUT.flush
+ end
+
+ def validate!
+ validate_payload!
+ validate_api_endpoints!
+ validate_data!
+ end
+
+ def validate_payload!
+ raise MissingPayloadError if !payload.is_a?(Hash) || payload.empty?
+ end
+
+ def validate_api_endpoints!
+ raise MissingAPIEndpointsError if !api_endpoints.is_a?(Array) ||
+ api_endpoints.empty?
+ end
+
+ def validate_data!
+ raise MissingDataError unless data.is_a?(Hash)
+ end
+end
diff --git a/spec/custom_action_status_spec.rb b/spec/custom_action_status_spec.rb
new file mode 100644
index 0000000..02c11b5
--- /dev/null
+++ b/spec/custom_action_status_spec.rb
@@ -0,0 +1,140 @@
+require_relative 'spec_helper'
+require_relative '../lib/custom_action_status'
+
+describe CustomActionStatus, vcr: true do
+ let(:key_id) { 'key-1' }
+ let(:secret) { "0a3938d9d95d807e94d937af3a4fbbea" }
+ let(:base_api_endpoint) { 'http://localhost:3000/api/v4' }
+ let(:json_str_valid) do
+ {
+ 'payload' => {
+ 'api_endpoints' => %w{fake/info_refs fake/push},
+ 'data' => {
+ 'username' => 'user1',
+ 'primary_repo' => 'http://localhost:3001/user1/repo1.git'
+ }
+ }
+ }.to_json
+ end
+ let(:json_str_missing_api_endpoints) do
+ {
+ 'payload' => {
+ 'data' => {
+ 'username' => 'user1',
+ 'primary_repo' => 'http://localhost:3001/user1/repo1.git'
+ }
+ }
+ }.to_json
+ end
+ let(:json_str_empty_api_endpoints) do
+ {
+ 'payload' => {
+ 'api_endpoints' => [],
+ 'data' => {
+ 'username' => 'user1',
+ 'primary_repo' => 'http://localhost:3001/user1/repo1.git'
+ }
+ }
+ }.to_json
+ end
+ let(:json_str_missing_data) do
+ {
+ 'payload' => {
+ 'api_endpoints' => %w{fake/info_refs fake/push}
+ }
+ }.to_json
+ end
+ let(:json_str_invalid_api_endpoints) do
+ {
+ 'payload' => {
+ 'api_endpoints' => %w{fake/info_refs_bad fake/push_bad},
+ 'data' => {
+ 'username' => 'user1',
+ 'primary_repo' => 'http://localhost:3001/user1/repo1.git'
+ }
+ }
+ }.to_json
+ end
+ let(:json_str_empty) { { '' => {} }.to_json }
+ let(:json_str_empty_payload) { { 'payload' => {} }.to_json }
+
+ describe '.create_from_json' do
+ it 'creates an instance from a JSON string' do
+ described_class.create_from_json(json_str_empty)
+ end
+ end
+
+ context 'instantiated' do
+ describe '#execute' do
+ context 'with an empy JSON string' do
+ subject { described_class.create_from_json(json_str_empty) }
+
+ it 'returns nil' do
+ expect { subject.execute(key_id) }.to raise_error(CustomActionStatus::MissingPayloadError)
+ end
+ end
+
+ context 'with an empty payload' do
+ subject { described_class.create_from_json(json_str_empty_payload) }
+
+ it 'returns nil' do
+ expect { subject.execute(key_id) }.to raise_error(CustomActionStatus::MissingPayloadError)
+ end
+ end
+
+ context 'with api_endpoints defined' do
+ before do
+ subject.stub(:base_api_endpoint).and_return(base_api_endpoint)
+ subject.stub(:secret_token).and_return(secret)
+ $stdin.stub(:read).and_return('')
+ end
+
+ context 'that are valid' do
+ subject { described_class.create_from_json(json_str_valid) }
+
+ it 'HTTP posts data to defined api_endpoints' do
+ VCR.use_cassette("custom-action-ok") do
+ subject.execute(key_id).should be_instance_of(Net::HTTPCreated)
+ end
+ end
+ end
+
+ context 'that are invalid' do
+ context 'where api_endpoints key is missing' do
+ subject { described_class.create_from_json(json_str_missing_api_endpoints) }
+
+ it 'raises a MissingAPIEndpointsError exception' do
+ expect { subject.execute(key_id) }.to raise_error(CustomActionStatus::MissingAPIEndpointsError)
+ end
+ end
+
+ context 'where api_endpoints is empty' do
+ subject { described_class.create_from_json(json_str_empty_api_endpoints) }
+
+ it 'raises a MissingAPIEndpointsError exception' do
+ expect { subject.execute(key_id) }.to raise_error(CustomActionStatus::MissingAPIEndpointsError)
+ end
+ end
+
+ context 'where data key is missing' do
+ subject { described_class.create_from_json(json_str_missing_data) }
+
+ it 'raises a MissingDataError exception' do
+ expect { subject.execute(key_id) }.to raise_error(CustomActionStatus::MissingDataError)
+ end
+ end
+
+ context 'where API endpoints are bad' do
+ subject { described_class.create_from_json(json_str_invalid_api_endpoints) }
+
+ it 'HTTP posts data to defined api_endpoints' do
+ VCR.use_cassette("custom-action-not-ok") do
+ subject.execute(key_id).should be_instance_of(Net::HTTPForbidden)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/vcr_cassettes/custom-action-not-ok.yml b/spec/vcr_cassettes/custom-action-not-ok.yml
new file mode 100644
index 0000000..24245c9
--- /dev/null
+++ b/spec/vcr_cassettes/custom-action-not-ok.yml
@@ -0,0 +1,40 @@
+---
+http_interactions:
+- request:
+ method: post
+ uri: http://localhost:3000/api/v4/fake/info_refs_bad
+ body:
+ encoding: UTF-8
+ string: '{"data":{"username":"user1","primary_repo":"http://localhost:3001/user1/repo1.git","key_id":"key-11"},"output":"","secret_token":"0a3938d9d95d807e94d937af3a4fbbea\n"}'
+ headers:
+ Content-Type:
+ - application/json
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ Host:
+ - localhost
+ response:
+ status:
+ code: 403
+ message: Forbidden
+ headers:
+ Date:
+ - Fri, 20 Jul 2018 06:54:21 GMT
+ Connection:
+ - close
+ Content-Type:
+ - application/json
+ X-Request-Id:
+ - ea0644ac-e1ad-45f6-aa72-cc7910274318
+ X-Runtime:
+ - '1.236672'
+ body:
+ encoding: UTF-8
+ string: '{"message":"You cannot perform write operations on a read-only instance"}'
+ http_version:
+ recorded_at: Fri, 20 Jul 2018 06:54:21 GMT
+recorded_with: VCR 2.4.0
diff --git a/spec/vcr_cassettes/custom-action-ok.yml b/spec/vcr_cassettes/custom-action-ok.yml
new file mode 100644
index 0000000..c90000d
--- /dev/null
+++ b/spec/vcr_cassettes/custom-action-ok.yml
@@ -0,0 +1,99 @@
+---
+http_interactions:
+- request:
+ method: post
+ uri: http://localhost:3000/api/v4/fake/info_refs
+ body:
+ encoding: UTF-8
+ string: '{"data":{"username":"user1","primary_repo":"http://localhost:3001/user1/repo1.git","key_id":"key-1"},"output":"","secret_token":"0a3938d9d95d807e94d937af3a4fbbea"}'
+ headers:
+ Content-Type:
+ - application/json
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ Host:
+ - localhost
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Fri, 20 Jul 2018 06:18:58 GMT
+ Connection:
+ - close
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Content-Type-Options:
+ - nosniff
+ Content-Type:
+ - application/json
+ Content-Length:
+ - '172'
+ Vary:
+ - Origin
+ Etag:
+ - W/"7d01e1e3dbcbe7cca9607461352f8244"
+ Cache-Control:
+ - max-age=0, private, must-revalidate
+ X-Request-Id:
+ - 03afa234-b6be-49ab-9392-4aa35c5dee25
+ X-Runtime:
+ - '1.436040'
+ body:
+ encoding: UTF-8
+ string: '{"result":"info_refs-result"}'
+ http_version:
+ recorded_at: Fri, 20 Jul 2018 06:18:58 GMT
+- request:
+ method: post
+ uri: http://localhost:3000/api/v4/fake/push
+ body:
+ encoding: UTF-8
+ string: '{"data":{"username":"user1","primary_repo":"http://localhost:3001/user1/repo1.git","key_id":"key-1"},"output":"info_refs-result","secret_token":"0a3938d9d95d807e94d937af3a4fbbea"}'
+ headers:
+ Content-Type:
+ - application/json
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ Host:
+ - localhost
+ response:
+ status:
+ code: 201
+ message: Created
+ headers:
+ Date:
+ - Fri, 20 Jul 2018 06:19:08 GMT
+ Connection:
+ - close
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Content-Type-Options:
+ - nosniff
+ Content-Type:
+ - application/json
+ Content-Length:
+ - '13'
+ Vary:
+ - Origin
+ Cache-Control:
+ - no-cache
+ X-Request-Id:
+ - 0c6894ac-7f8e-4cdb-871f-4cb64d3731ca
+ X-Runtime:
+ - '0.786754'
+ body:
+ encoding: UTF-8
+ string: '{"result":"push-result"}'
+ http_version:
+ recorded_at: Fri, 20 Jul 2018 06:19:08 GMT
+recorded_with: VCR 2.4.0