From 90a1f7d47525cfffe928e9a89becf622bd85a8a1 Mon Sep 17 00:00:00 2001
From: Peter Bex <peter@more-magic.net>
Date: Fri, 22 Jun 2018 22:22:24 +0200
Subject: Initial CHICKEN 5 port of intarweb 1.7

---
 tests/run.scm | 1243 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 1243 insertions(+)
 create mode 100644 tests/run.scm

(limited to 'tests')

diff --git a/tests/run.scm b/tests/run.scm
new file mode 100644
index 0000000..e2d0b70
--- /dev/null
+++ b/tests/run.scm
@@ -0,0 +1,1243 @@
+(import scheme chicken.base chicken.port
+        chicken.condition chicken.time.posix srfi-1 srfi-18 
+        test uri-common intarweb chicken.io chicken.format)
+
+;; Below, there are specific tests for when these do have a value
+(http-header-limit #f)
+(http-line-limit #f)
+(http-urlencoded-request-data-limit #f)
+
+(define-syntax test-error*
+  (syntax-rules ()
+    ((_ ?msg (?error-type ...) ?expr)
+     (let-syntax ((expression:
+                   (syntax-rules ()
+                     ((_ ?expr)
+                      (condition-case (begin ?expr "<no error thrown>")
+                                      ((?error-type ...) '(?error-type ...))
+                                      (exn () (##sys#slot exn 1)))))))
+       (test ?msg '(?error-type ...) (expression: ?expr))))
+    ((_ ?msg ?error-type ?expr)
+     (test-error* ?msg (?error-type) ?expr))
+    ((_ ?error-type ?expr)
+     (test-error* (sprintf "~S" '?expr) ?error-type ?expr))))
+
+(header-parse-error-handler (lambda (header-name contents header exn)
+                              (raise exn)))
+
+(define (test-read-headers str)
+  (call-with-input-string str read-headers))
+
+(test-begin "intarweb")
+(test-group "headers"
+  (test-group "single headers"
+   (parameterize ((single-headers '(foo qux))
+                  (header-parsers `((foo . ,(single identity))
+                                    (qux . ,(single identity)))))
+     (let ((headers (test-read-headers "foo: bar\r\nqux:\t   \tmooh\t   \r\n\r\n")))
+       (test "Basic test"
+             '("bar") (header-values 'foo headers))
+       ;; RFC 2616 4.2
+       (test "Extra spaces are ignored"
+             '("mooh") (header-values 'qux headers)))
+     (let ((headers (test-read-headers "foo: bar\r\n qux: mooh\r\nquux: mumble\r\n\r\n")))
+       ;; RFC 2616 2.2
+       (test "Continuation chars"
+             '("bar qux: mooh") (header-values 'foo headers)))
+     ;; Not in RFC but common behaviour - also, robustness principle
+     (let ((headers (test-read-headers "foo: bar\r\nfoo: qux\r\n")))
+       (test "Multiple headers for singular header types discarded"
+             '("qux") (header-values 'foo headers)))))
+  ;; All this RFC 2616 4.2
+  (test-group "multi-headers"
+   (parameterize ((header-parsers `((foo . ,(multiple identity)))))
+     (let ((headers (test-read-headers "foo: bar\r\nfoo: qux\r\nquux: mumble\r\n\r\n")))
+       (test "Multiple headers"
+             '("bar" "qux") (header-values 'foo headers)))
+     (let ((headers (test-read-headers "Foo: bar\r\nFoO: qux\r\nquux: mumble\r\n\r\n")))
+       (test "Multiple headers: case insensitivity"
+             '("bar" "qux") (header-values 'foo headers)))
+     (let ((headers (test-read-headers "foo: bar, qux\r\nquux: mumble\r\n\r\n")))
+       (test "Comma-separated headers"
+             '("bar" "qux") (header-values 'foo headers)))
+     (let ((headers (test-read-headers "foo: \"ba\\\"r, qux\"\r\nfoo: mooh\r\n\r\n")))
+       (test "Quoted headers"
+             '("ba\"r, qux" "mooh") (header-values 'foo headers))))
+   ;; RFC 2616 4.5
+   ;; "Unrecognized header fields are treated as entity-header fields."
+   ;;
+   ;; RFC 2616 7.1
+   ;; "Unrecognized header fields SHOULD be ignored by the recipient and MUST be
+   ;;  forwarded by transparent proxies."
+   (let ((headers (test-read-headers "unknown: foo, bar\r\nunknown: blah\r\n\r\n")))
+     (test "Unknown headers are not parsed and put into lists"
+           '("foo, bar" "blah") (header-values 'unknown headers))
+     (test "Unknown headers get raw instead of a parameter list"
+           'raw (header-params 'unknown headers))))
+  (test-group "miscellaneous header stuff"
+    (parameterize ((header-parsers `((foo . ,(multiple identity))
+                                     (bar . ,(lambda x (error "bad header")))))
+                   (http-header-limit 2))
+      (test-error "Missing header contents" (test-read-headers "foo\r\n\r\n"))
+      (test-error "Bad header w/ handler" (test-read-headers "bar: x\r\n\r\n"))
+      (parameterize ((header-parse-error-handler (lambda (n c h exn) h)))
+       (test "Bad header w/o handler" #t (headers? (test-read-headers "bar: x\r\n\r\n"))))
+      ;; RFC 2616 2.2
+      ;; "The backslash character ("\") MAY be used as a single-character
+      ;; quoting mechanism only within quoted-string and comment constructs."
+      ;;     quoted-pair = "\" CHAR
+      ;; CHAR implies any char, *including* CR/LF. This is clarified by RFC 822,
+      ;; on which RFC 2616 is based.
+      ;; Apparently, even \CRLF is allowed (as opposed to \CR\LF)
+      (test "Embedded newlines"
+            '("bar\r\nqux")
+            ;; It's unclear whether we should interpret the "\r\n" as EOL
+            ;; in "\\\r\n", or whether it should be seen as an embedded \r
+            ;; followed by a \n (which is then interpreted as a literal \n?)
+            (header-values 'foo (test-read-headers "Foo: \"bar\\\r\\\nqux\"")))
+      (test-error "Too many headers is an error"
+                  (test-read-headers "foo: bar\r\nfoo: qux\r\nfoo: hoohoo\r\n")))))
+
+(test-group "specialized header parsers"
+  (test-group "host/port"
+    (test "Hostname and port"
+          '(("foo.example.com" . 8080))
+          (header-values 'host (test-read-headers "Host: foo.example.com:8080")))
+    (test "Hostname, no port"
+          '(("foo.example.com" . #f))
+          (header-values 'host (test-read-headers "Host: foo.example.com"))))
+  (test-group "quality parameter"
+   (let* ((headers (test-read-headers "Accept: text/plain; Q=0.5, text/html, text/plain; q=0.123456, application/pdf; q=1.2345, text/xml; q=-0.234, text/whatever; q="))
+          (accept  (header-contents 'accept headers)))
+     ;; RFC 2616 3.6: "All transfer-coding values are case insensitive".
+     ;; This includes the parameter name (attribute) and value.
+     (test "quality value (case-insensitive)"
+           0.5 (get-param 'q (first accept) 1.0))
+     (test "quality encoding value"
+           'text/plain (get-value (first accept)))
+     (test "quality values have only three digits"
+           0.123 (get-param 'q (third accept) 1.0))
+     (test "quality values maximum is 1.0"
+           1.0 (get-param 'q (fourth accept) 1.0))
+     (test "quality values minimum is 0.0"
+           0.0 (get-param 'q (fifth accept) 1.0))
+     (test "missing quality value ok"
+           1.0 (get-param 'q (sixth accept) 1.0))))
+  (test-group "charset parameter"
+   (let* ((headers (test-read-headers "Content-Type: text/PLAIN; charset=ISO-8859-1"))
+          (content-type (header-contents 'content-type headers)))
+     (test "content-type value is lowercase symbol"
+           'text/plain (get-value (car content-type)))
+     ;; RFC 2616 3.4: "HTTP character sets are identified by
+     ;; case-insensitive tokens. The complete set of tokens is defined
+     ;; by the IANA Character Set registry."
+     (test "content-type charset is lowercase symbol"
+           'iso-8859-1 (get-param 'charset (car content-type)))))
+
+  (test-group "symbol-parser-ci"
+    (let* ((headers (test-read-headers "Accept-Ranges: FoO")))
+      (test "Case-insensitive"
+            '(foo) (header-values 'accept-ranges headers))))
+  
+  (test-group "symbol-parser"
+    (let* ((headers (test-read-headers "Allow: FoO, foo")))
+      (test "Case-sensitive"
+            '(FoO foo) (header-values 'allow headers))))
+
+  (test-group "natnum-subparser"
+    (parameterize ((single-headers '(foo bar qux mooh))
+                   (header-parsers `((foo . ,(single natnum-subparser))
+                                     (bar . ,(single natnum-subparser))
+                                     (qux . ,(single natnum-subparser))
+                                     (mooh . ,(single natnum-subparser)))))
+     (let ((headers (test-read-headers "Foo: 10\r\nBar: abc\r\nQux: -10\r\nMooh: 1.6")))
+       (test "Simple test"
+             10 (header-value 'foo headers))
+       (test "No number defaults to 0"
+             0 (header-value 'bar headers))
+       (test "No negative numbers"
+             0 (header-value 'qux headers))
+       ;; This is a "feature" in the interest of the robustness principle
+       (test "Rounding of real numbers"
+             2 (header-value 'mooh headers)))))
+
+  (test-group "cache-control-parser"
+    (let ((headers (test-read-headers "Cache-control: max-age=10, private")))
+      (test "max-age is a number"
+            '(max-age . 10) (assq 'max-age (header-values 'cache-control headers)))
+      (test "private without value"
+            '(private . #t) (assq 'private (header-values 'cache-control headers))))
+    (let ((headers (test-read-headers "Cache-control: private=\"accept-encoding, accept-ranges\"\r\nCache-control: must-revalidate")))
+      (test "private with values"
+            '(private . (accept-encoding accept-ranges))
+            (assq 'private (header-values 'cache-control headers)))
+      (test "Acts like a multi-header"
+            '(must-revalidate . #t) (assq 'must-revalidate (header-values 'cache-control headers)))))
+
+  (test-group "authorization-parser"
+    (test-group "basic auth"
+     (let ((headers (test-read-headers "Authorization: Basic QWxpIEJhYmE6b3BlbiBzZXNhbWU=\r\n")))
+       (test "basic"
+             'basic
+             (header-value 'authorization headers))
+       (test "username"
+             "Ali Baba"
+             (header-param 'username 'authorization headers))
+       (test "password"
+             "open sesame"
+             (header-param 'password 'authorization headers))))
+    (test-group "digest auth"
+      (let ((headers (test-read-headers "Authorization: Digest username=\"Mufasa\", realm=\"testrealm@host.com\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", uri=\"/dir/index.html\", qop=auth, nc=00000001, cnonce=\"0a4f113b\", response=\"6629fae49393a05397450978507c4ef1\", opaque=\"5ccc069c403ebaf9f0171e9517f40e41\", algorithm=MD5")))
+        (test "digest"
+              'digest
+              (header-value 'authorization headers))
+        (test "realm"
+              "testrealm@host.com"
+              (header-param 'realm 'authorization headers))
+        (test "nonce"
+              "dcd98b7102dd2f0e8b11d0f600bfb0c093"
+              (header-param 'nonce 'authorization headers))
+        (test "username"
+              "Mufasa"
+              (header-param 'username 'authorization headers))
+        (test "qop"
+              'auth
+              (header-param 'qop 'authorization headers))
+        (test "digest uri"
+              "/dir/index.html"
+              (uri->string (header-param 'uri 'authorization headers)))
+        (test "nonce count"
+              1
+              (header-param 'nc 'authorization headers))
+        (test "cnonce"
+              "0a4f113b"
+              (header-param 'cnonce 'authorization headers))
+        (test "response"
+              "6629fae49393a05397450978507c4ef1"
+              (header-param 'response 'authorization headers))
+        (test "opaque"
+              "5ccc069c403ebaf9f0171e9517f40e41"
+              (header-param 'opaque 'authorization headers))
+        (test "algorithm"
+              'md5
+              (header-param 'algorithm 'authorization headers))))
+    (test-group "custom authorization scheme"
+      (parameterize ((authorization-param-subparsers
+                      `((custom . ,(lambda (contents pos)
+                                     (receive (c p)
+                                       (parse-token contents pos)
+                                       (values `((contents . ,(http-name->symbol c))) p))))
+                        . ,(authorization-param-subparsers))))
+        (let ((headers (test-read-headers "Authorization: Custom Security-through-obscurity")))
+          (test "Custom"
+                'custom
+                (header-value 'authorization headers))
+          (test "Custom contents"
+                'security-through-obscurity
+                (header-param 'contents 'authorization headers))))))
+  
+  (test-group "authenticate parser"
+    (test-group "basic auth"
+      (let ((headers (test-read-headers "WWW-Authenticate: Basic realm=\"WallyWorld\"")))
+        (test "basic"
+              'basic
+              (header-value 'www-authenticate headers))
+        (test "realm"
+              "WallyWorld"
+              (header-param 'realm 'www-authenticate headers))))
+    (test-group "digest auth"
+      (let ((headers (test-read-headers "WWW-Authenticate: Digest realm=\"testrealm@host.com\", qop=\"auth, auth-int\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"")))
+        (test "digest"
+              'digest
+              (header-value 'www-authenticate headers))
+        (test "realm"
+              "testrealm@host.com"
+              (header-param 'realm 'www-authenticate headers))
+        (test "qop"
+              '(auth auth-int)
+              (header-param 'qop 'www-authenticate headers))
+        (test "nonce"
+              "dcd98b7102dd2f0e8b11d0f600bfb0c093"
+              (header-param 'nonce 'www-authenticate headers))
+        (test "opaque"
+              "5ccc069c403ebaf9f0171e9517f40e41"
+              (header-param 'opaque 'www-authenticate headers))
+        (test "missing stale value"
+              #f
+              (header-param 'stale 'www-authenticate headers)))
+      (let ((headers (test-read-headers "WWW-Authenticate: Digest domain=\"/example http://foo.com/bar\", stale=TRUE")))
+        (test "domains"
+              '("/example" "http://foo.com/bar")
+              (map uri->string
+                   (header-param 'domain 'www-authenticate headers)))
+        (test "stale"
+              #t
+              (header-param 'stale 'www-authenticate headers)))
+      (let ((headers (test-read-headers "WWW-Authenticate: Digest stale=whatever")))
+        (test "non-true stale value"
+              #f
+              (header-param 'stale 'www-authenticate headers)))))
+  
+  (test-group "pragma-parser"
+    (let ((headers (test-read-headers "Pragma: custom-value=10, no-cache")))
+      (test "value"
+            '(custom-value . "10")
+            (assq 'custom-value (header-values 'pragma headers)))
+      (test "no value"
+            '(no-cache . #t) (assq 'no-cache (header-values 'pragma headers))))
+    (let ((headers (test-read-headers "Cache-control: private=\"accept-encoding, accept-ranges\"\r\nCache-control: must-revalidate")))
+      (test "private with values"
+            '(private . (accept-encoding accept-ranges))
+            (assq 'private (header-values 'cache-control headers)))
+      (test "Acts like a multi-header"
+            '(must-revalidate . #t) (assq 'must-revalidate (header-values 'cache-control headers)))))
+
+  ;; RFC 2616, 14.15  &  RFC 1864 (Base64)
+  (test-group "base64-parser"
+    (let ((headers (test-read-headers "Content-md5: Q2hlY2sgSW50ZWdyaXR5IQ==")))
+      (test "md5 is base64-decoded"
+            "Check Integrity!"
+            (header-value 'content-md5 headers))))
+
+  (test-group "range-parser"
+    (let ((headers (test-read-headers "content-range: bytes 500-999/1234")))
+      (test "Simple range"
+            '(500 999 1234)
+            (header-value 'content-range headers))))
+
+  (test-group "content-disposition"
+    (let ((headers (test-read-headers "Content-Disposition: attachment; filename=dir/foo.jpg")))
+      (test "Attachment with filename parameter containing directory"
+            `(attachment (filename . "foo.jpg"))
+            (cons (header-value  'content-disposition headers)
+                  (header-params 'content-disposition headers))))
+    (let ((headers (test-read-headers "Content-Disposition: inline; filename=foo.jpg; creation-date=Sun, 06 Nov 1994 08:49:37 GMT")))
+      (test "Inline with filename and (not quoted) creation-date parameter"
+            `(inline
+              (filename . "foo.jpg")
+              (creation-date . ,(utc-time->seconds '#(37 49 08 06 10 94 0 310 #f 0))))
+            (cons (header-value  'content-disposition headers)
+                  (map (lambda (x)
+                         (if (vector? (cdr x))
+                             (cons (car x) (utc-time->seconds (cdr x)))
+                             x))
+                       (header-params 'content-disposition headers)))))
+    (let ((headers (test-read-headers "Content-Disposition: inline; read-date=\"Sun, 06 Nov 1994 08:49:37 GMT\"; size=100")))
+      (test "Inline with size and (quoted) read-date parameter"
+            `(inline
+              (read-date . ,(utc-time->seconds '#(37 49 08 06 10 94 0 310 #f 0)))
+              (size . 100))
+            (cons (header-value  'content-disposition headers)
+                  (map (lambda (x)
+                         (if (vector? (cdr x))
+                             (cons (car x) (utc-time->seconds (cdr x)))
+                             x))
+                       (header-params 'content-disposition headers))))))
+
+  (test-group "normalized-uri"
+    (let ((headers (test-read-headers "Location: http://example.com/foo")))
+      (test "Uri"
+            (uri-reference "http://example.com/foo")
+            (header-value 'location headers)))
+    (let ((headers (test-read-headers "Location: http://example.com/foo/../bar")))
+     (test "Auto-normalization"
+           (uri-reference "http://example.com/bar")
+           (header-value 'location headers))))
+
+  (test-group "etag-parser"
+    (let ((headers (test-read-headers "Etag: \"foo\"")))
+      (test "Strong tag"
+            '(strong . "foo")
+            (header-value 'etag headers)))
+    (let ((headers (test-read-headers "Etag: W/\"bar\"")))
+      (test "Weak tag"
+            '(weak . "bar")
+            (header-value 'etag headers)))
+    (let ((headers (test-read-headers "Etag: \"\"")))
+      (test "Empty tag"
+            '(strong . "")
+            (header-value 'etag headers)))
+    (let ((headers (test-read-headers "Etag: \"W/bar\"")))
+        (test "Strong tag, containing W/ prefix"
+              '(strong . "W/bar")
+              (header-value 'etag headers))))
+
+  (test-group "if-match parser"
+    (let ((headers (test-read-headers "If-match: foo")))
+      (test "Strong etag"
+            '(strong . "foo")
+            (header-value 'if-match headers)))
+    (let ((headers (test-read-headers "If-match: W/foo")))
+      (test "Weak etag"
+            '(weak . "foo")
+            (header-value 'if-match headers)))
+    (let ((headers (test-read-headers "If-match: W/foo bar")))
+      (test "Multiple etags"
+            '((weak . "foo") (strong . "bar"))
+            (header-values 'if-match headers)))
+    (let ((headers (test-read-headers "If-match: *")))
+      (test "Wildcard"
+            '*
+            (header-value 'if-match headers))))
+
+  (test-group "http-date-parser"
+    (let ((headers (test-read-headers "Date: Sun, 06 Nov 1994 08:49:37 GMT")))
+      (test "RFC1123 time"
+            (utc-time->seconds '#(37 49 08 06 10 94 0 310 #f 0))
+            (utc-time->seconds (header-value 'date headers))))
+    (let ((headers (test-read-headers "Date: Sunday, 06-Nov-94 08:49:37 GMT")))
+      (test "RFC850 time"
+            (utc-time->seconds '#(37 49 08 06 10 94 0 310 #f 0))
+            (utc-time->seconds (header-value 'date headers))))
+    (let ((headers (test-read-headers "Date: Sun Nov  6 08:49:37 1994")))
+      (test "asctime time"
+            (utc-time->seconds '#(37 49 08 06 10 94 0 310 #f 0))
+            (utc-time->seconds (header-value 'date headers)))))
+
+  ;; This seems a little excessive.. Maybe find a way to reduce the number
+  ;; of cases and still have a good representative test?
+  (test-group "If-Range parser"
+    (let ((headers (test-read-headers "If-Range: Sun, 06 Nov 1994 08:49:37 GMT")))
+      (test "RFC1123 time"
+            (utc-time->seconds '#(37 49 08 06 10 94 0 310 #f 0))
+            (utc-time->seconds (header-value 'if-range headers))))
+    (let ((headers (test-read-headers "If-Range: Sunday, 06-Nov-94 08:49:37 GMT")))
+      (test "RFC850 time"
+            (utc-time->seconds '#(37 49 08 06 10 94 0 310 #f 0))
+            (utc-time->seconds (header-value 'if-range headers))))
+    (let ((headers (test-read-headers "If-Range: Sun Nov  6 08:49:37 1994")))
+      (test "asctime time"
+            (utc-time->seconds '#(37 49 08 06 10 94 0 310 #f 0))
+            (utc-time->seconds (header-value 'if-range headers))))
+    (let ((headers (test-read-headers "If-Range: \"foo\"")))
+      (test "Strong Etag"
+            '(strong . "foo")
+            (header-value 'if-range headers)))
+    (let ((headers (test-read-headers "If-Range: W/\"bar\"")))
+      (test "Weak Etag"
+            '(weak . "bar")
+            (header-value 'if-range headers)))
+    (let ((headers (test-read-headers "If-Range: \"\"")))
+      (test "Empty Etag"
+            '(strong . "")
+            (header-value 'if-range headers)))
+    (let ((headers (test-read-headers "If-Range: \"W/bar\"")))
+        (test "Strong Etag, containing W/ prefix"
+              '(strong . "W/bar")
+              (header-value 'if-range headers)))    )
+
+  (test-group "via parser"
+    (let ((headers (test-read-headers "Via: 1.1")))
+      (test "simple"
+            '("1.1")
+            (header-values 'via headers)))
+    (let ((headers (test-read-headers "Via: 1.1 foo:80 (comment)")))
+      (test "complex"
+            '("1.1 foo:80 (comment)")
+            (header-values 'via headers)))
+    (let ((headers (test-read-headers "Via: 1.1 foo")))
+      (test "one hop"
+            '("1.1 foo")
+            (header-values 'via headers)))
+    (let ((headers (test-read-headers "Via: 1.1 foo, 1.0 bar")))
+      (test "two hops"
+            '("1.1 foo" "1.0 bar")
+            (header-values 'via headers))))
+
+  (test-group "product parser"
+    (test "Simple product"
+          '("websocket" . #f)
+          (header-value 'upgrade (test-read-headers "Upgrade: websocket\r\n")))
+    (test "Product with version"
+          '("TLS" . "1.0")
+          (header-value 'upgrade (test-read-headers "Upgrade: TLS/1.0\r\n"))))
+
+  (test-group "software parser"
+    (test "Simple product"
+          '(("Mozilla" "5.0" #f))
+          (header-value 'user-agent (test-read-headers "User-Agent: Mozilla/5.0\r\n")))
+    (test "Product with comment"
+          '(("Mozilla" #f "foo"))
+          (header-value 'user-agent (test-read-headers "User-Agent: Mozilla (foo)\r\n")))    
+    (test "Realistic product (comments, semicolons)"
+          '(("Mozilla" "5.0" "X11; U; NetBSD amd64; en-US; rv:1.9.0.3") ("Gecko" "2008110501" #f) ("Minefield" "3.0.3" #f))
+          (header-value 'user-agent (test-read-headers "User-Agent: Mozilla/5.0 (X11; U; NetBSD amd64; en-US; rv:1.9.0.3) Gecko/2008110501 Minefield/3.0.3\r\n")))
+    ;; Reported by Peter Danenberg; Google Drive returns this header
+    (test "Realistic product (quoted comment)"
+          '(("UploadServer" #f "Built on May 4 2015 17:31:43 (1430785903)"))
+          (header-value 'server (test-read-headers "Server: UploadServer (\"Built on May 4 2015 17:31:43 (1430785903)\")\r\n"))))
+
+  (test-group "Set-Cookie parser"
+    (let* ((headers (test-read-headers "Set-Cookie: foo=\"bar\"")))
+      (test "Simple name/value pair"
+            '("foo" . "bar")
+            (get-value (first (header-contents 'set-cookie headers)))))
+    (let* ((headers (test-read-headers "Set-Cookie: foo=qux\r\nSet-Cookie: Foo=\"bar\"")))
+      ;; XXX: Should intarweb remove these, or should the user code handle this?
+      ;; What if interacting with actual broken code on the other side?
+      (test "Multiple cookies with same name (CI) are all kept"
+            '(("foo" . "qux") ("Foo" . "bar"))
+            (map get-value (header-contents 'set-cookie headers))))
+    (let* ((headers (test-read-headers "Set-Cookie: Foo=bar")))
+      (test "Cookie names preserve case"
+            '("Foo" . "bar")
+            (get-value (first (header-contents 'set-cookie headers)))))
+    (let ((headers (test-read-headers "Set-Cookie: foo=bar=qux; max-age=10")))
+      (test "Cookie with = signs"
+            '("foo" . "bar=qux")
+            (get-value (first (header-contents 'set-cookie headers)))))
+    (let* ((headers (test-read-headers "Set-Cookie: foo=bar; Comment=\"Hi, there!\", qux=mooh\r\nSet-Cookie: mumble=mutter\r\n")))
+      (test "Comment"
+            "Hi, there!"
+            (get-param 'comment
+                       (first (header-contents 'set-cookie headers))))
+      (test "Multiple cookies in one header"
+            '("qux" . "mooh")
+            (get-value (second (header-contents 'set-cookie headers))))
+      (test "Multiple cookies in multiple headers"
+            '("mumble" . "mutter")
+            (get-value (third (header-contents 'set-cookie headers))))
+      (test "Missing \"secure\" value"
+            #f
+            (get-param 'secure
+                       (third (header-contents 'set-cookie headers)))))
+    (let* ((headers (test-read-headers "Set-Cookie: foo=; expires=Sunday, 20-Jul-08 15:23:42 GMT; secure; path = / ; Port=80,8080")))
+      (test "Missing value"
+            '("foo" . "")
+            (get-value (first (header-contents 'set-cookie headers))))
+      (test "Old-style cookie expires value"
+            (utc-time->seconds '#(42 23 15 20 6 108 0 309 #f 0))
+            (utc-time->seconds
+             (get-param 'expires
+                        (first (header-contents 'set-cookie headers)))))
+      (test "Secure value"
+            #t
+            (get-param 'secure
+                       (first (header-contents 'set-cookie headers))))
+      (test "Path"
+            (uri-reference "/")
+            (get-param 'path
+                       (first (header-contents 'set-cookie headers))))
+      (test "Port numbers"
+            '(80 8080)
+            (get-param 'port
+                       (first (header-contents 'set-cookie headers)))))
+    (let* ((headers (test-read-headers "Set-Cookie: foo=; expires=Sun, 20 Jul 2008 15:23:42 GMT; secure; path = / ")))
+      (test "Noncompliant syntax cookie expiry value (rfc1123)"
+            (utc-time->seconds '#(42 23 15 20 6 108 0 309 #f 0))
+            (utc-time->seconds
+             (get-param 'expires
+                        (first (header-contents 'set-cookie headers))))))
+    (let* ((headers (test-read-headers "Set-Cookie: foo=; expires=Sun, 20-Jul-2008 15:23:42 GMT; secure; path = / ")))
+      (test "Noncompliant syntax cookie expiry value (rfc850-like, abbrev day)"
+            (utc-time->seconds '#(42 23 15 20 6 108 0 309 #f 0))
+            (utc-time->seconds
+             (get-param 'expires
+                        (first (header-contents 'set-cookie headers)))))))
+  
+  (test-group "cookie-parser"
+    (let* ((headers (test-read-headers "Cookie: Foo=bar; $Path=/; qux=mooh; $unknown=something")))
+      (test "Multiple cookies in the same header"
+            '(("Foo" . "bar") . ("qux" . "mooh"))
+            (cons
+             (get-value (first  (header-contents 'cookie headers)))
+             (get-value (second (header-contents 'cookie headers)))))
+      (test "Parameters of cookies (spaces stripped)"
+            (uri-reference "/")
+            (get-param 'path (first (header-contents 'cookie headers))))
+      (test "Parameters of cookies"
+            "something"
+            (get-param 'unknown (second (header-contents 'cookie headers)))))
+    (let* ((headers (test-read-headers "Cookie: $Version=\"1\"; Foo=bar; $Path=/; qux=mooh; $unknown=something")))
+      (test "Version string is used for all cookies"
+            (cons 1 1)
+            (cons
+             (get-param 'version (first (header-contents 'cookie headers)))
+             (get-param 'version (second (header-contents 'cookie headers)))))))
+
+  (test-group "strict-transport-security-parser"
+    (let ((headers (test-read-headers "Strict-Transport-Security: max-age=10; includeSubDomains")))
+      (test "max-age is a number"
+            '(max-age . 10)
+            (assq 'max-age (header-value 'strict-transport-security headers)))
+      (test "includeSubDomains without value"
+            '(includesubdomains . #t)
+            (assq 'includesubdomains (header-value 'strict-transport-security headers)))))
+
+  (test-group "headers"
+    (test "Simple test"
+          `(bar qux)
+          (header-values 'foo (headers `((foo bar qux)))))
+    (test "Multi headers are folded"
+          `(bar qux)
+          (header-values 'foo (headers `((foo bar)
+                                         (foo qux)))))
+    (test "Single headers are unique"
+          `(qux)
+          (header-values 'foo (parameterize ((single-headers '(foo)))
+                                (headers `((foo bar)
+                                           (foo qux))))))
+    (test "Extra single headers are ignored"
+          `(qux)
+          (header-values 'foo (parameterize ((single-headers '(foo)))
+                                (headers `((foo bar qux))))))
+    (test "Parameters"
+          `((bar . qux))
+          (get-params
+           (car (header-contents 'foo (headers `((foo #(mooh ((bar . qux))))))))))
+    (test "Multi headers are folded into old headers"
+          `(bar qux)
+          (header-values 'foo (headers `((foo qux))
+                                       (headers `((foo bar))))))))
+
+(define (test-unparse-headers h)
+  (call-with-output-string
+   (lambda (o)
+     (unparse-headers (headers h) o))))
+
+(test-group "unparsers"
+  (test-group "default unparser"
+    (test "String"
+          "Foo: bar\r\n"
+          (test-unparse-headers `((foo "bar"))))
+    (test "Multiple strings"
+          "Foo: bar, qux\r\n"
+          (test-unparse-headers `((foo "bar" "qux"))))
+    (test "Auto-quoting on commas and whitespace"
+          "Foo: \"bar, qux\", \"mooh blah\"\r\n"
+          (test-unparse-headers `((foo "bar, qux" "mooh blah"))))
+    ;; RFC 2616 2.2
+    (test "Escaping quotes"
+          "Foo: \"bar \\\" qux\", mooh\r\n"
+          (test-unparse-headers `((foo "bar \" qux" "mooh"))))
+    (test "Escaping control characters"
+          "Foo: \"bar\\\r\\\x01qux\"\r\n"
+          (test-unparse-headers `((foo "bar\r\x01qux"))))
+    ;; Unfortunately, there are no or very few HTTP implementations
+    ;; which understand that newlines can be escaped with a backslash
+    ;; in a quoted string. That's why we don't allow it.
+    ;; The user is expected to escape the newlines according to the type
+    ;; of header (URLencoding, removing the newlines from cookies, etc)
+    (test-error* "Embedded newlines throw an error"
+                 (exn http unencoded-header)
+                 (test-unparse-headers `((foo "bar\n\x01qux"))))
+    (test "Alist"
+          "Foo: Bar=qux, Mooh=mumble\r\n"
+          (test-unparse-headers `((foo (bar . qux) (mooh . mumble)))))
+    (test "Alist with escapes"
+          "Foo: Bar=qux, Mooh=\"mum, ble\"\r\n"
+          (test-unparse-headers `((foo (bar . "qux") (mooh . "mum, ble")))))
+    (test "URI"
+          "Foo: http://foo.com/bar;xyz?a=b\r\n"
+          (test-unparse-headers `((foo ,(uri-reference "http://foo.com/bar;xyz?a=b")))))
+    (test "Parameters"
+          "Foo: bar; qux=mooh; mumble=mutter; blah\r\n"
+          (test-unparse-headers `((foo #(bar ((qux . mooh)
+                                              (mumble . mutter)
+                                              (blah . #t)
+                                              (feh . #f)))))))
+    (test "Raw headers are unparsed as-is"
+          "Foo: bla bla; whatever \"ohai\"\r\n"
+          (test-unparse-headers `((foo #("bla bla; whatever \"ohai\"" raw)))))
+    (test "Raw headers are unparsed as-is for known headers, too"
+          "Etag: \"hi there\r\n" ;; unclosed quote is intentional here
+          (test-unparse-headers `((etag #("\"hi there" raw)))))
+    (test-error* "Embedded newlines in raw headers also throw an error"
+                 (exn http unencoded-header)
+                 (test-unparse-headers `((foo #("bar\n\x01qux" raw))))))
+  (test-group "etag unparser"
+    (test "Weak tag"
+          "Etag: W/\"blah\"\r\n"
+          (test-unparse-headers `((etag (weak . "blah")))))
+    (test "Strong tag"
+          "Etag: \"blah\"\r\n"
+          (test-unparse-headers `((etag (strong . "blah")))))
+    (test "Strong tag starting with W/"
+          "Etag: \"W/blah\"\r\n"
+          (test-unparse-headers `((etag (strong . "W/blah"))))))
+  (test-group "if-match unparser"
+    (test "List of etags"
+          "If-Match: \"foo\", \"bar\", W/\"qux\"\r\n"
+          (test-unparse-headers
+           `((if-match (strong . "foo") (strong . "bar") (weak . "qux")))))
+    (test "Wildcard"
+          "If-Match: *\r\n"
+          (test-unparse-headers
+           `((if-match (strong . "foo") * (weak . "qux"))))))
+  ;; http-dates are all deserialized as rfc1123
+  (test-group "date/time unparser"
+    (test "RFC1123 time"
+          "If-Modified-Since: Sun, 06 Nov 1994 08:49:37 GMT\r\n"
+          ;; Having to specify a vector here twice is sucky and counter-intuitive
+          (test-unparse-headers
+           `((if-modified-since #(#(37 49 08 06 10 94 0 310 #f 0) ()))))))
+  (test-group "host/port unparser"
+    (test "No port specified"
+          "Host: foo.example.com\r\n"
+          (test-unparse-headers `((host ("foo.example.com" . #f)))))
+    (test "Different port"
+          "Host: foo.example.com:8080\r\n"
+          (test-unparse-headers `((host ("foo.example.com" . 8080))))))
+  (test-group "product unparser"
+    (test "Products without version"
+          "Upgrade: websocket, foo\r\n"
+          (test-unparse-headers `((upgrade ("websocket" . #f) ("foo" . #f)))))
+    (test "Products with version"
+          "Upgrade: TLS/1.0, bar/2\r\n"
+          (test-unparse-headers `((upgrade ("TLS" . "1.0") ("bar" . "2"))))))
+  (test-group "software unparser"
+    (test "Product with comments"
+          "User-Agent: Mozilla (X11) Gecko/2008110501\r\n"
+          (test-unparse-headers `((user-agent (("Mozilla" #f "X11") ("Gecko" "2008110501" #f))))))
+    (test "Realistic product"
+          "User-Agent: Mozilla/5.0 (X11; U; NetBSD amd64; en-US; rv:1.9.0.3) Gecko/2008110501 Minefield/3.0.3\r\n"
+          (test-unparse-headers `((user-agent (("Mozilla" "5.0" "X11; U; NetBSD amd64; en-US; rv:1.9.0.3") ("Gecko" "2008110501" #f) ("Minefield" "3.0.3" #f)))))))
+  (test-group "cookie unparser"
+    (test "Basic cookie"
+          "Cookie: foo=bar; $Path=/; Qux=mooh; $Unknown=something\r\n"
+          (test-unparse-headers `((cookie #(("foo" . "bar")
+                                            ((path . ,(uri-reference "/"))))
+                                          #(("Qux" . "mooh")
+                                            ((unknown . "something")))))))
+    (test "Port list"
+          "Cookie: Foo=bar; $Port=80,8080\r\n"
+          (test-unparse-headers `((cookie #(("Foo" . "bar")
+                                            ((port . (80 8080))))))))
+    (test "#t or #f values"
+          "Cookie: Foo=bar; $Port\r\n"
+          (test-unparse-headers `((cookie #(("Foo" . "bar")
+                                            ((port . #t) (domain . #f))))))))
+  (test-group "Set-Cookie unparser"
+    (test "Simple name/value pair"
+          "Set-Cookie: foo=\"bar with space\"\r\n"
+          (test-unparse-headers `((set-cookie ("foo" . "bar with space")))))
+    ;; XXX: Should intarweb remove these, or should the user code handle this?
+    ;; What if interacting with actual broken code on the other side?
+    (test "Multiple cookies with same name (CI) are all written"
+          "Set-Cookie: foo=qux\r\nSet-Cookie: Foo=bar\r\n"
+          (test-unparse-headers `((set-cookie ("foo" . "qux") ("Foo" . "bar")))))
+    (test "Cookie names preserve case"
+          "Set-Cookie: Foo=bar\r\n"
+          (test-unparse-headers `((set-cookie ("Foo" . "bar")))))
+    (test "Cookie with = signs"
+          "Set-Cookie: foo=\"bar=qux\"; Max-Age=10\r\n"
+          (test-unparse-headers `((set-cookie #(("foo" . "bar=qux") ((max-age . 10)))))))
+    (test "Comment"
+          "Set-Cookie: foo=bar; Comment=\"Hi, there!\"\r\n"
+          (test-unparse-headers `((set-cookie #(("foo" . "bar")
+                                                ((comment . "Hi, there!")))))))
+    (test "Old-style cookie expires value"
+          "Set-Cookie: foo=; Expires=Sunday, 20-Jul-08 15:23:42 GMT\r\n"
+          (test-unparse-headers `((set-cookie #(("foo" . "")
+                                                ((expires . #(42 23 15 20 6 108 0 309 #f 0))))))))    
+    (test "Secure (true)"
+          "Set-Cookie: foo=bar; Secure\r\n"
+          (test-unparse-headers `((set-cookie #(("foo" . "bar")
+                                                ((secure . #t)))))))
+    (test "Secure (false)"
+          "Set-Cookie: foo=bar\r\n"
+          (test-unparse-headers `((set-cookie #(("foo" . "bar")
+                                                ((secure . #f)))))))
+
+    (test "Path"
+          "Set-Cookie: foo=bar; Path=/blah\r\n"
+          (test-unparse-headers `((set-cookie #(("foo" . "bar")
+                                                ((path . ,(uri-reference "/blah"))
+                                                 (secure . #f))))))))  
+  (test-group "authorization unparser"
+    (test "Basic auth"
+          "Authorization: Basic QWxpIEJhYmE6b3BlbiBzZXNhbWU=\r\n"
+          (test-unparse-headers
+           `((authorization #(basic
+                              ((username . "Ali Baba")
+                               (password . "open sesame")))))))
+    (test-error* "Basic auth with colon in username"
+                 (exn http username-with-colon)
+                 (test-unparse-headers
+                  `((authorization #(basic
+                                     ((username . "foo:bar")
+                                      (password . "qux")))))))
+    (test "Digest auth"
+          "Authorization: Digest username=\"Mufasa\", realm=\"testrealm@host.com\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", uri=\"/dir/index.html\", qop=\"auth\", cnonce=\"0a4f113b\", response=\"6629fae49393a05397450978507c4ef1\", opaque=\"5ccc069c403ebaf9f0171e9517f40e41\", nc=00000001, algorithm=\"md5\"\r\n"
+          (test-unparse-headers
+           `((authorization #(digest
+                              ((username . "Mufasa")
+                               (realm . "testrealm@host.com")
+                               (nonce . "dcd98b7102dd2f0e8b11d0f600bfb0c093")
+                               (uri . ,(uri-reference "/dir/index.html"))
+                               (qop . auth)
+                               (cnonce . "0a4f113b")
+                               (response . "6629fae49393a05397450978507c4ef1")
+                               (opaque . "5ccc069c403ebaf9f0171e9517f40e41")
+                               (nc . 1)
+                               (algorithm . md5)))))))
+    (test "Custom auth"
+          "Authorization: Custom some-random-contents\r\n"
+          (parameterize ((authorization-param-subunparsers
+                          `((custom . ,(lambda (params)
+                                         (alist-ref 'contents params)))
+                            . ,(authorization-param-subparsers))))
+            (test-unparse-headers
+             `((authorization #(custom ((contents . some-random-contents)))))))))
+
+  (test-group "authenticate unparser"
+    (test-group "basic auth"
+      (test "basic"
+            "Www-Authenticate: Basic realm=\"WallyWorld\"\r\n"
+            (test-unparse-headers
+             `((www-authenticate #(basic
+                                   ((realm . "WallyWorld"))))))))
+    (test-group "digest auth"
+      (test "digest"
+            "Www-Authenticate: Digest realm=\"testrealm@host.com\", qop=\"auth,auth-int\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"\r\n"
+            (test-unparse-headers
+             `((www-authenticate #(digest
+                                   ((realm . "testrealm@host.com")
+                                    (qop . (auth auth-int))
+                                    (nonce . "dcd98b7102dd2f0e8b11d0f600bfb0c093")
+                                    (opaque . "5ccc069c403ebaf9f0171e9517f40e41")))))))
+      (test "domains"
+            "Www-Authenticate: Digest domain=\"/example http://foo.com/bar\"\r\n"
+            (test-unparse-headers
+             `((www-authenticate #(digest
+                                   ((domain . (,(uri-reference "/example")
+                                               ,(uri-reference "http://foo.com/bar")))))))))
+      (test "stale"
+            "Www-Authenticate: Digest realm=\"foo\", stale=TRUE\r\n"
+            (test-unparse-headers
+             `((www-authenticate #(digest
+                                   ((realm . "foo")
+                                    (stale . #t)))))))
+      (test "stale present but false"
+            "Www-Authenticate: Digest realm=\"foo\"\r\n"
+            (test-unparse-headers
+             `((www-authenticate #(digest
+                                   ((realm . "foo")
+                                    (stale . #f)))))))))
+  (test-group "content-disposition unparser"
+    (test "Attributes are always fully quoted and filenames stripped"
+          "Content-Disposition: form-data; name=\"foo\"; filename=\"a b c\"\r\n"
+          (test-unparse-headers `((content-disposition
+                                   #(form-data ((name . foo)
+                                                (filename . "blabla/a b c")))))))
+    (test "Size and dates are recognised correctly"
+          "Content-Disposition: inline; size=20; creation-date=\"Sun, 06 Nov 1994 08:49:37 GMT\"\r\n"
+          (test-unparse-headers `((content-disposition
+                                   #(inline ((size . 20)
+                                             (creation-date . #(37 49 08 06 10 94 0 310 #f 0)))))))))
+
+  (test-group "strict-transport-security unparser"
+    (test "Silly capitalization is honored, even if unneccessary"
+          "Strict-Transport-Security: max-age=10; includeSubDomains\r\n"
+          (test-unparse-headers `((strict-transport-security
+                                   ((max-age . 10)
+                                    (includesubdomains . #t))))))))
+
+(define (test-read-request str)
+  (call-with-input-string str read-request))
+
+(test-group "reading of requests"
+  (parameterize ((request-parsers `(,(lambda (line in)
+                                       (and (string=? line "foo") 'foo))
+                                    ,(lambda (line in)
+                                       (and (string=? line "bar") 'bar)))))
+    (test-error* (exn http unknown-protocol-line) (test-read-request "qux"))
+    (test #f (test-read-request ""))
+    (test 'foo (test-read-request "foo"))
+    (test 'bar (test-read-request "bar")))
+  ;; Even though we officially "should" support HTTP/0.9, we disable it
+  ;; by default because there are security implications of just outputting
+  ;; responses for random resources that might be under attacker control.
+  (test-group "HTTP/0.9"
+    (test-error* "By default, HTTP/0.9 is disabled"
+                 (exn http unknown-protocol-line)
+                 (test-read-request "GET /path/../to/stuff?arg1=val1&arg2=val2\r\n"))
+    (parameterize ((request-parsers (list http-1.x-request-parser http-0.9-request-parser)))
+     (let ((req (test-read-request "GET /path/../to/stuff?arg1=val1&arg2=val2\r\n")))
+       (test 0 (request-major req))
+       (test 9 (request-minor req))
+       (test 'GET (request-method req))
+       ;; Path-normalized URI (dots removed)
+       (test (uri-reference "/to/stuff?arg1=val1&arg2=val2") (request-uri req))
+       (test (headers '()) (request-headers req)))
+     ;; RFC 1945 5.0 does not mention case-sensitivity for the method in HTTP/0.9.
+     ;; It only mentions it in the context of HTTP/1.x (section 5.1.1).
+     ;; We obey the BNF syntax rule in 2.1:
+     ;;     "literal" - Quotation marks surround literal text.
+     ;;                 Unless stated otherwise, the text is case-insensitive.
+     ;; Section 4.1 defines:
+     ;;     Simple-Request  = "GET" SP Request-URI CRLF
+     (test "Method is case-insensitive" 'GET (request-method (test-read-request "geT /path\r\n")))
+     (test-error "0.9 only knows GET" (test-read-request "PUT /path"))))
+  (test-group "HTTP/1.0"
+    (test-error "Asterisk is not allowed for HTTP/1.0"
+                (request-uri (test-read-request "OPTIONS * HTTP/1.0\r\n")))
+    (let ((req (test-read-request "GET /path/to/stuff?arg1=val1&arg2=val2 HTTP/1.0\r\n\r\n")))
+      (test 1 (request-major req))
+      (test 0 (request-minor req))
+      (test 'GET (request-method req))
+      (test (uri-reference "/path/to/stuff?arg1=val1&arg2=val2") (request-uri req))
+      (test (headers '()) (request-headers req)))
+    (test 'PUT (request-method (test-read-request "PUT /path HTTP/1.0\r\n")))
+    (let ((req (test-read-request "POST / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\na\r\n1234567890\r\n")))
+      (test "Chunking ignored"
+            "3\r\nfoo\r\na\r\n1234567890\r\n"
+            (read-string #f (request-port req)))))
+  (test-group "HTTP/1.1" ; No need to test all things we test for 1.0
+    (test "Asterisk is treated specially and returns #f uri"
+          #f (request-uri (test-read-request "OPTIONS * HTTP/1.1\r\n")))
+   (let ((req (test-read-request "GET /path/to/stuff?arg1=val1&arg2=val2 HTTP/1.1\r\n\r\n")))
+     (test 1 (request-major req))
+     (test 1 (request-minor req)))
+   (test 'PUT (request-method (test-read-request "PUT /path HTTP/1.1\r\n\r\n")))
+   ; RFC 2616 5.1.1
+   (test "Method is case-sensitive" 'geT (request-method (test-read-request "geT /path HTTP/1.1\r\n\r\n")))
+   ; RFC 2616 3.1 + case-insensitivity BNF rule
+   (test "Protocol is case-insensitive" '1 (request-minor (test-read-request "GET /path htTP/1.1\r\n\r\n")))
+   ;; TODO: Test chunking
+   (test-error "Request line limit exceeded gives error"
+               (parameterize ((http-line-limit 5))
+                 (test-read-request "GET /path HTTP/1.1\r\n\r\n")))
+   (test "Reading request body"
+         '((abc . "def") (ghi . "jkl"))
+         (let ((req (test-read-request
+                     "GET / HTTP/1.1\r\nContent-Length: 15\r\n\r\nabc=def;ghi=jkl")))
+           (read-urlencoded-request-data req)))
+   (test "Reading request body with bigger limit"
+         '((abc . "def"))
+         (let ((req (test-read-request
+                     "GET / HTTP/1.1\r\nContent-Length: 7\r\n\r\nabc=def")))
+           ;; Test for 8, since 7 would error
+           (parameterize ((http-urlencoded-request-data-limit 8))
+             (read-urlencoded-request-data req))))
+   (test-error "Request body limit exceeded gives error"
+               (let ((req (test-read-request
+                           "GET / HTTP/1.1\r\nContent-Length: 7\r\n\r\nabc=def")))
+                 ;; This errors when the limit is hit, not when it is exceeded
+                 (parameterize ((http-urlencoded-request-data-limit 7))
+                   (read-urlencoded-request-data req))))
+   (let ((req (test-read-request "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\na\r\n1234567890\r\n0\r\n\r\n")))
+      (test "Chunking"
+            "foo1234567890"
+            (read-string #f (request-port req)))))
+  (test-group "Invalid protocols"
+    (test-error "Total cruft is unrecognised"
+                (test-read-request "whatever\r\n"))
+    (test-error "Invalid URI also causes protocol not to be recognised"
+                (test-read-request "GET //path HTTP/1.0\r\n"))))
+
+(define (test-write-request req . outputs)
+  (call-with-output-string
+    (lambda (out)
+      (request-port-set! req out)
+      (let ((r (write-request req)))
+       (for-each (lambda (output)
+		   (display output (request-port r)))
+		 outputs)
+       (finish-request-body r)))))
+
+(test-group "writing of requests"
+  ;; This can also be called Simple-Request as per RFC 1945 4.1
+  ;; RFC 2616 19.6 also states we should recognise 0.9 requests, but if
+  ;; we understand those we should also be able to generate them because
+  ;; a 0.9 server does not understand 1.x requests.
+  (test-group "HTTP/0.9"
+    (let ((req (make-request major: 0 minor: 9 method: 'GET
+                             uri: (uri-reference "/foo/bar.html"))))
+      (test-error* "By default, HTTP/0.9 is disabled"
+                   (exn http unknown-protocol)
+                   (test-write-request req))
+      (parameterize ((request-unparsers (list http-1.x-request-unparser
+                                              http-1.0-request-unparser
+                                              http-0.9-request-unparser)))
+        (test "Always empty headers"
+              "GET /foo/bar.html\r\n"
+              (test-write-request (update-request req
+                                                  headers:
+                                                  (headers `((foo bar))))))
+        (test "Always GET"
+              "GET /foo/bar.html\r\n"
+              (test-write-request (update-request req method: 'POST))))))
+  (test-group "HTTP/1.0"
+    (let ((req (make-request major: 1 minor: 0
+                             method: 'GET
+                             uri: (uri-reference "/foo/bar.html"))))
+      (test "Headers"
+            "GET /foo/bar.html HTTP/1.0\r\nFoo: bar\r\n\r\ntest"
+            (test-write-request
+             (update-request req
+                             headers: (headers `((foo bar))))
+             "test"))
+      (test "Chunking ignored"
+            "GET /foo/bar.html HTTP/1.0\r\nTransfer-Encoding: chunked\r\n\r\nfoobar"
+            (test-write-request
+             (update-request req
+                             headers: (headers `((transfer-encoding chunked))))
+             "foo" "" "bar"))))
+  (test-group "HTTP/1.1"
+    (let ((req (make-request major: 1 minor: 1
+                             method: 'GET
+                             uri: (uri-reference "/foo/bar.html"))))
+      (test "Headers"
+            "GET /foo/bar.html HTTP/1.1\r\nFoo: bar\r\n\r\ntest"
+            (test-write-request
+             (update-request req
+                             headers: (headers `((foo bar))))
+             "test"))
+      (test "Chunking"
+            "GET /foo/bar.html HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\na\r\n1234567890\r\n0\r\n\r\n"
+            (test-write-request
+             (update-request req
+                             headers: (headers `((transfer-encoding chunked))))
+             "foo" "" "1234567890"))
+      (test "OPTIONS-type asterisk if no URI"
+            "OPTIONS * HTTP/1.1\r\n\r\n"
+            (test-write-request
+             (update-request req method: 'OPTIONS uri: #f))))))
+
+(define (test-read-response input-string)
+  (call-with-input-string input-string read-response))
+
+(test-group "reading of responses"
+  (test-group "HTTP/1.1"
+    (let ((res (test-read-response "HTTP/1.1 303 See other\r\nFoo: bar\r\n\r\nContents")))
+      (test "Version detection"
+            '(1 . 1)
+            (cons (response-major res) (response-minor res)))
+      (test "Status"
+            '(see-other 303 "See other")
+            (list (response-status res) (response-code res) (response-reason res)))
+      (test "Headers"
+            '("bar")
+            (header-values 'foo (response-headers res)))
+      (test "Contents"
+            "Contents"
+            (read-string #f (response-port res))))
+    (test-error* (exn http unknown-protocol-line) (test-read-response "qux"))
+    (test #f (test-read-request ""))
+    (test-error "Response line limit exceeded gives error"
+                (parameterize ((http-line-limit 5))
+                  (test-read-response "HTTP/1.1 200 OK\r\n\r\n")))
+    (let ((res (test-read-response "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\na\r\n1234567890\r\n0\r\n\r\n")))
+      (test "Chunking"
+            "foo1234567890"
+            (read-string #f (response-port res))))
+    ;; Reported by "sz0ka" via IRC
+    (let ((res (test-read-response "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nfoo\r\n\r\n0\r\n\r\n")))
+      (test "First read of chunked port returns first line"
+            "foo"
+            (read-line (response-port res)))
+      (test "Peek-char returns EOF"
+            #!eof
+            (peek-char (response-port res)))
+      (test "Read-char also returns EOF"
+            #!eof
+            (read-char (response-port res)))))
+  (test-group "HTTP/1.0"
+    (let ((res (test-read-response "HTTP/1.0 303 See other\r\nFoo: bar\r\n\r\nContents")))
+      (test "Version detection"
+            '(1 . 0)
+            (cons (response-major res) (response-minor res)))
+      (test "Status"
+            '(303 . "See other")
+            (cons (response-code res) (response-reason res)))
+      (test "Headers"
+            '("bar")
+            (header-values 'foo (response-headers res)))
+      (test "Contents"
+            "Contents"
+            (read-string #f (response-port res))))
+    (let ((res (test-read-response "HTTP/1.0 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\na\r\n1234567890\r\n")))
+      (test "Chunking ignored"
+            "3\r\nfoo\r\na\r\n1234567890\r\n"
+            (read-string #f (response-port res)))))
+  (test-group "HTTP/0.9"
+    (test-error* "By default, HTTP/0.9 is disabled"
+                 (exn http unknown-protocol-line)
+                 (test-read-response "Doesn't matter what's here\r\nLine 2"))
+    (parameterize ((response-parsers (list http-1.x-response-parser
+                                           http-1.0-response-parser
+                                           http-0.9-response-parser)))
+      (let ((res (test-read-response "Doesn't matter what's here\r\nLine 2")))
+        (test "Always OK status"
+              '(200 . "OK")
+              (cons (response-code res) (response-reason res)))
+        (test "Version detection; fallback to 0.9"
+              '(0 . 9)
+              (cons (response-major res) (response-minor res)))
+        (test "No headers"
+              (headers '()) (response-headers res))
+        (test "Contents"
+              "Doesn't matter what's here\r\nLine 2"
+              (read-string #f (response-port res)))))))
+
+(define (test-write-response res . outputs)
+  (call-with-output-string
+    (lambda (out)
+      (response-port-set! res out)
+      (let ((r (write-response res)))
+       (for-each (lambda (output)
+		   (display output (response-port r)))
+		 outputs)
+       (finish-response-body r)))))
+
+(test-group "writing of responses"
+  (test-group "HTTP/0.9"
+    (let ((res (make-response major: 0 minor: 9
+                              code: 200 reason: "OK")))
+      (test-error* "By default, HTTP/0.9 is disabled"
+                   (exn http unknown-protocol)
+                   (test-write-response res "These are the contents\r\n"))
+      (parameterize ((response-unparsers (list http-1.x-response-unparser
+                                               http-1.0-response-unparser
+                                               http-0.9-response-unparser)))
+        (test "Headers ignored"
+              "These are the contents\r\n"
+              (test-write-response
+               (update-response res headers: (headers `((foo bar))))
+               "These are the contents\r\n")))))
+  (test-group "HTTP/1.0"
+    (let ((res (make-response major: 1 minor: 0
+                              code: 200 reason: "OK")))
+      (test "Headers used"
+            "HTTP/1.0 200 OK\r\nFoo: bar\r\n\r\nThese are the contents\r\n"
+            (test-write-response
+             (update-response res headers: (headers `((foo bar))))
+             "These are the contents\r\n"))
+      (test "Status code"
+            "HTTP/1.0 303 See other\r\n\r\nThese are the contents\r\n"
+            (test-write-response
+             (update-response res code: 303 reason: "See other")
+             "These are the contents\r\n"))
+      (test "Chunking ignored"
+            "HTTP/1.0 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nfoo1234567890"
+            (test-write-response
+             (update-response
+              res
+              headers: (headers `((transfer-encoding chunked))))
+             "foo" "1234567890"))))
+  (test-group "HTTP/1.1"
+   (let ((res (make-response major: 1 minor: 1
+                             code: 200 reason: "OK")))
+     (test "Headers used"
+           "HTTP/1.1 200 OK\r\nFoo: bar\r\n\r\nThese are the contents\r\n"
+            (test-write-response
+             (update-response res headers: (headers `((foo bar))))
+             "These are the contents\r\n"))
+     (test "Status code"
+           "HTTP/1.1 303 See other\r\n\r\nThese are the contents\r\n"
+           (test-write-response
+            (update-response res code: 303 reason: "See other")
+            "These are the contents\r\n"))
+     (test "Chunking"
+           "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\na\r\n1234567890\r\n0\r\n\r\n"
+           (test-write-response
+            (update-response
+             res
+             headers: (headers `((transfer-encoding chunked))))
+            "foo" "1234567890"))))
+  (test-group "status"
+    (let ((res (make-response major: 1 minor: 1)))
+      (test "reason and code are looked up by symbol properly"
+            "HTTP/1.1 409 Conflict\r\n\r\ntest"
+            (test-write-response (update-response res status: 'conflict)
+                                 "test"))
+      (test-error "an error is raised for unknown status codes"
+                  (update-response res status: 'unknown))
+      (test "any status can be used when code and reason are given directly"
+            "HTTP/1.1 999 No Way\r\n\r\ntest"
+            (test-write-response 
+             (update-response res code: 999 reason: "No Way")
+             "test"))
+      (test "defaults can be parameterized"
+            "HTTP/1.1 999 Say What\r\n\r\ntest"
+            (parameterize ((http-status-codes
+                            (alist-cons 'say-what (cons 999 "Say What")
+                                        (http-status-codes))))
+              (test-write-response (update-response res status: 'say-what)
+                                   "test"))))))
+
+(test-group "etag comparison procedures"
+  (test-group "weak comparison"
+    (test-assert "Strong etag does not match list not containing it"
+                 (not (etag-matches-weakly?
+                       '(strong . "xyz") `((strong . "blabla")))))
+    (test-assert "Weak etag does not match list not containing it"
+                 (not (etag-matches-weakly?
+                       '(weak . "xyz") `((weak . "blabla")))))
+    (test-assert "Weak etag matches list containing it"
+                 (etag-matches-weakly?
+                  '(weak . "xyz") `((strong . "blabla") (weak . "xyz"))))
+    (test-assert "Strong etag matches list containing it"
+                 (etag-matches-weakly?
+                  '(strong . "xyz") `((strong . "blabla") (strong . "xyz"))))
+    (test-assert "Weak etag does not match list containing same tag but strong"
+                 (not (etag-matches-weakly?
+                       '(weak . "xyz") `((strong . "blabla") (strong . "xyz")))))
+    (test-assert "Strong etag does not match list containing same tag but weak"
+                 (not (etag-matches-weakly?
+                       '(strong . "xyz") `((strong . "blabla") (weak . "xyz")))))
+    (test-assert "Weak etag matches list containing wildcard"
+                 (etag-matches-weakly?
+                  '(weak . "xyz") `((strong . "blabla") *)))
+    (test-assert "Strong etag matches list containing wildcard"
+                 (etag-matches-weakly?
+                  '(strong . "xyz") `((strong . "blabla") *))))
+  (test-group "strong comparison"
+    (test-assert "Strong etag does not match list not containing it"
+                 (not (etag-matches?
+                       '(strong . "xyz") `((strong . "blabla")))))
+    (test-assert "Weak etag does not match list not containing it"
+                 (not (etag-matches?
+                       '(weak . "xyz") `((weak . "blabla")))))
+    (test-assert "Weak etag does *not* match list containing it"
+                 (not (etag-matches?
+                       '(weak . "xyz") `((strong . "blabla") (weak . "xyz")))))
+    (test-assert "Strong etag matches list containing it"
+                 (etag-matches?
+                  '(strong . "xyz") `((strong . "blabla") (strong . "xyz"))))
+    (test-assert "Weak etag does not match list containing same tag but strong"
+                 (not (etag-matches?
+                       '(weak . "xyz") `((strong . "blabla") (strong . "xyz")))))
+    (test-assert "Strong etag does not match list containing same tag but weak"
+                 (not (etag-matches?
+                       '(strong . "xyz") `((strong . "blabla") (weak . "xyz")))))
+    (test-assert "Weak etag matches list containing wildcard"
+                 (etag-matches?
+                  '(weak . "xyz") `((strong . "blabla") *)))
+    (test-assert "Strong etag matches list containing wildcard"
+                 (etag-matches?
+                  '(strong . "xyz") `((strong . "blabla") *)))))
+
+
+;; We don't expose chunked-output-port/chunked-input-port.  Maybe we should?
+;; To work around this, prepend some stuff and parse some headers
+(define (chunked-inport string)
+  (let ((res (test-read-response
+              (string-append
+               "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
+               string))))
+    (response-port res)))
+
+(test-group "Chunked ports"
+  (let ((s "5\r\nab\ncd\r\n2\r\n\nx\r\n0\r\nDO NOT WANT"))
+    (test "read-lines" '("ab" "cd" "x") (read-lines (chunked-inport s)))
+    (let ((p (chunked-inport s)))
+      (test "read-char" #\a (read-char p))
+      (test "peek-char" #\b (peek-char p))
+      (test "partial read" "b\n" (read-string 2 p))
+      (test "short read" "c" (read-string 1 p))
+      (test "read across chunk boundaries" "d\nx" (read-string 3 p))
+      (test "read at eof" #!eof (read-string 1 p)))
+    (test "read beyond chunked port size"
+          "ab\ncd\nx" (read-string 10 (chunked-inport s)))))
+
+(test-end)
+
+(unless (zero? (test-failure-count)) (exit 1))
+
+;; TODO:
+;; - Fix the parsing system so it's not so broken (more comfortable combinators)
+;; - Test malformed headers
+;; - Add parsing capability for quoted-pairs inside tokens and comments
+;; - Rethink the auto-chunking stuff. Maybe this should be done at a higher level
-- 
cgit v1.2.3