summaryrefslogtreecommitdiff
path: root/test/spec_chunked.rb
blob: ceb7bdfb2e1df2e03b4a2fb77e25ccb3474347c0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# frozen_string_literal: true

require_relative 'helper'

describe Rack::Chunked do
  def chunked(app)
    proc do |env|
      app = Rack::Chunked.new(app)
      response = Rack::Lint.new(app).call(env)
      # we want to use body like an array, but it only has #each
      response[2] = response[2].to_enum.to_a
      response
    end
  end

  before do
    @env = Rack::MockRequest.
      env_for('/', 'SERVER_PROTOCOL' => 'HTTP/1.1', 'REQUEST_METHOD' => 'GET')
  end

  class TrailerBody
    def each(&block)
      ['Hello', ' ', 'World!'].each(&block)
    end

    def trailers
      { "Expires" => "tomorrow" }
    end
  end

  it 'yields trailer headers after the response' do
    app = lambda { |env|
      [200, { "Content-Type" => "text/plain", "Trailer" => "Expires" }, TrailerBody.new]
    }
    response = Rack::MockResponse.new(*chunked(app).call(@env))
    response.headers.wont_include 'Content-Length'
    response.headers['Transfer-Encoding'].must_equal 'chunked'
    response.body.must_equal "5\r\nHello\r\n1\r\n \r\n6\r\nWorld!\r\n0\r\nExpires: tomorrow\r\n\r\n"
  end

  it 'chunk responses with no Content-Length' do
    app = lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hello', ' ', 'World!']] }
    response = Rack::MockResponse.new(*chunked(app).call(@env))
    response.headers.wont_include 'Content-Length'
    response.headers['Transfer-Encoding'].must_equal 'chunked'
    response.body.must_equal "5\r\nHello\r\n1\r\n \r\n6\r\nWorld!\r\n0\r\n\r\n"
  end

  it 'chunks empty bodies properly' do
    app = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
    response = Rack::MockResponse.new(*chunked(app).call(@env))
    response.headers.wont_include 'Content-Length'
    response.headers['Transfer-Encoding'].must_equal 'chunked'
    response.body.must_equal "0\r\n\r\n"
  end

  it 'closes body' do
    obj = Object.new
    closed = false
    def obj.each; yield 's' end
    obj.define_singleton_method(:close) { closed = true }
    app = lambda { |env| [200, { "Content-Type" => "text/plain" }, obj] }
    response = Rack::MockRequest.new(Rack::Chunked.new(app)).get('/', @env)
    response.headers.wont_include 'Content-Length'
    response.headers['Transfer-Encoding'].must_equal 'chunked'
    response.body.must_equal "1\r\ns\r\n0\r\n\r\n"
    closed.must_equal true
  end

  it 'chunks encoded bodies properly' do
    body = ["\uFFFEHello", " ", "World"].map {|t| t.encode("UTF-16LE") }
    app  = lambda { |env| [200, { "Content-Type" => "text/plain" }, body] }
    response = Rack::MockResponse.new(*chunked(app).call(@env))
    response.headers.wont_include 'Content-Length'
    response.headers['Transfer-Encoding'].must_equal 'chunked'
    response.body.encoding.to_s.must_equal "ASCII-8BIT"
    response.body.must_equal "c\r\n\xFE\xFFH\x00e\x00l\x00l\x00o\x00\r\n2\r\n \x00\r\na\r\nW\x00o\x00r\x00l\x00d\x00\r\n0\r\n\r\n".dup.force_encoding("BINARY")
    response.body.must_equal "c\r\n\xFE\xFFH\x00e\x00l\x00l\x00o\x00\r\n2\r\n \x00\r\na\r\nW\x00o\x00r\x00l\x00d\x00\r\n0\r\n\r\n".dup.force_encoding(Encoding::BINARY)
  end

  it 'not modify response when Content-Length header present' do
    app = lambda { |env|
      [200, { "Content-Type" => "text/plain", 'Content-Length' => '12' }, ['Hello', ' ', 'World!']]
    }
    status, headers, body = chunked(app).call(@env)
    status.must_equal 200
    headers.wont_include 'Transfer-Encoding'
    headers.must_include 'Content-Length'
    body.join.must_equal 'Hello World!'
  end

  it 'not modify response when client is HTTP/1.0' do
    app = lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hello', ' ', 'World!']] }
    @env['SERVER_PROTOCOL'] = 'HTTP/1.0'
    status, headers, body = chunked(app).call(@env)
    status.must_equal 200
    headers.wont_include 'Transfer-Encoding'
    body.join.must_equal 'Hello World!'
  end

  it 'not modify response when client is ancient, pre-HTTP/1.0' do
    app = lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hello', ' ', 'World!']] }
    check = lambda do
      status, headers, body = chunked(app).call(@env.dup)
      status.must_equal 200
      headers.wont_include 'Transfer-Encoding'
      body.join.must_equal 'Hello World!'
    end

    @env.delete('SERVER_PROTOCOL') # unicorn will do this on pre-HTTP/1.0 requests
    check.call

    @env['SERVER_PROTOCOL'] = 'HTTP/0.9' # not sure if this happens in practice
    check.call
  end

  it 'not modify response when Transfer-Encoding header already present' do
    app = lambda { |env|
      [200, { "Content-Type" => "text/plain", 'Transfer-Encoding' => 'identity' }, ['Hello', ' ', 'World!']]
    }
    status, headers, body = chunked(app).call(@env)
    status.must_equal 200
    headers['Transfer-Encoding'].must_equal 'identity'
    body.join.must_equal 'Hello World!'
  end

  [100, 204, 304].each do |status_code|
    it "not modify response when status code is #{status_code}" do
      app = lambda { |env| [status_code, {}, []] }
      status, headers, _ = chunked(app).call(@env)
      status.must_equal status_code
      headers.wont_include 'Transfer-Encoding'
    end
  end
end