Migrated http_parser_test from SDK. (#51)

* Migrated http_parser_test from SDK.
diff --git a/test/http_head_test.dart b/test/http_head_test.dart
index 0388461..0833eac 100644
--- a/test/http_head_test.dart
+++ b/test/http_head_test.dart
@@ -7,9 +7,9 @@
 import 'package:http_io/http_io.dart';
 import 'package:test/test.dart';
 
-Future<Null> testHEAD(int totalConnections) {
+Future<Null> testHEAD(int totalConnections) async {
   final completer = new Completer<Null>();
-  HttpServer.bind("127.0.0.1", 0).then((server) {
+  HttpServer.bind("127.0.0.1", 0).then((server) async {
     server.listen((request) {
       var response = request.response;
       if (request.uri.path == "/test100") {
@@ -48,7 +48,7 @@
 
     for (int i = 0; i < totalConnections; i++) {
       int len = (i % 2 == 0) ? 100 : 200;
-      client
+      await client
           .open("HEAD", "127.0.0.1", server.port, "/test$len")
           .then((request) => request.close())
           .then((HttpClientResponse response) {
@@ -57,7 +57,7 @@
             onDone: requestDone);
       });
 
-      client
+      await client
           .open("HEAD", "127.0.0.1", server.port, "/testChunked$len")
           .then((request) => request.close())
           .then((HttpClientResponse response) {
diff --git a/test/http_keep_alive_test.dart b/test/http_keep_alive_test.dart
index e26f35c..266be96 100644
--- a/test/http_keep_alive_test.dart
+++ b/test/http_keep_alive_test.dart
@@ -7,7 +7,7 @@
 import 'package:http_io/http_io.dart';
 import 'package:test/test.dart';
 
-Future getData(HttpClient client, int port, bool chunked, int length) {
+Future<int> getData(HttpClient client, int port, bool chunked, int length) {
   return client
       .get("127.0.0.1", port, "/?chunked=$chunked&length=$length")
       .then((request) => request.close())
diff --git a/test/http_parser_test.dart b/test/http_parser_test.dart
new file mode 100644
index 0000000..a567838
--- /dev/null
+++ b/test/http_parser_test.dart
@@ -0,0 +1,811 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library dart.http;
+
+import "dart:async";
+import "dart:isolate";
+import "dart:math";
+import "dart:typed_data";
+
+import 'package:http_io/http_io.dart';
+import 'package:http_io/src/http_parser.dart';
+import 'package:test/test.dart';
+
+class HttpParserTest {
+  static void runAllTests() {
+    test('parseRequest', testParseRequest);
+    test('parseResponse', testParseResponse);
+    test('parseInvalidRequest', testParseInvalidRequest);
+    test('parseInvalidResponse', testParseInvalidResponse);
+  }
+
+  static void _testParseRequest(
+      String request, String expectedMethod, String expectedUri,
+      {int expectedTransferLength: 0,
+      int expectedBytesReceived: 0,
+      Map<String, String> expectedHeaders: null,
+      bool chunked: false,
+      bool upgrade: false,
+      int unparsedLength: 0,
+      bool connectionClose: false,
+      String expectedVersion: "1.1"}) {
+    StreamController<List<int>> controller;
+    void reset() {
+      HttpParser httpParser = new HttpParser.requestParser();
+      controller = new StreamController(sync: true);
+      var port1 = new ReceivePort();
+      var port2 = new ReceivePort();
+
+      String method;
+      Uri uri;
+      HttpHeaders headers;
+      int bytesReceived;
+      int unparsedBytesReceived;
+      bool upgraded;
+
+      httpParser.listenToStream(controller.stream);
+      httpParser.listen((incoming) {
+        method = incoming.method;
+        uri = incoming.uri;
+        headers = incoming.headers;
+        upgraded = incoming.upgraded;
+        expect(upgrade, equals(upgraded));
+
+        if (!chunked) {
+          expect(expectedTransferLength, equals(incoming.transferLength));
+        } else {
+          expect(-1, equals(incoming.transferLength));
+        }
+        if (expectedHeaders != null) {
+          expectedHeaders.forEach((String name, String value) =>
+              expect(value, equals(headers[name][0])));
+        }
+        incoming.listen((List<int> data) {
+          expect(upgraded, isFalse);
+          bytesReceived += data.length;
+        }, onDone: () {
+          port2.close();
+          expect(expectedMethod, equals(method));
+          expect(expectedUri, equals(uri.toString()));
+          expect(expectedVersion, equals(headers.protocolVersion));
+          if (upgrade) {
+            expect(0, equals(bytesReceived));
+            // port1 is closed by the listener on the detached data.
+          } else {
+            expect(expectedBytesReceived, equals(bytesReceived));
+          }
+        });
+
+        if (upgraded) {
+          port1.close();
+          httpParser.detachIncoming().listen((List<int> data) {
+            unparsedBytesReceived += data.length;
+          }, onDone: () {
+            expect(unparsedLength, equals(unparsedBytesReceived));
+            port2.close();
+          });
+        }
+
+        incoming.dataDone.then((_) {
+          port1.close();
+        });
+      });
+
+      method = null;
+      uri = null;
+      headers = null;
+      bytesReceived = 0;
+      unparsedBytesReceived = 0;
+      upgraded = false;
+    }
+
+    void testWrite(List<int> requestData, [int chunkSize = -1]) {
+      if (chunkSize == -1) chunkSize = requestData.length;
+      reset();
+      for (int pos = 0; pos < requestData.length; pos += chunkSize) {
+        int end = min(requestData.length, pos + chunkSize);
+        controller.add(requestData.sublist(pos, end));
+      }
+      controller.close();
+    }
+
+    // Test parsing the request three times delivering the data in
+    // different chunks.
+    List<int> requestData = new Uint8List.fromList(request.codeUnits);
+    testWrite(requestData);
+    testWrite(requestData, 10);
+    testWrite(requestData, 1);
+  }
+
+  static void _testParseRequestLean(
+      String request, String expectedMethod, String expectedUri,
+      {int expectedTransferLength: 0,
+      int expectedBytesReceived: 0,
+      Map<String, String> expectedHeaders: null,
+      bool chunked: false,
+      bool upgrade: false,
+      int unparsedLength: 0,
+      bool connectionClose: false,
+      String expectedVersion: "1.1"}) {
+    _testParseRequest(request, expectedMethod, expectedUri,
+        expectedTransferLength: expectedTransferLength,
+        expectedBytesReceived: expectedBytesReceived,
+        expectedHeaders: expectedHeaders,
+        chunked: chunked,
+        upgrade: upgrade,
+        unparsedLength: unparsedLength,
+        connectionClose: connectionClose,
+        expectedVersion: expectedVersion);
+    // Same test but with only \n instead of \r\n terminating each header line.
+    _testParseRequest(request.replaceAll('\r', ''), expectedMethod, expectedUri,
+        expectedTransferLength: expectedTransferLength,
+        expectedBytesReceived: expectedBytesReceived,
+        expectedHeaders: expectedHeaders,
+        chunked: chunked,
+        upgrade: upgrade,
+        unparsedLength: unparsedLength,
+        connectionClose: connectionClose,
+        expectedVersion: expectedVersion);
+  }
+
+  static void _testParseInvalidRequest(String request) {
+    HttpParser httpParser;
+    bool errorCalled;
+    StreamController<List<int>> controller;
+
+    void reset() {
+      httpParser = new HttpParser.requestParser();
+      controller = new StreamController(sync: true);
+      var port = new ReceivePort();
+      httpParser.listenToStream(controller.stream);
+      var subscription = httpParser.listen((incoming) {
+        fail("Expected request");
+      });
+      subscription.onError((e) {
+        errorCalled = true;
+      });
+      subscription.onDone(() {
+        port.close();
+        expect(errorCalled, isTrue);
+      });
+      errorCalled = false;
+    }
+
+    void testWrite(List<int> requestData, [int chunkSize = -1]) {
+      if (chunkSize == -1) chunkSize = requestData.length;
+      reset();
+      for (int pos = 0;
+          pos < requestData.length && !errorCalled;
+          pos += chunkSize) {
+        int end = min(requestData.length, pos + chunkSize);
+        controller.add(requestData.sublist(pos, end));
+      }
+      controller.close();
+    }
+
+    // Test parsing the request three times delivering the data in
+    // different chunks.
+    List<int> requestData = new Uint8List.fromList(request.codeUnits);
+    testWrite(requestData);
+    testWrite(requestData, 10);
+    testWrite(requestData, 1);
+  }
+
+  static void _testParseResponse(
+      String response, int expectedStatusCode, String expectedReasonPhrase,
+      {int expectedTransferLength: 0,
+      int expectedBytesReceived: 0,
+      Map<String, String> expectedHeaders: null,
+      bool chunked: false,
+      bool close: false,
+      String responseToMethod: null,
+      bool connectionClose: false,
+      bool upgrade: false,
+      int unparsedLength: 0,
+      String expectedVersion: "1.1"}) {
+    StreamController<List<int>> controller;
+
+    void reset() {
+      HttpParser httpParser;
+      bool headersCompleteCalled;
+      bool dataEndCalled;
+      bool dataEndClose;
+      int statusCode;
+      String reasonPhrase;
+      HttpHeaders headers;
+      int bytesReceived;
+      httpParser = new HttpParser.responseParser();
+      controller = new StreamController(sync: true);
+      var port = new ReceivePort();
+      httpParser.listenToStream(controller.stream);
+      int doneCallCount = 0;
+      // Called when done parsing entire message and done parsing body.
+      // Only executed when both are done.
+      void whenDone() {
+        doneCallCount++;
+        if (doneCallCount < 2) return;
+        expect(expectedVersion, equals(headers.protocolVersion));
+        expect(expectedStatusCode, equals(statusCode));
+        expect(expectedReasonPhrase, equals(reasonPhrase));
+        expect(headersCompleteCalled, isTrue);
+        expect(expectedBytesReceived, equals(bytesReceived));
+        if (!upgrade) {
+          expect(dataEndCalled, isTrue);
+          if (close) expect(dataEndClose, isTrue);
+          expect(dataEndClose, equals(connectionClose));
+        }
+      }
+
+      ;
+
+      httpParser.listen((incoming) {
+        port.close();
+        statusCode = incoming.statusCode;
+        reasonPhrase = incoming.reasonPhrase;
+        headers = incoming.headers;
+        expect(headersCompleteCalled, isFalse);
+        if (!chunked && !close) {
+          expect(expectedTransferLength, equals(incoming.transferLength));
+        } else {
+          expect(-1, equals(incoming.transferLength));
+        }
+        if (expectedHeaders != null) {
+          expectedHeaders.forEach((String name, String value) {
+            expect(value, equals(headers[name][0]));
+          });
+        }
+        expect(upgrade, equals(httpParser.upgrade));
+        headersCompleteCalled = true;
+        incoming.listen((List<int> data) {
+          expect(headersCompleteCalled, isTrue);
+          bytesReceived += data.length;
+        }, onDone: () {
+          dataEndCalled = true;
+          dataEndClose = close;
+          whenDone();
+        });
+      }, onDone: whenDone);
+
+      headersCompleteCalled = false;
+      dataEndCalled = false;
+      dataEndClose = null;
+      statusCode = -1;
+      reasonPhrase = null;
+      headers = null;
+      bytesReceived = 0;
+    }
+
+    void testWrite(List<int> requestData, [int chunkSize = -1]) {
+      if (chunkSize == -1) chunkSize = requestData.length;
+      reset();
+      for (int pos = 0; pos < requestData.length; pos += chunkSize) {
+        int end = min(requestData.length, pos + chunkSize);
+        controller.add(requestData.sublist(pos, end));
+      }
+      if (close) controller.close();
+    }
+
+    // Test parsing the request three times delivering the data in
+    // different chunks.
+    List<int> responseData = new Uint8List.fromList(response.codeUnits);
+    testWrite(responseData);
+    testWrite(responseData, 10);
+    testWrite(responseData, 1);
+  }
+
+  static void _testParseInvalidResponse(String response, [bool close = false]) {
+    void testWrite(List<int> requestData, [int chunkSize = -1]) {
+      HttpParser httpParser = new HttpParser.responseParser();
+      StreamController<List<int>> controller = new StreamController(sync: true);
+      bool errorCalled = false;
+      ;
+
+      if (chunkSize == -1) chunkSize = requestData.length;
+
+      var port = new ReceivePort();
+      httpParser.listenToStream(controller.stream);
+      var subscription = httpParser.listen((incoming) {
+        incoming.listen((data) {}, onError: (e) {
+          expect(errorCalled, isFalse);
+          errorCalled = true;
+        });
+      });
+      subscription.onError((e) {
+        expect(errorCalled, isFalse);
+        errorCalled = true;
+      });
+      subscription.onDone(() {
+        port.close();
+        expect(errorCalled, isTrue);
+      });
+
+      errorCalled = false;
+      for (int pos = 0;
+          pos < requestData.length && !errorCalled;
+          pos += chunkSize) {
+        int end = min(requestData.length, pos + chunkSize);
+        controller.add(requestData.sublist(pos, end));
+      }
+      controller.close();
+    }
+
+    // Test parsing the request three times delivering the data in
+    // different chunks.
+    List<int> responseData = new Uint8List.fromList(response.codeUnits);
+    testWrite(responseData);
+    testWrite(responseData, 10);
+    testWrite(responseData, 1);
+  }
+
+  static void testParseRequest() {
+    String request;
+    Map<String, String> headers;
+    var methods = [
+      // RFC 2616 methods.
+      "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT",
+      // WebDAV methods from RFC 4918.
+      "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK",
+      // WebDAV methods from RFC 5323.
+      "SEARCH",
+      // Methods with HTTP prefix.
+      "H", "HT", "HTT", "HTTP", "HX", "HTX", "HTTX", "HTTPX"
+    ];
+    methods = ['GET'];
+    methods.forEach((method) {
+      request = "$method / HTTP/1.1\r\n\r\n";
+      _testParseRequestLean(request, method, "/");
+      request = "$method /index.html HTTP/1.1\r\n\r\n";
+      _testParseRequestLean(request, method, "/index.html");
+    });
+    request = "GET / HTTP/1.0\r\n\r\n";
+    _testParseRequestLean(request, "GET", "/",
+        expectedVersion: "1.0", connectionClose: true);
+
+    request = "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n";
+    _testParseRequestLean(request, "GET", "/", expectedVersion: "1.0");
+
+    request = """
+POST /test HTTP/1.1\r
+AAA: AAA\r
+\r
+""";
+    _testParseRequestLean(request, "POST", "/test");
+
+    request = """
+POST /test HTTP/1.1\r
+\r
+""";
+    _testParseRequestLean(request, "POST", "/test");
+
+    request = """
+POST /test HTTP/1.1\r
+Header-A: AAA\r
+X-Header-B: bbb\r
+\r
+""";
+    headers = new Map();
+    headers["header-a"] = "AAA";
+    headers["x-header-b"] = "bbb";
+    _testParseRequestLean(request, "POST", "/test", expectedHeaders: headers);
+
+    request = """
+POST /test HTTP/1.1\r
+Empty-Header-1:\r
+Empty-Header-2:\r
+        \r
+\r
+""";
+    headers = new Map();
+    headers["empty-header-1"] = "";
+    headers["empty-header-2"] = "";
+    _testParseRequestLean(request, "POST", "/test", expectedHeaders: headers);
+
+    request = """
+POST /test HTTP/1.1\r
+Header-A:   AAA\r
+X-Header-B:\t \t bbb\r
+\r
+""";
+    headers = new Map();
+    headers["header-a"] = "AAA";
+    headers["x-header-b"] = "bbb";
+    _testParseRequestLean(request, "POST", "/test", expectedHeaders: headers);
+
+    request = """
+POST /test HTTP/1.1\r
+Header-A:   AA\r
+ A\r
+X-Header-B:           b\r
+  b\r
+\t    b\r
+\r
+""";
+
+    headers = new Map();
+    headers["header-a"] = "AAA";
+    headers["x-header-b"] = "bbb";
+    _testParseRequestLean(request, "POST", "/test", expectedHeaders: headers);
+
+    request = """
+POST /test HTTP/1.1\r
+Content-Length: 10\r
+\r
+0123456789""";
+    _testParseRequestLean(request, "POST", "/test",
+        expectedTransferLength: 10, expectedBytesReceived: 10);
+
+    // Test connection close header.
+    request = "GET /test HTTP/1.1\r\nConnection: close\r\n\r\n";
+    _testParseRequest(request, "GET", "/test", connectionClose: true);
+
+    // Test chunked encoding.
+    request = """
+POST /test HTTP/1.1\r
+Transfer-Encoding: chunked\r
+\r
+5\r
+01234\r
+5\r
+56789\r
+0\r\n\r\n""";
+    _testParseRequest(request, "POST", "/test",
+        expectedTransferLength: -1, expectedBytesReceived: 10, chunked: true);
+
+    // Test mixing chunked encoding and content length (content length
+    // is ignored).
+    request = """
+POST /test HTTP/1.1\r
+Content-Length: 7\r
+Transfer-Encoding: chunked\r
+\r
+5\r
+01234\r
+5\r
+56789\r
+0\r\n\r\n""";
+    _testParseRequest(request, "POST", "/test",
+        expectedTransferLength: -1, expectedBytesReceived: 10, chunked: true);
+
+    // Test mixing chunked encoding and content length (content length
+    // is ignored).
+    request = """
+POST /test HTTP/1.1\r
+Transfer-Encoding: chunked\r
+Content-Length: 3\r
+\r
+5\r
+01234\r
+5\r
+56789\r
+0\r\n\r\n""";
+    _testParseRequest(request, "POST", "/test",
+        expectedTransferLength: -1, expectedBytesReceived: 10, chunked: true);
+
+    // Test upper and lower case hex digits in chunked encoding.
+    request = """
+POST /test HTTP/1.1\r
+Transfer-Encoding: chunked\r
+\r
+1E\r
+012345678901234567890123456789\r
+1e\r
+012345678901234567890123456789\r
+0\r\n\r\n""";
+    _testParseRequest(request, "POST", "/test",
+        expectedTransferLength: -1, expectedBytesReceived: 60, chunked: true);
+
+    // Test chunk extensions in chunked encoding.
+    request = """
+POST /test HTTP/1.1\r
+Transfer-Encoding: chunked\r
+\r
+1E;xxx\r
+012345678901234567890123456789\r
+1E;yyy=zzz\r
+012345678901234567890123456789\r
+0\r\n\r\n""";
+    _testParseRequest(request, "POST", "/test",
+        expectedTransferLength: -1, expectedBytesReceived: 60, chunked: true);
+
+    // Test HTTP upgrade.
+    request = """
+GET /irc HTTP/1.1\r
+Upgrade: irc/1.2\r
+Connection: Upgrade\r
+\r\n\x01\x01\x01\x01\x01\x02\x02\x02\x02\xFF""";
+    headers = new Map();
+    headers["upgrade"] = "irc/1.2";
+    _testParseRequest(request, "GET", "/irc",
+        expectedHeaders: headers, upgrade: true, unparsedLength: 10);
+
+    // Test HTTP upgrade with protocol data.
+    request = """
+GET /irc HTTP/1.1\r
+Upgrade: irc/1.2\r
+Connection: Upgrade\r
+\r\n""";
+    headers = new Map();
+    headers["upgrade"] = "irc/1.2";
+    _testParseRequest(request, "GET", "/irc",
+        expectedHeaders: headers, upgrade: true);
+
+    // Test websocket upgrade.
+    request = """
+GET /chat HTTP/1.1\r
+Host: server.example.com\r
+Upgrade: websocket\r
+Connection: Upgrade\r
+Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
+Origin: http://example.com\r
+Sec-WebSocket-Version: 13\r
+\r\n""";
+    headers = new Map();
+    headers["host"] = "server.example.com";
+    headers["upgrade"] = "websocket";
+    headers["sec-websocket-key"] = "dGhlIHNhbXBsZSBub25jZQ==";
+    headers["origin"] = "http://example.com";
+    headers["sec-websocket-version"] = "13";
+    _testParseRequest(request, "GET", "/chat",
+        expectedHeaders: headers, upgrade: true);
+
+    // Test websocket upgrade with protocol data. NOTE: When using the
+    // WebSocket protocol this should never happen as the client
+    // should not send protocol data before processing the request
+    // part of the opening handshake. However the HTTP parser should
+    // still handle this.
+    request = """
+GET /chat HTTP/1.1\r
+Host: server.example.com\r
+Upgrade: websocket\r
+Connection: Upgrade\r
+Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
+Origin: http://example.com\r
+Sec-WebSocket-Version: 13\r
+\r\n0123456""";
+    headers = new Map();
+    headers["host"] = "server.example.com";
+    headers["upgrade"] = "websocket";
+    headers["sec-websocket-key"] = "dGhlIHNhbXBsZSBub25jZQ==";
+    headers["origin"] = "http://example.com";
+    headers["sec-websocket-version"] = "13";
+    _testParseRequest(request, "GET", "/chat",
+        expectedHeaders: headers, upgrade: true, unparsedLength: 7);
+  }
+
+  static void testParseResponse() {
+    String response;
+    Map<String, String> headers;
+    response = "HTTP/1.1 100 Continue\r\nContent-Length: 0\r\n\r\n";
+    _testParseResponse(response, 100, "Continue");
+
+    response = "HTTP/1.1 100 Continue\r\nContent-Length: 0\r\n\r\n";
+    _testParseResponse(response, 100, "Continue");
+
+    response = "HTTP/1.1 100 Continue\r\nContent-Length: 10\r\n\r\n";
+    _testParseResponse(response, 100, "Continue",
+        expectedTransferLength: 10, expectedBytesReceived: 0);
+
+    response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n"
+        "Connection: Close\r\n\r\n";
+    _testParseResponse(response, 200, "OK", connectionClose: true);
+
+    response = "HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n";
+    _testParseResponse(response, 200, "OK",
+        expectedVersion: "1.0", connectionClose: true);
+
+    response = "HTTP/1.0 200 OK\r\nContent-Length: 0\r\n"
+        "Connection: Keep-Alive\r\n\r\n";
+    _testParseResponse(response, 200, "OK", expectedVersion: "1.0");
+
+    response = "HTTP/1.1 204 No Content\r\nContent-Length: 11\r\n\r\n";
+    _testParseResponse(response, 204, "No Content",
+        expectedTransferLength: 11, expectedBytesReceived: 0);
+
+    response = "HTTP/1.1 304 Not Modified\r\nContent-Length: 12\r\n\r\n";
+    _testParseResponse(response, 304, "Not Modified",
+        expectedTransferLength: 12, expectedBytesReceived: 0);
+
+    response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
+    _testParseResponse(response, 200, "OK");
+
+    response = "HTTP/1.1 404 Not found\r\nContent-Length: 0\r\n\r\n";
+    _testParseResponse(response, 404, "Not found");
+
+    response = "HTTP/1.1 500 Server error\r\nContent-Length: 0\r\n\r\n";
+    _testParseResponse(response, 500, "Server error");
+
+    // Test response to HEAD request.
+    response = """
+HTTP/1.1 200 OK\r
+Content-Length: 20\r
+Content-Type: text/html\r
+\r\n""";
+    headers = new Map();
+    headers["content-length"] = "20";
+    headers["content-type"] = "text/html";
+    _testParseResponse(response, 200, "OK",
+        responseToMethod: "HEAD",
+        expectedTransferLength: 20,
+        expectedBytesReceived: 0,
+        expectedHeaders: headers);
+
+    // Test content.
+    response = """
+HTTP/1.1 200 OK\r
+Content-Length: 20\r
+\r
+01234567890123456789""";
+    _testParseResponse(response, 200, "OK",
+        expectedTransferLength: 20, expectedBytesReceived: 20);
+
+    // Test upper and lower case hex digits in chunked encoding.
+    response = """
+HTTP/1.1 200 OK\r
+Transfer-Encoding: chunked\r
+\r
+1A\r
+01234567890123456789012345\r
+1f\r
+0123456789012345678901234567890\r
+0\r\n\r\n""";
+    _testParseResponse(response, 200, "OK",
+        expectedTransferLength: -1, expectedBytesReceived: 57, chunked: true);
+
+    // Test connection close header.
+    response = """
+HTTP/1.1 200 OK\r
+Content-Length: 0\r
+Connection: close\r
+\r\n""";
+    _testParseResponse(response, 200, "OK", connectionClose: true);
+
+    // Test HTTP response without any transfer length indications
+    // where closing the connections indicates end of body.
+    response = """
+HTTP/1.1 200 OK\r
+\r
+01234567890123456789012345
+0123456789012345678901234567890
+""";
+    _testParseResponse(response, 200, "OK",
+        expectedTransferLength: -1,
+        expectedBytesReceived: 59,
+        close: true,
+        connectionClose: true);
+
+    // Test HTTP upgrade.
+    response = """
+HTTP/1.1 101 Switching Protocols\r
+Upgrade: irc/1.2\r
+Connection: Upgrade\r
+\r\n""";
+    headers = new Map();
+    headers["upgrade"] = "irc/1.2";
+    _testParseResponse(response, 101, "Switching Protocols",
+        expectedHeaders: headers, upgrade: true);
+
+    // Test HTTP upgrade with protocol data.
+    response = """
+HTTP/1.1 101 Switching Protocols\r
+Upgrade: irc/1.2\r
+Connection: Upgrade\r
+\r\n\x00\x10\x20\x30\x40\x50\x60\x70\x80\x90\xA0\xB0\xC0\xD0\xE0\xF0""";
+    headers = new Map();
+    headers["upgrade"] = "irc/1.2";
+    _testParseResponse(response, 101, "Switching Protocols",
+        expectedHeaders: headers, upgrade: true, unparsedLength: 16);
+
+    // Test websocket upgrade.
+    response = """
+HTTP/1.1 101 Switching Protocols\r
+Upgrade: websocket\r
+Connection: Upgrade\r
+Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
+\r\n""";
+    headers = new Map();
+    headers["upgrade"] = "websocket";
+    headers["sec-websocket-accept"] = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=";
+    _testParseResponse(response, 101, "Switching Protocols",
+        expectedHeaders: headers, upgrade: true);
+
+    // Test websocket upgrade with protocol data.
+    response = """
+HTTP/1.1 101 Switching Protocols\r
+Upgrade: websocket\r
+Connection: Upgrade\r
+Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
+\r\nABCD""";
+    headers = new Map();
+    headers["upgrade"] = "websocket";
+    headers["sec-websocket-accept"] = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=";
+    _testParseResponse(response, 101, "Switching Protocols",
+        expectedHeaders: headers, upgrade: true, unparsedLength: 4);
+  }
+
+  static void testParseInvalidRequest() {
+    String request;
+    request = "GET /\r\n\r\n";
+    _testParseInvalidRequest(request);
+
+    request = "GET / \r\n\r\n";
+    _testParseInvalidRequest(request);
+
+    request = "/ HTTP/1.1\r\n\r\n";
+    _testParseInvalidRequest(request);
+
+    request = "GET HTTP/1.1\r\n\r\n";
+    _testParseInvalidRequest(request);
+
+    request = " / HTTP/1.1\r\n\r\n";
+    _testParseInvalidRequest(request);
+
+    request = "@ / HTTP/1.1\r\n\r\n";
+    _testParseInvalidRequest(request);
+
+    request = "GET / TTP/1.1\r\n\r\n";
+    _testParseInvalidRequest(request);
+
+    request = "GET / HTTP/1.\r\n\r\n";
+    _testParseInvalidRequest(request);
+
+    request = "GET / HTTP/1.1\r\nKeep-Alive: False\r\nbadheader\r\n\r\n";
+    _testParseInvalidRequest(request);
+  }
+
+  static void testParseInvalidResponse() {
+    String response;
+
+    response = "HTTP/1.1\r\nContent-Length: 0\r\n\r\n";
+    _testParseInvalidResponse(response);
+
+    response = "HTTP/1.1 \r\nContent-Length: 0\r\n\r\n";
+    _testParseInvalidResponse(response);
+
+    response = "HTTP/1.1 200\r\nContent-Length: 0\r\n\r\n";
+    _testParseInvalidResponse(response);
+
+    response = "HTTP/1.1 200 \r\nContent-Length: 0\r\n\r\n";
+    _testParseInvalidResponse(response);
+
+    response = "HTTP/1.1 OK\r\nContent-Length: 0\r\n\r\n";
+    _testParseInvalidResponse(response);
+
+    response = "200 OK\r\nContent-Length: 0\r\n\r\n";
+    _testParseInvalidResponse(response);
+
+    response = "HTTP/1. 200 OK\r\nContent-Length: 0\r\n\r\n";
+    _testParseInvalidResponse(response);
+
+    response = "HTTP/1.1 200 O\rK\r\nContent-Length: 0\r\n\r\n";
+    _testParseInvalidResponse(response);
+
+    response = "HTTP/1.1 000 OK\r\nContent-Length: 0\r\n\r\n";
+    _testParseInvalidResponse(response);
+
+    response = "HTTP/1.1 999 Server Error\r\nContent-Length: 0\r\n\r\n";
+    _testParseInvalidResponse(response);
+
+    response = "HTTP/1.1 200 OK\r\nContent-Length: x\r\n\r\n";
+    _testParseInvalidResponse(response);
+
+    response = "HTTP/1.1 200 OK\r\nbadheader\r\n\r\n";
+    _testParseInvalidResponse(response);
+
+    response = """
+HTTP/1.1 200 OK\r
+Transfer-Encoding: chunked\r
+\r
+1A\r
+01234567890123456789012345\r
+1g\r
+0123456789012345678901234567890\r
+0\r\n\r\n""";
+    _testParseInvalidResponse(response);
+  }
+}
+
+void main() {
+  HttpParserTest.runAllTests();
+}