ethon 0.5.12 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.gitignore +7 -0
- data/.rspec +3 -0
- data/.travis.yml +11 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +1 -1
- data/Guardfile +9 -0
- data/ethon.gemspec +26 -0
- data/lib/ethon/curl.rb +0 -12
- data/lib/ethon/curls/constants.rb +6 -22
- data/lib/ethon/curls/functions.rb +38 -41
- data/lib/ethon/curls/infos.rb +19 -0
- data/lib/ethon/curls/options.rb +416 -219
- data/lib/ethon/curls/settings.rb +1 -0
- data/lib/ethon/easy.rb +12 -18
- data/lib/ethon/easy/callbacks.rb +40 -6
- data/lib/ethon/easy/debug_info.rb +46 -0
- data/lib/ethon/easy/mirror.rb +39 -0
- data/lib/ethon/easy/options.rb +17 -1235
- data/lib/ethon/easy/queryable.rb +6 -8
- data/lib/ethon/easy/response_callbacks.rb +1 -1
- data/lib/ethon/version.rb +1 -1
- data/profile/benchmarks.rb +137 -0
- data/profile/memory_leaks.rb +113 -0
- data/profile/perf_spec_helper.rb +36 -0
- data/profile/support/memory_test_helpers.rb +75 -0
- data/profile/support/os_memory_leak_tracker.rb +47 -0
- data/profile/support/ruby_object_leak_tracker.rb +48 -0
- data/spec/ethon/curl_spec.rb +27 -0
- data/spec/ethon/easy/callbacks_spec.rb +31 -0
- data/spec/ethon/easy/debug_info_spec.rb +52 -0
- data/spec/ethon/easy/form_spec.rb +76 -0
- data/spec/ethon/easy/header_spec.rb +78 -0
- data/spec/ethon/easy/http/custom_spec.rb +176 -0
- data/spec/ethon/easy/http/delete_spec.rb +20 -0
- data/spec/ethon/easy/http/get_spec.rb +89 -0
- data/spec/ethon/easy/http/head_spec.rb +79 -0
- data/spec/ethon/easy/http/options_spec.rb +50 -0
- data/spec/ethon/easy/http/patch_spec.rb +50 -0
- data/spec/ethon/easy/http/post_spec.rb +220 -0
- data/spec/ethon/easy/http/put_spec.rb +124 -0
- data/spec/ethon/easy/http_spec.rb +44 -0
- data/spec/ethon/easy/informations_spec.rb +82 -0
- data/spec/ethon/easy/mirror_spec.rb +39 -0
- data/spec/ethon/easy/operations_spec.rb +251 -0
- data/spec/ethon/easy/options_spec.rb +135 -0
- data/spec/ethon/easy/queryable_spec.rb +188 -0
- data/spec/ethon/easy/response_callbacks_spec.rb +50 -0
- data/spec/ethon/easy/util_spec.rb +27 -0
- data/spec/ethon/easy_spec.rb +105 -0
- data/spec/ethon/libc_spec.rb +13 -0
- data/spec/ethon/loggable_spec.rb +21 -0
- data/spec/ethon/multi/operations_spec.rb +297 -0
- data/spec/ethon/multi/options_spec.rb +70 -0
- data/spec/ethon/multi/stack_spec.rb +79 -0
- data/spec/ethon/multi_spec.rb +21 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/localhost_server.rb +94 -0
- data/spec/support/server.rb +114 -0
- metadata +91 -31
- data/lib/ethon/curls/auth_types.rb +0 -25
- data/lib/ethon/curls/http_versions.rb +0 -22
- data/lib/ethon/curls/postredir.rb +0 -15
- data/lib/ethon/curls/protocols.rb +0 -36
- data/lib/ethon/curls/proxy_types.rb +0 -25
- data/lib/ethon/curls/ssl_versions.rb +0 -23
@@ -0,0 +1,48 @@
|
|
1
|
+
class RubyObjectLeakTracker
|
2
|
+
attr_reader :previous_count_hash, :current_count_hash
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@previous_count_hash = @current_count_hash = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def difference_between_runs(basis=@previous_count_hash)
|
9
|
+
@difference_between_runs ||= Hash[@current_count_hash.map do |object_class, count|
|
10
|
+
[object_class, count - (basis[object_class] || 0)]
|
11
|
+
end]
|
12
|
+
end
|
13
|
+
|
14
|
+
def total_difference_between_runs
|
15
|
+
difference_between_runs(@initial_count_hash).values.inject(0) { |sum, count| sum + count }
|
16
|
+
end
|
17
|
+
|
18
|
+
def capture_initial_memory_usage
|
19
|
+
capture_memory_usage
|
20
|
+
@initial_count_hash = @current_count_hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def capture_memory_usage
|
24
|
+
@difference_between_runs = nil
|
25
|
+
@previous_count_hash = @current_count_hash
|
26
|
+
|
27
|
+
class_to_count = Hash.new { |hash, key| hash[key] = 0 }
|
28
|
+
ObjectSpace.each_object { |obj| class_to_count[obj.class] += 1 }
|
29
|
+
|
30
|
+
sorted_class_to_count = class_to_count.sort_by { |k, v| -v }
|
31
|
+
@current_count_hash = Hash[sorted_class_to_count]
|
32
|
+
end
|
33
|
+
|
34
|
+
def dump_status(logger)
|
35
|
+
diff = difference_between_runs
|
36
|
+
most_used_objects = current_count_hash.to_a.sort_by(&:last).reverse[0, 20]
|
37
|
+
|
38
|
+
most_used_objects.each do |object_class, count|
|
39
|
+
delta = diff[object_class]
|
40
|
+
logger.add(log_level(delta), sprintf("\t%s: %d (%+d)", object_class, count, delta))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def log_level(delta)
|
46
|
+
delta > 0 ? Logger::WARN : Logger::DEBUG
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ethon::Curl do
|
4
|
+
describe ".init" do
|
5
|
+
before { Ethon::Curl.send(:class_variable_set, :@@initialized, false) }
|
6
|
+
|
7
|
+
context "when global_init fails" do
|
8
|
+
it "raises global init error" do
|
9
|
+
Ethon::Curl.should_receive(:global_init).and_return(1)
|
10
|
+
expect{ Ethon::Curl.init }.to raise_error(Ethon::Errors::GlobalInit)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "when global_init works" do
|
15
|
+
before { Ethon::Curl.should_receive(:global_init).and_return(0) }
|
16
|
+
|
17
|
+
it "doesn't raises global init error" do
|
18
|
+
expect{ Ethon::Curl.init }.to_not raise_error(Ethon::Errors::GlobalInit)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "logs" do
|
22
|
+
Ethon.logger.should_receive(:debug)
|
23
|
+
Ethon::Curl.init
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ethon::Easy::Callbacks do
|
4
|
+
let!(:easy) { Ethon::Easy.new }
|
5
|
+
|
6
|
+
describe "#set_callbacks" do
|
7
|
+
before do
|
8
|
+
Ethon::Curl.should_receive(:set_option).exactly(3).times
|
9
|
+
end
|
10
|
+
|
11
|
+
it "sets write- and headerfunction" do
|
12
|
+
easy.set_callbacks
|
13
|
+
end
|
14
|
+
|
15
|
+
it "resets @response_body" do
|
16
|
+
easy.set_callbacks
|
17
|
+
expect(easy.instance_variable_get(:@response_body)).to eq("")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "resets @response_headers" do
|
21
|
+
easy.set_callbacks
|
22
|
+
expect(easy.instance_variable_get(:@response_headers)).to eq("")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "resets @debug_info" do
|
26
|
+
easy.set_callbacks
|
27
|
+
expect(easy.instance_variable_get(:@debug_info).to_a).to eq([])
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ethon::Easy::DebugInfo do
|
4
|
+
let(:easy) { Ethon::Easy.new }
|
5
|
+
|
6
|
+
before do
|
7
|
+
easy.url = "http://localhost:3001/"
|
8
|
+
easy.perform
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#debug_info" do
|
12
|
+
context "when verbose is not set to true" do
|
13
|
+
it "does not save any debug info after a request" do
|
14
|
+
expect(easy.debug_info.to_a.length).to eq(0)
|
15
|
+
expect(easy.debug_info.to_h.values.flatten.length).to eq(0)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context "when verbose is set to true" do
|
20
|
+
before do
|
21
|
+
easy.verbose = true
|
22
|
+
easy.perform
|
23
|
+
end
|
24
|
+
|
25
|
+
after do
|
26
|
+
easy.reset
|
27
|
+
end
|
28
|
+
|
29
|
+
it "saves debug info after a request" do
|
30
|
+
expect(easy.debug_info.to_a.length).to be > 0
|
31
|
+
end
|
32
|
+
|
33
|
+
it "saves request headers" do
|
34
|
+
expect(easy.debug_info.header_out.join).to include('GET / HTTP/1.1')
|
35
|
+
end
|
36
|
+
|
37
|
+
it "saves response headers" do
|
38
|
+
expect(easy.debug_info.header_in.length).to be > 0
|
39
|
+
expect(easy.response_headers).to include(easy.debug_info.header_in.join)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "saves incoming data" do
|
43
|
+
expect(easy.debug_info.data_in.length).to be > 0
|
44
|
+
expect(easy.response_body).to include(easy.debug_info.data_in.join)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "saves debug text" do
|
48
|
+
expect(easy.debug_info.text.length).to be > 0
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ethon::Easy::Form do
|
4
|
+
let(:hash) { {} }
|
5
|
+
let!(:easy) { Ethon::Easy.new }
|
6
|
+
let(:form) { Ethon::Easy::Form.new(easy, hash) }
|
7
|
+
|
8
|
+
describe ".new" do
|
9
|
+
it "assigns attribute to @params" do
|
10
|
+
expect(form.instance_variable_get(:@params)).to eq(hash)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#first" do
|
15
|
+
it "returns a pointer" do
|
16
|
+
expect(form.first).to be_a(FFI::Pointer)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#last" do
|
21
|
+
it "returns a pointer" do
|
22
|
+
expect(form.first).to be_a(FFI::Pointer)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#multipart?" do
|
27
|
+
before { form.instance_variable_set(:@query_pairs, pairs) }
|
28
|
+
|
29
|
+
context "when query_pairs contains string values" do
|
30
|
+
let(:pairs) { [['a', '1'], ['b', '2']] }
|
31
|
+
|
32
|
+
it "returns false" do
|
33
|
+
expect(form.multipart?).to be_false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when query_pairs contains file" do
|
38
|
+
let(:pairs) { [['a', '1'], ['b', ['path', 'encoding', 'abs_path']]] }
|
39
|
+
|
40
|
+
it "returns true" do
|
41
|
+
expect(form.multipart?).to be_true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#materialize" do
|
47
|
+
before { form.instance_variable_set(:@query_pairs, pairs) }
|
48
|
+
|
49
|
+
context "when query_pairs contains string values" do
|
50
|
+
let(:pairs) { [['a', '1']] }
|
51
|
+
|
52
|
+
it "adds params to form" do
|
53
|
+
Ethon::Curl.should_receive(:formadd)
|
54
|
+
form.materialize
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "when query_pairs contains nil" do
|
59
|
+
let(:pairs) { [['a', nil]] }
|
60
|
+
|
61
|
+
it "adds params to form" do
|
62
|
+
Ethon::Curl.should_receive(:formadd)
|
63
|
+
form.materialize
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "when query_pairs contains file" do
|
68
|
+
let(:pairs) { [['a', ["file", "type", "path/file"]]] }
|
69
|
+
|
70
|
+
it "adds file to form" do
|
71
|
+
Ethon::Curl.should_receive(:formadd)
|
72
|
+
form.materialize
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ethon::Easy::Header do
|
4
|
+
let(:easy) { Ethon::Easy.new }
|
5
|
+
|
6
|
+
describe "#headers=" do
|
7
|
+
let(:headers) { { 'User-Agent' => 'Ethon' } }
|
8
|
+
|
9
|
+
it "sets header" do
|
10
|
+
Ethon::Easy.any_instance.should_receive(:set_callbacks)
|
11
|
+
Ethon::Curl.should_receive(:set_option)
|
12
|
+
easy.headers = headers
|
13
|
+
end
|
14
|
+
|
15
|
+
context "when requesting" do
|
16
|
+
before do
|
17
|
+
easy.headers = headers
|
18
|
+
easy.url = "http://localhost:3001"
|
19
|
+
easy.perform
|
20
|
+
end
|
21
|
+
|
22
|
+
it "sends" do
|
23
|
+
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon"')
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when header value contains null byte" do
|
27
|
+
let(:headers) { { 'User-Agent' => "Ethon\0" } }
|
28
|
+
|
29
|
+
it "escapes" do
|
30
|
+
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon\\\\0"')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "when header value has leading whitespace" do
|
35
|
+
let(:headers) { { 'User-Agent' => " Ethon" } }
|
36
|
+
|
37
|
+
it "removes" do
|
38
|
+
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon"')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "when header value has traiing whitespace" do
|
43
|
+
let(:headers) { { 'User-Agent' => "Ethon " } }
|
44
|
+
|
45
|
+
it "removes" do
|
46
|
+
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon"')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#compose_header" do
|
53
|
+
it "has space in between" do
|
54
|
+
expect(easy.compose_header('a', 'b')).to eq('a: b')
|
55
|
+
end
|
56
|
+
|
57
|
+
context "when value is a symbol" do
|
58
|
+
it "works" do
|
59
|
+
expect{ easy.compose_header('a', :b) }.to_not raise_error
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#header_list" do
|
65
|
+
context "when no set_headers" do
|
66
|
+
it "returns nil" do
|
67
|
+
expect(easy.header_list).to eq(nil)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when set_headers" do
|
72
|
+
it "returns pointer to header list" do
|
73
|
+
easy.headers = {'User-Agent' => 'Custom'}
|
74
|
+
expect(easy.header_list).to be_a(FFI::Pointer)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ethon::Easy::Http::Custom do
|
4
|
+
let(:easy) { Ethon::Easy.new }
|
5
|
+
let(:url) { "http://localhost:3001/" }
|
6
|
+
let(:params) { nil }
|
7
|
+
let(:form) { nil }
|
8
|
+
let(:custom) { described_class.new("PURGE", url, {:params => params, :body => form}) }
|
9
|
+
|
10
|
+
describe "#setup" do
|
11
|
+
context "when nothing" do
|
12
|
+
it "sets url" do
|
13
|
+
custom.setup(easy)
|
14
|
+
expect(easy.url).to eq(url)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "makes a custom request" do
|
18
|
+
custom.setup(easy)
|
19
|
+
easy.perform
|
20
|
+
expect(easy.response_body).to include('"REQUEST_METHOD":"PURGE"')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "when params" do
|
25
|
+
let(:params) { {:a => "1&"} }
|
26
|
+
|
27
|
+
it "attaches escaped to url" do
|
28
|
+
custom.setup(easy)
|
29
|
+
expect(easy.url).to eq("#{url}?a=1%26")
|
30
|
+
end
|
31
|
+
|
32
|
+
context "when requesting" do
|
33
|
+
before do
|
34
|
+
easy.headers = { 'Expect' => '' }
|
35
|
+
custom.setup(easy)
|
36
|
+
easy.perform
|
37
|
+
end
|
38
|
+
|
39
|
+
it "is a custom verb" do
|
40
|
+
expect(easy.response_body).to include('"REQUEST_METHOD":"PURGE"')
|
41
|
+
end
|
42
|
+
|
43
|
+
it "does not use application/x-www-form-urlencoded content type" do
|
44
|
+
expect(easy.response_body).to_not include('"CONTENT_TYPE":"application/x-www-form-urlencoded"')
|
45
|
+
end
|
46
|
+
|
47
|
+
it "requests parameterized url" do
|
48
|
+
expect(easy.response_body).to include('"REQUEST_URI":"http://localhost:3001/?a=1%26"')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "when body" do
|
54
|
+
context "when multipart" do
|
55
|
+
let(:form) { {:a => File.open(__FILE__, 'r')} }
|
56
|
+
|
57
|
+
it "sets httppost" do
|
58
|
+
easy.should_receive(:httppost=)
|
59
|
+
custom.setup(easy)
|
60
|
+
end
|
61
|
+
|
62
|
+
context "when requesting" do
|
63
|
+
before do
|
64
|
+
easy.headers = { 'Expect' => '' }
|
65
|
+
custom.setup(easy)
|
66
|
+
easy.perform
|
67
|
+
end
|
68
|
+
|
69
|
+
it "returns ok" do
|
70
|
+
expect(easy.return_code).to eq(:ok)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "is a custom verb" do
|
74
|
+
expect(easy.response_body).to include('"REQUEST_METHOD":"PURGE"')
|
75
|
+
end
|
76
|
+
|
77
|
+
it "uses multipart/form-data content type" do
|
78
|
+
expect(easy.response_body).to include('"CONTENT_TYPE":"multipart/form-data')
|
79
|
+
end
|
80
|
+
|
81
|
+
it "submits a body" do
|
82
|
+
expect(easy.response_body).to match('"body":".+"')
|
83
|
+
end
|
84
|
+
|
85
|
+
it "submits the data" do
|
86
|
+
expect(easy.response_body).to include('"filename":"custom_spec.rb"')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "when not multipart" do
|
92
|
+
let(:form) { {:a => "1&b=2"} }
|
93
|
+
let(:encoded) { "a=1%26b%3D2" }
|
94
|
+
|
95
|
+
it "sets escaped copypostfields" do
|
96
|
+
easy.should_receive(:copypostfields=).with(encoded)
|
97
|
+
custom.setup(easy)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "sets postfieldsize" do
|
101
|
+
easy.should_receive(:postfieldsize=).with{ |value| expect(value).to be(encoded.bytesize) }
|
102
|
+
custom.setup(easy)
|
103
|
+
end
|
104
|
+
|
105
|
+
context "when requesting" do
|
106
|
+
before do
|
107
|
+
easy.headers = { 'Expect' => '' }
|
108
|
+
custom.setup(easy)
|
109
|
+
easy.perform
|
110
|
+
end
|
111
|
+
|
112
|
+
it "returns ok" do
|
113
|
+
expect(easy.return_code).to eq(:ok)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "is a custom verb" do
|
117
|
+
expect(easy.response_body).to include('"REQUEST_METHOD":"PURGE"')
|
118
|
+
end
|
119
|
+
|
120
|
+
it "uses multipart/form-data content type" do
|
121
|
+
expect(easy.response_body).to include('"CONTENT_TYPE":"application/x-www-form-urlencoded')
|
122
|
+
end
|
123
|
+
|
124
|
+
it "submits a body" do
|
125
|
+
expect(easy.response_body).to match('"body":"a=1%26b%3D2"')
|
126
|
+
end
|
127
|
+
|
128
|
+
it "submits the data" do
|
129
|
+
expect(easy.response_body).to include('"rack.request.form_hash":{"a":"1&b=2"}')
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context "when string" do
|
135
|
+
let(:form) { "{a: 1}" }
|
136
|
+
|
137
|
+
context "when requesting" do
|
138
|
+
before do
|
139
|
+
easy.headers = { 'Expect' => '' }
|
140
|
+
custom.setup(easy)
|
141
|
+
easy.perform
|
142
|
+
end
|
143
|
+
|
144
|
+
it "returns ok" do
|
145
|
+
expect(easy.return_code).to eq(:ok)
|
146
|
+
end
|
147
|
+
|
148
|
+
it "sends string" do
|
149
|
+
expect(easy.response_body).to include('"body":"{a: 1}"')
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context "when params and body" do
|
156
|
+
let(:form) { {:a => "1"} }
|
157
|
+
let(:params) { {:b => "2"} }
|
158
|
+
|
159
|
+
context "when requesting" do
|
160
|
+
before do
|
161
|
+
easy.headers = { 'Expect' => '' }
|
162
|
+
custom.setup(easy)
|
163
|
+
easy.perform
|
164
|
+
end
|
165
|
+
|
166
|
+
it "url contains params" do
|
167
|
+
expect(easy.response_body).to include('"REQUEST_URI":"http://localhost:3001/?b=2"')
|
168
|
+
end
|
169
|
+
|
170
|
+
it "body contains form" do
|
171
|
+
expect(easy.response_body).to include('"body":"a=1"')
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|