utils.sys.mjs (9593B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { CommonUtils } from "resource://services-common/utils.sys.mjs"; 6 7 import { Assert } from "resource://testing-common/Assert.sys.mjs"; 8 9 import { initTestLogging } from "resource://testing-common/services/common/logging.sys.mjs"; 10 import { 11 FakeCryptoService, 12 FakeFilesystemService, 13 FakeGUIDService, 14 fakeSHA256HMAC, 15 } from "resource://testing-common/services/sync/fakeservices.sys.mjs"; 16 17 import { 18 FxAccounts, 19 AccountState, 20 } from "resource://gre/modules/FxAccounts.sys.mjs"; 21 import { FxAccountsClient } from "resource://gre/modules/FxAccountsClient.sys.mjs"; 22 23 import { SCOPE_APP_SYNC } from "resource://gre/modules/FxAccountsCommon.sys.mjs"; 24 25 // A mock "storage manager" for FxAccounts that doesn't actually write anywhere. 26 export function MockFxaStorageManager() {} 27 28 MockFxaStorageManager.prototype = { 29 promiseInitialized: Promise.resolve(), 30 31 initialize(accountData) { 32 this.accountData = accountData; 33 }, 34 35 finalize() { 36 return Promise.resolve(); 37 }, 38 39 getAccountData(fields = null) { 40 let result; 41 if (!this.accountData) { 42 result = null; 43 } else if (fields == null) { 44 // can't use cloneInto as the keys get upset... 45 result = {}; 46 for (let field of Object.keys(this.accountData)) { 47 result[field] = this.accountData[field]; 48 } 49 } else { 50 if (!Array.isArray(fields)) { 51 fields = [fields]; 52 } 53 result = {}; 54 for (let field of fields) { 55 result[field] = this.accountData[field]; 56 } 57 } 58 return Promise.resolve(result); 59 }, 60 61 updateAccountData(updatedFields) { 62 for (let [name, value] of Object.entries(updatedFields)) { 63 if (value == null) { 64 delete this.accountData[name]; 65 } else { 66 this.accountData[name] = value; 67 } 68 } 69 return Promise.resolve(); 70 }, 71 72 deleteAccountData() { 73 this.accountData = null; 74 return Promise.resolve(); 75 }, 76 }; 77 78 /** 79 * First wait >100ms (nsITimers can take up to that much time to fire, so 80 * we can account for the timer in delayedAutoconnect) and then two event 81 * loop ticks (to account for the CommonUtils.nextTick() in autoConnect). 82 */ 83 export function waitForZeroTimer(callback) { 84 let ticks = 2; 85 function wait() { 86 if (ticks) { 87 ticks -= 1; 88 CommonUtils.nextTick(wait); 89 return; 90 } 91 callback(); 92 } 93 CommonUtils.namedTimer(wait, 150, {}, "timer"); 94 } 95 96 export var promiseZeroTimer = function () { 97 return new Promise(resolve => { 98 waitForZeroTimer(resolve); 99 }); 100 }; 101 102 export var promiseNamedTimer = function (wait, thisObj, name) { 103 return new Promise(resolve => { 104 CommonUtils.namedTimer(resolve, wait, thisObj, name); 105 }); 106 }; 107 108 // Return an identity configuration suitable for testing with our identity 109 // providers. |overrides| can specify overrides for any default values. 110 // |server| is optional, but if specified, will be used to form the cluster 111 // URL for the FxA identity. 112 export var makeIdentityConfig = function (overrides) { 113 // first setup the defaults. 114 let result = { 115 // Username used in both fxaccount and sync identity configs. 116 username: "foo", 117 // fxaccount specific credentials. 118 fxaccount: { 119 user: { 120 email: "foo", 121 scopedKeys: { 122 [SCOPE_APP_SYNC]: { 123 kid: "1234567890123-u7u7u7u7u7u7u7u7u7u7uw", 124 k: "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg", 125 kty: "oct", 126 }, 127 }, 128 sessionToken: "sessionToken", 129 uid: "a".repeat(32), 130 verified: true, 131 }, 132 token: { 133 endpoint: null, 134 duration: 300, 135 id: "id", 136 key: "key", 137 hashed_fxa_uid: "f".repeat(32), // used during telemetry validation 138 // uid will be set to the username. 139 }, 140 }, 141 }; 142 143 // Now handle any specified overrides. 144 if (overrides) { 145 if (overrides.username) { 146 result.username = overrides.username; 147 } 148 if (overrides.fxaccount) { 149 // TODO: allow just some attributes to be specified 150 result.fxaccount = overrides.fxaccount; 151 } 152 if (overrides.node_type) { 153 result.fxaccount.token.node_type = overrides.node_type; 154 } 155 } 156 return result; 157 }; 158 159 export var makeFxAccountsInternalMock = function (config) { 160 return { 161 newAccountState(credentials) { 162 // We only expect this to be called with null indicating the (mock) 163 // storage should be read. 164 if (credentials) { 165 throw new Error("Not expecting to have credentials passed"); 166 } 167 let storageManager = new MockFxaStorageManager(); 168 storageManager.initialize(config.fxaccount.user); 169 let accountState = new AccountState(storageManager); 170 return accountState; 171 }, 172 getOAuthToken: () => Promise.resolve("some-access-token"), 173 destroyOAuthToken: () => Promise.resolve(), 174 keys: { 175 getScopedKeys: () => 176 Promise.resolve({ 177 [SCOPE_APP_SYNC]: { 178 identifier: SCOPE_APP_SYNC, 179 keyRotationSecret: 180 "0000000000000000000000000000000000000000000000000000000000000000", 181 keyRotationTimestamp: 1510726317123, 182 }, 183 }), 184 }, 185 profile: { 186 getProfile() { 187 return null; 188 }, 189 }, 190 }; 191 }; 192 193 // Configure an instance of an FxAccount identity provider with the specified 194 // config (or the default config if not specified). 195 export var configureFxAccountIdentity = function ( 196 authService, 197 config = makeIdentityConfig(), 198 fxaInternal = makeFxAccountsInternalMock(config) 199 ) { 200 // until we get better test infrastructure for bid_identity, we set the 201 // signedin user's "email" to the username, simply as many tests rely on this. 202 config.fxaccount.user.email = config.username; 203 204 let fxa = new FxAccounts(fxaInternal); 205 206 let MockFxAccountsClient = function () { 207 FxAccountsClient.apply(this); 208 }; 209 MockFxAccountsClient.prototype = { 210 accountStatus() { 211 return Promise.resolve(true); 212 }, 213 }; 214 Object.setPrototypeOf( 215 MockFxAccountsClient.prototype, 216 FxAccountsClient.prototype 217 ); 218 let mockFxAClient = new MockFxAccountsClient(); 219 fxa._internal._fxAccountsClient = mockFxAClient; 220 221 let mockTSC = { 222 // TokenServerClient 223 async getTokenUsingOAuth(url, oauthToken) { 224 Assert.equal( 225 url, 226 Services.prefs.getStringPref("identity.sync.tokenserver.uri") 227 ); 228 Assert.ok(oauthToken, "oauth token present"); 229 config.fxaccount.token.uid = config.username; 230 return config.fxaccount.token; 231 }, 232 }; 233 authService._fxaService = fxa; 234 authService._tokenServerClient = mockTSC; 235 // Set the "account" of the sync auth manager to be the "email" of the 236 // logged in user of the mockFXA service. 237 authService._signedInUser = config.fxaccount.user; 238 authService._account = config.fxaccount.user.email; 239 }; 240 241 export var configureIdentity = async function (identityOverrides, server) { 242 let config = makeIdentityConfig(identityOverrides, server); 243 // Must be imported after the identity configuration is set up. 244 let { Service } = ChromeUtils.importESModule( 245 "resource://services-sync/service.sys.mjs" 246 ); 247 248 // If a server was specified, ensure FxA has a correct cluster URL available. 249 if (server && !config.fxaccount.token.endpoint) { 250 let ep = server.baseURI; 251 if (!ep.endsWith("/")) { 252 ep += "/"; 253 } 254 ep += "1.1/" + config.username + "/"; 255 config.fxaccount.token.endpoint = ep; 256 } 257 258 configureFxAccountIdentity(Service.identity, config); 259 Services.prefs.setStringPref("services.sync.username", config.username); 260 // many of these tests assume all the auth stuff is setup and don't hit 261 // a path which causes that auth to magically happen - so do it now. 262 await Service.identity._ensureValidToken(); 263 264 // and cheat to avoid requiring each test do an explicit login - give it 265 // a cluster URL. 266 if (config.fxaccount.token.endpoint) { 267 Service.clusterURL = config.fxaccount.token.endpoint; 268 } 269 }; 270 271 export function syncTestLogging(level = "Trace") { 272 let logStats = initTestLogging(level); 273 Services.prefs.setStringPref("services.sync.log.logger", level); 274 Services.prefs.setStringPref("services.sync.log.logger.engine", ""); 275 return logStats; 276 } 277 278 export var SyncTestingInfrastructure = async function (server, username) { 279 let config = makeIdentityConfig({ username }); 280 await configureIdentity(config, server); 281 return { 282 logStats: syncTestLogging(), 283 fakeFilesystem: new FakeFilesystemService({}), 284 fakeGUIDService: new FakeGUIDService(), 285 fakeCryptoService: new FakeCryptoService(), 286 }; 287 }; 288 289 /** 290 * Turn WBO cleartext into fake "encrypted" payload as it goes over the wire. 291 */ 292 export function encryptPayload(cleartext) { 293 if (typeof cleartext == "object") { 294 cleartext = JSON.stringify(cleartext); 295 } 296 297 return { 298 ciphertext: cleartext, // ciphertext == cleartext with fake crypto 299 IV: "irrelevant", 300 hmac: fakeSHA256HMAC(cleartext), 301 }; 302 } 303 304 export var sumHistogram = function (name, options = {}) { 305 let histogram = options.key 306 ? Services.telemetry.getKeyedHistogramById(name) 307 : Services.telemetry.getHistogramById(name); 308 let snapshot = histogram.snapshot(); 309 let sum = -Infinity; 310 if (snapshot) { 311 if (options.key && snapshot[options.key]) { 312 sum = snapshot[options.key].sum; 313 } else { 314 sum = snapshot.sum; 315 } 316 } 317 histogram.clear(); 318 return sum; 319 };