You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

http_io_spec.rb 11 kB

2 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. # frozen_string_literal: true
  2. require 'stringio'
  3. require 'webrick'
  4. require 'webrick/https'
  5. require 'spec_helper'
  6. require 'cucumber/formatter/io'
  7. module WEBrick
  8. module HTTPServlet
  9. class ProcHandler < AbstractServlet
  10. alias do_PUT do_GET # Webrick #mount_proc only works with GET,HEAD,POST,OPTIONS by default
  11. end
  12. end
  13. end
  14. RSpec.shared_context 'an HTTP server accepting file requests' do
  15. let(:putreport_returned_location) { URI('/s3').to_s }
  16. let(:success_banner) do
  17. [
  18. 'View your Cucumber Report at:',
  19. 'https://reports.cucumber.io/reports/<some-random-uid>'
  20. ].join("\n")
  21. end
  22. let(:failure_banner) { 'Oh noooo, something went horribly wrong :(' }
  23. # rubocop:disable Metrics/MethodLength
  24. # rubocop:disable Metrics/AbcSize
  25. def start_server
  26. uri = URI('http://localhost')
  27. @received_body_io = StringIO.new
  28. @received_headers = []
  29. @request_count = 0
  30. rd, wt = IO.pipe
  31. webrick_options = {
  32. Port: 0,
  33. Logger: WEBrick::Log.new(File.open(File::NULL, 'w')),
  34. AccessLog: [],
  35. StartCallback: proc do
  36. wt.write(1) # write "1", signal a server start message
  37. wt.close
  38. end
  39. }
  40. if uri.scheme == 'https'
  41. webrick_options[:SSLEnable] = true
  42. # Set up a self-signed cert
  43. webrick_options[:SSLCertName] = [%w[CN localhost]]
  44. end
  45. @server = WEBrick::HTTPServer.new(webrick_options)
  46. @server.mount_proc '/s3' do |req, res|
  47. @request_count += 1
  48. IO.copy_stream(req.body_reader, @received_body_io)
  49. @received_headers << req.header
  50. if req['authorization']
  51. res.status = 400
  52. res.body = 'Do not send Authorization header to S3'
  53. end
  54. end
  55. @server.mount_proc '/404' do |req, res|
  56. @request_count += 1
  57. @received_headers << req.header
  58. res.status = 404
  59. res.header['Content-Type'] = 'text/plain;charset=utf-8'
  60. res.body = failure_banner
  61. end
  62. @server.mount_proc '/401' do |req, res|
  63. @request_count += 1
  64. @received_headers << req.header
  65. res.status = 401
  66. res.header['Content-Type'] = 'text/plain;charset=utf-8'
  67. res.body = failure_banner
  68. end
  69. @server.mount_proc '/putreport' do |req, res|
  70. @request_count += 1
  71. IO.copy_stream(req.body_reader, @received_body_io)
  72. @received_headers << req.header
  73. if req.request_method == 'GET'
  74. res.status = 202 # Accepted
  75. res.header['location'] = putreport_returned_location if putreport_returned_location
  76. res.header['Content-Type'] = 'text/plain;charset=utf-8'
  77. res.body = success_banner
  78. else
  79. res.set_redirect(
  80. WEBrick::HTTPStatus::TemporaryRedirect,
  81. '/s3'
  82. )
  83. end
  84. end
  85. @server.mount_proc '/loop_redirect' do |req, res|
  86. @request_count += 1
  87. @received_headers << req.header
  88. res.set_redirect(
  89. WEBrick::HTTPStatus::TemporaryRedirect,
  90. '/loop_redirect'
  91. )
  92. end
  93. Thread.new do
  94. @server.start
  95. end
  96. rd.read(1) # read a byte for the server start signal
  97. rd.close
  98. "http://localhost:#{@server.config[:Port]}"
  99. end
  100. # rubocop:enable Metrics/MethodLength
  101. # rubocop:enable Metrics/AbcSize
  102. after do
  103. @server&.shutdown
  104. end
  105. end
  106. module Cucumber
  107. module Formatter
  108. class DummyFormatter
  109. include Io
  110. def initialize(config = nil); end
  111. def io(path_or_url_or_io, error_stream)
  112. ensure_io(path_or_url_or_io, error_stream)
  113. end
  114. end
  115. class DummyReporter
  116. def report(banner); end
  117. end
  118. describe HTTPIO do
  119. include_context 'an HTTP server accepting file requests'
  120. context 'created by Io#ensure_io' do
  121. it 'returns a IOHTTPBuffer' do
  122. url = start_server
  123. io = DummyFormatter.new.io("#{url}/s3 -X PUT", nil)
  124. expect(io).to be_a(Cucumber::Formatter::IOHTTPBuffer)
  125. io.close # Close during the test so the request is done while server still runs
  126. end
  127. it 'uses CurlOptionParser to pass correct options to IOHTTPBuffer' do
  128. url = start_server
  129. io = DummyFormatter.new.io("#{url}/s3 -X GET -H 'Content-Type: text/json'", nil)
  130. expect(io.uri).to eq(URI("#{url}/s3"))
  131. expect(io.method).to eq('GET')
  132. expect(io.headers).to eq('Content-Type' => 'text/json')
  133. io.close # Close during the test so the request is done while server still runs
  134. end
  135. end
  136. end
  137. describe CurlOptionParser do
  138. context '.parse' do
  139. context 'when a simple URL is given' do
  140. it 'returns the URL' do
  141. url, = CurlOptionParser.parse('http://whatever.ltd')
  142. expect(url).to eq('http://whatever.ltd')
  143. end
  144. it 'uses PUT as the default method' do
  145. _, http_method = CurlOptionParser.parse('http://whatever.ltd')
  146. expect(http_method).to eq('PUT')
  147. end
  148. it 'does not specify any header' do
  149. _, _, headers = CurlOptionParser.parse('http://whatever.ltd')
  150. expect(headers).to eq({})
  151. end
  152. end
  153. it 'detects the HTTP method with the flag -X' do
  154. expect(CurlOptionParser.parse('http://whatever.ltd -X POST')).to eq(
  155. ['http://whatever.ltd', 'POST', {}]
  156. )
  157. expect(CurlOptionParser.parse('http://whatever.ltd -X PUT')).to eq(
  158. ['http://whatever.ltd', 'PUT', {}]
  159. )
  160. end
  161. it 'detects the HTTP method with the flag --request' do
  162. expect(CurlOptionParser.parse('http://whatever.ltd --request GET')).to eq(
  163. ['http://whatever.ltd', 'GET', {}]
  164. )
  165. end
  166. it 'can recognize headers set with option -H and double quote' do
  167. expect(CurlOptionParser.parse('http://whatever.ltd -H "Content-Type: text/json" -H "Authorization: Bearer abcde"')).to eq(
  168. [
  169. 'http://whatever.ltd',
  170. 'PUT',
  171. {
  172. 'Content-Type' => 'text/json',
  173. 'Authorization' => 'Bearer abcde'
  174. }
  175. ]
  176. )
  177. end
  178. it 'can recognize headers set with option -H and single quote' do
  179. expect(CurlOptionParser.parse("http://whatever.ltd -H 'Content-Type: text/json' -H 'Content-Length: 12'")).to eq(
  180. [
  181. 'http://whatever.ltd',
  182. 'PUT',
  183. {
  184. 'Content-Type' => 'text/json',
  185. 'Content-Length' => '12'
  186. }
  187. ]
  188. )
  189. end
  190. it 'supports all options at once' do
  191. expect(CurlOptionParser.parse('http://whatever.ltd -H "Content-Type: text/json" -X GET -H "Transfer-Encoding: chunked"')).to eq(
  192. [
  193. 'http://whatever.ltd',
  194. 'GET',
  195. {
  196. 'Content-Type' => 'text/json',
  197. 'Transfer-Encoding' => 'chunked'
  198. }
  199. ]
  200. )
  201. end
  202. end
  203. end
  204. describe IOHTTPBuffer do
  205. include_context 'an HTTP server accepting file requests'
  206. let(:url) { start_server }
  207. # JRuby seems to have some issues with huge reports. At least during tests
  208. # Maybe something to see with Webrick configuration.
  209. let(:report_size) { RUBY_PLATFORM == 'java' ? 8_000 : 10_000_000 }
  210. let(:sent_body) { 'X' * report_size }
  211. it 'raises an error on close when server in unreachable' do
  212. io = IOHTTPBuffer.new("#{url}/404", 'PUT')
  213. expect { io.close }.to(raise_error("request to #{url}/404 failed with status 404"))
  214. end
  215. it 'raises an error on close when the server is unreachable' do
  216. io = IOHTTPBuffer.new('http://localhost:9987', 'PUT')
  217. expect { io.close }.to(raise_error(/Failed to open TCP connection to localhost:9987/))
  218. end
  219. it 'raises an error on close when there is too many redirect attempts' do
  220. io = IOHTTPBuffer.new("#{url}/loop_redirect", 'PUT')
  221. expect { io.close }.to(raise_error("request to #{url}/loop_redirect failed (too many redirections)"))
  222. end
  223. it 'sends the content over HTTP' do
  224. io = IOHTTPBuffer.new("#{url}/s3", 'PUT')
  225. io.write(sent_body)
  226. io.flush
  227. io.close
  228. @received_body_io.rewind
  229. received_body = @received_body_io.read
  230. expect(received_body).to eq(sent_body)
  231. end
  232. it 'sends the content over HTTPS' do
  233. io = IOHTTPBuffer.new("#{url}/s3", 'PUT', {}, OpenSSL::SSL::VERIFY_NONE)
  234. io.write(sent_body)
  235. io.flush
  236. io.close
  237. @received_body_io.rewind
  238. received_body = @received_body_io.read
  239. expect(received_body).to eq(sent_body)
  240. end
  241. it 'follows redirections and sends body twice' do
  242. io = IOHTTPBuffer.new("#{url}/putreport", 'PUT')
  243. io.write(sent_body)
  244. io.flush
  245. io.close
  246. @received_body_io.rewind
  247. received_body = @received_body_io.read
  248. expect(received_body).to eq("#{sent_body}#{sent_body}")
  249. end
  250. it 'only sends body once' do
  251. io = IOHTTPBuffer.new("#{url}/putreport", 'GET')
  252. io.write(sent_body)
  253. io.flush
  254. io.close
  255. @received_body_io.rewind
  256. received_body = @received_body_io.read
  257. expect(received_body).to eq(sent_body)
  258. end
  259. it 'does not send headers to 2nd PUT request' do
  260. io = IOHTTPBuffer.new("#{url}/putreport", 'GET', { Authorization: 'Bearer abcdefg' })
  261. io.write(sent_body)
  262. io.flush
  263. io.close
  264. expect(@received_headers[0]['authorization']).to eq(['Bearer abcdefg'])
  265. expect(@received_headers[1]['authorization']).to eq([])
  266. end
  267. it 'reports the body of the response to the reporter' do
  268. reporter = DummyReporter.new
  269. allow(reporter).to receive(:report)
  270. io = IOHTTPBuffer.new("#{url}/putreport", 'GET', {}, nil, reporter)
  271. io.write(sent_body)
  272. io.flush
  273. io.close
  274. expect(reporter).to have_received(:report).with(success_banner)
  275. end
  276. it 'reports the body of the response to the reporter when request failed' do
  277. reporter = DummyReporter.new
  278. allow(reporter).to receive(:report)
  279. begin
  280. io = IOHTTPBuffer.new("#{url}/401", 'GET', {}, nil, reporter)
  281. io.write(sent_body)
  282. io.flush
  283. io.close
  284. rescue StandardError
  285. # no-op
  286. end
  287. expect(reporter).to have_received(:report).with(failure_banner)
  288. end
  289. context 'when the location http header is not set on 202 response' do
  290. let(:putreport_returned_location) { nil }
  291. it 'does not follow the location' do
  292. io = IOHTTPBuffer.new("#{url}/putreport", 'GET')
  293. io.write(sent_body)
  294. io.flush
  295. io.close
  296. @received_body_io.rewind
  297. received_body = @received_body_io.read
  298. expect(received_body).to eq('')
  299. end
  300. end
  301. end
  302. end
  303. end

No Description

Contributors (1)