raptor-io 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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