netwerk/test/unit/test_range_requests.js

Wed, 31 Dec 2014 13:27:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 13:27:57 +0100
branch
TOR_BUG_3246
changeset 6
8bccb770b82d
permissions
-rw-r--r--

Ignore runtime configuration files generated during quality assurance.

michael@0 1 //
michael@0 2 // This test makes sure range-requests are sent and treated the way we want
michael@0 3 // See bug #612135 for a thorough discussion on the subject
michael@0 4 //
michael@0 5 // Necko does a range-request for a partial cache-entry iff
michael@0 6 //
michael@0 7 // 1) size of the cached entry < value of the cached Content-Length header
michael@0 8 // (not tested here - see bug #612135 comments 108-110)
michael@0 9 // 2) the size of the cached entry is > 0 (see bug #628607)
michael@0 10 // 3) the cached entry does not have a "no-store" Cache-Control header
michael@0 11 // 4) the cached entry does not have a Content-Encoding (see bug #613159)
michael@0 12 // 5) the request does not have a conditional-request header set by client
michael@0 13 // 6) nsHttpResponseHead::IsResumable() is true for the cached entry
michael@0 14 // 7) a basic positive test that makes sure byte ranges work
michael@0 15 // 8) ensure NS_ERROR_CORRUPTED_CONTENT is thrown when total entity size
michael@0 16 // of 206 does not match content-length of 200
michael@0 17 //
michael@0 18 // The test has one handler for each case and run_tests() fires one request
michael@0 19 // for each. None of the handlers should see a Range-header.
michael@0 20
michael@0 21 Cu.import("resource://testing-common/httpd.js");
michael@0 22
michael@0 23 var httpserver = null;
michael@0 24
michael@0 25 const clearTextBody = "This is a slightly longer test\n";
michael@0 26 const encodedBody = [0x1f, 0x8b, 0x08, 0x08, 0xef, 0x70, 0xe6, 0x4c, 0x00, 0x03, 0x74, 0x65, 0x78, 0x74, 0x66, 0x69,
michael@0 27 0x6c, 0x65, 0x2e, 0x74, 0x78, 0x74, 0x00, 0x0b, 0xc9, 0xc8, 0x2c, 0x56, 0x00, 0xa2, 0x44, 0x85,
michael@0 28 0xe2, 0x9c, 0xcc, 0xf4, 0x8c, 0x92, 0x9c, 0x4a, 0x85, 0x9c, 0xfc, 0xbc, 0xf4, 0xd4, 0x22, 0x85,
michael@0 29 0x92, 0xd4, 0xe2, 0x12, 0x2e, 0x2e, 0x00, 0x00, 0xe5, 0xe6, 0xf0, 0x20, 0x00, 0x00, 0x00];
michael@0 30 const decodedBody = [0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x73, 0x6c, 0x69, 0x67, 0x68, 0x74,
michael@0 31 0x6c, 0x79, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x65, 0x72, 0x20, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x0a];
michael@0 32
michael@0 33 const partial_data_length = 4;
michael@0 34 var port = null; // set in run_test
michael@0 35
michael@0 36 function make_channel(url, callback, ctx) {
michael@0 37 var ios = Cc["@mozilla.org/network/io-service;1"].
michael@0 38 getService(Ci.nsIIOService);
michael@0 39 var chan = ios.newChannel(url, "", null);
michael@0 40 return chan.QueryInterface(Ci.nsIHttpChannel);
michael@0 41 }
michael@0 42
michael@0 43 // StreamListener which cancels its request on first data available
michael@0 44 function Canceler(continueFn) {
michael@0 45 this.continueFn = continueFn;
michael@0 46 }
michael@0 47 Canceler.prototype = {
michael@0 48 QueryInterface: function(iid) {
michael@0 49 if (iid.equals(Ci.nsIStreamListener) ||
michael@0 50 iid.equals(Ci.nsIRequestObserver) ||
michael@0 51 iid.equals(Ci.nsISupports))
michael@0 52 return this;
michael@0 53 throw Components.results.NS_ERROR_NO_INTERFACE;
michael@0 54 },
michael@0 55 onStartRequest: function(request, context) { },
michael@0 56
michael@0 57 onDataAvailable: function(request, context, stream, offset, count) {
michael@0 58 request.QueryInterface(Ci.nsIChannel)
michael@0 59 .cancel(Components.results.NS_BINDING_ABORTED);
michael@0 60 },
michael@0 61 onStopRequest: function(request, context, status) {
michael@0 62 do_check_eq(status, Components.results.NS_BINDING_ABORTED);
michael@0 63 this.continueFn(request, null);
michael@0 64 }
michael@0 65 };
michael@0 66 // Simple StreamListener which performs no validations
michael@0 67 function MyListener(continueFn) {
michael@0 68 this.continueFn = continueFn;
michael@0 69 this._buffer = null;
michael@0 70 }
michael@0 71 MyListener.prototype = {
michael@0 72 QueryInterface: function(iid) {
michael@0 73 if (iid.equals(Ci.nsIStreamListener) ||
michael@0 74 iid.equals(Ci.nsIRequestObserver) ||
michael@0 75 iid.equals(Ci.nsISupports))
michael@0 76 return this;
michael@0 77 throw Components.results.NS_ERROR_NO_INTERFACE;
michael@0 78 },
michael@0 79 onStartRequest: function(request, context) { this._buffer = ""; },
michael@0 80
michael@0 81 onDataAvailable: function(request, context, stream, offset, count) {
michael@0 82 this._buffer = this._buffer.concat(read_stream(stream, count));
michael@0 83 },
michael@0 84 onStopRequest: function(request, context, status) {
michael@0 85 this.continueFn(request, this._buffer);
michael@0 86 }
michael@0 87 };
michael@0 88
michael@0 89 var case_8_range_request = false;
michael@0 90 function FailedChannelListener(continueFn) {
michael@0 91 this.continueFn = continueFn;
michael@0 92 }
michael@0 93 FailedChannelListener.prototype = {
michael@0 94 QueryInterface: function(iid) {
michael@0 95 if (iid.equals(Ci.nsIStreamListener) ||
michael@0 96 iid.equals(Ci.nsIRequestObserver) ||
michael@0 97 iid.equals(Ci.nsISupports))
michael@0 98 return this;
michael@0 99 throw Components.results.NS_ERROR_NO_INTERFACE;
michael@0 100 },
michael@0 101 onStartRequest: function(request, context) { },
michael@0 102
michael@0 103 onDataAvailable: function(request, context, stream, offset, count) { },
michael@0 104
michael@0 105 onStopRequest: function(request, context, status) {
michael@0 106 if (case_8_range_request)
michael@0 107 do_check_eq(status, Components.results.NS_ERROR_CORRUPTED_CONTENT);
michael@0 108 this.continueFn(request, null);
michael@0 109 }
michael@0 110 };
michael@0 111
michael@0 112 function received_cleartext(request, data) {
michael@0 113 do_check_eq(clearTextBody, data);
michael@0 114 testFinished();
michael@0 115 }
michael@0 116
michael@0 117 function setStdHeaders(response, length) {
michael@0 118 response.setHeader("Content-Type", "text/plain", false);
michael@0 119 response.setHeader("ETag", "Just testing");
michael@0 120 response.setHeader("Cache-Control", "max-age: 360000");
michael@0 121 response.setHeader("Accept-Ranges", "bytes");
michael@0 122 response.setHeader("Content-Length", "" + length);
michael@0 123 }
michael@0 124
michael@0 125 function handler_2(metadata, response) {
michael@0 126 setStdHeaders(response, clearTextBody.length);
michael@0 127 do_check_false(metadata.hasHeader("Range"));
michael@0 128 response.bodyOutputStream.write(clearTextBody, clearTextBody.length);
michael@0 129 }
michael@0 130 function received_partial_2(request, data) {
michael@0 131 do_check_eq(data, undefined);
michael@0 132 var chan = make_channel("http://localhost:" + port + "/test_2");
michael@0 133 chan.asyncOpen(new ChannelListener(received_cleartext, null), null);
michael@0 134 }
michael@0 135
michael@0 136 var case_3_request_no = 0;
michael@0 137 function handler_3(metadata, response) {
michael@0 138 var body = clearTextBody;
michael@0 139 setStdHeaders(response, body.length);
michael@0 140 response.setHeader("Cache-Control", "no-store", false);
michael@0 141 switch (case_3_request_no) {
michael@0 142 case 0:
michael@0 143 do_check_false(metadata.hasHeader("Range"));
michael@0 144 body = body.slice(0, partial_data_length);
michael@0 145 response.processAsync();
michael@0 146 response.bodyOutputStream.write(body, body.length);
michael@0 147 response.finish();
michael@0 148 break;
michael@0 149 case 1:
michael@0 150 do_check_false(metadata.hasHeader("Range"));
michael@0 151 response.bodyOutputStream.write(body, body.length);
michael@0 152 break;
michael@0 153 default:
michael@0 154 response.setStatusLine(metadata.httpVersion, 404, "Not Found");
michael@0 155 }
michael@0 156 case_3_request_no++;
michael@0 157 }
michael@0 158 function received_partial_3(request, data) {
michael@0 159 do_check_eq(partial_data_length, data.length);
michael@0 160 var chan = make_channel("http://localhost:" + port + "/test_3");
michael@0 161 chan.asyncOpen(new ChannelListener(received_cleartext, null), null);
michael@0 162 }
michael@0 163
michael@0 164 var case_4_request_no = 0;
michael@0 165 function handler_4(metadata, response) {
michael@0 166 switch (case_4_request_no) {
michael@0 167 case 0:
michael@0 168 do_check_false(metadata.hasHeader("Range"));
michael@0 169 var body = encodedBody;
michael@0 170 setStdHeaders(response, body.length);
michael@0 171 response.setHeader("Content-Encoding", "gzip", false);
michael@0 172 body = body.slice(0, partial_data_length);
michael@0 173 var bos = Cc["@mozilla.org/binaryoutputstream;1"]
michael@0 174 .createInstance(Ci.nsIBinaryOutputStream);
michael@0 175 bos.setOutputStream(response.bodyOutputStream);
michael@0 176 response.processAsync();
michael@0 177 bos.writeByteArray(body, body.length);
michael@0 178 response.finish();
michael@0 179 break;
michael@0 180 case 1:
michael@0 181 do_check_false(metadata.hasHeader("Range"));
michael@0 182 setStdHeaders(response, clearTextBody.length);
michael@0 183 response.bodyOutputStream.write(clearTextBody, clearTextBody.length);
michael@0 184 break;
michael@0 185 default:
michael@0 186 response.setStatusLine(metadata.httpVersion, 404, "Not Found");
michael@0 187 }
michael@0 188 case_4_request_no++;
michael@0 189 }
michael@0 190 function received_partial_4(request, data) {
michael@0 191 // checking length does not work with encoded data
michael@0 192 // do_check_eq(partial_data_length, data.length);
michael@0 193 var chan = make_channel("http://localhost:" + port + "/test_4");
michael@0 194 chan.asyncOpen(new MyListener(received_cleartext), null);
michael@0 195 }
michael@0 196
michael@0 197 var case_5_request_no = 0;
michael@0 198 function handler_5(metadata, response) {
michael@0 199 var body = clearTextBody;
michael@0 200 setStdHeaders(response, body.length);
michael@0 201 switch (case_5_request_no) {
michael@0 202 case 0:
michael@0 203 do_check_false(metadata.hasHeader("Range"));
michael@0 204 body = body.slice(0, partial_data_length);
michael@0 205 response.processAsync();
michael@0 206 response.bodyOutputStream.write(body, body.length);
michael@0 207 response.finish();
michael@0 208 break;
michael@0 209 case 1:
michael@0 210 do_check_false(metadata.hasHeader("Range"));
michael@0 211 response.bodyOutputStream.write(body, body.length);
michael@0 212 break;
michael@0 213 default:
michael@0 214 response.setStatusLine(metadata.httpVersion, 404, "Not Found");
michael@0 215 }
michael@0 216 case_5_request_no++;
michael@0 217 }
michael@0 218 function received_partial_5(request, data) {
michael@0 219 do_check_eq(partial_data_length, data.length);
michael@0 220 var chan = make_channel("http://localhost:" + port + "/test_5");
michael@0 221 chan.setRequestHeader("If-Match", "Some eTag", false);
michael@0 222 chan.asyncOpen(new ChannelListener(received_cleartext, null), null);
michael@0 223 }
michael@0 224
michael@0 225 var case_6_request_no = 0;
michael@0 226 function handler_6(metadata, response) {
michael@0 227 switch (case_6_request_no) {
michael@0 228 case 0:
michael@0 229 do_check_false(metadata.hasHeader("Range"));
michael@0 230 var body = clearTextBody;
michael@0 231 setStdHeaders(response, body.length);
michael@0 232 response.setHeader("Accept-Ranges", "", false);
michael@0 233 body = body.slice(0, partial_data_length);
michael@0 234 response.processAsync();
michael@0 235 response.bodyOutputStream.write(body, body.length);
michael@0 236 response.finish();
michael@0 237 break;
michael@0 238 case 1:
michael@0 239 do_check_false(metadata.hasHeader("Range"));
michael@0 240 setStdHeaders(response, clearTextBody.length);
michael@0 241 response.bodyOutputStream.write(clearTextBody, clearTextBody.length);
michael@0 242 break;
michael@0 243 default:
michael@0 244 response.setStatusLine(metadata.httpVersion, 404, "Not Found");
michael@0 245 }
michael@0 246 case_6_request_no++;
michael@0 247 }
michael@0 248 function received_partial_6(request, data) {
michael@0 249 // would like to verify that the response does not have Accept-Ranges
michael@0 250 do_check_eq(partial_data_length, data.length);
michael@0 251 var chan = make_channel("http://localhost:" + port + "/test_6");
michael@0 252 chan.asyncOpen(new ChannelListener(received_cleartext, null), null);
michael@0 253 }
michael@0 254
michael@0 255 const simpleBody = "0123456789";
michael@0 256
michael@0 257 function received_simple(request, data) {
michael@0 258 do_check_eq(simpleBody, data);
michael@0 259 testFinished();
michael@0 260 }
michael@0 261
michael@0 262 var case_7_request_no = 0;
michael@0 263 function handler_7(metadata, response) {
michael@0 264 switch (case_7_request_no) {
michael@0 265 case 0:
michael@0 266 do_check_false(metadata.hasHeader("Range"));
michael@0 267 response.setHeader("Content-Type", "text/plain", false);
michael@0 268 response.setHeader("ETag", "test7Etag");
michael@0 269 response.setHeader("Accept-Ranges", "bytes");
michael@0 270 response.setHeader("Cache-Control", "max-age=360000");
michael@0 271 response.setHeader("Content-Length", "10");
michael@0 272 response.processAsync();
michael@0 273 response.bodyOutputStream.write(simpleBody.slice(0, 4), 4);
michael@0 274 response.finish();
michael@0 275 break;
michael@0 276 case 1:
michael@0 277 response.setHeader("Content-Type", "text/plain", false);
michael@0 278 response.setHeader("ETag", "test7Etag");
michael@0 279 if (metadata.hasHeader("Range")) {
michael@0 280 do_check_true(metadata.hasHeader("If-Range"));
michael@0 281 response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
michael@0 282 response.setHeader("Content-Range", "4-9/10");
michael@0 283 response.setHeader("Content-Length", "6");
michael@0 284 response.bodyOutputStream.write(simpleBody.slice(4), 6);
michael@0 285 } else {
michael@0 286 response.setHeader("Content-Length", "10");
michael@0 287 response.bodyOutputStream.write(simpleBody, 10);
michael@0 288 }
michael@0 289 break;
michael@0 290 default:
michael@0 291 response.setStatusLine(metadata.httpVersion, 404, "Not Found");
michael@0 292 }
michael@0 293 case_7_request_no++;
michael@0 294 }
michael@0 295 function received_partial_7(request, data) {
michael@0 296 // make sure we get the first 4 bytes
michael@0 297 do_check_eq(4, data.length);
michael@0 298 // do it again to get the rest
michael@0 299 var chan = make_channel("http://localhost:" + port + "/test_7");
michael@0 300 chan.asyncOpen(new ChannelListener(received_simple, null), null);
michael@0 301 }
michael@0 302
michael@0 303 var case_8_request_no = 0;
michael@0 304 function handler_8(metadata, response) {
michael@0 305 switch (case_8_request_no) {
michael@0 306 case 0:
michael@0 307 do_check_false(metadata.hasHeader("Range"));
michael@0 308 response.setHeader("Content-Type", "text/plain", false);
michael@0 309 response.setHeader("ETag", "test8Etag");
michael@0 310 response.setHeader("Accept-Ranges", "bytes");
michael@0 311 response.setHeader("Cache-Control", "max-age=360000");
michael@0 312 response.setHeader("Content-Length", "10");
michael@0 313 response.processAsync();
michael@0 314 response.bodyOutputStream.write(simpleBody.slice(0, 4), 4);
michael@0 315 response.finish();
michael@0 316 break;
michael@0 317 case 1:
michael@0 318 if (metadata.hasHeader("Range")) {
michael@0 319 do_check_true(metadata.hasHeader("If-Range"));
michael@0 320 case_8_range_request = true;
michael@0 321 }
michael@0 322 response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
michael@0 323 response.setHeader("Content-Type", "text/plain", false);
michael@0 324 response.setHeader("ETag", "test8Etag");
michael@0 325 response.setHeader("Content-Range", "4-8/9"); // intentionally broken
michael@0 326 response.setHeader("Content-Length", "5");
michael@0 327 response.bodyOutputStream.write(simpleBody.slice(4), 5);
michael@0 328 break;
michael@0 329 default:
michael@0 330 response.setStatusLine(metadata.httpVersion, 404, "Not Found");
michael@0 331 }
michael@0 332 case_8_request_no++;
michael@0 333 }
michael@0 334 function received_partial_8(request, data) {
michael@0 335 // make sure we get the first 4 bytes
michael@0 336 do_check_eq(4, data.length);
michael@0 337 // do it again to get the rest
michael@0 338 var chan = make_channel("http://localhost:" + port + "/test_8");
michael@0 339 chan.asyncOpen(new FailedChannelListener(testFinished, null, CL_EXPECT_LATE_FAILURE), null);
michael@0 340 }
michael@0 341
michael@0 342 // Simple mechanism to keep track of tests and stop the server
michael@0 343 var numTestsFinished = 0;
michael@0 344 function testFinished() {
michael@0 345 if (++numTestsFinished == 7)
michael@0 346 httpserver.stop(do_test_finished);
michael@0 347 }
michael@0 348
michael@0 349 function run_test() {
michael@0 350 httpserver = new HttpServer();
michael@0 351 httpserver.registerPathHandler("/test_2", handler_2);
michael@0 352 httpserver.registerPathHandler("/test_3", handler_3);
michael@0 353 httpserver.registerPathHandler("/test_4", handler_4);
michael@0 354 httpserver.registerPathHandler("/test_5", handler_5);
michael@0 355 httpserver.registerPathHandler("/test_6", handler_6);
michael@0 356 httpserver.registerPathHandler("/test_7", handler_7);
michael@0 357 httpserver.registerPathHandler("/test_8", handler_8);
michael@0 358 httpserver.start(-1);
michael@0 359
michael@0 360 port = httpserver.identity.primaryPort;
michael@0 361
michael@0 362 // wipe out cached content
michael@0 363 evict_cache_entries();
michael@0 364
michael@0 365 // Case 2: zero-length partial entry must not trigger range-request
michael@0 366 var chan = make_channel("http://localhost:" + port + "/test_2");
michael@0 367 chan.asyncOpen(new Canceler(received_partial_2), null);
michael@0 368
michael@0 369 // Case 3: no-store response must not trigger range-request
michael@0 370 var chan = make_channel("http://localhost:" + port + "/test_3");
michael@0 371 chan.asyncOpen(new MyListener(received_partial_3), null);
michael@0 372
michael@0 373 // Case 4: response with content-encoding must not trigger range-request
michael@0 374 var chan = make_channel("http://localhost:" + port + "/test_4");
michael@0 375 chan.asyncOpen(new MyListener(received_partial_4), null);
michael@0 376
michael@0 377 // Case 5: conditional request-header set by client
michael@0 378 var chan = make_channel("http://localhost:" + port + "/test_5");
michael@0 379 chan.asyncOpen(new MyListener(received_partial_5), null);
michael@0 380
michael@0 381 // Case 6: response is not resumable (drop the Accept-Ranges header)
michael@0 382 var chan = make_channel("http://localhost:" + port + "/test_6");
michael@0 383 chan.asyncOpen(new MyListener(received_partial_6), null);
michael@0 384
michael@0 385 // Case 7: a basic positive test
michael@0 386 var chan = make_channel("http://localhost:" + port + "/test_7");
michael@0 387 chan.asyncOpen(new MyListener(received_partial_7), null);
michael@0 388
michael@0 389 // Case 8: check that mismatched 206 and 200 sizes throw error
michael@0 390 var chan = make_channel("http://localhost:" + port + "/test_8");
michael@0 391 chan.asyncOpen(new MyListener(received_partial_8), null);
michael@0 392
michael@0 393 do_test_pending();
michael@0 394 }

mercurial