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.
Files changed (91) hide show
  1. checksums.yaml +15 -0
  2. data/LICENSE +30 -0
  3. data/README.md +51 -0
  4. data/lib/rack/handler/raptor-io.rb +130 -0
  5. data/lib/raptor-io.rb +11 -0
  6. data/lib/raptor-io/error.rb +19 -0
  7. data/lib/raptor-io/protocol.rb +6 -0
  8. data/lib/raptor-io/protocol/error.rb +10 -0
  9. data/lib/raptor-io/protocol/http.rb +34 -0
  10. data/lib/raptor-io/protocol/http/client.rb +685 -0
  11. data/lib/raptor-io/protocol/http/error.rb +16 -0
  12. data/lib/raptor-io/protocol/http/headers.rb +132 -0
  13. data/lib/raptor-io/protocol/http/message.rb +67 -0
  14. data/lib/raptor-io/protocol/http/request.rb +307 -0
  15. data/lib/raptor-io/protocol/http/request/manipulator.rb +117 -0
  16. data/lib/raptor-io/protocol/http/request/manipulators.rb +217 -0
  17. data/lib/raptor-io/protocol/http/request/manipulators/authenticator.rb +110 -0
  18. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/basic.rb +36 -0
  19. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/digest.rb +135 -0
  20. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/negotiate.rb +69 -0
  21. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/ntlm.rb +29 -0
  22. data/lib/raptor-io/protocol/http/request/manipulators/redirect_follower.rb +65 -0
  23. data/lib/raptor-io/protocol/http/response.rb +166 -0
  24. data/lib/raptor-io/protocol/http/server.rb +446 -0
  25. data/lib/raptor-io/ruby.rb +4 -0
  26. data/lib/raptor-io/ruby/hash.rb +24 -0
  27. data/lib/raptor-io/ruby/ipaddr.rb +15 -0
  28. data/lib/raptor-io/ruby/openssl.rb +23 -0
  29. data/lib/raptor-io/ruby/string.rb +27 -0
  30. data/lib/raptor-io/socket.rb +175 -0
  31. data/lib/raptor-io/socket/comm.rb +143 -0
  32. data/lib/raptor-io/socket/comm/local.rb +94 -0
  33. data/lib/raptor-io/socket/comm/sapni.rb +75 -0
  34. data/lib/raptor-io/socket/comm/socks.rb +237 -0
  35. data/lib/raptor-io/socket/comm_chain.rb +30 -0
  36. data/lib/raptor-io/socket/error.rb +45 -0
  37. data/lib/raptor-io/socket/switch_board.rb +183 -0
  38. data/lib/raptor-io/socket/switch_board/route.rb +42 -0
  39. data/lib/raptor-io/socket/tcp.rb +231 -0
  40. data/lib/raptor-io/socket/tcp/ssl.rb +77 -0
  41. data/lib/raptor-io/socket/tcp_server.rb +16 -0
  42. data/lib/raptor-io/socket/tcp_server/ssl.rb +52 -0
  43. data/lib/raptor-io/socket/udp.rb +0 -0
  44. data/lib/raptor-io/version.rb +6 -0
  45. data/lib/tasks/yard.rake +26 -0
  46. data/spec/rack/handler/raptor_spec.rb +140 -0
  47. data/spec/raptor-io/protocol/http/client_spec.rb +671 -0
  48. data/spec/raptor-io/protocol/http/headers_spec.rb +189 -0
  49. data/spec/raptor-io/protocol/http/message_spec.rb +5 -0
  50. data/spec/raptor-io/protocol/http/request/manipulators/authenticator_spec.rb +193 -0
  51. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/basic_spec.rb +32 -0
  52. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/digest_spec.rb +76 -0
  53. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/negotiate_spec.rb +52 -0
  54. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/ntlm_spec.rb +37 -0
  55. data/spec/raptor-io/protocol/http/request/manipulators/redirect_follower_spec.rb +51 -0
  56. data/spec/raptor-io/protocol/http/request/manipulators_spec.rb +202 -0
  57. data/spec/raptor-io/protocol/http/request_spec.rb +965 -0
  58. data/spec/raptor-io/protocol/http/response_spec.rb +236 -0
  59. data/spec/raptor-io/protocol/http/server_spec.rb +345 -0
  60. data/spec/raptor-io/ruby/hash_spec.rb +20 -0
  61. data/spec/raptor-io/ruby/string_spec.rb +20 -0
  62. data/spec/raptor-io/socket/comm/local_spec.rb +50 -0
  63. data/spec/raptor-io/socket/switch_board/route_spec.rb +49 -0
  64. data/spec/raptor-io/socket/switch_board_spec.rb +87 -0
  65. data/spec/raptor-io/socket/tcp/ssl_spec.rb +18 -0
  66. data/spec/raptor-io/socket/tcp_server/ssl_spec.rb +59 -0
  67. data/spec/raptor-io/socket/tcp_server_spec.rb +19 -0
  68. data/spec/raptor-io/socket/tcp_spec.rb +14 -0
  69. data/spec/raptor-io/socket_spec.rb +16 -0
  70. data/spec/raptor-io/version_spec.rb +10 -0
  71. data/spec/spec_helper.rb +56 -0
  72. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/manifoolators/fooer.rb +25 -0
  73. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/niccolo_machiavelli.rb +20 -0
  74. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/options_validator.rb +28 -0
  75. data/spec/support/fixtures/raptor/socket/ssl_server.crt +18 -0
  76. data/spec/support/fixtures/raptor/socket/ssl_server.key +15 -0
  77. data/spec/support/lib/path_helpers.rb +11 -0
  78. data/spec/support/lib/webserver_option_parser.rb +26 -0
  79. data/spec/support/lib/webservers.rb +120 -0
  80. data/spec/support/shared/contexts/with_ssl_server.rb +70 -0
  81. data/spec/support/shared/contexts/with_tcp_server.rb +58 -0
  82. data/spec/support/shared/examples/raptor/comm_examples.rb +26 -0
  83. data/spec/support/shared/examples/raptor/protocols/http/message.rb +106 -0
  84. data/spec/support/shared/examples/raptor/socket_examples.rb +135 -0
  85. data/spec/support/webservers/raptor/protocols/http/client.rb +100 -0
  86. data/spec/support/webservers/raptor/protocols/http/client_close_connection.rb +29 -0
  87. data/spec/support/webservers/raptor/protocols/http/client_https.rb +43 -0
  88. data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/basic.rb +9 -0
  89. data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/digest.rb +22 -0
  90. data/spec/support/webservers/raptor/protocols/http/request/manipulators/redirect_follower.rb +11 -0
  91. 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