Bug 557596 - code audit and create unit test plan for resource.js [r=mconnor]

Lots of resource tests, Resource.serverTime initialized to null, Resource.headers normalized to lowercase.
This commit is contained in:
Philipp von Weitershausen 2010-06-01 15:15:53 -07:00
Родитель 46b96f4455
Коммит ada4345433
5 изменённых файлов: 303 добавлений и 35 удалений

Просмотреть файл

@ -660,7 +660,7 @@ SyncEngine.prototype = {
}
// Record the modified time of the upload
let modified = resp.headers["X-Weave-Timestamp"];
let modified = resp.headers["x-weave-timestamp"];
if (modified > this.lastSync)
this.lastSync = modified;

Просмотреть файл

@ -58,11 +58,16 @@ function Resource(uri) {
this._log.level =
Log4Moz.Level[Utils.prefs.getCharPref("log.logger.network.resources")];
this.uri = uri;
this._headers = {'Content-type': 'text/plain'};
this._headers = {};
}
Resource.prototype = {
_logName: "Net.Resource",
// ** {{{ Resource.serverTime }}} **
//
// Caches the latest server timestamp (X-Weave-Timestamp header).
serverTime: null,
// ** {{{ Resource.authenticator }}} **
//
// Getter and setter for the authenticator module
@ -81,9 +86,9 @@ Resource.prototype = {
// ** {{{ Resource.headers }}} **
//
// Getter for access to received headers after the request
// for the resource has been made, setter for headers to be included
// while making a request for the resource.
// Headers to be included when making a request for the resource.
// Note: Header names should be all lower case, there's no explicit
// check for duplicates due to case!
get headers() {
return this.authenticator.onRequest(this._headers);
},
@ -94,7 +99,7 @@ Resource.prototype = {
if (arguments.length % 2)
throw "setHeader only accepts arguments in multiples of 2";
for (let i = 0; i < arguments.length; i += 2) {
this._headers[arguments[i]] = arguments[i + 1];
this._headers[arguments[i].toLowerCase()] = arguments[i + 1];
}
},
@ -144,7 +149,7 @@ Resource.prototype = {
channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
// Setup a callback to handle bad HTTPS certificates.
channel.notificationCallbacks = new badCertListener();
channel.notificationCallbacks = new BadCertListener();
// Avoid calling the authorizer more than once
let headers = this.headers;
@ -182,8 +187,8 @@ Resource.prototype = {
this._log.debug(action + " Length: " + this._data.length);
this._log.trace(action + " Body: " + this._data);
let type = ('Content-Type' in this._headers)?
this._headers['Content-Type'] : 'text/plain';
let type = ('content-type' in this._headers) ?
this._headers['content-type'] : 'text/plain';
let stream = Cc["@mozilla.org/io/string-input-stream;1"].
createInstance(Ci.nsIStringInputStream);
@ -227,7 +232,7 @@ Resource.prototype = {
// Read out the response headers if available
channel.visitResponseHeaders({
visitHeader: function visitHeader(header, value) {
headers[header] = value;
headers[header.toLowerCase()] = value;
}
});
status = channel.responseStatus;
@ -244,8 +249,8 @@ Resource.prototype = {
this._log.trace(action + " body: " + this._data);
// this is a server-side safety valve to allow slowing down clients without hurting performance
if (headers["X-Weave-Backoff"])
Observers.notify("weave:service:backoff:interval", parseInt(headers["X-Weave-Backoff"], 10))
if (headers["x-weave-backoff"])
Observers.notify("weave:service:backoff:interval", parseInt(headers["x-weave-backoff"], 10))
}
// Got a response but no header; must be cached (use default values)
catch(ex) {
@ -360,16 +365,16 @@ ChannelListener.prototype = {
}
};
// = badCertListener =
// = BadCertListener =
//
// We use this listener to ignore bad HTTPS
// certificates and continue a request on a network
// channel. Probably not a very smart thing to do,
// but greatly simplifies debugging and is just very
// convenient.
function badCertListener() {
function BadCertListener() {
}
badCertListener.prototype = {
BadCertListener.prototype = {
getInterface: function(aIID) {
return this.QueryInterface(aIID);
},

Просмотреть файл

@ -1370,8 +1370,8 @@ WeaveSvc.prototype = {
_checkServerError: function WeaveSvc__checkServerError(resp) {
if (Utils.checkStatus(resp.status, null, [500, [502, 504]])) {
Status.enforceBackoff = true;
if (resp.status == 503 && resp.headers["Retry-After"])
Observers.notify("weave:service:backoff:interval", parseInt(resp.headers["Retry-After"], 10));
if (resp.status == 503 && resp.headers["retry-after"])
Observers.notify("weave:service:backoff:interval", parseInt(resp.headers["retry-after"], 10));
}
},
/**

Просмотреть файл

@ -24,6 +24,21 @@ function httpd_basic_auth_handler(body, metadata, response) {
response.bodyOutputStream.write(body, body.length);
}
/*
* Read bytes string from an nsIInputStream. If 'count' is omitted,
* all available input is read.
*/
function readBytesFromInputStream(inputStream, count) {
var BinaryInputStream = Components.Constructor(
"@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream",
"setInputStream");
if (!count) {
count = inputStream.available();
}
return new BinaryInputStream(inputStream).readBytes(count);
}
/*

Просмотреть файл

@ -3,14 +3,21 @@ Cu.import("resource://weave/util.js");
Cu.import("resource://weave/resource.js");
Cu.import("resource://weave/auth.js");
Cu.import("resource://weave/identity.js");
Cu.import("resource://weave/ext/Observers.js");
let logger;
let Httpd = {};
Cu.import("resource://harness/modules/httpd.js", Httpd);
function server_open(metadata, response) {
let body = "This path exists";
response.setStatusLine(metadata.httpVersion, 200, "OK");
let body;
if (metadata.method == "GET") {
body = "This path exists";
response.setStatusLine(metadata.httpVersion, 200, "OK");
} else {
body = "Wrong request method";
response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed");
}
response.bodyOutputStream.write(body, body.length);
}
@ -23,7 +30,6 @@ function server_protected(metadata, response) {
body = "This path exists and is protected";
response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
} else {
body = "This path exists and is protected - failed";
response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
@ -39,6 +45,88 @@ function server_404(metadata, response) {
response.bodyOutputStream.write(body, body.length);
}
let sample_data = {
some: "sample_data",
injson: "format",
number: 42
};
function server_upload(metadata, response) {
let body;
let input = readBytesFromInputStream(metadata.bodyInputStream);
if (input == JSON.stringify(sample_data)) {
body = "Valid data upload via " + metadata.method;
response.setStatusLine(metadata.httpVersion, 200, "OK");
} else {
body = "Invalid data upload via " + metadata.method + ': ' + input;
response.setStatusLine(metadata.httpVersion, 500, "Internal Server Error");
}
response.bodyOutputStream.write(body, body.length);
}
function server_delete(metadata, response) {
let body;
if (metadata.method == "DELETE") {
body = "This resource has been deleted";
response.setStatusLine(metadata.httpVersion, 200, "OK");
} else {
body = "Wrong request method";
response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed");
}
response.bodyOutputStream.write(body, body.length);
}
function server_json(metadata, response) {
let body = JSON.stringify(sample_data);
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
const TIMESTAMP = 1274380461;
function server_timestamp(metadata, response) {
let body = "Thank you for your request";
response.setHeader("X-Weave-Timestamp", ''+TIMESTAMP, false);
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
function server_backoff(metadata, response) {
let body = "Hey, back off!";
response.setHeader("X-Weave-Backoff", '600', false);
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
function server_headers(metadata, response) {
let ignore_headers = ["host", "user-agent", "accept", "accept-language",
"accept-encoding", "accept-charset", "keep-alive",
"connection", "pragma", "cache-control",
"content-length"];
let headers = metadata.headers;
let header_names = [];
while (headers.hasMoreElements()) {
let header = headers.getNext().toString();
if (ignore_headers.indexOf(header) == -1) {
header_names.push(header);
}
}
header_names = header_names.sort();
headers = {};
for each (let header in header_names) {
headers[header] = metadata.getHeader(header);
}
let body = JSON.stringify(headers);
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
function run_test() {
logger = Log4Moz.repository.getLogger('Test');
Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
@ -47,52 +135,212 @@ function run_test() {
server.registerPathHandler("/open", server_open);
server.registerPathHandler("/protected", server_protected);
server.registerPathHandler("/404", server_404);
server.registerPathHandler("/upload", server_upload);
server.registerPathHandler("/delete", server_delete);
server.registerPathHandler("/json", server_json);
server.registerPathHandler("/timestamp", server_timestamp);
server.registerPathHandler("/headers", server_headers);
server.registerPathHandler("/backoff", server_backoff);
server.start(8080);
Utils.prefs.setIntPref("network.numRetries", 1); // speed up test
// 1. A regular non-password-protected resource
_("Resource object memebers");
let res = new Resource("http://localhost:8080/open");
do_check_true(res.uri instanceof Ci.nsIURI);
do_check_eq(res.uri.spec, "http://localhost:8080/open");
do_check_eq(res.spec, "http://localhost:8080/open");
do_check_eq(typeof res.headers, "object");
do_check_eq(typeof res.authenticator, "object");
// Initially res.data is null since we haven't performed a GET or
// PUT/POST request yet.
do_check_eq(res.data, null);
_("GET a non-password-protected resource");
let content = res.get();
do_check_eq(content, "This path exists");
do_check_eq(content.status, 200);
do_check_true(content.success);
// res.data has been updated with the result from the request
do_check_eq(res.data, content);
// 2. A password protected resource (test that it'll fail w/o pass, no throw)
// Since we didn't receive proper JSON data, accessing content.obj
// will result in a SyntaxError from JSON.parse
let didThrow = false;
try {
content.obj;
} catch (ex) {
didThrow = true;
}
do_check_true(didThrow);
_("GET a password protected resource (test that it'll fail w/o pass, no throw)");
let res2 = new Resource("http://localhost:8080/protected");
content = res2.get();
do_check_eq(content, "This path exists and is protected - failed")
do_check_eq(content.status, 401);
do_check_false(content.success);
// 3. A password protected resource
_("GET a password protected resource");
let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest"));
let res3 = new Resource("http://localhost:8080/protected");
res3.authenticator = auth;
do_check_eq(res3.authenticator, auth);
content = res3.get();
do_check_eq(content, "This path exists and is protected");
do_check_eq(content.status, 200);
do_check_true(content.success);
// 4. A non-existent resource (test that it'll fail, but not throw)
_("GET a non-existent resource (test that it'll fail, but not throw)");
let res4 = new Resource("http://localhost:8080/404");
content = res4.get();
do_check_eq(content, "File not found");
do_check_eq(content.status, 404);
do_check_false(content.success);
_("5. Check some headers of the 404 response");
do_check_eq(content.headers.Connection, "close");
do_check_eq(content.headers.Server, "httpd.js");
do_check_eq(content.headers["Content-Length"], 14);
// Check some headers of the 404 response
do_check_eq(content.headers.connection, "close");
do_check_eq(content.headers.server, "httpd.js");
do_check_eq(content.headers["content-length"], 14);
// FIXME: additional coverage needed:
// * PUT requests
// * DELETE requests
// * JsonFilter
_("PUT to a resource (string)");
let res5 = new Resource("http://localhost:8080/upload");
content = res5.put(JSON.stringify(sample_data));
do_check_eq(content, "Valid data upload via PUT");
do_check_eq(content.status, 200);
do_check_eq(res5.data, content);
_("PUT to a resource (object)");
content = res5.put(sample_data);
do_check_eq(content, "Valid data upload via PUT");
do_check_eq(content.status, 200);
do_check_eq(res5.data, content);
_("PUT without data arg (uses resource.data) (string)");
res5.data = JSON.stringify(sample_data);
content = res5.put();
do_check_eq(content, "Valid data upload via PUT");
do_check_eq(content.status, 200);
do_check_eq(res5.data, content);
_("PUT without data arg (uses resource.data) (object)");
res5.data = sample_data;
content = res5.put();
do_check_eq(content, "Valid data upload via PUT");
do_check_eq(content.status, 200);
do_check_eq(res5.data, content);
_("POST to a resource (string)");
content = res5.post(JSON.stringify(sample_data));
do_check_eq(content, "Valid data upload via POST");
do_check_eq(content.status, 200);
do_check_eq(res5.data, content);
_("POST to a resource (object)");
content = res5.post(sample_data);
do_check_eq(content, "Valid data upload via POST");
do_check_eq(content.status, 200);
do_check_eq(res5.data, content);
_("POST without data arg (uses resource.data) (string)");
res5.data = JSON.stringify(sample_data);
content = res5.post();
do_check_eq(content, "Valid data upload via POST");
do_check_eq(content.status, 200);
do_check_eq(res5.data, content);
_("POST without data arg (uses resource.data) (object)");
res5.data = sample_data;
content = res5.post();
do_check_eq(content, "Valid data upload via POST");
do_check_eq(content.status, 200);
do_check_eq(res5.data, content);
_("DELETE a resource");
let res6 = new Resource("http://localhost:8080/delete");
content = res6.delete();
do_check_eq(content, "This resource has been deleted")
do_check_eq(content.status, 200);
_("JSON conversion of response body");
let res7 = new Resource("http://localhost:8080/json");
content = res7.get();
do_check_eq(content, JSON.stringify(sample_data));
do_check_eq(content.status, 200);
do_check_eq(JSON.stringify(content.obj), JSON.stringify(sample_data));
_("X-Weave-Timestamp header updates Resource.serverTime");
// Before having received any response containing the
// X-Weave-Timestamp header, Resource.serverTime is null.
do_check_eq(Resource.serverTime, null);
let res8 = new Resource("http://localhost:8080/timestamp");
content = res8.get();
do_check_eq(Resource.serverTime, TIMESTAMP);
_("GET: no special request headers");
let res9 = new Resource("http://localhost:8080/headers");
content = res9.get();
do_check_eq(content, '{}');
_("PUT: Content-Type defaults to text/plain");
content = res9.put('data');
do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
_("POST: Content-Type defaults to text/plain");
content = res9.post('data');
do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
_("setHeader(): setting simple header");
res9.setHeader('X-What-Is-Weave', 'awesome');
do_check_eq(res9.headers['x-what-is-weave'], 'awesome');
content = res9.get();
do_check_eq(content, JSON.stringify({"x-what-is-weave": "awesome"}));
_("setHeader(): setting multiple headers, overwriting existing header");
res9.setHeader('X-WHAT-is-Weave', 'more awesomer',
'X-Another-Header', 'hello world');
do_check_eq(res9.headers['x-what-is-weave'], 'more awesomer');
do_check_eq(res9.headers['x-another-header'], 'hello world');
content = res9.get();
do_check_eq(content, JSON.stringify({"x-another-header": "hello world",
"x-what-is-weave": "more awesomer"}));
_("Setting headers object");
res9.headers = {};
content = res9.get();
do_check_eq(content, "{}");
_("PUT/POST: override default Content-Type");
res9.setHeader('Content-Type', 'application/foobar');
do_check_eq(res9.headers['content-type'], 'application/foobar');
content = res9.put('data');
do_check_eq(content, JSON.stringify({"content-type": "application/foobar"}));
content = res9.post('data');
do_check_eq(content, JSON.stringify({"content-type": "application/foobar"}));
_("X-Weave-Backoff header notifies observer");
let backoffInterval;
function onBackoff(subject, data) {
backoffInterval = subject;
}
Observers.add("weave:service:backoff:interval", onBackoff);
let res10 = new Resource("http://localhost:8080/backoff");
content = res10.get();
do_check_eq(backoffInterval, 600);
_("Error handling in _request() preserves exception information");
let error;
let res11 = new Resource("http://localhost:12345/does/not/exist");
try {
content = res11.get();
} catch(ex) {
error = ex;
}
do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED");
do_check_eq(typeof error.stack, "string");
server.stop();
}