summaryrefslogtreecommitdiffstats
path: root/netwerk/test/httpserver/test
diff options
context:
space:
mode:
Diffstat (limited to 'netwerk/test/httpserver/test')
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^1
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^3
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_both.html2
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^2
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt9
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^1
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_status_override.html9
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^1
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt1
-rw-r--r--netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^1
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/bar.html^^10
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^2
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^1
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^1
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt2
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/foo.html^9
-rw-r--r--netwerk/test/httpserver/test/data/name-scheme/normal-file.txt1
-rw-r--r--netwerk/test/httpserver/test/data/ranges/empty.txt0
-rw-r--r--netwerk/test/httpserver/test/data/ranges/headers.txt1
-rw-r--r--netwerk/test/httpserver/test/data/ranges/headers.txt^headers^1
-rw-r--r--netwerk/test/httpserver/test/data/ranges/range.txt1
-rw-r--r--netwerk/test/httpserver/test/data/sjs/cgi.sjs8
-rw-r--r--netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^2
-rw-r--r--netwerk/test/httpserver/test/data/sjs/object-state.sjs87
-rw-r--r--netwerk/test/httpserver/test/data/sjs/qi.sjs48
-rw-r--r--netwerk/test/httpserver/test/data/sjs/range-checker.sjs3
-rw-r--r--netwerk/test/httpserver/test/data/sjs/sjs4
-rw-r--r--netwerk/test/httpserver/test/data/sjs/state1.sjs42
-rw-r--r--netwerk/test/httpserver/test/data/sjs/state2.sjs42
-rw-r--r--netwerk/test/httpserver/test/data/sjs/thrower.sjs6
-rw-r--r--netwerk/test/httpserver/test/head_utils.js600
-rw-r--r--netwerk/test/httpserver/test/test_async_response_sending.js1683
-rw-r--r--netwerk/test/httpserver/test/test_basic_functionality.js176
-rw-r--r--netwerk/test/httpserver/test/test_body_length.js64
-rw-r--r--netwerk/test/httpserver/test/test_byte_range.js278
-rw-r--r--netwerk/test/httpserver/test/test_cern_meta.js76
-rw-r--r--netwerk/test/httpserver/test/test_default_index_handler.js290
-rw-r--r--netwerk/test/httpserver/test/test_empty_body.js55
-rw-r--r--netwerk/test/httpserver/test/test_errorhandler_exception.js84
-rw-r--r--netwerk/test/httpserver/test/test_header_array.js67
-rw-r--r--netwerk/test/httpserver/test/test_headers.js189
-rw-r--r--netwerk/test/httpserver/test/test_host.js666
-rw-r--r--netwerk/test/httpserver/test/test_linedata.js20
-rw-r--r--netwerk/test/httpserver/test/test_load_module.js16
-rw-r--r--netwerk/test/httpserver/test/test_name_scheme.js90
-rw-r--r--netwerk/test/httpserver/test/test_processasync.js304
-rw-r--r--netwerk/test/httpserver/test/test_qi.js110
-rw-r--r--netwerk/test/httpserver/test/test_registerdirectory.js263
-rw-r--r--netwerk/test/httpserver/test/test_registerfile.js50
-rw-r--r--netwerk/test/httpserver/test/test_registerprefix.js127
-rw-r--r--netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js135
-rw-r--r--netwerk/test/httpserver/test/test_response_write.js55
-rw-r--r--netwerk/test/httpserver/test/test_seizepower.js182
-rw-r--r--netwerk/test/httpserver/test/test_setindexhandler.js67
-rw-r--r--netwerk/test/httpserver/test/test_setstatusline.js172
-rw-r--r--netwerk/test/httpserver/test/test_sjs.js251
-rw-r--r--netwerk/test/httpserver/test/test_sjs_object_state.js290
-rw-r--r--netwerk/test/httpserver/test/test_sjs_state.js186
-rw-r--r--netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js74
-rw-r--r--netwerk/test/httpserver/test/test_start_stop.js189
-rw-r--r--netwerk/test/httpserver/test/xpcshell.ini37
61 files changed, 7147 insertions, 0 deletions
diff --git a/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^
new file mode 100644
index 000000000..b005a65fd
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^
@@ -0,0 +1 @@
+If this has goofy headers on it, it's a success.
diff --git a/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^
new file mode 100644
index 000000000..66e152231
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^
@@ -0,0 +1,3 @@
+HTTP 500 This Isn't A Server Error
+Foo-RFC: 3092
+Shaving-Cream-Atom: Illudium Phosdex
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_both.html b/netwerk/test/httpserver/test/data/cern_meta/test_both.html
new file mode 100644
index 000000000..db18ea5d7
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_both.html
@@ -0,0 +1,2 @@
+This page is a text file served with status 501. (That's really a lie, tho,
+because this is definitely Implemented.)
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^
new file mode 100644
index 000000000..bb3c16a2e
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^
@@ -0,0 +1,2 @@
+HTTP 501 Unimplemented
+Content-Type: text/plain
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt
new file mode 100644
index 000000000..7235fa32a
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <title>This is really HTML, not text</title>
+</head>
+<body>
+<p>This file is really HTML; the test_ctype_override.txt^headers^ file sets a
+ new header that overwrites the default text/plain header.</p>
+</body>
+</html>
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^
new file mode 100644
index 000000000..156209f9c
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^
@@ -0,0 +1 @@
+Content-Type: text/html
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html
new file mode 100644
index 000000000..fd243c640
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <title>This is a 404 page</title>
+</head>
+<body>
+<p>This page has a 404 HTTP status associated with it, via
+ <code>test_status_override.html^headers^</code>.</p>
+</body>
+</html>
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^
new file mode 100644
index 000000000..f438a0574
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^
@@ -0,0 +1 @@
+HTTP 404 Can't Find This
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt
new file mode 100644
index 000000000..4718ec282
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt
@@ -0,0 +1 @@
+This page has an HTTP status override without a description (it defaults to "").
diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^
new file mode 100644
index 000000000..32da7632f
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^
@@ -0,0 +1 @@
+HTTP 732
diff --git a/netwerk/test/httpserver/test/data/name-scheme/bar.html^^ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^
new file mode 100644
index 000000000..bed1f34c9
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <title>Welcome to bar.html^</title>
+</head>
+<body>
+<p>This file is named with two trailing carets, so the last is stripped
+ away, producing bar.html^ as the final name.</p>
+</body>
+</html>
+
diff --git a/netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^
new file mode 100644
index 000000000..04fbaa08f
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^
@@ -0,0 +1,2 @@
+HTTP 200 OK
+Content-Type: text/html
diff --git a/netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^
new file mode 100644
index 000000000..dccee48e3
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^
@@ -0,0 +1 @@
+This file shouldn't be shown in directory listings.
diff --git a/netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^
new file mode 100644
index 000000000..a8ee35a3b
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^
@@ -0,0 +1 @@
+This file should show up in directory listings as SHOULD_SEE_THIS.txt^.
diff --git a/netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt b/netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt
new file mode 100644
index 000000000..2ceca8ca9
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt
@@ -0,0 +1,2 @@
+File in a directory named with a trailing caret (in the virtual FS; on disk it
+actually ends with two carets).
diff --git a/netwerk/test/httpserver/test/data/name-scheme/foo.html^ b/netwerk/test/httpserver/test/data/name-scheme/foo.html^
new file mode 100644
index 000000000..a3efe8b5c
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/foo.html^
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <title>ERROR</title>
+</head>
+<body>
+<p>This file should never be served by the web server because its name ends
+ with a caret not followed by another caret.</p>
+</body>
+</html>
diff --git a/netwerk/test/httpserver/test/data/name-scheme/normal-file.txt b/netwerk/test/httpserver/test/data/name-scheme/normal-file.txt
new file mode 100644
index 000000000..ab71eabaf
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/name-scheme/normal-file.txt
@@ -0,0 +1 @@
+This should be seen.
diff --git a/netwerk/test/httpserver/test/data/ranges/empty.txt b/netwerk/test/httpserver/test/data/ranges/empty.txt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/ranges/empty.txt
diff --git a/netwerk/test/httpserver/test/data/ranges/headers.txt b/netwerk/test/httpserver/test/data/ranges/headers.txt
new file mode 100644
index 000000000..6cf83528c
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/ranges/headers.txt
@@ -0,0 +1 @@
+Hello Kitty
diff --git a/netwerk/test/httpserver/test/data/ranges/headers.txt^headers^ b/netwerk/test/httpserver/test/data/ranges/headers.txt^headers^
new file mode 100644
index 000000000..d0a633f04
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/ranges/headers.txt^headers^
@@ -0,0 +1 @@
+X-SJS-Header: customized
diff --git a/netwerk/test/httpserver/test/data/ranges/range.txt b/netwerk/test/httpserver/test/data/ranges/range.txt
new file mode 100644
index 000000000..ab71eabaf
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/ranges/range.txt
@@ -0,0 +1 @@
+This should be seen.
diff --git a/netwerk/test/httpserver/test/data/sjs/cgi.sjs b/netwerk/test/httpserver/test/data/sjs/cgi.sjs
new file mode 100644
index 000000000..b1554f2bc
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/cgi.sjs
@@ -0,0 +1,8 @@
+function handleRequest(request, response)
+{
+ if (request.queryString == "throw")
+ throw "monkey wrench!";
+
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write("PASS");
+}
diff --git a/netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^ b/netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^
new file mode 100644
index 000000000..a83ff774a
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^
@@ -0,0 +1,2 @@
+HTTP 500 Error
+This-Header: SHOULD NOT APPEAR IN CGI.JSC RESPONSES!
diff --git a/netwerk/test/httpserver/test/data/sjs/object-state.sjs b/netwerk/test/httpserver/test/data/sjs/object-state.sjs
new file mode 100644
index 000000000..1d9ea8b4e
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/object-state.sjs
@@ -0,0 +1,87 @@
+function parseQueryString(str)
+{
+ var paramArray = str.split("&");
+ var regex = /^([^=]+)=(.*)$/;
+ var params = {};
+ for (var i = 0, sz = paramArray.length; i < sz; i++)
+ {
+ var match = regex.exec(paramArray[i]);
+ if (!match)
+ throw "Bad parameter in queryString! '" + paramArray[i] + "'";
+ params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
+ }
+
+ return params;
+}
+
+/*
+ * We're relying somewhat dubiously on all data being sent as soon as it's
+ * available at numerous levels (in Necko in the server-side part of the
+ * connection, in the OS's outgoing socket buffer, in the OS's incoming socket
+ * buffer, and in Necko in the client-side part of the connection), but to the
+ * best of my knowledge there's no way to force data flow at all those levels,
+ * so this is the best we can do.
+ */
+function handleRequest(request, response)
+{
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ /*
+ * NB: A Content-Type header is *necessary* to avoid content-sniffing, which
+ * will delay onStartRequest past the the point where the entire head of
+ * the response has been received.
+ */
+ response.setHeader("Content-Type", "text/plain", false);
+
+ var params = parseQueryString(request.queryString);
+
+ switch (params.state)
+ {
+ case "initial":
+ response.processAsync();
+ response.write("do");
+ var state =
+ {
+ QueryInterface: function(iid)
+ {
+ if (iid.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+ end: function()
+ {
+ response.write("ne");
+ response.finish();
+ }
+ };
+ state.wrappedJSObject = state;
+ setObjectState("object-state-test", state);
+ getObjectState("object-state-test", function(obj)
+ {
+ if (obj !== state)
+ {
+ response.write("FAIL bad state save");
+ response.finish();
+ }
+ });
+ break;
+
+ case "intermediate":
+ response.write("intermediate");
+ break;
+
+ case "trigger":
+ response.write("trigger");
+ getObjectState("object-state-test", function(obj)
+ {
+ obj.wrappedJSObject.end();
+ setObjectState("object-state-test", null);
+ });
+ break;
+
+ default:
+ response.setStatusLine(request.httpVersion, 500, "Unexpected State");
+ response.write("Bad state: " + params.state);
+ break;
+ }
+}
diff --git a/netwerk/test/httpserver/test/data/sjs/qi.sjs b/netwerk/test/httpserver/test/data/sjs/qi.sjs
new file mode 100644
index 000000000..89c7089b5
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/qi.sjs
@@ -0,0 +1,48 @@
+const Ci = Components.interfaces;
+
+function handleRequest(request, response)
+{
+ var exstr, qid;
+
+ response.setStatusLine(request.httpVersion, 500, "FAIL");
+
+ var passed = false;
+ try
+ {
+ qid = request.QueryInterface(Ci.nsIHttpRequest);
+ passed = qid === request;
+ }
+ catch (e)
+ {
+ exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0];
+ response.setStatusLine(request.httpVersion, 500,
+ "request doesn't QI: " + exstr);
+ return;
+ }
+ if (!passed)
+ {
+ response.setStatusLine(request.httpVersion, 500, "request QI'd wrongly?");
+ return;
+ }
+
+ passed = false;
+ try
+ {
+ qid = response.QueryInterface(Ci.nsIHttpResponse);
+ passed = qid === response;
+ }
+ catch (e)
+ {
+ exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0];
+ response.setStatusLine(request.httpVersion, 500,
+ "response doesn't QI: " + exstr);
+ return;
+ }
+ if (!passed)
+ {
+ response.setStatusLine(request.httpVersion, 500, "response QI'd wrongly?");
+ return;
+ }
+
+ response.setStatusLine(request.httpVersion, 200, "SJS QI Tests Passed");
+}
diff --git a/netwerk/test/httpserver/test/data/sjs/range-checker.sjs b/netwerk/test/httpserver/test/data/sjs/range-checker.sjs
new file mode 100644
index 000000000..39fcc2b88
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/range-checker.sjs
@@ -0,0 +1,3 @@
+function handleRequest(request, response)
+{
+}
diff --git a/netwerk/test/httpserver/test/data/sjs/sjs b/netwerk/test/httpserver/test/data/sjs/sjs
new file mode 100644
index 000000000..374ca4167
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/sjs
@@ -0,0 +1,4 @@
+function handleRequest(request, response)
+{
+ response.write("FAIL");
+}
diff --git a/netwerk/test/httpserver/test/data/sjs/state1.sjs b/netwerk/test/httpserver/test/data/sjs/state1.sjs
new file mode 100644
index 000000000..da2862d1e
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/state1.sjs
@@ -0,0 +1,42 @@
+function parseQueryString(str)
+{
+ var paramArray = str.split("&");
+ var regex = /^([^=]+)=(.*)$/;
+ var params = {};
+ for (var i = 0, sz = paramArray.length; i < sz; i++)
+ {
+ var match = regex.exec(paramArray[i]);
+ if (!match)
+ throw "Bad parameter in queryString! '" + paramArray[i] + "'";
+ params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
+ }
+
+ return params;
+}
+
+function handleRequest(request, response)
+{
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ var params = parseQueryString(request.queryString);
+
+ var oldShared = getSharedState("shared-value");
+ response.setHeader("X-Old-Shared-Value", oldShared, false);
+
+ var newShared = params.newShared;
+ if (newShared !== undefined)
+ {
+ setSharedState("shared-value", newShared);
+ response.setHeader("X-New-Shared-Value", newShared, false);
+ }
+
+ var oldPrivate = getState("private-value");
+ response.setHeader("X-Old-Private-Value", oldPrivate, false);
+
+ var newPrivate = params.newPrivate;
+ if (newPrivate !== undefined)
+ {
+ setState("private-value", newPrivate);
+ response.setHeader("X-New-Private-Value", newPrivate, false);
+ }
+}
diff --git a/netwerk/test/httpserver/test/data/sjs/state2.sjs b/netwerk/test/httpserver/test/data/sjs/state2.sjs
new file mode 100644
index 000000000..da2862d1e
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/state2.sjs
@@ -0,0 +1,42 @@
+function parseQueryString(str)
+{
+ var paramArray = str.split("&");
+ var regex = /^([^=]+)=(.*)$/;
+ var params = {};
+ for (var i = 0, sz = paramArray.length; i < sz; i++)
+ {
+ var match = regex.exec(paramArray[i]);
+ if (!match)
+ throw "Bad parameter in queryString! '" + paramArray[i] + "'";
+ params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
+ }
+
+ return params;
+}
+
+function handleRequest(request, response)
+{
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ var params = parseQueryString(request.queryString);
+
+ var oldShared = getSharedState("shared-value");
+ response.setHeader("X-Old-Shared-Value", oldShared, false);
+
+ var newShared = params.newShared;
+ if (newShared !== undefined)
+ {
+ setSharedState("shared-value", newShared);
+ response.setHeader("X-New-Shared-Value", newShared, false);
+ }
+
+ var oldPrivate = getState("private-value");
+ response.setHeader("X-Old-Private-Value", oldPrivate, false);
+
+ var newPrivate = params.newPrivate;
+ if (newPrivate !== undefined)
+ {
+ setState("private-value", newPrivate);
+ response.setHeader("X-New-Private-Value", newPrivate, false);
+ }
+}
diff --git a/netwerk/test/httpserver/test/data/sjs/thrower.sjs b/netwerk/test/httpserver/test/data/sjs/thrower.sjs
new file mode 100644
index 000000000..1aaf1639a
--- /dev/null
+++ b/netwerk/test/httpserver/test/data/sjs/thrower.sjs
@@ -0,0 +1,6 @@
+function handleRequest(request, response)
+{
+ if (request.queryString == "throw")
+ undefined[5];
+ response.setHeader("X-Test-Status", "PASS", false);
+}
diff --git a/netwerk/test/httpserver/test/head_utils.js b/netwerk/test/httpserver/test/head_utils.js
new file mode 100644
index 000000000..21f615117
--- /dev/null
+++ b/netwerk/test/httpserver/test/head_utils.js
@@ -0,0 +1,600 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var _HTTPD_JS_PATH = __LOCATION__.parent;
+_HTTPD_JS_PATH.append("httpd.js");
+load(_HTTPD_JS_PATH.path);
+
+// if these tests fail, we'll want the debug output
+DEBUG = true;
+
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+/**
+ * Constructs a new nsHttpServer instance. This function is intended to
+ * encapsulate construction of a server so that at some point in the future it
+ * is possible to run these tests (with at most slight modifications) against
+ * the server when used as an XPCOM component (not as an inline script).
+ */
+function createServer()
+{
+ return new nsHttpServer();
+}
+
+/**
+ * Creates a new HTTP channel.
+ *
+ * @param url
+ * the URL of the channel to create
+ */
+function makeChannel(url)
+{
+ return NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true})
+ .QueryInterface(Ci.nsIHttpChannel);
+}
+
+/**
+ * Make a binary input stream wrapper for the given stream.
+ *
+ * @param stream
+ * the nsIInputStream to wrap
+ */
+function makeBIS(stream)
+{
+ return new BinaryInputStream(stream);
+}
+
+
+/**
+ * Returns the contents of the file as a string.
+ *
+ * @param file : nsILocalFile
+ * the file whose contents are to be read
+ * @returns string
+ * the contents of the file
+ */
+function fileContents(file)
+{
+ const PR_RDONLY = 0x01;
+ var fis = new FileInputStream(file, PR_RDONLY, 0o444,
+ Ci.nsIFileInputStream.CLOSE_ON_EOF);
+ var sis = new ScriptableInputStream(fis);
+ var contents = sis.read(file.fileSize);
+ sis.close();
+ return contents;
+}
+
+/**
+ * Iterates over the lines, delimited by CRLF, in data, returning each line
+ * without the trailing line separator.
+ *
+ * @param data : string
+ * a string consisting of lines of data separated by CRLFs
+ * @returns Iterator
+ * an Iterator which returns each line from data in turn; note that this
+ * includes a final empty line if data ended with a CRLF
+ */
+function LineIterator(data)
+{
+ var start = 0, index = 0;
+ do
+ {
+ index = data.indexOf("\r\n");
+ if (index >= 0)
+ yield data.substring(0, index);
+ else
+ yield data;
+
+ data = data.substring(index + 2);
+ }
+ while (index >= 0);
+}
+
+/**
+ * Throws if iter does not contain exactly the CRLF-separated lines in the
+ * array expectedLines.
+ *
+ * @param iter : Iterator
+ * an Iterator which returns lines of text
+ * @param expectedLines : [string]
+ * an array of the expected lines of text
+ * @throws string
+ * an error message if iter doesn't agree with expectedLines
+ */
+function expectLines(iter, expectedLines)
+{
+ var index = 0;
+ for (var line in iter)
+ {
+ if (expectedLines.length == index)
+ throw "Error: got more than " + expectedLines.length + " expected lines!";
+
+ var expected = expectedLines[index++];
+ if (expected !== line)
+ throw "Error on line " + index + "!\n" +
+ " actual: '" + line + "',\n" +
+ " expect: '" + expected + "'";
+ }
+
+ if (expectedLines.length !== index)
+ {
+ throw "Expected more lines! Got " + index +
+ ", expected " + expectedLines.length;
+ }
+}
+
+/**
+ * Spew a bunch of HTTP metadata from request into the body of response.
+ *
+ * @param request : nsIHttpRequest
+ * the request whose metadata should be output
+ * @param response : nsIHttpResponse
+ * the response to which the metadata is written
+ */
+function writeDetails(request, response)
+{
+ response.write("Method: " + request.method + "\r\n");
+ response.write("Path: " + request.path + "\r\n");
+ response.write("Query: " + request.queryString + "\r\n");
+ response.write("Version: " + request.httpVersion + "\r\n");
+ response.write("Scheme: " + request.scheme + "\r\n");
+ response.write("Host: " + request.host + "\r\n");
+ response.write("Port: " + request.port);
+}
+
+/**
+ * Advances iter past all non-blank lines and a single blank line, after which
+ * point the body of the response will be returned next from the iterator.
+ *
+ * @param iter : Iterator
+ * an iterator over the CRLF-delimited lines in an HTTP response, currently
+ * just after the Request-Line
+ */
+function skipHeaders(iter)
+{
+ var line = iter.next();
+ while (line !== "")
+ line = iter.next();
+}
+
+/**
+ * Checks that the exception e (which may be an XPConnect-created exception
+ * object or a raw nsresult number) is the given nsresult.
+ *
+ * @param e : Exception or nsresult
+ * the actual exception
+ * @param code : nsresult
+ * the expected exception
+ */
+function isException(e, code)
+{
+ if (e !== code && e.result !== code)
+ do_throw("unexpected error: " + e);
+}
+
+/**
+ * Calls the given function at least the specified number of milliseconds later.
+ * The callback will not undershoot the given time, but it might overshoot --
+ * don't expect precision!
+ *
+ * @param milliseconds : uint
+ * the number of milliseconds to delay
+ * @param callback : function() : void
+ * the function to call
+ */
+function callLater(msecs, callback)
+{
+ do_timeout(msecs, callback);
+}
+
+
+/*******************************************************
+ * SIMPLE SUPPORT FOR LOADING/TESTING A SERIES OF URLS *
+ *******************************************************/
+
+/**
+ * Create a completion callback which will stop the given server and end the
+ * test, assuming nothing else remains to be done at that point.
+ */
+function testComplete(srv)
+{
+ return function complete()
+ {
+ do_test_pending();
+ srv.stop(function quit() { do_test_finished(); });
+ };
+}
+
+/**
+ * Represents a path to load from the tested HTTP server, along with actions to
+ * take before, during, and after loading the associated page.
+ *
+ * @param path
+ * the URL to load from the server
+ * @param initChannel
+ * a function which takes as a single parameter a channel created for path and
+ * initializes its state, or null if no additional initialization is needed
+ * @param onStartRequest
+ * called during onStartRequest for the load of the URL, with the same
+ * parameters; the request parameter has been QI'd to nsIHttpChannel and
+ * nsIHttpChannelInternal for convenience; may be null if nothing needs to be
+ * done
+ * @param onStopRequest
+ * called during onStopRequest for the channel, with the same parameters plus
+ * a trailing parameter containing an array of the bytes of data downloaded in
+ * the body of the channel response; the request parameter has been QI'd to
+ * nsIHttpChannel and nsIHttpChannelInternal for convenience; may be null if
+ * nothing needs to be done
+ */
+function Test(path, initChannel, onStartRequest, onStopRequest)
+{
+ function nil() { }
+
+ this.path = path;
+ this.initChannel = initChannel || nil;
+ this.onStartRequest = onStartRequest || nil;
+ this.onStopRequest = onStopRequest || nil;
+}
+
+/**
+ * Runs all the tests in testArray.
+ *
+ * @param testArray
+ * a non-empty array of Tests to run, in order
+ * @param done
+ * function to call when all tests have run (e.g. to shut down the server)
+ */
+function runHttpTests(testArray, done)
+{
+ /** Kicks off running the next test in the array. */
+ function performNextTest()
+ {
+ if (++testIndex == testArray.length)
+ {
+ try
+ {
+ done();
+ }
+ catch (e)
+ {
+ do_report_unexpected_exception(e, "running test-completion callback");
+ }
+ return;
+ }
+
+ do_test_pending();
+
+ var test = testArray[testIndex];
+ var ch = makeChannel(test.path);
+ try
+ {
+ test.initChannel(ch);
+ }
+ catch (e)
+ {
+ try
+ {
+ do_report_unexpected_exception(e, "testArray[" + testIndex + "].initChannel(ch)");
+ }
+ catch (e)
+ {
+ /* swallow and let tests continue */
+ }
+ }
+
+ listener._channel = ch;
+ ch.asyncOpen2(listener);
+ }
+
+ /** Index of the test being run. */
+ var testIndex = -1;
+
+ /** Stream listener for the channels. */
+ var listener =
+ {
+ /** Current channel being observed by this. */
+ _channel: null,
+ /** Array of bytes of data in body of response. */
+ _data: [],
+
+ onStartRequest: function(request, cx)
+ {
+ do_check_true(request === this._channel);
+ var ch = request.QueryInterface(Ci.nsIHttpChannel)
+ .QueryInterface(Ci.nsIHttpChannelInternal);
+
+ this._data.length = 0;
+ try
+ {
+ try
+ {
+ testArray[testIndex].onStartRequest(ch, cx);
+ }
+ catch (e)
+ {
+ do_report_unexpected_exception(e, "testArray[" + testIndex + "].onStartRequest");
+ }
+ }
+ catch (e)
+ {
+ do_note_exception(e, "!!! swallowing onStartRequest exception so onStopRequest is " +
+ "called...");
+ }
+ },
+ onDataAvailable: function(request, cx, inputStream, offset, count)
+ {
+ var quantum = 262144; // just above half the argument-count limit
+ var bis = makeBIS(inputStream);
+ for (var start = 0; start < count; start += quantum)
+ {
+ var newData = bis.readByteArray(Math.min(quantum, count - start));
+ Array.prototype.push.apply(this._data, newData);
+ }
+ },
+ onStopRequest: function(request, cx, status)
+ {
+ this._channel = null;
+
+ var ch = request.QueryInterface(Ci.nsIHttpChannel)
+ .QueryInterface(Ci.nsIHttpChannelInternal);
+
+ // NB: The onStopRequest callback must run before performNextTest here,
+ // because the latter runs the next test's initChannel callback, and
+ // we want one test to be sequentially processed before the next
+ // one.
+ try
+ {
+ testArray[testIndex].onStopRequest(ch, cx, status, this._data);
+ }
+ finally
+ {
+ try
+ {
+ performNextTest();
+ }
+ finally
+ {
+ do_test_finished();
+ }
+ }
+ },
+ QueryInterface: function(aIID)
+ {
+ if (aIID.equals(Ci.nsIStreamListener) ||
+ aIID.equals(Ci.nsIRequestObserver) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+ };
+
+ performNextTest();
+}
+
+
+/****************************************
+ * RAW REQUEST FORMAT TESTING FUNCTIONS *
+ ****************************************/
+
+/**
+ * Sends a raw string of bytes to the given host and port and checks that the
+ * response is acceptable.
+ *
+ * @param host : string
+ * the host to which a connection should be made
+ * @param port : PRUint16
+ * the port to use for the connection
+ * @param data : string or [string...]
+ * either:
+ * - the raw data to send, as a string of characters with codes in the
+ * range 0-255, or
+ * - an array of such strings whose concatenation forms the raw data
+ * @param responseCheck : function(string) : void
+ * a function which is provided with the data sent by the remote host which
+ * conducts whatever tests it wants on that data; useful for tweaking the test
+ * environment between tests
+ */
+function RawTest(host, port, data, responseCheck)
+{
+ if (0 > port || 65535 < port || port % 1 !== 0)
+ throw "bad port";
+ if (!(data instanceof Array))
+ data = [data];
+ if (data.length <= 0)
+ throw "bad data length";
+ if (!data.every(function(v) { return /^[\x00-\xff]*$/.test(v); }))
+ throw "bad data contained non-byte-valued character";
+
+ this.host = host;
+ this.port = port;
+ this.data = data;
+ this.responseCheck = responseCheck;
+}
+
+/**
+ * Runs all the tests in testArray, an array of RawTests.
+ *
+ * @param testArray : [RawTest]
+ * an array of RawTests to run, in order
+ * @param done
+ * function to call when all tests have run (e.g. to shut down the server)
+ */
+function runRawTests(testArray, done)
+{
+ do_test_pending();
+
+ var sts = Cc["@mozilla.org/network/socket-transport-service;1"]
+ .getService(Ci.nsISocketTransportService);
+
+ var currentThread = Cc["@mozilla.org/thread-manager;1"]
+ .getService()
+ .currentThread;
+
+ /** Kicks off running the next test in the array. */
+ function performNextTest()
+ {
+ if (++testIndex == testArray.length)
+ {
+ do_test_finished();
+ try
+ {
+ done();
+ }
+ catch (e)
+ {
+ do_report_unexpected_exception(e, "running test-completion callback");
+ }
+ return;
+ }
+
+
+ var rawTest = testArray[testIndex];
+
+ var transport =
+ sts.createTransport(null, 0, rawTest.host, rawTest.port, null);
+
+ var inStream = transport.openInputStream(0, 0, 0);
+ var outStream = transport.openOutputStream(0, 0, 0);
+
+ // reset
+ dataIndex = 0;
+ received = "";
+
+ waitForMoreInput(inStream);
+ waitToWriteOutput(outStream);
+ }
+
+ function waitForMoreInput(stream)
+ {
+ reader.stream = stream;
+ stream = stream.QueryInterface(Ci.nsIAsyncInputStream);
+ stream.asyncWait(reader, 0, 0, currentThread);
+ }
+
+ function waitToWriteOutput(stream)
+ {
+ // Do the QueryInterface here, not earlier, because there is no
+ // guarantee that 'stream' passed in here been QIed to nsIAsyncOutputStream
+ // since the last GC.
+ stream = stream.QueryInterface(Ci.nsIAsyncOutputStream);
+ stream.asyncWait(writer, 0, testArray[testIndex].data[dataIndex].length,
+ currentThread);
+ }
+
+ /** Index of the test being run. */
+ var testIndex = -1;
+
+ /**
+ * Index of remaining data strings to be written to the socket in current
+ * test.
+ */
+ var dataIndex = 0;
+
+ /** Data received so far from the server. */
+ var received = "";
+
+ /** Reads data from the socket. */
+ var reader =
+ {
+ onInputStreamReady: function(stream)
+ {
+ do_check_true(stream === this.stream);
+ try
+ {
+ var bis = new BinaryInputStream(stream);
+
+ var av = 0;
+ try
+ {
+ av = bis.available();
+ }
+ catch (e)
+ {
+ /* default to 0 */
+ do_note_exception(e);
+ }
+
+ if (av > 0)
+ {
+ var quantum = 262144;
+ for (var start = 0; start < av; start += quantum)
+ {
+ var bytes = bis.readByteArray(Math.min(quantum, av - start));
+ received += String.fromCharCode.apply(null, bytes);
+ }
+ waitForMoreInput(stream);
+ return;
+ }
+ }
+ catch(e)
+ {
+ do_report_unexpected_exception(e);
+ }
+
+ var rawTest = testArray[testIndex];
+ try
+ {
+ rawTest.responseCheck(received);
+ }
+ catch (e)
+ {
+ do_report_unexpected_exception(e);
+ }
+ finally
+ {
+ try
+ {
+ stream.close();
+ performNextTest();
+ }
+ catch (e)
+ {
+ do_report_unexpected_exception(e);
+ }
+ }
+ }
+ };
+
+ /** Writes data to the socket. */
+ var writer =
+ {
+ onOutputStreamReady: function(stream)
+ {
+ var str = testArray[testIndex].data[dataIndex];
+
+ var written = 0;
+ try
+ {
+ written = stream.write(str, str.length);
+ if (written == str.length)
+ dataIndex++;
+ else
+ testArray[testIndex].data[dataIndex] = str.substring(written);
+ }
+ catch (e)
+ {
+ do_note_exception(e);
+ /* stream could have been closed, just ignore */
+ }
+
+ try
+ {
+ // Keep writing data while we can write and
+ // until there's no more data to read
+ if (written > 0 && dataIndex < testArray[testIndex].data.length)
+ waitToWriteOutput(stream);
+ else
+ stream.close();
+ }
+ catch (e)
+ {
+ do_report_unexpected_exception(e);
+ }
+ }
+ };
+
+ performNextTest();
+}
diff --git a/netwerk/test/httpserver/test/test_async_response_sending.js b/netwerk/test/httpserver/test/test_async_response_sending.js
new file mode 100644
index 000000000..84ec74daf
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_async_response_sending.js
@@ -0,0 +1,1683 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Ensures that data a request handler writes out in response is sent only as
+ * quickly as the client can receive it, without racing ahead and being forced
+ * to block while writing that data.
+ *
+ * NB: These tests are extremely tied to the current implementation, in terms of
+ * when and how stream-ready notifications occur, the amount of data which will
+ * be read or written at each notification, and so on. If the implementation
+ * changes in any way with respect to stream copying, this test will probably
+ * have to change a little at the edges as well.
+ */
+
+gThreadManager = Cc["@mozilla.org/thread-manager;1"].createInstance();
+
+function run_test()
+{
+ do_test_pending();
+ tests.push(function testsComplete(_)
+ {
+ dumpn("******************\n" +
+ "* TESTS COMPLETE *\n" +
+ "******************");
+ do_test_finished();
+ });
+
+ runNextTest();
+}
+
+function runNextTest()
+{
+ testIndex++;
+ dumpn("*** runNextTest(), testIndex: " + testIndex);
+
+ try
+ {
+ var test = tests[testIndex];
+ test(runNextTest);
+ }
+ catch (e)
+ {
+ var msg = "exception running test " + testIndex + ": " + e;
+ if (e && "stack" in e)
+ msg += "\nstack follows:\n" + e.stack;
+ do_throw(msg);
+ }
+}
+
+
+/*************
+ * TEST DATA *
+ *************/
+
+const NOTHING = [];
+
+const FIRST_SEGMENT = [1, 2, 3, 4];
+const SECOND_SEGMENT = [5, 6, 7, 8];
+const THIRD_SEGMENT = [9, 10, 11, 12];
+
+const SEGMENT = FIRST_SEGMENT;
+const TWO_SEGMENTS = [1, 2, 3, 4, 5, 6, 7, 8];
+const THREE_SEGMENTS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
+
+const SEGMENT_AND_HALF = [1, 2, 3, 4, 5, 6];
+
+const QUARTER_SEGMENT = [1];
+const HALF_SEGMENT = [1, 2];
+const SECOND_HALF_SEGMENT = [3, 4];
+const THREE_QUARTER_SEGMENT = [1, 2, 3];
+const EXTRA_HALF_SEGMENT = [5, 6];
+const MIDDLE_HALF_SEGMENT = [2, 3];
+const LAST_QUARTER_SEGMENT = [4];
+const FOURTH_HALF_SEGMENT = [7, 8];
+const HALF_THIRD_SEGMENT = [9, 10];
+const LATTER_HALF_THIRD_SEGMENT = [11, 12];
+
+const TWO_HALF_SEGMENTS = [1, 2, 1, 2];
+
+
+/*********
+ * TESTS *
+ *********/
+
+var tests =
+ [
+ sourceClosedWithoutWrite,
+ writeOneSegmentThenClose,
+ simpleWriteThenRead,
+ writeLittleBeforeReading,
+ writeMultipleSegmentsThenRead,
+ writeLotsBeforeReading,
+ writeLotsBeforeReading2,
+ writeThenReadPartial,
+ manyPartialWrites,
+ partialRead,
+ partialWrite,
+ sinkClosedImmediately,
+ sinkClosedWithReadableData,
+ sinkClosedAfterWrite,
+ sourceAndSinkClosed,
+ sinkAndSourceClosed,
+ sourceAndSinkClosedWithPendingData,
+ sinkAndSourceClosedWithPendingData,
+ ];
+var testIndex = -1;
+
+function sourceClosedWithoutWrite(next)
+{
+ var t = new CopyTest("sourceClosedWithoutWrite", next);
+
+ t.closeSource(Cr.NS_OK);
+ t.expect(Cr.NS_OK, [NOTHING]);
+}
+
+function writeOneSegmentThenClose(next)
+{
+ var t = new CopyTest("writeLittleBeforeReading", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.closeSource(Cr.NS_OK);
+ t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]);
+ t.expect(Cr.NS_OK, [SEGMENT]);
+}
+
+function simpleWriteThenRead(next)
+{
+ var t = new CopyTest("simpleWriteThenRead", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]);
+ t.closeSource(Cr.NS_OK);
+ t.expect(Cr.NS_OK, [SEGMENT]);
+}
+
+function writeLittleBeforeReading(next)
+{
+ var t = new CopyTest("writeLittleBeforeReading", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.closeSource(Cr.NS_OK);
+ t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]);
+ t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]);
+ t.expect(Cr.NS_OK, [SEGMENT, SEGMENT]);
+}
+
+function writeMultipleSegmentsThenRead(next)
+{
+ var t = new CopyTest("writeMultipleSegmentsThenRead", next);
+
+ t.addToSource(TWO_SEGMENTS);
+ t.makeSourceReadable(TWO_SEGMENTS.length);
+ t.makeSinkWritableAndWaitFor(TWO_SEGMENTS.length,
+ [FIRST_SEGMENT, SECOND_SEGMENT]);
+ t.closeSource(Cr.NS_OK);
+ t.expect(Cr.NS_OK, [TWO_SEGMENTS]);
+}
+
+function writeLotsBeforeReading(next)
+{
+ var t = new CopyTest("writeLotsBeforeReading", next);
+
+ t.addToSource(TWO_SEGMENTS);
+ t.makeSourceReadable(TWO_SEGMENTS.length);
+ t.makeSinkWritableAndWaitFor(FIRST_SEGMENT.length, [FIRST_SEGMENT]);
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.makeSinkWritableAndWaitFor(SECOND_SEGMENT.length, [SECOND_SEGMENT]);
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.closeSource(Cr.NS_OK);
+ t.makeSinkWritableAndWaitFor(2 * SEGMENT.length, [SEGMENT, SEGMENT]);
+ t.expect(Cr.NS_OK, [TWO_SEGMENTS, SEGMENT, SEGMENT]);
+}
+
+function writeLotsBeforeReading2(next)
+{
+ var t = new CopyTest("writeLotsBeforeReading", next);
+
+ t.addToSource(THREE_SEGMENTS);
+ t.makeSourceReadable(THREE_SEGMENTS.length);
+ t.makeSinkWritableAndWaitFor(FIRST_SEGMENT.length, [FIRST_SEGMENT]);
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.makeSinkWritableAndWaitFor(SECOND_SEGMENT.length, [SECOND_SEGMENT]);
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.makeSinkWritableAndWaitFor(THIRD_SEGMENT.length, [THIRD_SEGMENT]);
+ t.closeSource(Cr.NS_OK);
+ t.makeSinkWritableAndWaitFor(2 * SEGMENT.length, [SEGMENT, SEGMENT]);
+ t.expect(Cr.NS_OK, [THREE_SEGMENTS, SEGMENT, SEGMENT]);
+}
+
+function writeThenReadPartial(next)
+{
+ var t = new CopyTest("writeThenReadPartial", next);
+
+ t.addToSource(SEGMENT_AND_HALF);
+ t.makeSourceReadable(SEGMENT_AND_HALF.length);
+ t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]);
+ t.closeSource(Cr.NS_OK);
+ t.makeSinkWritableAndWaitFor(EXTRA_HALF_SEGMENT.length, [EXTRA_HALF_SEGMENT]);
+ t.expect(Cr.NS_OK, [SEGMENT_AND_HALF]);
+}
+
+function manyPartialWrites(next)
+{
+ var t = new CopyTest("manyPartialWrites", next);
+
+ t.addToSource(HALF_SEGMENT);
+ t.makeSourceReadable(HALF_SEGMENT.length);
+
+ t.addToSource(HALF_SEGMENT);
+ t.makeSourceReadable(HALF_SEGMENT.length);
+ t.makeSinkWritableAndWaitFor(2 * HALF_SEGMENT.length, [TWO_HALF_SEGMENTS]);
+ t.closeSource(Cr.NS_OK);
+ t.expect(Cr.NS_OK, [TWO_HALF_SEGMENTS]);
+}
+
+function partialRead(next)
+{
+ var t = new CopyTest("partialRead", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.addToSource(HALF_SEGMENT);
+ t.makeSourceReadable(HALF_SEGMENT.length);
+ t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]);
+ t.closeSourceAndWaitFor(Cr.NS_OK, HALF_SEGMENT.length, [HALF_SEGMENT]);
+ t.expect(Cr.NS_OK, [SEGMENT, HALF_SEGMENT]);
+}
+
+function partialWrite(next)
+{
+ var t = new CopyTest("partialWrite", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.makeSinkWritableByIncrementsAndWaitFor(SEGMENT.length,
+ [QUARTER_SEGMENT,
+ MIDDLE_HALF_SEGMENT,
+ LAST_QUARTER_SEGMENT]);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.makeSinkWritableByIncrementsAndWaitFor(SEGMENT.length,
+ [HALF_SEGMENT, SECOND_HALF_SEGMENT]);
+
+ t.addToSource(THREE_SEGMENTS);
+ t.makeSourceReadable(THREE_SEGMENTS.length);
+ t.makeSinkWritableByIncrementsAndWaitFor(THREE_SEGMENTS.length,
+ [HALF_SEGMENT, SECOND_HALF_SEGMENT,
+ SECOND_SEGMENT,
+ HALF_THIRD_SEGMENT,
+ LATTER_HALF_THIRD_SEGMENT]);
+
+ t.closeSource(Cr.NS_OK);
+ t.expect(Cr.NS_OK, [SEGMENT, SEGMENT, THREE_SEGMENTS]);
+}
+
+function sinkClosedImmediately(next)
+{
+ var t = new CopyTest("sinkClosedImmediately", next);
+
+ t.closeSink(Cr.NS_OK);
+ t.expect(Cr.NS_ERROR_UNEXPECTED, [NOTHING]);
+}
+
+function sinkClosedWithReadableData(next)
+{
+ var t = new CopyTest("sinkClosedWithReadableData", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+ t.closeSink(Cr.NS_OK);
+ t.expect(Cr.NS_ERROR_UNEXPECTED, [NOTHING]);
+}
+
+function sinkClosedAfterWrite(next)
+{
+ var t = new CopyTest("sinkClosedAfterWrite", next);
+
+ t.addToSource(TWO_SEGMENTS);
+ t.makeSourceReadable(TWO_SEGMENTS.length);
+ t.makeSinkWritableAndWaitFor(FIRST_SEGMENT.length, [FIRST_SEGMENT]);
+ t.closeSink(Cr.NS_OK);
+ t.expect(Cr.NS_ERROR_UNEXPECTED, [FIRST_SEGMENT]);
+}
+
+function sourceAndSinkClosed(next)
+{
+ var t = new CopyTest("sourceAndSinkClosed", next);
+
+ t.closeSourceThenSink(Cr.NS_OK, Cr.NS_OK);
+ t.expect(Cr.NS_OK, []);
+}
+
+function sinkAndSourceClosed(next)
+{
+ var t = new CopyTest("sinkAndSourceClosed", next);
+
+ t.closeSinkThenSource(Cr.NS_OK, Cr.NS_OK);
+
+ // sink notify received first, hence error
+ t.expect(Cr.NS_ERROR_UNEXPECTED, []);
+}
+
+function sourceAndSinkClosedWithPendingData(next)
+{
+ var t = new CopyTest("sourceAndSinkClosedWithPendingData", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+
+ t.closeSourceThenSink(Cr.NS_OK, Cr.NS_OK);
+
+ // not all data from source copied, so error
+ t.expect(Cr.NS_ERROR_UNEXPECTED, []);
+}
+
+function sinkAndSourceClosedWithPendingData(next)
+{
+ var t = new CopyTest("sinkAndSourceClosedWithPendingData", next);
+
+ t.addToSource(SEGMENT);
+ t.makeSourceReadable(SEGMENT.length);
+
+ t.closeSinkThenSource(Cr.NS_OK, Cr.NS_OK);
+
+ // not all data from source copied, plus sink notify received first, so error
+ t.expect(Cr.NS_ERROR_UNEXPECTED, []);
+}
+
+
+/*************
+ * UTILITIES *
+ *************/
+
+/** Returns the sum of the elements in arr. */
+function sum(arr)
+{
+ var sum = 0;
+ for (var i = 0, sz = arr.length; i < sz; i++)
+ sum += arr[i];
+ return sum;
+}
+
+/**
+ * Returns a constructor for an input or output stream callback that will wrap
+ * the one provided to it as an argument.
+ *
+ * @param wrapperCallback : (nsIInputStreamCallback | nsIOutputStreamCallback) : void
+ * the original callback object (not a function!) being wrapped
+ * @param name : string
+ * either "onInputStreamReady" if we're wrapping an input stream callback or
+ * "onOutputStreamReady" if we're wrapping an output stream callback
+ * @returns function(nsIInputStreamCallback | nsIOutputStreamCallback) : (nsIInputStreamCallback | nsIOutputStreamCallback)
+ * a constructor function which constructs a callback object (not function!)
+ * which, when called, first calls the original callback provided to it and
+ * then calls wrapperCallback
+ */
+function createStreamReadyInterceptor(wrapperCallback, name)
+{
+ return function StreamReadyInterceptor(callback)
+ {
+ this.wrappedCallback = callback;
+ this[name] = function streamReadyInterceptor(stream)
+ {
+ dumpn("*** StreamReadyInterceptor." + name);
+
+ try
+ {
+ dumpn("*** calling original " + name + "...");
+ callback[name](stream);
+ }
+ catch (e)
+ {
+ dumpn("!!! error running inner callback: " + e);
+ throw e;
+ }
+ finally
+ {
+ dumpn("*** calling wrapper " + name + "...");
+ wrapperCallback[name](stream);
+ }
+ }
+ };
+}
+
+/**
+ * Print out a banner with the given message, uppercased, for debugging
+ * purposes.
+ */
+function note(m)
+{
+ m = m.toUpperCase();
+ var asterisks = Array(m.length + 1 + 4).join("*");
+ dumpn(asterisks + "\n* " + m + " *\n" + asterisks);
+}
+
+
+/***********
+ * MOCKERY *
+ ***********/
+
+/*
+ * Blatantly violate abstractions in the name of testability. THIS IS NOT
+ * PUBLIC API! If you use any of these I will knowingly break your code by
+ * changing the names of variables and properties.
+ */
+var BinaryInputStream = function BIS(stream) { return stream; };
+var BinaryOutputStream = function BOS(stream) { return stream; };
+Response.SEGMENT_SIZE = SEGMENT.length;
+
+/**
+ * Roughly mocks an nsIPipe, presenting non-blocking input and output streams
+ * that appear to also be binary streams and whose readability and writability
+ * amounts are configurable. Only the methods used in this test have been
+ * implemented -- these aren't exact mocks (can't be, actually, because input
+ * streams have unscriptable methods).
+ *
+ * @param name : string
+ * a name for this pipe, used in debugging output
+ */
+function CustomPipe(name)
+{
+ var self = this;
+
+ /** Data read from input that's buffered until it can be written to output. */
+ this._data = [];
+
+ /**
+ * The status of this pipe, which is to say the error result the ends of this
+ * pipe will return when attempts are made to use them. This value is always
+ * an error result when copying has finished, because success codes are
+ * converted to NS_BASE_STREAM_CLOSED.
+ */
+ this._status = Cr.NS_OK;
+
+ /** The input end of this pipe. */
+ var input = this.inputStream =
+ {
+ /** A name for this stream, used in debugging output. */
+ name: name + " input",
+
+ /**
+ * The number of bytes of data available to be read from this pipe, or
+ * Infinity if any amount of data in this pipe is made readable as soon as
+ * it is written to the pipe output.
+ */
+ _readable: 0,
+
+ /**
+ * Data regarding a pending stream-ready callback on this, or null if no
+ * callback is currently waiting to be called.
+ */
+ _waiter: null,
+
+ /**
+ * The event currently dispatched to make a stream-ready callback, if any
+ * such callback is currently ready to be made and not already in
+ * progress, or null when no callback is waiting to happen.
+ */
+ _event: null,
+
+ /**
+ * A stream-ready constructor to wrap an existing callback to intercept
+ * stream-ready notifications, or null if notifications shouldn't be
+ * wrapped at all.
+ */
+ _streamReadyInterceptCreator: null,
+
+ /**
+ * Registers a stream-ready wrapper creator function so that a
+ * stream-ready callback made in the future can be wrapped.
+ */
+ interceptStreamReadyCallbacks: function(streamReadyInterceptCreator)
+ {
+ dumpn("*** [" + this.name + "].interceptStreamReadyCallbacks");
+
+ do_check_true(this._streamReadyInterceptCreator === null,
+ "intercepting twice");
+ this._streamReadyInterceptCreator = streamReadyInterceptCreator;
+ if (this._waiter)
+ {
+ this._waiter.callback =
+ new streamReadyInterceptCreator(this._waiter.callback);
+ }
+ },
+
+ /**
+ * Removes a previously-registered stream-ready wrapper creator function,
+ * also clearing any current wrapping.
+ */
+ removeStreamReadyInterceptor: function()
+ {
+ dumpn("*** [" + this.name + "].removeStreamReadyInterceptor()");
+
+ do_check_true(this._streamReadyInterceptCreator !== null,
+ "removing interceptor when none present?");
+ this._streamReadyInterceptCreator = null;
+ if (this._waiter)
+ this._waiter.callback = this._waiter.callback.wrappedCallback;
+ },
+
+ //
+ // see nsIAsyncInputStream.asyncWait
+ //
+ asyncWait: function asyncWait(callback, flags, requestedCount, target)
+ {
+ dumpn("*** [" + this.name + "].asyncWait");
+
+ do_check_true(callback && typeof callback !== "function");
+
+ var closureOnly =
+ (flags & Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY) !== 0;
+
+ do_check_true(this._waiter === null ||
+ (this._waiter.closureOnly && !closureOnly),
+ "asyncWait already called with a non-closure-only " +
+ "callback? unexpected!");
+
+ this._waiter =
+ {
+ callback:
+ this._streamReadyInterceptCreator
+ ? new this._streamReadyInterceptCreator(callback)
+ : callback,
+ closureOnly: closureOnly,
+ requestedCount: requestedCount,
+ eventTarget: target
+ };
+
+ if (!Components.isSuccessCode(self._status) ||
+ (!closureOnly && this._readable >= requestedCount &&
+ self._data.length >= requestedCount))
+ {
+ this._notify();
+ }
+ },
+
+ //
+ // see nsIAsyncInputStream.closeWithStatus
+ //
+ closeWithStatus: function closeWithStatus(status)
+ {
+ dumpn("*** [" + this.name + "].closeWithStatus" +
+ "(" + status + ")");
+
+ if (!Components.isSuccessCode(self._status))
+ {
+ dumpn("*** ignoring second closure of [input " + this.name + "] " +
+ "(status " + self._status + ")");
+ return;
+ }
+
+ if (Components.isSuccessCode(status))
+ status = Cr.NS_BASE_STREAM_CLOSED;
+
+ self._status = status;
+
+ if (this._waiter)
+ this._notify();
+ if (output._waiter)
+ output._notify();
+ },
+
+ //
+ // see nsIBinaryInputStream.readByteArray
+ //
+ readByteArray: function readByteArray(count)
+ {
+ dumpn("*** [" + this.name + "].readByteArray(" + count + ")");
+
+ if (self._data.length === 0)
+ {
+ throw Components.isSuccessCode(self._status)
+ ? Cr.NS_BASE_STREAM_WOULD_BLOCK
+ : self._status;
+ }
+
+ do_check_true(this._readable <= self._data.length ||
+ this._readable === Infinity,
+ "consistency check");
+
+ if (this._readable < count || self._data.length < count)
+ throw Cr.NS_BASE_STREAM_WOULD_BLOCK;
+ this._readable -= count;
+ return self._data.splice(0, count);
+ },
+
+ /**
+ * Makes the given number of additional bytes of data previously written
+ * to the pipe's output stream available for reading, triggering future
+ * notifications when required.
+ *
+ * @param count : uint
+ * the number of bytes of additional data to make available; must not be
+ * greater than the number of bytes already buffered but not made
+ * available by previous makeReadable calls
+ */
+ makeReadable: function makeReadable(count)
+ {
+ dumpn("*** [" + this.name + "].makeReadable(" + count + ")");
+
+ do_check_true(Components.isSuccessCode(self._status), "errant call");
+ do_check_true(this._readable + count <= self._data.length ||
+ this._readable === Infinity,
+ "increasing readable beyond written amount");
+
+ this._readable += count;
+
+ dumpn("readable: " + this._readable + ", data: " + self._data);
+
+ var waiter = this._waiter;
+ if (waiter !== null)
+ {
+ if (waiter.requestedCount <= this._readable && !waiter.closureOnly)
+ this._notify();
+ }
+ },
+
+ /**
+ * Disables the readability limit on this stream, meaning that as soon as
+ * *any* amount of data is written to output it becomes available from
+ * this stream and a stream-ready event is dispatched (if any stream-ready
+ * callback is currently set).
+ */
+ disableReadabilityLimit: function disableReadabilityLimit()
+ {
+ dumpn("*** [" + this.name + "].disableReadabilityLimit()");
+
+ this._readable = Infinity;
+ },
+
+ //
+ // see nsIInputStream.available
+ //
+ available: function available()
+ {
+ dumpn("*** [" + this.name + "].available()");
+
+ if (self._data.length === 0 && !Components.isSuccessCode(self._status))
+ throw self._status;
+
+ return Math.min(this._readable, self._data.length);
+ },
+
+ /**
+ * Dispatches a pending stream-ready event ahead of schedule, rather than
+ * waiting for it to be dispatched in response to normal writes. This is
+ * useful when writing to the output has completed, and we need to have
+ * read all data written to this stream. If the output isn't closed and
+ * the reading of data from this races ahead of the last write to output,
+ * we need a notification to know when everything that's been written has
+ * been read. This ordinarily might be supplied by closing output, but
+ * in some cases it's not desirable to close output, so this supplies an
+ * alternative method to get notified when the last write has occurred.
+ */
+ maybeNotifyFinally: function maybeNotifyFinally()
+ {
+ dumpn("*** [" + this.name + "].maybeNotifyFinally()");
+
+ do_check_true(this._waiter !== null, "must be waiting now");
+
+ if (self._data.length > 0)
+ {
+ dumpn("*** data still pending, normal notifications will signal " +
+ "completion");
+ return;
+ }
+
+ // No data waiting to be written, so notify. We could just close the
+ // stream, but that's less faithful to the server's behavior (it doesn't
+ // close the stream, and we're pretending to impersonate the server as
+ // much as we can here), so instead we're going to notify when no data
+ // can be read. The CopyTest has already been flagged as complete, so
+ // the stream listener will detect that this is a wrap-it-up notify and
+ // invoke the next test.
+ this._notify();
+ },
+
+ /**
+ * Dispatches an event to call a previously-registered stream-ready
+ * callback.
+ */
+ _notify: function _notify()
+ {
+ dumpn("*** [" + this.name + "]._notify()");
+
+ var waiter = this._waiter;
+ do_check_true(waiter !== null, "no waiter?");
+
+ if (this._event === null)
+ {
+ var event = this._event =
+ {
+ run: function run()
+ {
+ input._waiter = null;
+ input._event = null;
+ try
+ {
+ do_check_true(!Components.isSuccessCode(self._status) ||
+ input._readable >= waiter.requestedCount);
+ waiter.callback.onInputStreamReady(input);
+ }
+ catch (e)
+ {
+ do_throw("error calling onInputStreamReady: " + e);
+ }
+ }
+ };
+ waiter.eventTarget.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL);
+ }
+ },
+
+ QueryInterface: function QueryInterface(iid)
+ {
+ if (iid.equals(Ci.nsIAsyncInputStream) ||
+ iid.equals(Ci.nsIInputStream) ||
+ iid.equals(Ci.nsISupports))
+ {
+ return this;
+ }
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+ };
+
+ /** The output end of this pipe. */
+ var output = this.outputStream =
+ {
+ /** A name for this stream, used in debugging output. */
+ name: name + " output",
+
+ /**
+ * The number of bytes of data which may be written to this pipe without
+ * blocking.
+ */
+ _writable: 0,
+
+ /**
+ * The increments in which pending data should be written, rather than
+ * simply defaulting to the amount requested (which, given that
+ * input.asyncWait precisely respects the requestedCount argument, will
+ * ordinarily always be writable in that amount), as an array whose
+ * elements from start to finish are the number of bytes to write each
+ * time write() or writeByteArray() is subsequently called. The sum of
+ * the values in this array, if this array is not empty, is always equal
+ * to this._writable.
+ */
+ _writableAmounts: [],
+
+ /**
+ * Data regarding a pending stream-ready callback on this, or null if no
+ * callback is currently waiting to be called.
+ */
+ _waiter: null,
+
+ /**
+ * The event currently dispatched to make a stream-ready callback, if any
+ * such callback is currently ready to be made and not already in
+ * progress, or null when no callback is waiting to happen.
+ */
+ _event: null,
+
+ /**
+ * A stream-ready constructor to wrap an existing callback to intercept
+ * stream-ready notifications, or null if notifications shouldn't be
+ * wrapped at all.
+ */
+ _streamReadyInterceptCreator: null,
+
+ /**
+ * Registers a stream-ready wrapper creator function so that a
+ * stream-ready callback made in the future can be wrapped.
+ */
+ interceptStreamReadyCallbacks: function(streamReadyInterceptCreator)
+ {
+ dumpn("*** [" + this.name + "].interceptStreamReadyCallbacks");
+
+ do_check_true(this._streamReadyInterceptCreator !== null,
+ "intercepting onOutputStreamReady twice");
+ this._streamReadyInterceptCreator = streamReadyInterceptCreator;
+ if (this._waiter)
+ {
+ this._waiter.callback =
+ new streamReadyInterceptCreator(this._waiter.callback);
+ }
+ },
+
+ /**
+ * Removes a previously-registered stream-ready wrapper creator function,
+ * also clearing any current wrapping.
+ */
+ removeStreamReadyInterceptor: function()
+ {
+ dumpn("*** [" + this.name + "].removeStreamReadyInterceptor()");
+
+ do_check_true(this._streamReadyInterceptCreator !== null,
+ "removing interceptor when none present?");
+ this._streamReadyInterceptCreator = null;
+ if (this._waiter)
+ this._waiter.callback = this._waiter.callback.wrappedCallback;
+ },
+
+ //
+ // see nsIAsyncOutputStream.asyncWait
+ //
+ asyncWait: function asyncWait(callback, flags, requestedCount, target)
+ {
+ dumpn("*** [" + this.name + "].asyncWait");
+
+ do_check_true(callback && typeof callback !== "function");
+
+ var closureOnly =
+ (flags & Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY) !== 0;
+
+ do_check_true(this._waiter === null ||
+ (this._waiter.closureOnly && !closureOnly),
+ "asyncWait already called with a non-closure-only " +
+ "callback? unexpected!");
+
+ this._waiter =
+ {
+ callback:
+ this._streamReadyInterceptCreator
+ ? new this._streamReadyInterceptCreator(callback)
+ : callback,
+ closureOnly: closureOnly,
+ requestedCount: requestedCount,
+ eventTarget: target,
+ toString: function toString()
+ {
+ return "waiter(" + (closureOnly ? "closure only, " : "") +
+ "requestedCount: " + requestedCount + ", target: " +
+ target + ")";
+ }
+ };
+
+ if ((!closureOnly && this._writable >= requestedCount) ||
+ !Components.isSuccessCode(this.status))
+ {
+ this._notify();
+ }
+ },
+
+ //
+ // see nsIAsyncOutputStream.closeWithStatus
+ //
+ closeWithStatus: function closeWithStatus(status)
+ {
+ dumpn("*** [" + this.name + "].closeWithStatus(" + status + ")");
+
+ if (!Components.isSuccessCode(self._status))
+ {
+ dumpn("*** ignoring redundant closure of [input " + this.name + "] " +
+ "because it's already closed (status " + self._status + ")");
+ return;
+ }
+
+ if (Components.isSuccessCode(status))
+ status = Cr.NS_BASE_STREAM_CLOSED;
+
+ self._status = status;
+
+ if (input._waiter)
+ input._notify();
+ if (this._waiter)
+ this._notify();
+ },
+
+ //
+ // see nsIBinaryOutputStream.writeByteArray
+ //
+ writeByteArray: function writeByteArray(bytes, length)
+ {
+ dumpn("*** [" + this.name + "].writeByteArray" +
+ "([" + bytes + "], " + length + ")");
+
+ do_check_eq(bytes.length, length, "sanity");
+ if (!Components.isSuccessCode(self._status))
+ throw self._status;
+
+ do_check_eq(this._writableAmounts.length, 0,
+ "writeByteArray can't support specified-length writes");
+
+ if (this._writable < length)
+ throw Cr.NS_BASE_STREAM_WOULD_BLOCK;
+
+ self._data.push.apply(self._data, bytes);
+ this._writable -= length;
+
+ if (input._readable === Infinity && input._waiter &&
+ !input._waiter.closureOnly)
+ {
+ input._notify();
+ }
+ },
+
+ //
+ // see nsIOutputStream.write
+ //
+ write: function write(str, length)
+ {
+ dumpn("*** [" + this.name + "].write");
+
+ do_check_eq(str.length, length, "sanity");
+ if (!Components.isSuccessCode(self._status))
+ throw self._status;
+ if (this._writable === 0)
+ throw Cr.NS_BASE_STREAM_WOULD_BLOCK;
+
+ var actualWritten;
+ if (this._writableAmounts.length === 0)
+ {
+ actualWritten = Math.min(this._writable, length);
+ }
+ else
+ {
+ do_check_true(this._writable >= this._writableAmounts[0],
+ "writable amounts value greater than writable data?");
+ do_check_eq(this._writable, sum(this._writableAmounts),
+ "total writable amount not equal to sum of writable " +
+ "increments");
+ actualWritten = this._writableAmounts.shift();
+ }
+
+ var bytes = str.substring(0, actualWritten)
+ .split("")
+ .map(function(v) { return v.charCodeAt(0); });
+
+ self._data.push.apply(self._data, bytes);
+ this._writable -= actualWritten;
+
+ if (input._readable === Infinity && input._waiter &&
+ !input._waiter.closureOnly)
+ {
+ input._notify();
+ }
+
+ return actualWritten;
+ },
+
+ /**
+ * Increase the amount of data that can be written without blocking by the
+ * given number of bytes, triggering future notifications when required.
+ *
+ * @param count : uint
+ * the number of bytes of additional data to make writable
+ */
+ makeWritable: function makeWritable(count)
+ {
+ dumpn("*** [" + this.name + "].makeWritable(" + count + ")");
+
+ do_check_true(Components.isSuccessCode(self._status));
+
+ this._writable += count;
+
+ var waiter = this._waiter;
+ if (waiter && !waiter.closureOnly &&
+ waiter.requestedCount <= this._writable)
+ {
+ this._notify();
+ }
+ },
+
+ /**
+ * Increase the amount of data that can be written without blocking, but
+ * do so by specifying a number of bytes that will be written each time
+ * a write occurs, even as asyncWait notifications are initially triggered
+ * as usual. Thus, rather than writes eagerly writing everything possible
+ * at each step, attempts to write out data by segment devolve into a
+ * partial segment write, then another, and so on until the amount of data
+ * specified as permitted to be written, has been written.
+ *
+ * Note that the writeByteArray method is incompatible with the previous
+ * calling of this method, in that, until all increments provided to this
+ * method have been consumed, writeByteArray cannot be called. Once all
+ * increments have been consumed, writeByteArray may again be called.
+ *
+ * @param increments : [uint]
+ * an array whose elements are positive numbers of bytes to permit to be
+ * written each time write() is subsequently called on this, ignoring
+ * the total amount of writable space specified by the sum of all
+ * increments
+ */
+ makeWritableByIncrements: function makeWritableByIncrements(increments)
+ {
+ dumpn("*** [" + this.name + "].makeWritableByIncrements" +
+ "([" + increments.join(", ") + "])");
+
+ do_check_true(increments.length > 0, "bad increments");
+ do_check_true(increments.every(function(v) { return v > 0; }),
+ "zero increment?");
+
+ do_check_true(Components.isSuccessCode(self._status));
+
+ this._writable += sum(increments);
+ this._writableAmounts = increments;
+
+ var waiter = this._waiter;
+ if (waiter && !waiter.closureOnly &&
+ waiter.requestedCount <= this._writable)
+ {
+ this._notify();
+ }
+ },
+
+ /**
+ * Dispatches an event to call a previously-registered stream-ready
+ * callback.
+ */
+ _notify: function _notify()
+ {
+ dumpn("*** [" + this.name + "]._notify()");
+
+ var waiter = this._waiter;
+ do_check_true(waiter !== null, "no waiter?");
+
+ if (this._event === null)
+ {
+ var event = this._event =
+ {
+ run: function run()
+ {
+ output._waiter = null;
+ output._event = null;
+
+ try
+ {
+ waiter.callback.onOutputStreamReady(output);
+ }
+ catch (e)
+ {
+ do_throw("error calling onOutputStreamReady: " + e);
+ }
+ }
+ };
+ waiter.eventTarget.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL);
+ }
+ },
+
+ QueryInterface: function QueryInterface(iid)
+ {
+ if (iid.equals(Ci.nsIAsyncOutputStream) ||
+ iid.equals(Ci.nsIOutputStream) ||
+ iid.equals(Ci.nsISupports))
+ {
+ return this;
+ }
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+ };
+}
+
+/**
+ * Represents a sequence of interactions to perform with a copier, in a given
+ * order and at the desired time intervals.
+ *
+ * @param name : string
+ * test name, used in debugging output
+ */
+function CopyTest(name, next)
+{
+ /** Name used in debugging output. */
+ this.name = name;
+
+ /** A function called when the test completes. */
+ this._done = next;
+
+ var sourcePipe = new CustomPipe(name + "-source");
+
+ /** The source of data for the copier to copy. */
+ this._source = sourcePipe.inputStream;
+
+ /**
+ * The sink to which to write data which will appear in the copier's source.
+ */
+ this._copyableDataStream = sourcePipe.outputStream;
+
+ var sinkPipe = new CustomPipe(name + "-sink");
+
+ /** The sink to which the copier copies data. */
+ this._sink = sinkPipe.outputStream;
+
+ /** Input stream from which to read data the copier's written to its sink. */
+ this._copiedDataStream = sinkPipe.inputStream;
+
+ this._copiedDataStream.disableReadabilityLimit();
+
+ /**
+ * True if there's a callback waiting to read data written by the copier to
+ * its output, from the input end of the pipe representing the copier's sink.
+ */
+ this._waitingForData = false;
+
+ /**
+ * An array of the bytes of data expected to be written to output by the
+ * copier when this test runs.
+ */
+ this._expectedData = undefined;
+
+ /** Array of bytes of data received so far. */
+ this._receivedData = [];
+
+ /** The expected final status returned by the copier. */
+ this._expectedStatus = -1;
+
+ /** The actual final status returned by the copier. */
+ this._actualStatus = -1;
+
+ /** The most recent sequence of bytes written to output by the copier. */
+ this._lastQuantum = [];
+
+ /**
+ * True iff we've received the last quantum of data written to the sink by the
+ * copier.
+ */
+ this._allDataWritten = false;
+
+ /**
+ * True iff the copier has notified its associated stream listener of
+ * completion.
+ */
+ this._copyingFinished = false;
+
+ /** Index of the next task to execute while driving the copier. */
+ this._currentTask = 0;
+
+ /** Array containing all tasks to run. */
+ this._tasks = [];
+
+ /** The copier used by this test. */
+ this._copier =
+ new WriteThroughCopier(this._source, this._sink, this, null);
+
+ // Start watching for data written by the copier to the sink.
+ this._waitForWrittenData();
+}
+CopyTest.prototype =
+{
+ /**
+ * Adds the given array of bytes to data in the copier's source.
+ *
+ * @param bytes : [uint]
+ * array of bytes of data to add to the source for the copier
+ */
+ addToSource: function addToSource(bytes)
+ {
+ var self = this;
+ this._addToTasks(function addToSourceTask()
+ {
+ note("addToSourceTask");
+
+ try
+ {
+ self._copyableDataStream.makeWritable(bytes.length);
+ self._copyableDataStream.writeByteArray(bytes, bytes.length);
+ }
+ finally
+ {
+ self._stageNextTask();
+ }
+ });
+ },
+
+ /**
+ * Makes bytes of data previously added to the source available to be read by
+ * the copier.
+ *
+ * @param count : uint
+ * number of bytes to make available for reading
+ */
+ makeSourceReadable: function makeSourceReadable(count)
+ {
+ var self = this;
+ this._addToTasks(function makeSourceReadableTask()
+ {
+ note("makeSourceReadableTask");
+
+ self._source.makeReadable(count);
+ self._stageNextTask();
+ });
+ },
+
+ /**
+ * Increases available space in the sink by the given amount, waits for the
+ * given series of arrays of bytes to be written to sink by the copier, and
+ * causes execution to asynchronously continue to the next task when the last
+ * of those arrays of bytes is received.
+ *
+ * @param bytes : uint
+ * number of bytes of space to make available in the sink
+ * @param dataQuantums : [[uint]]
+ * array of byte arrays to expect to be written in sequence to the sink
+ */
+ makeSinkWritableAndWaitFor:
+ function makeSinkWritableAndWaitFor(bytes, dataQuantums)
+ {
+ var self = this;
+
+ do_check_eq(bytes,
+ dataQuantums.reduce(function(partial, current)
+ {
+ return partial + current.length;
+ }, 0),
+ "bytes/quantums mismatch");
+
+ function increaseSinkSpaceTask()
+ {
+ /* Now do the actual work to trigger the interceptor. */
+ self._sink.makeWritable(bytes);
+ }
+
+ this._waitForHelper("increaseSinkSpaceTask",
+ dataQuantums, increaseSinkSpaceTask);
+ },
+
+ /**
+ * Increases available space in the sink by the given amount, waits for the
+ * given series of arrays of bytes to be written to sink by the copier, and
+ * causes execution to asynchronously continue to the next task when the last
+ * of those arrays of bytes is received.
+ *
+ * @param bytes : uint
+ * number of bytes of space to make available in the sink
+ * @param dataQuantums : [[uint]]
+ * array of byte arrays to expect to be written in sequence to the sink
+ */
+ makeSinkWritableByIncrementsAndWaitFor:
+ function makeSinkWritableByIncrementsAndWaitFor(bytes, dataQuantums)
+ {
+ var self = this;
+
+ var desiredAmounts = dataQuantums.map(function(v) { return v.length; });
+ do_check_eq(bytes, sum(desiredAmounts), "bytes/quantums mismatch");
+
+ function increaseSinkSpaceByIncrementsTask()
+ {
+ /* Now do the actual work to trigger the interceptor incrementally. */
+ self._sink.makeWritableByIncrements(desiredAmounts);
+ }
+
+ this._waitForHelper("increaseSinkSpaceByIncrementsTask",
+ dataQuantums, increaseSinkSpaceByIncrementsTask);
+ },
+
+ /**
+ * Close the copier's source stream, then asynchronously continue to the next
+ * task.
+ *
+ * @param status : nsresult
+ * the status to provide when closing the copier's source stream
+ */
+ closeSource: function closeSource(status)
+ {
+ var self = this;
+
+ this._addToTasks(function closeSourceTask()
+ {
+ note("closeSourceTask");
+
+ self._source.closeWithStatus(status);
+ self._stageNextTask();
+ });
+ },
+
+ /**
+ * Close the copier's source stream, then wait for the given number of bytes
+ * and for the given series of arrays of bytes to be written to the sink, then
+ * asynchronously continue to the next task.
+ *
+ * @param status : nsresult
+ * the status to provide when closing the copier's source stream
+ * @param bytes : uint
+ * number of bytes of space to make available in the sink
+ * @param dataQuantums : [[uint]]
+ * array of byte arrays to expect to be written in sequence to the sink
+ */
+ closeSourceAndWaitFor:
+ function closeSourceAndWaitFor(status, bytes, dataQuantums)
+ {
+ var self = this;
+
+ do_check_eq(bytes, sum(dataQuantums.map(function(v) { return v.length; })),
+ "bytes/quantums mismatch");
+
+ function closeSourceAndWaitForTask()
+ {
+ self._sink.makeWritable(bytes);
+ self._copyableDataStream.closeWithStatus(status);
+ }
+
+ this._waitForHelper("closeSourceAndWaitForTask",
+ dataQuantums, closeSourceAndWaitForTask);
+ },
+
+ /**
+ * Closes the copier's sink stream, providing the given status, then
+ * asynchronously continue to the next task.
+ *
+ * @param status : nsresult
+ * the status to provide when closing the copier's sink stream
+ */
+ closeSink: function closeSink(status)
+ {
+ var self = this;
+ this._addToTasks(function closeSinkTask()
+ {
+ note("closeSinkTask");
+
+ self._sink.closeWithStatus(status);
+ self._stageNextTask();
+ });
+ },
+
+ /**
+ * Closes the copier's source stream, then immediately closes the copier's
+ * sink stream, then asynchronously continues to the next task.
+ *
+ * @param sourceStatus : nsresult
+ * the status to provide when closing the copier's source stream
+ * @param sinkStatus : nsresult
+ * the status to provide when closing the copier's sink stream
+ */
+ closeSourceThenSink: function closeSourceThenSink(sourceStatus, sinkStatus)
+ {
+ var self = this;
+ this._addToTasks(function closeSourceThenSinkTask()
+ {
+ note("closeSourceThenSinkTask");
+
+ self._source.closeWithStatus(sourceStatus);
+ self._sink.closeWithStatus(sinkStatus);
+ self._stageNextTask();
+ });
+ },
+
+ /**
+ * Closes the copier's sink stream, then immediately closes the copier's
+ * source stream, then asynchronously continues to the next task.
+ *
+ * @param sinkStatus : nsresult
+ * the status to provide when closing the copier's sink stream
+ * @param sourceStatus : nsresult
+ * the status to provide when closing the copier's source stream
+ */
+ closeSinkThenSource: function closeSinkThenSource(sinkStatus, sourceStatus)
+ {
+ var self = this;
+ this._addToTasks(function closeSinkThenSourceTask()
+ {
+ note("closeSinkThenSource");
+
+ self._sink.closeWithStatus(sinkStatus);
+ self._source.closeWithStatus(sourceStatus);
+ self._stageNextTask();
+ });
+ },
+
+ /**
+ * Indicates that the given status is expected to be returned when the stream
+ * listener for the copy indicates completion, that the expected data copied
+ * by the copier to sink are the concatenation of the arrays of bytes in
+ * receivedData, and kicks off the tasks in this test.
+ *
+ * @param expectedStatus : nsresult
+ * the status expected to be returned by the copier at completion
+ * @param receivedData : [[uint]]
+ * an array containing arrays of bytes whose concatenation constitutes the
+ * expected copied data
+ */
+ expect: function expect(expectedStatus, receivedData)
+ {
+ this._expectedStatus = expectedStatus;
+ this._expectedData = [];
+ for (var i = 0, sz = receivedData.length; i < sz; i++)
+ this._expectedData.push.apply(this._expectedData, receivedData[i]);
+
+ this._stageNextTask();
+ },
+
+ /**
+ * Sets up a stream interceptor that will verify that each piece of data
+ * written to the sink by the copier corresponds to the currently expected
+ * pieces of data, calls the trigger, then waits for those pieces of data to
+ * be received. Once all have been received, the interceptor is removed and
+ * the next task is asynchronously executed.
+ *
+ * @param name : string
+ * name of the task created by this, used in debugging output
+ * @param dataQuantums : [[uint]]
+ * array of expected arrays of bytes to be written to the sink by the copier
+ * @param trigger : function() : void
+ * function to call after setting up the interceptor to wait for
+ * notifications (which will be generated as a result of this function's
+ * actions)
+ */
+ _waitForHelper: function _waitForHelper(name, dataQuantums, trigger)
+ {
+ var self = this;
+ this._addToTasks(function waitForHelperTask()
+ {
+ note(name);
+
+ var quantumIndex = 0;
+
+ /*
+ * Intercept all data-available notifications so we can continue when all
+ * the ones we expect have been received.
+ */
+ var streamReadyCallback =
+ {
+ onInputStreamReady: function wrapperOnInputStreamReady(input)
+ {
+ dumpn("*** streamReadyCallback.onInputStreamReady" +
+ "(" + input.name + ")");
+
+ do_check_eq(this, streamReadyCallback, "sanity");
+
+ try
+ {
+ if (quantumIndex < dataQuantums.length)
+ {
+ var quantum = dataQuantums[quantumIndex++];
+ var sz = quantum.length;
+ do_check_eq(self._lastQuantum.length, sz,
+ "different quantum lengths");
+ for (var i = 0; i < sz; i++)
+ {
+ do_check_eq(self._lastQuantum[i], quantum[i],
+ "bad data at " + i);
+ }
+
+ dumpn("*** waiting to check remaining " +
+ (dataQuantums.length - quantumIndex) + " quantums...");
+ }
+ }
+ finally
+ {
+ if (quantumIndex === dataQuantums.length)
+ {
+ dumpn("*** data checks completed! next task...");
+ self._copiedDataStream.removeStreamReadyInterceptor();
+ self._stageNextTask();
+ }
+ }
+ }
+ };
+
+ var interceptor =
+ createStreamReadyInterceptor(streamReadyCallback, "onInputStreamReady");
+ self._copiedDataStream.interceptStreamReadyCallbacks(interceptor);
+
+ /* Do the deed. */
+ trigger();
+ });
+ },
+
+ /**
+ * Initiates asynchronous waiting for data written to the copier's sink to be
+ * available for reading from the input end of the sink's pipe. The callback
+ * stores the received data for comparison in the interceptor used in the
+ * callback added by _waitForHelper and signals test completion when it
+ * receives a zero-data-available notification (if the copier has notified
+ * that it is finished; otherwise allows execution to continue until that has
+ * occurred).
+ */
+ _waitForWrittenData: function _waitForWrittenData()
+ {
+ dumpn("*** _waitForWrittenData (" + this.name + ")");
+
+ var self = this;
+ var outputWrittenWatcher =
+ {
+ onInputStreamReady: function onInputStreamReady(input)
+ {
+ dumpn("*** outputWrittenWatcher.onInputStreamReady" +
+ "(" + input.name + ")");
+
+ if (self._allDataWritten)
+ {
+ do_throw("ruh-roh! why are we getting notified of more data " +
+ "after we should have received all of it?");
+ }
+
+ self._waitingForData = false;
+
+ try
+ {
+ var avail = input.available();
+ }
+ catch (e)
+ {
+ dumpn("*** available() threw! error: " + e);
+ if (self._completed)
+ {
+ dumpn("*** NB: this isn't a problem, because we've copied " +
+ "completely now, and this notify may have been expedited " +
+ "by maybeNotifyFinally such that we're being called when " +
+ "we can *guarantee* nothing is available any more");
+ }
+ avail = 0;
+ }
+
+ if (avail > 0)
+ {
+ var data = input.readByteArray(avail);
+ do_check_eq(data.length, avail,
+ "readByteArray returned wrong number of bytes?");
+ self._lastQuantum = data;
+ self._receivedData.push.apply(self._receivedData, data);
+ }
+
+ if (avail === 0)
+ {
+ dumpn("*** all data received!");
+
+ self._allDataWritten = true;
+
+ if (self._copyingFinished)
+ {
+ dumpn("*** copying already finished, continuing to next test");
+ self._testComplete();
+ }
+ else
+ {
+ dumpn("*** copying not finished, waiting for that to happen");
+ }
+
+ return;
+ }
+
+ self._waitForWrittenData();
+ }
+ };
+
+ this._copiedDataStream.asyncWait(outputWrittenWatcher, 0, 1,
+ gThreadManager.currentThread);
+ this._waitingForData = true;
+ },
+
+ /**
+ * Indicates this test is complete, does the final data-received and copy
+ * status comparisons, and calls the test-completion function provided when
+ * this test was first created.
+ */
+ _testComplete: function _testComplete()
+ {
+ dumpn("*** CopyTest(" + this.name + ") complete! " +
+ "On to the next test...");
+
+ try
+ {
+ do_check_true(this._allDataWritten, "expect all data written now!");
+ do_check_true(this._copyingFinished, "expect copying finished now!");
+
+ do_check_eq(this._actualStatus, this._expectedStatus,
+ "wrong final status");
+
+ var expected = this._expectedData, received = this._receivedData;
+ dumpn("received: [" + received + "], expected: [" + expected + "]");
+ do_check_eq(received.length, expected.length, "wrong data");
+ for (var i = 0, sz = expected.length; i < sz; i++)
+ do_check_eq(received[i], expected[i], "bad data at " + i);
+ }
+ catch (e)
+ {
+ dumpn("!!! ERROR PERFORMING FINAL " + this.name + " CHECKS! " + e);
+ throw e;
+ }
+ finally
+ {
+ dumpn("*** CopyTest(" + this.name + ") complete! " +
+ "Invoking test-completion callback...");
+ this._done();
+ }
+ },
+
+ /** Dispatches an event at this thread which will run the next task. */
+ _stageNextTask: function _stageNextTask()
+ {
+ dumpn("*** CopyTest(" + this.name + ")._stageNextTask()");
+
+ if (this._currentTask === this._tasks.length)
+ {
+ dumpn("*** CopyTest(" + this.name + ") tasks complete!");
+ return;
+ }
+
+ var task = this._tasks[this._currentTask++];
+ var self = this;
+ var event =
+ {
+ run: function run()
+ {
+ try
+ {
+ task();
+ }
+ catch (e)
+ {
+ do_throw("exception thrown running task: " + e);
+ }
+ }
+ };
+ gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ /**
+ * Adds the given function as a task to be run at a later time.
+ *
+ * @param task : function() : void
+ * the function to call as a task
+ */
+ _addToTasks: function _addToTasks(task)
+ {
+ this._tasks.push(task);
+ },
+
+ //
+ // see nsIRequestObserver.onStartRequest
+ //
+ onStartRequest: function onStartRequest(self, _)
+ {
+ dumpn("*** CopyTest.onStartRequest (" + self.name + ")");
+
+ do_check_true(_ === null);
+ do_check_eq(this._receivedData.length, 0);
+ do_check_eq(this._lastQuantum.length, 0);
+ },
+
+ //
+ // see nsIRequestObserver.onStopRequest
+ //
+ onStopRequest: function onStopRequest(self, _, status)
+ {
+ dumpn("*** CopyTest.onStopRequest (" + self.name + ", " + status + ")");
+
+ do_check_true(_ === null);
+ this._actualStatus = status;
+
+ this._copyingFinished = true;
+
+ if (this._allDataWritten)
+ {
+ dumpn("*** all data written, continuing with remaining tests...");
+ this._testComplete();
+ }
+ else
+ {
+ /*
+ * Everything's copied as far as the copier is concerned. However, there
+ * may be a backup transferring from the output end of the copy sink to
+ * the input end where we can actually verify that the expected data was
+ * written as expected, because that transfer occurs asynchronously. If
+ * we do final data-received checks now, we'll miss still-pending data.
+ * Therefore, to wrap up this copy test we still need to asynchronously
+ * wait on the input end of the sink until we hit end-of-stream or some
+ * error condition. Then we know we're done and can continue with the
+ * next test.
+ */
+ dumpn("*** not all data copied, waiting for that to happen...");
+
+ if (!this._waitingForData)
+ this._waitForWrittenData();
+
+ this._copiedDataStream.maybeNotifyFinally();
+ }
+ }
+};
diff --git a/netwerk/test/httpserver/test/test_basic_functionality.js b/netwerk/test/httpserver/test/test_basic_functionality.js
new file mode 100644
index 000000000..9151bed4b
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_basic_functionality.js
@@ -0,0 +1,176 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Basic functionality test, from the client programmer's POV.
+ */
+
+XPCOMUtils.defineLazyGetter(this, "port", function() {
+ return srv.identity.primaryPort;
+});
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test("http://localhost:" + port + "/objHandler",
+ null, start_objHandler, null),
+ new Test("http://localhost:" + port + "/functionHandler",
+ null, start_functionHandler, null),
+ new Test("http://localhost:" + port + "/nonexistent-path",
+ null, start_non_existent_path, null),
+ new Test("http://localhost:" + port + "/lotsOfHeaders",
+ null, start_lots_of_headers, null),
+ ];
+});
+
+var srv;
+
+function run_test()
+{
+ srv = createServer();
+
+ // base path
+ // XXX should actually test this works with a file by comparing streams!
+ var dirServ = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties);
+ var path = dirServ.get("CurProcD", Ci.nsILocalFile);
+ srv.registerDirectory("/", path);
+
+ // register a few test paths
+ srv.registerPathHandler("/objHandler", objHandler);
+ srv.registerPathHandler("/functionHandler", functionHandler);
+ srv.registerPathHandler("/lotsOfHeaders", lotsOfHeadersHandler);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+const HEADER_COUNT = 1000;
+
+// TEST DATA
+
+// common properties *always* appended by server
+// or invariants for every URL in paths
+function commonCheck(ch)
+{
+ do_check_true(ch.contentLength > -1);
+ do_check_eq(ch.getResponseHeader("connection"), "close");
+ do_check_false(ch.isNoStoreResponse());
+ do_check_false(ch.isPrivateResponse());
+}
+
+function start_objHandler(ch, cx)
+{
+ commonCheck(ch);
+
+ do_check_eq(ch.responseStatus, 200);
+ do_check_true(ch.requestSucceeded);
+ do_check_eq(ch.getResponseHeader("content-type"), "text/plain");
+ do_check_eq(ch.responseStatusText, "OK");
+
+ var reqMin = {}, reqMaj = {}, respMin = {}, respMaj = {};
+ ch.getRequestVersion(reqMaj, reqMin);
+ ch.getResponseVersion(respMaj, respMin);
+ do_check_true(reqMaj.value == respMaj.value &&
+ reqMin.value == respMin.value);
+}
+
+function start_functionHandler(ch, cx)
+{
+ commonCheck(ch);
+
+ do_check_eq(ch.responseStatus, 404);
+ do_check_false(ch.requestSucceeded);
+ do_check_eq(ch.getResponseHeader("foopy"), "quux-baz");
+ do_check_eq(ch.responseStatusText, "Page Not Found");
+
+ var reqMin = {}, reqMaj = {}, respMin = {}, respMaj = {};
+ ch.getRequestVersion(reqMaj, reqMin);
+ ch.getResponseVersion(respMaj, respMin);
+ do_check_true(reqMaj.value == 1 && reqMin.value == 1);
+ do_check_true(respMaj.value == 1 && respMin.value == 1);
+}
+
+function start_non_existent_path(ch, cx)
+{
+ commonCheck(ch);
+
+ do_check_eq(ch.responseStatus, 404);
+ do_check_false(ch.requestSucceeded);
+}
+
+function start_lots_of_headers(ch, cx)
+{
+ commonCheck(ch);
+
+ do_check_eq(ch.responseStatus, 200);
+ do_check_true(ch.requestSucceeded);
+
+ for (var i = 0; i < HEADER_COUNT; i++)
+ do_check_eq(ch.getResponseHeader("X-Header-" + i), "value " + i);
+}
+
+// PATH HANDLERS
+
+// /objHandler
+var objHandler =
+ {
+ handle: function(metadata, response)
+ {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+
+ var body = "Request (slightly reformatted):\n\n";
+ body += metadata.method + " " + metadata.path;
+
+ do_check_eq(metadata.port, port);
+
+ if (metadata.queryString)
+ body += "?" + metadata.queryString;
+
+ body += " HTTP/" + metadata.httpVersion + "\n";
+
+ var headEnum = metadata.headers;
+ while (headEnum.hasMoreElements())
+ {
+ var fieldName = headEnum.getNext()
+ .QueryInterface(Ci.nsISupportsString)
+ .data;
+ body += fieldName + ": " + metadata.getHeader(fieldName) + "\n";
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+ },
+ QueryInterface: function(id)
+ {
+ if (id.equals(Ci.nsISupports) || id.equals(Ci.nsIHttpRequestHandler))
+ return this;
+ throw Cr.NS_ERROR_NOINTERFACE;
+ }
+ };
+
+// /functionHandler
+function functionHandler(metadata, response)
+{
+ response.setStatusLine("1.1", 404, "Page Not Found");
+ response.setHeader("foopy", "quux-baz", false);
+
+ do_check_eq(metadata.port, port);
+ do_check_eq(metadata.host, "localhost");
+ do_check_eq(metadata.path.charAt(0), "/");
+
+ var body = "this is text\n";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+// /lotsOfHeaders
+function lotsOfHeadersHandler(request, response)
+{
+ response.setHeader("Content-Type", "text/plain", false);
+
+ for (var i = 0; i < HEADER_COUNT; i++)
+ response.setHeader("X-Header-" + i, "value " + i, false);
+}
diff --git a/netwerk/test/httpserver/test/test_body_length.js b/netwerk/test/httpserver/test/test_body_length.js
new file mode 100644
index 000000000..0fd2236d7
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_body_length.js
@@ -0,0 +1,64 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests that the Content-Length header in incoming requests is interpreted as
+ * a decimal number, even if it has the form (including leading zero) of an
+ * octal number.
+ */
+
+var srv;
+
+function run_test()
+{
+ srv = createServer();
+ srv.registerPathHandler("/content-length", contentLength);
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+const REQUEST_DATA = "12345678901234567";
+
+function contentLength(request, response)
+{
+ do_check_eq(request.method, "POST");
+ do_check_eq(request.getHeader("Content-Length"), "017");
+
+ var body = new ScriptableInputStream(request.bodyInputStream);
+
+ var avail;
+ var data = "";
+ while ((avail = body.available()) > 0)
+ data += body.read(avail);
+
+ do_check_eq(data, REQUEST_DATA);
+}
+
+/***************
+ * BEGIN TESTS *
+ ***************/
+
+XPCOMUtils.defineLazyGetter(this, 'tests', function() {
+ return [
+ new Test("http://localhost:" + srv.identity.primaryPort + "/content-length",
+ init_content_length),
+ ];
+});
+
+function init_content_length(ch)
+{
+ var content = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ content.data = REQUEST_DATA;
+
+ ch.QueryInterface(Ci.nsIUploadChannel)
+ .setUploadStream(content, "text/plain", REQUEST_DATA.length);
+
+ // Override the values implicitly set by setUploadStream above.
+ ch.requestMethod = "POST";
+ ch.setRequestHeader("Content-Length", "017", false); // 17 bytes, not 15
+}
diff --git a/netwerk/test/httpserver/test/test_byte_range.js b/netwerk/test/httpserver/test/test_byte_range.js
new file mode 100644
index 000000000..53d23e522
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_byte_range.js
@@ -0,0 +1,278 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// checks if a byte range request and non-byte range request retrieve the
+// correct data.
+
+var srv;
+XPCOMUtils.defineLazyGetter(this, "PREFIX", function() {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test(PREFIX + "/range.txt",
+ init_byterange, start_byterange, stop_byterange),
+ new Test(PREFIX + "/range.txt",
+ init_byterange2, start_byterange2),
+ new Test(PREFIX + "/range.txt",
+ init_byterange3, start_byterange3, stop_byterange3),
+ new Test(PREFIX + "/range.txt",
+ init_byterange4, start_byterange4),
+ new Test(PREFIX + "/range.txt",
+ init_byterange5, start_byterange5, stop_byterange5),
+ new Test(PREFIX + "/range.txt",
+ init_byterange6, start_byterange6, stop_byterange6),
+ new Test(PREFIX + "/range.txt",
+ init_byterange7, start_byterange7, stop_byterange7),
+ new Test(PREFIX + "/range.txt",
+ init_byterange8, start_byterange8, stop_byterange8),
+ new Test(PREFIX + "/range.txt",
+ init_byterange9, start_byterange9, stop_byterange9),
+ new Test(PREFIX + "/range.txt",
+ init_byterange10, start_byterange10),
+ new Test(PREFIX + "/range.txt",
+ init_byterange11, start_byterange11, stop_byterange11),
+ new Test(PREFIX + "/empty.txt",
+ null, start_byterange12, stop_byterange12),
+ new Test(PREFIX + "/headers.txt",
+ init_byterange13, start_byterange13, null),
+ new Test(PREFIX + "/range.txt",
+ null, start_normal, stop_normal)
+ ];
+});
+
+function run_test()
+{
+ srv = createServer();
+ var dir = do_get_file("data/ranges/");
+ srv.registerDirectory("/", dir);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+function start_normal(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 200);
+ do_check_eq(ch.getResponseHeader("Content-Length"), "21");
+ do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain");
+}
+
+function stop_normal(ch, cx, status, data)
+{
+ do_check_eq(data.length, 21);
+ do_check_eq(data[0], 0x54);
+ do_check_eq(data[20], 0x0a);
+}
+
+function init_byterange(ch)
+{
+ ch.setRequestHeader("Range", "bytes=10-", false);
+}
+
+function start_byterange(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 206);
+ do_check_eq(ch.getResponseHeader("Content-Length"), "11");
+ do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain");
+ do_check_eq(ch.getResponseHeader("Content-Range"), "bytes 10-20/21");
+}
+
+function stop_byterange(ch, cx, status, data)
+{
+ do_check_eq(data.length, 11);
+ do_check_eq(data[0], 0x64);
+ do_check_eq(data[10], 0x0a);
+}
+
+function init_byterange2(ch)
+{
+ ch.setRequestHeader("Range", "bytes=21-", false);
+}
+
+function start_byterange2(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 416);
+}
+
+function init_byterange3(ch)
+{
+ ch.setRequestHeader("Range", "bytes=10-15", false);
+}
+
+function start_byterange3(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 206);
+ do_check_eq(ch.getResponseHeader("Content-Length"), "6");
+ do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain");
+ do_check_eq(ch.getResponseHeader("Content-Range"), "bytes 10-15/21");
+}
+
+function stop_byterange3(ch, cx, status, data)
+{
+ do_check_eq(data.length, 6);
+ do_check_eq(data[0], 0x64);
+ do_check_eq(data[1], 0x20);
+ do_check_eq(data[2], 0x62);
+ do_check_eq(data[3], 0x65);
+ do_check_eq(data[4], 0x20);
+ do_check_eq(data[5], 0x73);
+}
+
+function init_byterange4(ch)
+{
+ ch.setRequestHeader("Range", "xbytes=21-", false);
+}
+
+function start_byterange4(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 400);
+}
+
+function init_byterange5(ch)
+{
+ ch.setRequestHeader("Range", "bytes=-5", false);
+}
+
+function start_byterange5(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 206);
+}
+
+function stop_byterange5(ch, cx, status, data)
+{
+ do_check_eq(data.length, 5);
+ do_check_eq(data[0], 0x65);
+ do_check_eq(data[1], 0x65);
+ do_check_eq(data[2], 0x6e);
+ do_check_eq(data[3], 0x2e);
+ do_check_eq(data[4], 0x0a);
+}
+
+function init_byterange6(ch)
+{
+ ch.setRequestHeader("Range", "bytes=15-12", false);
+}
+
+function start_byterange6(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 200);
+}
+
+function stop_byterange6(ch, cx, status, data)
+{
+ do_check_eq(data.length, 21);
+ do_check_eq(data[0], 0x54);
+ do_check_eq(data[20], 0x0a);
+}
+
+function init_byterange7(ch)
+{
+ ch.setRequestHeader("Range", "bytes=0-5", false);
+}
+
+function start_byterange7(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 206);
+ do_check_eq(ch.getResponseHeader("Content-Length"), "6");
+ do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain");
+ do_check_eq(ch.getResponseHeader("Content-Range"), "bytes 0-5/21");
+}
+
+function stop_byterange7(ch, cx, status, data)
+{
+ do_check_eq(data.length, 6);
+ do_check_eq(data[0], 0x54);
+ do_check_eq(data[1], 0x68);
+ do_check_eq(data[2], 0x69);
+ do_check_eq(data[3], 0x73);
+ do_check_eq(data[4], 0x20);
+ do_check_eq(data[5], 0x73);
+}
+
+function init_byterange8(ch)
+{
+ ch.setRequestHeader("Range", "bytes=20-21", false);
+}
+
+function start_byterange8(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 206);
+ do_check_eq(ch.getResponseHeader("Content-Range"), "bytes 20-20/21");
+}
+
+function stop_byterange8(ch, cx, status, data)
+{
+ do_check_eq(data.length, 1);
+ do_check_eq(data[0], 0x0a);
+}
+
+function init_byterange9(ch)
+{
+ ch.setRequestHeader("Range", "bytes=020-021", false);
+}
+
+function start_byterange9(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 206);
+}
+
+function stop_byterange9(ch, cx, status, data)
+{
+ do_check_eq(data.length, 1);
+ do_check_eq(data[0], 0x0a);
+}
+
+function init_byterange10(ch)
+{
+ ch.setRequestHeader("Range", "bytes=-", false);
+}
+
+function start_byterange10(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 400);
+}
+
+function init_byterange11(ch)
+{
+ ch.setRequestHeader("Range", "bytes=-500", false);
+}
+
+function start_byterange11(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 206);
+}
+
+function stop_byterange11(ch, cx, status, data)
+{
+ do_check_eq(data.length, 21);
+ do_check_eq(data[0], 0x54);
+ do_check_eq(data[20], 0x0a);
+}
+
+function start_byterange12(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 200);
+ do_check_eq(ch.getResponseHeader("Content-Length"), "0");
+}
+
+function stop_byterange12(ch, cx, status, data)
+{
+ do_check_eq(data.length, 0);
+}
+
+function init_byterange13(ch)
+{
+ ch.setRequestHeader("Range", "bytes=9999999-", false);
+}
+
+function start_byterange13(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 416);
+ do_check_eq(ch.getResponseHeader("X-SJS-Header"), "customized");
+}
diff --git a/netwerk/test/httpserver/test/test_cern_meta.js b/netwerk/test/httpserver/test/test_cern_meta.js
new file mode 100644
index 000000000..54062bc3e
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_cern_meta.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// exercises support for mod_cern_meta-style header/status line modification
+var srv;
+
+XPCOMUtils.defineLazyGetter(this, 'PREFIX', function() {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+XPCOMUtils.defineLazyGetter(this, 'tests', function() {
+ return [
+ new Test(PREFIX + "/test_both.html",
+ null, start_testBoth, null),
+ new Test(PREFIX + "/test_ctype_override.txt",
+ null, start_test_ctype_override_txt, null),
+ new Test(PREFIX + "/test_status_override.html",
+ null, start_test_status_override_html, null),
+ new Test(PREFIX + "/test_status_override_nodesc.txt",
+ null, start_test_status_override_nodesc_txt, null),
+ new Test(PREFIX + "/caret_test.txt^",
+ null, start_caret_test_txt_, null)
+ ];
+});
+
+function run_test()
+{
+ srv = createServer();
+
+ var cernDir = do_get_file("data/cern_meta/");
+ srv.registerDirectory("/", cernDir);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+
+// TEST DATA
+
+function start_testBoth(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 501);
+ do_check_eq(ch.responseStatusText, "Unimplemented");
+
+ do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain");
+}
+
+function start_test_ctype_override_txt(ch, cx)
+{
+ do_check_eq(ch.getResponseHeader("Content-Type"), "text/html");
+}
+
+function start_test_status_override_html(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 404);
+ do_check_eq(ch.responseStatusText, "Can't Find This");
+}
+
+function start_test_status_override_nodesc_txt(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 732);
+ do_check_eq(ch.responseStatusText, "");
+}
+
+function start_caret_test_txt_(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 500);
+ do_check_eq(ch.responseStatusText, "This Isn't A Server Error");
+
+ do_check_eq(ch.getResponseHeader("Foo-RFC"), "3092");
+ do_check_eq(ch.getResponseHeader("Shaving-Cream-Atom"), "Illudium Phosdex");
+}
diff --git a/netwerk/test/httpserver/test/test_default_index_handler.js b/netwerk/test/httpserver/test/test_default_index_handler.js
new file mode 100644
index 000000000..c2c1b4e73
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_default_index_handler.js
@@ -0,0 +1,290 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// checks for correct output with the default index handler, mostly to do
+// escaping checks -- highly dependent on the default index handler output
+// format
+
+var srv, dir, dirEntries;
+
+XPCOMUtils.defineLazyGetter(this, 'BASE_URL', function() {
+ return "http://localhost:" + srv.identity.primaryPort + "/";
+});
+
+function run_test()
+{
+ createTestDirectory();
+
+ srv = createServer();
+ srv.registerDirectory("/", dir);
+
+ var nameDir = do_get_file("data/name-scheme/");
+ srv.registerDirectory("/bar/", nameDir);
+
+ srv.start(-1);
+
+ function done()
+ {
+ do_test_pending();
+ destroyTestDirectory();
+ srv.stop(function() { do_test_finished(); });
+ }
+
+ runHttpTests(tests, done);
+}
+
+function createTestDirectory()
+{
+ dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("TmpD", Ci.nsIFile);
+ dir.append("index_handler_test_" + Math.random());
+ dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o744);
+
+ // populate with test directories, files, etc.
+ // Files must be in expected order of display on the index page!
+
+ var files = [];
+
+ makeFile("aa_directory", true, dir, files);
+ makeFile("Ba_directory", true, dir, files);
+ makeFile("bb_directory", true, dir, files);
+ makeFile("foo", true, dir, files);
+ makeFile("a_file", false, dir, files);
+ makeFile("B_file", false, dir, files);
+ makeFile("za'z", false, dir, files);
+ makeFile("zb&z", false, dir, files);
+ makeFile("zc<q", false, dir, files);
+ makeFile('zd"q', false, dir, files);
+ makeFile("ze%g", false, dir, files);
+ makeFile("zf%200h", false, dir, files);
+ makeFile("zg>m", false, dir, files);
+
+ dirEntries = [files];
+
+ var subdir = dir.clone();
+ subdir.append("foo");
+
+ files = [];
+
+ makeFile("aa_dir", true, subdir, files);
+ makeFile("b_dir", true, subdir, files);
+ makeFile("AA_file.txt", false, subdir, files);
+ makeFile("test.txt", false, subdir, files);
+
+ dirEntries.push(files);
+}
+
+function destroyTestDirectory()
+{
+ dir.remove(true);
+}
+
+
+/*************
+ * UTILITIES *
+ *************/
+
+/** Verifies data in bytes for the trailing-caret path above. */
+function hiddenDataCheck(bytes, uri, path)
+{
+ var data = String.fromCharCode.apply(null, bytes);
+
+ var parser = Cc["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Ci.nsIDOMParser);
+
+ // Note: the index format isn't XML -- it's actually HTML -- but we require
+ // the index format also be valid XML, albeit XML without namespaces,
+ // XML declarations, etc. Doing this simplifies output checking.
+ try
+ {
+ var doc = parser.parseFromString(data, "application/xml");
+ }
+ catch (e)
+ {
+ do_throw("document failed to parse as XML");
+ }
+
+ // See all the .QueryInterface()s and .item()s happening here? That's because
+ // xpcshell sucks and doesn't have classinfo, so no automatic interface
+ // flattening or array-style access to items in NodeLists. Suck.
+
+ var body = doc.documentElement.getElementsByTagName("body");
+ do_check_eq(body.length, 1);
+ body = body.item(0);
+
+ // header
+ var header = body.QueryInterface(Ci.nsIDOMElement)
+ .getElementsByTagName("h1");
+ do_check_eq(header.length, 1);
+
+ do_check_eq(header.item(0).QueryInterface(Ci.nsIDOMNode).textContent, path);
+
+ // files
+ var lst = body.getElementsByTagName("ol");
+ do_check_eq(lst.length, 1);
+ var items = lst.item(0).QueryInterface(Ci.nsIDOMElement)
+ .getElementsByTagName("li");
+
+ var ios = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+
+ var top = ios.newURI(uri, null, null);
+
+ // N.B. No ERROR_IF_SEE_THIS.txt^ file!
+ var dirEntries = [{name: "file.txt", isDirectory: false},
+ {name: "SHOULD_SEE_THIS.txt^", isDirectory: false}];
+
+ for (var i = 0; i < items.length; i++)
+ {
+ var link = items.item(i)
+ .childNodes
+ .item(0)
+ .QueryInterface(Ci.nsIDOMNode)
+ .QueryInterface(Ci.nsIDOMElement);
+ var f = dirEntries[i];
+
+ var sep = f.isDirectory ? "/" : "";
+
+ do_check_eq(link.textContent, f.name + sep);
+
+ uri = ios.newURI(link.getAttribute("href"), null, top);
+ do_check_eq(decodeURIComponent(uri.path), path + f.name + sep);
+ }
+}
+
+/**
+ * Verifies data in bytes (an array of bytes) represents an index page for the
+ * given URI and path, which should be a page listing the given directory
+ * entries, in order.
+ *
+ * @param bytes
+ * array of bytes representing the index page's contents
+ * @param uri
+ * string which is the URI of the index page
+ * @param path
+ * the path portion of uri
+ * @param dirEntries
+ * sorted (in the manner the directory entries should be sorted) array of
+ * objects, each of which has a name property (whose value is the file's name,
+ * without / if it's a directory) and an isDirectory property (with expected
+ * value)
+ */
+function dataCheck(bytes, uri, path, dirEntries)
+{
+ var data = String.fromCharCode.apply(null, bytes);
+
+ var parser = Cc["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Ci.nsIDOMParser);
+
+ // Note: the index format isn't XML -- it's actually HTML -- but we require
+ // the index format also be valid XML, albeit XML without namespaces,
+ // XML declarations, etc. Doing this simplifies output checking.
+ try
+ {
+ var doc = parser.parseFromString(data, "application/xml");
+ }
+ catch (e)
+ {
+ do_throw("document failed to parse as XML");
+ }
+
+ // See all the .QueryInterface()s and .item()s happening here? That's because
+ // xpcshell sucks and doesn't have classinfo, so no automatic interface
+ // flattening or array-style access to items in NodeLists. Suck.
+
+ var body = doc.documentElement.getElementsByTagName("body");
+ do_check_eq(body.length, 1);
+ body = body.item(0);
+
+ // header
+ var header = body.QueryInterface(Ci.nsIDOMElement)
+ .getElementsByTagName("h1");
+ do_check_eq(header.length, 1);
+
+ do_check_eq(header.item(0).QueryInterface(Ci.nsIDOMNode).textContent, path);
+
+ // files
+ var lst = body.getElementsByTagName("ol");
+ do_check_eq(lst.length, 1);
+ var items = lst.item(0).QueryInterface(Ci.nsIDOMElement)
+ .getElementsByTagName("li");
+
+ var ios = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+
+ var dirURI = ios.newURI(uri, null, null);
+
+ for (var i = 0; i < items.length; i++)
+ {
+ var link = items.item(i)
+ .childNodes
+ .item(0)
+ .QueryInterface(Ci.nsIDOMNode)
+ .QueryInterface(Ci.nsIDOMElement);
+ var f = dirEntries[i];
+
+ var sep = f.isDirectory ? "/" : "";
+
+ do_check_eq(link.textContent, f.name + sep);
+
+ uri = ios.newURI(link.getAttribute("href"), null, top);
+ do_check_eq(decodeURIComponent(uri.path), path + f.name + sep);
+ }
+}
+
+/**
+ * Create a file/directory with the given name underneath parentDir, and
+ * append an object with name/isDirectory properties to lst corresponding
+ * to it if the file/directory could be created.
+ */
+function makeFile(name, isDirectory, parentDir, lst)
+{
+ var type = Ci.nsIFile[isDirectory ? "DIRECTORY_TYPE" : "NORMAL_FILE_TYPE"];
+ var file = parentDir.clone();
+
+ try
+ {
+ file.append(name);
+ file.create(type, 0o755);
+ lst.push({name: name, isDirectory: isDirectory});
+ }
+ catch (e) { /* OS probably doesn't like file name, skip */ }
+}
+
+/*********
+ * TESTS *
+ *********/
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test(BASE_URL, null, start, stopRootDirectory),
+ new Test(BASE_URL + "foo/", null, start, stopFooDirectory),
+ new Test(BASE_URL + "bar/folder^/", null, start, stopTrailingCaretDirectory),
+ ];
+});
+
+// check top-level directory listing
+function start(ch)
+{
+ do_check_eq(ch.getResponseHeader("Content-Type"), "text/html;charset=utf-8");
+}
+function stopRootDirectory(ch, cx, status, data)
+{
+ dataCheck(data, BASE_URL, "/", dirEntries[0]);
+}
+
+// check non-top-level, too
+function stopFooDirectory(ch, cx, status, data)
+{
+ dataCheck(data, BASE_URL + "foo/", "/foo/", dirEntries[1]);
+}
+
+// trailing-caret leaf with hidden files
+function stopTrailingCaretDirectory(ch, cx, status, data)
+{
+ hiddenDataCheck(data, BASE_URL + "bar/folder^/", "/bar/folder^/");
+}
diff --git a/netwerk/test/httpserver/test/test_empty_body.js b/netwerk/test/httpserver/test/test_empty_body.js
new file mode 100644
index 000000000..85cc3d345
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_empty_body.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// in its original incarnation, the server didn't like empty response-bodies;
+// see the comment in _end for details
+
+var srv;
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test("http://localhost:" + srv.identity.primaryPort + "/empty-body-unwritten",
+ null, ensureEmpty, null),
+ new Test("http://localhost:" + srv.identity.primaryPort + "/empty-body-written",
+ null, ensureEmpty, null),
+ ];
+});
+
+function run_test()
+{
+ srv = createServer();
+
+ // register a few test paths
+ srv.registerPathHandler("/empty-body-unwritten", emptyBodyUnwritten);
+ srv.registerPathHandler("/empty-body-written", emptyBodyWritten);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+// TEST DATA
+
+function ensureEmpty(ch, cx)
+{
+ do_check_true(ch.contentLength == 0);
+}
+
+// PATH HANDLERS
+
+// /empty-body-unwritten
+function emptyBodyUnwritten(metadata, response)
+{
+ response.setStatusLine("1.1", 200, "OK");
+}
+
+// /empty-body-written
+function emptyBodyWritten(metadata, response)
+{
+ response.setStatusLine("1.1", 200, "OK");
+ var body = "";
+ response.bodyOutputStream.write(body, body.length);
+}
diff --git a/netwerk/test/httpserver/test/test_errorhandler_exception.js b/netwerk/test/httpserver/test/test_errorhandler_exception.js
new file mode 100644
index 000000000..c70dd1f11
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_errorhandler_exception.js
@@ -0,0 +1,84 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Request handlers may throw exceptions, and those exception should be caught
+// by the server and converted into the proper error codes.
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test("http://localhost:" + srv.identity.primaryPort + "/throws/exception",
+ null, start_throws_exception, succeeded),
+ new Test("http://localhost:" + srv.identity.primaryPort +
+ "/this/file/does/not/exist/and/404s",
+ null, start_nonexistent_404_fails_so_400, succeeded),
+ new Test("http://localhost:" + srv.identity.primaryPort +
+ "/attempts/404/fails/so/400/fails/so/500s",
+ register400Handler, start_multiple_exceptions_500, succeeded),
+ ];
+});
+
+var srv;
+
+function run_test()
+{
+ srv = createServer();
+
+ srv.registerErrorHandler(404, throwsException);
+ srv.registerPathHandler("/throws/exception", throwsException);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+
+// TEST DATA
+
+function checkStatusLine(channel, httpMaxVer, httpMinVer, httpCode, statusText)
+{
+ do_check_eq(channel.responseStatus, httpCode);
+ do_check_eq(channel.responseStatusText, statusText);
+
+ var respMaj = {}, respMin = {};
+ channel.getResponseVersion(respMaj, respMin);
+ do_check_eq(respMaj.value, httpMaxVer);
+ do_check_eq(respMin.value, httpMinVer);
+}
+
+function start_throws_exception(ch, cx)
+{
+ checkStatusLine(ch, 1, 1, 500, "Internal Server Error");
+}
+
+function start_nonexistent_404_fails_so_400(ch, cx)
+{
+ checkStatusLine(ch, 1, 1, 400, "Bad Request");
+}
+
+function start_multiple_exceptions_500(ch, cx)
+{
+ checkStatusLine(ch, 1, 1, 500, "Internal Server Error");
+}
+
+function succeeded(ch, cx, status, data)
+{
+ do_check_true(Components.isSuccessCode(status));
+}
+
+function register400Handler(ch)
+{
+ srv.registerErrorHandler(400, throwsException);
+}
+
+
+// PATH HANDLERS
+
+// /throws/exception (and also a 404 and 400 error handler)
+function throwsException(metadata, response)
+{
+ throw "this shouldn't cause an exit...";
+ do_throw("Not reached!");
+}
diff --git a/netwerk/test/httpserver/test/test_header_array.js b/netwerk/test/httpserver/test/test_header_array.js
new file mode 100644
index 000000000..2933f9aa6
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_header_array.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// test that special headers are sent as an array of headers with the same name
+
+var srv;
+
+function run_test()
+{
+ srv;
+
+ srv = createServer();
+ srv.registerPathHandler("/path-handler", pathHandler);
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+
+/************
+ * HANDLERS *
+ ************/
+
+function pathHandler(request, response)
+{
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ response.setHeader("Proxy-Authenticate", "First line 1", true);
+ response.setHeader("Proxy-Authenticate", "Second line 1", true);
+ response.setHeader("Proxy-Authenticate", "Third line 1", true);
+
+ response.setHeader("WWW-Authenticate", "Not merged line 1", false);
+ response.setHeader("WWW-Authenticate", "Not merged line 2", true);
+
+ response.setHeader("WWW-Authenticate", "First line 2", false);
+ response.setHeader("WWW-Authenticate", "Second line 2", true);
+ response.setHeader("WWW-Authenticate", "Third line 2", true);
+
+ response.setHeader("X-Single-Header-Merge", "Single 1", true);
+ response.setHeader("X-Single-Header-Merge", "Single 2", true);
+}
+
+/***************
+ * BEGIN TESTS *
+ ***************/
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test("http://localhost:" + srv.identity.primaryPort + "/path-handler",
+ null, check)
+ ];
+});
+
+function check(ch, cx)
+{
+ var headerValue;
+
+ headerValue = ch.getResponseHeader("Proxy-Authenticate");
+ do_check_eq(headerValue, "First line 1\nSecond line 1\nThird line 1");
+ headerValue = ch.getResponseHeader("WWW-Authenticate");
+ do_check_eq(headerValue, "First line 2\nSecond line 2\nThird line 2");
+ headerValue = ch.getResponseHeader("X-Single-Header-Merge");
+ do_check_eq(headerValue, "Single 1,Single 2");
+}
diff --git a/netwerk/test/httpserver/test/test_headers.js b/netwerk/test/httpserver/test/test_headers.js
new file mode 100644
index 000000000..74314a966
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_headers.js
@@ -0,0 +1,189 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// tests for header storage in httpd.js; nsHttpHeaders is an *internal* data
+// structure and is not to be used directly outside of httpd.js itself except
+// for testing purposes
+
+
+/**
+ * Ensures that a fieldname-fieldvalue combination is a valid header.
+ *
+ * @param fieldName
+ * the name of the header
+ * @param fieldValue
+ * the value of the header
+ * @param headers
+ * an nsHttpHeaders object to use to check validity
+ */
+function assertValidHeader(fieldName, fieldValue, headers)
+{
+ try
+ {
+ headers.setHeader(fieldName, fieldValue, false);
+ }
+ catch (e)
+ {
+ do_throw("Unexpected exception thrown: " + e);
+ }
+}
+
+/**
+ * Ensures that a fieldname-fieldvalue combination is not a valid header.
+ *
+ * @param fieldName
+ * the name of the header
+ * @param fieldValue
+ * the value of the header
+ * @param headers
+ * an nsHttpHeaders object to use to check validity
+ */
+function assertInvalidHeader(fieldName, fieldValue, headers)
+{
+ try
+ {
+ headers.setHeader(fieldName, fieldValue, false);
+ throw "Setting (" + fieldName + ", " +
+ fieldValue + ") as header succeeded!";
+ }
+ catch (e)
+ {
+ if (e !== Cr.NS_ERROR_INVALID_ARG)
+ do_throw("Unexpected exception thrown: " + e);
+ }
+}
+
+
+function run_test()
+{
+ testHeaderValidity();
+ testGetHeader();
+ testHeaderEnumerator();
+ testHasHeader();
+}
+
+function testHeaderValidity()
+{
+ var headers = new nsHttpHeaders();
+
+ assertInvalidHeader("f o", "bar", headers);
+ assertInvalidHeader("f\0n", "bar", headers);
+ assertInvalidHeader("foo:", "bar", headers);
+ assertInvalidHeader("f\\o", "bar", headers);
+ assertInvalidHeader("@xml", "bar", headers);
+ assertInvalidHeader("fiz(", "bar", headers);
+ assertInvalidHeader("HTTP/1.1", "bar", headers);
+ assertInvalidHeader("b\"b", "bar", headers);
+ assertInvalidHeader("ascsd\t", "bar", headers);
+ assertInvalidHeader("{fds", "bar", headers);
+ assertInvalidHeader("baz?", "bar", headers);
+ assertInvalidHeader("a\\b\\c", "bar", headers);
+ assertInvalidHeader("\0x7F", "bar", headers);
+ assertInvalidHeader("\0x1F", "bar", headers);
+ assertInvalidHeader("f\n", "bar", headers);
+ assertInvalidHeader("foo", "b\nar", headers);
+ assertInvalidHeader("foo", "b\rar", headers);
+ assertInvalidHeader("foo", "b\0", headers);
+
+ // request splitting, fwiw -- we're actually immune to this type of attack so
+ // long as we don't implement persistent connections
+ assertInvalidHeader("f\r\nGET /badness HTTP/1.1\r\nFoo", "bar", headers);
+
+ assertValidHeader("f'", "baz", headers);
+ assertValidHeader("f`", "baz", headers);
+ assertValidHeader("f.", "baz", headers);
+ assertValidHeader("f---", "baz", headers);
+ assertValidHeader("---", "baz", headers);
+ assertValidHeader("~~~", "baz", headers);
+ assertValidHeader("~~~", "b\r\n bar", headers);
+ assertValidHeader("~~~", "b\r\n\tbar", headers);
+}
+
+function testGetHeader()
+{
+ var headers = new nsHttpHeaders();
+
+ headers.setHeader("Content-Type", "text/html", false);
+ var c = headers.getHeader("content-type");
+ do_check_eq(c, "text/html");
+
+ headers.setHeader("test", "FOO", false);
+ var c = headers.getHeader("test");
+ do_check_eq(c, "FOO");
+
+ try
+ {
+ headers.getHeader(":");
+ throw "Failed to throw for invalid header";
+ }
+ catch (e)
+ {
+ if (e !== Cr.NS_ERROR_INVALID_ARG)
+ do_throw("headers.getHeader(':') must throw invalid arg");
+ }
+
+ try
+ {
+ headers.getHeader("valid");
+ throw 'header doesn\'t exist';
+ }
+ catch (e)
+ {
+ if (e !== Cr.NS_ERROR_NOT_AVAILABLE)
+ do_throw("shouldn't be a header named 'valid' in headers!");
+ }
+}
+
+function testHeaderEnumerator()
+{
+ var headers = new nsHttpHeaders();
+
+ var heads =
+ {
+ "foo": "17",
+ "baz": "two six niner",
+ "decaf": "class Program { int .7; int main(){ .7 = 5; return 7 - .7; } }"
+ };
+
+ for (var i in heads)
+ headers.setHeader(i, heads[i], false);
+
+ var en = headers.enumerator;
+ while (en.hasMoreElements())
+ {
+ var it = en.getNext().QueryInterface(Ci.nsISupportsString).data;
+ do_check_true(it.toLowerCase() in heads);
+ delete heads[it.toLowerCase()];
+ }
+
+ for (var i in heads)
+ do_throw("still have properties in heads!?!?");
+
+}
+
+function testHasHeader()
+{
+ var headers = new nsHttpHeaders();
+
+ headers.setHeader("foo", "bar", false);
+ do_check_true(headers.hasHeader("foo"));
+ do_check_true(headers.hasHeader("fOo"));
+ do_check_false(headers.hasHeader("not-there"));
+
+ headers.setHeader("f`'~", "bar", false);
+ do_check_true(headers.hasHeader("F`'~"));
+
+ try
+ {
+ headers.hasHeader(":");
+ throw "failed to throw";
+ }
+ catch (e)
+ {
+ if (e !== Cr.NS_ERROR_INVALID_ARG)
+ do_throw(".hasHeader for an invalid name should throw");
+ }
+}
diff --git a/netwerk/test/httpserver/test/test_host.js b/netwerk/test/httpserver/test/test_host.js
new file mode 100644
index 000000000..503a04fef
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_host.js
@@ -0,0 +1,666 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that the scheme, host, and port of the server are correctly recorded
+ * and used in HTTP requests and responses.
+ */
+
+const PORT = 4444;
+const FAKE_PORT_ONE = 8888;
+const FAKE_PORT_TWO = 8889;
+
+var srv, id;
+
+function run_test()
+{
+ dumpn("*** run_test");
+
+ srv = createServer();
+
+ srv.registerPathHandler("/http/1.0-request", http10Request);
+ srv.registerPathHandler("/http/1.1-good-host", http11goodHost);
+ srv.registerPathHandler("/http/1.1-good-host-wacky-port",
+ http11goodHostWackyPort);
+ srv.registerPathHandler("/http/1.1-ip-host", http11ipHost);
+
+ srv.start(FAKE_PORT_ONE);
+
+ id = srv.identity;
+
+ // The default location is http://localhost:PORT, where PORT is whatever you
+ // provided when you started the server. http://127.0.0.1:PORT is also part
+ // of the default set of locations.
+ do_check_eq(id.primaryScheme, "http");
+ do_check_eq(id.primaryHost, "localhost");
+ do_check_eq(id.primaryPort, FAKE_PORT_ONE);
+ do_check_true(id.has("http", "localhost", FAKE_PORT_ONE));
+ do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ // This should be a nop.
+ id.add("http", "localhost", FAKE_PORT_ONE);
+ do_check_eq(id.primaryScheme, "http");
+ do_check_eq(id.primaryHost, "localhost");
+ do_check_eq(id.primaryPort, FAKE_PORT_ONE);
+ do_check_true(id.has("http", "localhost", FAKE_PORT_ONE));
+ do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ // Change the primary location and make sure all the getters work correctly.
+ id.setPrimary("http", "127.0.0.1", FAKE_PORT_ONE);
+ do_check_eq(id.primaryScheme, "http");
+ do_check_eq(id.primaryHost, "127.0.0.1");
+ do_check_eq(id.primaryPort, FAKE_PORT_ONE);
+ do_check_true(id.has("http", "localhost", FAKE_PORT_ONE));
+ do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ // Okay, now remove the primary location -- we fall back to the original
+ // location.
+ id.remove("http", "127.0.0.1", FAKE_PORT_ONE);
+ do_check_eq(id.primaryScheme, "http");
+ do_check_eq(id.primaryHost, "localhost");
+ do_check_eq(id.primaryPort, FAKE_PORT_ONE);
+ do_check_true(id.has("http", "localhost", FAKE_PORT_ONE));
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ // You can't remove every location -- try this and the original default
+ // location will be silently readded.
+ id.remove("http", "localhost", FAKE_PORT_ONE);
+ do_check_eq(id.primaryScheme, "http");
+ do_check_eq(id.primaryHost, "localhost");
+ do_check_eq(id.primaryPort, FAKE_PORT_ONE);
+ do_check_true(id.has("http", "localhost", FAKE_PORT_ONE));
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ // Okay, now that we've exercised that behavior, shut down the server and
+ // restart it on the correct port, to exercise port-changing behaviors at
+ // server start and stop.
+ do_test_pending();
+ srv.stop(function()
+ {
+ try
+ {
+ do_test_pending();
+ run_test_2();
+ }
+ finally
+ {
+ do_test_finished();
+ }
+ });
+}
+
+function run_test_2()
+{
+ dumpn("*** run_test_2");
+
+ do_test_finished();
+
+ // Our primary location is gone because it was dependent on the port on which
+ // the server was running.
+ checkPrimariesThrow(id);
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+ do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+
+ srv.start(FAKE_PORT_TWO);
+
+ // We should have picked up http://localhost:8889 as our primary location now
+ // that we've restarted.
+ do_check_eq(id.primaryScheme, "http");
+ do_check_eq(id.primaryHost, "localhost", FAKE_PORT_TWO);
+ do_check_eq(id.primaryPort, FAKE_PORT_TWO);
+ do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+ do_check_true(id.has("http", "localhost", FAKE_PORT_TWO));
+ do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+
+ // Now we're going to see what happens when we shut down with a primary
+ // location that wasn't a default. That location should persist, and the
+ // default we remove should still not be present.
+ id.setPrimary("http", "example.com", FAKE_PORT_TWO);
+ do_check_true(id.has("http", "example.com", FAKE_PORT_TWO));
+ do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+ do_check_true(id.has("http", "localhost", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+ do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+
+ id.remove("http", "localhost", FAKE_PORT_TWO);
+ do_check_true(id.has("http", "example.com", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "localhost", FAKE_PORT_TWO));
+ do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ id.remove("http", "127.0.0.1", FAKE_PORT_TWO);
+ do_check_true(id.has("http", "example.com", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "localhost", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ do_test_pending();
+ srv.stop(function()
+ {
+ try
+ {
+ do_test_pending();
+ run_test_3();
+ }
+ finally
+ {
+ do_test_finished();
+ }
+ });
+}
+
+function run_test_3()
+{
+ dumpn("*** run_test_3");
+
+ do_test_finished();
+
+ // Only the default added location disappears; any others stay around,
+ // possibly as the primary location. We may have removed the default primary
+ // location, but the one we set manually should persist here.
+ do_check_eq(id.primaryScheme, "http");
+ do_check_eq(id.primaryHost, "example.com");
+ do_check_eq(id.primaryPort, FAKE_PORT_TWO);
+ do_check_true(id.has("http", "example.com", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "localhost", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+ srv.start(PORT);
+
+ // Starting always adds HTTP entries for 127.0.0.1:port and localhost:port.
+ do_check_true(id.has("http", "example.com", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "localhost", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+ do_check_true(id.has("http", "localhost", PORT));
+ do_check_true(id.has("http", "127.0.0.1", PORT));
+
+ // Remove the primary location we'd left set from last time.
+ id.remove("http", "example.com", FAKE_PORT_TWO);
+
+ // Default-port behavior testing requires the server responds to requests
+ // claiming to be on one such port.
+ id.add("http", "localhost", 80);
+
+ // Make sure we don't have anything lying around from running on either the
+ // first or the second port -- all we should have is our generated default,
+ // plus the additional port to test "portless" hostport variants.
+ do_check_true(id.has("http", "localhost", 80));
+ do_check_eq(id.primaryScheme, "http");
+ do_check_eq(id.primaryHost, "localhost");
+ do_check_eq(id.primaryPort, PORT);
+ do_check_true(id.has("http", "localhost", PORT));
+ do_check_true(id.has("http", "127.0.0.1", PORT));
+ do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+ do_check_false(id.has("http", "example.com", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "localhost", FAKE_PORT_TWO));
+ do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+
+ // Okay, finally done with identity testing. Our primary location is the one
+ // we want it to be, so we're off!
+ runRawTests(tests, testComplete(srv));
+}
+
+
+/*********************
+ * UTILITY FUNCTIONS *
+ *********************/
+
+/**
+ * Verifies that all .primary* getters on a server identity correctly throw
+ * NS_ERROR_NOT_INITIALIZED.
+ *
+ * @param id : nsIHttpServerIdentity
+ * the server identity to test
+ */
+function checkPrimariesThrow(id)
+{
+ var threw = false;
+ try
+ {
+ id.primaryScheme;
+ }
+ catch (e)
+ {
+ threw = e === Cr.NS_ERROR_NOT_INITIALIZED;
+ }
+ do_check_true(threw);
+
+ threw = false;
+ try
+ {
+ id.primaryHost;
+ }
+ catch (e)
+ {
+ threw = e === Cr.NS_ERROR_NOT_INITIALIZED;
+ }
+ do_check_true(threw);
+
+ threw = false;
+ try
+ {
+ id.primaryPort;
+ }
+ catch (e)
+ {
+ threw = e === Cr.NS_ERROR_NOT_INITIALIZED;
+ }
+ do_check_true(threw);
+}
+
+/**
+ * Utility function to check for a 400 response.
+ */
+function check400(data)
+{
+ var iter = LineIterator(data);
+
+ // Status-Line
+ var firstLine = iter.next();
+ do_check_eq(firstLine.substring(0, HTTP_400_LEADER_LENGTH), HTTP_400_LEADER);
+}
+
+
+/***************
+ * BEGIN TESTS *
+ ***************/
+
+const HTTP_400_LEADER = "HTTP/1.1 400 ";
+const HTTP_400_LEADER_LENGTH = HTTP_400_LEADER.length;
+
+var test, data;
+var tests = [];
+
+// HTTP/1.0 request, to ensure we see our default scheme/host/port
+
+function http10Request(request, response)
+{
+ writeDetails(request, response);
+ response.setStatusLine("1.0", 200, "TEST PASSED");
+}
+data = "GET /http/1.0-request HTTP/1.0\r\n" +
+ "\r\n";
+function check10(data)
+{
+ var iter = LineIterator(data);
+
+ // Status-Line
+ do_check_eq(iter.next(), "HTTP/1.0 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ var body =
+ [
+ "Method: GET",
+ "Path: /http/1.0-request",
+ "Query: ",
+ "Version: 1.0",
+ "Scheme: http",
+ "Host: localhost",
+ "Port: 4444",
+ ];
+
+ expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check10),
+tests.push(test);
+
+
+// HTTP/1.1 request, no Host header, expect a 400 response
+
+data = "GET /http/1.1-request HTTP/1.1\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, wrong host, expect a 400 response
+
+data = "GET /http/1.1-request HTTP/1.1\r\n" +
+ "Host: not-localhost\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, wrong host/right port, expect a 400 response
+
+data = "GET /http/1.1-request HTTP/1.1\r\n" +
+ "Host: not-localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, Host header has host but no port, expect a 400 response
+
+data = "GET /http/1.1-request HTTP/1.1\r\n" +
+ "Host: 127.0.0.1\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, Request-URI has wrong port, expect a 400 response
+
+data = "GET http://127.0.0.1/http/1.1-request HTTP/1.1\r\n" +
+ "Host: 127.0.0.1\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, Request-URI has wrong port, expect a 400 response
+
+data = "GET http://localhost:31337/http/1.1-request HTTP/1.1\r\n" +
+ "Host: localhost:31337\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, Request-URI has wrong scheme, expect a 400 response
+
+data = "GET https://localhost:4444/http/1.1-request HTTP/1.1\r\n" +
+ "Host: localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, correct Host header, expect handler's response
+
+function http11goodHost(request, response)
+{
+ writeDetails(request, response);
+ response.setStatusLine("1.1", 200, "TEST PASSED");
+}
+data = "GET /http/1.1-good-host HTTP/1.1\r\n" +
+ "Host: localhost:4444\r\n" +
+ "\r\n";
+function check11goodHost(data)
+{
+ var iter = LineIterator(data);
+
+ // Status-Line
+ do_check_eq(iter.next(), "HTTP/1.1 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ var body =
+ [
+ "Method: GET",
+ "Path: /http/1.1-good-host",
+ "Query: ",
+ "Version: 1.1",
+ "Scheme: http",
+ "Host: localhost",
+ "Port: 4444",
+ ];
+
+ expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check11goodHost),
+tests.push(test);
+
+
+// HTTP/1.1 request, Host header is secondary identity
+
+function http11ipHost(request, response)
+{
+ writeDetails(request, response);
+ response.setStatusLine("1.1", 200, "TEST PASSED");
+}
+data = "GET /http/1.1-ip-host HTTP/1.1\r\n" +
+ "Host: 127.0.0.1:4444\r\n" +
+ "\r\n";
+function check11ipHost(data)
+{
+ var iter = LineIterator(data);
+
+ // Status-Line
+ do_check_eq(iter.next(), "HTTP/1.1 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ var body =
+ [
+ "Method: GET",
+ "Path: /http/1.1-ip-host",
+ "Query: ",
+ "Version: 1.1",
+ "Scheme: http",
+ "Host: 127.0.0.1",
+ "Port: 4444",
+ ];
+
+ expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check11ipHost),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute path, accurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+ "Host: localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHost),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute path, inaccurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+ "Host: localhost:1234\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHost),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute path, different inaccurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+ "Host: not-localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHost),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute path, yet another inaccurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+ "Host: yippity-skippity\r\n" +
+ "\r\n";
+function checkInaccurate(data)
+{
+ check11goodHost(data);
+
+ // dynamism setup
+ srv.identity.setPrimary("http", "127.0.0.1", 4444);
+}
+test = new RawTest("localhost", PORT, data, checkInaccurate),
+tests.push(test);
+
+
+// HTTP/1.0 request, absolute path, different inaccurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data = "GET /http/1.0-request HTTP/1.0\r\n" +
+ "Host: not-localhost:4444\r\n" +
+ "\r\n";
+function check10ip(data)
+{
+ var iter = LineIterator(data);
+
+ // Status-Line
+ do_check_eq(iter.next(), "HTTP/1.0 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ var body =
+ [
+ "Method: GET",
+ "Path: /http/1.0-request",
+ "Query: ",
+ "Version: 1.0",
+ "Scheme: http",
+ "Host: 127.0.0.1",
+ "Port: 4444",
+ ];
+
+ expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check10ip),
+tests.push(test);
+
+
+// HTTP/1.1 request, Host header with implied port
+
+function http11goodHostWackyPort(request, response)
+{
+ writeDetails(request, response);
+ response.setStatusLine("1.1", 200, "TEST PASSED");
+}
+data = "GET /http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+function check11goodHostWackyPort(data)
+{
+ var iter = LineIterator(data);
+
+ // Status-Line
+ do_check_eq(iter.next(), "HTTP/1.1 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ var body =
+ [
+ "Method: GET",
+ "Path: /http/1.1-good-host-wacky-port",
+ "Query: ",
+ "Version: 1.1",
+ "Scheme: http",
+ "Host: localhost",
+ "Port: 80",
+ ];
+
+ expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort),
+tests.push(test);
+
+
+// HTTP/1.1 request, Host header with wacky implied port
+
+data = "GET /http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+ "Host: localhost:\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute URI with implied port
+
+data = "GET http://localhost/http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute URI with wacky implied port
+
+data = "GET http://localhost:/http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute URI with explicit implied port, ignored Host
+
+data = "GET http://localhost:80/http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+ "Host: who-cares\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort),
+tests.push(test);
+
+
+// HTTP/1.1 request, a malformed Request-URI
+
+data = "GET is-this-the-real-life-is-this-just-fantasy HTTP/1.1\r\n" +
+ "Host: localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, a malformed Host header
+
+data = "GET /http/1.1-request HTTP/1.1\r\n" +
+ "Host: la la la\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, a malformed Host header but absolute URI, 5.2 sez fine
+
+data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+ "Host: la la la\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHost),
+tests.push(test);
+
+
+// HTTP/1.0 request, absolute URI, but those aren't valid in HTTP/1.0
+
+data = "GET http://localhost:4444/http/1.1-request HTTP/1.0\r\n" +
+ "Host: localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute URI with unrecognized host
+
+data = "GET http://not-localhost:4444/http/1.1-request HTTP/1.1\r\n" +
+ "Host: not-localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute URI with unrecognized host (but not in Host)
+
+data = "GET http://not-localhost:4444/http/1.1-request HTTP/1.1\r\n" +
+ "Host: localhost:4444\r\n" +
+ "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
diff --git a/netwerk/test/httpserver/test/test_linedata.js b/netwerk/test/httpserver/test/test_linedata.js
new file mode 100644
index 000000000..49f4f8258
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_linedata.js
@@ -0,0 +1,20 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// test that the LineData internal data structure works correctly
+
+function run_test()
+{
+ var data = new LineData();
+ data.appendBytes(["a".charCodeAt(0), CR]);
+
+ var out = { value: "" };
+ do_check_false(data.readLine(out));
+
+ data.appendBytes([LF]);
+ do_check_true(data.readLine(out));
+ do_check_eq(out.value, "a");
+}
diff --git a/netwerk/test/httpserver/test/test_load_module.js b/netwerk/test/httpserver/test/test_load_module.js
new file mode 100644
index 000000000..8233833fa
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_load_module.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Ensure httpd.js can be imported as a module and that a server starts.
+ */
+function run_test() {
+ Components.utils.import("resource://testing-common/httpd.js");
+
+ let server = new HttpServer();
+ server.start(-1);
+
+ do_test_pending();
+
+ server.stop(do_test_finished);
+}
diff --git a/netwerk/test/httpserver/test/test_name_scheme.js b/netwerk/test/httpserver/test/test_name_scheme.js
new file mode 100644
index 000000000..154f73d25
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_name_scheme.js
@@ -0,0 +1,90 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// requests for files ending with a caret (^) are handled specially to enable
+// htaccess-like functionality without the need to explicitly disable display
+// of such files
+
+var srv;
+
+XPCOMUtils.defineLazyGetter(this, "PREFIX", function() {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test(PREFIX + "/bar.html^",
+ null, start_bar_html_, null),
+ new Test(PREFIX + "/foo.html^",
+ null, start_foo_html_, null),
+ new Test(PREFIX + "/normal-file.txt",
+ null, start_normal_file_txt, null),
+ new Test(PREFIX + "/folder^/file.txt",
+ null, start_folder__file_txt, null),
+
+ new Test(PREFIX + "/foo/bar.html^",
+ null, start_bar_html_, null),
+ new Test(PREFIX + "/foo/foo.html^",
+ null, start_foo_html_, null),
+ new Test(PREFIX + "/foo/normal-file.txt",
+ null, start_normal_file_txt, null),
+ new Test(PREFIX + "/foo/folder^/file.txt",
+ null, start_folder__file_txt, null),
+
+ new Test(PREFIX + "/end-caret^/bar.html^",
+ null, start_bar_html_, null),
+ new Test(PREFIX + "/end-caret^/foo.html^",
+ null, start_foo_html_, null),
+ new Test(PREFIX + "/end-caret^/normal-file.txt",
+ null, start_normal_file_txt, null),
+ new Test(PREFIX + "/end-caret^/folder^/file.txt",
+ null, start_folder__file_txt, null)
+ ];
+});
+
+
+function run_test()
+{
+ srv = createServer();
+
+ // make sure underscores work in directories "mounted" in directories with
+ // folders starting with _
+ var nameDir = do_get_file("data/name-scheme/");
+ srv.registerDirectory("/", nameDir);
+ srv.registerDirectory("/foo/", nameDir);
+ srv.registerDirectory("/end-caret^/", nameDir);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+
+// TEST DATA
+
+function start_bar_html_(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 200);
+
+ do_check_eq(ch.getResponseHeader("Content-Type"), "text/html");
+}
+
+function start_foo_html_(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 404);
+}
+
+function start_normal_file_txt(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 200);
+ do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain");
+}
+
+function start_folder__file_txt(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 200);
+ do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain");
+}
diff --git a/netwerk/test/httpserver/test/test_processasync.js b/netwerk/test/httpserver/test/test_processasync.js
new file mode 100644
index 000000000..21ded660d
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_processasync.js
@@ -0,0 +1,304 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests for correct behavior of asynchronous responses.
+ */
+
+XPCOMUtils.defineLazyGetter(this, "PREPATH", function() {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+var srv;
+
+function run_test()
+{
+ srv = createServer();
+ for (var path in handlers)
+ srv.registerPathHandler(path, handlers[path]);
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+
+/***************
+ * BEGIN TESTS *
+ ***************/
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test(PREPATH + "/handleSync", null, start_handleSync, null),
+ new Test(PREPATH + "/handleAsync1", null, start_handleAsync1,
+ stop_handleAsync1),
+ new Test(PREPATH + "/handleAsync2", init_handleAsync2, start_handleAsync2,
+ stop_handleAsync2),
+ new Test(PREPATH + "/handleAsyncOrdering", null, null,
+ stop_handleAsyncOrdering)
+ ];
+});
+
+var handlers = {};
+
+function handleSync(request, response)
+{
+ response.setStatusLine(request.httpVersion, 500, "handleSync fail");
+
+ try
+ {
+ response.finish();
+ do_throw("finish called on sync response");
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_UNEXPECTED);
+ }
+
+ response.setStatusLine(request.httpVersion, 200, "handleSync pass");
+}
+handlers["/handleSync"] = handleSync;
+
+function start_handleSync(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 200);
+ do_check_eq(ch.responseStatusText, "handleSync pass");
+}
+
+function handleAsync1(request, response)
+{
+ response.setStatusLine(request.httpVersion, 500, "Old status line!");
+ response.setHeader("X-Foo", "old value", false);
+
+ response.processAsync();
+
+ response.setStatusLine(request.httpVersion, 200, "New status line!");
+ response.setHeader("X-Foo", "new value", false);
+
+ response.finish();
+
+ try
+ {
+ response.setStatusLine(request.httpVersion, 500, "Too late!");
+ do_throw("late setStatusLine didn't throw");
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ try
+ {
+ response.setHeader("X-Foo", "late value", false);
+ do_throw("late setHeader didn't throw");
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ try
+ {
+ response.bodyOutputStream;
+ do_throw("late bodyOutputStream get didn't throw");
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ try
+ {
+ response.write("fugly");
+ do_throw("late write() didn't throw");
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+}
+handlers["/handleAsync1"] = handleAsync1;
+
+function start_handleAsync1(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 200);
+ do_check_eq(ch.responseStatusText, "New status line!");
+ do_check_eq(ch.getResponseHeader("X-Foo"), "new value");
+}
+
+function stop_handleAsync1(ch, cx, status, data)
+{
+ do_check_eq(data.length, 0);
+}
+
+const startToHeaderDelay = 500;
+const startToFinishedDelay = 750;
+
+function handleAsync2(request, response)
+{
+ response.processAsync();
+
+ response.setStatusLine(request.httpVersion, 200, "Status line");
+ response.setHeader("X-Custom-Header", "value", false);
+
+ callLater(startToHeaderDelay, function()
+ {
+ var body = "BO";
+ response.bodyOutputStream.write(body, body.length);
+
+ try
+ {
+ response.setStatusLine(request.httpVersion, 500, "after body write");
+ do_throw("setStatusLine succeeded");
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ try
+ {
+ response.setHeader("X-Custom-Header", "new 1", false);
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ callLater(startToFinishedDelay - startToHeaderDelay, function()
+ {
+ var body = "DY";
+ response.bodyOutputStream.write(body, body.length);
+
+ response.finish();
+ response.finish(); // idempotency
+
+ try
+ {
+ response.setStatusLine(request.httpVersion, 500, "after finish");
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ try
+ {
+ response.setHeader("X-Custom-Header", "new 2", false);
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ try
+ {
+ response.write("EVIL");
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ });
+ });
+}
+handlers["/handleAsync2"] = handleAsync2;
+
+var startTime_handleAsync2;
+
+function init_handleAsync2(ch)
+{
+ var now = startTime_handleAsync2 = Date.now();
+ dumpn("*** init_HandleAsync2: start time " + now);
+}
+
+function start_handleAsync2(ch, cx)
+{
+ var now = Date.now();
+ dumpn("*** start_handleAsync2: onStartRequest time " + now + ", " +
+ (now - startTime_handleAsync2) + "ms after start time");
+ do_check_true(now >= startTime_handleAsync2 + startToHeaderDelay);
+
+ do_check_eq(ch.responseStatus, 200);
+ do_check_eq(ch.responseStatusText, "Status line");
+ do_check_eq(ch.getResponseHeader("X-Custom-Header"), "value");
+}
+
+function stop_handleAsync2(ch, cx, status, data)
+{
+ var now = Date.now();
+ dumpn("*** stop_handleAsync2: onStopRequest time " + now + ", " +
+ (now - startTime_handleAsync2) + "ms after header time");
+ do_check_true(now >= startTime_handleAsync2 + startToFinishedDelay);
+
+ do_check_eq(String.fromCharCode.apply(null, data), "BODY");
+}
+
+/*
+ * Tests that accessing output stream *before* calling processAsync() works
+ * correctly, sending written data immediately as it is written, not buffering
+ * until finish() is called -- which for this much data would mean we would all
+ * but certainly deadlock, since we're trying to read/write all this data in one
+ * process on a single thread.
+ */
+function handleAsyncOrdering(request, response)
+{
+ var out = new BinaryOutputStream(response.bodyOutputStream);
+
+ var data = [];
+ for (var i = 0; i < 65536; i++)
+ data[i] = 0;
+ var count = 20;
+
+ var writeData =
+ {
+ run: function()
+ {
+ if (count-- === 0)
+ {
+ response.finish();
+ return;
+ }
+
+ try
+ {
+ out.writeByteArray(data, data.length);
+ step();
+ }
+ catch (e)
+ {
+ try
+ {
+ do_throw("error writing data: " + e);
+ }
+ finally
+ {
+ response.finish();
+ }
+ }
+ }
+ };
+ function step()
+ {
+ // Use gThreadManager here because it's expedient, *not* because it's
+ // intended for public use! If you do this in client code, expect me to
+ // knowingly break your code by changing the variable name. :-P
+ gThreadManager.currentThread
+ .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL);
+ }
+ step();
+ response.processAsync();
+}
+handlers["/handleAsyncOrdering"] = handleAsyncOrdering;
+
+function stop_handleAsyncOrdering(ch, cx, status, data)
+{
+ do_check_eq(data.length, 20 * 65536);
+ data.forEach(function(v, index)
+ {
+ if (v !== 0)
+ do_throw("value " + v + " at index " + index + " should be zero");
+ });
+}
diff --git a/netwerk/test/httpserver/test/test_qi.js b/netwerk/test/httpserver/test/test_qi.js
new file mode 100644
index 000000000..aeac79d89
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_qi.js
@@ -0,0 +1,110 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Verify the presence of explicit QueryInterface methods on XPCOM objects
+ * exposed by httpd.js, rather than allowing QueryInterface to be implicitly
+ * created by XPConnect.
+ */
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test("http://localhost:" + srv.identity.primaryPort + "/test",
+ null, start_test, null),
+ new Test("http://localhost:" + srv.identity.primaryPort + "/sjs/qi.sjs",
+ null, start_sjs_qi, null),
+ ];
+});
+
+var srv;
+
+function run_test()
+{
+ srv = createServer();
+
+ var qi;
+ try
+ {
+ qi = srv.identity.QueryInterface(Ci.nsIHttpServerIdentity);
+ }
+ catch (e)
+ {
+ var exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0];
+ do_throw("server identity didn't QI: " + exstr);
+ return;
+ }
+
+ srv.registerPathHandler("/test", testHandler);
+ srv.registerDirectory("/", do_get_file("data/"));
+ srv.registerContentType("sjs", "sjs");
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+
+// TEST DATA
+
+function start_test(ch, cx)
+{
+ do_check_eq(ch.responseStatusText, "QI Tests Passed");
+ do_check_eq(ch.responseStatus, 200);
+}
+
+function start_sjs_qi(ch, cx)
+{
+ do_check_eq(ch.responseStatusText, "SJS QI Tests Passed");
+ do_check_eq(ch.responseStatus, 200);
+}
+
+
+function testHandler(request, response)
+{
+ var exstr;
+ var qid;
+
+ response.setStatusLine(request.httpVersion, 500, "FAIL");
+
+ var passed = false;
+ try
+ {
+ qid = request.QueryInterface(Ci.nsIHttpRequest);
+ passed = qid === request;
+ }
+ catch (e)
+ {
+ exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0];
+ response.setStatusLine(request.httpVersion, 500,
+ "request doesn't QI: " + exstr);
+ return;
+ }
+ if (!passed)
+ {
+ response.setStatusLine(request.httpVersion, 500, "request QI'd wrongly?");
+ return;
+ }
+
+ passed = false;
+ try
+ {
+ qid = response.QueryInterface(Ci.nsIHttpResponse);
+ passed = qid === response;
+ }
+ catch (e)
+ {
+ exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0];
+ response.setStatusLine(request.httpVersion, 500,
+ "response doesn't QI: " + exstr);
+ return;
+ }
+ if (!passed)
+ {
+ response.setStatusLine(request.httpVersion, 500, "response QI'd wrongly?");
+ return;
+ }
+
+ response.setStatusLine(request.httpVersion, 200, "QI Tests Passed");
+}
diff --git a/netwerk/test/httpserver/test/test_registerdirectory.js b/netwerk/test/httpserver/test/test_registerdirectory.js
new file mode 100644
index 000000000..fbb41293e
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_registerdirectory.js
@@ -0,0 +1,263 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// tests the registerDirectory API
+
+XPCOMUtils.defineLazyGetter(this, "BASE", function() {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+
+function nocache(ch)
+{
+ ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // important!
+}
+
+function notFound(ch)
+{
+ do_check_eq(ch.responseStatus, 404);
+ do_check_false(ch.requestSucceeded);
+}
+
+function checkOverride(ch)
+{
+ do_check_eq(ch.responseStatus, 200);
+ do_check_eq(ch.responseStatusText, "OK");
+ do_check_true(ch.requestSucceeded);
+ do_check_eq(ch.getResponseHeader("Override-Succeeded"), "yes");
+}
+
+function check200(ch)
+{
+ do_check_eq(ch.responseStatus, 200);
+ do_check_eq(ch.responseStatusText, "OK");
+}
+
+function checkFile(ch, cx, status, data)
+{
+ do_check_eq(ch.responseStatus, 200);
+ do_check_true(ch.requestSucceeded);
+
+ var actualFile = serverBasePath.clone();
+ actualFile.append("test_registerdirectory.js");
+ do_check_eq(ch.getResponseHeader("Content-Length"),
+ actualFile.fileSize.toString());
+ do_check_eq(data.map(v => String.fromCharCode(v)).join(""),
+ fileContents(actualFile));
+}
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+
+/***********************
+ * without a base path *
+ ***********************/
+ new Test(BASE + "/test_registerdirectory.js",
+ nocache, notFound, null),
+
+/********************
+ * with a base path *
+ ********************/
+ new Test(BASE + "/test_registerdirectory.js",
+ function(ch)
+ {
+ nocache(ch);
+ serverBasePath = testsDirectory.clone();
+ srv.registerDirectory("/", serverBasePath);
+ },
+ null,
+ checkFile),
+
+/*****************************
+ * without a base path again *
+ *****************************/
+ new Test(BASE + "/test_registerdirectory.js",
+ function(ch)
+ {
+ nocache(ch);
+ serverBasePath = null;
+ srv.registerDirectory("/", serverBasePath);
+ },
+ notFound,
+ null),
+
+/***************************
+ * registered path handler *
+ ***************************/
+ new Test(BASE + "/test_registerdirectory.js",
+ function(ch)
+ {
+ nocache(ch);
+ srv.registerPathHandler("/test_registerdirectory.js",
+ override_test_registerdirectory);
+ },
+ checkOverride,
+ null),
+
+/************************
+ * removed path handler *
+ ************************/
+ new Test(BASE + "/test_registerdirectory.js",
+ function init_registerDirectory6(ch)
+ {
+ nocache(ch);
+ srv.registerPathHandler("/test_registerdirectory.js", null);
+ },
+ notFound,
+ null),
+
+/********************
+ * with a base path *
+ ********************/
+ new Test(BASE + "/test_registerdirectory.js",
+ function(ch)
+ {
+ nocache(ch);
+
+ // set the base path again
+ serverBasePath = testsDirectory.clone();
+ srv.registerDirectory("/", serverBasePath);
+ },
+ null,
+ checkFile),
+
+/*************************
+ * ...and a path handler *
+ *************************/
+ new Test(BASE + "/test_registerdirectory.js",
+ function(ch)
+ {
+ nocache(ch);
+ srv.registerPathHandler("/test_registerdirectory.js",
+ override_test_registerdirectory);
+ },
+ checkOverride,
+ null),
+
+/************************
+ * removed base handler *
+ ************************/
+ new Test(BASE + "/test_registerdirectory.js",
+ function(ch)
+ {
+ nocache(ch);
+ serverBasePath = null;
+ srv.registerDirectory("/", serverBasePath);
+ },
+ checkOverride,
+ null),
+
+/************************
+ * removed path handler *
+ ************************/
+ new Test(BASE + "/test_registerdirectory.js",
+ function(ch)
+ {
+ nocache(ch);
+ srv.registerPathHandler("/test_registerdirectory.js", null);
+ },
+ notFound,
+ null),
+
+/*************************
+ * mapping set up, works *
+ *************************/
+ new Test(BASE + "/foo/test_registerdirectory.js",
+ function(ch)
+ {
+ nocache(ch);
+ serverBasePath = testsDirectory.clone();
+ srv.registerDirectory("/foo/", serverBasePath);
+ },
+ check200,
+ null),
+
+/*********************
+ * no mapping, fails *
+ *********************/
+ new Test(BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js",
+ nocache,
+ notFound,
+ null),
+
+/******************
+ * mapping, works *
+ ******************/
+ new Test(BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js",
+ function(ch)
+ {
+ nocache(ch);
+ srv.registerDirectory("/foo/test_registerdirectory.js/",
+ serverBasePath);
+ },
+ null,
+ checkFile),
+
+/************************************
+ * two mappings set up, still works *
+ ************************************/
+ new Test(BASE + "/foo/test_registerdirectory.js",
+ nocache, null, checkFile),
+
+/**************************
+ * remove topmost mapping *
+ **************************/
+ new Test(BASE + "/foo/test_registerdirectory.js",
+ function(ch)
+ {
+ nocache(ch);
+ srv.registerDirectory("/foo/", null);
+ },
+ notFound,
+ null),
+
+/**************************************
+ * lower mapping still present, works *
+ **************************************/
+ new Test(BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js",
+ nocache, null, checkFile),
+
+/*******************
+ * mapping removed *
+ *******************/
+ new Test(BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js",
+ function(ch)
+ {
+ nocache(ch);
+ srv.registerDirectory("/foo/test_registerdirectory.js/", null);
+ },
+ notFound,
+ null)
+ ];
+});
+
+
+var srv;
+var serverBasePath;
+var testsDirectory;
+
+function run_test()
+{
+ testsDirectory = do_get_cwd();
+
+ srv = createServer();
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+
+// PATH HANDLERS
+
+// override of /test_registerdirectory.js
+function override_test_registerdirectory(metadata, response)
+{
+ response.setStatusLine("1.1", 200, "OK");
+ response.setHeader("Override-Succeeded", "yes", false);
+
+ var body = "success!";
+ response.bodyOutputStream.write(body, body.length);
+}
diff --git a/netwerk/test/httpserver/test/test_registerfile.js b/netwerk/test/httpserver/test/test_registerfile.js
new file mode 100644
index 000000000..16a1270f5
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_registerfile.js
@@ -0,0 +1,50 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// tests the registerFile API
+
+XPCOMUtils.defineLazyGetter(this, "BASE", function() {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+var file = do_get_file("test_registerfile.js");
+
+function onStart(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 200);
+}
+
+function onStop(ch, cx, status, data)
+{
+ // not sufficient for equality, but not likely to be wrong!
+ do_check_eq(data.length, file.fileSize);
+}
+
+XPCOMUtils.defineLazyGetter(this, "test", function() {
+ return new Test(BASE + "/foo", null, onStart, onStop);
+});
+
+var srv;
+
+function run_test()
+{
+ srv = createServer();
+
+ try
+ {
+ srv.registerFile("/foo", do_get_profile());
+ throw "registerFile succeeded!";
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ srv.registerFile("/foo", file);
+ srv.start(-1);
+
+ runHttpTests([test], testComplete(srv));
+}
diff --git a/netwerk/test/httpserver/test/test_registerprefix.js b/netwerk/test/httpserver/test/test_registerprefix.js
new file mode 100644
index 000000000..fa3c3390a
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_registerprefix.js
@@ -0,0 +1,127 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// tests the registerPrefixHandler API
+
+XPCOMUtils.defineLazyGetter(this, "BASE", function() {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+function nocache(ch)
+{
+ ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // important!
+}
+
+function notFound(ch)
+{
+ do_check_eq(ch.responseStatus, 404);
+ do_check_false(ch.requestSucceeded);
+}
+
+function makeCheckOverride(magic)
+{
+ return (function checkOverride(ch)
+ {
+ do_check_eq(ch.responseStatus, 200);
+ do_check_eq(ch.responseStatusText, "OK");
+ do_check_true(ch.requestSucceeded);
+ do_check_eq(ch.getResponseHeader("Override-Succeeded"), magic);
+ });
+}
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test(BASE + "/prefix/dummy", prefixHandler, null,
+ makeCheckOverride("prefix")),
+ new Test(BASE + "/prefix/dummy", pathHandler, null,
+ makeCheckOverride("path")),
+ new Test(BASE + "/prefix/subpath/dummy", longerPrefixHandler, null,
+ makeCheckOverride("subpath")),
+ new Test(BASE + "/prefix/dummy", removeHandlers, null, notFound),
+ new Test(BASE + "/prefix/subpath/dummy", newPrefixHandler, null,
+ makeCheckOverride("subpath"))
+ ];
+});
+
+/***************************
+ * registered prefix handler *
+ ***************************/
+
+function prefixHandler(channel)
+{
+ nocache(channel);
+ srv.registerPrefixHandler("/prefix/", makeOverride("prefix"));
+}
+
+/********************************
+ * registered path handler on top *
+ ********************************/
+
+function pathHandler(channel)
+{
+ nocache(channel);
+ srv.registerPathHandler("/prefix/dummy", makeOverride("path"));
+}
+
+/**********************************
+ * registered longer prefix handler *
+ **********************************/
+
+function longerPrefixHandler(channel)
+{
+ nocache(channel);
+ srv.registerPrefixHandler("/prefix/subpath/", makeOverride("subpath"));
+}
+
+/************************
+ * removed prefix handler *
+ ************************/
+
+function removeHandlers(channel)
+{
+ nocache(channel);
+ srv.registerPrefixHandler("/prefix/", null);
+ srv.registerPathHandler("/prefix/dummy", null);
+}
+
+/*****************************
+ * re-register shorter handler *
+ *****************************/
+
+function newPrefixHandler(channel)
+{
+ nocache(channel);
+ srv.registerPrefixHandler("/prefix/", makeOverride("prefix"));
+}
+
+var srv;
+var serverBasePath;
+var testsDirectory;
+
+function run_test()
+{
+ testsDirectory = do_get_profile();
+
+ srv = createServer();
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+// PATH HANDLERS
+
+// generate an override
+function makeOverride(magic)
+{
+ return (function override(metadata, response)
+ {
+ response.setStatusLine("1.1", 200, "OK");
+ response.setHeader("Override-Succeeded", magic, false);
+
+ var body = "success!";
+ response.bodyOutputStream.write(body, body.length);
+ });
+}
diff --git a/netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js b/netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js
new file mode 100644
index 000000000..b1a863f48
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js
@@ -0,0 +1,135 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that even when an incoming request's data for the Request-Line doesn't
+ * all fit in a single onInputStreamReady notification, the request is handled
+ * properly.
+ */
+
+var srv = createServer();
+srv.start(-1);
+const PORT = srv.identity.primaryPort;
+
+function run_test()
+{
+ srv.registerPathHandler("/lots-of-leading-blank-lines",
+ lotsOfLeadingBlankLines);
+ srv.registerPathHandler("/very-long-request-line",
+ veryLongRequestLine);
+
+ runRawTests(tests, testComplete(srv));
+}
+
+
+/***************
+ * BEGIN TESTS *
+ ***************/
+
+var test, data, str;
+var tests = [];
+
+
+function veryLongRequestLine(request, response)
+{
+ writeDetails(request, response);
+ response.setStatusLine(request.httpVersion, 200, "TEST PASSED");
+}
+
+var path = "/very-long-request-line?";
+var reallyLong = "0123456789ABCDEF0123456789ABCDEF"; // 32
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 128
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 512
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 2048
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 8192
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 32768
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 131072
+reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 524288
+if (reallyLong.length !== 524288)
+ throw new TypeError("generated length not as long as expected");
+str = "GET /very-long-request-line?" + reallyLong + " HTTP/1.1\r\n" +
+ "Host: localhost:" + PORT + "\r\n" +
+ "\r\n";
+data = [];
+for (var i = 0; i < str.length; i += 16384)
+ data.push(str.substr(i, 16384));
+
+function checkVeryLongRequestLine(data)
+{
+ var iter = LineIterator(data);
+
+ print("data length: " + data.length);
+ print("iter object: " + iter);
+
+ // Status-Line
+ do_check_eq(iter.next(), "HTTP/1.1 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ var body =
+ [
+ "Method: GET",
+ "Path: /very-long-request-line",
+ "Query: " + reallyLong,
+ "Version: 1.1",
+ "Scheme: http",
+ "Host: localhost",
+ "Port: " + PORT,
+ ];
+
+ expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, checkVeryLongRequestLine),
+tests.push(test);
+
+
+function lotsOfLeadingBlankLines(request, response)
+{
+ writeDetails(request, response);
+ response.setStatusLine(request.httpVersion, 200, "TEST PASSED");
+}
+
+var blankLines = "\r\n";
+for (var i = 0; i < 14; i++)
+ blankLines += blankLines;
+str = blankLines +
+ "GET /lots-of-leading-blank-lines HTTP/1.1\r\n" +
+ "Host: localhost:" + PORT + "\r\n" +
+ "\r\n";
+data = [];
+for (var i = 0; i < str.length; i += 100)
+ data.push(str.substr(i, 100));
+
+function checkLotsOfLeadingBlankLines(data)
+{
+ var iter = LineIterator(data);
+
+ // Status-Line
+ print("data length: " + data.length);
+ print("iter object: " + iter);
+
+ do_check_eq(iter.next(), "HTTP/1.1 200 TEST PASSED");
+
+ skipHeaders(iter);
+
+ // Okay, next line must be the data we expected to be written
+ var body =
+ [
+ "Method: GET",
+ "Path: /lots-of-leading-blank-lines",
+ "Query: ",
+ "Version: 1.1",
+ "Scheme: http",
+ "Host: localhost",
+ "Port: " + PORT,
+ ];
+
+ expectLines(iter, body);
+}
+
+test = new RawTest("localhost", PORT, data, checkLotsOfLeadingBlankLines),
+tests.push(test);
diff --git a/netwerk/test/httpserver/test/test_response_write.js b/netwerk/test/httpserver/test/test_response_write.js
new file mode 100644
index 000000000..0a37ee44b
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_response_write.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// make sure response.write works for strings, and coerces other args to strings
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test("http://localhost:" + srv.identity.primaryPort + "/writeString",
+ null, check_1234, succeeded),
+ new Test("http://localhost:" + srv.identity.primaryPort + "/writeInt",
+ null, check_1234, succeeded),
+ ];
+});
+
+var srv;
+
+function run_test()
+{
+ srv = createServer();
+
+ srv.registerPathHandler("/writeString", writeString);
+ srv.registerPathHandler("/writeInt", writeInt);
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+
+// TEST DATA
+
+function succeeded(ch, cx, status, data)
+{
+ do_check_true(Components.isSuccessCode(status));
+ do_check_eq(data.map(v => String.fromCharCode(v)).join(""), "1234");
+}
+
+function check_1234(ch, cx)
+{
+ do_check_eq(ch.getResponseHeader("Content-Length"), "4");
+}
+
+// PATH HANDLERS
+
+function writeString(metadata, response)
+{
+ response.write("1234");
+}
+
+function writeInt(metadata, response)
+{
+ response.write(1234);
+}
diff --git a/netwerk/test/httpserver/test/test_seizepower.js b/netwerk/test/httpserver/test/test_seizepower.js
new file mode 100644
index 000000000..f2d9e32c1
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_seizepower.js
@@ -0,0 +1,182 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests that the seizePower API works correctly.
+ */
+
+XPCOMUtils.defineLazyGetter(this, "PORT", function() {
+ return srv.identity.primaryPort;
+});
+
+var srv;
+
+function run_test()
+{
+ srv = createServer();
+
+ srv.registerPathHandler("/raw-data", handleRawData);
+ srv.registerPathHandler("/called-too-late", handleTooLate);
+ srv.registerPathHandler("/exceptions", handleExceptions);
+ srv.registerPathHandler("/async-seizure", handleAsyncSeizure);
+ srv.registerPathHandler("/seize-after-async", handleSeizeAfterAsync);
+
+ srv.start(-1);
+
+ runRawTests(tests, testComplete(srv));
+}
+
+
+function checkException(fun, err, msg)
+{
+ try
+ {
+ fun();
+ }
+ catch (e)
+ {
+ if (e !== err && e.result !== err)
+ do_throw(msg);
+ return;
+ }
+ do_throw(msg);
+}
+
+function callASAPLater(fun)
+{
+ gThreadManager.currentThread.dispatch({
+ run: function()
+ {
+ fun();
+ }
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+}
+
+
+/*****************
+ * PATH HANDLERS *
+ *****************/
+
+function handleRawData(request, response)
+{
+ response.seizePower();
+ response.write("Raw data!");
+ response.finish();
+}
+
+function handleTooLate(request, response)
+{
+ response.write("DO NOT WANT");
+ var output = response.bodyOutputStream;
+
+ response.seizePower();
+
+ if (response.bodyOutputStream !== output)
+ response.write("bodyOutputStream changed!");
+ else
+ response.write("too-late passed");
+ response.finish();
+}
+
+function handleExceptions(request, response)
+{
+ response.seizePower();
+ checkException(function() { response.setStatusLine("1.0", 500, "ISE"); },
+ Cr.NS_ERROR_NOT_AVAILABLE,
+ "setStatusLine should throw not-available after seizePower");
+ checkException(function() { response.setHeader("X-Fail", "FAIL", false); },
+ Cr.NS_ERROR_NOT_AVAILABLE,
+ "setHeader should throw not-available after seizePower");
+ checkException(function() { response.processAsync(); },
+ Cr.NS_ERROR_NOT_AVAILABLE,
+ "processAsync should throw not-available after seizePower");
+ var out = response.bodyOutputStream;
+ var data = "exceptions test passed";
+ out.write(data, data.length);
+ response.seizePower(); // idempotency test of seizePower
+ response.finish();
+ response.finish(); // idempotency test of finish after seizePower
+ checkException(function() { response.seizePower(); },
+ Cr.NS_ERROR_UNEXPECTED,
+ "seizePower should throw unexpected after finish");
+}
+
+function handleAsyncSeizure(request, response)
+{
+ response.seizePower();
+ callLater(1, function()
+ {
+ response.write("async seizure passed");
+ response.bodyOutputStream.close();
+ callLater(1, function()
+ {
+ response.finish();
+ });
+ });
+}
+
+function handleSeizeAfterAsync(request, response)
+{
+ response.setStatusLine(request.httpVersion, 200, "async seizure pass");
+ response.processAsync();
+ checkException(function() { response.seizePower(); },
+ Cr.NS_ERROR_NOT_AVAILABLE,
+ "seizePower should throw not-available after processAsync");
+ callLater(1, function()
+ {
+ response.finish();
+ });
+}
+
+
+/***************
+ * BEGIN TESTS *
+ ***************/
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new RawTest("localhost", PORT, data0, checkRawData),
+ new RawTest("localhost", PORT, data1, checkTooLate),
+ new RawTest("localhost", PORT, data2, checkExceptions),
+ new RawTest("localhost", PORT, data3, checkAsyncSeizure),
+ new RawTest("localhost", PORT, data4, checkSeizeAfterAsync),
+ ];
+});
+
+var data0 = "GET /raw-data HTTP/1.0\r\n" +
+ "\r\n";
+function checkRawData(data)
+{
+ do_check_eq(data, "Raw data!");
+}
+
+var data1 = "GET /called-too-late HTTP/1.0\r\n" +
+ "\r\n";
+function checkTooLate(data)
+{
+ do_check_eq(LineIterator(data).next(), "too-late passed");
+}
+
+var data2 = "GET /exceptions HTTP/1.0\r\n" +
+ "\r\n";
+function checkExceptions(data)
+{
+ do_check_eq("exceptions test passed", data);
+}
+
+var data3 = "GET /async-seizure HTTP/1.0\r\n" +
+ "\r\n";
+function checkAsyncSeizure(data)
+{
+ do_check_eq(data, "async seizure passed");
+}
+
+var data4 = "GET /seize-after-async HTTP/1.0\r\n" +
+ "\r\n";
+function checkSeizeAfterAsync(data)
+{
+ do_check_eq(LineIterator(data).next(), "HTTP/1.0 200 async seizure pass");
+}
diff --git a/netwerk/test/httpserver/test/test_setindexhandler.js b/netwerk/test/httpserver/test/test_setindexhandler.js
new file mode 100644
index 000000000..6e733f4db
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_setindexhandler.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Make sure setIndexHandler works as expected
+
+var srv, serverBasePath;
+
+function run_test()
+{
+ srv = createServer();
+ serverBasePath = do_get_profile();
+ srv.registerDirectory("/", serverBasePath);
+ srv.setIndexHandler(myIndexHandler);
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + srv.identity.primaryPort + "/";
+});
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test(URL, init, startCustomIndexHandler, stopCustomIndexHandler),
+ new Test(URL, init, startDefaultIndexHandler, stopDefaultIndexHandler)
+ ];
+});
+
+function init(ch)
+{
+ ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // important!
+}
+function startCustomIndexHandler(ch, cx)
+{
+ do_check_eq(ch.getResponseHeader("Content-Length"), "10");
+ srv.setIndexHandler(null);
+}
+function stopCustomIndexHandler(ch, cx, status, data)
+{
+ do_check_true(Components.isSuccessCode(status));
+ do_check_eq(String.fromCharCode.apply(null, data), "directory!");
+}
+
+function startDefaultIndexHandler(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 200);
+}
+function stopDefaultIndexHandler(ch, cx, status, data)
+{
+ do_check_true(Components.isSuccessCode(status));
+}
+
+// PATH HANDLERS
+
+function myIndexHandler(metadata, response)
+{
+ var dir = metadata.getProperty("directory");
+ do_check_true(dir != null);
+ do_check_true(dir instanceof Ci.nsIFile);
+ do_check_true(dir.equals(serverBasePath));
+
+ response.write("directory!");
+}
diff --git a/netwerk/test/httpserver/test/test_setstatusline.js b/netwerk/test/httpserver/test/test_setstatusline.js
new file mode 100644
index 000000000..f6f651488
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_setstatusline.js
@@ -0,0 +1,172 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// exercise nsIHttpResponse.setStatusLine, ensure its atomicity, and ensure the
+// specified behavior occurs if it's not called
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+var srv;
+
+function run_test()
+{
+ srv = createServer();
+
+ srv.registerPathHandler("/no/setstatusline", noSetstatusline);
+ srv.registerPathHandler("/http1_0", http1_0);
+ srv.registerPathHandler("/http1_1", http1_1);
+ srv.registerPathHandler("/invalidVersion", invalidVersion);
+ srv.registerPathHandler("/invalidStatus", invalidStatus);
+ srv.registerPathHandler("/invalidDescription", invalidDescription);
+ srv.registerPathHandler("/crazyCode", crazyCode);
+ srv.registerPathHandler("/nullVersion", nullVersion);
+
+ srv.start(-1);
+
+ runHttpTests(tests, testComplete(srv));
+}
+
+
+/*************
+ * UTILITIES *
+ *************/
+
+function checkStatusLine(channel, httpMaxVer, httpMinVer, httpCode, statusText)
+{
+ do_check_eq(channel.responseStatus, httpCode);
+ do_check_eq(channel.responseStatusText, statusText);
+
+ var respMaj = {}, respMin = {};
+ channel.getResponseVersion(respMaj, respMin);
+ do_check_eq(respMaj.value, httpMaxVer);
+ do_check_eq(respMin.value, httpMinVer);
+}
+
+
+/*********
+ * TESTS *
+ *********/
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test(URL + "/no/setstatusline", null, startNoSetStatusLine, stop),
+ new Test(URL + "/http1_0", null, startHttp1_0, stop),
+ new Test(URL + "/http1_1", null, startHttp1_1, stop),
+ new Test(URL + "/invalidVersion", null, startPassedTrue, stop),
+ new Test(URL + "/invalidStatus", null, startPassedTrue, stop),
+ new Test(URL + "/invalidDescription", null, startPassedTrue, stop),
+ new Test(URL + "/crazyCode", null, startCrazy, stop),
+ new Test(URL + "/nullVersion", null, startNullVersion, stop)
+ ];
+});
+
+
+// /no/setstatusline
+function noSetstatusline(metadata, response)
+{
+}
+function startNoSetStatusLine(ch, cx)
+{
+ checkStatusLine(ch, 1, 1, 200, "OK");
+}
+function stop(ch, cx, status, data)
+{
+ do_check_true(Components.isSuccessCode(status));
+}
+
+
+// /http1_0
+function http1_0(metadata, response)
+{
+ response.setStatusLine("1.0", 200, "OK");
+}
+function startHttp1_0(ch, cx)
+{
+ checkStatusLine(ch, 1, 0, 200, "OK");
+}
+
+
+// /http1_1
+function http1_1(metadata, response)
+{
+ response.setStatusLine("1.1", 200, "OK");
+}
+function startHttp1_1(ch, cx)
+{
+ checkStatusLine(ch, 1, 1, 200, "OK");
+}
+
+
+// /invalidVersion
+function invalidVersion(metadata, response)
+{
+ try
+ {
+ response.setStatusLine(" 1.0", 200, "FAILED");
+ }
+ catch (e)
+ {
+ response.setHeader("Passed", "true", false);
+ }
+}
+function startPassedTrue(ch, cx)
+{
+ checkStatusLine(ch, 1, 1, 200, "OK");
+ do_check_eq(ch.getResponseHeader("Passed"), "true");
+}
+
+
+// /invalidStatus
+function invalidStatus(metadata, response)
+{
+ try
+ {
+ response.setStatusLine("1.0", 1000, "FAILED");
+ }
+ catch (e)
+ {
+ response.setHeader("Passed", "true", false);
+ }
+}
+
+
+// /invalidDescription
+function invalidDescription(metadata, response)
+{
+ try
+ {
+ response.setStatusLine("1.0", 200, "FAILED\x01");
+ }
+ catch (e)
+ {
+ response.setHeader("Passed", "true", false);
+ }
+}
+
+
+// /crazyCode
+function crazyCode(metadata, response)
+{
+ response.setStatusLine("1.1", 617, "Crazy");
+}
+function startCrazy(ch, cx)
+{
+ checkStatusLine(ch, 1, 1, 617, "Crazy");
+}
+
+
+// /nullVersion
+function nullVersion(metadata, response)
+{
+ response.setStatusLine(null, 255, "NULL");
+}
+function startNullVersion(ch, cx)
+{
+ // currently, this server implementation defaults to 1.1
+ checkStatusLine(ch, 1, 1, 255, "NULL");
+}
diff --git a/netwerk/test/httpserver/test/test_sjs.js b/netwerk/test/httpserver/test/test_sjs.js
new file mode 100644
index 000000000..2a9814196
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_sjs.js
@@ -0,0 +1,251 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// tests support for server JS-generated pages
+
+var srv = createServer();
+
+var sjs = do_get_file("data/sjs/cgi.sjs");
+// NB: The server has no state at this point -- all state is set up and torn
+// down in the tests, because we run the same tests twice with only a
+// different query string on the requests, followed by the oddball
+// test that doesn't care about throwing or not.
+srv.start(-1);
+const PORT = srv.identity.primaryPort;
+
+const BASE = "http://localhost:" + PORT;
+var test;
+var tests = [];
+
+
+/*********************
+ * UTILITY FUNCTIONS *
+ *********************/
+
+function bytesToString(bytes)
+{
+ return bytes.map(function(v) { return String.fromCharCode(v); }).join("");
+}
+
+function skipCache(ch)
+{
+ ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+}
+
+
+/********************
+ * DEFINE THE TESTS *
+ ********************/
+
+/**
+ * Adds the set of tests defined in here, differentiating between tests with a
+ * SJS which throws an exception and creates a server error and tests with a
+ * normal, successful SJS.
+ */
+function setupTests(throwing)
+{
+ const TEST_URL = BASE + "/cgi.sjs" + (throwing ? "?throw" : "");
+
+ // registerFile with SJS => raw text
+
+ function setupFile(ch)
+ {
+ srv.registerFile("/cgi.sjs", sjs);
+ skipCache(ch);
+ }
+
+ function verifyRawText(channel, cx, status, bytes)
+ {
+ dumpn(channel.originalURI.spec);
+ do_check_eq(bytesToString(bytes), fileContents(sjs));
+ }
+
+ test = new Test(TEST_URL, setupFile, null, verifyRawText);
+ tests.push(test);
+
+
+ // add mapping, => interpreted
+
+ function addTypeMapping(ch)
+ {
+ srv.registerContentType("sjs", "sjs");
+ skipCache(ch);
+ }
+
+ function checkType(ch, cx)
+ {
+ if (throwing)
+ {
+ do_check_false(ch.requestSucceeded);
+ do_check_eq(ch.responseStatus, 500);
+ }
+ else
+ {
+ do_check_eq(ch.contentType, "text/plain");
+ }
+ }
+
+ function checkContents(ch, cx, status, data)
+ {
+ if (!throwing)
+ do_check_eq("PASS", bytesToString(data));
+ }
+
+ test = new Test(TEST_URL, addTypeMapping, checkType, checkContents);
+ tests.push(test);
+
+
+ // remove file/type mapping, map containing directory => raw text
+
+ function setupDirectoryAndRemoveType(ch)
+ {
+ dumpn("removing type mapping");
+ srv.registerContentType("sjs", null);
+ srv.registerFile("/cgi.sjs", null);
+ srv.registerDirectory("/", sjs.parent);
+ skipCache(ch);
+ }
+
+ test = new Test(TEST_URL, setupDirectoryAndRemoveType, null, verifyRawText);
+ tests.push(test);
+
+
+ // add mapping, => interpreted
+
+ function contentAndCleanup(ch, cx, status, data)
+ {
+ checkContents(ch, cx, status, data);
+
+ // clean up state we've set up
+ srv.registerDirectory("/", null);
+ srv.registerContentType("sjs", null);
+ }
+
+ test = new Test(TEST_URL, addTypeMapping, checkType, contentAndCleanup);
+ tests.push(test);
+
+ // NB: No remaining state in the server right now! If we have any here,
+ // either the second run of tests (without ?throw) or the tests added
+ // after the two sets will almost certainly fail.
+}
+
+
+/*****************
+ * ADD THE TESTS *
+ *****************/
+
+setupTests(true);
+setupTests(false);
+
+// Test that when extension-mappings are used, the entire filename cannot be
+// treated as an extension -- there must be at least one dot for a filename to
+// match an extension.
+
+function init(ch)
+{
+ // clean up state we've set up
+ srv.registerDirectory("/", sjs.parent);
+ srv.registerContentType("sjs", "sjs");
+ skipCache(ch);
+}
+
+function checkNotSJS(ch, cx, status, data)
+{
+ do_check_neq("FAIL", bytesToString(data));
+}
+
+test = new Test(BASE + "/sjs", init, null, checkNotSJS);
+tests.push(test);
+
+// Test that Range requests are passed through to the SJS file without
+// bounds checking.
+
+function rangeInit(expectedRangeHeader)
+{
+ return function setupRangeRequest(ch)
+ {
+ ch.setRequestHeader("Range", expectedRangeHeader, false);
+ };
+}
+
+function checkRangeResult(ch, cx)
+{
+ try
+ {
+ var val = ch.getResponseHeader("Content-Range");
+ }
+ catch (e) { /* IDL doesn't specify a particular exception to require */ }
+ if (val !== undefined)
+ {
+ do_throw("should not have gotten a Content-Range header, but got one " +
+ "with this value: " + val);
+ }
+ do_check_eq(200, ch.responseStatus);
+ do_check_eq("OK", ch.responseStatusText);
+}
+
+test = new Test(BASE + "/range-checker.sjs",
+ rangeInit("not-a-bytes-equals-specifier"),
+ checkRangeResult, null);
+tests.push(test);
+test = new Test(BASE + "/range-checker.sjs",
+ rangeInit("bytes=-"),
+ checkRangeResult, null);
+tests.push(test);
+test = new Test(BASE + "/range-checker.sjs",
+ rangeInit("bytes=1000000-"),
+ checkRangeResult, null);
+tests.push(test);
+test = new Test(BASE + "/range-checker.sjs",
+ rangeInit("bytes=1-4"),
+ checkRangeResult, null);
+tests.push(test);
+test = new Test(BASE + "/range-checker.sjs",
+ rangeInit("bytes=-4"),
+ checkRangeResult, null);
+tests.push(test);
+
+// One last test: for file mappings, the content-type is determined by the
+// extension of the file on the server, not by the extension of the requested
+// path.
+
+function setupFileMapping(ch)
+{
+ srv.registerFile("/script.html", sjs);
+}
+
+function onStart(ch, cx)
+{
+ do_check_eq(ch.contentType, "text/plain");
+}
+
+function onStop(ch, cx, status, data)
+{
+ do_check_eq("PASS", bytesToString(data));
+}
+
+test = new Test(BASE + "/script.html", setupFileMapping, onStart, onStop);
+tests.push(test);
+
+
+/*****************
+ * RUN THE TESTS *
+ *****************/
+
+function run_test()
+{
+ // Test for a content-type which isn't a field-value
+ try
+ {
+ srv.registerContentType("foo", "bar\nbaz");
+ throw "this server throws on content-types which aren't field-values";
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_INVALID_ARG);
+ }
+ runHttpTests(tests, testComplete(srv));
+}
diff --git a/netwerk/test/httpserver/test/test_sjs_object_state.js b/netwerk/test/httpserver/test/test_sjs_object_state.js
new file mode 100644
index 000000000..b0f4e546d
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_sjs_object_state.js
@@ -0,0 +1,290 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests that the object-state-preservation mechanism works correctly.
+ */
+
+
+XPCOMUtils.defineLazyGetter(this, "PATH", function() {
+ return "http://localhost:" + srv.identity.primaryPort + "/object-state.sjs";
+});
+
+var srv;
+
+function run_test()
+{
+ srv = createServer();
+ var sjsDir = do_get_file("data/sjs/");
+ srv.registerDirectory("/", sjsDir);
+ srv.registerContentType("sjs", "sjs");
+ srv.start(-1);
+
+ do_test_pending();
+
+ new HTTPTestLoader(PATH + "?state=initial", initialStart, initialStop);
+}
+
+/********************
+ * OBSERVER METHODS *
+ ********************/
+
+/*
+ * In practice the current implementation will guarantee an exact ordering of
+ * all start and stop callbacks. However, in the interests of robustness, this
+ * test will pass given any valid ordering of callbacks. Any ordering of calls
+ * which obeys the partial ordering shown by this directed acyclic graph will be
+ * handled correctly:
+ *
+ * initialStart
+ * |
+ * V
+ * intermediateStart
+ * |
+ * V
+ * intermediateStop
+ * | \
+ * | V
+ * | initialStop
+ * V
+ * triggerStart
+ * |
+ * V
+ * triggerStop
+ *
+ */
+
+var initialStarted = false;
+function initialStart(ch, cx)
+{
+ dumpn("*** initialStart");
+
+ if (initialStarted)
+ do_throw("initialStart: initialStarted is true?!?!");
+
+ initialStarted = true;
+
+ new HTTPTestLoader(PATH + "?state=intermediate",
+ intermediateStart, intermediateStop);
+}
+
+var initialStopped = false;
+function initialStop(ch, cx, status, data)
+{
+ dumpn("*** initialStop");
+
+ do_check_eq(data.map(function(v) { return String.fromCharCode(v); }).join(""),
+ "done");
+
+ do_check_eq(srv.getObjectState("object-state-test"), null);
+
+ if (!initialStarted)
+ do_throw("initialStop: initialStarted is false?!?!");
+ if (initialStopped)
+ do_throw("initialStop: initialStopped is true?!?!");
+ if (!intermediateStarted)
+ do_throw("initialStop: intermediateStarted is false?!?!");
+ if (!intermediateStopped)
+ do_throw("initialStop: intermediateStopped is false?!?!");
+
+ initialStopped = true;
+
+ checkForFinish();
+}
+
+var intermediateStarted = false;
+function intermediateStart(ch, cx)
+{
+ dumpn("*** intermediateStart");
+
+ do_check_neq(srv.getObjectState("object-state-test"), null);
+
+ if (!initialStarted)
+ do_throw("intermediateStart: initialStarted is false?!?!");
+ if (intermediateStarted)
+ do_throw("intermediateStart: intermediateStarted is true?!?!");
+
+ intermediateStarted = true;
+}
+
+var intermediateStopped = false;
+function intermediateStop(ch, cx, status, data)
+{
+ dumpn("*** intermediateStop");
+
+ do_check_eq(data.map(function(v) { return String.fromCharCode(v); }).join(""),
+ "intermediate");
+
+ do_check_neq(srv.getObjectState("object-state-test"), null);
+
+ if (!initialStarted)
+ do_throw("intermediateStop: initialStarted is false?!?!");
+ if (!intermediateStarted)
+ do_throw("intermediateStop: intermediateStarted is false?!?!");
+ if (intermediateStopped)
+ do_throw("intermediateStop: intermediateStopped is true?!?!");
+
+ intermediateStopped = true;
+
+ new HTTPTestLoader(PATH + "?state=trigger", triggerStart,
+ triggerStop);
+}
+
+var triggerStarted = false;
+function triggerStart(ch, cx)
+{
+ dumpn("*** triggerStart");
+
+ if (!initialStarted)
+ do_throw("triggerStart: initialStarted is false?!?!");
+ if (!intermediateStarted)
+ do_throw("triggerStart: intermediateStarted is false?!?!");
+ if (!intermediateStopped)
+ do_throw("triggerStart: intermediateStopped is false?!?!");
+ if (triggerStarted)
+ do_throw("triggerStart: triggerStarted is true?!?!");
+
+ triggerStarted = true;
+}
+
+var triggerStopped = false;
+function triggerStop(ch, cx, status, data)
+{
+ dumpn("*** triggerStop");
+
+ do_check_eq(data.map(function(v) { return String.fromCharCode(v); }).join(""),
+ "trigger");
+
+ if (!initialStarted)
+ do_throw("triggerStop: initialStarted is false?!?!");
+ if (!intermediateStarted)
+ do_throw("triggerStop: intermediateStarted is false?!?!");
+ if (!intermediateStopped)
+ do_throw("triggerStop: intermediateStopped is false?!?!");
+ if (!triggerStarted)
+ do_throw("triggerStop: triggerStarted is false?!?!");
+ if (triggerStopped)
+ do_throw("triggerStop: triggerStopped is false?!?!");
+
+ triggerStopped = true;
+
+ checkForFinish();
+}
+
+var finished = false;
+function checkForFinish()
+{
+ if (finished)
+ {
+ try
+ {
+ do_throw("uh-oh, how are we being finished twice?!?!");
+ }
+ finally
+ {
+ quit(1);
+ }
+ }
+
+ if (triggerStopped && initialStopped)
+ {
+ finished = true;
+ try
+ {
+ do_check_eq(srv.getObjectState("object-state-test"), null);
+
+ if (!initialStarted)
+ do_throw("checkForFinish: initialStarted is false?!?!");
+ if (!intermediateStarted)
+ do_throw("checkForFinish: intermediateStarted is false?!?!");
+ if (!intermediateStopped)
+ do_throw("checkForFinish: intermediateStopped is false?!?!");
+ if (!triggerStarted)
+ do_throw("checkForFinish: triggerStarted is false?!?!");
+ }
+ finally
+ {
+ srv.stop(do_test_finished);
+ }
+ }
+}
+
+
+/*********************************
+ * UTILITY OBSERVABLE URL LOADER *
+ *********************************/
+
+/** Stream listener for the channels. */
+function HTTPTestLoader(path, start, stop)
+{
+ /** Path to load. */
+ this._path = path;
+
+ /** Array of bytes of data in body of response. */
+ this._data = [];
+
+ /** onStartRequest callback. */
+ this._start = start;
+
+ /** onStopRequest callback. */
+ this._stop = stop;
+
+ var channel = makeChannel(path);
+ channel.asyncOpen2(this);
+}
+HTTPTestLoader.prototype =
+ {
+ onStartRequest: function(request, cx)
+ {
+ dumpn("*** HTTPTestLoader.onStartRequest for " + this._path);
+
+ var ch = request.QueryInterface(Ci.nsIHttpChannel)
+ .QueryInterface(Ci.nsIHttpChannelInternal);
+
+ try
+ {
+ try
+ {
+ this._start(ch, cx);
+ }
+ catch (e)
+ {
+ do_throw(this._path + ": error in onStartRequest: " + e);
+ }
+ }
+ catch (e)
+ {
+ dumpn("!!! swallowing onStartRequest exception so onStopRequest is " +
+ "called...");
+ }
+ },
+ onDataAvailable: function(request, cx, inputStream, offset, count)
+ {
+ dumpn("*** HTTPTestLoader.onDataAvailable for " + this._path);
+
+ Array.prototype.push.apply(this._data,
+ makeBIS(inputStream).readByteArray(count));
+ },
+ onStopRequest: function(request, cx, status)
+ {
+ dumpn("*** HTTPTestLoader.onStopRequest for " + this._path);
+
+ var ch = request.QueryInterface(Ci.nsIHttpChannel)
+ .QueryInterface(Ci.nsIHttpChannelInternal);
+
+ this._stop(ch, cx, status, this._data);
+ },
+ QueryInterface: function(aIID)
+ {
+ dumpn("*** QueryInterface: " + aIID);
+
+ if (aIID.equals(Ci.nsIStreamListener) ||
+ aIID.equals(Ci.nsIRequestObserver) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+ };
diff --git a/netwerk/test/httpserver/test/test_sjs_state.js b/netwerk/test/httpserver/test/test_sjs_state.js
new file mode 100644
index 000000000..ccf5c4b03
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_sjs_state.js
@@ -0,0 +1,186 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// exercises the server's state-preservation API
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+var srv;
+
+function run_test()
+{
+ srv = createServer();
+ var sjsDir = do_get_file("data/sjs/");
+ srv.registerDirectory("/", sjsDir);
+ srv.registerContentType("sjs", "sjs");
+ srv.registerPathHandler("/path-handler", pathHandler);
+ srv.start(-1);
+
+ function done()
+ {
+ do_check_eq(srv.getSharedState("shared-value"), "done!");
+ do_check_eq(srv.getState("/path-handler", "private-value"),
+ "pathHandlerPrivate2");
+ do_check_eq(srv.getState("/state1.sjs", "private-value"),
+ "");
+ do_check_eq(srv.getState("/state2.sjs", "private-value"),
+ "newPrivate5");
+ do_test_pending();
+ srv.stop(function() { do_test_finished(); });
+ }
+
+ runHttpTests(tests, done);
+}
+
+
+/************
+ * HANDLERS *
+ ************/
+
+var firstTime = true;
+
+function pathHandler(request, response)
+{
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ response.setHeader("X-Old-Shared-Value", srv.getSharedState("shared-value"),
+ false);
+ response.setHeader("X-Old-Private-Value", srv.getState("/path-handler", "private-value"),
+ false);
+
+ var privateValue, sharedValue;
+ if (firstTime)
+ {
+ firstTime = false;
+ privateValue = "pathHandlerPrivate";
+ sharedValue = "pathHandlerShared";
+ }
+ else
+ {
+ privateValue = "pathHandlerPrivate2";
+ sharedValue = "";
+ }
+
+ srv.setState("/path-handler", "private-value", privateValue);
+ srv.setSharedState("shared-value", sharedValue);
+
+ response.setHeader("X-New-Private-Value", privateValue, false);
+ response.setHeader("X-New-Shared-Value", sharedValue, false);
+}
+
+
+/***************
+ * BEGIN TESTS *
+ ***************/
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ return [
+ new Test(URL + "/state1.sjs?" +
+ "newShared=newShared&newPrivate=newPrivate",
+ null, start_initial, null),
+ new Test(URL + "/state1.sjs?" +
+ "newShared=newShared2&newPrivate=newPrivate2",
+ null, start_overwrite, null),
+ new Test(URL + "/state1.sjs?" +
+ "newShared=&newPrivate=newPrivate3",
+ null, start_remove, null),
+ new Test(URL + "/path-handler",
+ null, start_handler, null),
+ new Test(URL + "/path-handler",
+ null, start_handler_again, null),
+ new Test(URL + "/state2.sjs?" +
+ "newShared=newShared4&newPrivate=newPrivate4",
+ null, start_other_initial, null),
+ new Test(URL + "/state2.sjs?" +
+ "newShared=",
+ null, start_other_remove_ignore, null),
+ new Test(URL + "/state2.sjs?" +
+ "newShared=newShared5&newPrivate=newPrivate5",
+ null, start_other_set_new, null),
+ new Test(URL + "/state1.sjs?" +
+ "newShared=done!&newPrivate=",
+ null, start_set_remove_original, null)
+ ];
+});
+
+/* Hack around bug 474845 for now. */
+function getHeaderFunction(ch)
+{
+ function getHeader(name)
+ {
+ try
+ {
+ return ch.getResponseHeader(name);
+ }
+ catch (e)
+ {
+ if (e.result !== Cr.NS_ERROR_NOT_AVAILABLE)
+ throw e;
+ }
+ return "";
+ }
+ return getHeader;
+}
+
+function expectValues(ch, oldShared, newShared, oldPrivate, newPrivate)
+{
+ var getHeader = getHeaderFunction(ch);
+
+ do_check_eq(ch.responseStatus, 200);
+ do_check_eq(getHeader("X-Old-Shared-Value"), oldShared);
+ do_check_eq(getHeader("X-New-Shared-Value"), newShared);
+ do_check_eq(getHeader("X-Old-Private-Value"), oldPrivate);
+ do_check_eq(getHeader("X-New-Private-Value"), newPrivate);
+}
+
+function start_initial(ch, cx)
+{
+dumpn("XXX start_initial");
+ expectValues(ch, "", "newShared", "", "newPrivate");
+}
+
+function start_overwrite(ch, cx)
+{
+ expectValues(ch, "newShared", "newShared2", "newPrivate", "newPrivate2");
+}
+
+function start_remove(ch, cx)
+{
+ expectValues(ch, "newShared2", "", "newPrivate2", "newPrivate3");
+}
+
+function start_handler(ch, cx)
+{
+ expectValues(ch, "", "pathHandlerShared", "", "pathHandlerPrivate");
+}
+
+function start_handler_again(ch, cx)
+{
+ expectValues(ch, "pathHandlerShared", "",
+ "pathHandlerPrivate", "pathHandlerPrivate2");
+}
+
+function start_other_initial(ch, cx)
+{
+ expectValues(ch, "", "newShared4", "", "newPrivate4");
+}
+
+function start_other_remove_ignore(ch, cx)
+{
+ expectValues(ch, "newShared4", "", "newPrivate4", "");
+}
+
+function start_other_set_new(ch, cx)
+{
+ expectValues(ch, "", "newShared5", "newPrivate4", "newPrivate5");
+}
+
+function start_set_remove_original(ch, cx)
+{
+ expectValues(ch, "newShared5", "done!", "newPrivate3", "");
+}
diff --git a/netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js b/netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js
new file mode 100644
index 000000000..2e4855b01
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js
@@ -0,0 +1,74 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests that running an SJS a whole lot of times doesn't have any ill effects
+ * (like exceeding open-file limits due to not closing the SJS file each time,
+ * then preventing any file from being opened).
+ */
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + srv.identity.primaryPort;
+});
+
+var srv;
+
+function run_test()
+{
+ srv = createServer();
+ var sjsDir = do_get_file("data/sjs/");
+ srv.registerDirectory("/", sjsDir);
+ srv.registerContentType("sjs", "sjs");
+ srv.start(-1);
+
+ function done()
+ {
+ do_test_pending();
+ srv.stop(function() { do_test_finished(); });
+ do_check_eq(gStartCount, TEST_RUNS);
+ do_check_true(lastPassed);
+ }
+
+ runHttpTests(tests, done);
+}
+
+/***************
+ * BEGIN TESTS *
+ ***************/
+
+var gStartCount = 0;
+var lastPassed = false;
+
+// This hits the open-file limit for me on OS X; your mileage may vary.
+const TEST_RUNS = 250;
+
+XPCOMUtils.defineLazyGetter(this, "tests", function() {
+ var _tests = new Array(TEST_RUNS + 1);
+ var _test = new Test(URL + "/thrower.sjs?throw", null, start_thrower);
+ for (var i = 0; i < TEST_RUNS; i++)
+ _tests[i] = _test;
+ // ...and don't forget to stop!
+ _tests[TEST_RUNS] = new Test(URL + "/thrower.sjs", null, start_last);
+ return _tests;
+});
+
+function start_thrower(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 500);
+ do_check_false(ch.requestSucceeded);
+
+ gStartCount++;
+}
+
+function start_last(ch, cx)
+{
+ do_check_eq(ch.responseStatus, 200);
+ do_check_true(ch.requestSucceeded);
+
+ do_check_eq(ch.getResponseHeader("X-Test-Status"), "PASS");
+
+ lastPassed = true;
+}
diff --git a/netwerk/test/httpserver/test/test_start_stop.js b/netwerk/test/httpserver/test/test_start_stop.js
new file mode 100644
index 000000000..e9f42bddd
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_start_stop.js
@@ -0,0 +1,189 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests for correct behavior of the server start() and stop() methods.
+ */
+
+XPCOMUtils.defineLazyGetter(this, "PORT", function() {
+ return srv.identity.primaryPort;
+});
+
+XPCOMUtils.defineLazyGetter(this, "PREPATH", function() {
+ return "http://localhost:" + PORT;
+});
+
+var srv, srv2;
+
+function run_test()
+{
+ if (mozinfo.os == "win")
+ {
+ dumpn("*** not running test_start_stop.js on Windows for now, because " +
+ "Windows is dumb");
+ return;
+ }
+
+ dumpn("*** run_test");
+
+ srv = createServer();
+ srv.start(-1);
+
+ try
+ {
+ srv.start(PORT);
+ do_throw("starting a started server");
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_ALREADY_INITIALIZED);
+ }
+
+ try
+ {
+ srv.stop();
+ do_throw("missing argument to stop");
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_NULL_POINTER);
+ }
+
+ try
+ {
+ srv.stop(null);
+ do_throw("null argument to stop");
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_NULL_POINTER);
+ }
+
+ do_test_pending();
+ srv.stop(function()
+ {
+ try
+ {
+ do_test_pending();
+ run_test_2();
+ }
+ finally
+ {
+ do_test_finished();
+ }
+ });
+}
+
+function run_test_2()
+{
+ dumpn("*** run_test_2");
+
+ do_test_finished();
+
+ srv.start(PORT);
+ srv2 = createServer();
+
+ try
+ {
+ srv2.start(PORT);
+ do_throw("two servers on one port?");
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ do_test_pending();
+ try
+ {
+ srv.stop({onStopped: function()
+ {
+ try
+ {
+ do_test_pending();
+ run_test_3();
+ }
+ finally
+ {
+ do_test_finished();
+ }
+ }
+ });
+ }
+ catch (e)
+ {
+ do_throw("error stopping with an object: " + e);
+ }
+}
+
+function run_test_3()
+{
+ dumpn("*** run_test_3");
+
+ do_test_finished();
+
+ srv.registerPathHandler("/handle", handle);
+ srv.start(PORT);
+
+ // Don't rely on the exact (but implementation-constant) sequence of events
+ // as it currently exists by making either run_test_4 or serverStopped handle
+ // the final shutdown.
+ do_test_pending();
+
+ runHttpTests([new Test(PREPATH + "/handle")], run_test_4);
+}
+
+var testsComplete = false;
+
+function run_test_4()
+{
+ dumpn("*** run_test_4");
+
+ testsComplete = true;
+ if (stopped)
+ do_test_finished();
+}
+
+
+const INTERVAL = 500;
+
+function handle(request, response)
+{
+ response.processAsync();
+
+ dumpn("*** stopping server...");
+ srv.stop(serverStopped);
+
+ callLater(INTERVAL, function()
+ {
+ do_check_false(stopped);
+
+ callLater(INTERVAL, function()
+ {
+ do_check_false(stopped);
+ response.finish();
+
+ try
+ {
+ response.processAsync();
+ do_throw("late processAsync didn't throw?");
+ }
+ catch (e)
+ {
+ isException(e, Cr.NS_ERROR_UNEXPECTED);
+ }
+ });
+ });
+}
+
+var stopped = false;
+function serverStopped()
+{
+ dumpn("*** server really, fully shut down now");
+ stopped = true;
+ if (testsComplete)
+ do_test_finished();
+}
diff --git a/netwerk/test/httpserver/test/xpcshell.ini b/netwerk/test/httpserver/test/xpcshell.ini
new file mode 100644
index 000000000..7890917a6
--- /dev/null
+++ b/netwerk/test/httpserver/test/xpcshell.ini
@@ -0,0 +1,37 @@
+[DEFAULT]
+head = head_utils.js
+tail =
+support-files = data/** ../httpd.js
+
+[test_async_response_sending.js]
+[test_basic_functionality.js]
+[test_body_length.js]
+[test_byte_range.js]
+[test_cern_meta.js]
+[test_default_index_handler.js]
+[test_empty_body.js]
+[test_errorhandler_exception.js]
+[test_header_array.js]
+[test_headers.js]
+[test_host.js]
+run-sequentially = Reusing same server on different specific ports.
+[test_linedata.js]
+[test_load_module.js]
+[test_name_scheme.js]
+[test_processasync.js]
+[test_qi.js]
+[test_registerdirectory.js]
+[test_registerfile.js]
+[test_registerprefix.js]
+[test_request_line_split_in_two_packets.js]
+[test_response_write.js]
+[test_seizepower.js]
+[test_setindexhandler.js]
+[test_setstatusline.js]
+[test_sjs.js]
+[test_sjs_object_state.js]
+[test_sjs_state.js]
+[test_sjs_throwing_exceptions.js]
+# Bug 1063533: frequent timeouts on Android 4.3
+skip-if = os == "android"
+[test_start_stop.js]