require "helper" describe HTTP2::Stream do before(:each) do @client = Client.new @stream = @client.new_stream end context "stream states" do it "should initiliaze all streams to IDLE" do @stream.state.should eq :idle end it "should set custom stream priority" do stream = @client.new_stream(priority: 3) stream.priority.should eq 3 end context "reserved (local)" do before(:each) { @stream.send PUSH_PROMISE } it "should transition on sent PUSH_PROMISE" do @stream.state.should eq :reserved_local end it "should allow HEADERS to be sent" do expect { @stream.send HEADERS }.to_not raise_error end it "should raise error if sending invalid frames" do (FRAME_TYPES - [HEADERS, RST_STREAM]).each do |type| expect { @stream.dup.send type }.to raise_error StreamError end end it "should raise error on receipt of invalid frames" do (FRAME_TYPES - [PRIORITY, RST_STREAM]).each do |type| expect { @stream.dup.receive type }.to raise_error StreamError end end it "should transition to half closed (remote) on sent HEADERS" do @stream.send HEADERS @stream.state.should eq :half_closed_remote end it "should transition to closed on sent RST_STREAM" do @stream.close @stream.state.should eq :closed end it "should transition to closed on received RST_STREAM" do @stream.receive RST_STREAM @stream.state.should eq :closed end it "should reprioritize stream on PRIORITY" do @stream.receive PRIORITY.merge({priority: 30}) @stream.priority.should eq 30 end end context "reserved (remote)" do before(:each) { @stream.receive PUSH_PROMISE } it "should transition on received PUSH_PROMISE" do @stream.state.should eq :reserved_remote end it "should raise error if sending invalid frames" do (FRAME_TYPES - [PRIORITY, RST_STREAM]).each do |type| expect { @stream.dup.send type }.to raise_error StreamError end end it "should raise error on receipt of invalid frames" do (FRAME_TYPES - [HEADERS, RST_STREAM]).each do |type| expect { @stream.dup.receive type }.to raise_error StreamError end end it "should transition to half closed (local) on received HEADERS" do @stream.receive HEADERS @stream.state.should eq :half_closed_local end it "should transition to closed on sent RST_STREAM" do @stream.close @stream.state.should eq :closed end it "should transition to closed on received RST_STREAM" do @stream.receive RST_STREAM @stream.state.should eq :closed end it "should reprioritize stream on PRIORITY" do @stream.send PRIORITY @stream.priority.should eq 15 end end context "open" do before(:each) { @stream.receive HEADERS } it "should allow any valid frames types to be sent" do (FRAME_TYPES - [PING, GOAWAY, SETTINGS]).each do |type| expect { @stream.dup.send type }.to_not raise_error end end it "should allow frames of any type to be received" do FRAME_TYPES.each do |type| expect { @stream.dup.receive type }.to_not raise_error end end it "should transition to half closed (local) if sending END_STREAM" do [DATA, HEADERS, CONTINUATION].each do |frame| s, f = @stream.dup, frame.dup f[:flags] = [:end_stream] s.send f s.state.should eq :half_closed_local end end it "should transition to half closed (remote) if receiving END_STREAM" do [DATA, HEADERS, CONTINUATION].each do |frame| s, f = @stream.dup, frame.dup f[:flags] = [:end_stream] s.receive f s.state.should eq :half_closed_remote end end it "should transition to half closed if remote opened with END_STREAM" do s = @client.new_stream hclose = HEADERS.dup hclose[:flags] = [:end_stream] s.receive hclose s.state.should eq :half_closed_remote end it "should transition to half closed if local opened with END_STREAM" do s = @client.new_stream hclose = HEADERS.dup hclose[:flags] = [:end_stream] s.send hclose s.state.should eq :half_closed_local end it "should transition to closed if sending RST_STREAM" do @stream.close @stream.state.should eq :closed end it "should transition to closed if receiving RST_STREAM" do @stream.receive RST_STREAM @stream.state.should eq :closed end it "should emit :active on open transition" do openp, openr = false, false sp = @client.new_stream sr = @client.new_stream sp.on(:active) { openp = true } sr.on(:active) { openr = true } sp.receive HEADERS sr.send HEADERS openp.should be_true openr.should be_true end it "should not emit :active on transition from open" do order, stream = [], @client.new_stream stream.on(:active) { order << :active } stream.on(:half_close) { order << :half_close } stream.on(:close) { order << :close } req = HEADERS.dup req[:flags] = [:end_headers] stream.send req stream.send DATA order.should eq [:active, :half_close] end it "should emit :close on close transition" do closep, closer = false, false sp, sr = @stream.dup, @stream.dup sp.on(:close) { closep = true } sr.on(:close) { closer = true } sp.receive RST_STREAM sr.close closep.should be_true closer.should be_true end it "should emit :close after frame is processed" do order, stream = [], @client.new_stream stream.on(:active) { order << :active } stream.on(:data) { order << :data } stream.on(:half_close) { order << :half_close } stream.on(:close) { order << :close } req = HEADERS.dup req[:flags] = [:end_stream, :end_headers] stream.send req stream.receive HEADERS stream.receive DATA order.should eq [:active, :half_close, :data, :close] end it "should emit :close with reason" do reason = nil @stream.on(:close) {|r| reason = r } @stream.receive RST_STREAM reason.should_not be_nil end end context "half closed (local)" do before(:each) { @stream.send HEADERS_END_STREAM } it "should raise error on attempt to send frames" do (FRAME_TYPES - [RST_STREAM]).each do |frame| expect { @stream.dup.send frame }.to raise_error StreamError end end it "should transition to closed on receipt of END_STREAM flag" do [DATA, HEADERS, CONTINUATION].each do |frame| s, f = @stream.dup, frame.dup f[:flags] = [:end_stream] s.receive f s.state.should eq :closed end end it "should transition to closed on receipt of RST_STREAM frame" do @stream.receive RST_STREAM @stream.state.should eq :closed end it "should transition to closed if RST_STREAM frame is sent" do @stream.send RST_STREAM @stream.state.should eq :closed end it "should ignore received WINDOW_UPDATE, PRIORITY frames" do expect { @stream.receive WINDOW_UPDATE }.to_not raise_error expect { @stream.receive PRIORITY }.to_not raise_error @stream.state.should eq :half_closed_local end it "should emit :half_close event on transition" do order = [] stream = @client.new_stream stream.on(:active) { order << :active } stream.on(:half_close) { order << :half_close } req = HEADERS.dup req[:flags] = [:end_stream, :end_headers] stream.send req order.should eq [:active, :half_close] end it "should emit :close event on transition to closed" do closed = false @stream.on(:close) { closed = true } @stream.receive RST_STREAM @stream.state.should eq :closed closed.should be_true end end context "half closed (remote)" do before(:each) { @stream.receive HEADERS_END_STREAM } it "should raise STREAM_CLOSED error on reciept of frames" do (FRAME_TYPES - [RST_STREAM, WINDOW_UPDATE]).each do |frame| expect { @stream.dup.receive frame }.to raise_error(StreamClosed) end end it "should transition to closed if END_STREAM flag is sent" do [DATA, HEADERS, CONTINUATION].each do |frame| s, f = @stream.dup, frame.dup f[:flags] = [:end_stream] s.on(:close) { s.state.should eq :closed } s.send f s.state.should eq :closed end end it "should transition to closed if RST_STREAM is sent" do @stream.close @stream.state.should eq :closed end it "should transition to closed on reciept of RST_STREAM frame" do @stream.receive RST_STREAM @stream.state.should eq :closed end it "should ignore received WINDOW_UPDATE frames" do expect { @stream.receive WINDOW_UPDATE }.to_not raise_error @stream.state.should eq :half_closed_remote end it "should emit :half_close event on transition" do order = [] stream = @client.new_stream stream.on(:active) { order << :active } stream.on(:half_close) { order << :half_close } req = HEADERS.dup req[:flags] = [:end_stream, :end_headers] stream.receive req order.should eq [:active, :half_close] end it "should emit :close event on close transition" do closed = false @stream.on(:close) { closed = true } @stream.close @stream.state.should eq :closed closed.should be_true end end context "closed" do context "remote closed stream" do before(:each) do @stream.send HEADERS_END_STREAM # half closed local @stream.receive HEADERS_END_STREAM # closed by remote end it "should raise STREAM_CLOSED on attempt to send frames" do (FRAME_TYPES - [RST_STREAM]).each do |frame| expect { @stream.dup.send frame }.to raise_error(StreamClosed) end end it "should raise STREAM_CLOSED on receipt of frame" do (FRAME_TYPES - [RST_STREAM]).each do |frame| expect { @stream.dup.receive frame }.to raise_error(StreamClosed) end end it "should allow RST_STREAM to be sent" do expect { @stream.send RST_STREAM }.to_not raise_error end it "should not send RST_STREAM on receipt of RST_STREAM" do expect { @stream.receive RST_STREAM }.to_not raise_error end end context "local closed via RST_STREAM frame" do before(:each) do @stream.send HEADERS # open @stream.send RST_STREAM # closed by local end it "should ignore received frames" do (FRAME_TYPES - [PUSH_PROMISE]).each do |frame| expect { cb = [] @stream.on(:data) { cb << :data } @stream.on(:headers) { cb << :headers} @stream.dup.receive frame cb.should be_empty }.to_not raise_error end end #it "should transition to reserved remote on PUSH_PROMISE" do # An endpoint might receive a PUSH_PROMISE frame after it sends # RST_STREAM. PUSH_PROMISE causes a stream to become "reserved". # ... # We're auto RST'ing PUSH streams in connection class, hence # skipping this transition for now. #end end context "local closed via END_STREAM flag" do before(:each) do @stream.send HEADERS # open @stream.send DATA # contains end_stream flag end it "should ignore received frames" do FRAME_TYPES.each do |frame| expect { @stream.dup.receive frame }.to_not raise_error end end end end end # end stream states context "flow control" do it "should initialize to default flow control window" do @stream.window.should eq DEFAULT_FLOW_WINDOW end it "should update window size on DATA frames only" do @stream.send HEADERS # go to open @stream.window.should eq DEFAULT_FLOW_WINDOW (FRAME_TYPES - [DATA,PING,GOAWAY,SETTINGS]).each do |frame| s = @stream.dup s.send frame s.window.should eq DEFAULT_FLOW_WINDOW end @stream.send DATA @stream.window.should eq DEFAULT_FLOW_WINDOW - DATA[:payload].bytesize end it "should update window size on receipt of WINDOW_UPDATE" do @stream.send HEADERS @stream.send DATA @stream.receive WINDOW_UPDATE @stream.window.should eq ( DEFAULT_FLOW_WINDOW - DATA[:payload].bytesize + WINDOW_UPDATE[:increment] ) end it "should observe session flow control" do settings, data = SETTINGS.dup, DATA.dup settings[:payload] = { settings_initial_window_size: 1000 } settings[:stream] = 0 framer = Framer.new @client << framer.generate(settings) s1 = @client.new_stream s1.send HEADERS s1.send data.merge({payload: "x" * 900, flags: []}) s1.window.should eq 100 s1.send data.merge({payload: "x" * 200}) s1.window.should eq 0 s1.buffered_amount.should eq 100 @client << framer.generate(WINDOW_UPDATE.merge({ stream: s1.id, increment: 1000 })) s1.buffered_amount.should eq 0 s1.window.should eq 900 end end context "client API" do it ".reprioritize should emit PRIORITY frame" do @stream.should_receive(:send) do |frame| frame[:type].should eq :priority frame[:priority].should eq 30 end @stream.reprioritize 30 end it ".reprioritize should raise error if invoked by server" do srv = Server.new stream = srv.new_stream expect { stream.reprioritize(10) }.to raise_error(StreamError) end it ".headers should emit HEADERS frames" do payload = { ':method' => 'GET', ':scheme' => 'http', ':host' => 'www.example.org', ':path' => '/resource', 'custom' => 'value' } @stream.should_receive(:send) do |frame| frame[:type].should eq :headers frame[:payload].should eq payload.to_a frame[:flags].should eq [:end_headers] end @stream.headers(payload, end_stream: false, end_headers: true) end it ".data should emit DATA frames" do @stream.should_receive(:send) do |frame| frame[:type].should eq :data frame[:payload].should eq "text" frame[:flags].should be_empty end @stream.data("text", end_stream: false) @stream.should_receive(:send) do |frame| frame[:flags].should eq [:end_stream] end @stream.data("text") end it ".data should split large DATA frames" do data = "x" * HTTP2::MAX_FRAME_SIZE * 2 @stream.stub(:send) @stream.should_receive(:send).exactly(3).times @stream.data(data + "x") end it ".cancel should reset stream with cancel error code" do @stream.should_receive(:send) do |frame| frame[:type].should eq :rst_stream frame[:error].should eq :cancel end @stream.cancel end it ".refuse should reset stream with refused stream error code" do @stream.should_receive(:send) do |frame| frame[:type].should eq :rst_stream frame[:error].should eq :refused_stream end @stream.refuse end end context "server API" do before(:each) do @srv = Server.new @frm = Framer.new @client.on(:frame) {|bytes| @srv << bytes } @client_stream = @client.new_stream end it "should emit received headers via on(:headers)" do headers, recv = {"header" => "value"}, nil @srv.on(:stream) do |stream| stream.on(:headers) {|h| recv = h} end @client_stream.headers(headers) recv.should eq headers end it "should emit received payload via on(:data)" do payload, recv = "some-payload", nil @srv.on(:stream) do |stream| stream.on(:data) do |recv| recv.should eq payload end end @client_stream.headers({"key" => "value"}) @client_stream.data(payload) end it "should emit received priority via on(:priority)" do new_priority, recv = 15, 0 @srv.on(:stream) do |stream| stream.on(:priority) do |pri| pri.should eq new_priority end end @client_stream.headers({"key" => "value"}) @client_stream.reprioritize(new_priority) end context "push" do before(:each) do @srv.on(:frame) {|bytes| @client << bytes } @srv.on(:stream) do |stream| @server_stream = stream end # @srv << @frm.generate(SETTINGS) @client_stream.headers({"key" => "value"}) end it ".promise should emit server initiated stream" do push = nil @server_stream.promise({"key" => "val"}) { |pstream| push = pstream } push.id.should eq 2 end it ".promise push stream should have parent stream" do push = nil @server_stream.promise({"key" => "val"}) { |pstream| push = pstream } push.state.should eq :reserved_local push.parent.id.should eq @server_stream.id end context "stream states" do it "server: active > half close > close" do order = [] @server_stream.promise({"key" => "val"}) do |push| stream = push push.state.should eq :reserved_local order << :reserved push.on(:active) { order << :active } push.on(:half_close){ order << :half_close } push.on(:close) { order << :close } push.headers({"key2" => "val2"}) push.send DATA.merge({stream: stream.id}) end order.should eq [:reserved, :active, :half_close, :close] end it "client: headers > active > headers > .. > data > close" do order, headers = [], {} @client.on(:promise) do |push| order << :reserved push.on(:active) { order << :active } push.on(:data) { order << :data } push.on(:half_close){ order << :half_close } push.on(:close) { order << :close } push.on(:headers) do |h| order << :headers headers.merge!(h) end push.id.should be_even end @server_stream.promise({"key" => "val"}) do |push| push.headers("key2" => "val2") push.data("somedata") end headers.should eq({"key" => "val", "key2" => "val2"}) order.should eq [:reserved, :headers, :active, :headers, :half_close, :data, :close] end end end end end