heventmachine_httpserver 0.2.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.
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{heventmachine_httpserver}
3
+ s.version = "0.2.1"
4
+
5
+ s.specification_version = 1 if s.respond_to? :specification_version=
6
+
7
+ s.required_rubygems_version = nil if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Francis Cianfrocca"]
9
+ s.date = %q{2020-11-13}
10
+ s.description = %q{}
11
+ s.email = %q{hunglkt@gmail.com}
12
+ s.extensions = ["ext/extconf.rb"]
13
+ s.extra_rdoc_files = `git ls-files docs`.split
14
+ s.files = `git ls-files | grep -v .gitignore`.split
15
+ s.has_rdoc = true
16
+ s.homepage = %q{https://github.com/hunglkt/eventmachine_httpserver}
17
+ s.rdoc_options = ["--title", "EventMachine_HttpServer", "--main", "docs/README", "--line-numbers"]
18
+ s.require_paths = ["lib"]
19
+ s.required_ruby_version = Gem::Requirement.new("> 0.0.0")
20
+ s.rubygems_version = %q{1.3.1}
21
+ s.summary = %q{EventMachine HTTP Server}
22
+ end
@@ -0,0 +1,35 @@
1
+ # EventMachine HTTP Server
2
+ #
3
+ # Author:: blackhedd (gmail address: garbagecat10).
4
+ #
5
+ # Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
6
+ #
7
+ # This program is made available under the terms of the GPL version 2.
8
+ #
9
+ #----------------------------------------------------------------------------
10
+ #
11
+ # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
12
+ #
13
+ # Gmail: garbagecat10
14
+ #
15
+ # This program is free software; you can redistribute it and/or modify
16
+ # it under the terms of the GNU General Public License as published by
17
+ # the Free Software Foundation; either version 2 of the License, or
18
+ # (at your option) any later version.
19
+ #
20
+ # This program is distributed in the hope that it will be useful,
21
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
22
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
+ # GNU General Public License for more details.
24
+ #
25
+ # You should have received a copy of the GNU General Public License
26
+ # along with this program; if not, write to the Free Software
27
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
28
+ #
29
+ #---------------------------------------------------------------------------
30
+ #
31
+
32
+
33
+ require 'eventmachine_httpserver'
34
+ require 'evma_httpserver/response'
35
+
@@ -0,0 +1,356 @@
1
+ # EventMachine HTTP Server
2
+ # HTTP Response-support class
3
+ #
4
+ # Author:: blackhedd (gmail address: garbagecat10).
5
+ #
6
+ # Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
7
+ #
8
+ # This program is made available under the terms of the GPL version 2.
9
+ #
10
+ #----------------------------------------------------------------------------
11
+ #
12
+ # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
13
+ #
14
+ # Gmail: garbagecat10
15
+ #
16
+ # This program is free software; you can redistribute it and/or modify
17
+ # it under the terms of the GNU General Public License as published by
18
+ # the Free Software Foundation; either version 2 of the License, or
19
+ # (at your option) any later version.
20
+ #
21
+ # This program is distributed in the hope that it will be useful,
22
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
23
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
+ # GNU General Public License for more details.
25
+ #
26
+ # You should have received a copy of the GNU General Public License
27
+ # along with this program; if not, write to the Free Software
28
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
29
+ #
30
+ #---------------------------------------------------------------------------
31
+ #
32
+
33
+ module EventMachine
34
+
35
+ # This class provides a wide variety of features for generating and
36
+ # dispatching HTTP responses. It allows you to conveniently generate
37
+ # headers and content (including chunks and multiparts), and dispatch
38
+ # responses (including deferred or partially-complete responses).
39
+ #
40
+ # Although HttpResponse is coded as a class, it's not complete as it
41
+ # stands. It assumes that it has certain of the behaviors of
42
+ # EventMachine::Connection. You must add these behaviors, either by
43
+ # subclassing HttpResponse, or using the alternate version of this
44
+ # class, DelegatedHttpResponse. See the test cases for current information
45
+ # on which behaviors you have to add.
46
+ #
47
+ # TODO, someday it would be nice to provide a version of this functionality
48
+ # that is coded as a Module, so it can simply be mixed into an instance of
49
+ # EventMachine::Connection.
50
+ #
51
+ class HttpResponse
52
+ STATUS_CODES = {
53
+ 100 => "100 Continue",
54
+ 101 => "101 Switching Protocols",
55
+ 200 => "200 OK",
56
+ 201 => "201 Created",
57
+ 202 => "202 Accepted",
58
+ 203 => "203 Non-Authoritative Information",
59
+ 204 => "204 No Content",
60
+ 205 => "205 Reset Content",
61
+ 206 => "206 Partial Content",
62
+ 300 => "300 Multiple Choices",
63
+ 301 => "301 Moved Permanently",
64
+ 302 => "302 Found",
65
+ 303 => "303 See Other",
66
+ 304 => "304 Not Modified",
67
+ 305 => "305 Use Proxy",
68
+ 307 => "307 Temporary Redirect",
69
+ 400 => "400 Bad Request",
70
+ 401 => "401 Unauthorized",
71
+ 402 => "402 Payment Required",
72
+ 403 => "403 Forbidden",
73
+ 404 => "404 Not Found",
74
+ 405 => "405 Method Not Allowed",
75
+ 406 => "406 Not Acceptable",
76
+ 407 => "407 Proxy Authentication Required",
77
+ 408 => "408 Request Timeout",
78
+ 409 => "409 Conflict",
79
+ 410 => "410 Gone",
80
+ 411 => "411 Length Required",
81
+ 412 => "412 Precondition Failed",
82
+ 413 => "413 Request Entity Too Large",
83
+ 414 => "414 Request-URI Too Long",
84
+ 415 => "415 Unsupported Media Type",
85
+ 416 => "416 Requested Range Not Satisfiable",
86
+ 417 => "417 Expectation Failed",
87
+ 500 => "500 Internal Server Error",
88
+ 501 => "501 Not Implemented",
89
+ 502 => "502 Bad Gateway",
90
+ 503 => "503 Service Unavailable",
91
+ 504 => "504 Gateway Timeout",
92
+ 505 => "505 HTTP Version Not Supported"
93
+ }
94
+
95
+ attr_accessor :status, :headers, :chunks, :multiparts
96
+
97
+ def initialize
98
+ @headers = {}
99
+ end
100
+
101
+ def content=(value) @content = value.to_s end
102
+ def content() @content || '' end
103
+ def content?() !!@content end
104
+
105
+ def keep_connection_open arg=true
106
+ @keep_connection_open = arg
107
+ end
108
+
109
+ def status=(status)
110
+ @status = STATUS_CODES[status.to_i] || status
111
+ end
112
+
113
+ # sugarings for headers
114
+ def content_type *mime
115
+ if mime.length > 0
116
+ @headers["Content-Type"] = mime.first.to_s
117
+ else
118
+ @headers["Content-Type"]
119
+ end
120
+ end
121
+
122
+ # Sugaring for Set-Cookie headers. These are a pain because there can easily and
123
+ # legitimately be more than one. So we use an ugly verb to signify that.
124
+ # #add_set_cookies does NOT disturb the Set-Cookie headers which may have been
125
+ # added on a prior call. #set_cookie clears them out first.
126
+ def add_set_cookie *ck
127
+ if ck.length > 0
128
+ h = (@headers["Set-Cookie"] ||= [])
129
+ ck.each {|c| h << c}
130
+ end
131
+ end
132
+ def set_cookie *ck
133
+ h = (@headers["Set-Cookie"] ||= [])
134
+ if ck.length > 0
135
+ h.clear
136
+ add_set_cookie *ck
137
+ else
138
+ h
139
+ end
140
+ end
141
+
142
+
143
+ # This is intended to send a complete HTTP response, including closing the connection
144
+ # if appropriate at the end of the transmission. Don't use this method to send partial
145
+ # or iterated responses. This method will send chunks and multiparts provided they
146
+ # are all available when we get here.
147
+ # Note that the default @status is 200 if the value doesn't exist.
148
+ def send_response
149
+ send_headers
150
+ send_body
151
+ send_trailer
152
+ close_connection_after_writing unless (@keep_connection_open and (@status || "200 OK") == "200 OK")
153
+ end
154
+
155
+ # Send the headers out in alpha-sorted order. This will degrade performance to some
156
+ # degree, and is intended only to simplify the construction of unit tests.
157
+ #
158
+ def send_headers
159
+ raise "sent headers already" if @sent_headers
160
+ @sent_headers = true
161
+
162
+ fixup_headers
163
+
164
+ ary = []
165
+ ary << "HTTP/1.1 #{@status || '200 OK'}\r\n"
166
+ ary += generate_header_lines(@headers)
167
+ ary << "\r\n"
168
+
169
+ send_data ary.join
170
+ end
171
+
172
+
173
+ def generate_header_lines in_hash
174
+ out_ary = []
175
+ in_hash.keys.sort.each {|k|
176
+ v = in_hash[k]
177
+ if v.is_a?(Array)
178
+ v.each {|v1| out_ary << "#{k}: #{v1}\r\n" }
179
+ else
180
+ out_ary << "#{k}: #{v}\r\n"
181
+ end
182
+ }
183
+ out_ary
184
+ end
185
+ private :generate_header_lines
186
+
187
+
188
+ # Examine the content type and data and other things, and perform a final
189
+ # fixup of the header array. We expect this to be called just before sending
190
+ # headers to the remote peer.
191
+ # In the case of multiparts, we ASSUME we will get called before any content
192
+ # gets sent out, because the multipart boundary is created here.
193
+ #
194
+ def fixup_headers
195
+ if @content
196
+ @headers["Content-Length"] = @content.bytesize
197
+ elsif @chunks
198
+ @headers["Transfer-Encoding"] = "chunked"
199
+ # Might be nice to ENSURE there is no content-length header,
200
+ # but how to detect all the possible permutations of upper/lower case?
201
+ elsif @multiparts
202
+ @multipart_boundary = self.class.concoct_multipart_boundary
203
+ @headers["Content-Type"] = "multipart/x-mixed-replace; boundary=\"#{@multipart_boundary}\""
204
+ else
205
+ @headers["Content-Length"] = 0
206
+ end
207
+ end
208
+
209
+ # we send either content, chunks, or multiparts. Content can only be sent once.
210
+ # Chunks and multiparts can be sent any number of times.
211
+ # DO NOT close the connection or send any goodbye kisses. This method can
212
+ # be called multiple times to send out chunks or multiparts.
213
+ def send_body
214
+ if @chunks
215
+ send_chunks
216
+ elsif @multiparts
217
+ send_multiparts
218
+ else
219
+ send_content
220
+ end
221
+ end
222
+
223
+ # send a trailer which depends on the type of body we're dealing with.
224
+ # The assumption is that we're about to end the transmission of this particular
225
+ # HTTP response. (A connection-close may or may not follow.)
226
+ #
227
+ def send_trailer
228
+ send_headers unless @sent_headers
229
+ if @chunks
230
+ unless @last_chunk_sent
231
+ chunk ""
232
+ send_chunks
233
+ end
234
+ elsif @multiparts
235
+ # in the lingo of RFC 2046/5.1.1, we're sending an "epilog"
236
+ # consisting of a blank line. I really don't know how that is
237
+ # supposed to interact with the case where we leave the connection
238
+ # open after transmitting the multipart response.
239
+ send_data "\r\n--#{@multipart_boundary}--\r\n\r\n"
240
+ end
241
+ end
242
+
243
+ def send_content
244
+ raise "sent content already" if @sent_content
245
+ @sent_content = true
246
+ send_data(content)
247
+ end
248
+
249
+ # add a chunk to go to the output.
250
+ # Will cause the headers to pick up "content-transfer-encoding"
251
+ # Add the chunk to a list. Calling #send_chunks will send out the
252
+ # available chunks and clear the chunk list WITHOUT closing the connection,
253
+ # so it can be called any number of times.
254
+ # TODO!!! Per RFC2616, we may not send chunks to an HTTP/1.0 client.
255
+ # Raise an exception here if our user tries to do so.
256
+ # Chunked transfer coding is defined in RFC2616 pgh 3.6.1.
257
+ # The argument can be a string or a hash. The latter allows for
258
+ # sending chunks with extensions (someday).
259
+ #
260
+ def chunk text
261
+ @chunks ||= []
262
+ @chunks << text
263
+ end
264
+
265
+ # send the contents of the chunk list and clear it out.
266
+ # ASSUMES that headers have been sent.
267
+ # Does NOT close the connection.
268
+ # Can be called multiple times.
269
+ # According to RFC2616, phg 3.6.1, the last chunk will be zero length.
270
+ # But some caller could accidentally set a zero-length chunk in the middle
271
+ # of the stream. If that should happen, raise an exception.
272
+ # The reason for supporting chunks that are hashes instead of just strings
273
+ # is to enable someday supporting chunk-extension codes (cf the RFC).
274
+ # TODO!!! We're not supporting the final entity-header that may be
275
+ # transmitted after the last (zero-length) chunk.
276
+ #
277
+ def send_chunks
278
+ send_headers unless @sent_headers
279
+ while chunk = @chunks.shift
280
+ raise "last chunk already sent" if @last_chunk_sent
281
+ text = chunk.is_a?(Hash) ? chunk[:text] : chunk.to_s
282
+ send_data "#{format("%x", text.length).upcase}\r\n#{text}\r\n"
283
+ @last_chunk_sent = true if text.length == 0
284
+ end
285
+ end
286
+
287
+ # To add a multipart to the outgoing response, specify the headers and the
288
+ # body. If only a string is given, it's treated as the body (in this case,
289
+ # the header is assumed to be empty).
290
+ #
291
+ def multipart arg
292
+ vals = if arg.is_a?(String)
293
+ {:body => arg, :headers => {}}
294
+ else
295
+ arg
296
+ end
297
+
298
+ @multiparts ||= []
299
+ @multiparts << vals
300
+ end
301
+
302
+ # Multipart syntax is defined in RFC 2046, pgh 5.1.1 et seq.
303
+ # The CRLF which introduces the boundary line of each part (content entity)
304
+ # is defined as being part of the boundary, not of the preceding part.
305
+ # So we don't need to mess with interpreting the last bytes of a part
306
+ # to ensure they are CRLF-terminated.
307
+ #
308
+ def send_multiparts
309
+ send_headers unless @sent_headers
310
+ while part = @multiparts.shift
311
+ send_data "\r\n--#{@multipart_boundary}\r\n"
312
+ send_data( generate_header_lines( part[:headers] || {} ).join)
313
+ send_data "\r\n"
314
+ send_data part[:body].to_s
315
+ end
316
+ end
317
+
318
+ # TODO, this is going to be way too slow. Cache up the uuidgens.
319
+ #
320
+ def self.concoct_multipart_boundary
321
+ @multipart_index ||= 0
322
+ @multipart_index += 1
323
+ if @multipart_index >= 1000
324
+ @multipart_index = 0
325
+ @multipart_guid = nil
326
+ end
327
+ @multipart_guid ||= `uuidgen -r`.chomp.gsub(/\-/,"")
328
+ "#{@multipart_guid}#{@multipart_index}"
329
+ end
330
+
331
+ def send_redirect location
332
+ @status = 302 # TODO, make 301 available by parameter
333
+ @headers["Location"] = location
334
+ send_response
335
+ end
336
+ end
337
+ end
338
+
339
+ #----------------------------------------------------------------------------
340
+
341
+ require 'forwardable'
342
+
343
+ module EventMachine
344
+ class DelegatedHttpResponse < HttpResponse
345
+ extend Forwardable
346
+ def_delegators :@delegate,
347
+ :send_data,
348
+ :close_connection,
349
+ :close_connection_after_writing
350
+
351
+ def initialize dele
352
+ super()
353
+ @delegate = dele
354
+ end
355
+ end
356
+ end
@@ -0,0 +1,239 @@
1
+ require 'test/unit'
2
+ require 'evma_httpserver'
3
+
4
+ begin
5
+ once = false
6
+ require 'eventmachine'
7
+ rescue LoadError => e
8
+ raise e if once
9
+ once = true
10
+ require 'rubygems'
11
+ retry
12
+ end
13
+
14
+
15
+
16
+ #--------------------------------------
17
+
18
+ module EventMachine
19
+ module HttpServer
20
+ def process_http_request
21
+ send_data generate_response()
22
+ close_connection_after_writing
23
+ end
24
+ end
25
+ end
26
+
27
+ #--------------------------------------
28
+
29
+ require 'socket'
30
+
31
+ class TestApp < Test::Unit::TestCase
32
+
33
+ TestHost = "127.0.0.1"
34
+ TestPort = 8911
35
+
36
+ TestResponse_1 = <<EORESP
37
+ HTTP/1.0 200 OK
38
+ Content-length: 4
39
+ Content-type: text/plain
40
+ Connection: close
41
+
42
+ 1234
43
+ EORESP
44
+
45
+ Thread.abort_on_exception = true
46
+
47
+ def test_simple_get
48
+ received_response = nil
49
+
50
+ EventMachine::HttpServer.module_eval do
51
+ def generate_response
52
+ TestResponse_1
53
+ end
54
+ end
55
+
56
+
57
+ EventMachine.run do
58
+ EventMachine.start_server TestHost, TestPort, EventMachine::HttpServer
59
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
60
+
61
+ cb = proc do
62
+ tcp = TCPSocket.new TestHost, TestPort
63
+ tcp.write "GET / HTTP/1.0\r\n\r\n"
64
+ received_response = tcp.read
65
+ end
66
+ eb = proc { EventMachine.stop }
67
+ EventMachine.defer cb, eb
68
+ end
69
+
70
+ assert_equal( TestResponse_1, received_response )
71
+ end
72
+
73
+
74
+
75
+
76
+ # This frowsy-looking protocol handler allows the test harness to make some
77
+ # its local variables visible, so we can set them here and they can be asserted later.
78
+ class MyTestServer < EventMachine::Connection
79
+ include EventMachine::HttpServer
80
+ def initialize *args
81
+ super
82
+ end
83
+ def generate_response
84
+ @assertions.call
85
+ TestResponse_1
86
+ end
87
+ end
88
+
89
+
90
+
91
+ def test_parameters
92
+ path_info = "/test.html"
93
+ query_string = "a=b&c=d"
94
+ cookie = "eat_me=I'm a cookie"
95
+ etag = "12345"
96
+
97
+ # collect all the stuff we want to assert outside the actual test,
98
+ # to ensure it gets asserted even if the test catches some exception.
99
+ received_response = nil
100
+ request_parms = {}
101
+
102
+
103
+ EventMachine.run do
104
+ EventMachine.start_server(TestHost, TestPort, MyTestServer) do |conn|
105
+ # In each accepted connection, set up a procedure that will copy
106
+ # the request parameters into a local variable visible here, so
107
+ # we can assert the values later.
108
+ conn.instance_eval do
109
+ @assertions = proc {
110
+ parms = %w( PATH_INFO QUERY_STRING HTTP_COOKIE IF_NONE_MATCH
111
+ CONTENT_TYPE REQUEST_METHOD REQUEST_URI )
112
+ parms.each {|parm|
113
+ # request_parms is bound to a local variable visible in this context.
114
+ request_parms[parm] = ENV[parm]
115
+ }
116
+ }
117
+ end
118
+ end
119
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
120
+
121
+ cb = proc do
122
+ tcp = TCPSocket.new TestHost, TestPort
123
+ data = [
124
+ "GET #{path_info}?#{query_string} HTTP/1.1\r\n",
125
+ "Cookie: #{cookie}\r\n",
126
+ "If-none-match: #{etag}\r\n",
127
+ "\r\n"
128
+ ].join
129
+ tcp.write(data)
130
+ received_response = tcp.read
131
+ end
132
+ eb = proc { EventMachine.stop }
133
+ EventMachine.defer cb, eb
134
+ end
135
+
136
+ assert_equal( TestResponse_1, received_response )
137
+ assert_equal( path_info, request_parms["PATH_INFO"] )
138
+ assert_equal( query_string, request_parms["QUERY_STRING"] )
139
+ assert_equal( cookie, request_parms["HTTP_COOKIE"] )
140
+ assert_equal( etag, request_parms["IF_NONE_MATCH"] )
141
+ assert_equal( nil, request_parms["CONTENT_TYPE"] )
142
+ assert_equal( "GET", request_parms["REQUEST_METHOD"] )
143
+ assert_equal( path_info, request_parms["REQUEST_URI"] )
144
+ end
145
+
146
+
147
+ def test_headers
148
+ received_header_string = nil
149
+ received_header_ary = nil
150
+
151
+ EventMachine.run do
152
+ EventMachine.start_server(TestHost, TestPort, MyTestServer) do |conn|
153
+ # In each accepted connection, set up a procedure that will copy
154
+ # the request parameters into a local variable visible here, so
155
+ # we can assert the values later.
156
+ # The @http_headers is set automatically and can easily be parsed.
157
+ # It isn't automatically parsed into Ruby values because that is
158
+ # a costly operation, but we should provide an optional method that
159
+ # does the parsing so it doesn't need to be done by users.
160
+ conn.instance_eval do
161
+ @assertions = proc do
162
+ received_header_string = @http_headers
163
+ received_header_ary = @http_headers.split(/\0/).map {|line| line.split(/:\s*/, 2) }
164
+ end
165
+ end
166
+ end
167
+
168
+ cb = proc do
169
+ tcp = TCPSocket.new TestHost, TestPort
170
+ data = [
171
+ "GET / HTTP/1.1\r\n",
172
+ "aaa: 111\r\n",
173
+ "bbb: 222\r\n",
174
+ "ccc: 333\r\n",
175
+ "ddd: 444\r\n",
176
+ "\r\n"
177
+ ].join
178
+ tcp.write data
179
+ received_response = tcp.read
180
+ end
181
+ eb = proc { EventMachine.stop }
182
+ EventMachine.defer cb, eb
183
+
184
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
185
+ end
186
+
187
+ assert_equal( "aaa: 111\0bbb: 222\0ccc: 333\0ddd: 444\0\0", received_header_string )
188
+ assert_equal( [["aaa","111"], ["bbb","222"], ["ccc","333"], ["ddd","444"]], received_header_ary )
189
+ end
190
+
191
+
192
+
193
+
194
+
195
+ def test_post
196
+ received_header_string = nil
197
+ post_content = "1234567890"
198
+ content_type = "text/plain"
199
+ received_post_content = ""
200
+ received_content_type = ""
201
+
202
+ EventMachine.run do
203
+ EventMachine.start_server(TestHost, TestPort, MyTestServer) do |conn|
204
+ # In each accepted connection, set up a procedure that will copy
205
+ # the request parameters into a local variable visible here, so
206
+ # we can assert the values later.
207
+ # The @http_post_content variable is set automatically.
208
+ conn.instance_eval do
209
+ @assertions = proc do
210
+ received_post_content = @http_post_content
211
+ received_content_type = ENV["CONTENT_TYPE"]
212
+ end
213
+ end
214
+ end
215
+ EventMachine.add_timer(1) {raise "timed out"} # make sure the test completes
216
+
217
+ cb = proc do
218
+ tcp = TCPSocket.new TestHost, TestPort
219
+ data = [
220
+ "POST / HTTP/1.1\r\n",
221
+ "Content-type: #{content_type}\r\n",
222
+ "Content-length: #{post_content.length}\r\n",
223
+ "\r\n",
224
+ post_content
225
+ ].join
226
+ tcp.write(data)
227
+ received_response = tcp.read
228
+ end
229
+ eb = proc do
230
+ EventMachine.stop
231
+ end
232
+ EventMachine.defer cb, eb
233
+ end
234
+
235
+ assert_equal( received_post_content, post_content )
236
+ assert_equal( received_content_type, content_type )
237
+ end
238
+
239
+ end