raptor-io 0.0.1
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/LICENSE +30 -0
- data/README.md +51 -0
- data/lib/rack/handler/raptor-io.rb +130 -0
- data/lib/raptor-io.rb +11 -0
- data/lib/raptor-io/error.rb +19 -0
- data/lib/raptor-io/protocol.rb +6 -0
- data/lib/raptor-io/protocol/error.rb +10 -0
- data/lib/raptor-io/protocol/http.rb +34 -0
- data/lib/raptor-io/protocol/http/client.rb +685 -0
- data/lib/raptor-io/protocol/http/error.rb +16 -0
- data/lib/raptor-io/protocol/http/headers.rb +132 -0
- data/lib/raptor-io/protocol/http/message.rb +67 -0
- data/lib/raptor-io/protocol/http/request.rb +307 -0
- data/lib/raptor-io/protocol/http/request/manipulator.rb +117 -0
- data/lib/raptor-io/protocol/http/request/manipulators.rb +217 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticator.rb +110 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/basic.rb +36 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/digest.rb +135 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/negotiate.rb +69 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/ntlm.rb +29 -0
- data/lib/raptor-io/protocol/http/request/manipulators/redirect_follower.rb +65 -0
- data/lib/raptor-io/protocol/http/response.rb +166 -0
- data/lib/raptor-io/protocol/http/server.rb +446 -0
- data/lib/raptor-io/ruby.rb +4 -0
- data/lib/raptor-io/ruby/hash.rb +24 -0
- data/lib/raptor-io/ruby/ipaddr.rb +15 -0
- data/lib/raptor-io/ruby/openssl.rb +23 -0
- data/lib/raptor-io/ruby/string.rb +27 -0
- data/lib/raptor-io/socket.rb +175 -0
- data/lib/raptor-io/socket/comm.rb +143 -0
- data/lib/raptor-io/socket/comm/local.rb +94 -0
- data/lib/raptor-io/socket/comm/sapni.rb +75 -0
- data/lib/raptor-io/socket/comm/socks.rb +237 -0
- data/lib/raptor-io/socket/comm_chain.rb +30 -0
- data/lib/raptor-io/socket/error.rb +45 -0
- data/lib/raptor-io/socket/switch_board.rb +183 -0
- data/lib/raptor-io/socket/switch_board/route.rb +42 -0
- data/lib/raptor-io/socket/tcp.rb +231 -0
- data/lib/raptor-io/socket/tcp/ssl.rb +77 -0
- data/lib/raptor-io/socket/tcp_server.rb +16 -0
- data/lib/raptor-io/socket/tcp_server/ssl.rb +52 -0
- data/lib/raptor-io/socket/udp.rb +0 -0
- data/lib/raptor-io/version.rb +6 -0
- data/lib/tasks/yard.rake +26 -0
- data/spec/rack/handler/raptor_spec.rb +140 -0
- data/spec/raptor-io/protocol/http/client_spec.rb +671 -0
- data/spec/raptor-io/protocol/http/headers_spec.rb +189 -0
- data/spec/raptor-io/protocol/http/message_spec.rb +5 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticator_spec.rb +193 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/basic_spec.rb +32 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/digest_spec.rb +76 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/negotiate_spec.rb +52 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/ntlm_spec.rb +37 -0
- data/spec/raptor-io/protocol/http/request/manipulators/redirect_follower_spec.rb +51 -0
- data/spec/raptor-io/protocol/http/request/manipulators_spec.rb +202 -0
- data/spec/raptor-io/protocol/http/request_spec.rb +965 -0
- data/spec/raptor-io/protocol/http/response_spec.rb +236 -0
- data/spec/raptor-io/protocol/http/server_spec.rb +345 -0
- data/spec/raptor-io/ruby/hash_spec.rb +20 -0
- data/spec/raptor-io/ruby/string_spec.rb +20 -0
- data/spec/raptor-io/socket/comm/local_spec.rb +50 -0
- data/spec/raptor-io/socket/switch_board/route_spec.rb +49 -0
- data/spec/raptor-io/socket/switch_board_spec.rb +87 -0
- data/spec/raptor-io/socket/tcp/ssl_spec.rb +18 -0
- data/spec/raptor-io/socket/tcp_server/ssl_spec.rb +59 -0
- data/spec/raptor-io/socket/tcp_server_spec.rb +19 -0
- data/spec/raptor-io/socket/tcp_spec.rb +14 -0
- data/spec/raptor-io/socket_spec.rb +16 -0
- data/spec/raptor-io/version_spec.rb +10 -0
- data/spec/spec_helper.rb +56 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/manifoolators/fooer.rb +25 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/niccolo_machiavelli.rb +20 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/options_validator.rb +28 -0
- data/spec/support/fixtures/raptor/socket/ssl_server.crt +18 -0
- data/spec/support/fixtures/raptor/socket/ssl_server.key +15 -0
- data/spec/support/lib/path_helpers.rb +11 -0
- data/spec/support/lib/webserver_option_parser.rb +26 -0
- data/spec/support/lib/webservers.rb +120 -0
- data/spec/support/shared/contexts/with_ssl_server.rb +70 -0
- data/spec/support/shared/contexts/with_tcp_server.rb +58 -0
- data/spec/support/shared/examples/raptor/comm_examples.rb +26 -0
- data/spec/support/shared/examples/raptor/protocols/http/message.rb +106 -0
- data/spec/support/shared/examples/raptor/socket_examples.rb +135 -0
- data/spec/support/webservers/raptor/protocols/http/client.rb +100 -0
- data/spec/support/webservers/raptor/protocols/http/client_close_connection.rb +29 -0
- data/spec/support/webservers/raptor/protocols/http/client_https.rb +43 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/basic.rb +9 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/digest.rb +22 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/redirect_follower.rb +11 -0
- metadata +336 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'RaptorIO::Protocol::HTTP::Request::Manipulators::Authenticators::Negotiate' do
|
|
4
|
+
before :all do
|
|
5
|
+
WebServers.start :basic
|
|
6
|
+
@url = "http://#{ENV['IIS']}/negotiate/"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
before( :each ) do
|
|
10
|
+
RaptorIO::Protocol::HTTP::Request::Manipulators.reset
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
let(:client) { RaptorIO::Protocol::HTTP::Client.new }
|
|
14
|
+
|
|
15
|
+
it 'provides Negotiate authentication' do
|
|
16
|
+
pending if !ENV['IIS']
|
|
17
|
+
|
|
18
|
+
opts = {
|
|
19
|
+
mode: :sync, manipulators: {
|
|
20
|
+
'authenticators/negotiate' =>
|
|
21
|
+
{
|
|
22
|
+
username: 'msfadmin',
|
|
23
|
+
password: 'msfadmin'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
2.times do
|
|
29
|
+
client.get( @url, opts ).code.should == 200
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context 'on wrong credentials' do
|
|
34
|
+
it 'returns a 401' do
|
|
35
|
+
pending if !ENV['IIS']
|
|
36
|
+
|
|
37
|
+
opts = {
|
|
38
|
+
mode: :sync, manipulators: {
|
|
39
|
+
'authenticators/negotiate' =>
|
|
40
|
+
{
|
|
41
|
+
username: 'blah',
|
|
42
|
+
password: 'blah'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
2.times do
|
|
48
|
+
client.get( @url, opts ).code.should == 401
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'RaptorIO::Protocol::HTTP::Request::Manipulators::Authenticators::NTLM' do
|
|
4
|
+
before :all do
|
|
5
|
+
WebServers.start :basic
|
|
6
|
+
@url = "http://#{ENV['IIS']}/ntlm/"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
before( :each ) do
|
|
10
|
+
RaptorIO::Protocol::HTTP::Request::Manipulators.reset
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
let(:client) { RaptorIO::Protocol::HTTP::Client.new }
|
|
14
|
+
|
|
15
|
+
it 'provides NTLM authentication' do
|
|
16
|
+
pending if !ENV['IIS']
|
|
17
|
+
|
|
18
|
+
response = client.get( @url, mode: :sync )
|
|
19
|
+
response.code.should == 401
|
|
20
|
+
|
|
21
|
+
opts = {
|
|
22
|
+
mode: :sync, manipulators: {
|
|
23
|
+
'authenticators/ntlm' =>
|
|
24
|
+
{
|
|
25
|
+
username: 'msfadmin',
|
|
26
|
+
password: 'msfadmin',
|
|
27
|
+
response: response
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
2.times do
|
|
33
|
+
client.get( @url, opts ).code.should == 200
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'RaptorIO::Protocol::HTTP::Request::Manipulators::RedirectFollower' do
|
|
4
|
+
before :all do
|
|
5
|
+
WebServers.start :redirect_follower
|
|
6
|
+
@url = WebServers.url_for( :redirect_follower )
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
before( :each ) do
|
|
10
|
+
RaptorIO::Protocol::HTTP::Request::Manipulators.reset
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
subject(:client) do
|
|
14
|
+
RaptorIO::Protocol::HTTP::Client.new(
|
|
15
|
+
{
|
|
16
|
+
switch_board: RaptorIO::Socket::SwitchBoard.new
|
|
17
|
+
}.merge(options)
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
let(:options) do
|
|
21
|
+
{
|
|
22
|
+
manipulators: {
|
|
23
|
+
redirect_follower: { max: 6 }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'sets the limit on how many stacked redirections to follow' do
|
|
29
|
+
response = client.get( "#{@url}/10", mode: :sync )
|
|
30
|
+
response.redirections.size.should == 6
|
|
31
|
+
response.headers['Location'].should == "#{@url}/3"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context 'without a :max' do
|
|
35
|
+
let(:options) do
|
|
36
|
+
{
|
|
37
|
+
manipulators: {
|
|
38
|
+
redirect_follower: {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
it 'defaults to 5' do
|
|
43
|
+
|
|
44
|
+
response = client.get( "#{@url}/10", mode: :sync )
|
|
45
|
+
response.redirections.size.should == 5
|
|
46
|
+
response.headers['Location'].should == "#{@url}/4"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe RaptorIO::Protocol::HTTP::Request::Manipulators do
|
|
4
|
+
before( :each ) do
|
|
5
|
+
described_class.reset
|
|
6
|
+
described_class.library = manipulator_fixtures_path
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it 'is Enumerable' do
|
|
10
|
+
described_class.should be_kind_of Enumerable
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '.class_to_name' do
|
|
14
|
+
it 'returns the name of a loaded manipulator based on its class' do
|
|
15
|
+
described_class.load_all
|
|
16
|
+
described_class.class_to_name( RaptorIO::Protocol::HTTP::Request::Manipulators::NiccoloMachiavelli ).should ==
|
|
17
|
+
'niccolo_machiavelli'
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe '.process' do
|
|
22
|
+
let(:client) do
|
|
23
|
+
RaptorIO::Protocol::HTTP::Client.new(switch_board:RaptorIO::Socket::SwitchBoard.new)
|
|
24
|
+
end
|
|
25
|
+
it 'processes the given request and client with the given manipulator' do
|
|
26
|
+
request = RaptorIO::Protocol::HTTP::Request.new( url: 'http://test/' )
|
|
27
|
+
options = { stuff: 1 }
|
|
28
|
+
|
|
29
|
+
datastore = client.datastore['niccolo_machiavelli']
|
|
30
|
+
datastore['stuff'] = 'blah'
|
|
31
|
+
|
|
32
|
+
described_class.process( :niccolo_machiavelli, client, request, options ).should ==
|
|
33
|
+
[client, request, options, datastore]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
context 'when given a non-existent manipulator' do
|
|
37
|
+
it 'raises LoadError' do
|
|
38
|
+
request = RaptorIO::Protocol::HTTP::Request.new( url: 'http://test/' )
|
|
39
|
+
|
|
40
|
+
expect do
|
|
41
|
+
described_class.process( :huaa!, client, request )
|
|
42
|
+
end.to raise_error LoadError
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe '.validate_options' do
|
|
48
|
+
context 'when the options are invalid' do
|
|
49
|
+
it 'returns a Hash with errors' do
|
|
50
|
+
described_class.validate_options(
|
|
51
|
+
:options_validator,
|
|
52
|
+
{ mandatory_string: 12 },
|
|
53
|
+
nil
|
|
54
|
+
).should eq({
|
|
55
|
+
mandatory_string: 'Must be string.'
|
|
56
|
+
})
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
context 'when the options are valid' do
|
|
61
|
+
it 'returns an empty Hash' do
|
|
62
|
+
described_class.validate_options(
|
|
63
|
+
:options_validator,
|
|
64
|
+
{ mandatory_string: 'Stuff' },
|
|
65
|
+
nil
|
|
66
|
+
).should be_empty
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe '.validate_batch_options' do
|
|
72
|
+
context 'when the options are invalid' do
|
|
73
|
+
it 'returns a Hash with errors' do
|
|
74
|
+
described_class.validate_batch_options(
|
|
75
|
+
{ options_validator: { mandatory_string: 12 } },
|
|
76
|
+
nil
|
|
77
|
+
).should eq({
|
|
78
|
+
options_validator: { mandatory_string: 'Must be string.' }
|
|
79
|
+
})
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
context 'when the options are valid' do
|
|
84
|
+
it 'returns an empty Hash' do
|
|
85
|
+
described_class.validate_batch_options(
|
|
86
|
+
{ options_validator: { mandatory_string: 'Stuff' } },
|
|
87
|
+
nil
|
|
88
|
+
).should be_empty
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe '.library' do
|
|
94
|
+
it 'returns the directory of the manipulators\' library' do
|
|
95
|
+
File.directory?( described_class.library ).should be_true
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe '.library=' do
|
|
100
|
+
it 'sets the manipulators\' library directory' do
|
|
101
|
+
described_class.library = '/tmp/'
|
|
102
|
+
File.directory?( described_class.library ).should be_true
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe '.paths' do
|
|
107
|
+
it 'returns paths of all manipulators' do
|
|
108
|
+
described_class.paths.each do |path|
|
|
109
|
+
path.should start_with described_class.library
|
|
110
|
+
File.exist?( path ).should be_true
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe '.exist?' do
|
|
116
|
+
context 'when a manipulator exists' do
|
|
117
|
+
it 'returns true' do
|
|
118
|
+
described_class.exist?( 'niccolo_machiavelli' ).should be_true
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
context 'when a manipulator does not exist' do
|
|
123
|
+
it 'returns false' do
|
|
124
|
+
described_class.exist?( 'stuffer_stufferson' ).should be_false
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe '.available' do
|
|
130
|
+
it 'returns the names of all available manipulators' do
|
|
131
|
+
described_class.available.sort.should eq [ 'niccolo_machiavelli', 'manifoolators/fooer', 'options_validator' ].sort
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
describe '.load' do
|
|
136
|
+
it 'loads a manipulator by filename' do
|
|
137
|
+
described_class.load( :niccolo_machiavelli )
|
|
138
|
+
described_class.loaded['niccolo_machiavelli'].should ==
|
|
139
|
+
RaptorIO::Protocol::HTTP::Request::Manipulators::NiccoloMachiavelli
|
|
140
|
+
end
|
|
141
|
+
it 'returns the loaded manipulator' do
|
|
142
|
+
described_class.load( :niccolo_machiavelli ).should ==
|
|
143
|
+
RaptorIO::Protocol::HTTP::Request::Manipulators::NiccoloMachiavelli
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
context 'when given a non-existent manipulator' do
|
|
147
|
+
it 'raises LoadError' do
|
|
148
|
+
expect{ described_class.load( :huaa! ) }.to raise_error LoadError
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
describe '.load_all' do
|
|
154
|
+
it 'loads all manipulators' do
|
|
155
|
+
described_class.load_all
|
|
156
|
+
described_class.loaded['niccolo_machiavelli'].should ==
|
|
157
|
+
RaptorIO::Protocol::HTTP::Request::Manipulators::NiccoloMachiavelli
|
|
158
|
+
end
|
|
159
|
+
it 'returns the loaded manipulators' do
|
|
160
|
+
described_class.load_all.should eq ({
|
|
161
|
+
'manifoolators/fooer' => RaptorIO::Protocol::HTTP::Request::Manipulators::Manifoolators::Fooer,
|
|
162
|
+
'niccolo_machiavelli' => RaptorIO::Protocol::HTTP::Request::Manipulators::NiccoloMachiavelli,
|
|
163
|
+
'options_validator' => RaptorIO::Protocol::HTTP::Request::Manipulators::OptionsValidator
|
|
164
|
+
})
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
describe '.unload' do
|
|
169
|
+
it 'unload a manipulator' do
|
|
170
|
+
described_class.load( :niccolo_machiavelli )
|
|
171
|
+
described_class.unload( :niccolo_machiavelli )
|
|
172
|
+
described_class.loaded.should_not include :niccolo_machiavelli
|
|
173
|
+
|
|
174
|
+
expect do
|
|
175
|
+
RaptorIO::Protocol::HTTP::Request::Manipulators::NiccoloMachiavelli
|
|
176
|
+
end.to raise_error NameError
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
describe '.unload_all' do
|
|
181
|
+
it 'unloads all manipulators' do
|
|
182
|
+
described_class.load_all
|
|
183
|
+
described_class.constants.size.should >= 1
|
|
184
|
+
|
|
185
|
+
described_class.unload_all
|
|
186
|
+
described_class.loaded.should be_empty
|
|
187
|
+
described_class.constants.should be_empty
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
describe '.each' do
|
|
192
|
+
it 'returns each loaded manipulator' do
|
|
193
|
+
described_class.load_all
|
|
194
|
+
described_class.loaded.should be_any
|
|
195
|
+
|
|
196
|
+
described_class.each do |name, klass|
|
|
197
|
+
described_class.loaded[name].should == klass
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
end
|
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
require 'spec_helper'
|
|
3
|
+
require 'ostruct'
|
|
4
|
+
|
|
5
|
+
describe RaptorIO::Protocol::HTTP::Request do
|
|
6
|
+
it_should_behave_like 'RaptorIO::Protocol::HTTP::Message'
|
|
7
|
+
|
|
8
|
+
let(:url) { 'http://test.com' }
|
|
9
|
+
let(:parsed_url) { URI(url) }
|
|
10
|
+
let(:url_with_query) { 'http://test.com/?id=1&stuff=blah' }
|
|
11
|
+
|
|
12
|
+
describe '#initialize' do
|
|
13
|
+
it 'sets the instance attributes by the options' do
|
|
14
|
+
options = {
|
|
15
|
+
url: url,
|
|
16
|
+
http_method: :get,
|
|
17
|
+
parameters: { 'test' => 'blah' },
|
|
18
|
+
timeout: 10,
|
|
19
|
+
continue: false,
|
|
20
|
+
raw: true
|
|
21
|
+
}
|
|
22
|
+
r = described_class.new( options )
|
|
23
|
+
r.url.should == url
|
|
24
|
+
r.http_method.should == options[:http_method]
|
|
25
|
+
r.parameters.should == options[:parameters]
|
|
26
|
+
r.timeout.should == options[:timeout]
|
|
27
|
+
r.continue.should == options[:continue]
|
|
28
|
+
r.raw.should == options[:raw]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'uses the setter methods when configuring' do
|
|
32
|
+
options = { url: url, http_method: 'gEt', parameters: { 'test' => 'blah' } }
|
|
33
|
+
described_class.new( options ).http_method.should == :get
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
context 'when no :url option has been provided' do
|
|
37
|
+
it 'raises ArgumentError' do
|
|
38
|
+
raised = false
|
|
39
|
+
begin
|
|
40
|
+
described_class.new
|
|
41
|
+
rescue ArgumentError
|
|
42
|
+
raised = true
|
|
43
|
+
end
|
|
44
|
+
raised.should be_true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe '#connection_id' do
|
|
50
|
+
it 'returns an ID for the given host:port' do
|
|
51
|
+
described_class.new( url: 'http://stuff' ).connection_id.should ==
|
|
52
|
+
described_class.new( url: 'http://stuff:80' ).connection_id
|
|
53
|
+
|
|
54
|
+
described_class.new( url: 'http://stuff' ).connection_id.should_not ==
|
|
55
|
+
described_class.new( url: 'http://stuff:81' ).connection_id
|
|
56
|
+
|
|
57
|
+
described_class.new( url: 'http://stuff.com' ).connection_id.should ==
|
|
58
|
+
described_class.new( url: 'http://stuff.com' ).connection_id
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe '#continue?' do
|
|
63
|
+
context 'default' do
|
|
64
|
+
it 'returns true' do
|
|
65
|
+
described_class.new( url: url ).continue?.should be_true
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context 'when the continue option has been set to' do
|
|
70
|
+
context true do
|
|
71
|
+
it 'returns false' do
|
|
72
|
+
described_class.new( url: url, continue: true ).continue?.should be_true
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
context false do
|
|
77
|
+
it 'returns false' do
|
|
78
|
+
described_class.new( url: url, continue: false ).continue?.should be_false
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
describe '#url' do
|
|
86
|
+
it 'returns the configured value' do
|
|
87
|
+
described_class.new( url: url ).url.should == url
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe '#parsed_url' do
|
|
92
|
+
it 'returns the configured URL as a parsed object' do
|
|
93
|
+
described_class.new( url: url ).parsed_url.should == URI(url)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe '#http_method' do
|
|
98
|
+
it 'defaults to :get' do
|
|
99
|
+
described_class.new( url: url ).http_method.should == :get
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
describe '#http_method=' do
|
|
104
|
+
it 'normalizes the HTTP method to a downcase symbol' do
|
|
105
|
+
request = described_class.new( url: url )
|
|
106
|
+
request.http_method = 'pOsT'
|
|
107
|
+
request.http_method.should == :post
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
describe '#idempotent?' do
|
|
112
|
+
context 'when http_method is post' do
|
|
113
|
+
it 'returns false' do
|
|
114
|
+
described_class.new( url: url, http_method: :post ).idempotent?.should be_false
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
context 'when http_method is not post' do
|
|
119
|
+
it 'returns true' do
|
|
120
|
+
described_class.new( url: url ).idempotent?.should be_true
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe '#parameters' do
|
|
126
|
+
it 'defaults to an empty Hash' do
|
|
127
|
+
described_class.new( url: url ).parameters.should == {}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'recursively forces converts keys and values to strings' do
|
|
131
|
+
with_symbols = {
|
|
132
|
+
test: 'blah',
|
|
133
|
+
another_hash: {
|
|
134
|
+
stuff: 'test'
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
with_strings = {
|
|
138
|
+
'test' => 'blah',
|
|
139
|
+
'another_hash' => {
|
|
140
|
+
'stuff' => 'test'
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
request = described_class.new( url: url )
|
|
145
|
+
request.parameters = with_symbols
|
|
146
|
+
request.parameters.should == with_strings
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
describe '#query_parameters' do
|
|
151
|
+
context 'when :raw option is' do
|
|
152
|
+
context true do
|
|
153
|
+
it 'does not decode the URL query parameters' do
|
|
154
|
+
weird_url = 'http://test.com/?first=test%3Fblah%2F&second%2F%26=blah'
|
|
155
|
+
r = described_class.new( raw: true, url: weird_url, http_method: :other )
|
|
156
|
+
r.query_parameters.should == { 'first' => 'test%3Fblah%2F', 'second%2F%26' => 'blah' }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
context false do
|
|
161
|
+
it 'decodes the URL query parameters' do
|
|
162
|
+
weird_url = 'http://test.com/?first=test%3Fblah%2F&second%2F%26=blah'
|
|
163
|
+
r = described_class.new( raw: false, url: weird_url, http_method: :other )
|
|
164
|
+
r.query_parameters.should == { 'first' => 'test?blah/', 'second/&' => 'blah' }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
context 'default' do
|
|
169
|
+
it 'decodes the URL query parameters' do
|
|
170
|
+
weird_url = 'http://test.com/?first=test%3Fblah%2F&second%2F%26=blah'
|
|
171
|
+
r = described_class.new( url: weird_url, http_method: :other )
|
|
172
|
+
r.query_parameters.should == { 'first' => 'test?blah/', 'second/&' => 'blah' }
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
context 'when the request method is' do
|
|
178
|
+
context 'GET' do
|
|
179
|
+
context 'when no parameters have been provided as options' do
|
|
180
|
+
it 'returns the query parameters as a Hash' do
|
|
181
|
+
r = described_class.new( url: url_with_query, http_method: :get )
|
|
182
|
+
r.query_parameters.should == { 'id' => '1', 'stuff' => 'blah' }
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
context 'when there are parameters as options' do
|
|
186
|
+
let(:parameters) { { 'id' => '2', 'stuff' => 'blah' } }
|
|
187
|
+
|
|
188
|
+
context 'and the URL has no query parameters' do
|
|
189
|
+
it 'returns the parameters from options' do
|
|
190
|
+
r = described_class.new( url: url, http_method: :get, parameters: parameters )
|
|
191
|
+
r.query_parameters.should == parameters
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
context 'and the URL has query parameters' do
|
|
195
|
+
it 'returns the query parameters merged with the options parameters' do
|
|
196
|
+
r = described_class.new( url: url_with_query, http_method: :get, parameters: parameters )
|
|
197
|
+
r.query_parameters.should == { 'id' => '1', 'stuff' => 'blah' }.merge(parameters)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
context 'other' do
|
|
203
|
+
it 'returns the query parameters as a Hash' do
|
|
204
|
+
r = described_class.new( url: url_with_query, http_method: :other )
|
|
205
|
+
r.query_parameters.should == { 'id' => '1', 'stuff' => 'blah' }
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
describe '#resource' do
|
|
212
|
+
it 'returns the resource to be requested' do
|
|
213
|
+
r = described_class.new( url: url, parameters: { 'first' => 'test?blah/', 'second/&' => 'blah' } )
|
|
214
|
+
r.resource.to_s.should == '/?first=test%3Fblah%2F&second%2F%26=blah'
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
describe '#effective_url' do
|
|
219
|
+
context 'when :raw option is' do
|
|
220
|
+
context true do
|
|
221
|
+
it 'does not encode the URL query parameters' do
|
|
222
|
+
r = described_class.new( raw: true, url: url, parameters: { 'first' => 'test?blah/', 'second/&' => 'blah' } )
|
|
223
|
+
r.effective_url.to_s.should == 'http://test.com/?first=test?blah/&second/&=blah'
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
context false do
|
|
228
|
+
it 'encodes the URL query parameters' do
|
|
229
|
+
r = described_class.new( raw: false, url: url, parameters: { 'first' => 'test?blah/', 'second/&' => 'blah' } )
|
|
230
|
+
r.effective_url.to_s.should == 'http://test.com/?first=test%3Fblah%2F&second%2F%26=blah'
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
context 'default' do
|
|
235
|
+
it 'encodes the URL query parameters' do
|
|
236
|
+
r = described_class.new( url: url, parameters: { 'first' => 'test?blah/', 'second/&' => 'blah' } )
|
|
237
|
+
r.effective_url.to_s.should == 'http://test.com/?first=test%3Fblah%2F&second%2F%26=blah'
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it 'has UTF8 support' do
|
|
243
|
+
options = {
|
|
244
|
+
url: url_with_query,
|
|
245
|
+
parameters: {
|
|
246
|
+
'test' => 'τεστ'
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
described_class.new( options ).effective_url.to_s.should ==
|
|
250
|
+
"http://test.com/?id=1&stuff=blah&test=%CF%84%CE%B5%CF%83%CF%84"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
context 'when the request method is' do
|
|
254
|
+
context 'GET' do
|
|
255
|
+
context 'when no parameters have been provided as options' do
|
|
256
|
+
it 'returns the original URL' do
|
|
257
|
+
r = described_class.new( url: url_with_query, http_method: :get )
|
|
258
|
+
r.effective_url.to_s.should == url_with_query
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
context 'when there are parameters as options' do
|
|
262
|
+
let(:parameters) { { 'id' => '2', 'stuff' => 'blah' } }
|
|
263
|
+
|
|
264
|
+
context 'and the URL has no query parameters' do
|
|
265
|
+
it 'returns a URL with the option parameters' do
|
|
266
|
+
r = described_class.new( url: url, http_method: :get, parameters: parameters )
|
|
267
|
+
r.effective_url.to_s.should == "#{url}/?id=2&stuff=blah"
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
context 'and the URL has query parameters' do
|
|
271
|
+
it 'returns the query parameters merged with the options parameters' do
|
|
272
|
+
r = described_class.new( url: url_with_query, http_method: :get, parameters: parameters )
|
|
273
|
+
r.effective_url.to_s.should == "#{url}/?id=2&stuff=blah"
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
context 'other' do
|
|
279
|
+
it 'returns the original URL' do
|
|
280
|
+
r = described_class.new( url: url_with_query, http_method: :other )
|
|
281
|
+
r.effective_url.to_s.should == url_with_query
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
describe '#effective_body' do
|
|
288
|
+
context 'when the Expect header field has been set' do
|
|
289
|
+
it 'returns an empty string' do
|
|
290
|
+
described_class.new( url: url,
|
|
291
|
+
body: 'stuff',
|
|
292
|
+
headers: { 'Expect' => '100-continue' }
|
|
293
|
+
).effective_body.should == ''
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
context 'when no body has been provided' do
|
|
298
|
+
it 'returns an empty string' do
|
|
299
|
+
described_class.new( url: url ).effective_body.should == ''
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
context 'when there is a body' do
|
|
304
|
+
context 'when :raw option is' do
|
|
305
|
+
context true do
|
|
306
|
+
it 'does not encode it' do
|
|
307
|
+
options = {
|
|
308
|
+
raw: true,
|
|
309
|
+
url: url,
|
|
310
|
+
body: "fds g45\#$ 6@ %y @^2\r\n"
|
|
311
|
+
}
|
|
312
|
+
described_class.new( options ).effective_body.should ==
|
|
313
|
+
options[:body]
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
context false do
|
|
318
|
+
it 'encodes it' do
|
|
319
|
+
options = {
|
|
320
|
+
raw: false,
|
|
321
|
+
url: url,
|
|
322
|
+
body: "fds g45\#$ 6@ %y @^2\r\n"
|
|
323
|
+
}
|
|
324
|
+
described_class.new( options ).effective_body.should ==
|
|
325
|
+
'fds+g45%23%24+6%40+%25y+%40%5E2%0D%0A'
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
context 'default' do
|
|
330
|
+
it 'encodes it' do
|
|
331
|
+
options = {
|
|
332
|
+
url: url,
|
|
333
|
+
body: "fds g45\#$ 6@ %y @^2\r\n"
|
|
334
|
+
}
|
|
335
|
+
described_class.new( options ).effective_body.should ==
|
|
336
|
+
'fds+g45%23%24+6%40+%25y+%40%5E2%0D%0A'
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
it 'has UTF8 support' do
|
|
342
|
+
described_class.new( url: 'http://stuff', body: 'τεστ' ).effective_body.should == '%CF%84%CE%B5%CF%83%CF%84'
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
context 'when the request method is' do
|
|
347
|
+
context 'POST' do
|
|
348
|
+
context 'when no parameters have been provided as options' do
|
|
349
|
+
context 'when :raw option is' do
|
|
350
|
+
context true do
|
|
351
|
+
it 'returns the original body' do
|
|
352
|
+
options = {
|
|
353
|
+
raw: true,
|
|
354
|
+
url: url_with_query,
|
|
355
|
+
http_method: :post,
|
|
356
|
+
body: 'stuff=/1&blah=/test'
|
|
357
|
+
}
|
|
358
|
+
described_class.new( options ).effective_body.should == options[:body]
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
context false do
|
|
363
|
+
it 'escapes and returns the body' do
|
|
364
|
+
options = {
|
|
365
|
+
raw: false,
|
|
366
|
+
url: url_with_query,
|
|
367
|
+
http_method: :post,
|
|
368
|
+
body: 'stuff=/1&blah=/test'
|
|
369
|
+
}
|
|
370
|
+
described_class.new( options ).effective_body.should == 'stuff=%2F1&blah=%2Ftest'
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
context 'default' do
|
|
375
|
+
it 'escapes and returns the body' do
|
|
376
|
+
options = {
|
|
377
|
+
raw: false,
|
|
378
|
+
url: url_with_query,
|
|
379
|
+
http_method: :post,
|
|
380
|
+
body: 'stuff=/1&blah=/test'
|
|
381
|
+
}
|
|
382
|
+
described_class.new( options ).effective_body.should == 'stuff=%2F1&blah=%2Ftest'
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
context 'when there are parameters as options' do
|
|
389
|
+
let(:parameters) { { 'id/' => '2', 'stuff' => 'blah/' } }
|
|
390
|
+
|
|
391
|
+
context 'and there is no body configured' do
|
|
392
|
+
context 'when :raw option is' do
|
|
393
|
+
context true do
|
|
394
|
+
it 'returns the option parameters' do
|
|
395
|
+
options = {
|
|
396
|
+
raw: true,
|
|
397
|
+
url: url_with_query,
|
|
398
|
+
http_method: :post,
|
|
399
|
+
parameters: parameters
|
|
400
|
+
}
|
|
401
|
+
described_class.new( options ).effective_body.to_s.should ==
|
|
402
|
+
"id/=2&stuff=blah/"
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
context false do
|
|
407
|
+
it 'returns the escaped option parameters' do
|
|
408
|
+
options = {
|
|
409
|
+
raw: false,
|
|
410
|
+
url: url_with_query,
|
|
411
|
+
http_method: :post,
|
|
412
|
+
parameters: parameters
|
|
413
|
+
}
|
|
414
|
+
described_class.new( options ).effective_body.to_s.should ==
|
|
415
|
+
"id%2F=2&stuff=blah%2F"
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
context 'default' do
|
|
420
|
+
it 'returns the escaped option parameters' do
|
|
421
|
+
options = {
|
|
422
|
+
raw: false,
|
|
423
|
+
url: url_with_query,
|
|
424
|
+
http_method: :post,
|
|
425
|
+
parameters: parameters
|
|
426
|
+
}
|
|
427
|
+
described_class.new( options ).effective_body.to_s.should ==
|
|
428
|
+
"id%2F=2&stuff=blah%2F"
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
it 'has UTF8 support' do
|
|
436
|
+
options = {
|
|
437
|
+
url: url_with_query,
|
|
438
|
+
http_method: :post,
|
|
439
|
+
parameters: {
|
|
440
|
+
'test' => 'τεστ'
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
described_class.new( options ).effective_body.to_s.should ==
|
|
444
|
+
"test=%CF%84%CE%B5%CF%83%CF%84"
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
context 'and there is a body' do
|
|
448
|
+
it 'returns the body parameters merged with the options parameters' do
|
|
449
|
+
options = {
|
|
450
|
+
url: url_with_query,
|
|
451
|
+
http_method: :post,
|
|
452
|
+
body: 'stuff 4354%$43=$#535!35VWE g4 %yt5&stuff=1',
|
|
453
|
+
parameters: parameters
|
|
454
|
+
}
|
|
455
|
+
described_class.new( options ).effective_body.to_s.should ==
|
|
456
|
+
"stuff+4354%25%2443=%24%23535%2135VWE+g4+%25yt5&stuff=blah%2F&id%2F=2"
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
context 'other' do
|
|
463
|
+
context 'when :raw option is' do
|
|
464
|
+
context true do
|
|
465
|
+
it 'returns the original body' do
|
|
466
|
+
options = {
|
|
467
|
+
raw: true,
|
|
468
|
+
url: url_with_query,
|
|
469
|
+
http_method: :other,
|
|
470
|
+
body: 'stuff here #$^#46 %H# '
|
|
471
|
+
}
|
|
472
|
+
described_class.new( options ).effective_body.should == options[:body]
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
context false do
|
|
477
|
+
it 'escapes and returns the original body' do
|
|
478
|
+
options = {
|
|
479
|
+
raw: false,
|
|
480
|
+
url: url_with_query,
|
|
481
|
+
http_method: :other,
|
|
482
|
+
body: 'stuff here #$^#46 %H# '
|
|
483
|
+
}
|
|
484
|
+
described_class.new( options ).effective_body.should ==
|
|
485
|
+
'stuff+here+%23%24%5E%2346+%25H%23+'
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
context 'default' do
|
|
490
|
+
it 'escapes and returns the original body' do
|
|
491
|
+
options = {
|
|
492
|
+
url: url_with_query,
|
|
493
|
+
http_method: :other,
|
|
494
|
+
body: 'stuff here #$^#46 %H# '
|
|
495
|
+
}
|
|
496
|
+
described_class.new( options ).effective_body.should ==
|
|
497
|
+
'stuff+here+%23%24%5E%2346+%25H%23+'
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
describe '#clear_callbacks' do
|
|
507
|
+
it 'clears all callbacks' do
|
|
508
|
+
request = described_class.new( url: url )
|
|
509
|
+
|
|
510
|
+
request.on_complete { |res| res }
|
|
511
|
+
request.on_complete.size.should == 1
|
|
512
|
+
|
|
513
|
+
request.on_success { |res| res }
|
|
514
|
+
request.on_success.size.should == 1
|
|
515
|
+
|
|
516
|
+
request.on_failure { |res| res }
|
|
517
|
+
request.on_failure.size.should == 1
|
|
518
|
+
|
|
519
|
+
request.clear_callbacks
|
|
520
|
+
|
|
521
|
+
request.on_complete.should be_empty
|
|
522
|
+
request.on_success.should be_empty
|
|
523
|
+
request.on_failure.should be_empty
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
describe '#on_complete' do
|
|
528
|
+
context 'when passed a block' do
|
|
529
|
+
it 'adds it as a callback to be passed the response' do
|
|
530
|
+
request = described_class.new( url: url )
|
|
531
|
+
|
|
532
|
+
passed_response = nil
|
|
533
|
+
request.on_complete { |res| passed_response = res }
|
|
534
|
+
|
|
535
|
+
response = RaptorIO::Protocol::HTTP::Response.new( url: url )
|
|
536
|
+
request.handle_response( response )
|
|
537
|
+
|
|
538
|
+
passed_response.should == response
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
it 'can add multiple callbacks' do
|
|
542
|
+
request = described_class.new( url: url )
|
|
543
|
+
|
|
544
|
+
passed_responses = []
|
|
545
|
+
|
|
546
|
+
2.times do
|
|
547
|
+
request.on_complete { |res| passed_responses << res }
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
response = RaptorIO::Protocol::HTTP::Response.new( url: url )
|
|
551
|
+
request.handle_response( response )
|
|
552
|
+
|
|
553
|
+
passed_responses.size.should == 2
|
|
554
|
+
passed_responses.uniq.size.should == 1
|
|
555
|
+
passed_responses.uniq.first.should == response
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
it 'returns all relevant callbacks' do
|
|
560
|
+
request = described_class.new( url: url )
|
|
561
|
+
2.times do
|
|
562
|
+
request.on_complete { |res| res }
|
|
563
|
+
end
|
|
564
|
+
request.on_complete.size.should == 2
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
describe '#on_success' do
|
|
569
|
+
context 'when passed a block' do
|
|
570
|
+
it 'adds it as a callback to be called on a successful request' do
|
|
571
|
+
request = described_class.new( url: url )
|
|
572
|
+
|
|
573
|
+
passed_response = nil
|
|
574
|
+
request.on_success { |res| passed_response = res }
|
|
575
|
+
|
|
576
|
+
response = RaptorIO::Protocol::HTTP::Response.new( url: url, code: 200 )
|
|
577
|
+
request.handle_response( response )
|
|
578
|
+
|
|
579
|
+
passed_response.should == response
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
it 'can add multiple callbacks' do
|
|
583
|
+
request = described_class.new( url: url )
|
|
584
|
+
|
|
585
|
+
passed_responses = []
|
|
586
|
+
|
|
587
|
+
2.times do
|
|
588
|
+
request.on_success { |res| passed_responses << res }
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
response = RaptorIO::Protocol::HTTP::Response.new( url: url, code: 200 )
|
|
592
|
+
request.handle_response( response )
|
|
593
|
+
|
|
594
|
+
passed_responses.size.should == 2
|
|
595
|
+
passed_responses.uniq.size.should == 1
|
|
596
|
+
passed_responses.uniq.first.should == response
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
it 'returns all relevant callbacks' do
|
|
601
|
+
request = described_class.new( url: url )
|
|
602
|
+
2.times do
|
|
603
|
+
request.on_success { |res| res }
|
|
604
|
+
end
|
|
605
|
+
request.on_success.size.should == 2
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
describe '#on_failure' do
|
|
610
|
+
context 'when passed a block' do
|
|
611
|
+
it 'adds a callback block to be called on a failed request' do
|
|
612
|
+
request = described_class.new( url: url )
|
|
613
|
+
|
|
614
|
+
passed_response = nil
|
|
615
|
+
request.on_failure { |res| passed_response = res }
|
|
616
|
+
|
|
617
|
+
response = RaptorIO::Protocol::HTTP::Response.new( url: url, code: 0 )
|
|
618
|
+
request.handle_response( response )
|
|
619
|
+
|
|
620
|
+
passed_response.should == response
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
it 'can add multiple callbacks' do
|
|
624
|
+
request = described_class.new( url: url )
|
|
625
|
+
|
|
626
|
+
passed_responses = []
|
|
627
|
+
|
|
628
|
+
2.times do
|
|
629
|
+
request.on_failure { |res| passed_responses << res }
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
response = RaptorIO::Protocol::HTTP::Response.new( url: url, code: 0 )
|
|
633
|
+
request.handle_response( response )
|
|
634
|
+
|
|
635
|
+
passed_responses.size.should == 2
|
|
636
|
+
passed_responses.uniq.size.should == 1
|
|
637
|
+
passed_responses.uniq.first.should == response
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
it 'returns all relevant callbacks' do
|
|
642
|
+
request = described_class.new( url: url )
|
|
643
|
+
2.times do
|
|
644
|
+
request.on_failure { |res| res }
|
|
645
|
+
end
|
|
646
|
+
request.on_failure.size.should == 2
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
describe '#handle_response' do
|
|
651
|
+
it 'assigns self as the #request attribute of the response' do
|
|
652
|
+
request = described_class.new( url: url )
|
|
653
|
+
|
|
654
|
+
passed_response = nil
|
|
655
|
+
request.on_complete { |res| passed_response = res }
|
|
656
|
+
|
|
657
|
+
response = RaptorIO::Protocol::HTTP::Response.new( url: url )
|
|
658
|
+
request.handle_response( response )
|
|
659
|
+
|
|
660
|
+
passed_response.request.should == request
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
context 'when a response is successful' do
|
|
664
|
+
let(:response) { RaptorIO::Protocol::HTTP::Response.new( url: url, code: 200 ) }
|
|
665
|
+
|
|
666
|
+
it 'calls #on_complete callbacks' do
|
|
667
|
+
request = described_class.new( url: url )
|
|
668
|
+
|
|
669
|
+
passed_response = nil
|
|
670
|
+
request.on_complete { |res| passed_response = res }
|
|
671
|
+
request.handle_response( response )
|
|
672
|
+
|
|
673
|
+
passed_response.should == response
|
|
674
|
+
end
|
|
675
|
+
it 'calls #on_success callbacks' do
|
|
676
|
+
request = described_class.new( url: url )
|
|
677
|
+
|
|
678
|
+
passed_response = nil
|
|
679
|
+
request.on_success { |res| passed_response = res }
|
|
680
|
+
request.handle_response( response )
|
|
681
|
+
|
|
682
|
+
passed_response.should == response
|
|
683
|
+
end
|
|
684
|
+
it 'does not call #on_failure callbacks' do
|
|
685
|
+
request = described_class.new( url: url )
|
|
686
|
+
|
|
687
|
+
passed_response = nil
|
|
688
|
+
request.on_failure { |res| passed_response = res }
|
|
689
|
+
request.handle_response( response )
|
|
690
|
+
|
|
691
|
+
passed_response.should be_nil
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
context 'when a request fails' do
|
|
695
|
+
let(:response) { RaptorIO::Protocol::HTTP::Response.new( url: url, code: 0 ) }
|
|
696
|
+
|
|
697
|
+
it 'calls #on_complete callbacks' do
|
|
698
|
+
request = described_class.new( url: url )
|
|
699
|
+
|
|
700
|
+
passed_response = nil
|
|
701
|
+
request.on_complete { |res| passed_response = res }
|
|
702
|
+
request.handle_response( response )
|
|
703
|
+
|
|
704
|
+
passed_response.should == response
|
|
705
|
+
end
|
|
706
|
+
it 'does not call #on_success callbacks' do
|
|
707
|
+
request = described_class.new( url: url )
|
|
708
|
+
|
|
709
|
+
passed_response = nil
|
|
710
|
+
request.on_success { |res| passed_response = res }
|
|
711
|
+
request.handle_response( response )
|
|
712
|
+
|
|
713
|
+
passed_response.should be_nil
|
|
714
|
+
end
|
|
715
|
+
it 'calls #on_failure callbacks' do
|
|
716
|
+
request = described_class.new( url: url )
|
|
717
|
+
|
|
718
|
+
passed_response = nil
|
|
719
|
+
request.on_failure { |res| passed_response = res }
|
|
720
|
+
request.handle_response( response )
|
|
721
|
+
|
|
722
|
+
passed_response.should == response
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
describe '#to_s' do
|
|
728
|
+
it 'includes a Host header' do
|
|
729
|
+
described_class.new( url: url ).to_s.should ==
|
|
730
|
+
"GET / HTTP/1.1\r\n" +
|
|
731
|
+
"Host: #{parsed_url.host}:#{parsed_url.port}\r\n\r\n"
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
context 'when the request method is' do
|
|
735
|
+
context 'GET' do
|
|
736
|
+
context 'when no parameters have been provided as options' do
|
|
737
|
+
it 'uses the original URL' do
|
|
738
|
+
r = described_class.new( url: url_with_query, http_method: :get )
|
|
739
|
+
r.to_s.lines.first.should == "GET /?id=1&stuff=blah HTTP/1.1\r\n"
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
context 'when there are parameters as options' do
|
|
743
|
+
let(:parameters) { { 'id' => '2', 'stuff' => 'blah' } }
|
|
744
|
+
|
|
745
|
+
context 'and the URL has no query parameters' do
|
|
746
|
+
it 'uses the URL with the option parameters' do
|
|
747
|
+
r = described_class.new( url: url, http_method: :get, parameters: parameters )
|
|
748
|
+
r.to_s.lines.first.should == "GET /?id=2&stuff=blah HTTP/1.1\r\n"
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
context 'and the URL has query parameters' do
|
|
752
|
+
it 'uses the query parameters merged with the options parameters' do
|
|
753
|
+
r = described_class.new( url: url_with_query, http_method: :get, parameters: parameters )
|
|
754
|
+
r.to_s.lines.first.should == "GET /?id=2&stuff=blah HTTP/1.1\r\n"
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
end
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
context 'POST' do
|
|
761
|
+
context 'when no parameters have been provided as options' do
|
|
762
|
+
it 'uses the original body' do
|
|
763
|
+
options = {
|
|
764
|
+
url: url_with_query,
|
|
765
|
+
http_method: :post,
|
|
766
|
+
body: 'stuff=1&blah=test'
|
|
767
|
+
}
|
|
768
|
+
described_class.new( options ).to_s.split( /[\n\r]+/ ).last.should ==
|
|
769
|
+
options[:body]
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
context 'when there are parameters as options' do
|
|
773
|
+
let(:parameters) { { 'id $#^3 4q%$#' => '2 dfgr ', 'stuff' => 'blah' } }
|
|
774
|
+
|
|
775
|
+
context 'and there is no body configured' do
|
|
776
|
+
it 'uses the escaped option parameters' do
|
|
777
|
+
options = {
|
|
778
|
+
url: url_with_query,
|
|
779
|
+
http_method: :post,
|
|
780
|
+
parameters: parameters
|
|
781
|
+
}
|
|
782
|
+
described_class.new( options ).to_s.split( /[\n\r]+/ ).last.should ==
|
|
783
|
+
"id+%24%23%5E3+4q%25%24%23=2+dfgr+&stuff=blah"
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
context 'and there is a body' do
|
|
787
|
+
it 'uses the body parameters merged with the options parameters' do
|
|
788
|
+
options = {
|
|
789
|
+
url: url_with_query,
|
|
790
|
+
http_method: :post,
|
|
791
|
+
body: 'stuff 4354%$43=$#535!35VWE g4 %yt5&stuff=1',
|
|
792
|
+
parameters: parameters
|
|
793
|
+
}
|
|
794
|
+
described_class.new( options ).to_s.split( /[\n\r]+/ ).last.should ==
|
|
795
|
+
"stuff+4354%25%2443=%24%23535%2135VWE+g4+%25yt5&stuff=blah&id+%24%23%5E3+4q%25%24%23=2+dfgr+"
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
context 'other' do
|
|
802
|
+
it 'returns the original body' do
|
|
803
|
+
options = {
|
|
804
|
+
url: url_with_query,
|
|
805
|
+
http_method: :other,
|
|
806
|
+
body: 'stuff'
|
|
807
|
+
}
|
|
808
|
+
described_class.new( options ).to_s.split( /[\n\r]+/ ).last.should ==
|
|
809
|
+
options[:body]
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
it 'escapes the original body' do
|
|
813
|
+
options = {
|
|
814
|
+
url: url_with_query,
|
|
815
|
+
http_method: :other,
|
|
816
|
+
body: 'stuff here #$^#46 %H# '
|
|
817
|
+
}
|
|
818
|
+
described_class.new( options ).to_s.split( /[\n\r]+/ ).last.should ==
|
|
819
|
+
'stuff+here+%23%24%5E%2346+%25H%23+'
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
it 'returns the original URL' do
|
|
823
|
+
options = {
|
|
824
|
+
url: url_with_query,
|
|
825
|
+
http_method: :other
|
|
826
|
+
}
|
|
827
|
+
described_class.new( options ).to_s.lines.first.should ==
|
|
828
|
+
"OTHER /?id=1&stuff=blah HTTP/1.1\r\n"
|
|
829
|
+
end
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
context 'when headers' do
|
|
834
|
+
context 'have been provided' do
|
|
835
|
+
it 'escapes and includes them in the request' do
|
|
836
|
+
options = {
|
|
837
|
+
url: url,
|
|
838
|
+
headers: {
|
|
839
|
+
'X-Stuff' => "blah"
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
described_class.new( options ).to_s.should ==
|
|
843
|
+
"GET / HTTP/1.1\r\n" +
|
|
844
|
+
"Host: #{parsed_url.host}:#{parsed_url.port}\r\n" +
|
|
845
|
+
"X-Stuff: blah\r\n\r\n"
|
|
846
|
+
end
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
context 'when an HTTP method' do
|
|
851
|
+
context 'has been provided' do
|
|
852
|
+
it 'includes it the request' do
|
|
853
|
+
options = {
|
|
854
|
+
url: url,
|
|
855
|
+
http_method: :stuff
|
|
856
|
+
}
|
|
857
|
+
described_class.new( options ).to_s.lines.first.should == "STUFF / HTTP/1.1\r\n"
|
|
858
|
+
end
|
|
859
|
+
end
|
|
860
|
+
context 'has not been provided' do
|
|
861
|
+
it 'defaults to GET' do
|
|
862
|
+
described_class.new( url: url ).to_s.lines.first.should == "GET / HTTP/1.1\r\n"
|
|
863
|
+
end
|
|
864
|
+
end
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
context 'when an HTTP version' do
|
|
868
|
+
context 'has been provided' do
|
|
869
|
+
it 'includes it the request' do
|
|
870
|
+
options = {
|
|
871
|
+
url: url,
|
|
872
|
+
version: '2'
|
|
873
|
+
}
|
|
874
|
+
described_class.new( options ).to_s.lines.first.should == "GET / HTTP/2\r\n"
|
|
875
|
+
end
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
context 'has not been provided' do
|
|
879
|
+
it 'defaults to 1.1' do
|
|
880
|
+
described_class.new( url: url ).to_s.lines.first.should == "GET / HTTP/1.1\r\n"
|
|
881
|
+
end
|
|
882
|
+
end
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
context 'when there is a body' do
|
|
886
|
+
it 'encodes it' do
|
|
887
|
+
options = {
|
|
888
|
+
url: url,
|
|
889
|
+
body: "fds g45\#$ 6@ %y @^2\r\n"
|
|
890
|
+
}
|
|
891
|
+
described_class.new( options ).to_s.split( /[\n\r]+/ ).last.should ==
|
|
892
|
+
"fds+g45%23%24+6%40+%25y+%40%5E2%0D%0A"
|
|
893
|
+
end
|
|
894
|
+
it 'sets the Content-Length header' do
|
|
895
|
+
options = {
|
|
896
|
+
url: url,
|
|
897
|
+
body: "fds g45\#$ 6@ %y @^2\r\n"
|
|
898
|
+
}
|
|
899
|
+
described_class.new( options ).to_s.should ==
|
|
900
|
+
"GET / HTTP/1.1\r\n" +
|
|
901
|
+
"Host: #{parsed_url.host}:#{parsed_url.port}\r\n" +
|
|
902
|
+
"Content-Length: 37\r\n\r\n" +
|
|
903
|
+
"fds+g45%23%24+6%40+%25y+%40%5E2%0D%0A"
|
|
904
|
+
end
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
describe '.parse' do
|
|
910
|
+
it 'parses a request string into a Request object' do
|
|
911
|
+
request = <<EOTXT
|
|
912
|
+
GET /stuff?pname=pvalue HTTP/1.1
|
|
913
|
+
Host: localhost:88
|
|
914
|
+
User-Agent: Stuff agent
|
|
915
|
+
Cookie: cname=cvalue; c2name=c2value
|
|
916
|
+
|
|
917
|
+
body+here
|
|
918
|
+
EOTXT
|
|
919
|
+
|
|
920
|
+
request = described_class.parse( request )
|
|
921
|
+
request.url.should == '/stuff?pname=pvalue'
|
|
922
|
+
request.parsed_url.to_s.should == '/stuff?pname=pvalue'
|
|
923
|
+
request.version.should == '1.1'
|
|
924
|
+
request.http_method.should == :get
|
|
925
|
+
request.body.should == "body+here\n"
|
|
926
|
+
request.headers.should eq({
|
|
927
|
+
'Host' => 'localhost:88',
|
|
928
|
+
'User-Agent' => 'Stuff agent',
|
|
929
|
+
'Cookie' => 'cname=cvalue; c2name=c2value'
|
|
930
|
+
})
|
|
931
|
+
|
|
932
|
+
request.headers.cookies.should == [
|
|
933
|
+
{
|
|
934
|
+
name: 'cname',
|
|
935
|
+
value: 'cvalue',
|
|
936
|
+
version: 0,
|
|
937
|
+
port: nil,
|
|
938
|
+
discard: nil,
|
|
939
|
+
comment_url: nil,
|
|
940
|
+
expires: nil,
|
|
941
|
+
max_age: nil,
|
|
942
|
+
comment: nil,
|
|
943
|
+
secure: nil,
|
|
944
|
+
path: nil,
|
|
945
|
+
domain: nil
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
name: 'c2name',
|
|
949
|
+
value: 'c2value',
|
|
950
|
+
version: 0,
|
|
951
|
+
port: nil,
|
|
952
|
+
discard: nil,
|
|
953
|
+
comment_url: nil,
|
|
954
|
+
expires: nil,
|
|
955
|
+
max_age: nil,
|
|
956
|
+
comment: nil,
|
|
957
|
+
secure: nil,
|
|
958
|
+
path: nil,
|
|
959
|
+
domain: nil
|
|
960
|
+
}
|
|
961
|
+
]
|
|
962
|
+
end
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
end
|