169 lines
6.1 KiB
Text
169 lines
6.1 KiB
Text
import Std.Http.Test.Helpers
|
||
|
||
open Std.Async
|
||
open Std Http Internal Test
|
||
|
||
-- Handlers for Expect: 100-continue testing
|
||
|
||
private structure RejectContinueHandler where
|
||
onRequestCalls : IO.Ref Nat
|
||
|
||
instance : Std.Http.Server.Handler RejectContinueHandler where
|
||
onRequest self _ := do
|
||
self.onRequestCalls.modify (· + 1)
|
||
Response.ok |>.text "request-ran"
|
||
|
||
onContinue _ _ := pure false
|
||
|
||
private structure AcceptContinueHandler where
|
||
onRequestCalls : IO.Ref Nat
|
||
|
||
instance : Std.Http.Server.Handler AcceptContinueHandler where
|
||
onRequest self request := do
|
||
self.onRequestCalls.modify (· + 1)
|
||
let body : String ← request.body.readAll
|
||
Response.ok |>.text s!"accepted:{body}"
|
||
|
||
onContinue _ _ := pure true
|
||
|
||
-- Per-test runner for generic handlers
|
||
|
||
private def checkH {σ : Type} [Std.Http.Server.Handler σ]
|
||
(name : String)
|
||
(raw : String)
|
||
(handler : σ)
|
||
(expect : ByteArray → IO Unit)
|
||
(config : Config := defaultConfig) : IO Unit := do
|
||
let (client, server) ← Mock.new
|
||
let response ← Async.block do
|
||
client.send raw.toUTF8
|
||
Std.Http.Server.serveConnection server handler config |>.run
|
||
return (← client.recv?).getD .empty
|
||
|
||
try expect response
|
||
catch e => throw (IO.userError s!"[{name}] {e}")
|
||
|
||
private def assertCallCount (ref : IO.Ref Nat) (expected : Nat) : IO Unit := do
|
||
let got ← ref.get
|
||
unless got == expected do
|
||
throw <| IO.userError s!"expected {expected} onRequest calls, got {got}"
|
||
|
||
-- RFC 9110 §10.1.1: Expect: 100-continue
|
||
|
||
#eval runGroup "Expect: 100-continue — reject" do
|
||
let calls ← IO.mkRef 0
|
||
let handler : RejectContinueHandler := { onRequestCalls := calls }
|
||
|
||
checkH "rejected Expect → 417, handler not called"
|
||
(raw := "POST /upload HTTP/1.1\x0d\nHost: example.com\x0d\nExpect: 100-continue\x0d\nContent-Length: 5\x0d\nConnection: close\x0d\n\x0d\nhello")
|
||
(handler := handler)
|
||
(expect := fun r =>
|
||
assertContains r "HTTP/1.1 417 Expectation Failed" *>
|
||
assertAbsent r "100 Continue" *>
|
||
assertAbsent r "request-ran" *>
|
||
assertResponseCount r 1)
|
||
|
||
assertCallCount calls 0
|
||
|
||
#eval runGroup "Expect: 100-continue — reject blocks pipelining" do
|
||
let calls ← IO.mkRef 0
|
||
let handler : RejectContinueHandler := { onRequestCalls := calls }
|
||
|
||
checkH "rejected Expect closes exchange, blocks pipelined second request"
|
||
(raw :=
|
||
"POST /first HTTP/1.1\x0d\nHost: example.com\x0d\nExpect: 100-continue\x0d\nContent-Length: 5\x0d\n\x0d\nhello" ++
|
||
"GET /second HTTP/1.1\x0d\nHost: example.com\x0d\nConnection: close\x0d\n\x0d\n")
|
||
(handler := handler)
|
||
(expect := fun r =>
|
||
assertContains r "HTTP/1.1 417 Expectation Failed" *>
|
||
assertAbsent r "/second")
|
||
|
||
assertCallCount calls 0
|
||
|
||
#eval runGroup "Expect: 100-continue — accept" do
|
||
let calls ← IO.mkRef 0
|
||
let handler : AcceptContinueHandler := { onRequestCalls := calls }
|
||
|
||
checkH "accepted Expect → 100 Continue then 200"
|
||
(raw := "POST /ok HTTP/1.1\x0d\nHost: example.com\x0d\nExpect: 100-continue\x0d\nContent-Length: 5\x0d\nConnection: close\x0d\n\x0d\nhello")
|
||
(handler := handler)
|
||
(expect := fun r =>
|
||
assertContains r "HTTP/1.1 100 Continue" *>
|
||
assertContains r "HTTP/1.1 200 OK" *>
|
||
assertContains r "accepted:hello" *>
|
||
assertResponseCount r 2) -- one interim + one final
|
||
|
||
assertCallCount calls 1
|
||
|
||
#eval runGroup "Expect: misc" do
|
||
let rejectCalls ← IO.mkRef 0
|
||
let rejectHandler : RejectContinueHandler := { onRequestCalls := rejectCalls }
|
||
|
||
checkH "non-100 Expect token → normal request, no interim"
|
||
(raw := "POST /odd HTTP/1.1\x0d\nHost: example.com\x0d\nExpect: something-else\x0d\nContent-Length: 5\x0d\nConnection: close\x0d\n\x0d\nhello")
|
||
(handler := rejectHandler)
|
||
(expect := fun r =>
|
||
assertContains r "HTTP/1.1 200 OK" *>
|
||
assertContains r "request-ran" *>
|
||
assertAbsent r "100 Continue")
|
||
|
||
assertCallCount rejectCalls 1
|
||
|
||
let acceptCalls ← IO.mkRef 0
|
||
let acceptHandler : AcceptContinueHandler := { onRequestCalls := acceptCalls }
|
||
|
||
checkH "Expect: 100-CONTINUE (case-insensitive) → 100 then 200"
|
||
(raw := "POST /case HTTP/1.1\x0d\nHost: example.com\x0d\nExpect: 100-CONTINUE\x0d\nContent-Length: 5\x0d\nConnection: close\x0d\n\x0d\nhello")
|
||
(handler := acceptHandler)
|
||
(expect := fun r =>
|
||
assertContains r "HTTP/1.1 100 Continue" *>
|
||
assertContains r "HTTP/1.1 200 OK")
|
||
|
||
assertCallCount acceptCalls 1
|
||
|
||
let noCalls ← IO.mkRef 0
|
||
let noExpectHandler : AcceptContinueHandler := { onRequestCalls := noCalls }
|
||
|
||
checkH "no Expect header → no 100 Continue emitted"
|
||
(raw := "POST /no-expect HTTP/1.1\x0d\nHost: example.com\x0d\nContent-Length: 5\x0d\nConnection: close\x0d\n\x0d\nhello")
|
||
(handler := noExpectHandler)
|
||
(expect := fun r =>
|
||
assertContains r "HTTP/1.1 200 OK" *>
|
||
assertContains r "accepted:hello" *>
|
||
assertAbsent r "100 Continue" *>
|
||
assertResponseCount r 1)
|
||
|
||
assertCallCount noCalls 1
|
||
|
||
-- Date header generation
|
||
|
||
#eval runGroup "Date header" do
|
||
check "generateDate: true adds Date header"
|
||
(raw := mkGetClose "/date")
|
||
(handler := fun _ => Response.ok |>.text "hello")
|
||
(config := { defaultConfig with generateDate := true })
|
||
(expect := fun r =>
|
||
assertStatus r "HTTP/1.1 200" *>
|
||
assertContains r "Date: ")
|
||
|
||
check "generateDate: false omits Date header"
|
||
(raw := mkGetClose "/no-date")
|
||
(handler := fun _ => Response.ok |>.text "hello")
|
||
(config := { defaultConfig with generateDate := false })
|
||
(expect := fun r =>
|
||
assertStatus r "HTTP/1.1 200" *>
|
||
assertAbsent r "Date: ")
|
||
|
||
check "user-supplied Date header preserved and not duplicated"
|
||
(raw := mkGetClose "/custom-date")
|
||
(handler := fun _ =>
|
||
Response.ok
|
||
|>.header! "Date" "Mon, 01 Jan 2024 00:00:00 GMT"
|
||
|>.text "hello")
|
||
(config := { defaultConfig with generateDate := true })
|
||
(expect := fun r => do
|
||
assertContains r "Date: Mon, 01 Jan 2024 00:00:00 GMT"
|
||
let text := String.fromUTF8! r
|
||
let count := (text.splitOn "Date: ").length - 1
|
||
unless count == 1 do
|
||
throw <| IO.userError s!"expected 1 Date header, got {count}:\n{text.quote}")
|