Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Curl do
describe ".init" do
before { Ethon::Curl.send(:class_variable_set, :@@initialized, false) }
context "when global_init fails" do
it "raises global init error" do
expect(Ethon::Curl).to receive(:global_init).and_return(1)
expect{ Ethon::Curl.init }.to raise_error(Ethon::Errors::GlobalInit)
end
end
context "when global_init works" do
before { expect(Ethon::Curl).to receive(:global_init).and_return(0) }
it "doesn't raises global init error" do
expect{ Ethon::Curl.init }.to_not raise_error
end
it "logs" do
expect(Ethon.logger).to receive(:debug)
Ethon::Curl.init
end
end
context "when global_cleanup is called" do
before { expect(Ethon::Curl).to receive(:global_cleanup) }
it "logs" do
expect(Ethon.logger).to receive(:debug).twice
Ethon::Curl.init
Ethon::Curl.cleanup
end
end
end
end
@@ -0,0 +1,81 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Callbacks do
let!(:easy) { Ethon::Easy.new }
describe "#set_callbacks" do
before do
expect(Ethon::Curl).to receive(:set_option).exactly(3).times
end
it "sets write-, debug-, and headerfunction" do
easy.set_callbacks
end
it "resets @response_body" do
easy.set_callbacks
expect(easy.instance_variable_get(:@response_body)).to eq("")
end
it "resets @response_headers" do
easy.set_callbacks
expect(easy.instance_variable_get(:@response_headers)).to eq("")
end
it "resets @debug_info" do
easy.set_callbacks
expect(easy.instance_variable_get(:@debug_info).to_a).to eq([])
end
end
describe "#progress_callback" do
it "returns 0" do
expect(easy.progress_callback.call(0,1,1,1,1)).to be(0)
end
end
describe "#body_write_callback" do
let(:body_write_callback) { easy.instance_variable_get(:@body_write_callback) }
let(:stream) { double(:read_string => "") }
context "when body returns not :abort" do
it "returns number bigger than 0" do
expect(body_write_callback.call(stream, 1, 1, nil) > 0).to be(true)
end
end
context "when body returns :abort" do
before do
easy.on_body.clear
easy.on_body { :abort }
end
let(:body_write_callback) { easy.instance_variable_get(:@body_write_callback) }
it "returns -1 to indicate abort to libcurl" do
expect(body_write_callback.call(stream, 1, 1, nil)).to eq(-1)
end
end
end
describe "#header_write_callback" do
let(:header_write_callback) { easy.instance_variable_get(:@header_write_callback) }
let(:stream) { double(:read_string => "") }
context "when header returns not :abort" do
it "returns number bigger than 0" do
expect(header_write_callback.call(stream, 1, 1, nil) > 0).to be(true)
end
end
context "when header returns :abort" do
before do
easy.on_headers.clear
easy.on_headers { :abort }
end
let(:header_write_callback) { easy.instance_variable_get(:@header_write_callback) }
it "returns -1 to indicate abort to libcurl" do
expect(header_write_callback.call(stream, 1, 1, nil)).to eq(-1)
end
end
end
end
@@ -0,0 +1,54 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::DebugInfo do
let(:easy) { Ethon::Easy.new }
before do
easy.url = "http://localhost:3001/"
easy.perform
end
describe "#debug_info" do
context "when verbose is not set to true" do
it "does not save any debug info after a request" do
expect(easy.debug_info.to_a.length).to eq(0)
expect(easy.debug_info.to_h.values.flatten.length).to eq(0)
end
end
context "when verbose is set to true" do
before do
easy.verbose = true
easy.perform
end
after do
easy.verbose = false
easy.reset
end
it "saves debug info after a request" do
expect(easy.debug_info.to_a.length).to be > 0
end
it "saves request headers" do
expect(easy.debug_info.header_out.join).to include('GET / HTTP/1.1')
end
it "saves response headers" do
expect(easy.debug_info.header_in.length).to be > 0
expect(easy.response_headers).to include(easy.debug_info.header_in.join)
end
it "saves incoming data" do
expect(easy.debug_info.data_in.length).to be > 0
expect(easy.response_body).to include(easy.debug_info.data_in.join)
end
it "saves debug text" do
expect(easy.debug_info.text.length).to be > 0
end
end
end
end
@@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Informations do
describe "#supports_asynch_dns?" do
it "returns boolean" do
expect([true, false].include? Ethon::Easy.supports_asynch_dns?).to be_truthy
end
end
describe "#supports_zlib?" do
it "returns boolean" do
expect([true, false].include? Ethon::Easy.supports_zlib?).to be_truthy
end
end
describe "#supports_timeout_ms?" do
it "returns boolean" do
expect([true, false].include? Ethon::Easy.supports_timeout_ms?).to be_truthy
end
end
end
@@ -0,0 +1,104 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Form do
let(:hash) { {} }
let!(:easy) { Ethon::Easy.new }
let(:form) { Ethon::Easy::Form.new(easy, hash) }
describe ".new" do
it "assigns attribute to @params" do
expect(form.instance_variable_get(:@params)).to eq(hash)
end
end
describe "#first" do
it "returns a pointer" do
expect(form.first).to be_a(FFI::Pointer)
end
end
describe "#last" do
it "returns a pointer" do
expect(form.first).to be_a(FFI::Pointer)
end
end
describe "#multipart?" do
before { form.instance_variable_set(:@query_pairs, pairs) }
context "when query_pairs contains string values" do
let(:pairs) { [['a', '1'], ['b', '2']] }
it "returns false" do
expect(form.multipart?).to be_falsey
end
end
context "when query_pairs contains file" do
let(:pairs) { [['a', '1'], ['b', ['path', 'encoding', 'abs_path']]] }
it "returns true" do
expect(form.multipart?).to be_truthy
end
end
context "when options contains multipart=true" do
before { form.instance_variable_set(:@multipart, true) }
let(:pairs) { [['a', '1'], ['b', '2']] }
it "returns true" do
expect(form.multipart?).to be_truthy
end
end
end
describe "#materialize" do
before { form.instance_variable_set(:@query_pairs, pairs) }
context "when query_pairs contains string values" do
let(:pairs) { [['a', '1']] }
it "adds params to form" do
expect(Ethon::Curl).to receive(:formadd)
form.materialize
end
end
context "when query_pairs contains nil" do
let(:pairs) { [['a', nil]] }
it "adds params to form" do
expect(Ethon::Curl).to receive(:formadd)
form.materialize
end
end
context "when query_pairs contains file" do
let(:pairs) { [['a', ["file", "type", "path/file"]]] }
it "adds file to form" do
expect(Ethon::Curl).to receive(:formadd)
form.materialize
end
end
context "when query_pairs contains file and string values" do
let(:pairs) { [['a', ["file", "type", "path/file"]], ['b', '1']] }
it "adds file to form" do
expect(Ethon::Curl).to receive(:formadd).twice
form.materialize
end
end
context "when query_pairs contains file, string and int values" do
let(:pairs) { [['a', ["file", "type", "path/file"]], ['b', '1'], ['c', 1]] }
it "adds file to form" do
expect(Ethon::Curl).to receive(:formadd).exactly(3).times
form.materialize
end
end
end
end
@@ -0,0 +1,79 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Header do
let(:easy) { Ethon::Easy.new }
describe "#headers=" do
let(:headers) { { 'User-Agent' => 'Ethon' } }
it "sets header" do
expect_any_instance_of(Ethon::Easy).to receive(:set_callbacks)
expect(Ethon::Curl).to receive(:set_option)
easy.headers = headers
end
context "when requesting" do
before do
easy.headers = headers
easy.url = "http://localhost:3001"
easy.perform
end
it "sends" do
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon"')
end
context "when header value contains null byte" do
let(:headers) { { 'User-Agent' => "Ethon\0" } }
it "escapes" do
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon\\\\0"')
end
end
context "when header value has leading whitespace" do
let(:headers) { { 'User-Agent' => " Ethon" } }
it "removes" do
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon"')
end
end
context "when header value has traiing whitespace" do
let(:headers) { { 'User-Agent' => "Ethon " } }
it "removes" do
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon"')
end
end
end
end
describe "#compose_header" do
it "has space in between" do
expect(easy.compose_header('a', 'b')).to eq('a: b')
end
context "when value is a symbol" do
it "works" do
expect{ easy.compose_header('a', :b) }.to_not raise_error
end
end
end
describe "#header_list" do
context "when no set_headers" do
it "returns nil" do
expect(easy.header_list).to eq(nil)
end
end
context "when set_headers" do
it "returns pointer to header list" do
easy.headers = {'User-Agent' => 'Custom'}
expect(easy.header_list).to be_a(FFI::Pointer)
end
end
end
end
@@ -0,0 +1,177 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Http::Custom do
let(:easy) { Ethon::Easy.new }
let(:url) { "http://localhost:3001/" }
let(:params) { nil }
let(:form) { nil }
let(:custom) { described_class.new("PURGE", url, {:params => params, :body => form}) }
describe "#setup" do
context "when nothing" do
it "sets url" do
custom.setup(easy)
expect(easy.url).to eq(url)
end
it "makes a custom request" do
custom.setup(easy)
easy.perform
expect(easy.response_body).to include('"REQUEST_METHOD":"PURGE"')
end
end
context "when params" do
let(:params) { {:a => "1&"} }
it "attaches escaped to url" do
custom.setup(easy)
expect(easy.url).to eq("#{url}?a=1%26")
end
context "when requesting" do
before do
easy.headers = { 'Expect' => '' }
custom.setup(easy)
easy.perform
end
it "is a custom verb" do
expect(easy.response_body).to include('"REQUEST_METHOD":"PURGE"')
end
it "does not use application/x-www-form-urlencoded content type" do
expect(easy.response_body).to_not include('"CONTENT_TYPE":"application/x-www-form-urlencoded"')
end
it "requests parameterized url" do
expect(easy.response_body).to include('"REQUEST_URI":"http://localhost:3001/?a=1%26"')
end
end
end
context "when body" do
context "when multipart" do
let(:form) { {:a => File.open(__FILE__, 'r')} }
it "sets httppost" do
expect(easy).to receive(:httppost=)
custom.setup(easy)
end
context "when requesting" do
before do
easy.headers = { 'Expect' => '' }
custom.setup(easy)
easy.perform
end
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "is a custom verb" do
expect(easy.response_body).to include('"REQUEST_METHOD":"PURGE"')
end
it "uses multipart/form-data content type" do
expect(easy.response_body).to include('"CONTENT_TYPE":"multipart/form-data')
end
it "submits a body" do
expect(easy.response_body).to match('"body":".+"')
end
it "submits the data" do
expect(easy.response_body).to include('"filename":"custom_spec.rb"')
end
end
end
context "when not multipart" do
let(:form) { {:a => "1&b=2"} }
let(:encoded) { "a=1%26b%3D2" }
it "sets escaped copypostfields" do
expect(easy).to receive(:copypostfields=).with(encoded)
custom.setup(easy)
end
it "sets postfieldsize" do
expect(easy).to receive(:postfieldsize=).with(encoded.bytesize)
custom.setup(easy)
end
context "when requesting" do
before do
easy.headers = { 'Expect' => '' }
custom.setup(easy)
easy.perform
end
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "is a custom verb" do
expect(easy.response_body).to include('"REQUEST_METHOD":"PURGE"')
end
it "uses multipart/form-data content type" do
expect(easy.response_body).to include('"CONTENT_TYPE":"application/x-www-form-urlencoded')
end
it "submits a body" do
expect(easy.response_body).to match('"body":"a=1%26b%3D2"')
end
it "submits the data" do
expect(easy.response_body).to include('"rack.request.form_hash":{"a":"1&b=2"}')
end
end
end
context "when string" do
let(:form) { "{a: 1}" }
context "when requesting" do
before do
easy.headers = { 'Expect' => '' }
custom.setup(easy)
easy.perform
end
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "sends string" do
expect(easy.response_body).to include('"body":"{a: 1}"')
end
end
end
end
context "when params and body" do
let(:form) { {:a => "1"} }
let(:params) { {:b => "2"} }
context "when requesting" do
before do
easy.headers = { 'Expect' => '' }
custom.setup(easy)
easy.perform
end
it "url contains params" do
expect(easy.response_body).to include('"REQUEST_URI":"http://localhost:3001/?b=2"')
end
it "body contains form" do
expect(easy.response_body).to include('"body":"a=1"')
end
end
end
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Http::Delete do
let(:easy) { Ethon::Easy.new }
let(:url) { "http://localhost:3001/" }
let(:params) { nil }
let(:form) { nil }
let(:delete) { described_class.new(url, {:params => params, :body => form}) }
context "when requesting" do
before do
delete.setup(easy)
easy.perform
end
it "makes a delete request" do
expect(easy.response_body).to include('"REQUEST_METHOD":"DELETE"')
end
end
end
@@ -0,0 +1,126 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Http::Get do
let(:easy) { Ethon::Easy.new }
let(:url) { "http://localhost:3001/" }
let(:params) { nil }
let(:form) { nil }
let(:options) { {} }
let(:get) { described_class.new(url, {:params => params, :body => form}.merge(options)) }
describe "#setup" do
it "sets url" do
get.setup(easy)
expect(easy.url).to eq(url)
end
context "when body" do
let(:form) { { :a => 1 } }
it "sets customrequest" do
expect(easy).to receive(:customrequest=).with("GET")
get.setup(easy)
end
end
context "when no body" do
it "doesn't set customrequest" do
expect(easy).to receive(:customrequest=).never
get.setup(easy)
end
end
context "when requesting" do
before do
get.setup(easy)
easy.perform
end
context "when url already contains params" do
let(:url) { "http://localhost:3001/?query=here" }
let(:params) { {:a => "1&b=2"} }
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "is a get request" do
expect(easy.response_body).to include('"REQUEST_METHOD":"GET"')
end
it "requests parameterized url" do
expect(easy.effective_url).to eq("http://localhost:3001/?query=here&a=1%26b%3D2")
end
end
context "when params and no body" do
let(:params) { {:a => "1&b=2"} }
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "is a get request" do
expect(easy.response_body).to include('"REQUEST_METHOD":"GET"')
end
it "requests parameterized url" do
expect(easy.effective_url).to eq("http://localhost:3001/?a=1%26b%3D2")
end
end
context "when params and body" do
let(:params) { {:a => "1&b=2"} }
let(:form) { {:b => "2"} }
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "is a get request" do
expect(easy.response_body).to include('"REQUEST_METHOD":"GET"')
end
it "requests parameterized url" do
expect(easy.effective_url).to eq("http://localhost:3001/?a=1%26b%3D2")
end
end
context "with :escape" do
let(:params) { {:a => "1&b=2"} }
context 'missing' do
it "escapes values" do
expect(easy.url).to eq("#{url}?a=1%26b%3D2")
end
end
context 'nil' do
let(:options) { {:escape => nil} }
it "escapes values" do
expect(easy.url).to eq("#{url}?a=1%26b%3D2")
end
end
context 'true' do
let(:options) { {:escape => true} }
it "escapes values" do
expect(easy.url).to eq("#{url}?a=1%26b%3D2")
end
end
context 'false' do
let(:options) { {:escape => false} }
it "sends raw values" do
expect(easy.url).to eq("#{url}?a=1&b=2")
end
end
end
end
end
end
@@ -0,0 +1,80 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Http::Head do
let(:easy) { Ethon::Easy.new }
let(:url) { "http://localhost:3001/" }
let(:params) { nil }
let(:form) { nil }
let(:head) { described_class.new(url, {:params => params, :body => form}) }
describe "#setup" do
context "when nothing" do
it "sets nobody" do
expect(easy).to receive(:nobody=).with(true)
head.setup(easy)
end
it "sets url" do
head.setup(easy)
expect(easy.url).to eq(url)
end
end
context "when params" do
let(:params) { {:a => "1&b=2"} }
it "sets nobody" do
expect(easy).to receive(:nobody=).with(true)
head.setup(easy)
end
it "attaches escaped to url" do
head.setup(easy)
expect(easy.url).to eq("#{url}?a=1%26b%3D2")
end
context "when requesting" do
before do
head.setup(easy)
easy.perform
end
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "has no body" do
expect(easy.response_body).to be_empty
end
it "requests parameterized url" do
expect(easy.effective_url).to eq("http://localhost:3001/?a=1%26b%3D2")
end
context "when url already contains params" do
let(:url) { "http://localhost:3001/?query=here" }
it "requests parameterized url" do
expect(easy.effective_url).to eq("http://localhost:3001/?query=here&a=1%26b%3D2")
end
end
end
end
context "when body" do
let(:form) { {:a => 1} }
context "when requesting" do
before do
head.setup(easy)
easy.perform
end
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
end
end
end
end
@@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Http::Options do
let(:easy) { Ethon::Easy.new }
let(:url) { "http://localhost:3001/" }
let(:params) { nil }
let(:form) { nil }
let(:options) { described_class.new(url, {:params => params, :body => form}) }
describe "#setup" do
it "sets customrequest" do
expect(easy).to receive(:customrequest=).with("OPTIONS")
options.setup(easy)
end
it "sets url" do
options.setup(easy)
expect(easy.url).to eq(url)
end
context "when requesting" do
let(:params) { {:a => "1&b=2"} }
before do
options.setup(easy)
easy.perform
end
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "is a options request" do
expect(easy.response_body).to include('"REQUEST_METHOD":"OPTIONS"')
end
it "requests parameterized url" do
expect(easy.effective_url).to eq("http://localhost:3001/?a=1%26b%3D2")
end
context "when url already contains params" do
let(:url) { "http://localhost:3001/?query=here" }
it "requests parameterized url" do
expect(easy.effective_url).to eq("http://localhost:3001/?query=here&a=1%26b%3D2")
end
end
end
end
end
@@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Http::Patch do
let(:easy) { Ethon::Easy.new }
let(:url) { "http://localhost:3001/" }
let(:params) { nil }
let(:form) { nil }
let(:patch) { described_class.new(url, {:params => params, :body => form}) }
describe "#setup" do
it "sets customrequest" do
expect(easy).to receive(:customrequest=).with("PATCH")
patch.setup(easy)
end
it "sets url" do
patch.setup(easy)
expect(easy.url).to eq(url)
end
context "when requesting" do
let(:params) { {:a => "1&b=2"} }
before do
patch.setup(easy)
easy.perform
end
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "is a patch request" do
expect(easy.response_body).to include('"REQUEST_METHOD":"PATCH"')
end
it "requests parameterized url" do
expect(easy.effective_url).to eq("http://localhost:3001/?a=1%26b%3D2")
end
context "when url already contains params" do
let(:url) { "http://localhost:3001/?query=here" }
it "requests parameterized url" do
expect(easy.effective_url).to eq("http://localhost:3001/?query=here&a=1%26b%3D2")
end
end
end
end
end
@@ -0,0 +1,317 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Http::Post do
let(:easy) { Ethon::Easy.new }
let(:url) { "http://localhost:3001/" }
let(:params) { nil }
let(:form) { nil }
let(:options) { Hash.new }
let(:post) { described_class.new(url, options.merge({:params => params, :body => form})) }
describe "#setup" do
context "when nothing" do
it "sets url" do
post.setup(easy)
expect(easy.url).to eq(url)
end
it "sets postfield_size" do
expect(easy).to receive(:postfieldsize=).with(0)
post.setup(easy)
end
it "sets copy_postfields" do
expect(easy).to receive(:copypostfields=).with("")
post.setup(easy)
end
it "makes a post request" do
post.setup(easy)
easy.perform
expect(easy.response_body).to include('"REQUEST_METHOD":"POST"')
end
end
context "when params" do
let(:params) { {:a => "1&"} }
it "attaches escaped to url" do
post.setup(easy)
expect(easy.url).to eq("#{url}?a=1%26")
end
context "with arrays" do
let(:params) { {:a => %w( foo bar )} }
context "by default" do
it "encodes them with indexes" do
post.setup(easy)
expect(easy.url).to eq("#{url}?a%5B0%5D=foo&a%5B1%5D=bar")
end
end
context "when params_encoding is :rack" do
let(:options) { {:params_encoding => :rack} }
it "encodes them without indexes" do
post.setup(easy)
expect(easy.url).to eq("#{url}?a%5B%5D=foo&a%5B%5D=bar")
end
end
end
context "with :escape" do
context 'missing' do
it "escapes values" do
post.setup(easy)
expect(easy.url).to eq("#{url}?a=1%26")
end
end
context 'nil' do
let(:options) { {:escape => nil} }
it "escapes values" do
post.setup(easy)
expect(easy.url).to eq("#{url}?a=1%26")
end
end
context 'true' do
let(:options) { {:escape => true} }
it "escapes values" do
post.setup(easy)
expect(easy.url).to eq("#{url}?a=1%26")
end
end
context 'false' do
let(:options) { {:escape => false} }
it "sends raw values" do
post.setup(easy)
expect(easy.url).to eq("#{url}?a=1&")
end
end
end
it "sets postfieldsize" do
expect(easy).to receive(:postfieldsize=).with(0)
post.setup(easy)
end
it "sets copypostfields" do
expect(easy).to receive(:copypostfields=).with("")
post.setup(easy)
end
context "when requesting" do
let(:postredir) { nil }
before do
easy.headers = { 'Expect' => '' }
post.setup(easy)
easy.postredir = postredir
easy.followlocation = true
easy.perform
end
it "is a post" do
expect(easy.response_body).to include('"REQUEST_METHOD":"POST"')
end
it "uses application/x-www-form-urlencoded content type" do
expect(easy.response_body).to include('"CONTENT_TYPE":"application/x-www-form-urlencoded"')
end
it "requests parameterized url" do
expect(easy.response_body).to include('"REQUEST_URI":"http://localhost:3001/?a=1%26"')
end
context "when redirection" do
let(:url) { "localhost:3001/redirect" }
context "when no postredirs" do
it "is a get" do
expect(easy.response_body).to include('"REQUEST_METHOD":"GET"')
end
end
unless ENV['TRAVIS']
context "when postredirs" do
let(:postredir) { :post_all }
it "is a post" do
expect(easy.response_body).to include('"REQUEST_METHOD":"POST"')
end
end
end
end
end
end
context "when body" do
context "when multipart" do
let(:form) { {:a => File.open(__FILE__, 'r')} }
it "sets httppost" do
expect(easy).to receive(:httppost=)
post.setup(easy)
end
context "when requesting" do
before do
easy.headers = { 'Expect' => '' }
post.setup(easy)
easy.perform
end
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "is a post" do
expect(easy.response_body).to include('"REQUEST_METHOD":"POST"')
end
it "uses multipart/form-data content type" do
expect(easy.response_body).to include('"CONTENT_TYPE":"multipart/form-data')
end
it "submits a body" do
expect(easy.response_body).to match('"body":".+"')
end
it "submits the data" do
expect(easy.response_body).to include('"filename":"post_spec.rb"')
end
end
end
context "when not multipart" do
let(:form) { {:a => "1&b=2"} }
let(:encoded) { "a=1%26b%3D2" }
it "sets escaped copypostfields" do
expect(easy).to receive(:copypostfields=).with(encoded)
post.setup(easy)
end
it "sets postfieldsize" do
expect(easy).to receive(:postfieldsize=).with(encoded.bytesize)
post.setup(easy)
end
context "when requesting" do
before do
easy.headers = { 'Expect' => '' }
post.setup(easy)
easy.perform
end
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "is a post" do
expect(easy.response_body).to include('"REQUEST_METHOD":"POST"')
end
it "uses multipart/form-data content type" do
expect(easy.response_body).to include('"CONTENT_TYPE":"application/x-www-form-urlencoded')
end
it "submits a body" do
expect(easy.response_body).to match('"body":"a=1%26b%3D2"')
end
it "submits the data" do
expect(easy.response_body).to include('"rack.request.form_hash":{"a":"1&b=2"}')
end
end
end
context "when string" do
let(:form) { "{a: 1}" }
context "when requesting" do
before do
easy.headers = { 'Expect' => '' }
post.setup(easy)
easy.perform
end
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "sends string" do
expect(easy.response_body).to include('"body":"{a: 1}"')
end
end
end
context "when binary with null bytes" do
let(:form) { [1, 0, 1].pack('c*') }
context "when requesting" do
before do
easy.headers = { 'Expect' => '' }
post.setup(easy)
easy.perform
end
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "sends binary data" do
expect(easy.response_body).to include('"body":"\\u0001\\u0000\\u0001"')
end
end
end
context "when arrays" do
let(:form) { {:a => %w( foo bar )} }
context "by default" do
it "sets copypostfields with indexed, escaped representation" do
expect(easy).to receive(:copypostfields=).with('a%5B0%5D=foo&a%5B1%5D=bar')
post.setup(easy)
end
end
context "when params_encoding is :rack" do
let(:options) { {:params_encoding => :rack} }
it "sets copypostfields with non-indexed, escaped representation" do
expect(easy).to receive(:copypostfields=).with('a%5B%5D=foo&a%5B%5D=bar')
post.setup(easy)
end
end
end
end
context "when params and body" do
let(:form) { {:a => "1"} }
let(:params) { {:b => "2"} }
context "when requesting" do
before do
easy.headers = { 'Expect' => '' }
post.setup(easy)
easy.perform
end
it "url contains params" do
expect(easy.response_body).to include('"REQUEST_URI":"http://localhost:3001/?b=2"')
end
it "body contains form" do
expect(easy.response_body).to include('"body":"a=1"')
end
end
end
end
end
@@ -0,0 +1,168 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Http::Put do
let(:easy) { Ethon::Easy.new }
let(:url) { "http://localhost:3001/" }
let(:params) { nil }
let(:form) { nil }
let(:options) { Hash.new }
let(:put) { described_class.new(url, options.merge({:params => params, :body => form})) }
describe "#setup" do
context "when nothing" do
it "sets url" do
put.setup(easy)
expect(easy.url).to eq(url)
end
it "sets upload" do
expect(easy).to receive(:upload=).with(true)
put.setup(easy)
end
it "sets infilesize" do
expect(easy).to receive(:infilesize=).with(0)
put.setup(easy)
end
context "when requesting" do
it "makes a put request" do
put.setup(easy)
easy.perform
expect(easy.response_body).to include('"REQUEST_METHOD":"PUT"')
end
end
end
context "when params" do
let(:params) { {:a => "1&"} }
it "attaches escaped to url" do
put.setup(easy)
expect(easy.url).to eq("#{url}?a=1%26")
end
context "with arrays" do
let(:params) { {:a => %w( foo bar )} }
context "by default" do
it "encodes them with indexes" do
put.setup(easy)
expect(easy.url).to eq("#{url}?a%5B0%5D=foo&a%5B1%5D=bar")
end
end
context "when params_encoding is :rack" do
let(:options) { {:params_encoding => :rack} }
it "encodes them without indexes" do
put.setup(easy)
expect(easy.url).to eq("#{url}?a%5B%5D=foo&a%5B%5D=bar")
end
end
end
it "sets upload" do
expect(easy).to receive(:upload=).with(true)
put.setup(easy)
end
it "sets infilesize" do
expect(easy).to receive(:infilesize=).with(0)
put.setup(easy)
end
context "when requesting" do
before do
put.setup(easy)
easy.perform
end
it "makes a put request" do
expect(easy.response_body).to include('"REQUEST_METHOD":"PUT"')
end
end
end
context "when body" do
let(:form) { {:a => "1&b=2"} }
it "sets infilesize" do
expect(easy).to receive(:infilesize=).with(11)
put.setup(easy)
end
it "sets readfunction" do
expect(easy).to receive(:readfunction)
put.setup(easy)
end
it "sets upload" do
expect(easy).to receive(:upload=).with(true)
put.setup(easy)
end
context "when requesting" do
context "sending string body" do
before do
easy.headers = { 'Expect' => '' }
put.setup(easy)
easy.perform
end
it "makes a put request" do
expect(easy.response_body).to include('"REQUEST_METHOD":"PUT"')
end
it "submits a body" do
expect(easy.response_body).to include('"body":"a=1%26b%3D2"')
end
end
context "when injecting a file as body" do
let(:file) { File.open(__FILE__) }
let(:easy) do
e = Ethon::Easy.new(:url => url, :upload => true)
e.set_read_callback(file)
e.infilesize = File.size(file.path)
e
end
before do
easy.headers = { 'Expect' => '' }
easy.perform
end
it "submits file" do
expect(easy.response_body).to include("injecting")
end
end
end
context "when arrays" do
let(:form) { {:a => %w( foo bar )} }
before do
put.setup(easy)
easy.perform
end
context "by default" do
it "submits an indexed, escaped representation" do
expect(easy.response_body).to include('"body":"a%5B0%5D=foo&a%5B1%5D=bar"')
end
end
context "when params_encoding is :rack" do
let(:options) { {:params_encoding => :rack} }
it "submits an non-indexed, escaped representation" do
expect(easy.response_body).to include('"body":"a%5B%5D=foo&a%5B%5D=bar"')
end
end
end
end
context "when params and body"
end
end
@@ -0,0 +1,64 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Http do
let(:easy) { Ethon::Easy.new }
describe "#http_request" do
let(:url) { "http://localhost:3001/" }
let(:action_name) { :get }
let(:options) { {} }
let(:get) { double(:setup) }
let(:get_class) { Ethon::Easy::Http::Get }
it "instanciates action" do
expect(get).to receive(:setup)
expect(get_class).to receive(:new).and_return(get)
easy.http_request(url, action_name, options)
end
context "when requesting" do
[ :get, :post, :put, :delete, :head, :patch, :options ].map do |action|
it "returns ok" do
easy.http_request(url, action, options)
easy.perform
expect(easy.return_code).to be(:ok)
end
unless action == :head
it "makes a #{action.to_s.upcase} request" do
easy.http_request(url, action, options)
easy.perform
expect(easy.response_body).to include("\"REQUEST_METHOD\":\"#{action.to_s.upcase}\"")
end
it "streams the response body from the #{action.to_s.upcase} request" do
bytes_read = 0
easy.on_body { |chunk, response| bytes_read += chunk.bytesize }
easy.http_request(url, action, options)
easy.perform
content_length = ((easy.response_headers =~ /Content-Length: (\d+)/) && $1.to_i)
expect(bytes_read).to eq(content_length)
expect(easy.response_body).to eq("")
end
it "notifies when headers are ready" do
headers = []
easy.on_headers { |r| headers << r.response_headers }
easy.http_request(url, action, options)
easy.perform
expect(headers).to eq([easy.response_headers])
expect(headers.first).to match(/Content-Length: (\d+)/)
end
end
end
it "makes requests with custom HTTP verbs" do
easy.http_request(url, :purge, options)
easy.perform
expect(easy.response_body).to include(%{"REQUEST_METHOD":"PURGE"})
end
end
end
end
@@ -0,0 +1,126 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Informations do
let(:easy) { Ethon::Easy.new }
before do
easy.url = "http://localhost:3001"
easy.perform
end
describe "#httpauth_avail" do
it "returns" do
expect(easy.httpauth_avail).to be
end
end
describe "#total_time" do
it "returns float" do
expect(easy.total_time).to be_a(Float)
end
end
describe "#starttransfer_time" do
it "returns float" do
expect(easy.starttransfer_time).to be_a(Float)
end
end
describe "#appconnect_time" do
it "returns float" do
expect(easy.appconnect_time).to be_a(Float)
end
end
describe "#pretransfer_time" do
it "returns float" do
expect(easy.pretransfer_time).to be_a(Float)
end
end
describe "#connect_time" do
it "returns float" do
expect(easy.connect_time).to be_a(Float)
end
end
describe "#namelookup_time" do
it "returns float" do
expect(easy.namelookup_time).to be_a(Float)
end
end
describe "#redirect_time" do
it "returns float" do
expect(easy.redirect_time).to be_a(Float)
end
end
describe "#effective_url" do
it "returns url" do
expect(easy.effective_url).to match(/^http:\/\/localhost:3001\/?/)
end
end
describe "#primary_ip" do
it "returns localhost" do
expect(easy.primary_ip).to match(/::1|127\.0\.0\.1/)
end
end
describe "#response_code" do
it "returns 200" do
expect(easy.response_code).to eq(200)
end
end
describe "#redirect_count" do
it "returns 0" do
expect(easy.redirect_count).to eq(0)
end
end
describe "#redirect_url" do
it "returns nil as there is no redirect" do
expect(easy.redirect_url).to be(nil)
end
end
describe "#request_size" do
it "returns 53" do
expect(easy.request_size).to eq(53)
end
end
describe "#supports_zlib?" do
it "returns true" do
expect(Kernel).to receive(:warn)
expect(easy.supports_zlib?).to be_truthy
end
end
describe "#size_upload" do
it "returns float" do
expect(easy.size_upload).to be_a(Float)
end
end
describe "#size_download" do
it "returns float" do
expect(easy.size_download).to be_a(Float)
end
end
describe "#speed_upload" do
it "returns float" do
expect(easy.speed_upload).to be_a(Float)
end
end
describe "#speed_download" do
it "returns float" do
expect(easy.speed_download).to be_a(Float)
end
end
end
@@ -0,0 +1,47 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Mirror do
let(:options) { nil }
let(:mirror) { described_class.new(options) }
describe "::INFORMATIONS_TO_LOG" do
[
:return_code, :response_code, :response_body, :response_headers,
:total_time, :starttransfer_time, :appconnect_time,
:pretransfer_time, :connect_time, :namelookup_time, :redirect_time,
:size_upload, :size_download, :speed_upload, :speed_upload,
:effective_url, :primary_ip, :redirect_count, :redirect_url, :debug_info
].each do |name|
it "contains #{name}" do
expect(described_class::INFORMATIONS_TO_MIRROR).to include(name)
end
end
end
describe "#to_hash" do
let(:options) { {:return_code => 1} }
it "returns mirror as hash" do
expect(mirror.to_hash).to eq(options)
end
end
describe "#log_informations" do
let(:options) { {:return_code => 1} }
it "returns hash" do
expect(mirror.log_informations).to be_a(Hash)
end
it "only calls methods that exist" do
described_class::INFORMATIONS_TO_LOG.each do |method_name|
expect(mirror.respond_to? method_name).to eql(true)
end
end
it "includes return code" do
expect(mirror.log_informations).to include(options)
end
end
end
@@ -0,0 +1,271 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Operations do
let(:easy) { Ethon::Easy.new }
describe "#handle" do
it "returns a pointer" do
expect(easy.handle).to be_a(FFI::Pointer)
end
end
describe "#perform" do
let(:url) { nil }
let(:timeout) { nil }
let(:connect_timeout) { nil }
let(:follow_location) { nil }
let(:max_redirs) { nil }
let(:user_pwd) { nil }
let(:http_auth) { nil }
let(:headers) { nil }
let(:protocols) { nil }
let(:redir_protocols) { nil }
let(:username) { nil }
let(:password) { nil }
before do
Ethon.logger.level = Logger::DEBUG
easy.url = url
easy.timeout = timeout
easy.connecttimeout = connect_timeout
easy.followlocation = follow_location
easy.maxredirs = max_redirs
easy.httpauth = http_auth
easy.headers = headers
easy.protocols = protocols
easy.redir_protocols = redir_protocols
if user_pwd
easy.userpwd = user_pwd
else
easy.username = username
easy.password = password
end
easy.perform
end
it "calls Curl.easy_perform" do
expect(Ethon::Curl).to receive(:easy_perform)
easy.perform
end
it "calls Curl.easy_cleanup" do
expect_any_instance_of(FFI::AutoPointer).to receive(:free)
easy.cleanup
end
it "logs" do
expect(Ethon.logger).to receive(:debug)
easy.perform
end
it "doesn't log after completing because completing could reset" do
easy.on_complete{ expect(Ethon.logger).to receive(:debug).never }
easy.perform
end
context "when url" do
let(:url) { "http://localhost:3001/" }
it "returns ok" do
expect(easy.return_code).to eq(:ok)
end
it "sets response body" do
expect(easy.response_body).to be
end
it "sets response headers" do
expect(easy.response_headers).to be
end
context "when request timed out" do
let(:url) { "http://localhost:3001/?delay=1" }
let(:timeout) { 1 }
it "returns operation_timedout" do
expect(easy.return_code).to eq(:operation_timedout)
end
end
context "when connection timed out" do
let(:url) { "http://localhost:3009" }
let(:connect_timeout) { 1 }
it "returns couldnt_connect" do
expect(easy.return_code).to eq(:couldnt_connect)
end
end
context "when no follow location" do
let(:url) { "http://localhost:3001/redirect" }
let(:follow_location) { false }
it "doesn't follow" do
expect(easy.response_code).to eq(302)
expect(easy.redirect_url).to eq("http://localhost:3001/")
end
end
context "when follow location" do
let(:url) { "http://localhost:3001/redirect" }
let(:follow_location) { true }
it "follows" do
expect(easy.response_code).to eq(200)
expect(easy.redirect_url).to eq(nil)
end
context "when infinite redirect loop" do
let(:url) { "http://localhost:3001/bad_redirect" }
let(:max_redirs) { 5 }
context "when max redirect set" do
it "follows only x times" do
expect(easy.response_code).to eq(302)
expect(easy.redirect_url).to eq("http://localhost:3001/bad_redirect")
end
end
end
end
context "when user agent" do
let(:headers) { { 'User-Agent' => 'Ethon' } }
it "sets" do
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon"')
end
end
end
context "when auth url" do
before { easy.url = url }
context "when basic auth" do
let(:url) { "http://localhost:3001/auth_basic/username/password" }
context "when no user_pwd" do
it "returns 401" do
expect(easy.response_code).to eq(401)
end
end
context "when invalid user_pwd" do
let(:user_pwd) { "invalid:invalid" }
it "returns 401" do
expect(easy.response_code).to eq(401)
end
end
context "when valid user_pwd" do
let(:user_pwd) { "username:password" }
it "returns 200" do
expect(easy.response_code).to eq(200)
end
end
context "when user and password" do
let(:username) { "username" }
let(:password) { "password" }
it "returns 200" do
expect(easy.response_code).to eq(200)
end
end
end
context "when ntlm" do
let(:url) { "http://localhost:3001/auth_ntlm" }
let(:http_auth) { :ntlm }
context "when no user_pwd" do
it "returns 401" do
expect(easy.response_code).to eq(401)
end
end
context "when user_pwd" do
let(:user_pwd) { "username:password" }
it "returns 200" do
expect(easy.response_code).to eq(200)
end
end
end
end
context "when protocols" do
context "when asking for a allowed url" do
let(:url) { "http://localhost:3001" }
let(:protocols) { :http }
it "returns ok" do
expect(easy.return_code).to be(:ok)
end
end
context "when asking for a not allowed url" do
let(:url) { "http://localhost:3001" }
let(:protocols) { :https }
it "returns unsupported_protocol" do
expect(easy.return_code).to be(:unsupported_protocol)
end
end
end
context "when multiple protocols" do
context "when asking for a allowed url" do
let(:protocols) { [:http, :https] }
context "when http" do
let(:url) { "http://localhost:3001" }
it "returns ok for http" do
expect(easy.return_code).to be(:ok)
end
end
context "when https" do
let(:url) { "https://localhost:3001" }
it "returns ssl_connect_error for https" do
expect(easy.return_code).to be(:ssl_connect_error)
end
end
end
context "when asking for a not allowed url" do
let(:url) { "ssh://localhost" }
let(:protocols) { [:https, :http] }
it "returns unsupported_protocol" do
expect(easy.return_code).to be(:unsupported_protocol)
end
end
end
context "when redir_protocols" do
context "when redirecting to a not allowed url" do
let(:url) { "http://localhost:3001/redirect" }
let(:follow_location) { true }
let(:redir_protocols) { :https }
it "returns unsupported_protocol" do
expect(easy.return_code).to be(:unsupported_protocol)
end
end
end
context "when no url" do
it "returns url_malformat" do
expect(easy.perform).to eq(:url_malformat)
end
end
end
end
@@ -0,0 +1,193 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Options do
let(:easy) { Ethon::Easy.new }
[
:accept_encoding, :cainfo, :capath, :connecttimeout, :connecttimeout_ms, :cookie,
:cookiejar, :cookiefile, :copypostfields, :customrequest, :dns_cache_timeout,
:followlocation, :forbid_reuse, :http_version, :httpauth, :httpget, :httppost,
:infilesize, :interface, :keypasswd, :maxredirs, :nobody, :nosignal,
:postfieldsize, :postredir, :protocols, :proxy, :proxyauth, :proxyport, :proxytype,
:proxyuserpwd, :readdata, :readfunction, :redir_protocols, :ssl_verifyhost,
:ssl_verifypeer, :sslcert, :sslcerttype, :sslkey, :sslkeytype, :sslversion,
:timeout, :timeout_ms, :unrestricted_auth, :upload, :url, :useragent,
:userpwd, :verbose, :pipewait, :dns_shuffle_addresses, :path_as_is
].each do |name|
describe "#{name}=" do
it "responds_to" do
expect(easy).to respond_to("#{name}=")
end
it "sets option" do
expect_any_instance_of(Ethon::Easy).to receive(:set_callbacks)
expect(Ethon::Curl).to receive(:set_option).with(name, anything, anything)
value = case name
when :http_version
:httpv1_0
when :httpauth
:basic
when :protocols, :redir_protocols
:http
when :postredir
:post_301
when :proxytype
:http
when :sslversion
:default
when :httppost
FFI::Pointer::NULL
else
1
end
easy.method("#{name}=").call(value)
end
end
end
describe '#escape?' do
context 'by default' do
it 'returns true' do
expect(easy.escape?).to be_truthy
end
end
context 'when #escape=nil' do
it 'returns true' do
easy.escape = nil
expect(easy.escape?).to be_truthy
end
end
context 'when #escape=true' do
it 'returns true' do
easy.escape = true
expect(easy.escape?).to be_truthy
end
end
context 'when #escape=false' do
it 'returns true' do
easy.escape = false
expect(easy.escape?).to be_falsey
end
end
end
describe '#multipart?' do
context 'by default' do
it 'returns false' do
expect(easy.multipart?).to be_falsey
end
end
context 'when #multipart=nil' do
it 'returns false' do
easy.multipart = nil
expect(easy.multipart?).to be_falsey
end
end
context 'when #multipart=true' do
it 'returns true' do
easy.multipart = true
expect(easy.multipart?).to be_truthy
end
end
context 'when #multipart=false' do
it 'returns false' do
easy.multipart = false
expect(easy.multipart?).to be_falsey
end
end
end
describe "#httppost=" do
it "raises unless given a FFI::Pointer" do
expect{ easy.httppost = 1 }.to raise_error(Ethon::Errors::InvalidValue)
end
end
context "when requesting" do
let(:url) { "localhost:3001" }
let(:timeout) { nil }
let(:timeout_ms) { nil }
let(:connecttimeout) { nil }
let(:connecttimeout_ms) { nil }
let(:userpwd) { nil }
before do
easy.url = url
easy.timeout = timeout
easy.timeout_ms = timeout_ms
easy.connecttimeout = connecttimeout
easy.connecttimeout_ms = connecttimeout_ms
easy.userpwd = userpwd
easy.perform
end
context "when userpwd" do
context "when contains /" do
let(:url) { "localhost:3001/auth_basic/test/te%2Fst" }
let(:userpwd) { "test:te/st" }
it "works" do
expect(easy.response_code).to eq(200)
end
end
end
context "when timeout" do
let(:timeout) { 1 }
context "when request takes longer" do
let(:url) { "localhost:3001?delay=2" }
it "times out" do
expect(easy.return_code).to eq(:operation_timedout)
end
end
end
context "when connecttimeout" do
let(:connecttimeout) { 1 }
context "when cannot connect" do
let(:url) { "localhost:3002" }
it "times out" do
expect(easy.return_code).to eq(:couldnt_connect)
end
end
end
if Ethon::Easy.supports_timeout_ms?
context "when timeout_ms" do
let(:timeout_ms) { 100 }
context "when request takes longer" do
let(:url) { "localhost:3001?delay=1" }
it "times out" do
expect(easy.return_code).to eq(:operation_timedout)
end
end
end
context "when connecttimeout_ms" do
let(:connecttimeout_ms) { 100 }
context "when cannot connect" do
let(:url) { "localhost:3002" }
it "times out" do
# this can either lead to a timeout or couldnt connect depending on which happens first
expect([:couldnt_connect, :operation_timedout]).to include(easy.return_code)
end
end
end
end
end
end
@@ -0,0 +1,235 @@
# encoding: utf-8
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Queryable do
let(:hash) { {} }
let!(:easy) { Ethon::Easy.new }
let(:params) { Ethon::Easy::Params.new(easy, hash) }
describe "#to_s" do
context "when query_pairs empty" do
before { params.instance_variable_set(:@query_pairs, []) }
it "returns empty string" do
expect(params.to_s).to eq("")
end
end
context "when query_pairs not empty" do
context "when escape" do
before do
params.escape = true
end
{
'!' => '%21', '*' => '%2A', "'" => '%27', '(' => '%28',
')' => '%29', ';' => '%3B', ':' => '%3A', '@' => '%40',
'&' => '%26', '=' => '%3D', '+' => '%2B', '$' => '%24',
',' => '%2C', '/' => '%2F', '?' => '%3F', '#' => '%23',
'[' => '%5B', ']' => '%5D',
'<' => '%3C', '>' => '%3E', '"' => '%22', '{' => '%7B',
'}' => '%7D', '|' => '%7C', '\\' => '%5C', '`' => '%60',
'^' => '%5E', '%' => '%25', ' ' => '%20', "\0" => '%00',
'まつもと' => '%E3%81%BE%E3%81%A4%E3%82%82%E3%81%A8',
}.each do |value, percent|
it "turns #{value.inspect} into #{percent}" do
params.instance_variable_set(:@query_pairs, [[:a, value]])
expect(params.to_s).to eq("a=#{percent}")
end
end
{
'.' => '%2E', '-' => '%2D', '_' => '%5F', '~' => '%7E',
}.each do |value, percent|
it "leaves #{value.inspect} instead of turning into #{percent}" do
params.instance_variable_set(:@query_pairs, [[:a, value]])
expect(params.to_s).to eq("a=#{value}")
end
end
end
context "when no escape" do
before { params.instance_variable_set(:@query_pairs, [[:a, 1], [:b, 2]]) }
it "returns concatenated query string" do
expect(params.to_s).to eq("a=1&b=2")
end
end
end
context "when query_pairs contains a string" do
before { params.instance_variable_set(:@query_pairs, ["{a: 1}"]) }
it "returns correct string" do
expect(params.to_s).to eq("{a: 1}")
end
end
end
describe "#build_query_pairs" do
let(:pairs) { params.method(:build_query_pairs).call(hash) }
context "when params is empty" do
it "returns empty array" do
expect(pairs).to eq([])
end
end
context "when params is string" do
let(:hash) { "{a: 1}" }
it "wraps it in an array" do
expect(pairs).to eq([hash])
end
end
context "when params is simple hash" do
let(:hash) { {:a => 1, :b => 2} }
it "transforms" do
expect(pairs).to include([:a, 1])
expect(pairs).to include([:b, 2])
end
end
context "when params is a nested hash" do
let(:hash) { {:a => 1, :b => {:c => 2}} }
it "transforms" do
expect(pairs).to include([:a, 1])
expect(pairs).to include(["b[c]", 2])
end
end
context "when params contains an array" do
let(:hash) { {:a => 1, :b => [2, 3]} }
context "by default" do
it "transforms" do
expect(pairs).to include([:a, 1])
expect(pairs).to include(["b[0]", 2])
expect(pairs).to include(["b[1]", 3])
end
end
context "when params_encoding is :rack" do
before { params.params_encoding = :rack }
it "transforms without indexes" do
expect(pairs).to include([:a, 1])
expect(pairs).to include(["b[]", 2])
expect(pairs).to include(["b[]", 3])
end
end
context "when params_encoding is :none" do
before { params.params_encoding = :none }
it "does no transformation" do
expect(pairs).to include([:a, 1])
expect(pairs).to include([:b, [2, 3]])
end
end
end
context "when params contains something nested in an array" do
context "when string" do
let(:hash) { {:a => {:b => ["hello", "world"]}} }
it "transforms" do
expect(pairs).to eq([["a[b][0]", "hello"], ["a[b][1]", "world"]])
end
end
context "when hash" do
let(:hash) { {:a => {:b => [{:c =>1}, {:d => 2}]}} }
it "transforms" do
expect(pairs).to eq([["a[b][0][c]", 1], ["a[b][1][d]", 2]])
end
end
context "when file" do
let(:file) { File.open("spec/spec_helper.rb") }
let(:file_info) { params.method(:file_info).call(file) }
let(:hash) { {:a => {:b => [file]}} }
let(:mime_type) { file_info[1] }
it "transforms" do
expect(pairs).to eq([["a[b][0]", file_info]])
end
context "when MIME" do
if defined?(MIME)
context "when mime type" do
it "sets mime type to text" do
expect(mime_type).to eq("application/x-ruby")
end
end
end
context "when no mime type" do
let(:file) { Tempfile.new("fubar") }
it "sets mime type to default application/octet-stream" do
expect(mime_type).to eq("application/octet-stream")
end
end
end
context "when no MIME" do
before { hide_const("MIME") }
it "sets mime type to default application/octet-stream" do
expect(mime_type).to eq("application/octet-stream")
end
end
end
end
context "when params contains file" do
let(:file) { Tempfile.new("fubar") }
let(:file_info) { params.method(:file_info).call(file) }
let(:hash) { {:a => 1, :b => file} }
it "transforms" do
expect(pairs).to include([:a, 1])
expect(pairs).to include([:b, file_info])
end
end
context "when params key contains a null byte" do
let(:hash) { {:a => "1\0" } }
it "preserves" do
expect(pairs).to eq([[:a, "1\0"]])
end
end
context "when params value contains a null byte" do
let(:hash) { {"a\0" => 1 } }
it "preserves" do
expect(pairs).to eq([["a\0", 1]])
end
end
end
describe "#empty?" do
context "when params empty" do
it "returns true" do
expect(params.empty?).to be_truthy
end
end
context "when params not empty" do
let(:hash) { {:a => 1} }
it "returns false" do
expect(params.empty?).to be_falsey
end
end
end
end
@@ -0,0 +1,152 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::ResponseCallbacks do
let(:easy) { Ethon::Easy.new }
[:on_complete, :on_headers, :on_body, :on_progress].each do |callback_type|
describe "##{callback_type}" do
it "responds" do
expect(easy).to respond_to("#{callback_type}")
end
context "when no block given" do
it "returns @#{callback_type}" do
expect(easy.send("#{callback_type}")).to eq([])
end
end
context "when block given" do
it "stores" do
easy.send(callback_type) { p 1 }
expect(easy.instance_variable_get("@#{callback_type}").size).to eq(1)
end
end
context "when multiple blocks given" do
it "stores" do
easy.send(callback_type) { p 1 }
easy.send(callback_type) { p 2 }
expect(easy.instance_variable_get("@#{callback_type}").size).to eq(2)
end
end
end
end
describe "#complete" do
before do
easy.on_complete {|r| String.new(r.url) }
end
it "executes blocks and passes self" do
expect(String).to receive(:new).with(easy.url)
easy.complete
end
context "when @on_complete nil" do
it "doesn't raise" do
easy.instance_variable_set(:@on_complete, nil)
expect{ easy.complete }.to_not raise_error
end
end
end
describe "#headers" do
before do
easy.on_headers {|r| String.new(r.url) }
end
it "executes blocks and passes self" do
expect(String).to receive(:new).with(easy.url)
easy.headers
end
context "when @on_headers nil" do
it "doesn't raise" do
easy.instance_variable_set(:@on_headers, nil)
expect{ easy.headers }.to_not raise_error
end
end
end
describe "#progress" do
context "when requesting for realz" do
it "executes callback" do
post = Ethon::Easy::Http::Post.new("http://localhost:3001", {:body => "bar=fu"})
post.setup(easy)
@called = false
@has_dltotal = false
@has_ultotal = false
easy.on_progress { @called = true }
easy.on_progress { |dltotal, _, _, _| @has_dltotal ||= true }
easy.on_progress { |_, _, ultotal, _| @has_ultotal ||= true }
easy.perform
expect(@called).to be true
expect(@has_dltotal).to be true
expect(@has_ultotal).to be true
end
end
context "when pretending" do
before do
@dltotal = nil
@dlnow = nil
@ultotal = nil
@ulnow = nil
easy.on_progress { |dltotal, dlnow, ultotal, ulnow| @dltotal = dltotal ; @dlnow = dlnow; @ultotal = ultotal; @ulnow = ulnow }
end
it "executes blocks and passes dltotal" do
easy.progress(1, 2, 3, 4)
expect(@dltotal).to eq(1)
end
it "executes blocks and passes dlnow" do
easy.progress(1, 2, 3, 4)
expect(@dlnow).to eq(2)
end
it "executes blocks and passes ultotal" do
easy.progress(1, 2, 3, 4)
expect(@ultotal).to eq(3)
end
it "executes blocks and passes ulnow" do
easy.progress(1, 2, 3, 4)
expect(@ulnow).to eq(4)
end
context "when @on_progress nil" do
it "doesn't raise" do
easy.instance_variable_set(:@on_progress, nil)
expect{ easy.progress(1, 2, 3, 4) }.to_not raise_error
end
end
end
end
describe "#body" do
before do
@chunk = nil
@r = nil
easy.on_body { |chunk, r| @chunk = chunk ; @r = r }
end
it "executes blocks and passes self" do
easy.body("the chunk")
expect(@r).to be(easy)
end
it "executes blocks and passes chunk" do
easy.body("the chunk")
expect(@chunk).to eq("the chunk")
end
context "when @on_body nil" do
it "doesn't raise" do
easy.instance_variable_set(:@on_body, nil)
expect{ easy.body("the chunk") }.to_not raise_error
end
end
end
end
@@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy::Util do
class Dummy
include Ethon::Easy::Util
end
let(:klass) { Dummy.new }
describe "escape_zero_byte" do
context "when value has no zero byte" do
let(:value) { "hello world" }
it "returns same value" do
expect(klass.escape_zero_byte(value)).to be(value)
end
end
context "when value has zero byte" do
let(:value) { "hello \0world" }
it "returns escaped" do
expect(klass.escape_zero_byte(value)).to eq("hello \\0world")
end
end
end
end
@@ -0,0 +1,203 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Easy do
let(:easy) { Ethon::Easy.new }
describe ".new" do
it "inits curl" do
expect(Ethon::Curl).to receive(:init)
easy
end
context "when options are empty" do
it "sets only callbacks" do
expect_any_instance_of(Ethon::Easy).to receive(:set_callbacks)
expect(Ethon::Easy).to receive(:set_option).never
easy
end
end
context "when options not empty" do
context "when followlocation is set" do
let(:options) { { :followlocation => true } }
let(:easy) { Ethon::Easy.new(options) }
it "sets followlocation" do
expect_any_instance_of(Ethon::Easy).to receive(:set_callbacks)
expect(Ethon::Curl).to receive(:set_option).with(:followlocation, true, anything)
easy
end
end
end
end
describe "#set_attributes" do
context "when options are empty" do
it "sets only callbacks" do
expect_any_instance_of(Ethon::Easy).to receive(:set_callbacks)
expect(Ethon::Easy).to receive(:set_option).never
easy
end
end
context "when options aren't empty" do
context "when valid key" do
it "sets" do
expect(easy).to receive(:verbose=).with(true)
easy.set_attributes({:verbose => true})
end
end
context "when invalid key" do
it "raises invalid option error" do
expect{ easy.set_attributes({:fubar => 1}) }.to raise_error(Ethon::Errors::InvalidOption)
end
end
end
end
describe "#reset" do
before { easy.url = "www.example.com" }
it "resets url" do
easy.reset
expect(easy.url).to be_nil
end
it "resets escape?" do
easy.escape = false
easy.reset
expect(easy.escape?).to be_truthy
end
it "resets hash" do
easy.reset
expect(easy.instance_variable_get(:@hash)).to be_nil
end
it "resets easy handle" do
expect(Ethon::Curl).to receive(:easy_reset)
easy.reset
end
it "resets on_complete" do
easy.on_complete { p 1 }
easy.reset
expect(easy.on_complete).to be_empty
end
it "resets on_headers" do
easy.on_headers { p 1 }
easy.reset
expect(easy.on_headers).to be_empty
end
it "resets on_body" do
easy.on_body { p 1 }
easy.reset
expect(easy.on_body).to be_empty
end
end
describe "#dup" do
let!(:easy) do
easy = Ethon::Easy.new
easy.url = "http://localhost:3001/"
easy.on_complete { 'on_complete' }
easy.on_headers { 'on_headers' }
easy.on_progress { 'on_progress' }
easy.response_body = String.new('test_body')
easy.response_headers = String.new('test_headers')
easy
end
let!(:e) { easy.dup }
it "sets a new handle" do
expect(e.handle).not_to eq(easy.handle)
end
it "preserves url" do
expect(e.url).to eq(easy.url)
end
it "preserves on_complete callback" do
expect(e.on_complete).to be(easy.on_complete)
end
it "preserves on_headers callback" do
expect(e.on_headers).to be(easy.on_headers)
end
it 'preserves body_write_callback of original handle' do
expect { easy.perform }.to change { easy.response_body }
expect { easy.perform }.not_to change { e.response_body }
end
it "preserves on_progress callback" do
expect(e.on_progress).to be(easy.on_progress)
end
it 'sets new body_write_callback of duplicated handle' do
expect { e.perform }.to change { e.response_body }
expect { e.perform }.not_to change { easy.response_body }
end
it 'preserves headers_write_callback of original handle' do
expect { easy.perform }.to change { easy.response_headers }
expect { easy.perform }.not_to change { e.response_headers }
end
it 'sets new headers_write_callback of duplicated handle' do
expect { e.perform }.to change { e.response_headers }
expect { e.perform }.not_to change { easy.response_headers }
end
it "resets response_body" do
expect(e.response_body).to be_empty
end
it "resets response_headers" do
expect(e.response_headers).to be_empty
end
it "sets response_body for duplicated Easy" do
e.perform
expect(e.response_body).not_to be_empty
end
it "sets response_headers for duplicated Easy" do
e.perform
expect(e.response_headers).not_to be_empty
end
it "preserves response_body for original Easy" do
e.perform
expect(easy.response_body).to eq('test_body')
end
it "preserves response_headers for original Easy" do
e.perform
expect(easy.response_headers).to eq('test_headers')
end
end
describe "#mirror" do
it "returns a Mirror" do
expect(easy.mirror).to be_a(Ethon::Easy::Mirror)
end
it "builds from easy" do
expect(Ethon::Easy::Mirror).to receive(:from_easy).with(easy)
easy.mirror
end
end
describe "#log_inspect" do
[ :url, :response_code, :return_code, :total_time ].each do |name|
it "contains #{name}" do
expect(easy.log_inspect).to match name.to_s
end
end
end
end
@@ -0,0 +1,14 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Libc do
describe "#getdtablesize", :if => !Ethon::Curl.windows? do
it "returns an integer" do
expect(Ethon::Libc.getdtablesize).to be_a(Integer)
end
it "returns bigger zero", :if => !Ethon::Curl.windows? do
expect(Ethon::Libc.getdtablesize).to_not be_zero
end
end
end
@@ -0,0 +1,22 @@
# frozen_string_literal: true
require "spec_helper"
describe Ethon::Loggable do
describe "#logger=" do
let(:logger) do
Logger.new($stdout).tap do |log|
log.level = Logger::INFO
end
end
before do
Ethon.logger = logger
end
it "sets the logger" do
expect(Ethon.logger).to eq(logger)
end
end
end
@@ -0,0 +1,298 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Multi::Operations do
let(:multi) { Ethon::Multi.new }
let(:easy) { Ethon::Easy.new }
let(:pointer) { FFI::MemoryPointer.new(:int) }
describe "#handle" do
it "returns a pointer" do
expect(multi.handle).to be_a(FFI::Pointer)
end
end
describe "#running_count" do
context "when hydra has no easy" do
it "returns nil" do
expect(multi.send(:running_count)).to be_nil
end
end
context "when hydra has easy" do
before do
easy.url = "http://localhost:3001/"
multi.add(easy)
multi.send(:trigger, pointer)
end
it "returns 1" do
expect(multi.send(:running_count)).to eq(1)
end
end
context "when hydra has more easys" do
let(:another_easy) { Ethon::Easy.new }
before do
easy.url = "http://localhost:3001/"
another_easy.url = "http://localhost:3001/"
multi.add(easy)
multi.add(another_easy)
multi.send(:trigger, pointer)
end
it "returns 2" do
expect(multi.send(:running_count)).to eq(2)
end
end
end
describe "#get_timeout" do
context "when code ok" do
let(:timeout) { 1 }
before do
expect(Ethon::Curl).to receive(:multi_timeout).and_return(:ok)
multi.instance_variable_set(:@timeout, double(:read_long => timeout))
end
it "doesn't raise" do
expect{ multi.send(:get_timeout) }.to_not raise_error
end
context "when timeout smaller zero" do
let(:timeout) { -1 }
it "returns 1" do
expect(multi.send(:get_timeout)).to eq(1)
end
end
context "when timeout bigger or equal zero" do
let(:timeout) { 2 }
it "returns timeout" do
expect(multi.send(:get_timeout)).to eq(timeout)
end
end
end
context "when code not ok" do
before { expect(Ethon::Curl).to receive(:multi_timeout).and_return(:not_ok) }
it "raises MultiTimeout error" do
expect{ multi.send(:get_timeout) }.to raise_error(Ethon::Errors::MultiTimeout)
end
end
end
describe "#set_fds" do
let(:timeout) { 1 }
let(:max_fd) { 1 }
context "when code ok" do
before { expect(Ethon::Curl).to receive(:multi_fdset).and_return(:ok) }
it "doesn't raise" do
expect{ multi.method(:set_fds).call(timeout) }.to_not raise_error
end
context "when max_fd -1" do
let(:max_fd) { -1 }
before do
multi.instance_variable_set(:@max_fd, double(:read_int => max_fd))
expect(multi).to receive(:sleep).with(0.001)
end
it "waits 100ms" do
multi.method(:set_fds).call(timeout)
end
end
context "when max_fd not -1" do
context "when code smaller zero" do
before { expect(Ethon::Curl).to receive(:select).and_return(-1) }
it "raises Select error" do
expect{ multi.method(:set_fds).call(timeout) }.to raise_error(Ethon::Errors::Select)
end
end
context "when code bigger or equal zero" do
before { expect(Ethon::Curl).to receive(:select).and_return(0) }
it "doesn't raise" do
expect{ multi.method(:set_fds).call(timeout) }.to_not raise_error
end
end
end
end
context "when code not ok" do
before { expect(Ethon::Curl).to receive(:multi_fdset).and_return(:not_ok) }
it "raises MultiFdset error" do
expect{ multi.method(:set_fds).call(timeout) }.to raise_error(Ethon::Errors::MultiFdset)
end
end
end
describe "#perform" do
context "when no easy handles" do
it "returns nil" do
expect(multi.perform).to be_nil
end
it "logs" do
expect(Ethon.logger).to receive(:debug).twice
multi.perform
end
end
context "when easy handle" do
before do
easy.url = "http://localhost:3001/"
multi.add(easy)
end
it "requests" do
multi.perform
end
it "sets easy" do
multi.perform
expect(easy.response_code).to eq(200)
end
end
context "when four easy handles" do
let(:easies) do
ary = []
4.times do
ary << another_easy = Ethon::Easy.new
another_easy.url = "http://localhost:3001/"
end
ary
end
before do
easies.each { |e| multi.add(e) }
multi.perform
end
it "sets response codes" do
expect(easies.all?{ |e| e.response_code == 200 }).to be_truthy
end
end
end
describe "#ongoing?" do
context "when easy_handles" do
before { multi.easy_handles << 1 }
context "when running_count not greater 0" do
before { multi.instance_variable_set(:@running_count, 0) }
it "returns true" do
expect(multi.method(:ongoing?).call).to be_truthy
end
end
context "when running_count greater 0" do
before { multi.instance_variable_set(:@running_count, 1) }
it "returns true" do
expect(multi.method(:ongoing?).call).to be_truthy
end
end
end
context "when no easy_handles" do
context "when running_count not greater 0" do
before { multi.instance_variable_set(:@running_count, 0) }
it "returns false" do
expect(multi.method(:ongoing?).call).to be_falsey
end
end
context "when running_count greater 0" do
before { multi.instance_variable_set(:@running_count, 1) }
it "returns true" do
expect(multi.method(:ongoing?).call).to be_truthy
end
end
end
end
describe "#init_vars" do
it "sets @timeout" do
expect(multi.instance_variable_get(:@timeout)).to be_a(FFI::MemoryPointer)
end
it "sets @timeval" do
expect(multi.instance_variable_get(:@timeval)).to be_a(Ethon::Curl::Timeval)
end
it "sets @fd_read" do
expect(multi.instance_variable_get(:@fd_read)).to be_a(Ethon::Curl::FDSet)
end
it "sets @fd_write" do
expect(multi.instance_variable_get(:@fd_write)).to be_a(Ethon::Curl::FDSet)
end
it "sets @fd_excep" do
expect(multi.instance_variable_get(:@fd_excep)).to be_a(Ethon::Curl::FDSet)
end
it "sets @max_fd" do
expect(multi.instance_variable_get(:@max_fd)).to be_a(FFI::MemoryPointer)
end
end
describe "#reset_fds" do
after { multi.method(:reset_fds).call }
it "resets @fd_read" do
expect(multi.instance_variable_get(:@fd_read)).to receive(:clear)
end
it "resets @fd_write" do
expect(multi.instance_variable_get(:@fd_write)).to receive(:clear)
end
it "resets @fd_excep" do
expect(multi.instance_variable_get(:@fd_excep)).to receive(:clear)
end
end
describe "#check" do
it { skip("untested") }
end
describe "#run" do
it { skip("untested") }
end
describe "#trigger" do
it "calls multi perform" do
expect(Ethon::Curl).to receive(:multi_perform)
multi.send(:trigger, pointer)
end
it "sets running count" do
multi.instance_variable_set(:@running_count, nil)
multi.send(:trigger, pointer)
expect(multi.instance_variable_get(:@running_count)).to_not be_nil
end
it "returns multi perform code" do
expect(Ethon::Curl).to receive(:multi_perform).and_return(:ok)
expect(multi.send(:trigger, pointer)).to eq(:ok)
end
end
end
@@ -0,0 +1,182 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Multi::Options do
let(:multi) { Ethon::Multi.new }
[
:maxconnects, :pipelining, :socketdata, :socketfunction,
:timerdata, :timerfunction, :max_total_connections
].each do |name|
describe "#{name}=" do
it "responds_to" do
expect(multi).to respond_to("#{name}=")
end
it "sets option" do
expect(Ethon::Curl).to receive(:set_option).with(name, anything, anything, anything)
multi.method("#{name}=").call(1)
end
end
end
context "socket_action mode" do
let(:multi) { Ethon::Multi.new(execution_mode: :socket_action) }
describe "#socketfunction callbacks" do
it "allows multi_code return values" do
calls = []
multi.socketfunction = proc do |handle, sock, what, userp, socketp|
calls << what
:ok
end
easy = Ethon::Easy.new
easy.url = "http://localhost:3001/?delay=1"
multi.add(easy)
expect(calls).to eq([])
5.times do
multi.socket_action
break unless calls.empty?
sleep 0.1
end
expect(calls.last).to eq(:in).or(eq(:out))
multi.delete(easy)
expect(calls.last).to eq(:remove)
end
it "allows integer return values (compatibility)" do
called = false
multi.socketfunction = proc do |handle, sock, what, userp, socketp|
called = true
0
end
easy = Ethon::Easy.new
easy.url = "http://localhost:3001/?delay=1"
multi.add(easy)
5.times do
multi.socket_action
break if called
sleep 0.1
end
multi.delete(easy)
expect(called).to be_truthy
end
it "errors on invalid return codes" do
called = false
multi.socketfunction = proc do |handle, sock, what, userp, socketp|
called = true
"hi"
end
easy = Ethon::Easy.new
easy.url = "http://localhost:3001/?delay=1"
multi.add(easy)
expect {
5.times do
multi.socket_action
break if called
sleep 0.1
end
}.to raise_error(ArgumentError)
expect { multi.delete(easy) }.to raise_error(ArgumentError)
end
end
describe "#timerfunction callbacks" do
it "allows multi_code return values" do
calls = []
multi.timerfunction = proc do |handle, timeout_ms, userp|
calls << timeout_ms
:ok
end
easy = Ethon::Easy.new
easy.url = "http://localhost:3001/?delay=1"
multi.add(easy)
expect(calls.last).to be >= 0 # adds an immediate timeout
multi.delete(easy)
expect(calls.last).to eq(-1) # cancels the timer
end
it "allows integer return values (compatibility)" do
called = false
multi.timerfunction = proc do |handle, timeout_ms, userp|
called = true
0
end
easy = Ethon::Easy.new
easy.url = "http://localhost:3001/?delay=1"
multi.add(easy)
multi.socket_action
multi.delete(easy)
expect(called).to be_truthy
end
it "errors on invalid return codes" do
called = false
multi.timerfunction = proc do |handle, timeout_ms, userp|
called = true
"hi"
end
easy = Ethon::Easy.new
easy.url = "http://localhost:3001/?delay=1"
expect { multi.add(easy) }.to raise_error(ArgumentError)
end
end
end
describe "#value_for" do
context "when option in bool" do
context "when value true" do
let(:value) { true }
it "returns 1" do
expect(multi.method(:value_for).call(value, :bool)).to eq(1)
end
end
context "when value false" do
let(:value) { false }
it "returns 0" do
expect(multi.method(:value_for).call(value, :bool)).to eq(0)
end
end
end
context "when value in int" do
let(:value) { "2" }
it "returns value casted to int" do
expect(multi.method(:value_for).call(value, :int)).to eq(2)
end
end
context "when value in unspecific_options" do
context "when value a string" do
let(:value) { "www.example.\0com" }
it "returns zero byte escaped string" do
expect(multi.method(:value_for).call(value, nil)).to eq("www.example.\\0com")
end
end
context "when value not a string" do
let(:value) { 1 }
it "returns value" do
expect(multi.method(:value_for).call(value, nil)).to eq(1)
end
end
end
end
end
@@ -0,0 +1,80 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Multi::Stack do
let(:multi) { Ethon::Multi.new }
let(:easy) { Ethon::Easy.new }
describe "#add" do
context "when easy already added" do
before { multi.add(easy) }
it "returns nil" do
expect(multi.add(easy)).to be_nil
end
end
context "when easy new" do
it "adds easy to multi" do
expect(Ethon::Curl).to receive(:multi_add_handle).and_return(:ok)
multi.add(easy)
end
it "adds easy to easy_handles" do
multi.add(easy)
expect(multi.easy_handles).to include(easy)
end
end
context "when multi_add_handle fails" do
it "raises multi add error" do
expect(Ethon::Curl).to receive(:multi_add_handle).and_return(:bad_easy_handle)
expect{ multi.add(easy) }.to raise_error(Ethon::Errors::MultiAdd)
end
end
context "when multi cleaned up before" do
it "raises multi add error" do
Ethon::Curl.multi_cleanup(multi.handle)
expect{ multi.add(easy) }.to raise_error(Ethon::Errors::MultiAdd)
end
end
end
describe "#delete" do
context "when easy in easy_handles" do
before { multi.add(easy) }
it "deletes easy from multi" do
expect(Ethon::Curl).to receive(:multi_remove_handle).and_return(:ok)
multi.delete(easy)
end
it "deletes easy from easy_handles" do
multi.delete(easy)
expect(multi.easy_handles).to_not include(easy)
end
end
context "when easy is not in easy_handles" do
it "does nothing" do
expect(Ethon::Curl).to receive(:multi_add_handle).and_return(:ok)
multi.add(easy)
end
it "adds easy to easy_handles" do
multi.add(easy)
expect(multi.easy_handles).to include(easy)
end
end
context "when multi_remove_handle fails" do
before { multi.add(easy) }
it "raises multi remove error" do
expect(Ethon::Curl).to receive(:multi_remove_handle).and_return(:bad_easy_handle)
expect{ multi.delete(easy) }.to raise_error(Ethon::Errors::MultiRemove)
end
end
end
end
@@ -0,0 +1,152 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ethon::Multi do
describe ".new" do
it "inits curl" do
expect(Ethon::Curl).to receive(:init)
Ethon::Multi.new
end
context "with default options" do
it "allows running #perform with the default execution_mode" do
Ethon::Multi.new.perform
end
it "refuses to run #socket_action" do
expect { Ethon::Multi.new.socket_action }.to raise_error(ArgumentError)
end
end
context "when options not empty" do
context "when pipelining is set" do
let(:options) { { :pipelining => true } }
it "sets pipelining" do
expect_any_instance_of(Ethon::Multi).to receive(:pipelining=).with(true)
Ethon::Multi.new(options)
end
end
context "when execution_mode option is :socket_action" do
let(:options) { { :execution_mode => :socket_action } }
let(:multi) { Ethon::Multi.new(options) }
it "refuses to run #perform" do
expect { multi.perform }.to raise_error(ArgumentError)
end
it "allows running #socket_action" do
multi.socket_action
end
end
end
end
describe "#socket_action" do
let(:options) { { :execution_mode => :socket_action } }
let(:select_state) { { :readers => [], :writers => [], :timeout => 0 } }
let(:multi) {
multi = Ethon::Multi.new(options)
multi.timerfunction = proc do |handle, timeout_ms, userp|
timeout_ms = nil if timeout_ms == -1
select_state[:timeout] = timeout_ms
:ok
end
multi.socketfunction = proc do |handle, sock, what, userp, socketp|
case what
when :remove
select_state[:readers].delete(sock)
select_state[:writers].delete(sock)
when :in
select_state[:readers].push(sock) unless select_state[:readers].include? sock
select_state[:writers].delete(sock)
when :out
select_state[:readers].delete(sock)
select_state[:writers].push(sock) unless select_state[:writers].include? sock
when :inout
select_state[:readers].push(sock) unless select_state[:readers].include? sock
select_state[:writers].push(sock) unless select_state[:writers].include? sock
else
raise ArgumentError, "invalid value for 'what' in socketfunction callback"
end
:ok
end
multi
}
def fds_to_ios(fds)
fds.map do |fd|
IO.for_fd(fd).tap { |io| io.autoclose = false }
end
end
def perform_socket_action_until_complete
multi.socket_action # start things off
while multi.ongoing?
readers, writers, _ = IO.select(
fds_to_ios(select_state[:readers]),
fds_to_ios(select_state[:writers]),
[],
select_state[:timeout]
)
to_notify = Hash.new { |hash, key| hash[key] = [] }
unless readers.nil?
readers.each do |reader|
to_notify[reader] << :in
end
end
unless writers.nil?
writers.each do |writer|
to_notify[writer] << :out
end
end
to_notify.each do |io, readiness|
multi.socket_action(io, readiness)
end
# if we didn't have anything to notify, then we timed out
multi.socket_action if to_notify.empty?
end
ensure
multi.easy_handles.dup.each do |h|
multi.delete(h)
end
end
it "supports an end-to-end request" do
easy = Ethon::Easy.new
easy.url = "http://localhost:3001/"
multi.add(easy)
perform_socket_action_until_complete
expect(multi.ongoing?).to eq(false)
end
it "supports multiple concurrent requests" do
handles = []
10.times do
easy = Ethon::Easy.new
easy.url = "http://localhost:3001/?delay=1"
multi.add(easy)
handles << easy
end
start = Time.now
perform_socket_action_until_complete
duration = Time.now - start
# these should have happened concurrently
expect(duration).to be < 2
expect(multi.ongoing?).to eq(false)
handles.each do |handle|
expect(handle.response_code).to eq(200)
end
end
end
end