Bug 1857386 [wpt PR 42383] - Update wpt metadata, a=testonly
[gecko.git] / netwerk / test / unit / test_resumable_channel.js
blob0407aa498306cd831160bb04467cdaea24cd4b94
1 /* Tests various aspects of nsIResumableChannel in combination with HTTP */
2 "use strict";
4 const { HttpServer } = ChromeUtils.importESModule(
5   "resource://testing-common/httpd.sys.mjs"
6 );
8 ChromeUtils.defineLazyGetter(this, "URL", function () {
9   return "http://localhost:" + httpserver.identity.primaryPort;
10 });
12 var httpserver = null;
14 const NS_ERROR_ENTITY_CHANGED = 0x804b0020;
15 const NS_ERROR_NOT_RESUMABLE = 0x804b0019;
17 const rangeBody = "Body of the range request handler.\r\n";
19 function make_channel(url, callback, ctx) {
20   return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
23 function AuthPrompt2() {}
25 AuthPrompt2.prototype = {
26   user: "guest",
27   pass: "guest",
29   QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
31   promptAuth: function ap2_promptAuth(channel, level, authInfo) {
32     authInfo.username = this.user;
33     authInfo.password = this.pass;
34     return true;
35   },
37   asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) {
38     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
39   },
42 function Requestor() {}
44 Requestor.prototype = {
45   QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
47   getInterface: function requestor_gi(iid) {
48     if (iid.equals(Ci.nsIAuthPrompt2)) {
49       // Allow the prompt to store state by caching it here
50       if (!this.prompt2) {
51         this.prompt2 = new AuthPrompt2();
52       }
53       return this.prompt2;
54     }
56     throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
57   },
59   prompt2: null,
62 function run_test() {
63   dump("*** run_test\n");
64   httpserver = new HttpServer();
65   httpserver.registerPathHandler("/auth", authHandler);
66   httpserver.registerPathHandler("/range", rangeHandler);
67   httpserver.registerPathHandler("/acceptranges", acceptRangesHandler);
68   httpserver.registerPathHandler("/redir", redirHandler);
70   var entityID;
72   function get_entity_id(request, data, ctx) {
73     dump("*** get_entity_id()\n");
74     Assert.ok(
75       request instanceof Ci.nsIResumableChannel,
76       "must be a resumable channel"
77     );
78     entityID = request.entityID;
79     dump("*** entity id = " + entityID + "\n");
81     // Try a non-resumable URL (responds with 200)
82     var chan = make_channel(URL);
83     chan.nsIResumableChannel.resumeAt(1, entityID);
84     chan.asyncOpen(new ChannelListener(try_resume, null, CL_EXPECT_FAILURE));
85   }
87   function try_resume(request, data, ctx) {
88     dump("*** try_resume()\n");
89     Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
91     // Try a successful resume
92     var chan = make_channel(URL + "/range");
93     chan.nsIResumableChannel.resumeAt(1, entityID);
94     chan.asyncOpen(new ChannelListener(try_resume_zero, null));
95   }
97   function try_resume_zero(request, data, ctx) {
98     dump("*** try_resume_zero()\n");
99     Assert.ok(request.nsIHttpChannel.requestSucceeded);
100     Assert.equal(data, rangeBody.substring(1));
102     // Try a server which doesn't support range requests
103     var chan = make_channel(URL + "/acceptranges");
104     chan.nsIResumableChannel.resumeAt(0, entityID);
105     chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "none", false);
106     chan.asyncOpen(new ChannelListener(try_no_range, null, CL_EXPECT_FAILURE));
107   }
109   function try_no_range(request, data, ctx) {
110     dump("*** try_no_range()\n");
111     Assert.ok(request.nsIHttpChannel.requestSucceeded);
112     Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
114     // Try a server which supports "bytes" range requests
115     var chan = make_channel(URL + "/acceptranges");
116     chan.nsIResumableChannel.resumeAt(0, entityID);
117     chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "bytes", false);
118     chan.asyncOpen(new ChannelListener(try_bytes_range, null));
119   }
121   function try_bytes_range(request, data, ctx) {
122     dump("*** try_bytes_range()\n");
123     Assert.ok(request.nsIHttpChannel.requestSucceeded);
124     Assert.equal(data, rangeBody);
126     // Try a server which supports "foo" and "bar" range requests
127     var chan = make_channel(URL + "/acceptranges");
128     chan.nsIResumableChannel.resumeAt(0, entityID);
129     chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foo, bar", false);
130     chan.asyncOpen(
131       new ChannelListener(try_foo_bar_range, null, CL_EXPECT_FAILURE)
132     );
133   }
135   function try_foo_bar_range(request, data, ctx) {
136     dump("*** try_foo_bar_range()\n");
137     Assert.ok(request.nsIHttpChannel.requestSucceeded);
138     Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
140     // Try a server which supports "foobar" range requests
141     var chan = make_channel(URL + "/acceptranges");
142     chan.nsIResumableChannel.resumeAt(0, entityID);
143     chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foobar", false);
144     chan.asyncOpen(
145       new ChannelListener(try_foobar_range, null, CL_EXPECT_FAILURE)
146     );
147   }
149   function try_foobar_range(request, data, ctx) {
150     dump("*** try_foobar_range()\n");
151     Assert.ok(request.nsIHttpChannel.requestSucceeded);
152     Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
154     // Try a server which supports "bytes" and "foobar" range requests
155     var chan = make_channel(URL + "/acceptranges");
156     chan.nsIResumableChannel.resumeAt(0, entityID);
157     chan.nsIHttpChannel.setRequestHeader(
158       "X-Range-Type",
159       "bytes, foobar",
160       false
161     );
162     chan.asyncOpen(new ChannelListener(try_bytes_foobar_range, null));
163   }
165   function try_bytes_foobar_range(request, data, ctx) {
166     dump("*** try_bytes_foobar_range()\n");
167     Assert.ok(request.nsIHttpChannel.requestSucceeded);
168     Assert.equal(data, rangeBody);
170     // Try a server which supports "bytesfoo" and "bar" range requests
171     var chan = make_channel(URL + "/acceptranges");
172     chan.nsIResumableChannel.resumeAt(0, entityID);
173     chan.nsIHttpChannel.setRequestHeader(
174       "X-Range-Type",
175       "bytesfoo, bar",
176       false
177     );
178     chan.asyncOpen(
179       new ChannelListener(try_bytesfoo_bar_range, null, CL_EXPECT_FAILURE)
180     );
181   }
183   function try_bytesfoo_bar_range(request, data, ctx) {
184     dump("*** try_bytesfoo_bar_range()\n");
185     Assert.ok(request.nsIHttpChannel.requestSucceeded);
186     Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
188     // Try a server which doesn't send Accept-Ranges header at all
189     var chan = make_channel(URL + "/acceptranges");
190     chan.nsIResumableChannel.resumeAt(0, entityID);
191     chan.asyncOpen(new ChannelListener(try_no_accept_ranges, null));
192   }
194   function try_no_accept_ranges(request, data, ctx) {
195     dump("*** try_no_accept_ranges()\n");
196     Assert.ok(request.nsIHttpChannel.requestSucceeded);
197     Assert.equal(data, rangeBody);
199     // Try a successful suspend/resume from 0
200     var chan = make_channel(URL + "/range");
201     chan.nsIResumableChannel.resumeAt(0, entityID);
202     chan.asyncOpen(
203       new ChannelListener(
204         try_suspend_resume,
205         null,
206         CL_SUSPEND | CL_EXPECT_3S_DELAY
207       )
208     );
209   }
211   function try_suspend_resume(request, data, ctx) {
212     dump("*** try_suspend_resume()\n");
213     Assert.ok(request.nsIHttpChannel.requestSucceeded);
214     Assert.equal(data, rangeBody);
216     // Try a successful resume from 0
217     var chan = make_channel(URL + "/range");
218     chan.nsIResumableChannel.resumeAt(0, entityID);
219     chan.asyncOpen(new ChannelListener(success, null));
220   }
222   function success(request, data, ctx) {
223     dump("*** success()\n");
224     Assert.ok(request.nsIHttpChannel.requestSucceeded);
225     Assert.equal(data, rangeBody);
227     // Authentication (no password; working resume)
228     // (should not give us any data)
229     var chan = make_channel(URL + "/range");
230     chan.nsIResumableChannel.resumeAt(1, entityID);
231     chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false);
232     chan.asyncOpen(
233       new ChannelListener(test_auth_nopw, null, CL_EXPECT_FAILURE)
234     );
235   }
237   function test_auth_nopw(request, data, ctx) {
238     dump("*** test_auth_nopw()\n");
239     Assert.ok(!request.nsIHttpChannel.requestSucceeded);
240     Assert.equal(request.status, NS_ERROR_ENTITY_CHANGED);
242     // Authentication + not working resume
243     var chan = make_channel(
244       "http://guest:guest@localhost:" +
245         httpserver.identity.primaryPort +
246         "/auth"
247     );
248     chan.nsIResumableChannel.resumeAt(1, entityID);
249     chan.notificationCallbacks = new Requestor();
250     chan.asyncOpen(new ChannelListener(test_auth, null, CL_EXPECT_FAILURE));
251   }
252   function test_auth(request, data, ctx) {
253     dump("*** test_auth()\n");
254     Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
255     Assert.ok(request.nsIHttpChannel.responseStatus < 300);
257     // Authentication + working resume
258     var chan = make_channel(
259       "http://guest:guest@localhost:" +
260         httpserver.identity.primaryPort +
261         "/range"
262     );
263     chan.nsIResumableChannel.resumeAt(1, entityID);
264     chan.notificationCallbacks = new Requestor();
265     chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false);
266     chan.asyncOpen(new ChannelListener(test_auth_resume, null));
267   }
269   function test_auth_resume(request, data, ctx) {
270     dump("*** test_auth_resume()\n");
271     Assert.equal(data, rangeBody.substring(1));
272     Assert.ok(request.nsIHttpChannel.requestSucceeded);
274     // 404 page (same content length as real content)
275     var chan = make_channel(URL + "/range");
276     chan.nsIResumableChannel.resumeAt(1, entityID);
277     chan.nsIHttpChannel.setRequestHeader("X-Want-404", "true", false);
278     chan.asyncOpen(new ChannelListener(test_404, null, CL_EXPECT_FAILURE));
279   }
281   function test_404(request, data, ctx) {
282     dump("*** test_404()\n");
283     Assert.equal(request.status, NS_ERROR_ENTITY_CHANGED);
284     Assert.equal(request.nsIHttpChannel.responseStatus, 404);
286     // 416 Requested Range Not Satisfiable
287     var chan = make_channel(URL + "/range");
288     chan.nsIResumableChannel.resumeAt(1000, entityID);
289     chan.asyncOpen(new ChannelListener(test_416, null, CL_EXPECT_FAILURE));
290   }
292   function test_416(request, data, ctx) {
293     dump("*** test_416()\n");
294     Assert.equal(request.status, NS_ERROR_ENTITY_CHANGED);
295     Assert.equal(request.nsIHttpChannel.responseStatus, 416);
297     // Redirect + successful resume
298     var chan = make_channel(URL + "/redir");
299     chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/range", false);
300     chan.nsIResumableChannel.resumeAt(1, entityID);
301     chan.asyncOpen(new ChannelListener(test_redir_resume, null));
302   }
304   function test_redir_resume(request, data, ctx) {
305     dump("*** test_redir_resume()\n");
306     Assert.ok(request.nsIHttpChannel.requestSucceeded);
307     Assert.equal(data, rangeBody.substring(1));
308     Assert.equal(request.nsIHttpChannel.responseStatus, 206);
310     // Redirect + failed resume
311     var chan = make_channel(URL + "/redir");
312     chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/", false);
313     chan.nsIResumableChannel.resumeAt(1, entityID);
314     chan.asyncOpen(
315       new ChannelListener(test_redir_noresume, null, CL_EXPECT_FAILURE)
316     );
317   }
319   function test_redir_noresume(request, data, ctx) {
320     dump("*** test_redir_noresume()\n");
321     Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
323     httpserver.stop(do_test_finished);
324   }
326   httpserver.start(-1);
327   var chan = make_channel(URL + "/range");
328   chan.asyncOpen(new ChannelListener(get_entity_id, null));
329   do_test_pending();
332 // HANDLERS
334 function handleAuth(metadata, response) {
335   // btoa("guest:guest"), but that function is not available here
336   var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q=";
338   if (
339     metadata.hasHeader("Authorization") &&
340     metadata.getHeader("Authorization") == expectedHeader
341   ) {
342     response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
343     response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
345     return true;
346   }
347   // didn't know guest:guest, failure
348   response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
349   response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
350   return false;
353 // /auth
354 function authHandler(metadata, response) {
355   response.setHeader("Content-Type", "text/html", false);
356   var body = handleAuth(metadata, response) ? "success" : "failure";
357   response.bodyOutputStream.write(body, body.length);
360 // /range
361 function rangeHandler(metadata, response) {
362   response.setHeader("Content-Type", "text/html", false);
364   if (metadata.hasHeader("X-Need-Auth")) {
365     if (!handleAuth(metadata, response)) {
366       body = "auth failed";
367       response.bodyOutputStream.write(body, body.length);
368       return;
369     }
370   }
372   if (metadata.hasHeader("X-Want-404")) {
373     response.setStatusLine(metadata.httpVersion, 404, "Not Found");
374     body = rangeBody;
375     response.bodyOutputStream.write(body, body.length);
376     return;
377   }
379   var body = rangeBody;
381   if (metadata.hasHeader("Range")) {
382     // Syntax: bytes=[from]-[to] (we don't support multiple ranges)
383     var matches = metadata
384       .getHeader("Range")
385       .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
386     var from = matches[1] === undefined ? 0 : matches[1];
387     var to = matches[2] === undefined ? rangeBody.length - 1 : matches[2];
388     if (from >= rangeBody.length) {
389       response.setStatusLine(metadata.httpVersion, 416, "Start pos too high");
390       response.setHeader("Content-Range", "*/" + rangeBody.length, false);
391       return;
392     }
393     body = body.substring(from, to + 1);
394     // always respond to successful range requests with 206
395     response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
396     response.setHeader(
397       "Content-Range",
398       from + "-" + to + "/" + rangeBody.length,
399       false
400     );
401   }
403   response.bodyOutputStream.write(body, body.length);
406 // /acceptranges
407 function acceptRangesHandler(metadata, response) {
408   response.setHeader("Content-Type", "text/html", false);
409   if (metadata.hasHeader("X-Range-Type")) {
410     response.setHeader(
411       "Accept-Ranges",
412       metadata.getHeader("X-Range-Type"),
413       false
414     );
415   }
416   response.bodyOutputStream.write(rangeBody, rangeBody.length);
419 // /redir
420 function redirHandler(metadata, response) {
421   response.setStatusLine(metadata.httpVersion, 302, "Found");
422   response.setHeader("Content-Type", "text/html", false);
423   response.setHeader("Location", metadata.getHeader("X-Redir-To"), false);
424   var body = "redirect\r\n";
425   response.bodyOutputStream.write(body, body.length);