Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* Any copyright is dedicated to the Public Domain.
"use strict";
const { RESTRequest } = ChromeUtils.importESModule(
);
function run_test() {
Log.repository.getLogger("Services.Common.RESTRequest").level =
Log.Level.Trace;
initTestLogging("Trace");
run_next_test();
}
/**
* Initializing a RESTRequest with an invalid URI throws
* NS_ERROR_MALFORMED_URI.
*/
add_test(function test_invalid_uri() {
do_check_throws(function () {
new RESTRequest("an invalid URI");
}, Cr.NS_ERROR_MALFORMED_URI);
run_next_test();
});
/**
* Verify initial values for attributes.
*/
add_test(function test_attributes() {
let uri = "http://foo.com/bar/baz";
let request = new RESTRequest(uri);
Assert.ok(request.uri instanceof Ci.nsIURI);
Assert.equal(request.uri.spec, uri);
Assert.equal(request.response, null);
Assert.equal(request.status, request.NOT_SENT);
let expectedLoadFlags =
Ci.nsIRequest.LOAD_BYPASS_CACHE |
Ci.nsIRequest.INHIBIT_CACHING |
Ci.nsIRequest.LOAD_ANONYMOUS;
Assert.equal(request.loadFlags, expectedLoadFlags);
run_next_test();
});
/**
* Verify that a proxy auth redirect doesn't break us. This has to be the first
* request made in the file!
*/
add_task(async function test_proxy_auth_redirect() {
let pacFetched = false;
function pacHandler(metadata, response) {
pacFetched = true;
let body = 'function FindProxyForURL(url, host) { return "DIRECT"; }';
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.setHeader(
"Content-Type",
"application/x-ns-proxy-autoconfig",
false
);
response.bodyOutputStream.write(body, body.length);
}
let fetched = false;
function original(metadata, response) {
fetched = true;
let body = "TADA!";
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
let server = httpd_setup({
"/original": original,
"/pac3": pacHandler,
});
PACSystemSettings.PACURI = server.baseURI + "/pac3";
installFakePAC();
let req = new RESTRequest(server.baseURI + "/original");
await req.get();
Assert.ok(pacFetched);
Assert.ok(fetched);
Assert.ok(req.response.success);
Assert.equal("TADA!", req.response.body);
uninstallFakePAC();
await promiseStopServer(server);
});
/**
* Ensure that failures that cause asyncOpen to throw
* result in callbacks being invoked.
*/
add_task(async function test_forbidden_port() {
let request = new RESTRequest("http://localhost:6000/");
await Assert.rejects(
request.get(),
error => error.result == Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED
);
});
/**
* Demonstrate API short-hand: create a request and dispatch it immediately.
*/
add_task(async function test_simple_get() {
let handler = httpd_handler(200, "OK", "Huzzah!");
let server = httpd_setup({ "/resource": handler });
let request = new RESTRequest(server.baseURI + "/resource");
let promiseResponse = request.get();
Assert.equal(request.status, request.SENT);
Assert.equal(request.method, "GET");
let response = await promiseResponse;
Assert.equal(response, request.response);
Assert.equal(request.status, request.COMPLETED);
Assert.ok(response.success);
Assert.equal(response.status, 200);
Assert.equal(response.body, "Huzzah!");
await promiseStopServer(server);
});
/**
* Test HTTP GET with all bells and whistles.
*/
add_task(async function test_get() {
let handler = httpd_handler(200, "OK", "Huzzah!");
let server = httpd_setup({ "/resource": handler });
let request = new RESTRequest(server.baseURI + "/resource");
Assert.equal(request.status, request.NOT_SENT);
let promiseResponse = request.get();
Assert.equal(request.status, request.SENT);
Assert.equal(request.method, "GET");
Assert.ok(!!(request.channel.loadFlags & Ci.nsIRequest.LOAD_BYPASS_CACHE));
Assert.ok(!!(request.channel.loadFlags & Ci.nsIRequest.INHIBIT_CACHING));
let response = await promiseResponse;
Assert.equal(response, request.response);
Assert.equal(request.status, request.COMPLETED);
Assert.ok(request.response.success);
Assert.equal(request.response.status, 200);
Assert.equal(request.response.body, "Huzzah!");
Assert.equal(handler.request.method, "GET");
await Assert.rejects(request.get(), /Request has already been sent/);
await promiseStopServer(server);
});
/**
* Test HTTP GET with UTF-8 content, and custom Content-Type.
*/
add_task(async function test_get_utf8() {
let response = "Hello World or Καλημέρα κόσμε or こんにちは 世界 😺";
let contentType = "text/plain";
let charset = true;
let charsetSuffix = "; charset=UTF-8";
let server = httpd_setup({
"/resource": function (req, res) {
res.setStatusLine(req.httpVersion, 200, "OK");
res.setHeader(
"Content-Type",
contentType + (charset ? charsetSuffix : "")
);
let converter = Cc[
"@mozilla.org/intl/converter-output-stream;1"
].createInstance(Ci.nsIConverterOutputStream);
converter.init(res.bodyOutputStream, "UTF-8");
converter.writeString(response);
converter.close();
},
});
// Check if charset in Content-Type is propertly interpreted.
let request1 = new RESTRequest(server.baseURI + "/resource");
await request1.get();
Assert.equal(request1.response.status, 200);
Assert.equal(request1.response.body, response);
Assert.equal(
request1.response.headers["content-type"],
contentType + charsetSuffix
);
// Check that we default to UTF-8 if Content-Type doesn't have a charset
charset = false;
let request2 = new RESTRequest(server.baseURI + "/resource");
await request2.get();
Assert.equal(request2.response.status, 200);
Assert.equal(request2.response.body, response);
Assert.equal(request2.response.headers["content-type"], contentType);
Assert.equal(request2.response.charset, "utf-8");
let request3 = new RESTRequest(server.baseURI + "/resource");
// With the test server we tend to get onDataAvailable in chunks of 8192 (in
// real network requests there doesn't appear to be any pattern to the size of
// the data `onDataAvailable` is called with), the smiling cat emoji encodes as
// 4 bytes, and so when utf8 encoded, the `"a" + "😺".repeat(2048)` will not be
// aligned onto a codepoint.
//
// Since 8192 isn't guaranteed and could easily change, the following string is
// a) very long, and b) misaligned on roughly 3/4 of the bytes, as a safety
// measure.
response = ("a" + "😺".repeat(2048)).repeat(10);
await request3.get();
Assert.equal(request3.response.status, 200);
// Make sure it came through ok, despite the misalignment.
Assert.equal(request3.response.body, response);
await promiseStopServer(server);
});
/**
* Test HTTP POST data is encoded as UTF-8 by default.
*/
add_task(async function test_post_utf8() {
// We setup a handler that responds with exactly what it received.
// Given we've already tested above that responses are correctly utf-8
// decoded we can surmise that the correct response coming back means the
// input must also have been encoded.
let server = httpd_setup({
"/echo": function (req, res) {
res.setStatusLine(req.httpVersion, 200, "OK");
res.setHeader("Content-Type", req.getHeader("content-type"));
// Get the body as bytes and write them back without touching them
let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
Ci.nsIScriptableInputStream
);
sis.init(req.bodyInputStream);
let body = sis.read(sis.available());
sis.close();
res.write(body);
},
});
let data = {
copyright: "©",
// See the comment in test_get_utf8 about this string.
long: ("a" + "😺".repeat(2048)).repeat(10),
};
let request1 = new RESTRequest(server.baseURI + "/echo");
await request1.post(data);
Assert.equal(request1.response.status, 200);
deepEqual(JSON.parse(request1.response.body), data);
Assert.equal(
request1.response.headers["content-type"],
"application/json; charset=utf-8"
);
await promiseStopServer(server);
});
/**
* Test more variations of charset handling.
*/
add_task(async function test_charsets() {
let response = "Hello World, I can't speak Russian";
let contentType = "text/plain";
let charset = true;
let charsetSuffix = "; charset=us-ascii";
let server = httpd_setup({
"/resource": function (req, res) {
res.setStatusLine(req.httpVersion, 200, "OK");
res.setHeader(
"Content-Type",
contentType + (charset ? charsetSuffix : "")
);
let converter = Cc[
"@mozilla.org/intl/converter-output-stream;1"
].createInstance(Ci.nsIConverterOutputStream);
converter.init(res.bodyOutputStream, "us-ascii");
converter.writeString(response);
converter.close();
},
});
// Check that provided charset overrides hint.
let request1 = new RESTRequest(server.baseURI + "/resource");
request1.charset = "not-a-charset";
await request1.get();
Assert.equal(request1.response.status, 200);
Assert.equal(request1.response.body, response);
Assert.equal(
request1.response.headers["content-type"],
contentType + charsetSuffix
);
Assert.equal(request1.response.charset, "us-ascii");
// Check that hint is used if Content-Type doesn't have a charset.
charset = false;
let request2 = new RESTRequest(server.baseURI + "/resource");
request2.charset = "us-ascii";
await request2.get();
Assert.equal(request2.response.status, 200);
Assert.equal(request2.response.body, response);
Assert.equal(request2.response.headers["content-type"], contentType);
Assert.equal(request2.response.charset, "us-ascii");
await promiseStopServer(server);
});
/**
* Used for testing PATCH/PUT/POST methods.
*/
async function check_posting_data(method) {
let funcName = method.toLowerCase();
let handler = httpd_handler(200, "OK", "Got it!");
let server = httpd_setup({ "/resource": handler });
let request = new RESTRequest(server.baseURI + "/resource");
Assert.equal(request.status, request.NOT_SENT);
let responsePromise = request[funcName]("Hullo?");
Assert.equal(request.status, request.SENT);
Assert.equal(request.method, method);
let response = await responsePromise;
Assert.equal(response, request.response);
Assert.equal(request.status, request.COMPLETED);
Assert.ok(request.response.success);
Assert.equal(request.response.status, 200);
Assert.equal(request.response.body, "Got it!");
Assert.equal(handler.request.method, method);
Assert.equal(handler.request.body, "Hullo?");
Assert.equal(handler.request.getHeader("Content-Type"), "text/plain");
await Assert.rejects(
request[funcName]("Hai!"),
/Request has already been sent/
);
await promiseStopServer(server);
}
/**
* Test HTTP PATCH with a simple string argument and default Content-Type.
*/
add_task(async function test_patch() {
await check_posting_data("PATCH");
});
/**
* Test HTTP PUT with a simple string argument and default Content-Type.
*/
add_task(async function test_put() {
await check_posting_data("PUT");
});
/**
* Test HTTP POST with a simple string argument and default Content-Type.
*/
add_task(async function test_post() {
await check_posting_data("POST");
});
/**
* Test HTTP DELETE.
*/
add_task(async function test_delete() {
let handler = httpd_handler(200, "OK", "Got it!");
let server = httpd_setup({ "/resource": handler });
let request = new RESTRequest(server.baseURI + "/resource");
Assert.equal(request.status, request.NOT_SENT);
let responsePromise = request.delete();
Assert.equal(request.status, request.SENT);
Assert.equal(request.method, "DELETE");
let response = await responsePromise;
Assert.equal(response, request.response);
Assert.equal(request.status, request.COMPLETED);
Assert.ok(request.response.success);
Assert.equal(request.response.status, 200);
Assert.equal(request.response.body, "Got it!");
Assert.equal(handler.request.method, "DELETE");
await Assert.rejects(request.delete(), /Request has already been sent/);
await promiseStopServer(server);
});
/**
* Test an HTTP response with a non-200 status code.
*/
add_task(async function test_get_404() {
let handler = httpd_handler(404, "Not Found", "Cannae find it!");
let server = httpd_setup({ "/resource": handler });
let request = new RESTRequest(server.baseURI + "/resource");
await request.get();
Assert.equal(request.status, request.COMPLETED);
Assert.ok(!request.response.success);
Assert.equal(request.response.status, 404);
Assert.equal(request.response.body, "Cannae find it!");
await promiseStopServer(server);
});
/**
* The 'data' argument to PUT, if not a string already, is automatically
* stringified as JSON.
*/
add_task(async function test_put_json() {
let handler = httpd_handler(200, "OK");
let server = httpd_setup({ "/resource": handler });
let sample_data = {
some: "sample_data",
injson: "format",
number: 42,
};
let request = new RESTRequest(server.baseURI + "/resource");
await request.put(sample_data);
Assert.equal(request.status, request.COMPLETED);
Assert.ok(request.response.success);
Assert.equal(request.response.status, 200);
Assert.equal(request.response.body, "");
Assert.equal(handler.request.method, "PUT");
Assert.equal(handler.request.body, JSON.stringify(sample_data));
Assert.equal(
handler.request.getHeader("Content-Type"),
"application/json; charset=utf-8"
);
await promiseStopServer(server);
});
/**
* The 'data' argument to POST, if not a string already, is automatically
* stringified as JSON.
*/
add_task(async function test_post_json() {
let handler = httpd_handler(200, "OK");
let server = httpd_setup({ "/resource": handler });
let sample_data = {
some: "sample_data",
injson: "format",
number: 42,
};
let request = new RESTRequest(server.baseURI + "/resource");
await request.post(sample_data);
Assert.equal(request.status, request.COMPLETED);
Assert.ok(request.response.success);
Assert.equal(request.response.status, 200);
Assert.equal(request.response.body, "");
Assert.equal(handler.request.method, "POST");
Assert.equal(handler.request.body, JSON.stringify(sample_data));
Assert.equal(
handler.request.getHeader("Content-Type"),
"application/json; charset=utf-8"
);
await promiseStopServer(server);
});
/**
* The content-type will be text/plain without a charset if the 'data' argument
* to POST is already a string.
*/
add_task(async function test_post_json() {
let handler = httpd_handler(200, "OK");
let server = httpd_setup({ "/resource": handler });
let sample_data = "hello";
let request = new RESTRequest(server.baseURI + "/resource");
await request.post(sample_data);
Assert.equal(request.status, request.COMPLETED);
Assert.ok(request.response.success);
Assert.equal(request.response.status, 200);
Assert.equal(request.response.body, "");
Assert.equal(handler.request.method, "POST");
Assert.equal(handler.request.body, sample_data);
Assert.equal(handler.request.getHeader("Content-Type"), "text/plain");
await promiseStopServer(server);
});
/**
* HTTP PUT with a custom Content-Type header.
*/
add_task(async function test_put_override_content_type() {
let handler = httpd_handler(200, "OK");
let server = httpd_setup({ "/resource": handler });
let request = new RESTRequest(server.baseURI + "/resource");
request.setHeader("Content-Type", "application/lolcat");
await request.put("O HAI!!1!");
Assert.equal(request.status, request.COMPLETED);
Assert.ok(request.response.success);
Assert.equal(request.response.status, 200);
Assert.equal(request.response.body, "");
Assert.equal(handler.request.method, "PUT");
Assert.equal(handler.request.body, "O HAI!!1!");
Assert.equal(handler.request.getHeader("Content-Type"), "application/lolcat");
await promiseStopServer(server);
});
/**
* HTTP POST with a custom Content-Type header.
*/
add_task(async function test_post_override_content_type() {
let handler = httpd_handler(200, "OK");
let server = httpd_setup({ "/resource": handler });
let request = new RESTRequest(server.baseURI + "/resource");
request.setHeader("Content-Type", "application/lolcat");
await request.post("O HAI!!1!");
Assert.equal(request.status, request.COMPLETED);
Assert.ok(request.response.success);
Assert.equal(request.response.status, 200);
Assert.equal(request.response.body, "");
Assert.equal(handler.request.method, "POST");
Assert.equal(handler.request.body, "O HAI!!1!");
Assert.equal(handler.request.getHeader("Content-Type"), "application/lolcat");
await promiseStopServer(server);
});
/**
* No special headers are sent by default on a GET request.
*/
add_task(async function test_get_no_headers() {
let handler = httpd_handler(200, "OK");
let server = httpd_setup({ "/resource": handler });
let ignore_headers = [
"host",
"user-agent",
"accept",
"accept-language",
"accept-encoding",
"accept-charset",
"keep-alive",
"connection",
"pragma",
"cache-control",
"content-length",
"sec-fetch-dest",
"sec-fetch-mode",
"sec-fetch-site",
"sec-fetch-user",
"priority",
];
let request = new RESTRequest(server.baseURI + "/resource");
await request.get();
Assert.equal(request.response.status, 200);
Assert.equal(request.response.body, "");
let server_headers = handler.request.headers;
while (server_headers.hasMoreElements()) {
let header = server_headers.getNext().toString();
if (!ignore_headers.includes(header)) {
do_throw("Got unexpected header!");
}
}
await promiseStopServer(server);
});
/**
* Client includes default Accept header in API requests
*/
add_task(async function test_default_accept_headers() {
let handler = httpd_handler(200, "OK");
let server = httpd_setup({ "/resource": handler });
let request = new RESTRequest(server.baseURI + "/resource");
await request.get();
Assert.equal(request.response.status, 200);
Assert.equal(request.response.body, "");
let accept_header = handler.request.getHeader("accept");
Assert.ok(!accept_header.includes("text/html"));
Assert.ok(!accept_header.includes("application/xhtml+xml"));
Assert.ok(!accept_header.includes("applcation/xml"));
Assert.ok(
accept_header.includes("application/json") ||
accept_header.includes("application/newlines")
);
await promiseStopServer(server);
});
/**
* Test changing the URI after having created the request.
*/
add_task(async function test_changing_uri() {
let handler = httpd_handler(200, "OK");
let server = httpd_setup({ "/resource": handler });
let request = new RESTRequest("http://localhost:1234/the-wrong-resource");
request.uri = CommonUtils.makeURI(server.baseURI + "/resource");
let response = await request.get();
Assert.equal(response.status, 200);
await promiseStopServer(server);
});
/**
* Test setting HTTP request headers.
*/
add_task(async function test_request_setHeader() {
let handler = httpd_handler(200, "OK");
let server = httpd_setup({ "/resource": handler });
let request = new RESTRequest(server.baseURI + "/resource");
request.setHeader("X-What-Is-Weave", "awesome");
request.setHeader("X-WHAT-is-Weave", "more awesomer");
request.setHeader("Another-Header", "Hello World");
await request.get();
Assert.equal(request.response.status, 200);
Assert.equal(request.response.body, "");
Assert.equal(handler.request.getHeader("X-What-Is-Weave"), "more awesomer");
Assert.equal(handler.request.getHeader("another-header"), "Hello World");
await promiseStopServer(server);
});
/**
* Test receiving HTTP response headers.
*/
add_task(async function test_response_headers() {
function handler(request, response) {
response.setHeader("X-What-Is-Weave", "awesome");
response.setHeader("Another-Header", "Hello World");
response.setStatusLine(request.httpVersion, 200, "OK");
}
let server = httpd_setup({ "/resource": handler });
let request = new RESTRequest(server.baseURI + "/resource");
await request.get();
Assert.equal(request.response.status, 200);
Assert.equal(request.response.body, "");
Assert.equal(request.response.headers["x-what-is-weave"], "awesome");
Assert.equal(request.response.headers["another-header"], "Hello World");
await promiseStopServer(server);
});
/**
* The onComplete() handler gets called in case of any network errors
* (e.g. NS_ERROR_CONNECTION_REFUSED).
*/
add_task(async function test_connection_refused() {
let request = new RESTRequest("http://localhost:1234/resource");
// Fail the test if we resolve, return the error if we reject
await Assert.rejects(
request.get(),
error =>
error.result == Cr.NS_ERROR_CONNECTION_REFUSED &&
error.message == "NS_ERROR_CONNECTION_REFUSED"
);
Assert.equal(request.status, request.COMPLETED);
});
/**
* Abort a request that just sent off.
*/
add_task(async function test_abort() {
function handler() {
do_throw("Shouldn't have gotten here!");
}
let server = httpd_setup({ "/resource": handler });
let request = new RESTRequest(server.baseURI + "/resource");
// Aborting a request that hasn't been sent yet is pointless and will throw.
do_check_throws(function () {
request.abort();
});
let responsePromise = request.get();
request.abort();
// Aborting an already aborted request is pointless and will throw.
do_check_throws(function () {
request.abort();
});
Assert.equal(request.status, request.ABORTED);
await Assert.rejects(responsePromise, /NS_BINDING_ABORTED/);
await promiseStopServer(server);
});
/**
* A non-zero 'timeout' property specifies the amount of seconds to wait after
* channel activity until the request is automatically canceled.
*/
add_task(async function test_timeout() {
let server = new HttpServer();
let server_connection;
server._handler.handleResponse = function (connection) {
// This is a handler that doesn't do anything, just keeps the connection
// open, thereby mimicking a timing out connection. We keep a reference to
// the open connection for later so it can be properly disposed of. That's
// why you really only want to make one HTTP request to this server ever.
server_connection = connection;
};
server.start();
let identity = server.identity;
let uri =
identity.primaryScheme +
"://" +
identity.primaryHost +
":" +
identity.primaryPort;
let request = new RESTRequest(uri + "/resource");
request.timeout = 0.1; // 100 milliseconds
await Assert.rejects(
request.get(),
error => error.result == Cr.NS_ERROR_NET_TIMEOUT
);
Assert.equal(request.status, request.ABORTED);
// server_connection is undefined on the Android emulator for reasons
// unknown. Yet, we still get here. If this test is refactored, we should
// investigate the reason why the above callback is behaving differently.
if (server_connection) {
_("Closing connection.");
server_connection.close();
}
await promiseStopServer(server);
});
add_task(async function test_new_channel() {
_("Ensure a redirect to a new channel is handled properly.");
function checkUA(metadata) {
let ua = metadata.getHeader("User-Agent");
_("User-Agent is " + ua);
Assert.equal("foo bar", ua);
}
let redirectRequested = false;
let redirectURL;
function redirectHandler(metadata, response) {
checkUA(metadata);
redirectRequested = true;
let body = "Redirecting";
response.setStatusLine(metadata.httpVersion, 307, "TEMPORARY REDIRECT");
response.setHeader("Location", redirectURL);
response.bodyOutputStream.write(body, body.length);
}
let resourceRequested = false;
function resourceHandler(metadata, response) {
checkUA(metadata);
resourceRequested = true;
let body = "Test";
response.setHeader("Content-Type", "text/plain");
response.bodyOutputStream.write(body, body.length);
}
let server1 = httpd_setup({ "/redirect": redirectHandler });
let server2 = httpd_setup({ "/resource": resourceHandler });
redirectURL = server2.baseURI + "/resource";
let request = new RESTRequest(server1.baseURI + "/redirect");
request.setHeader("User-Agent", "foo bar");
// Swizzle in our own fakery, because this redirect is neither
// internal nor URI-preserving. RESTRequest's policy is to only
// copy headers under certain circumstances.
let protoMethod = request.shouldCopyOnRedirect;
request.shouldCopyOnRedirect = function wrapped(o, n, f) {
// Check the default policy.
Assert.ok(!protoMethod.call(this, o, n, f));
return true;
};
let response = await request.get();
Assert.equal(200, response.status);
Assert.equal("Test", response.body);
Assert.ok(redirectRequested);
Assert.ok(resourceRequested);
await promiseStopServer(server1);
await promiseStopServer(server2);
});
add_task(async function test_not_sending_cookie() {
function handler(metadata, response) {
let body = "COOKIE!";
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
Assert.ok(!metadata.hasHeader("Cookie"));
}
let server = httpd_setup({ "/test": handler });
let uri = CommonUtils.makeURI(server.baseURI);
let channel = NetUtil.newChannel({
uri,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
});
Services.cookies.setCookieStringFromHttp(uri, "test=test; path=/;", channel);
let res = new RESTRequest(server.baseURI + "/test");
let response = await res.get();
Assert.ok(response.success);
Assert.equal("COOKIE!", response.body);
await promiseStopServer(server);
});