Bug 1857386 [wpt PR 42383] - Update wpt metadata, a=testonly
[gecko.git] / netwerk / test / unit / test_captive_portal_service.js
blobd5c951d16c3bbcaad4a7805e735600d535697c05
1 "use strict";
3 const { HttpServer } = ChromeUtils.importESModule(
4   "resource://testing-common/httpd.sys.mjs"
5 );
7 let httpserver = null;
8 ChromeUtils.defineLazyGetter(this, "cpURI", function () {
9   return (
10     "http://localhost:" + httpserver.identity.primaryPort + "/captive.html"
11   );
12 });
14 const SUCCESS_STRING =
15   '<meta http-equiv="refresh" content="0;url=https://support.mozilla.org/kb/captive-portal"/>';
16 let cpResponse = SUCCESS_STRING;
17 function contentHandler(metadata, response) {
18   response.setHeader("Content-Type", "text/html");
19   response.bodyOutputStream.write(cpResponse, cpResponse.length);
22 const PREF_CAPTIVE_ENABLED = "network.captive-portal-service.enabled";
23 const PREF_CAPTIVE_TESTMODE = "network.captive-portal-service.testMode";
24 const PREF_CAPTIVE_ENDPOINT = "captivedetect.canonicalURL";
25 const PREF_CAPTIVE_MINTIME = "network.captive-portal-service.minInterval";
26 const PREF_CAPTIVE_MAXTIME = "network.captive-portal-service.maxInterval";
27 const PREF_DNS_NATIVE_IS_LOCALHOST = "network.dns.native-is-localhost";
29 const cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
30   Ci.nsICaptivePortalService
33 registerCleanupFunction(async () => {
34   Services.prefs.clearUserPref(PREF_CAPTIVE_ENABLED);
35   Services.prefs.clearUserPref(PREF_CAPTIVE_TESTMODE);
36   Services.prefs.clearUserPref(PREF_CAPTIVE_ENDPOINT);
37   Services.prefs.clearUserPref(PREF_CAPTIVE_MINTIME);
38   Services.prefs.clearUserPref(PREF_CAPTIVE_MAXTIME);
39   Services.prefs.clearUserPref(PREF_DNS_NATIVE_IS_LOCALHOST);
41   await new Promise(resolve => {
42     httpserver.stop(resolve);
43   });
44 });
46 function observerPromise(topic) {
47   return new Promise(resolve => {
48     let observer = {
49       QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
50       observe(aSubject, aTopic, aData) {
51         if (aTopic == topic) {
52           Services.obs.removeObserver(observer, topic);
53           resolve(aData);
54         }
55       },
56     };
57     Services.obs.addObserver(observer, topic);
58   });
61 add_task(function setup() {
62   httpserver = new HttpServer();
63   httpserver.registerPathHandler("/captive.html", contentHandler);
64   httpserver.start(-1);
66   Services.prefs.setCharPref(PREF_CAPTIVE_ENDPOINT, cpURI);
67   Services.prefs.setIntPref(PREF_CAPTIVE_MINTIME, 50);
68   Services.prefs.setIntPref(PREF_CAPTIVE_MAXTIME, 100);
69   Services.prefs.setBoolPref(PREF_CAPTIVE_TESTMODE, true);
70   Services.prefs.setBoolPref(PREF_DNS_NATIVE_IS_LOCALHOST, true);
71 });
73 add_task(async function test_simple() {
74   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
76   equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
78   let notification = observerPromise("network:captive-portal-connectivity");
79   // The service is started by nsIOService when the pref becomes true.
80   // We might want to add a method to do this in the future.
81   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
83   let observerPayload = await notification;
84   equal(observerPayload, "clear");
85   equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
87   cpResponse = "other";
88   notification = observerPromise("captive-portal-login");
89   cps.recheckCaptivePortal();
90   await notification;
91   equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
93   cpResponse = SUCCESS_STRING;
94   notification = observerPromise("captive-portal-login-success");
95   cps.recheckCaptivePortal();
96   await notification;
97   equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL);
98 });
100 // This test redirects to another URL which returns the same content.
101 // It should still be interpreted as a captive portal.
102 add_task(async function test_redirect_success() {
103   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
104   equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
106   httpserver.registerPathHandler("/succ.txt", (metadata, response) => {
107     response.setHeader("Content-Type", "text/html");
108     response.bodyOutputStream.write(cpResponse, cpResponse.length);
109   });
110   httpserver.registerPathHandler("/captive.html", (metadata, response) => {
111     response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily");
112     response.setHeader(
113       "Location",
114       `http://localhost:${httpserver.identity.primaryPort}/succ.txt`
115     );
116   });
118   let notification = observerPromise("captive-portal-login").then(
119     () => "login"
120   );
121   let succNotif = observerPromise("network:captive-portal-connectivity").then(
122     () => "connectivity"
123   );
124   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
126   let winner = await Promise.race([notification, succNotif]);
127   equal(winner, "login", "This should have been a login, not a success");
128   equal(
129     cps.state,
130     Ci.nsICaptivePortalService.LOCKED_PORTAL,
131     "Should be locked after redirect to same text"
132   );
135 // This redirects to another URI with a different content.
136 // We check that it triggers a captive portal login
137 add_task(async function test_redirect_bad() {
138   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
139   equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
141   httpserver.registerPathHandler("/bad.txt", (metadata, response) => {
142     response.setHeader("Content-Type", "text/html");
143     response.bodyOutputStream.write("bad", "bad".length);
144   });
146   httpserver.registerPathHandler("/captive.html", (metadata, response) => {
147     response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily");
148     response.setHeader(
149       "Location",
150       `http://localhost:${httpserver.identity.primaryPort}/bad.txt`
151     );
152   });
154   let notification = observerPromise("captive-portal-login");
155   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
157   await notification;
158   equal(
159     cps.state,
160     Ci.nsICaptivePortalService.LOCKED_PORTAL,
161     "Should be locked after redirect to bad text"
162   );
165 // This redirects to the same URI.
166 // We check that it triggers a captive portal login
167 add_task(async function test_redirect_loop() {
168   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
169   equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
171   // This is actually a redirect loop
172   httpserver.registerPathHandler("/captive.html", (metadata, response) => {
173     response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily");
174     response.setHeader("Location", cpURI);
175   });
177   let notification = observerPromise("captive-portal-login");
178   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
180   await notification;
181   equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
184 // This redirects to a https URI.
185 // We check that it triggers a captive portal login
186 add_task(async function test_redirect_https() {
187   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
188   equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
190   let h2Port = Services.env.get("MOZHTTP2_PORT");
191   Assert.notEqual(h2Port, null);
192   Assert.notEqual(h2Port, "");
194   // Any kind of redirection should trigger the captive portal login.
195   httpserver.registerPathHandler("/captive.html", (metadata, response) => {
196     response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily");
197     response.setHeader("Location", `https://foo.example.com:${h2Port}/exit`);
198   });
200   let notification = observerPromise("captive-portal-login");
201   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
203   await notification;
204   equal(
205     cps.state,
206     Ci.nsICaptivePortalService.LOCKED_PORTAL,
207     "Should be locked after redirect to https"
208   );
209   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
212 // This test uses a 511 status code to request a captive portal login
213 // We check that it triggers a captive portal login
214 // See RFC 6585 for details
215 add_task(async function test_511_error() {
216   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
217   equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
219   httpserver.registerPathHandler("/captive.html", (metadata, response) => {
220     response.setStatusLine(
221       metadata.httpVersion,
222       511,
223       "Network Authentication Required"
224     );
225     cpResponse = '<meta http-equiv="refresh" content="0;url=/login">';
226     contentHandler(metadata, response);
227   });
229   let notification = observerPromise("captive-portal-login");
230   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
232   await notification;
233   equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
236 // Any other 5xx HTTP error, is assumed to be an issue with the
237 // canonical web server, and should not trigger a captive portal login
238 add_task(async function test_generic_5xx_error() {
239   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
240   equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
242   let requests = 0;
243   httpserver.registerPathHandler("/captive.html", (metadata, response) => {
244     if (requests++ === 0) {
245       // on first attempt, send 503 error
246       response.setStatusLine(
247         metadata.httpVersion,
248         503,
249         "Internal Server Error"
250       );
251       cpResponse = "<h1>Internal Server Error</h1>";
252     } else {
253       // on retry, send canonical reply
254       cpResponse = SUCCESS_STRING;
255     }
256     contentHandler(metadata, response);
257   });
259   let notification = observerPromise("network:captive-portal-connectivity");
260   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
262   await notification;
263   equal(requests, 2);
264   equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
267 add_task(async function test_changed_notification() {
268   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
269   equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
271   httpserver.registerPathHandler("/captive.html", contentHandler);
272   cpResponse = SUCCESS_STRING;
274   let changedNotificationCount = 0;
275   let observer = {
276     QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
277     observe(aSubject, aTopic, aData) {
278       changedNotificationCount += 1;
279     },
280   };
281   Services.obs.addObserver(
282     observer,
283     "network:captive-portal-connectivity-changed"
284   );
286   let notification = observerPromise(
287     "network:captive-portal-connectivity-changed"
288   );
289   Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
290   await notification;
291   equal(changedNotificationCount, 1);
292   equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
294   notification = observerPromise("network:captive-portal-connectivity");
295   cps.recheckCaptivePortal();
296   await notification;
297   equal(changedNotificationCount, 1);
298   equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
300   notification = observerPromise("captive-portal-login");
301   cpResponse = "you are captive";
302   cps.recheckCaptivePortal();
303   await notification;
304   equal(changedNotificationCount, 1);
305   equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
307   notification = observerPromise("captive-portal-login-success");
308   cpResponse = SUCCESS_STRING;
309   cps.recheckCaptivePortal();
310   await notification;
311   equal(changedNotificationCount, 2);
312   equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL);
314   notification = observerPromise("captive-portal-login");
315   cpResponse = "you are captive";
316   cps.recheckCaptivePortal();
317   await notification;
318   equal(changedNotificationCount, 2);
319   equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
321   Services.obs.removeObserver(
322     observer,
323     "network:captive-portal-connectivity-changed"
324   );