File: response_spec.cr

package info (click to toggle)
crystal 1.14.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 24,384 kB
  • sloc: javascript: 6,400; sh: 695; makefile: 269; ansic: 121; python: 105; cpp: 77; xml: 32
file content (416 lines) | stat: -rw-r--r-- 12,983 bytes parent folder | download
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
require "spec"
require "http/server/response"
require "http/headers"
require "http/status"
require "http/cookie"

private alias Response = HTTP::Server::Response

private class ReverseResponseOutput < IO
  @output : IO

  def initialize(@output : IO)
  end

  def write(slice : Bytes) : Nil
    slice.reverse_each do |byte|
      @output.write_byte(byte)
    end
  end

  def read(slice : Bytes)
    raise "Not implemented"
  end

  def close
    @output.close
  end

  def flush
    @output.flush
  end
end

describe HTTP::Server::Response do
  it "closes" do
    io = IO::Memory.new
    response = Response.new(io)
    response.close
    response.closed?.should be_true
    io.closed?.should be_false
    expect_raises(IO::Error, "Closed stream") { response << "foo" }
    io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
  end

  it "does not automatically add the `content-length` header if the response is a 304" do
    io = IO::Memory.new
    response = Response.new(io)
    response.status = :not_modified
    response.close
    io.to_s.should eq("HTTP/1.1 304 Not Modified\r\n\r\n")
  end

  it "does not automatically add the `content-length` header if the response is a 204" do
    io = IO::Memory.new
    response = Response.new(io)
    response.status = :no_content
    response.close
    io.to_s.should eq("HTTP/1.1 204 No Content\r\n\r\n")
  end

  it "does not automatically add the `content-length` header if the response is informational" do
    io = IO::Memory.new
    response = Response.new(io)
    response.status = :processing
    response.close
    io.to_s.should eq("HTTP/1.1 102 Processing\r\n\r\n")
  end

  # Case where the content-length represents the size of the data that would have been returned.
  it "allows specifying the content-length header explicitly" do
    io = IO::Memory.new
    response = Response.new(io)
    response.status = :not_modified
    response.headers["Content-Length"] = "5"
    response.close
    io.to_s.should eq("HTTP/1.1 304 Not Modified\r\nContent-Length: 5\r\n\r\n")
  end

  it "prints less then buffer's size" do
    io = IO::Memory.new
    response = Response.new(io)
    response.print("Hello")
    response.close
    io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello")
  end

  it "prints less then buffer's size to output" do
    io = IO::Memory.new
    response = Response.new(io)
    response.output.print("Hello")
    response.output.close
    io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello")
  end

  it "prints more then buffer's size" do
    io = IO::Memory.new
    response = Response.new(io)
    str = "1234567890"
    slices = (IO::DEFAULT_BUFFER_SIZE // 10)
    slices.times do
      response.print(str)
    end
    response.print(str)
    response.close
    first_chunk = str * slices
    second_chunk = str
    io.to_s.should eq("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n#{(first_chunk.bytesize).to_s(16)}\r\n#{first_chunk}\r\n#{(second_chunk.bytesize).to_s(16)}\r\n#{second_chunk}\r\n0\r\n\r\n")
  end

  it "prints with content length" do
    io = IO::Memory.new
    response = Response.new(io)
    response.headers["Content-Length"] = "10"
    response.print("1234")
    response.print("567890")
    response.close
    io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n1234567890")
  end

  it "prints with content length (method)" do
    io = IO::Memory.new
    response = Response.new(io)
    response.content_length = 10
    response.print("1234")
    response.print("567890")
    response.close
    io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n1234567890")
  end

  it "doesn't override content-length when there's no body" do
    io = IO::Memory.new
    response = Response.new(io)
    response.content_length = 10
    response.close
    io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n")
  end

  it "adds header" do
    io = IO::Memory.new
    response = Response.new(io)
    response.headers["Content-Type"] = "text/plain"
    response.print("Hello")
    response.close
    io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nHello")
  end

  it "sets content type" do
    io = IO::Memory.new
    response = Response.new(io)
    response.content_type = "text/plain"
    response.print("Hello")
    response.close
    io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nHello")
  end

  it "sets content type after headers sent" do
    io = IO::Memory.new
    response = Response.new(io)
    response.print("Hello")
    response.flush
    expect_raises(IO::Error, "Headers already sent") do
      response.content_type = "text/plain"
    end
  end

  it "sets status code" do
    io = IO::Memory.new
    response = Response.new(io)
    return_value = response.status_code = 201
    return_value.should eq 201
    response.status.should eq HTTP::Status::CREATED
    response.status_message.should eq "Created"
    response.print("Hello")
    response.flush
    expect_raises(IO::Error, "Headers already sent") do
      response.status_code = 201
    end
  end

  it "retrieves status code" do
    io = IO::Memory.new
    response = Response.new(io)
    response.status = :created
    response.status_code.should eq 201
  end

  it "changes status message" do
    io = IO::Memory.new
    response = Response.new(io)
    response.status = :not_found
    response.status_message = "Custom status"
    response.close
    io.to_s.should eq("HTTP/1.1 404 Custom status\r\nContent-Length: 0\r\n\r\n")
    response.status_message.should eq "Custom status"

    expect_raises(IO::Error, "Closed stream") do
      response.status_message = "Other status"
    end
  end

  it "changes status and others" do
    io = IO::Memory.new
    response = Response.new(io)
    response.status = :not_found
    response.version = "HTTP/1.0"
    response.close
    io.to_s.should eq("HTTP/1.0 404 Not Found\r\nContent-Length: 0\r\n\r\n")
  end

  it "changes status and others after headers sent" do
    io = IO::Memory.new
    response = Response.new(io)
    response.print("Foo")
    response.flush
    expect_raises(IO::Error, "Headers already sent") do
      response.status = :not_found
    end
    expect_raises(IO::Error, "Headers already sent") do
      response.version = "HTTP/1.0"
    end
  end

  it "closes gracefully with replaced output that syncs close (#11389)" do
    output = IO::Memory.new
    response = HTTP::Server::Response.new(output)

    response.output = IO::Stapled.new(response.output, response.output, sync_close: true)
    response.print "some body"

    response.close

    output.rewind.gets_to_end.should eq "HTTP/1.1 200 OK\r\nContent-Length: 9\r\n\r\nsome body"
  end

  it "flushes" do
    io = IO::Memory.new
    response = Response.new(io)
    response.print("Hello")
    response.flush
    io.to_s.should eq("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n")
    response.close
    io.to_s.should eq("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\n\r\n")
  end

  it "wraps output" do
    io = IO::Memory.new
    response = Response.new(io)
    response.output = ReverseResponseOutput.new(response.output)
    response.print("1234")
    response.close
    io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\n4321")
  end

  it "writes and flushes with HTTP 1.0" do
    io = IO::Memory.new
    response = Response.new(io, "HTTP/1.0")
    response.print("1234")
    response.flush
    io.to_s.should eq("HTTP/1.0 200 OK\r\n\r\n1234")
  end

  it "resets and clears headers and cookies" do
    io = IO::Memory.new
    response = Response.new(io)
    response.headers["Foo"] = "Bar"
    response.cookies["Bar"] = "Foo"
    response.status = HTTP::Status::USE_PROXY
    response.status_message = "Baz"
    response.reset
    response.headers.should be_empty
    response.cookies.should be_empty
    response.status.should eq HTTP::Status::OK
    response.status_message.should eq "OK"
  end

  it "writes cookie headers" do
    io = IO::Memory.new
    response = Response.new(io)
    response.cookies["Bar"] = "Foo"
    response.close
    io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nSet-Cookie: Bar=Foo\r\n\r\n")

    io = IO::Memory.new
    response = Response.new(io)
    response.cookies["Bar"] = "Foo"
    response.print("Hello")
    response.close
    io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 5\r\nSet-Cookie: Bar=Foo\r\n\r\nHello")
  end

  it "closes when it fails to write" do
    io = IO::Memory.new
    response = Response.new(io)
    response.print("Hello")
    response.flush
    io.close
    response.print("Hello")
    expect_raises(HTTP::Server::ClientError) { response.flush }
    response.closed?.should be_true
  end

  describe "#respond_with_status" do
    it "uses default values" do
      io = IO::Memory.new
      response = Response.new(io)
      response.content_type = "text/html"
      response.respond_with_status(500)
      io.to_s.should eq("HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\nContent-Length: 26\r\n\r\n500 Internal Server Error\n")
      response.status_message.should eq "Internal Server Error"
    end

    it "sends custom code and message" do
      io = IO::Memory.new
      response = Response.new(io)
      response.respond_with_status(400, "Request Error")
      io.to_s.should eq("HTTP/1.1 400 Request Error\r\nContent-Type: text/plain\r\nContent-Length: 18\r\n\r\n400 Request Error\n")
      response.status_message.should eq "Request Error"
    end

    it "sends HTTP::Status" do
      io = IO::Memory.new
      response = Response.new(io)
      response.respond_with_status(HTTP::Status::URI_TOO_LONG)
      io.to_s.should eq("HTTP/1.1 414 URI Too Long\r\nContent-Type: text/plain\r\nContent-Length: 17\r\n\r\n414 URI Too Long\n")
      response.status_message.should eq "URI Too Long"
    end

    it "sends HTTP::Status and custom message" do
      io = IO::Memory.new
      response = Response.new(io)
      response.respond_with_status(HTTP::Status::URI_TOO_LONG, "Request Error")
      io.to_s.should eq("HTTP/1.1 414 Request Error\r\nContent-Type: text/plain\r\nContent-Length: 18\r\n\r\n414 Request Error\n")
      response.status_message.should eq "Request Error"
    end

    it "raises when response is closed" do
      io = IO::Memory.new
      response = Response.new(io)
      response.close
      expect_raises(IO::Error, "Closed stream") do
        response.respond_with_status(400)
      end
    end

    it "raises when headers written" do
      io = IO::Memory.new
      response = Response.new(io)
      response.print("Hello")
      response.flush
      expect_raises(IO::Error, "Headers already sent") do
        response.respond_with_status(400)
      end
    end
  end

  describe "#redirect" do
    ["/path", URI.parse("/path")].each do |location|
      it "#{location.class} location" do
        io = IO::Memory.new
        response = Response.new(io)
        response.redirect(location)
        io.to_s.should eq("HTTP/1.1 302 Found\r\nLocation: /path\r\nContent-Length: 0\r\n\r\n")
      end
    end

    it "encodes special characters" do
      io = IO::Memory.new
      response = Response.new(io)
      response.redirect("https://example.com/path\nfoo bar")
      io.to_s.should eq("HTTP/1.1 302 Found\r\nLocation: https://example.com/path%0Afoo%20bar\r\nContent-Length: 0\r\n\r\n")
    end

    it "doesn't encode URIs twice" do
      io = IO::Memory.new
      response = Response.new(io)
      u = URI.new "https", host: "example.com", path: "auth",
        query: URI::Params.new({"redirect_uri" => ["http://example.com/callback"]})
      response.redirect(u)
      io.to_s.should eq("HTTP/1.1 302 Found\r\nLocation: https://example.com/auth?redirect_uri=http%3A%2F%2Fexample.com%2Fcallback\r\nContent-Length: 0\r\n\r\n")
    end

    it "permanent redirect" do
      io = IO::Memory.new
      response = Response.new(io)
      response.redirect("/path", status: :moved_permanently)
      io.to_s.should eq("HTTP/1.1 301 Moved Permanently\r\nLocation: /path\r\nContent-Length: 0\r\n\r\n")
    end

    it "with header" do
      io = IO::Memory.new
      response = Response.new(io)
      response.headers["Foo"] = "Bar"
      response.redirect("/path", status: :moved_permanently)
      io.to_s.should eq("HTTP/1.1 301 Moved Permanently\r\nFoo: Bar\r\nLocation: /path\r\nContent-Length: 0\r\n\r\n")
    end

    it "fails if headers already sent" do
      io = IO::Memory.new
      response = Response.new(io)
      response.puts "foo"
      response.flush
      expect_raises(IO::Error, "Headers already sent") do
        response.redirect("/path")
      end
    end

    it "fails if closed" do
      io = IO::Memory.new
      response = Response.new(io)
      response.close
      expect_raises(IO::Error, "Closed stream") do
        response.redirect("/path")
      end
    end
  end
end