summaryrefslogtreecommitdiffstats
path: root/dom/tethering/tests/marionette/head.js
blob: c6b6abe26e4051577721a76a8218a225a929bb44 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

const TYPE_WIFI = "wifi";
const TYPE_BLUETOOTH = "bt";
const TYPE_USB = "usb";

/**
 * General tethering setting.
 */
const TETHERING_SETTING_IP = "192.168.1.1";
const TETHERING_SETTNG_PREFIX = "24";
const TETHERING_SETTING_START_IP = "192.168.1.10";
const TETHERING_SETTING_END_IP = "192.168.1.30";
const TETHERING_SETTING_DNS1 = "8.8.8.8";
const TETHERING_SETTING_DNS2 = "8.8.4.4";

const TETHERING_NETWORK_ADDR = "192.168.1.0/24";

/**
 * Wifi tethering setting.
 */
const TETHERING_SETTING_SSID = "FirefoxHotSpot";
const TETHERING_SETTING_SECURITY = "open";
const TETHERING_SETTING_KEY = "1234567890";

const SETTINGS_RIL_DATA_ENABLED = 'ril.data.enabled';
const SETTINGS_KEY_DATA_APN_SETTINGS = "ril.data.apnSettings";

// Emulate Promise.jsm semantics.
Promise.defer = function() { return new Deferred(); }
function Deferred()  {
  this.promise = new Promise(function(resolve, reject) {
    this.resolve = resolve;
    this.reject = reject;
  }.bind(this));
  Object.freeze(this);
}

var gTestSuite = (function() {
  let suite = {};

  let tetheringManager;
  let pendingEmulatorShellCount = 0;

  /**
   * A wrapper function of "is".
   *
   * Calls the marionette function "is" as well as throws an exception
   * if the givens values are not equal.
   *
   * @param value1
   *        Any type of value to compare.
   *
   * @param value2
   *        Any type of value to compare.
   *
   * @param message
   *        Debug message for this check.
   *
   */
  function isOrThrow(value1, value2, message) {
    is(value1, value2, message);
    if (value1 !== value2) {
      throw message;
    }
  }

  /**
   * Send emulator shell command with safe guard.
   *
   * We should only call |finish()| after all emulator command transactions
   * end, so here comes with the pending counter.  Resolve when the emulator
   * gives positive response, and reject otherwise.
   *
   * Fulfill params:
   *   result -- an array of emulator response lines.
   * Reject params:
   *   result -- an array of emulator response lines.
   *
   * @param aCommand
   *        A string command to be passed to emulator through its telnet console.
   *
   * @return A deferred promise.
   */
  function runEmulatorShellSafe(aCommand) {
    let deferred = Promise.defer();

    ++pendingEmulatorShellCount;
    runEmulatorShell(aCommand, function(aResult) {
      --pendingEmulatorShellCount;

      ok(true, "Emulator shell response: " + JSON.stringify(aResult));
      if (Array.isArray(aResult)) {
        deferred.resolve(aResult);
      } else {
        deferred.reject(aResult);
      }
    });

    return deferred.promise;
  }

  /**
   * Wait for timeout.
   *
   * Resolve when the given duration elapsed. Never reject.
   *
   * Fulfill params: (none)
   *
   * @param aTimeoutMs
   *        The duration after which the timeout event should occurs.
   *
   * @return A deferred promise.
   */
  function waitForTimeout(aTimeoutMs) {
    let deferred = Promise.defer();

    setTimeout(function() {
      deferred.resolve();
    }, aTimeoutMs);

    return deferred.promise;
  }

  /**
   * Get mozSettings value specified by @aKey.
   *
   * Resolve if that mozSettings value is retrieved successfully, reject
   * otherwise.
   *
   * Fulfill params:
   *   The corresponding mozSettings value of the key.
   * Reject params: (none)
   *
   * @param aKey
   *        A string.
   *
   * @return A deferred promise.
   */
  function getSettings(aKey) {
    let request = navigator.mozSettings.createLock().get(aKey);

    return wrapDomRequestAsPromise(request)
      .then(function resolve(aEvent) {
        ok(true, "getSettings(" + aKey + ") - success");
        return aEvent.target.result[aKey];
      }, function reject(aEvent) {
        ok(false, "getSettings(" + aKey + ") - error");
        throw aEvent.target.error;
      });
  }

  /**
   * Set mozSettings values.
   *
   * Resolve if that mozSettings value is set successfully, reject otherwise.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aSettings
   *        An object of format |{key1: value1, key2: value2, ...}|.
   * @return A deferred promise.
   */
  function setSettings(aSettings) {
    let lock = window.navigator.mozSettings.createLock();
    let request = lock.set(aSettings);
    let deferred = Promise.defer();
    lock.onsettingstransactionsuccess = function () {
        ok(true, "setSettings(" + JSON.stringify(aSettings) + ")");
      deferred.resolve();
    };
    lock.onsettingstransactionfailure = function (aEvent) {
        ok(false, "setSettings(" + JSON.stringify(aSettings) + ")");
      deferred.reject();
        throw aEvent.target.error;
    };
    return deferred.promise;
  }

  /**
   * Set mozSettings value with only one key.
   *
   * Resolve if that mozSettings value is set successfully, reject otherwise.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aKey
   *        A string key.
   * @param aValue
   *        An object value.
   * @param aAllowError [optional]
   *        A boolean value.  If set to true, an error response won't be treated
   *        as test failure.  Default: false.
   *
   * @return A deferred promise.
   */
  function setSettings1(aKey, aValue, aAllowError) {
    let settings = {};
    settings[aKey] = aValue;
    return setSettings(settings, aAllowError);
  }

  /**
   * Convenient MozSettings getter for SETTINGS_KEY_DATA_APN_SETTINGS.
   */
  function getDataApnSettings(aAllowError) {
    return getSettings(SETTINGS_KEY_DATA_APN_SETTINGS, aAllowError);
  }

  /**
   * Convenient MozSettings setter for SETTINGS_KEY_DATA_APN_SETTINGS.
   */
  function setDataApnSettings(aApnSettings, aAllowError) {
    return setSettings1(SETTINGS_KEY_DATA_APN_SETTINGS, aApnSettings, aAllowError);
  }

  /**
   * Set 'ro.tethering.dun_required' system property to 1. Note that this is a
   * 'ro' property, it can only be set once.
   */
  function setTetheringDunRequired() {
    return runEmulatorShellSafe(['setprop', 'ro.tethering.dun_required', '1']);
  }

  /**
   * Wrap DOMRequest onsuccess/onerror events to Promise resolve/reject.
   *
   * Fulfill params: A DOMEvent.
   * Reject params: A DOMEvent.
   *
   * @param aRequest
   *        A DOMRequest instance.
   *
   * @return A deferred promise.
   */
  function wrapDomRequestAsPromise(aRequest) {
    let deffered = Promise.defer();

    ok(aRequest instanceof DOMRequest,
      "aRequest is instanceof" + aRequest.constructor);

    aRequest.onsuccess = function(aEvent) {
      deffered.resolve(aEvent);
    };
    aRequest.onerror = function(aEvent) {
      deffered.reject(aEvent);
    };

    return deffered.promise;
  }

  /**
   * Wait for one named MozMobileConnection event.
   *
   * Resolve if that named event occurs.  Never reject.
   *
   * Fulfill params: the DOMEvent passed.
   *
   * @param aEventName
   *        A string event name.
   *
   * @return A deferred promise.
   */
  function waitForMobileConnectionEventOnce(aEventName, aServiceId) {
    aServiceId = aServiceId || 0;

    let deferred = Promise.defer();
    let mobileconnection = navigator.mozMobileConnections[aServiceId];

    mobileconnection.addEventListener(aEventName, function onevent(aEvent) {
      mobileconnection.removeEventListener(aEventName, onevent);

      ok(true, "Mobile connection event '" + aEventName + "' got.");
      deferred.resolve(aEvent);
    });

    return deferred.promise;
  }

  /**
   * Wait for RIL data being connected.
   *
   * This function will check |MozMobileConnection.data.connected| on
   * every 'datachange' event. Resolve when |MozMobileConnection.data.connected|
   * becomes the expected state. Never reject.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aConnected
   *        Boolean that indicates the desired data state.
   *
   * @param aServiceId [optional]
   *        A numeric DSDS service id. Default: 0.
   *
   * @return A deferred promise.
   */
  function waitForRilDataConnected(aConnected, aServiceId) {
    aServiceId = aServiceId || 0;

    return waitForMobileConnectionEventOnce('datachange', aServiceId)
      .then(function () {
        let mobileconnection = navigator.mozMobileConnections[aServiceId];
        if (mobileconnection.data.connected !== aConnected) {
          return waitForRilDataConnected(aConnected, aServiceId);
        }
      });
  }

  /**
   * Verify everything about routing when the wifi tethering is either on or off.
   *
   * We use two unix commands to verify the routing: 'netcfg' and 'ip route'.
   * For now the following two things will be checked:
   *   1) The default route interface should be 'rmnet0'.
   *   2) wlan0 is up and its ip is set to a private subnet.
   *
   * We also verify iptables output as netd's NatController will execute
   *   $ iptables -t nat -A POSTROUTING -o rmnet0 -j MASQUERADE
   *
   * For tethering through dun, we use 'ip rule' to find the secondary routing
   * table and look for default route on that table.
   *
   * Resolve when the verification is successful and reject otherwise.
   *
   * Fulfill params: (none)
   * Reject params: String that indicates the reason of rejection.
   *
   * @return A deferred promise.
   */
  function verifyTetheringRouting(aEnabled, aIsDun) {
    let netcfgResult = {};
    let ipRouteResult = {};
    let ipSecondaryRouteResult = {};

    // Execute 'netcfg' and parse to |netcfgResult|, each key of which is the
    // interface name and value is { ip(string) }.
    function exeAndParseNetcfg() {
      return runEmulatorShellSafe(['netcfg'])
        .then(function (aLines) {
          // Sample output:
          //
          // lo       UP     127.0.0.1/8   0x00000049 00:00:00:00:00:00
          // eth0     UP     10.0.2.15/24  0x00001043 52:54:00:12:34:56
          // rmnet1   DOWN   0.0.0.0/0   0x00001002 52:54:00:12:34:58
          // rmnet2   DOWN   0.0.0.0/0   0x00001002 52:54:00:12:34:59
          // rmnet3   DOWN   0.0.0.0/0   0x00001002 52:54:00:12:34:5a
          // wlan0    UP     192.168.1.1/24  0x00001043 52:54:00:12:34:5b
          // sit0     DOWN   0.0.0.0/0   0x00000080 00:00:00:00:00:00
          // rmnet0   UP     10.0.2.100/24  0x00001043 52:54:00:12:34:57
          //
          aLines.forEach(function (aLine) {
            let tokens = aLine.split(/\s+/);
            if (tokens.length < 5) {
              return;
            }
            let ifname = tokens[0];
            let ip = (tokens[2].split('/'))[0];
            netcfgResult[ifname] = { ip: ip };
          });
        });
    }

    // Execute 'ip route' and parse to |ipRouteResult|, each key of which is the
    // interface name and value is { src(string), default(boolean) }.
    function exeAndParseIpRoute() {
      return runEmulatorShellSafe(['ip', 'route'])
        .then(function (aLines) {
          // Sample output:
          //
          // 10.0.2.4 via 10.0.2.2 dev rmnet0
          // 10.0.2.3 via 10.0.2.2 dev rmnet0
          // 192.168.1.0/24 dev wlan0  proto kernel  scope link  src 192.168.1.1
          // 10.0.2.0/24 dev eth0  proto kernel  scope link  src 10.0.2.15
          // 10.0.2.0/24 dev rmnet0  proto kernel  scope link  src 10.0.2.100
          // default via 10.0.2.2 dev rmnet0
          // default via 10.0.2.2 dev eth0  metric 2
          //

          // Parse source ip for each interface.
          aLines.forEach(function (aLine) {
            let tokens = aLine.trim().split(/\s+/);
            let srcIndex = tokens.indexOf('src');
            if (srcIndex < 0 || srcIndex + 1 >= tokens.length) {
              return;
            }
            let ifname = tokens[2];
            let src = tokens[srcIndex + 1];
            ipRouteResult[ifname] = { src: src, default: false };
          });

          // Parse default interfaces.
          aLines.forEach(function (aLine) {
            let tokens = aLine.split(/\s+/);
            if (tokens.length < 2) {
              return;
            }
            if ('default' === tokens[0]) {
              let ifnameIndex = tokens.indexOf('dev');
              if (ifnameIndex < 0 || ifnameIndex + 1 >= tokens.length) {
                return;
              }
              let ifname = tokens[ifnameIndex + 1];
              if (ipRouteResult[ifname]) {
                ipRouteResult[ifname].default = true;
              }
              return;
            }
          });

        });

    }

    // Find MASQUERADE in POSTROUTING section. 'MASQUERADE' should be found
    // when tethering is enabled. 'MASQUERADE' shouldn't be found when tethering
    // is disabled.
    function verifyIptables() {
      return runEmulatorShellSafe(['iptables', '-t', 'nat', '-L', 'POSTROUTING'])
        .then(function(aLines) {
          // $ iptables -t nat -L POSTROUTING
          //
          // Sample output (tethering on):
          //
          // Chain POSTROUTING (policy ACCEPT)
          // target     prot opt source               destination
          // MASQUERADE  all  --  anywhere             anywhere
          //
          let found = (function find_MASQUERADE() {
            // Skip first two lines.
            for (let i = 2; i < aLines.length; i++) {
              if (-1 !== aLines[i].indexOf('MASQUERADE')) {
                return true;
              }
            }
            return false;
          })();

          if ((aEnabled && !found) || (!aEnabled && found)) {
            throw 'MASQUERADE' + (found ? '' : ' not') + ' found while tethering is ' +
                  (aEnabled ? 'enabled' : 'disabled');
          }
        });
    }

    // Execute 'ip rule show', there must be one rule for tethering network
    // address to lookup for a secondary routing table, return that table id.
    function verifyIpRule() {
      if (!aIsDun) {
        return;
      }

      return runEmulatorShellSafe(['ip', 'rule', 'show'])
        .then(function (aLines) {
          // Sample output:
          //
          // 0: from all lookup local
          // 32765: from 192.168.1.0/24 lookup 60
          // 32766: from all lookup main
          // 32767: from all lookup default
          //
          let tableId = (function findTableId() {
            for (let i = 0; i < aLines.length; i++) {
              let tokens = aLines[i].split(/\s+/);
              if (-1 != tokens.indexOf(TETHERING_NETWORK_ADDR)) {
                let lookupIndex = tokens.indexOf('lookup');
                if (lookupIndex < 0 || lookupIndex + 1 >= tokens.length) {
                  return;
                }
                return tokens[lookupIndex + 1];
              }
            }
            return;
          })();

          if ((aEnabled && !tableId) || (!aEnabled && tableId)) {
            throw 'Secondary table' + (tableId ? '' : ' not') + ' found while tethering is ' +
                  (aEnabled ? 'enabled' : 'disabled');
          }

          return tableId;
        });
    }

    // Given the table id, use 'ip rule show table <table id>' to find the
    // default route on that secondary routing table.
    function execAndParseSecondaryTable(aTableId) {
      if (!aIsDun || !aEnabled) {
        return;
      }

      return runEmulatorShellSafe(['ip', 'route', 'show', 'table', aTableId])
        .then(function (aLines) {
          // We only look for default route in secondary table.
          aLines.forEach(function (aLine) {
            let tokens = aLine.split(/\s+/);
            if (tokens.length < 2) {
              return;
            }
            if ('default' === tokens[0]) {
              let ifnameIndex = tokens.indexOf('dev');
              if (ifnameIndex < 0 || ifnameIndex + 1 >= tokens.length) {
                return;
              }
              let ifname = tokens[ifnameIndex + 1];
              ipSecondaryRouteResult[ifname] = { default: true };
              return;
            }
          });
        });
    }

    function verifyDefaultRouteAndIp(aExpectedWifiTetheringIp) {
      log(JSON.stringify(ipRouteResult));
      log(JSON.stringify(ipSecondaryRouteResult));
      log(JSON.stringify(netcfgResult));

      if (aEnabled) {
        isOrThrow(ipRouteResult['rmnet0'].src, netcfgResult['rmnet0'].ip, 'rmnet0.ip');
        isOrThrow(ipRouteResult['rmnet0'].default, true, 'rmnet0.default');

        isOrThrow(ipRouteResult['wlan0'].src, netcfgResult['wlan0'].ip, 'wlan0.ip');
        isOrThrow(ipRouteResult['wlan0'].src, aExpectedWifiTetheringIp, 'expected ip');
        isOrThrow(ipRouteResult['wlan0'].default, false, 'wlan0.default');

        if (aIsDun) {
          isOrThrow(ipRouteResult['rmnet1'].src, netcfgResult['rmnet1'].ip, 'rmnet1.ip');
          isOrThrow(ipRouteResult['rmnet1'].default, false, 'rmnet1.default');
          // Dun's network default route is set on secondary routing table.
          isOrThrow(ipSecondaryRouteResult['rmnet1'].default, true, 'secondary rmnet1.default');
        }
      }
    }

    return verifyIptables()
      .then(verifyIpRule)
      .then(tableId => execAndParseSecondaryTable(tableId))
      .then(exeAndParseNetcfg)
      .then(exeAndParseIpRoute)
      .then(() => verifyDefaultRouteAndIp(TETHERING_SETTING_IP));
  }

  /**
   * Request to enable/disable wifi tethering.
   *
   * Enable/disable wifi tethering by using setTetheringEnabled API
   * Resolve when the routing is verified to set up successfully in 20 seconds. The polling
   * period is 1 second.
   *
   * Fulfill params: (none)
   * Reject params: The error message.
   *
   * @param aEnabled
   *        Boolean that indicates to enable or disable wifi tethering.
   * @param aIsDun
   *        Boolean that indicates whether dun is required.
   *
   * @return A deferred promise.
   */
  function setWifiTetheringEnabled(aEnabled, aIsDun) {
    let RETRY_INTERVAL_MS = 1000;
    let retryCnt = 20;

    let config = {
      "ip"        : TETHERING_SETTING_IP,
      "prefix"    : TETHERING_SETTNG_PREFIX,
      "startIp"   : TETHERING_SETTING_START_IP,
      "endIp"     : TETHERING_SETTING_END_IP,
      "dns1"      : TETHERING_SETTING_DNS1,
      "dns2"      : TETHERING_SETTING_DNS2,
      "wifiConfig": {
        "ssid"      : TETHERING_SETTING_SSID,
        "security"  : TETHERING_SETTING_SECURITY
      }
    };

    return tetheringManager.setTetheringEnabled(aEnabled, TYPE_WIFI, config)
      .then(function waitForRoutingVerified() {
        return verifyTetheringRouting(aEnabled, aIsDun)
          .then(null, function onreject(aReason) {

            log('verifyTetheringRouting rejected due to ' + aReason +
                ' (' + retryCnt + ')');

            if (!retryCnt--) {
              throw aReason;
            }

            return waitForTimeout(RETRY_INTERVAL_MS).then(waitForRoutingVerified);
          });
      });
  }

  /**
   * Ensure wifi is enabled/disabled.
   *
   * Issue a wifi enable/disable request if wifi is not in the desired state;
   * return a resolved promise otherwise.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @return a resolved promise or deferred promise.
   */
  function ensureWifiEnabled(aEnabled) {
    let wifiManager = window.navigator.mozWifiManager;
    if (wifiManager.enabled === aEnabled) {
      return Promise.resolve();
    }
    let request = wifiManager.setWifiEnabled(aEnabled);
    return wrapDomRequestAsPromise(request)
  }

  /**
   * Ensure tethering manager exists.
   *
   * Check navigator property |mozTetheringManager| to ensure we could access
   * tethering related function.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @return A deferred promise.
   */
  function ensureTetheringManager() {
    let deferred = Promise.defer();

    tetheringManager = window.navigator.mozTetheringManager;

    if (tetheringManager instanceof MozTetheringManager) {
      deferred.resolve();
    } else {
      log("navigator.mozTetheringManager is unavailable");
      deferred.reject();
    }

    return deferred.promise;
  }

  /**
   * Add required permissions for tethering. Never reject.
   *
   * The permissions required for wifi testing are 'wifi-manage' and 'settings-write'.
   * Never reject.
   *
   * Fulfill params: (none)
   *
   * @return A deferred promise.
   */
  function acquirePermission() {
    let deferred = Promise.defer();

    let permissions = [{ 'type': 'wifi-manage', 'allow': 1, 'context': window.document },
                       { 'type': 'settings-write', 'allow': 1, 'context': window.document },
                       { 'type': 'settings-read', 'allow': 1, 'context': window.document },
                       { 'type': 'settings-api-write', 'allow': 1, 'context': window.document },
                       { 'type': 'settings-api-read', 'allow': 1, 'context': window.document },
                       { 'type': 'mobileconnection', 'allow': 1, 'context': window.document }];

    SpecialPowers.pushPermissions(permissions, function() {
      deferred.resolve();
    });

    return deferred.promise;
  }

  /**
   * Common test routine.
   *
   * Start a test with the given test case chain. The test environment will be
   * settled down before the test. After the test, all the affected things will
   * be restored.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aTestCaseChain
   *        The test case entry point, which can be a function or a promise.
   *
   * @return A deferred promise.
   */
  suite.startTest = function(aTestCaseChain) {
    function setUp() {
      return ensureTetheringManager()
        .then(acquirePermission);
    }

    function tearDown() {
      waitFor(finish, function() {
        return pendingEmulatorShellCount === 0;
      });
    }

    return setUp()
      .then(aTestCaseChain)
      .then(function onresolve() {
        tearDown();
      }, function onreject(aReason) {
        ok(false, 'Promise rejects during test' + (aReason ? '(' + aReason + ')' : ''));
        tearDown();
      });
  };

  //---------------------------------------------------
  // Public test suite functions
  //---------------------------------------------------
  suite.ensureWifiEnabled = ensureWifiEnabled;
  suite.setWifiTetheringEnabled = setWifiTetheringEnabled;
  suite.getDataApnSettings = getDataApnSettings;
  suite.setDataApnSettings = setDataApnSettings;
  suite.setTetheringDunRequired = setTetheringDunRequired;


  /**
   * The common test routine for wifi tethering.
   *
   * Set 'ril.data.enabled' to true
   * before testing and restore it afterward. It will also verify 'ril.data.enabled'
   * and 'tethering.wifi.enabled' to be false in the beginning. Note that this routine
   * will NOT change the state of 'tethering.wifi.enabled' so the user should enable
   * than disable on his/her own. This routine will only check if tethering is turned
   * off after testing.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aTestCaseChain
   *        The test case entry point, which can be a function or a promise.
   *
   * @return A deferred promise.
   */
  suite.startTetheringTest = function(aTestCaseChain) {
    let oriDataEnabled;
    function verifyInitialState() {
      return getSettings(SETTINGS_RIL_DATA_ENABLED)
        .then(enabled => initTetheringTestEnvironment(enabled));
    }

    function initTetheringTestEnvironment(aEnabled) {
      oriDataEnabled = aEnabled;
      if (aEnabled) {
        return Promise.resolve();
      } else {
        return Promise.all([waitForRilDataConnected(true),
                            setSettings1(SETTINGS_RIL_DATA_ENABLED, true)]);
      }
    }

    function restoreToInitialState() {
      return setSettings1(SETTINGS_RIL_DATA_ENABLED, oriDataEnabled);
    }

    return suite.startTest(function() {
      return verifyInitialState()
        .then(aTestCaseChain)
        .then(restoreToInitialState, function onreject(aReason) {
          return restoreToInitialState()
            .then(() => { throw aReason; }); // Re-throw the orignal reject reason.
        });
    });
  };

  return suite;
})();