From 66e3e9b958532d58c821ff519dc0524cd5c74d3b Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 11 Sep 2023 11:06:42 +1200 Subject: [PATCH] Migrate tests from `rspec` -> `sus`. --- config/sus.rb | 5 +- fixtures/async/http/a_protocol.rb | 566 ++++++++++++++++++++ fixtures/async/http/body/a_writable_body.rb | 110 ++++ gems.rb | 2 + test/async/http/body.rb | 54 +- test/async/http/body/hijack.rb | 36 +- test/async/http/body/pipe.rb | 63 +-- test/async/http/body/slowloris.rb | 25 +- test/async/http/body/writable.rb | 13 +- test/async/http/body/writable_examples.rb | 114 ---- test/async/http/client.rb | 55 +- test/async/http/client/codeotaku.rb | 42 ++ test/async/http/client/google.rb | 12 +- test/async/http/endpoint.rb | 158 +++--- test/async/http/internet.rb | 22 +- test/async/http/internet/instance.rb | 5 +- test/async/http/performance.rb | 89 --- test/async/http/protocol/http10.rb | 6 +- test/async/http/protocol/http11.rb | 113 ++-- test/async/http/protocol/http11/desync.rb | 24 +- test/async/http/protocol/http2.rb | 148 ++--- test/async/http/protocol/shared_examples.rb | 553 ------------------- test/async/http/proxy.rb | 82 +-- test/async/http/relative_location.rb | 63 ++- test/async/http/retry.rb | 27 +- test/async/http/server_context.rb | 59 -- test/async/http/ssl.rb | 44 +- test/async/http/statistics.rb | 18 +- 28 files changed, 1208 insertions(+), 1300 deletions(-) create mode 100644 fixtures/async/http/a_protocol.rb create mode 100644 fixtures/async/http/body/a_writable_body.rb delete mode 100644 test/async/http/body/writable_examples.rb create mode 100644 test/async/http/client/codeotaku.rb delete mode 100755 test/async/http/performance.rb delete mode 100644 test/async/http/protocol/shared_examples.rb delete mode 100644 test/async/http/server_context.rb diff --git a/config/sus.rb b/config/sus.rb index cf784c2f..ee30cfcf 100644 --- a/config/sus.rb +++ b/config/sus.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2023, by Samuel Williams. +# Copyright, 2017-2023, by Samuel Williams. +# Copyright, 2018, by Janko Marohnić. + +ENV['CONSOLE_LEVEL'] ||= 'fatal' require 'covered/sus' include Covered::Sus diff --git a/fixtures/async/http/a_protocol.rb b/fixtures/async/http/a_protocol.rb new file mode 100644 index 00000000..a151efea --- /dev/null +++ b/fixtures/async/http/a_protocol.rb @@ -0,0 +1,566 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2018-2023, by Samuel Williams. +# Copyright, 2020, by Igor Sidorov. + +require 'async' +require 'async/clock' +require 'async/http/client' +require 'async/http/server' +require 'async/http/endpoint' +require 'async/http/body/hijack' +require 'tempfile' + +require 'protocol/http/body/file' + +require 'sus/fixtures/async/http' + +module Async + module HTTP + AProtocol = Sus::Shared("a protocol") do + include Sus::Fixtures::Async::HTTP::ServerContext + + let(:protocol) {subject} + + it "should have valid scheme" do + expect(client.scheme).to be == "http" + end + + with '#close' do + it 'can close the connection' do + Async do |task| + response = client.get("/") + expect(response).to be(:success?) + response.finish + + client.close + + expect(task.children).to be(:empty?) + end.wait + end + end + + with "huge body", timeout: 600 do + let(:body) {::Protocol::HTTP::Body::File.open("/dev/zero", size: 512*1024**2)} + + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[200, {}, body] + end + end + + it "client can download data quickly" do + response = client.get("/") + expect(response).to be(:success?) + + data_size = 0 + duration = Async::Clock.measure do + while chunk = response.body.read + data_size += chunk.bytesize + chunk.clear + end + + response.finish + end + + size_mbytes = data_size / 1024**2 + + inform "Data size: #{size_mbytes}MB Duration: #{duration.round(2)}s Throughput: #{(size_mbytes / duration).round(2)}MB/s" + end + end + + with 'buffered body' do + let(:body) {Async::HTTP::Body::Buffered.new(["Hello World"])} + let(:response) {::Protocol::HTTP::Response[200, {}, body]} + + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + response + end + end + + it "response body should be closed" do + expect(body).to receive(:close) + # expect(response).to receive(:close) + + expect(client.get("/", {}).read).to be == "Hello World" + end + end + + with 'empty body' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[204] + end + end + + it 'properly handles no content responses' do + expect(client.get("/", {}).read).to be_nil + end + end + + with 'with trailer' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + if trailer = request.headers['trailer'] + expect(request.headers).not.to have_keys('etag') + request.finish + expect(request.headers).to have_keys('etag') + + ::Protocol::HTTP::Response[200, [], "request trailer"] + else + headers = ::Protocol::HTTP::Headers.new + headers.add('trailer', 'etag') + + body = Async::HTTP::Body::Writable.new + + Async do |task| + body.write("response trailer") + task.sleep(0.01) + headers.add('etag', 'abcd') + body.close + end + + ::Protocol::HTTP::Response[200, headers, body] + end + end + end + + it "can send request trailer" do + skip "Protocol does not support trailers!" unless subject.bidirectional? + + headers = ::Protocol::HTTP::Headers.new + headers.add('trailer', 'etag') + body = Async::HTTP::Body::Writable.new + + Async do |task| + body.write("Hello") + task.sleep(0.01) + headers.add('etag', 'abcd') + body.close + end + + response = client.post("/", headers, body) + expect(response.read).to be == "request trailer" + + expect(response).to be(:success?) + end + + it "can receive response trailer" do + skip "Protocol does not support trailers!" unless subject.bidirectional? + + response = client.get("/") + expect(response.headers).to have_keys('trailer') + headers = response.headers + expect(headers).not.to have_keys('etag') + + expect(response.read).to be == "response trailer" + expect(response).to be(:success?) + + # It was sent as a trailer. + expect(headers).to have_keys('etag') + end + end + + with 'with working server' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + if request.method == 'POST' + # We stream the request body directly to the response. + ::Protocol::HTTP::Response[200, {}, request.body] + elsif request.method == 'GET' + expect(request.body).to be_nil + + ::Protocol::HTTP::Response[200, { + 'remote-address' => request.remote_address.inspect + }, ["#{request.method} #{request.version}"]] + else + ::Protocol::HTTP::Response[200, {}, ["Hello World"]] + end + end + end + + it "should have valid scheme" do + expect(server.scheme).to be == "http" + end + + it "disconnects slow clients" do + response = client.get("/") + response.read + + # We expect this connection to be closed: + connection = response.connection + + reactor.sleep(1.0) + + response = client.get("/") + response.read + + expect(connection).not.to be(:reusable?) + + # client.close + # reactor.sleep(0.1) + # reactor.print_hierarchy + end + + with 'using GET method' do + let(:expected) {"GET #{protocol::VERSION}"} + + it "can handle many simultaneous requests" do + duration = Async::Clock.measure do + 10.times do + tasks = 100.times.collect do + Async do + client.get("/") + end + end + + tasks.each do |task| + response = task.wait + expect(response).to be(:success?) + expect(response.read).to be == expected + end + end + end + + inform "Pool: #{client.pool}" + inform "Duration: #{duration.round(2)}" + end + + with 'with response' do + let(:response) {client.get("/")} + + def after + response.finish + super + end + + it "can finish gracefully" do + expect(response).to be(:success?) + end + + it "is successful" do + expect(response).to be(:success?) + expect(response.read).to be == expected + end + + it "provides content length" do + expect(response.body.length).not.to be_nil + end + + let(:tempfile) {Tempfile.new} + + it "can save to disk" do + response.save(tempfile.path) + expect(tempfile.read).to be == expected + + tempfile.close + end + + it "has remote-address header" do + expect(response.headers['remote-address']).not.to be_nil + end + + it "has protocol version" do + expect(response.version).not.to be_nil + end + end + end + + with 'HEAD' do + let(:response) {client.head("/")} + + it "is successful and without body" do + expect(response).to be(:success?) + expect(response.body).not.to be_nil + expect(response.body).to be(:empty?) + expect(response.body.length).not.to be_nil + expect(response.read).to be_nil + end + end + + with 'POST' do + let(:response) {client.post("/", {}, ["Hello", " ", "World"])} + + def after + response.finish + super + end + + it "is successful" do + expect(response).to be(:success?) + expect(response.read).to be == "Hello World" + expect(client.pool).not.to be(:busy?) + end + + it "can buffer response" do + buffer = response.finish + + expect(buffer.join).to be == "Hello World" + + expect(client.pool).not.to be(:busy?) + end + + it "should not contain content-length response header" do + expect(response.headers).not.to have_keys('content-length') + end + + it "fails gracefully when closing connection" do + client.pool.acquire do |connection| + connection.stream.close + end + end + end + end + + with 'content length' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[200, [], ["Content Length: #{request.body.length}"]] + end + end + + it "can send push promises" do + response = client.post("/test", [], ["Hello World!"]) + expect(response).to be(:success?) + + expect(response.body.length).to be == 18 + expect(response.read).to be == "Content Length: 12" + end + end + + with 'hijack with nil response' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + nil + end + end + + it "fails with appropriate error" do + response = client.get("/") + + expect(response).to be(:server_failure?) + end + end + + with 'partial hijack' do + let(:content) {"Hello World!"} + + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| + stream.write content + stream.write content + stream.close + end + end + end + + it "reads hijacked body" do + response = client.get("/") + + expect(response.read).to be == (content*2) + end + end + + with 'body with incorrect length' do + let(:bad_body) {Async::HTTP::Body::Buffered.new(["Borked"], 10)} + + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[200, {}, bad_body] + end + end + + it "fails with appropriate error" do + response = client.get("/") + + expect do + response.read + end.to raise_exception(EOFError) + end + end + + with 'streaming server' do + let(:sent_chunks) {[]} + + let(:app) do + chunks = sent_chunks + + ::Protocol::HTTP::Middleware.for do |request| + body = Async::HTTP::Body::Writable.new + + Async::Reactor.run do |task| + 10.times do |i| + chunk = "Chunk #{i}" + chunks << chunk + + body.write chunk + task.sleep 0.25 + end + + body.finish + end + + ::Protocol::HTTP::Response[200, {}, body] + end + end + + it "can cancel response" do + response = client.get("/") + + expect(response.body.read).to be == "Chunk 0" + + response.close + + expect(sent_chunks).to be == ["Chunk 0"] + end + end + + with 'hijack server' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + if request.hijack? + io = request.hijack! + io.write "HTTP/1.1 200 Okay\r\nContent-Length: 16\r\n\r\nHijack Succeeded" + io.flush + io.close + else + ::Protocol::HTTP::Response[200, {}, ["Hijack Failed"]] + end + end + end + + it "will hijack response if possible" do + response = client.get("/") + + expect(response.read).to be =~ /Hijack/ + end + end + + with 'broken server' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + raise RuntimeError.new('simulated failure') + end + end + + it "can't get /" do + expect do + response = client.get("/") + end.to raise_exception(Exception) + end + end + + with 'slow server' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + sleep(endpoint.timeout * 2) + ::Protocol::HTTP::Response[200, {}, []] + end + end + + it "can't get /" do + expect do + client.get("/") + end.to raise_exception(Async::TimeoutError) + end + end + + with 'bi-directional streaming' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + # Echo the request body back to the client. + ::Protocol::HTTP::Response[200, {}, request.body] + end + end + + it "can read from request body and write response body simultaneously" do + skip "Protocol does not support bidirectional streaming!" unless subject.bidirectional? + + body = Async::HTTP::Body::Writable.new + + # Ideally, the flow here is as follows: + # 1/ Client writes headers to server. + # 2/ Client starts writing data to server (in async task). + # 3/ Client reads headers from server. + # 4a/ Client reads data from server. + # 4b/ Client finishes sending data to server. + response = client.post(endpoint.path, [], body) + + expect(response).to be(:success?) + + body.write "." + count = 0 + + response.each do |chunk| + if chunk.bytesize > 32 + body.close + else + count += 1 + body.write chunk*2 + Async::Task.current.sleep(0.1) + end + end + + expect(count).to be == 6 + end + end + + with 'multiple client requests' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[200, {}, [request.path]] + end + end + + def around + current = Console.logger.level + Console.logger.fatal! + + super + ensure + Console.logger.level = current + end + + it "doesn't cancel all requests" do + tasks = [] + task = Async::Task.current + stopped = [] + + 10.times do + tasks << task.async { + begin + loop do + client.get('http://127.0.0.1:8080/a').finish + end + ensure + stopped << 'a' + end + } + end + + 10.times do + tasks << task.async { + begin + loop do + client.get('http://127.0.0.1:8080/b').finish + end + ensure + stopped << 'b' + end + } + end + + tasks.each do |child| + task.sleep 0.01 + child.stop + end + + expect(stopped.sort).to be == stopped + end + end + end + end +end diff --git a/fixtures/async/http/body/a_writable_body.rb b/fixtures/async/http/body/a_writable_body.rb new file mode 100644 index 00000000..eca1c5d9 --- /dev/null +++ b/fixtures/async/http/body/a_writable_body.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019-2023, by Samuel Williams. + +require 'protocol/http/body/deflate' + +module Async + module HTTP + module Body + AWritableBody = Sus::Shared("a writable body") do + it "can write and read data" do + 3.times do |i| + body.write("Hello World #{i}") + expect(body.read).to be == "Hello World #{i}" + end + end + + it "can buffer data in order" do + 3.times do |i| + body.write("Hello World #{i}") + end + + 3.times do |i| + expect(body.read).to be == "Hello World #{i}" + end + end + + with '#join' do + it "can join chunks" do + 3.times do |i| + body.write("#{i}") + end + + body.close + + expect(body.join).to be == "012" + end + end + + with '#each' do + it "can read all data in order" do + 3.times do |i| + body.write("Hello World #{i}") + end + + body.close + + 3.times do |i| + chunk = body.read + expect(chunk).to be == "Hello World #{i}" + end + end + + it "can propagate failures" do + reactor.async do + expect do + body.each do |chunk| + raise RuntimeError.new("It was too big!") + end + end.to raise_exception(RuntimeError, message: be =~ /big/) + end + + expect{ + body.write("Beep boop") # This will cause a failure. + ::Async::Task.current.yield + body.write("Beep boop") # This will fail. + }.to raise_exception(RuntimeError, message: be =~ /big/) + end + + it "can propagate failures in nested bodies" do + nested = ::Protocol::HTTP::Body::Deflate.for(body) + + reactor.async do + expect do + nested.each do |chunk| + raise RuntimeError.new("It was too big!") + end + end.to raise_exception(RuntimeError, message: be =~ /big/) + end + + expect{ + body.write("Beep boop") # This will cause a failure. + ::Async::Task.current.yield + body.write("Beep boop") # This will fail. + }.to raise_exception(RuntimeError, message: be =~ /big/) + end + + it "will stop after finishing" do + output_task = reactor.async do + body.each do |chunk| + expect(chunk).to be == "Hello World!" + end + end + + body.write("Hello World!") + body.close + + expect(body).not.to be(:empty?) + + ::Async::Task.current.yield + + expect(output_task).to be(:finished?) + expect(body).to be(:empty?) + end + end + end + end + end +end diff --git a/gems.rb b/gems.rb index 1e7c447f..2173f993 100644 --- a/gems.rb +++ b/gems.rb @@ -28,6 +28,8 @@ gem "covered" gem "sus" gem "sus-fixtures-async" + gem "sus-fixtures-async-http", "~> 0.7" + gem "sus-fixtures-openssl" gem "bake" gem "bake-test" diff --git a/test/async/http/body.rb b/test/async/http/body.rb index e16e13f9..e08ad660 100644 --- a/test/async/http/body.rb +++ b/test/async/http/body.rb @@ -5,22 +5,15 @@ require 'async/http/body' -require 'async/http/server' -require 'async/http/client' -require 'async/http/endpoint' - -require 'async/io/ssl_socket' - -require_relative 'server_context' - +require 'sus/fixtures/async' +require 'sus/fixtures/openssl' +require 'sus/fixtures/async/http' require 'localhost/authority' -RSpec.shared_examples Async::HTTP::Body do - let(:client) {Async::HTTP::Client.new(client_endpoint, protocol: described_class)} - - context 'with echo server' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint, protocol: described_class) do |request| +ABody = Sus::Shared("a body") do + with 'echo server' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| input = request.body output = Async::HTTP::Body::Writable.new @@ -46,16 +39,16 @@ response = client.post("/", {}, output) - expect(response).to be_success + expect(response).to be(:success?) expect(response.read).to be == "!dlroW olleH" end end - context "with streaming server" do + with "streaming server" do let(:notification) {Async::Notification.new} - let(:server) do - Async::HTTP::Server.for(@bound_endpoint, protocol: described_class) do |request| + let(:app) do + Protocol::HTTP::Middleware.for do |request| body = Async::HTTP::Body::Writable.new Async::Task.current.async do |task| @@ -74,7 +67,7 @@ it "can stream response" do response = client.get("/") - expect(response).to be_success + expect(response).to be(:success?) j = 0 # This validates interleaving @@ -88,23 +81,28 @@ end end -RSpec.describe Async::HTTP::Protocol::HTTP1 do - include_context Async::HTTP::Server +describe Async::HTTP::Protocol::HTTP1 do + include Sus::Fixtures::Async::HTTP::ServerContext - it_should_behave_like Async::HTTP::Body + it_behaves_like ABody end -RSpec.describe Async::HTTP::Protocol::HTTPS do - include_context Async::HTTP::Server +describe Async::HTTP::Protocol::HTTPS do + include Sus::Fixtures::Async::HTTP::ServerContext + include Sus::Fixtures::OpenSSL::ValidCertificateContext let(:authority) {Localhost::Authority.new} let(:server_context) {authority.server_context} let(:client_context) {authority.client_context} - # Shared port for localhost network tests. - let(:server_endpoint) {Async::HTTP::Endpoint.parse("https://localhost:0", ssl_context: server_context, reuse_port: true)} - let(:client_endpoint) {Async::HTTP::Endpoint.parse("https://localhost:0", ssl_context: client_context, reuse_port: true)} + def make_server_endpoint(bound_endpoint) + Async::IO::SSLEndpoint.new(super, ssl_context: server_context) + end + + def make_client_endpoint(bound_endpoint) + Async::IO::SSLEndpoint.new(super, ssl_context: client_context) + end - it_should_behave_like Async::HTTP::Body + it_behaves_like ABody end diff --git a/test/async/http/body/hijack.rb b/test/async/http/body/hijack.rb index 5397180f..73cad02d 100644 --- a/test/async/http/body/hijack.rb +++ b/test/async/http/body/hijack.rb @@ -5,42 +5,44 @@ require 'async/http/body/hijack' -RSpec.describe Async::HTTP::Body::Hijack do - include_context Async::RSpec::Reactor +require 'sus/fixtures/async' + +describe Async::HTTP::Body::Hijack do + include Sus::Fixtures::Async::ReactorContext + + let(:body) do + subject.wrap do |stream| + 3.times do + stream.write(content) + end + stream.close + end + end let(:content) {"Hello World!"} - describe '#call' do + with '#call' do let(:stream) {Async::HTTP::Body::Writable.new} - subject do - described_class.wrap do |stream| - 3.times do - stream.write(content) - end - stream.close - end - end - it "should generate body using direct invocation" do - subject.call(stream) + body.call(stream) 3.times do expect(stream.read).to be == content end expect(stream.read).to be_nil - expect(stream).to be_empty + expect(stream).to be(:empty?) end it "should generate body using stream" do 3.times do - expect(subject.read).to be == content + expect(body.read).to be == content end - expect(subject.read).to be_nil + expect(body.read).to be_nil - expect(subject).to be_empty + expect(body).to be(:empty?) end end end diff --git a/test/async/http/body/pipe.rb b/test/async/http/body/pipe.rb index 9f8ce2c8..fe620ec9 100644 --- a/test/async/http/body/pipe.rb +++ b/test/async/http/body/pipe.rb @@ -8,19 +8,25 @@ require 'async/http/body/pipe' require 'async/http/body/writable' -RSpec.describe Async::HTTP::Body::Pipe do - let(:input) { Async::HTTP::Body::Writable.new } - let(:pipe) { described_class.new(input) } +require 'sus/fixtures/async' + +describe Async::HTTP::Body::Pipe do + let(:input) {Async::HTTP::Body::Writable.new} + let(:pipe) {subject.new(input)} - let(:data) { 'Hello World!' } + let(:data) {'Hello World!'} - describe '#to_io' do - include_context Async::RSpec::Reactor + with '#to_io' do + include Sus::Fixtures::Async::ReactorContext + let(:input_write_duration) {0} let(:io) { pipe.to_io } - before do - Async::Task.current.async do |task| # input writer task + def before + super + + # input writer task + Async do |task| first, second = data.split(' ') input.write("#{first} ") task.sleep(input_write_duration) if input_write_duration > 0 @@ -29,38 +35,35 @@ end end - after { io.close } - - shared_examples :returns_io_socket do - it 'returns an io socket' do - expect(io).to be_a(Async::IO::Socket) - expect(io.read).to eq data - end + def aftrer + io.close + + super end - context 'when reading blocks' do - let(:input_write_duration) { 0.01 } - - include_examples :returns_io_socket + it "returns an io socket" do + expect(io).to be_a(Async::IO::Socket) + expect(io.read).to be == data end - context 'when reading does not block' do - let(:input_write_duration) { 0 } + with 'blocking reads' do + let(:input_write_duration) {0.01} - include_examples :returns_io_socket + it 'returns an io socket' do + expect(io.read).to be == data + end end end - describe 'going out of reactor scope' do - context 'when pipe is closed' do - it 'finishes' do - Async { pipe.close } - end + with 'reactor going out of scope' do + it 'finishes' do + # ensures pipe background tasks are transient + Async{pipe} end - context 'when pipe is not closed' do - it 'finishes' do # ensures pipe background tasks are transient - Async { pipe } + with 'closed pipe' do + it 'finishes' do + Async{pipe.close} end end end diff --git a/test/async/http/body/slowloris.rb b/test/async/http/body/slowloris.rb index 8ae0a1cf..dc3e48be 100644 --- a/test/async/http/body/slowloris.rb +++ b/test/async/http/body/slowloris.rb @@ -3,30 +3,33 @@ # Released under the MIT License. # Copyright, 2019-2023, by Samuel Williams. -require_relative 'writable_examples' - require 'async/http/body/slowloris' -RSpec.describe Async::HTTP::Body::Slowloris do - include_context Async::RSpec::Reactor +require 'sus/fixtures/async' +require 'async/http/body/a_writable_body' + +describe Async::HTTP::Body::Slowloris do + include Sus::Fixtures::Async::ReactorContext + + let(:body) {subject.new} - it_behaves_like Async::HTTP::Body::Writable + it_behaves_like Async::HTTP::Body::AWritableBody it "closes body with error if throughput is not maintained" do - subject.write("Hello World") + body.write("Hello World") sleep 0.1 expect do - subject.write("Hello World") - end.to raise_error(Async::HTTP::Body::Slowloris::ThroughputError, /Slow write/) + body.write("Hello World") + end.to raise_exception(Async::HTTP::Body::Slowloris::ThroughputError, message: be =~ /Slow write/) end it "doesn't close body if throughput is exceeded" do - subject.write("Hello World") + body.write("Hello World") expect do - subject.write("Hello World") - end.to_not raise_error + body.write("Hello World") + end.not.to raise_exception end end diff --git a/test/async/http/body/writable.rb b/test/async/http/body/writable.rb index 8340ecf1..9d553a58 100644 --- a/test/async/http/body/writable.rb +++ b/test/async/http/body/writable.rb @@ -3,10 +3,15 @@ # Released under the MIT License. # Copyright, 2018-2023, by Samuel Williams. -require_relative 'writable_examples' +require 'async/http/body/slowloris' -RSpec.describe Async::HTTP::Body::Writable do - include_context Async::RSpec::Reactor +require 'sus/fixtures/async' +require 'async/http/body/a_writable_body' + +describe Async::HTTP::Body::Writable do + include Sus::Fixtures::Async::ReactorContext + + let(:body) {subject.new} - it_behaves_like Async::HTTP::Body::Writable + it_behaves_like Async::HTTP::Body::AWritableBody end diff --git a/test/async/http/body/writable_examples.rb b/test/async/http/body/writable_examples.rb deleted file mode 100644 index dca3a926..00000000 --- a/test/async/http/body/writable_examples.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. - -require 'async/http/server' -require 'async/http/client' -require 'async/reactor' - -require 'async/http/body' -require 'protocol/http/body/deflate' -require 'async/http/body/writable' -require 'async/http/endpoint' - -require 'async/io/ssl_socket' -require 'async/rspec/ssl' - -RSpec.shared_examples_for Async::HTTP::Body::Writable do - it "can write and read data" do - 3.times do |i| - subject.write("Hello World #{i}") - expect(subject.read).to be == "Hello World #{i}" - end - end - - it "can buffer data in order" do - 3.times do |i| - subject.write("Hello World #{i}") - end - - 3.times do |i| - expect(subject.read).to be == "Hello World #{i}" - end - end - - context '#join' do - it "can join chunks" do - 3.times do |i| - subject.write("#{i}") - end - - subject.close - - expect(subject.join).to be == "012" - end - end - - context '#each' do - it "can read all data in order" do - 3.times do |i| - subject.write("Hello World #{i}") - end - - subject.close - - 3.times do |i| - chunk = subject.read - expect(chunk).to be == "Hello World #{i}" - end - end - - it "can propagate failures" do - reactor.async do - expect do - subject.each do |chunk| - raise RuntimeError.new("It was too big!") - end - end.to raise_error(RuntimeError, /big/) - end - - expect{ - subject.write("Beep boop") # This will cause a failure. - Async::Task.current.yield - subject.write("Beep boop") # This will fail. - }.to raise_error(RuntimeError, /big/) - end - - it "can propagate failures in nested bodies" do - nested = Protocol::HTTP::Body::Deflate.for(subject) - - reactor.async do - expect do - nested.each do |chunk| - raise RuntimeError.new("It was too big!") - end - end.to raise_error(RuntimeError, /big/) - end - - expect{ - subject.write("Beep boop") # This will cause a failure. - Async::Task.current.yield - subject.write("Beep boop") # This will fail. - }.to raise_error(RuntimeError, /big/) - end - - it "will stop after finishing" do - output_task = reactor.async do - subject.each do |chunk| - expect(chunk).to be == "Hello World!" - end - end - - subject.write("Hello World!") - subject.close - - expect(subject).to_not be_empty - - Async::Task.current.yield - - expect(output_task).to be_finished - expect(subject).to be_empty - end - end -end diff --git a/test/async/http/client.rb b/test/async/http/client.rb index 19bc7b35..3a4cdb91 100644 --- a/test/async/http/client.rb +++ b/test/async/http/client.rb @@ -3,8 +3,6 @@ # Released under the MIT License. # Copyright, 2017-2023, by Samuel Williams. -require_relative 'server_context' - require 'async/http/server' require 'async/http/client' require 'async/reactor' @@ -13,20 +11,22 @@ require 'async/http/endpoint' require 'protocol/http/accept_encoding' -RSpec.describe Async::HTTP::Client, timeout: 5 do - describe Async::HTTP::Protocol::HTTP1 do - include_context Async::HTTP::Server - let(:protocol) {described_class} +require 'sus/fixtures/async' +require 'sus/fixtures/async/http' + +describe Async::HTTP::Client do + with 'basic server' do + include Sus::Fixtures::Async::HTTP::ServerContext it "client can get resource" do response = client.get("/") response.read - expect(response).to be_success + expect(response).to be(:success?) end end - context 'non-existant host' do - include_context Async::RSpec::Reactor + with 'non-existant host' do + include Sus::Fixtures::Async::ReactorContext let(:endpoint) {Async::HTTP::Endpoint.parse('http://the.future')} let(:client) {Async::HTTP::Client.new(endpoint)} @@ -34,42 +34,7 @@ it "should fail to connect" do expect do client.get("/") - end.to raise_error(SocketError, /not known/) - end - end - - describe Async::HTTP::Protocol::HTTPS do - include_context Async::RSpec::Reactor - - let(:endpoint) {Async::HTTP::Endpoint.parse('https://www.codeotaku.com')} - let(:client) {Async::HTTP::Client.new(endpoint)} - - it "should specify hostname" do - expect(endpoint.hostname).to be == "www.codeotaku.com" - expect(client.authority).to be == "www.codeotaku.com" - end - - it "can request remote resource" do - 2.times do - response = client.get("/index") - expect(response).to be_success - response.finish - end - - client.close - end - - it "can request remote resource with compression" do - compressor = Protocol::HTTP::AcceptEncoding.new(client) - - response = compressor.get("/index", {'accept-encoding' => 'gzip'}) - - expect(response).to be_success - - expect(response.body).to be_kind_of Async::HTTP::Body::Inflate - expect(response.read).to be_start_with('') - - client.close + end.to raise_exception(SocketError, message: be =~ /not known/) end end end diff --git a/test/async/http/client/codeotaku.rb b/test/async/http/client/codeotaku.rb new file mode 100644 index 00000000..5dfe7487 --- /dev/null +++ b/test/async/http/client/codeotaku.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023, by Samuel Williams. + +require 'async/http/client' +require 'async/http/endpoint' +require 'protocol/http/accept_encoding' + +require 'sus/fixtures/async' + +describe Async::HTTP::Client do + include Sus::Fixtures::Async::ReactorContext + + let(:endpoint) {Async::HTTP::Endpoint.parse('https://www.codeotaku.com')} + let(:client) {Async::HTTP::Client.new(endpoint)} + + it "should specify hostname" do + expect(endpoint.hostname).to be == "www.codeotaku.com" + expect(client.authority).to be == "www.codeotaku.com" + end + + it 'can fetch remote resource' do + response = client.get('/index') + + response.finish + + expect(response).not.to be(:failure?) + end + + it "can request remote resource with compression" do + compressor = Protocol::HTTP::AcceptEncoding.new(client) + + response = compressor.get("/index", {'accept-encoding' => 'gzip'}) + + expect(response).to be(:success?) + + expect(response.body).to be_a Async::HTTP::Body::Inflate + expect(response.read).to be(:start_with?, '') + end +end + diff --git a/test/async/http/client/google.rb b/test/async/http/client/google.rb index 63f583c4..f8c8ae27 100644 --- a/test/async/http/client/google.rb +++ b/test/async/http/client/google.rb @@ -6,18 +6,20 @@ require 'async/http/client' require 'async/http/endpoint' -RSpec.describe Async::HTTP::Client, timeout: 5 do - include_context Async::RSpec::Reactor +require 'sus/fixtures/async' + +describe Async::HTTP::Client do + include Sus::Fixtures::Async::ReactorContext let(:endpoint) {Async::HTTP::Endpoint.parse('https://www.google.com')} let(:client) {Async::HTTP::Client.new(endpoint)} it 'can fetch remote resource' do response = client.get('/', 'accept' => '*/*') - + response.finish - - expect(response).to_not be_failure + + expect(response).not.to be(:failure?) client.close end diff --git a/test/async/http/endpoint.rb b/test/async/http/endpoint.rb index b02a6ec8..d14b1464 100644 --- a/test/async/http/endpoint.rb +++ b/test/async/http/endpoint.rb @@ -6,135 +6,169 @@ require 'async/http/endpoint' -RSpec.describe Async::HTTP::Endpoint do +describe Async::HTTP::Endpoint do it "should fail to parse relative url" do - expect{ - described_class.parse("/foo/bar") - }.to raise_error(ArgumentError, /absolute/) + expect do + subject.parse("/foo/bar") + end.to raise_exception(ArgumentError, message: be =~ /absolute/) end - describe '#port' do + with '#port' do let(:url_string) {"https://localhost:9292"} it "extracts port from URL" do endpoint = Async::HTTP::Endpoint.parse(url_string) - expect(endpoint.port).to eq 9292 + expect(endpoint).to have_attributes(port: be == 9292) end it "extracts port from options" do endpoint = Async::HTTP::Endpoint.parse(url_string, port: 9000) - expect(endpoint.port).to eq 9000 + expect(endpoint).to have_attributes(port: be == 9000) end end - describe '#hostname' do + with '#hostname' do describe Async::HTTP::Endpoint.parse("https://127.0.0.1:9292") do - it {is_expected.to have_attributes(hostname: '127.0.0.1')} + it 'has correct hostname' do + expect(subject).to have_attributes(hostname: be == '127.0.0.1') + end it "should be connecting to 127.0.0.1" do expect(subject.endpoint).to be_a Async::IO::SSLEndpoint - expect(subject.endpoint).to have_attributes(hostname: '127.0.0.1') - expect(subject.endpoint.endpoint).to have_attributes(hostname: '127.0.0.1') + expect(subject.endpoint).to have_attributes(hostname: be == '127.0.0.1') + expect(subject.endpoint.endpoint).to have_attributes(hostname: be == '127.0.0.1') end end describe Async::HTTP::Endpoint.parse("https://127.0.0.1:9292", hostname: 'localhost') do - it {is_expected.to have_attributes(hostname: 'localhost')} - it {is_expected.to_not be_localhost} + it 'has correct hostname' do + expect(subject).to have_attributes(hostname: be == 'localhost') + expect(subject).not.to be(:localhost?) + end it "should be connecting to localhost" do expect(subject.endpoint).to be_a Async::IO::SSLEndpoint - expect(subject.endpoint).to have_attributes(hostname: '127.0.0.1') - expect(subject.endpoint.endpoint).to have_attributes(hostname: 'localhost') + expect(subject.endpoint).to have_attributes(hostname: be == '127.0.0.1') + expect(subject.endpoint.endpoint).to have_attributes(hostname: be == 'localhost') end end end - describe '.for' do - context Async::HTTP::Endpoint.for("http", "localhost") do - it {is_expected.to have_attributes(scheme: "http", hostname: "localhost", path: "/")} - it {is_expected.to_not be_secure} + with '.for' do + describe Async::HTTP::Endpoint.for("http", "localhost") do + it "should have correct attributes" do + expect(subject).to have_attributes( + scheme: be == "http", + hostname: be == "localhost", + path: be == "/" + ) + + expect(subject).not.to be(:secure?) + end end - context Async::HTTP::Endpoint.for("http", "localhost", "/foo") do - it {is_expected.to have_attributes(scheme: "http", hostname: "localhost", path: "/foo")} + describe Async::HTTP::Endpoint.for("http", "localhost", "/foo") do + it "should have correct attributes" do + expect(subject).to have_attributes( + scheme: be == "http", + hostname: be == "localhost", + path: be == "/foo" + ) + + expect(subject).not.to be(:secure?) + end end end - describe '#secure?' do - subject {Async::HTTP::Endpoint.parse(description)} - - context 'http://localhost' do - it { is_expected.to_not be_secure } + with '#secure?' do + describe Async::HTTP::Endpoint.parse("http://localhost") do + it "should not be secure" do + expect(subject).not.to be(:secure?) + end end - context 'https://localhost' do - it { is_expected.to be_secure } + describe Async::HTTP::Endpoint.parse("https://localhost") do + it "should be secure" do + expect(subject).to be(:secure?) + end end - context 'with scheme: https' do - subject {Async::HTTP::Endpoint.parse("http://localhost", scheme: 'https')} - - it { is_expected.to be_secure } + with 'scheme: https' do + describe Async::HTTP::Endpoint.parse("http://localhost", scheme: 'https') do + it "should be secure" do + expect(subject).to be(:secure?) + end + end end end - describe '#localhost?' do - subject {Async::HTTP::Endpoint.parse(description)} - - context 'http://localhost' do - it { is_expected.to be_localhost } + with '#localhost?' do + describe Async::HTTP::Endpoint.parse("http://localhost") do + it "should be localhost" do + expect(subject).to be(:localhost?) + end end - context 'http://hello.localhost' do - it { is_expected.to be_localhost } + describe Async::HTTP::Endpoint.parse("http://hello.localhost") do + it "should be localhost" do + expect(subject).to be(:localhost?) + end end - context 'http://localhost.' do - it { is_expected.to be_localhost } + describe Async::HTTP::Endpoint.parse("http://localhost.") do + it "should be localhost" do + expect(subject).to be(:localhost?) + end end - context 'http://hello.localhost.' do - it { is_expected.to be_localhost } + describe Async::HTTP::Endpoint.parse("http://hello.localhost.") do + it "should be localhost" do + expect(subject).to be(:localhost?) + end end - context 'http://localhost.com' do - it { is_expected.to_not be_localhost } + describe Async::HTTP::Endpoint.parse("http://localhost.com") do + it "should not be localhost" do + expect(subject).not.to be(:localhost?) + end end end - describe '#path' do - it "can normal urls" do - endpoint = Async::HTTP::Endpoint.parse("http://foo.com/bar?baz") - expect(endpoint.path).to be == "/bar?baz" + with '#path' do + describe Async::HTTP::Endpoint.parse("http://foo.com/bar?baz") do + it "should have correct path" do + expect(subject).to have_attributes(path: be == "/bar?baz") + end end - it "can handle websocket urls" do - endpoint = Async::HTTP::Endpoint.parse("wss://foo.com/bar?baz") - expect(endpoint.path).to be == "/bar?baz" + with 'websocket scheme' do + describe Async::HTTP::Endpoint.parse("wss://foo.com/bar?baz") do + it "should have correct path" do + expect(subject).to have_attributes(path: be == "/bar?baz") + end + end end end end -RSpec.describe "http://www.google.com/search" do - let(:endpoint) {Async::HTTP::Endpoint.parse(subject)} - - it "should be valid endpoint" do - expect{endpoint}.to_not raise_error - end - +describe Async::HTTP::Endpoint.parse("http://www.google.com/search") do it "should select the correct protocol" do - expect(endpoint.protocol).to be Async::HTTP::Protocol::HTTP1 + expect(subject.protocol).to be == Async::HTTP::Protocol::HTTP1 end it "should parse the correct hostname" do - expect(endpoint.hostname).to be == "www.google.com" + expect(subject).to have_attributes( + scheme: be == "http", + hostname: be == "www.google.com", + path: be == "/search" + ) end it "should not be equal if path is different" do other = Async::HTTP::Endpoint.parse('http://www.google.com/search?q=ruby') - expect(endpoint).to_not be_eql other + expect(subject).not.to be == other + expect(subject).not.to be(:eql?, other) end end diff --git a/test/async/http/internet.rb b/test/async/http/internet.rb index 15597b48..ac25f408 100644 --- a/test/async/http/internet.rb +++ b/test/async/http/internet.rb @@ -7,20 +7,18 @@ require 'async/reactor' require 'json' +require 'sus/fixtures/async' -RSpec.describe Async::HTTP::Internet, timeout: 30 do - include_context Async::RSpec::Reactor +describe Async::HTTP::Internet do + include Sus::Fixtures::Async::ReactorContext + let(:internet) {subject.new} let(:headers) {[['accept', '*/*'], ['user-agent', 'async-http']]} - after do - subject.close - end - it "can fetch remote website" do - response = subject.get("https://www.codeotaku.com/index", headers) + response = internet.get("https://www.codeotaku.com/index", headers) - expect(response).to be_success + expect(response).to be(:success?) response.close end @@ -29,10 +27,10 @@ let(:body) {[JSON.dump(sample)]} # This test is increasingly flakey. - xit "can fetch remote json" do - response = subject.post("https://httpbin.org/anything", headers, body) + it "can fetch remote json" do + response = internet.post("https://httpbin.org/anything", headers, body) - expect(response).to be_success - expect{JSON.parse(response.read)}.to_not raise_error + expect(response).to be(:success?) + expect{JSON.parse(response.read)}.not.to raise_exception end end diff --git a/test/async/http/internet/instance.rb b/test/async/http/internet/instance.rb index 846d2028..d254bd59 100644 --- a/test/async/http/internet/instance.rb +++ b/test/async/http/internet/instance.rb @@ -4,12 +4,11 @@ # Copyright, 2021-2023, by Samuel Williams. require 'async/http/internet/instance' -require 'async/reactor' -RSpec.describe Async::HTTP::Internet, timeout: 5 do +describe Async::HTTP::Internet do describe '.instance' do it "returns an internet instance" do - expect(Async::HTTP::Internet.instance).to be_kind_of(Async::HTTP::Internet) + expect(Async::HTTP::Internet.instance).to be_a(Async::HTTP::Internet) end end end diff --git a/test/async/http/performance.rb b/test/async/http/performance.rb deleted file mode 100755 index acb3df2b..00000000 --- a/test/async/http/performance.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2017-2023, by Samuel Williams. - -require 'async/http/server' -require 'async/http/client' - -require_relative 'server_context' -require 'async/container' - -require 'etc' - -RSpec.shared_examples_for 'client benchmark' do - let(:endpoint) {Async::HTTP::Endpoint.parse('http://127.0.0.1:9294', timeout: 0.8, reuse_port: true)} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, []] - end - end - - let(:url) {endpoint.url.to_s} - let(:repeats) {1000} - let(:concurrency) {Etc.nprocessors || 2} - - before do - Sync do - # We bind the endpoint before running the server so that we know incoming connections will be accepted: - @bound_endpoint = Async::IO::SharedEndpoint.bound(endpoint) - end - - # I feel a dedicated class might be better than this hack: - allow(@bound_endpoint).to receive(:protocol).and_return(endpoint.protocol) - allow(@bound_endpoint).to receive(:scheme).and_return(endpoint.scheme) - - @container = Async::Container.new - - GC.disable - - @container.run(count: concurrency) do |instance| - Async do - instance.ready! - server.run - end - end - - @bound_endpoint.close - end - - after do - @container.stop - - GC.enable - end - - it "runs benchmark", timeout: nil do - if ab = `which ab`.chomp! - system(ab, "-k", "-n", (concurrency*repeats).to_s, "-c", concurrency.to_s, url) - end - - if wrk = `which wrk`.chomp! - system(wrk, "-c", concurrency.to_s, "-d", "2", "-t", concurrency.to_s, url) - end - end -end - -RSpec.describe Async::HTTP::Server do - describe Protocol::HTTP::Middleware::Okay do - let(:server) do - Async::HTTP::Server.new( - Protocol::HTTP::Middleware::Okay, - @bound_endpoint - ) - end - - include_examples 'client benchmark' - end - - describe 'multiple chunks' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do - Protocol::HTTP::Response[200, {}, "Hello World".chars] - end - end - - include_examples 'client benchmark' - end -end diff --git a/test/async/http/protocol/http10.rb b/test/async/http/protocol/http10.rb index 7f06eb98..26ae0be4 100644 --- a/test/async/http/protocol/http10.rb +++ b/test/async/http/protocol/http10.rb @@ -4,8 +4,8 @@ # Copyright, 2018-2023, by Samuel Williams. require 'async/http/protocol/http10' -require_relative 'shared_examples' +require 'async/http/a_protocol' -RSpec.describe Async::HTTP::Protocol::HTTP10 do - it_behaves_like Async::HTTP::Protocol +describe Async::HTTP::Protocol::HTTP10 do + it_behaves_like Async::HTTP::AProtocol end diff --git a/test/async/http/protocol/http11.rb b/test/async/http/protocol/http11.rb index d16bf8ba..af2497ff 100755 --- a/test/async/http/protocol/http11.rb +++ b/test/async/http/protocol/http11.rb @@ -6,75 +6,74 @@ # Copyright, 2023, by Thomas Morgan. require 'async/http/protocol/http11' -require_relative 'shared_examples' +require 'async/http/a_protocol' -RSpec.describe Async::HTTP::Protocol::HTTP11 do - it_behaves_like Async::HTTP::Protocol +describe Async::HTTP::Protocol::HTTP11 do + it_behaves_like Async::HTTP::AProtocol - context 'bad requests' do - include_context Async::HTTP::Server + with 'server' do + include Sus::Fixtures::Async::HTTP::ServerContext + let(:protocol) {subject} - around do |example| - current = Console.logger.level - Console.logger.fatal! - - example.run - ensure - Console.logger.level = current - end - - it "should fail cleanly when path is empty" do - response = client.get("") - - expect(response.status).to be == 400 - end - end - - context 'head request' do - include_context Async::HTTP::Server - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, ["Hello", "World"]] - end - end - - it "doesn't reply with body" do - 5.times do - response = client.head("/") + with 'bad requests' do + def around + current = Console.logger.level + Console.logger.fatal! - expect(response).to be_success - expect(response.version).to be == "HTTP/1.1" - expect(response.body).to be_empty + super + ensure + Console.logger.level = current + end + + it "should fail cleanly when path is empty" do + response = client.get("") - response.read + expect(response.status).to be == 400 end end - end - - context 'raw response' do - include_context Async::HTTP::Server - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - peer = request.hijack! - - peer.write( - "#{request.version} 200 It worked!\r\n" + - "connection: close\r\n" + - "\r\n" + - "Hello World!" - ) - peer.close - - nil + with 'head request' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| + Protocol::HTTP::Response[200, {}, ["Hello", "World"]] + end + end + + it "doesn't reply with body" do + 5.times do + response = client.head("/") + + expect(response).to be(:success?) + expect(response.version).to be == "HTTP/1.1" + expect(response.body).to be(:empty?) + + response.read + end end end - it "reads raw response" do - response = client.get("/") + with 'raw response' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| + peer = request.hijack! + + peer.write( + "#{request.version} 200 It worked!\r\n" + + "connection: close\r\n" + + "\r\n" + + "Hello World!" + ) + peer.close + + nil + end + end - expect(response.read).to be == "Hello World!" + it "reads raw response" do + response = client.get("/") + + expect(response.read).to be == "Hello World!" + end end end end diff --git a/test/async/http/protocol/http11/desync.rb b/test/async/http/protocol/http11/desync.rb index 5e9f7cf2..d31598a6 100644 --- a/test/async/http/protocol/http11/desync.rb +++ b/test/async/http/protocol/http11/desync.rb @@ -3,23 +3,26 @@ # Released under the MIT License. # Copyright, 2021-2023, by Samuel Williams. -require_relative '../../server_context' require 'async/http/protocol/http11' -RSpec.describe Async::HTTP::Protocol::HTTP11, timeout: 30 do - include_context Async::HTTP::Server +require 'sus/fixtures/async/http/server_context' + +describe Async::HTTP::Protocol::HTTP11 do + include Sus::Fixtures::Async::ReactorContext + include Sus::Fixtures::Async::HTTP::ServerContext - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + let(:app) do + Protocol::HTTP::Middleware.for do |request| Protocol::HTTP::Response[200, {}, [request.path]] end end - around do |example| + + def around current = Console.logger.level Console.logger.fatal! - - example.run + + super ensure Console.logger.level = current end @@ -63,7 +66,8 @@ child.stop end - puts "Backtraces" - pp backtraces.sort.uniq + # puts "Backtraces" + # pp backtraces.sort.uniq + expect(backtraces).not.to be(:empty?) end end diff --git a/test/async/http/protocol/http2.rb b/test/async/http/protocol/http2.rb index 2e8a52a8..f914653f 100644 --- a/test/async/http/protocol/http2.rb +++ b/test/async/http/protocol/http2.rb @@ -4,98 +4,98 @@ # Copyright, 2018-2023, by Samuel Williams. require 'async/http/protocol/http2' -require_relative 'shared_examples' +require 'async/http/a_protocol' -RSpec.describe Async::HTTP::Protocol::HTTP2 do - it_behaves_like Async::HTTP::Protocol +describe Async::HTTP::Protocol::HTTP2 do + it_behaves_like Async::HTTP::AProtocol - context 'bad requests' do - include_context Async::HTTP::Server + with 'server' do + include Sus::Fixtures::Async::HTTP::ServerContext + let(:protocol) {subject} - it "should fail with explicit authority" do - expect do - client.post("/", [[':authority', 'foo']]) - end.to raise_error(Protocol::HTTP2::StreamError) - end - end - - context 'closed streams' do - include_context Async::HTTP::Server - - it 'should delete stream after response stream is closed' do - response = client.get("/") - connection = response.connection - - response.read - - expect(connection.streams).to be_empty + with 'bad requests' do + it "should fail with explicit authority" do + expect do + client.post("/", [[':authority', 'foo']]) + end.to raise_exception(Protocol::HTTP2::StreamError) + end end - end - - context 'host header' do - include_context Async::HTTP::Server - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, request.headers, ["Authority: #{request.authority.inspect}"]] + with 'closed streams' do + it 'should delete stream after response stream is closed' do + response = client.get("/") + connection = response.connection + + response.read + + expect(connection.streams).to be(:empty?) end end - # We specify nil for the authority - it won't be sent. - let!(:client) {Async::HTTP::Client.new(endpoint, authority: nil)} - - it "should not send :authority header if host header is present" do - response = client.post("/", [['host', 'foo']]) + with 'host header' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| + Protocol::HTTP::Response[200, request.headers, ["Authority: #{request.authority.inspect}"]] + end + end - expect(response.headers).to include('host') - expect(response.headers['host']).to be == 'foo' + def make_client(endpoint, **options) + # We specify nil for the authority - it won't be sent. + options[:authority] = nil + super + end - # TODO Should HTTP/2 respect host header? - expect(response.read).to be == "Authority: nil" - end - end - - context 'stopping requests' do - include_context Async::HTTP::Server - - let(:notification) {Async::Notification.new} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - body = Async::HTTP::Body::Writable.new + it "should not send :authority header if host header is present" do + response = client.post("/", [['host', 'foo']]) - reactor.async do |task| - begin - 100.times do |i| - body.write("Chunk #{i}") - task.sleep (0.01) - end - rescue - # puts "Response generation failed: #{$!}" - ensure - body.close - notification.signal - end - end + expect(response.headers).to have_keys('host') + expect(response.headers['host']).to be == 'foo' - Protocol::HTTP::Response[200, {}, body] + # TODO Should HTTP/2 respect host header? + expect(response.read).to be == "Authority: nil" end end - let(:pool) {client.pool} - - it "should close stream without closing connection" do - expect(pool).to be_empty - - response = client.get("/") + with 'stopping requests' do + let(:notification) {Async::Notification.new} - expect(pool).to_not be_empty - - response.close + let(:app) do + Protocol::HTTP::Middleware.for do |request| + body = Async::HTTP::Body::Writable.new + + reactor.async do |task| + begin + 100.times do |i| + body.write("Chunk #{i}") + task.sleep (0.01) + end + rescue + # puts "Response generation failed: #{$!}" + ensure + body.close + notification.signal + end + end + + Protocol::HTTP::Response[200, {}, body] + end + end - notification.wait + let(:pool) {client.pool} - expect(response.stream.connection).to be_reusable + it "should close stream without closing connection" do + expect(pool).to be(:empty?) + + response = client.get("/") + + expect(pool).not.to be(:empty?) + + response.close + + notification.wait + + expect(response.stream.connection).to be(:reusable?) + end end end end diff --git a/test/async/http/protocol/shared_examples.rb b/test/async/http/protocol/shared_examples.rb deleted file mode 100644 index b29873d5..00000000 --- a/test/async/http/protocol/shared_examples.rb +++ /dev/null @@ -1,553 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2018-2023, by Samuel Williams. -# Copyright, 2020, by Igor Sidorov. - -require_relative '../server_context' - -require 'async' -require 'async/clock' -require 'async/http/client' -require 'async/http/server' -require 'async/http/endpoint' -require 'async/http/body/hijack' -require 'tempfile' - -require 'protocol/http/body/file' - -require 'async/rspec/profile' - -RSpec.shared_examples_for Async::HTTP::Protocol do - include_context Async::HTTP::Server - - it "should have valid scheme" do - expect(client.scheme).to be == "http" - end - - context '#close' do - it 'can close the connection' do - Async do |task| - response = client.get("/") - expect(response).to be_success - response.finish - - client.close - - expect(task.children).to be_empty - end.wait - end - end - - context "huge body", timeout: 600 do - let(:body) {Protocol::HTTP::Body::File.open("/dev/zero", size: 512*1024**2)} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, body] - end - end - - it "client can download data quickly" do |example| - response = client.get("/") - expect(response).to be_success - - data_size = 0 - duration = Async::Clock.measure do - while chunk = response.body.read - data_size += chunk.bytesize - chunk.clear - end - - response.finish - end - - size_mbytes = data_size / 1024**2 - - example.reporter.message "Data size: #{size_mbytes}MB Duration: #{duration.round(2)}s Throughput: #{(size_mbytes / duration).round(2)}MB/s" - end - end - - context 'buffered body' do - let(:body) {Async::HTTP::Body::Buffered.new(["Hello World"])} - let(:response) {Protocol::HTTP::Response[200, {}, body]} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - response - end - end - - it "response body should be closed" do - expect(body).to receive(:close).and_call_original - # expect(response).to receive(:close).and_call_original - - expect(client.get("/", {}).read).to be == "Hello World" - end - end - - context 'empty body' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[204] - end - end - - it 'properly handles no content responses' do - expect(client.get("/", {}).read).to be_nil - end - end - - context 'with trailer', if: described_class.bidirectional? do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - if trailer = request.headers['trailer'] - expect(request.headers).to_not include('etag') - request.finish - expect(request.headers).to include('etag') - - Protocol::HTTP::Response[200, [], "request trailer"] - else - headers = Protocol::HTTP::Headers.new - headers.add('trailer', 'etag') - - body = Async::HTTP::Body::Writable.new - - Async do |task| - body.write("response trailer") - task.sleep(0.01) - headers.add('etag', 'abcd') - body.close - end - - Protocol::HTTP::Response[200, headers, body] - end - end - end - - it "can send request trailer" do - headers = Protocol::HTTP::Headers.new - headers.add('trailer', 'etag') - body = Async::HTTP::Body::Writable.new - - Async do |task| - body.write("Hello") - task.sleep(0.01) - headers.add('etag', 'abcd') - body.close - end - - response = client.post("/", headers, body) - expect(response.read).to be == "request trailer" - - expect(response).to be_success - end - - it "can receive response trailer" do - response = client.get("/") - expect(response.headers).to include('trailer') - headers = response.headers - expect(headers).to_not include('etag') - - expect(response.read).to be == "response trailer" - expect(response).to be_success - - # It was sent as a trailer. - expect(headers).to include('etag') - end - end - - context 'with working server' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - if request.method == 'POST' - # We stream the request body directly to the response. - Protocol::HTTP::Response[200, {}, request.body] - elsif request.method == 'GET' - expect(request.body).to be nil - - Protocol::HTTP::Response[200, { - 'remote-address' => request.remote_address.inspect - }, ["#{request.method} #{request.version}"]] - else - Protocol::HTTP::Response[200, {}, ["Hello World"]] - end - end - end - - it "should have valid scheme" do - expect(server.scheme).to be == "http" - end - - it "disconnects slow clients" do - response = client.get("/") - response.read - - # We expect this connection to be closed: - connection = response.connection - - reactor.sleep(1.0) - - response = client.get("/") - response.read - - expect(connection).to_not be_reusable - - # client.close - # reactor.sleep(0.1) - # reactor.print_hierarchy - end - - context 'using GET method' do - let(:expected) {"GET #{protocol::VERSION}"} - - it "can handle many simultaneous requests", timeout: 10 do |example| - duration = Async::Clock.measure do - 10.times do - tasks = 100.times.collect do - Async do - client.get("/") - end - end - - tasks.each do |task| - response = task.wait - expect(response).to be_success - expect(response.read).to eq expected - end - end - end - - example.reporter.message "Pool: #{client.pool}" - example.reporter.message "Duration = #{duration.round(2)}" - end - - context 'with response' do - let(:response) {client.get("/")} - after {response.finish} - - it "can finish gracefully" do - expect(response).to be_success - end - - it "is successful" do - expect(response).to be_success - expect(response.read).to eq expected - end - - it "provides content length" do - expect(response.body.length).to_not be_nil - end - - let(:tempfile) {Tempfile.new} - - it "can save to disk" do - response.save(tempfile.path) - expect(tempfile.read).to eq expected - - tempfile.close - end - - it "has remote-address header" do - expect(response.headers['remote-address']).to_not be_nil - end - - it "has protocol version" do - expect(response.version).to_not be_nil - end - end - end - - context 'HEAD' do - let(:response) {client.head("/")} - after {response.finish} - - it "is successful and without body" do - expect(response).to be_success - expect(response.body).to_not be_nil - expect(response.body).to be_empty - expect(response.body.length).to_not be_nil - expect(response.read).to be_nil - end - end - - context 'POST' do - let(:response) {client.post("/", {}, ["Hello", " ", "World"])} - - after {response.finish} - - it "is successful" do - expect(response).to be_success - expect(response.read).to be == "Hello World" - - expect(client.pool).to_not be_busy - end - - it "can buffer response" do - buffer = response.finish - - expect(buffer.join).to be == "Hello World" - - expect(client.pool).to_not be_busy - end - - it "should not contain content-length response header" do - expect(response.headers).to_not include('content-length') - end - - it "fails gracefully when closing connection" do - client.pool.acquire do |connection| - connection.stream.close - end - end - end - end - - context 'content length' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, [], ["Content Length: #{request.body.length}"]] - end - end - - it "can send push promises" do - response = client.post("/test", [], ["Hello World!"]) - expect(response).to be_success - - expect(response.body.length).to be == 18 - expect(response.read).to be == "Content Length: 12" - end - end - - context 'hijack with nil response' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - nil - end - end - - it "fails with appropriate error" do - response = client.get("/") - - expect(response).to be_server_failure - end - end - - context 'partial hijack' do - let(:content) {"Hello World!"} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| - stream.write content - stream.write content - stream.close - end - end - end - - it "reads hijacked body" do - response = client.get("/") - - expect(response.read).to be == (content*2) - end - end - - context 'body with incorrect length' do - let(:bad_body) {Async::HTTP::Body::Buffered.new(["Borked"], 10)} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, bad_body] - end - end - - it "fails with appropriate error" do - response = client.get("/") - - expect do - response.read - end.to raise_error(EOFError) - end - end - - context 'streaming server' do - let!(:sent_chunks) {[]} - - let(:server) do - chunks = sent_chunks - - Async::HTTP::Server.for(@bound_endpoint) do |request| - body = Async::HTTP::Body::Writable.new - - Async::Reactor.run do |task| - 10.times do |i| - chunk = "Chunk #{i}" - chunks << chunk - - body.write chunk - task.sleep 0.25 - end - - body.finish - end - - Protocol::HTTP::Response[200, {}, body] - end - end - - it "can cancel response" do - response = client.get("/") - - expect(response.body.read).to be == "Chunk 0" - - response.close - - expect(sent_chunks).to be == ["Chunk 0"] - end - end - - context 'hijack server' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - if request.hijack? - io = request.hijack! - io.write "HTTP/1.1 200 Okay\r\nContent-Length: 16\r\n\r\nHijack Succeeded" - io.flush - io.close - else - Protocol::HTTP::Response[200, {}, ["Hijack Failed"]] - end - end - end - - it "will hijack response if possible" do - response = client.get("/") - - expect(response.read).to include("Hijack") - end - end - - context 'broken server' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - raise RuntimeError.new('simulated failure') - end - end - - it "can't get /" do - expect do - response = client.get("/") - end.to raise_error(Exception) - end - end - - context 'slow server' do - let(:endpoint) {Async::HTTP::Endpoint.parse('http://127.0.0.1:0', reuse_port: true, timeout: 0.1)} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Async::Task.current.sleep(endpoint.timeout * 2) - Protocol::HTTP::Response[200, {}, []] - end - end - - it "can't get /" do - expect do - client.get("/") - end.to raise_error(Async::TimeoutError) - end - end - - context 'bi-directional streaming', if: described_class.bidirectional? do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - # Echo the request body back to the client. - Protocol::HTTP::Response[200, {}, request.body] - end - end - - it "can read from request body and write response body simultaneously" do - body = Async::HTTP::Body::Writable.new - - # Ideally, the flow here is as follows: - # 1/ Client writes headers to server. - # 2/ Client starts writing data to server (in async task). - # 3/ Client reads headers from server. - # 4a/ Client reads data from server. - # 4b/ Client finishes sending data to server. - response = client.post(endpoint.path, [], body) - - expect(response).to be_success - - body.write "." - count = 0 - - response.each do |chunk| - if chunk.bytesize > 32 - body.close - else - count += 1 - body.write chunk*2 - Async::Task.current.sleep(0.1) - end - end - - expect(count).to be == 6 - end - end - - context 'multiple client requests' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, [request.path]] - end - end - - around do |example| - current = Console.logger.level - Console.logger.fatal! - - example.run - ensure - Console.logger.level = current - end - - it "doesn't cancel all requests" do - tasks = [] - task = Async::Task.current - stopped = [] - - 10.times do - tasks << task.async { - begin - loop do - client.get('http://127.0.0.1:8080/a').finish - end - ensure - stopped << 'a' - end - } - end - - 10.times do - tasks << task.async { - begin - loop do - client.get('http://127.0.0.1:8080/b').finish - end - ensure - stopped << 'b' - end - } - end - - tasks.each do |child| - task.sleep 0.01 - child.stop - end - - expect(stopped.sort).to be == stopped - end - end -end diff --git a/test/async/http/proxy.rb b/test/async/http/proxy.rb index ace9665f..23c823e4 100644 --- a/test/async/http/proxy.rb +++ b/test/async/http/proxy.rb @@ -9,32 +9,34 @@ require 'async/http/protocol' require 'async/http/body/hijack' -require_relative 'server_context' +require 'sus/fixtures/async/http' -RSpec.shared_examples_for Async::HTTP::Proxy do - include_context Async::HTTP::Server +AProxy = Sus::Shared("a proxy") do + include Sus::Fixtures::Async::HTTP::ServerContext - describe '.proxied_endpoint' do + let(:protocol) {subject} + + with '.proxied_endpoint' do it "can construct valid endpoint" do endpoint = Async::HTTP::Endpoint.parse("http://www.codeotaku.com") proxied_endpoint = client.proxied_endpoint(endpoint) - expect(proxied_endpoint).to be_kind_of(Async::HTTP::Endpoint) + expect(proxied_endpoint).to be_a(Async::HTTP::Endpoint) end end - describe '.proxied_client' do + with '.proxied_client' do it "can construct valid client" do endpoint = Async::HTTP::Endpoint.parse("http://www.codeotaku.com") proxied_client = client.proxied_client(endpoint) - expect(proxied_client).to be_kind_of(Async::HTTP::Client) + expect(proxied_client).to be_a(Async::HTTP::Client) end end - context 'CONNECT' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with 'CONNECT' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| chunk = stream.read stream.close_read @@ -52,7 +54,7 @@ response = client.connect("127.0.0.1:1234", [], input) - expect(response).to be_success + expect(response).to be(:success?) input.write(data) input.close @@ -61,9 +63,9 @@ end end - context 'echo server' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with 'echo server' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| expect(request.path).to be == "localhost:1" Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| @@ -81,7 +83,7 @@ it "can connect to remote system using block" do proxy = Async::HTTP::Proxy.tcp(client, "localhost", 1) - expect(proxy.client.pool).to be_empty + expect(proxy.client.pool).to be(:empty?) proxy.connect do |peer| stream = Async::IO::Stream.new(peer) @@ -93,12 +95,12 @@ end proxy.close - expect(proxy.client.pool).to be_empty + expect(proxy.client.pool).to be(:empty?) end it "can connect to remote system" do proxy = Async::HTTP::Proxy.tcp(client, "localhost", 1) - expect(proxy.client.pool).to be_empty + expect(proxy.client.pool).to be(:empty?) stream = Async::IO::Stream.new(proxy.connect) @@ -110,13 +112,13 @@ stream.close proxy.close - expect(proxy.client.pool).to be_empty + expect(proxy.client.pool).to be(:empty?) end end - context 'proxied client' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with 'proxied client' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| expect(request.method).to be == "CONNECT" unless authorization_lambda.call(request) @@ -174,18 +176,16 @@ proxy_client = client.proxied_client(endpoint) response = proxy_client.get("/search") - expect(response).to_not be_failure + expect(response).not.to be(:failure?) # The response would be a redirect: - expect(response).to be_redirection + expect(response).to be(:redirection?) response.finish # The proxy.connnect response is not being released correctly - after pipe is done: - expect(proxy_client.pool).to_not be_empty + expect(proxy_client.pool).not.to be(:empty?) proxy_client.close - expect(proxy_client.pool).to be_empty - - pp client + expect(proxy_client.pool).to be(:empty?) end it 'can get secure website' do @@ -194,18 +194,18 @@ response = proxy_client.get("/search") - expect(response).to_not be_failure - expect(response.read).to_not be_empty + expect(response).not.to be(:failure?) + expect(response.read).not.to be(:empty?) proxy_client.close end - context 'authorization header required' do + with 'authorization header required' do let(:authorization_lambda) do ->(request) {request.headers['proxy-authorization'] == 'supersecretpassword' } end - context 'request includes headers' do + with 'request includes headers' do let(:headers) { [['Proxy-Authorization', 'supersecretpassword']] } it 'succeeds' do @@ -214,14 +214,14 @@ response = proxy_client.get('/search') - expect(response).to_not be_failure - expect(response.read).to_not be_empty + expect(response).not.to be(:failure?) + expect(response.read).not.to be(:empty?) proxy_client.close end end - context 'request does not include headers' do + with 'request does not include headers' do it 'does not succeed' do endpoint = Async::HTTP::Endpoint.parse("https://www.google.com") proxy_client = client.proxied_client(endpoint) @@ -229,7 +229,7 @@ expect do # Why is this response not 407? Because the response should come from the proxied connection, but that connection failed to be established. Because of that, there is no response. If we respond here with 407, it would be indistinguisable from the remote server returning 407. That would be an odd case, but none-the-less a valid one. response = proxy_client.get('/search') - end.to raise_error(Async::HTTP::Proxy::ConnectFailure) + end.to raise_exception(Async::HTTP::Proxy::ConnectFailure) proxy_client.close end @@ -238,14 +238,14 @@ end end -RSpec.describe Async::HTTP::Protocol::HTTP10 do - it_behaves_like Async::HTTP::Proxy +describe Async::HTTP::Protocol::HTTP10 do + it_behaves_like AProxy end -RSpec.describe Async::HTTP::Protocol::HTTP11 do - it_behaves_like Async::HTTP::Proxy +describe Async::HTTP::Protocol::HTTP11 do + it_behaves_like AProxy end -RSpec.describe Async::HTTP::Protocol::HTTP2 do - it_behaves_like Async::HTTP::Proxy +describe Async::HTTP::Protocol::HTTP2 do + it_behaves_like AProxy end diff --git a/test/async/http/relative_location.rb b/test/async/http/relative_location.rb index 55657d39..4f453dbb 100644 --- a/test/async/http/relative_location.rb +++ b/test/async/http/relative_location.rb @@ -4,21 +4,20 @@ # Copyright, 2018-2023, by Samuel Williams. # Copyright, 2019-2020, by Brian Morearty. -require_relative 'server_context' - require 'async/http/relative_location' require 'async/http/server' -RSpec.describe Async::HTTP::RelativeLocation do - include_context Async::HTTP::Server - let(:protocol) {Async::HTTP::Protocol::HTTP1} +require 'sus/fixtures/async/http' + +describe Async::HTTP::RelativeLocation do + include Sus::Fixtures::Async::HTTP::ServerContext - subject {described_class.new(@client, 1)} + let(:relative_location) {subject.new(@client, 1)} - context 'server redirections' do - context '301' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with 'server redirections' do + with '301' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| case request.path when '/home' Protocol::HTTP::Response[301, {'location' => '/'}, []] @@ -31,30 +30,30 @@ end it 'should redirect POST to GET' do - response = subject.post('/') + response = relative_location.post('/') - expect(response).to be_success + expect(response).to be(:success?) expect(response.read).to be == "GET" end - context 'limiting redirects' do + with 'limiting redirects' do it 'should allow the maximum number of redirects' do - response = subject.get('/') + response = relative_location.get('/') response.finish - expect(response).to be_success + expect(response).to be(:success?) end it 'should fail with maximum redirects' do expect{ - response = subject.get('/home') - }.to raise_error(Async::HTTP::TooManyRedirects, /maximum/) + response = relative_location.get('/home') + }.to raise_exception(Async::HTTP::TooManyRedirects, message: be =~ /maximum/) end end end - context '302' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with '302' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| case request.path when '/' Protocol::HTTP::Response[302, {'location' => '/index.html'}, []] @@ -65,16 +64,16 @@ end it 'should redirect POST to GET' do - response = subject.post('/') + response = relative_location.post('/') - expect(response).to be_success + expect(response).to be(:success?) expect(response.read).to be == "GET" end end - context '307' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with '307' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| case request.path when '/' Protocol::HTTP::Response[307, {'location' => '/index.html'}, []] @@ -85,16 +84,16 @@ end it 'should redirect with same method' do - response = subject.post('/') + response = relative_location.post('/') - expect(response).to be_success + expect(response).to be(:success?) expect(response.read).to be == "POST" end end - context '308' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with '308' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| case request.path when '/' Protocol::HTTP::Response[308, {'location' => '/index.html'}, []] @@ -105,9 +104,9 @@ end it 'should redirect with same method' do - response = subject.post('/') + response = relative_location.post('/') - expect(response).to be_success + expect(response).to be(:success?) expect(response.read).to be == "POST" end end diff --git a/test/async/http/retry.rb b/test/async/http/retry.rb index 2c648ad4..20fb7dd8 100644 --- a/test/async/http/retry.rb +++ b/test/async/http/retry.rb @@ -3,37 +3,38 @@ # Released under the MIT License. # Copyright, 2020-2023, by Samuel Williams. -require_relative 'server_context' - require 'async/http/client' require 'async/http/endpoint' -RSpec.describe 'consistent retry behaviour' do - include_context Async::HTTP::Server - let(:protocol) {Async::HTTP::Protocol::HTTP1} +require 'sus/fixtures/async/http' + +describe 'consistent retry behaviour' do + include Sus::Fixtures::Async::HTTP::ServerContext let(:delay) {0.1} let(:retries) {2} - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Async::Task.current.sleep(delay) + let(:app) do + Protocol::HTTP::Middleware.for do |request| + sleep(delay) Protocol::HTTP::Response[200, {}, []] end end def make_request(body) # This causes the first request to fail with "SocketError" which is retried: - Async::Task.current.with_timeout(delay / 2, SocketError) do + Async::Task.current.with_timeout(delay / 2.0, SocketError) do return client.get('/', {}, body) end end - specify 'with nil body' do - make_request(nil) + it "retries with nil body" do + response = make_request(nil) + expect(response).to be(:success?) end - specify 'with empty array body' do - make_request([]) + it "retries with empty body" do + response = make_request([]) + expect(response).to be(:success?) end end diff --git a/test/async/http/server_context.rb b/test/async/http/server_context.rb deleted file mode 100644 index 82779170..00000000 --- a/test/async/http/server_context.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. - -require 'async/http/server' -require 'async/http/client' -require 'async/http/endpoint' -require 'async/io/shared_endpoint' - -RSpec.shared_context Async::HTTP::Server do - include_context Async::RSpec::Reactor - - let(:protocol) {described_class} - let(:endpoint) {Async::HTTP::Endpoint.parse('http://127.0.0.1:0', timeout: 0.8, reuse_port: true, protocol: protocol)} - - let(:server_endpoint) {endpoint} - let(:client_endpoint) {endpoint} - - let(:retries) {1} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, []] - end - end - - before do - # We bind the endpoint before running the server so that we know incoming connections will be accepted: - @bound_endpoint = Async::IO::SharedEndpoint.bound(server_endpoint) - - # I feel a dedicated class might be better than this hack: - allow(@bound_endpoint).to receive(:protocol).and_return(server_endpoint.protocol) - allow(@bound_endpoint).to receive(:scheme).and_return(server_endpoint.scheme) - - @server_task = Async do - server.run - end - - local_address_endpoint = @bound_endpoint.local_address_endpoint - - if timeout = client_endpoint.timeout - local_address_endpoint.each do |endpoint| - endpoint.options = {timeout: timeout} - end - end - - client_endpoint.endpoint = local_address_endpoint - @client = Async::HTTP::Client.new(client_endpoint, protocol: client_endpoint.protocol, retries: retries) - end - - after do - @client&.close - @server_task&.stop - @bound_endpoint&.close - end - - let(:client) {@client} -end diff --git a/test/async/http/ssl.rb b/test/async/http/ssl.rb index c2586a37..54ccb70c 100644 --- a/test/async/http/ssl.rb +++ b/test/async/http/ssl.rb @@ -9,14 +9,15 @@ require 'async/io/ssl_socket' -require 'async/rspec/reactor' -require 'async/rspec/ssl' +require 'sus/fixtures/async' +require 'sus/fixtures/openssl' +require 'sus/fixtures/async/http' -RSpec.describe Async::HTTP::Server, timeout: 5 do - include_context Async::RSpec::Reactor - include_context Async::RSpec::SSL::ValidCertificate +describe Async::HTTP::Server do + include Sus::Fixtures::Async::HTTP::ServerContext + include Sus::Fixtures::OpenSSL::ValidCertificateContext - describe "application layer protocol negotiation" do + with "application layer protocol negotiation" do let(:server_context) do OpenSSL::SSL::SSLContext.new.tap do |context| context.cert = certificate @@ -39,30 +40,19 @@ end end - # Shared port for localhost network tests. - let(:server_endpoint) {Async::HTTP::Endpoint.parse("https://localhost:6779", ssl_context: server_context)} - let(:client_endpoint) {Async::HTTP::Endpoint.parse("https://localhost:6779", ssl_context: client_context)} + def make_server_endpoint(bound_endpoint) + Async::IO::SSLEndpoint.new(super, ssl_context: server_context) + end + + def make_client_endpoint(bound_endpoint) + Async::IO::SSLEndpoint.new(super, ssl_context: client_context) + end it "client can get a resource via https" do - server = Async::HTTP::Server.for(server_endpoint, protocol: Async::HTTP::Protocol::HTTP1) do |request| - Protocol::HTTP::Response[200, {}, ['Hello World']] - end - - client = Async::HTTP::Client.new(client_endpoint) - - Async do |task| - server_task = task.async do - server.run - end - - response = client.get("/") - - expect(response).to be_success - expect(response.read).to be == "Hello World" + response = client.get("/") - client.close - server_task.stop - end + expect(response).to be(:success?) + expect(response.read).to be == "Hello World!" end end end diff --git a/test/async/http/statistics.rb b/test/async/http/statistics.rb index dac46248..df0aa039 100644 --- a/test/async/http/statistics.rb +++ b/test/async/http/statistics.rb @@ -3,17 +3,15 @@ # Released under the MIT License. # Copyright, 2018-2023, by Samuel Williams. -require_relative 'server_context' - require 'async/http/statistics' +require 'sus/fixtures/async/http' -RSpec.describe Async::HTTP::Statistics, timeout: 5 do - include_context Async::HTTP::Server - let(:protocol) {Async::HTTP::Protocol::HTTP1} +describe Async::HTTP::Statistics do + include Sus::Fixtures::Async::HTTP::ServerContext - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - statistics = described_class.start + let(:app) do + Protocol::HTTP::Middleware.for do |request| + statistics = subject.start response = Protocol::HTTP::Response[200, {}, ["Hello ", "World!"]] @@ -21,7 +19,7 @@ expect(statistics.sent).to be == 12 expect(error).to be_nil end.tap do |response| - expect(response.body).to receive(:complete_statistics).and_call_original + expect(response.body).to receive(:complete_statistics) end end end @@ -30,6 +28,6 @@ response = client.get("/") expect(response.read).to be == "Hello World!" - expect(response).to be_success + expect(response).to be(:success?) end end