Commit ee259653 authored by Alejandro Rodríguez's avatar Alejandro Rodríguez

Handle GL_REPOSITORY env variable and use it in api calls

parent 1cf14770
......@@ -5,12 +5,13 @@
refs = $stdin.read
key_id = ENV.delete('GL_ID')
gl_repository = ENV.delete('GL_REPOSITORY')
repo_path = Dir.pwd
require_relative '../lib/gitlab_custom_hook'
require_relative '../lib/gitlab_post_receive'
if GitlabPostReceive.new(repo_path, key_id, refs).exec &&
if GitlabPostReceive.new(gl_repository, repo_path, key_id, refs).exec &&
GitlabCustomHook.new(repo_path, key_id).post_receive(refs)
exit 0
else
......
......@@ -7,6 +7,7 @@ refs = $stdin.read
key_id = ENV.delete('GL_ID')
protocol = ENV.delete('GL_PROTOCOL')
repo_path = Dir.pwd
gl_repository = ENV['GL_REPOSITORY']
require_relative '../lib/gitlab_custom_hook'
require_relative '../lib/gitlab_reference_counter'
......@@ -16,7 +17,7 @@ require_relative '../lib/gitlab_access'
# last so that it only runs if everything else succeeded. On post-receive on the
# other hand, we run GitlabPostReceive first because the push is already done
# and we don't want to skip it if the custom hook fails.
if GitlabAccess.new(repo_path, key_id, refs, protocol).exec &&
if GitlabAccess.new(gl_repository, repo_path, key_id, refs, protocol).exec &&
GitlabCustomHook.new(repo_path, key_id).pre_receive(refs) &&
GitlabReferenceCounter.new(repo_path).increase
exit 0
......
......@@ -10,10 +10,11 @@ class GitlabAccess
include NamesHelper
attr_reader :config, :repo_path, :changes, :protocol
attr_reader :config, :gl_repository, :repo_path, :changes, :protocol
def initialize(repo_path, actor, changes, protocol)
def initialize(gl_repository, repo_path, actor, changes, protocol)
@config = GitlabConfig.new
@gl_repository = gl_repository
@repo_path = repo_path.strip
@actor = actor
@changes = changes.lines
......@@ -27,7 +28,7 @@ class GitlabAccess
"GIT_OBJECT_DIRECTORY" => ENV["GIT_OBJECT_DIRECTORY"]
}
api.check_access('git-receive-pack', @repo_path, @actor, @changes, @protocol, env: env.to_json)
api.check_access('git-receive-pack', @gl_repository, @repo_path, @actor, @changes, @protocol, env: env.to_json)
end
raise AccessDeniedError, status.message unless status.allowed?
......
require 'json'
class GitAccessStatus
attr_reader :message, :repository_path
attr_reader :message, :gl_repository, :repository_path
def initialize(status, message, repository_path)
def initialize(status, message, gl_repository, repository_path)
@status = status
@message = message
@gl_repository = gl_repository
@repository_path = repository_path
end
def self.create_from_json(json)
values = JSON.parse(json)
self.new(values["status"], values["message"], values["repository_path"])
self.new(values["status"], values["message"], values["gl_repository"], values["repository_path"])
end
def allowed?
......
......@@ -15,17 +15,19 @@ class GitlabNet
CHECK_TIMEOUT = 5
READ_TIMEOUT = 300
def check_access(cmd, repo, actor, changes, protocol, env: {})
def check_access(cmd, gl_repository, repo, actor, changes, protocol, env: {})
changes = changes.join("\n") unless changes.kind_of?(String)
params = {
action: cmd,
changes: changes,
gl_repository: gl_repository,
project: sanitize_path(repo),
protocol: protocol,
env: env
}
if actor =~ /\Akey\-\d+\Z/
params.merge!(key_id: actor.gsub("key-", ""))
elsif actor =~ /\Auser\-\d+\Z/
......@@ -38,7 +40,7 @@ class GitlabNet
if resp.code == '200'
GitAccessStatus.create_from_json(resp.body)
else
GitAccessStatus.new(false, 'API is not accessible', nil)
GitAccessStatus.new(false, 'API is not accessible', nil, nil)
end
end
......@@ -66,10 +68,12 @@ class GitlabNet
JSON.parse(resp.body) rescue {}
end
def merge_request_urls(repo_path, changes)
def merge_request_urls(gl_repository, repo_path, changes)
changes = changes.join("\n") unless changes.kind_of?(String)
changes = changes.encode('UTF-8', 'ASCII', invalid: :replace, replace: '')
resp = get("#{host_v3}/merge_request_urls?project=#{URI.escape(repo_path)}&changes=#{URI.escape(changes)}")
url = "#{host_v3}/merge_request_urls?project=#{URI.escape(repo_path)}&changes=#{URI.escape(changes)}"
url += "&gl_repository=#{URI.escape(gl_repository)}" if gl_repository
resp = get(url)
JSON.parse(resp.body) rescue []
end
......@@ -93,8 +97,9 @@ class GitlabNet
{}
end
def notify_post_receive(repo_path)
resp = post("#{host}/notify_post_receive", repo_path: repo_path)
def notify_post_receive(gl_repository, repo_path)
params = { gl_repository: gl_repository, project: repo_path }
resp = post("#{host}/notify_post_receive", params)
resp.code == '200'
rescue
......
......@@ -9,10 +9,11 @@ require 'securerandom'
class GitlabPostReceive
include NamesHelper
attr_reader :config, :repo_path, :changes, :jid
attr_reader :config, :gl_repository, :repo_path, :changes, :jid
def initialize(repo_path, actor, changes)
def initialize(gl_repository, repo_path, actor, changes)
@config = GitlabConfig.new
@gl_repository = gl_repository
@repo_path, @actor = repo_path.strip, actor
@changes = changes
@jid = SecureRandom.hex(12)
......@@ -32,11 +33,11 @@ class GitlabPostReceive
end
merge_request_urls = GitlabMetrics.measure("merge-request-urls") do
api.merge_request_urls(@repo_path, @changes)
api.merge_request_urls(@gl_repository, @repo_path, @changes)
end
print_merge_request_links(merge_request_urls)
api.notify_post_receive(repo_path)
api.notify_post_receive(gl_repository, repo_path)
rescue GitlabNet::ApiUnreachableError
nil
end
......
......@@ -13,7 +13,7 @@ class GitlabShell
API_COMMANDS = %w(2fa_recovery_codes)
GL_PROTOCOL = 'ssh'.freeze
attr_accessor :key_id, :repo_name, :command, :git_access
attr_accessor :key_id, :gl_repository, :repo_name, :command, :git_access
attr_reader :repo_path
def initialize(key_id)
......@@ -89,11 +89,12 @@ class GitlabShell
end
def verify_access
status = api.check_access(@git_access, @repo_name, @key_id, '_any', GL_PROTOCOL)
status = api.check_access(@git_access, nil, @repo_name, @key_id, '_any', GL_PROTOCOL)
raise AccessDeniedError, status.message unless status.allowed?
self.repo_path = status.repository_path
@gl_repository = status.gl_repository
end
def process_cmd(args)
......@@ -125,7 +126,8 @@ class GitlabShell
'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'],
'LANG' => ENV['LANG'],
'GL_ID' => @key_id,
'GL_PROTOCOL' => GL_PROTOCOL
'GL_PROTOCOL' => GL_PROTOCOL,
'GL_REPOSITORY' => @gl_repository
}
if git_trace_available?
......
......@@ -7,11 +7,11 @@ describe GitlabAccess do
let(:repo_path) { File.join(repository_path, repo_name) + ".git" }
let(:api) do
double(GitlabNet).tap do |api|
api.stub(check_access: GitAccessStatus.new(true, 'ok', '/home/git/repositories'))
api.stub(check_access: GitAccessStatus.new(true, 'ok', 'project-1', '/home/git/repositories'))
end
end
subject do
GitlabAccess.new(repo_path, 'key-123', 'wow', 'ssh').tap do |access|
GitlabAccess.new(nil, repo_path, 'key-123', 'wow', 'ssh').tap do |access|
access.stub(exec_cmd: :exec_called)
access.stub(api: api)
end
......@@ -38,7 +38,7 @@ describe GitlabAccess do
context "access is denied" do
before do
api.stub(check_access: GitAccessStatus.new(false, 'denied', nil))
api.stub(check_access: GitAccessStatus.new(false, 'denied', nil, nil))
end
it "returns false" do
......
......@@ -6,10 +6,12 @@ require_relative '../lib/gitlab_access_status'
describe GitlabNet, vcr: true do
let(:gitlab_net) { GitlabNet.new }
let(:changes) { ['0000000000000000000000000000000000000000 92d0970eefd7acb6d548878925ce2208cfe2d2ec refs/heads/branch4'] }
let(:host_v3) { 'https://dev.gitlab.org/api/v3/internal' }
let(:host) { 'https://dev.gitlab.org/api/v4/internal' }
before do
gitlab_net.stub(:host_v3).and_return('https://dev.gitlab.org/api/v3/internal')
gitlab_net.stub(:host).and_return('https://dev.gitlab.org/api/v4/internal')
gitlab_net.stub(:host_v3).and_return(host_v3)
gitlab_net.stub(:host).and_return(host)
gitlab_net.stub(:secret_token).and_return('a123')
end
......@@ -91,6 +93,25 @@ describe GitlabNet, vcr: true do
end
end
describe :merge_request_urls do
let(:gl_repository) { "project-1" }
let(:repo_path) { "/path/to/my/repo.git" }
let(:changes) { "123456 789012 refs/heads/test\n654321 210987 refs/tags/tag" }
let(:encoded_changes) { "123456%20789012%20refs/heads/test%0A654321%20210987%20refs/tags/tag" }
it "sends the given arguments as encoded URL parameters" do
gitlab_net.should_receive(:get).with("#{host_v3}/merge_request_urls?project=#{repo_path}&changes=#{encoded_changes}&gl_repository=#{gl_repository}")
gitlab_net.merge_request_urls(gl_repository, repo_path, changes)
end
it "omits the gl_repository parameter if it's nil" do
gitlab_net.should_receive(:get).with("#{host_v3}/merge_request_urls?project=#{repo_path}&changes=#{encoded_changes}")
gitlab_net.merge_request_urls(nil, repo_path, changes)
end
end
describe :authorized_key do
let (:ssh_key) { "AAAAB3NzaC1yc2EAAAADAQABAAACAQDPKPqqnqQ9PDFw65cO7iHXrKw6ucSZg8Bd2CZ150Yy1YRDPJOWeRNCnddS+M/Lk" }
......@@ -140,20 +161,31 @@ describe GitlabNet, vcr: true do
end
describe '#notify_post_receive' do
let(:gl_repository) { 'project-1' }
let(:repo_path) { '/path/to/my/repo.git' }
let(:params) do
{ gl_repository: gl_repository, project: repo_path }
end
it 'sets the arguments as form parameters' do
VCR.use_cassette('notify-post-receive') do
Net::HTTP::Post.any_instance.should_receive(:set_form_data).with(hash_including(params))
gitlab_net.notify_post_receive(gl_repository, repo_path)
end
end
it 'returns true if notification was succesful' do
VCR.use_cassette('notify-post-receive') do
expect(gitlab_net.notify_post_receive(repo_path)).to be_true
expect(gitlab_net.notify_post_receive(gl_repository, repo_path)).to be_true
end
end
end
describe :check_access do
context 'ssh key with access to project' do
context 'ssh key with access nil, to project' do
it 'should allow pull access for dev.gitlab.org' do
VCR.use_cassette("allowed-pull") do
access = gitlab_net.check_access('git-receive-pack', 'gitlab/gitlabhq.git', 'key-126', changes, 'ssh')
access = gitlab_net.check_access('git-receive-pack', nil, 'gitlab/gitlabhq.git', 'key-126', changes, 'ssh')
access.allowed?.should be_true
end
end
......@@ -161,13 +193,13 @@ describe GitlabNet, vcr: true do
it 'adds the secret_token to the request' do
VCR.use_cassette("allowed-pull") do
Net::HTTP::Post.any_instance.should_receive(:set_form_data).with(hash_including(secret_token: 'a123'))
gitlab_net.check_access('git-receive-pack', 'gitlab/gitlabhq.git', 'key-126', changes, 'ssh')
gitlab_net.check_access('git-receive-pack', nil, 'gitlab/gitlabhq.git', 'key-126', changes, 'ssh')
end
end
it 'should allow push access for dev.gitlab.org' do
VCR.use_cassette("allowed-push") do
access = gitlab_net.check_access('git-upload-pack', 'gitlab/gitlabhq.git', 'key-126', changes, 'ssh')
access = gitlab_net.check_access('git-upload-pack', nil, 'gitlab/gitlabhq.git', 'key-126', changes, 'ssh')
access.allowed?.should be_true
end
end
......@@ -176,7 +208,7 @@ describe GitlabNet, vcr: true do
context 'ssh access has been disabled' do
it 'should deny pull access for dev.gitlab.org' do
VCR.use_cassette('ssh-access-disabled') do
access = gitlab_net.check_access('git-receive-pack', 'gitlab/gitlabhq.git', 'key-2', changes, 'ssh')
access = gitlab_net.check_access('git-receive-pack', nil, 'gitlab/gitlabhq.git', 'key-2', changes, 'ssh')
access.allowed?.should be_false
access.message.should eq 'Git access over SSH is not allowed'
end
......@@ -184,7 +216,7 @@ describe GitlabNet, vcr: true do
it 'should deny pull access for dev.gitlab.org' do
VCR.use_cassette('ssh-access-disabled') do
access = gitlab_net.check_access('git-receive-pack', 'gitlab/gitlabhq.git', 'key-2', changes, 'ssh')
access = gitlab_net.check_access('git-receive-pack', nil, 'gitlab/gitlabhq.git', 'key-2', changes, 'ssh')
access.allowed?.should be_false
access.message.should eq 'Git access over SSH is not allowed'
end
......@@ -194,7 +226,7 @@ describe GitlabNet, vcr: true do
context 'http access has been disabled' do
it 'should deny pull access for dev.gitlab.org' do
VCR.use_cassette('http-access-disabled') do
access = gitlab_net.check_access('git-receive-pack', 'gitlab/gitlabhq.git', 'key-2', changes, 'http')
access = gitlab_net.check_access('git-receive-pack', nil, 'gitlab/gitlabhq.git', 'key-2', changes, 'http')
access.allowed?.should be_false
access.message.should eq 'Git access over HTTP is not allowed'
end
......@@ -202,7 +234,7 @@ describe GitlabNet, vcr: true do
it 'should deny pull access for dev.gitlab.org' do
VCR.use_cassette('http-access-disabled') do
access = gitlab_net.check_access('git-receive-pack', 'gitlab/gitlabhq.git', 'key-2', changes, 'http')
access = gitlab_net.check_access('git-receive-pack', nil, 'gitlab/gitlabhq.git', 'key-2', changes, 'http')
access.allowed?.should be_false
access.message.should eq 'Git access over HTTP is not allowed'
end
......@@ -212,21 +244,21 @@ describe GitlabNet, vcr: true do
context 'ssh key without access to project' do
it 'should deny pull access for dev.gitlab.org' do
VCR.use_cassette("denied-pull") do
access = gitlab_net.check_access('git-receive-pack', 'gitlab/gitlabhq.git', 'key-2', changes, 'ssh')
access = gitlab_net.check_access('git-receive-pack', nil, 'gitlab/gitlabhq.git', 'key-2', changes, 'ssh')
access.allowed?.should be_false
end
end
it 'should deny push access for dev.gitlab.org' do
VCR.use_cassette("denied-push") do
access = gitlab_net.check_access('git-upload-pack', 'gitlab/gitlabhq.git', 'key-2', changes, 'ssh')
access = gitlab_net.check_access('git-upload-pack', nil, 'gitlab/gitlabhq.git', 'key-2', changes, 'ssh')
access.allowed?.should be_false
end
end
it 'should deny push access for dev.gitlab.org (with user)' do
VCR.use_cassette("denied-push-with-user") do
access = gitlab_net.check_access('git-upload-pack', 'gitlab/gitlabhq.git', 'user-1', changes, 'ssh')
access = gitlab_net.check_access('git-upload-pack', nil, 'gitlab/gitlabhq.git', 'user-1', changes, 'ssh')
access.allowed?.should be_false
end
end
......@@ -235,7 +267,7 @@ describe GitlabNet, vcr: true do
it "raises an exception if the connection fails" do
Net::HTTP.any_instance.stub(:request).and_raise(StandardError)
expect {
gitlab_net.check_access('git-upload-pack', 'gitlab/gitlabhq.git', 'user-1', changes, 'ssh')
gitlab_net.check_access('git-upload-pack', nil, 'gitlab/gitlabhq.git', 'user-1', changes, 'ssh')
}.to raise_error(GitlabNet::ApiUnreachableError)
end
end
......
......@@ -10,7 +10,8 @@ describe GitlabPostReceive do
let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") }
let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) }
let(:repo_path) { File.join(repository_path, repo_name) + ".git" }
let(:gitlab_post_receive) { GitlabPostReceive.new(repo_path, actor, wrongly_encoded_changes) }
let(:gl_repository) { "project-1" }
let(:gitlab_post_receive) { GitlabPostReceive.new(gl_repository, repo_path, actor, wrongly_encoded_changes) }
let(:message) { "test " * 10 + "message " * 10 }
let(:redis_client) { double('redis_client') }
let(:enqueued_at) { Time.new(2016, 6, 23, 6, 59) }
......@@ -18,7 +19,7 @@ describe GitlabPostReceive do
before do
GitlabConfig.any_instance.stub(repos_path: repository_path)
GitlabNet.any_instance.stub(broadcast_message: { })
GitlabNet.any_instance.stub(:merge_request_urls).with(repo_path, wrongly_encoded_changes) { [] }
GitlabNet.any_instance.stub(:merge_request_urls).with(gl_repository, repo_path, wrongly_encoded_changes) { [] }
GitlabNet.any_instance.stub(notify_post_receive: true)
expect(Time).to receive(:now).and_return(enqueued_at)
end
......@@ -36,7 +37,7 @@ describe GitlabPostReceive do
context 'Without broad cast message' do
context 'pushing new branch' do
before do
GitlabNet.any_instance.stub(:merge_request_urls).with(repo_path, wrongly_encoded_changes) do
GitlabNet.any_instance.stub(:merge_request_urls).with(gl_repository, repo_path, wrongly_encoded_changes) do
[{
"branch_name" => "new_branch",
"url" => "http://localhost/dzaporozhets/gitlab-ci/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
......@@ -63,7 +64,7 @@ describe GitlabPostReceive do
context 'pushing existing branch with merge request created' do
before do
GitlabNet.any_instance.stub(:merge_request_urls).with(repo_path, wrongly_encoded_changes) do
GitlabNet.any_instance.stub(:merge_request_urls).with(gl_repository, repo_path, wrongly_encoded_changes) do
[{
"branch_name" => "feature_branch",
"url" => "http://localhost/dzaporozhets/gitlab-ci/merge_requests/1",
......@@ -91,7 +92,7 @@ describe GitlabPostReceive do
context 'show broadcast message and merge request link' do
before do
GitlabNet.any_instance.stub(:merge_request_urls).with(repo_path, wrongly_encoded_changes) do
GitlabNet.any_instance.stub(:merge_request_urls).with(gl_repository, repo_path, wrongly_encoded_changes) do
[{
"branch_name" => "new_branch",
"url" => "http://localhost/dzaporozhets/gitlab-ci/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
......@@ -176,7 +177,7 @@ describe GitlabPostReceive do
context 'post_receive notification' do
it 'calls the api to notify the execution of the hook' do
expect_any_instance_of(GitlabNet).to receive(:notify_post_receive).
with(repo_path)
with(gl_repository, repo_path)
gitlab_post_receive.exec
end
......
......@@ -22,7 +22,7 @@ describe GitlabShell do
let(:api) do
double(GitlabNet).tap do |api|
api.stub(discover: { 'name' => 'John Doe' })
api.stub(check_access: GitAccessStatus.new(true, 'ok', repo_path))
api.stub(check_access: GitAccessStatus.new(true, 'ok', gl_repository, repo_path))
api.stub(two_factor_recovery_codes: {
'success' => true,
'recovery_codes' => ['f67c514de60c4953', '41278385fc00c1e0']
......@@ -36,6 +36,7 @@ describe GitlabShell do
let(:repo_name) { 'gitlab-ci.git' }
let(:repo_path) { File.join(tmp_repos_path, repo_name) }
let(:gl_repository) { 'project-1' }
before do
GitlabConfig.any_instance.stub(audit_usernames: false)
......@@ -262,11 +263,11 @@ describe GitlabShell do
after { subject.exec(ssh_cmd) }
it "should call api.check_access" do
api.should_receive(:check_access).with('git-upload-pack', 'gitlab-ci.git', key_id, '_any', 'ssh')
api.should_receive(:check_access).with('git-upload-pack', nil, 'gitlab-ci.git', key_id, '_any', 'ssh')
end
it "should disallow access and log the attempt if check_access returns false status" do
api.stub(check_access: GitAccessStatus.new(false, 'denied', nil))
api.stub(check_access: GitAccessStatus.new(false, 'denied', nil, nil))
message = "gitlab-shell: Access denied for git command <git-upload-pack gitlab-ci.git> "
message << "by user with key #{key_id}."
$logger.should_receive(:warn).with(message)
......@@ -295,10 +296,24 @@ describe GitlabShell do
describe :exec_cmd do
let(:shell) { GitlabShell.new(key_id) }
before { Kernel.stub(:exec) }
let(:env) do
{
'HOME' => ENV['HOME'],
'PATH' => ENV['PATH'],
'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'],
'LANG' => ENV['LANG'],
'GL_ID' => key_id,
'GL_PROTOCOL' => 'ssh',
'GL_REPOSITORY' => gl_repository
}
end
before do
Kernel.stub(:exec)
shell.gl_repository = gl_repository
end
it "uses Kernel::exec method" do
Kernel.should_receive(:exec).with(kind_of(Hash), 1, 2, unsetenv_others: true).once
Kernel.should_receive(:exec).with(env, 1, 2, unsetenv_others: true).once
shell.send :exec_cmd, 1, 2
end
......@@ -307,7 +322,7 @@ describe GitlabShell do
end
it "allows one argument if it is an array" do
Kernel.should_receive(:exec).with(kind_of(Hash), [1, 2], unsetenv_others: true).once
Kernel.should_receive(:exec).with(env, [1, 2], unsetenv_others: true).once
shell.send :exec_cmd, [1, 2]
end
......@@ -347,7 +362,7 @@ describe GitlabShell do
expect($logger).to receive(:warn).
with("gitlab-shell: is configured to trace git commands with #{git_trace_log_file.inspect} but an absolute path needs to be provided")
Kernel.should_receive(:exec).with(kind_of(Hash), [1, 2], unsetenv_others: true).once
Kernel.should_receive(:exec).with(env, [1, 2], unsetenv_others: true).once
shell.send :exec_cmd, [1, 2]
end
end
......@@ -371,7 +386,7 @@ describe GitlabShell do
expect($logger).to receive(:warn).
with("gitlab-shell: is configured to trace git commands with #{git_trace_log_file.inspect} but it's not possible to write in that path Permission denied")
Kernel.should_receive(:exec).with(kind_of(Hash), [1, 2], unsetenv_others: true).once
Kernel.should_receive(:exec).with(env, [1, 2], unsetenv_others: true).once
shell.send :exec_cmd, [1, 2]
end
end
......
......@@ -5,7 +5,7 @@ http_interactions:
uri: https://dev.gitlab.org/api/v4/internal/notify_post_receive
body:
encoding: US-ASCII
string: repo_path=%2Fpath%2Fto%2Fmy%2Frepo.git&secret_token=a123
string: gl_repository=project-1&repo_path=%2Fpath%2Fto%2Fmy%2Frepo.git&secret_token=a123
headers:
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
......@@ -27,18 +27,18 @@ http_interactions:
Content-Type:
- application/json
Date:
- Fri, 10 Feb 2017 17:06:53 GMT
- Fri, 28 Apr 2017 20:07:14 GMT
Etag:
- W/"99914b932bd37a50b983c5e7c90ae93b"
Vary:
- Origin
X-Request-Id:
- cfefede6-9400-4ca5-a61d-2a519405295c
- 404a3fa8-28ca-40d6-acbb-7923c7fbf342
X-Runtime:
- '20.623406'
- '3.988681'
body:
encoding: UTF-8
string: "{}"
http_version:
recorded_at: Fri, 10 Feb 2017 17:06:53 GMT
recorded_at: Fri, 28 Apr 2017 20:07:14 GMT
recorded_with: VCR 2.4.0
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment