test_ContextId_RustBackend.js (9628B)
1 /* Any copyright is dedicated to the Public Domain. 2 https://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { _ContextId } = ChromeUtils.importESModule( 7 "moz-src:///browser/modules/ContextId.sys.mjs" 8 ); 9 10 const { ObliviousHTTP } = ChromeUtils.importESModule( 11 "resource://gre/modules/ObliviousHTTP.sys.mjs" 12 ); 13 14 const { sinon } = ChromeUtils.importESModule( 15 "resource://testing-common/Sinon.sys.mjs" 16 ); 17 18 const CONTEXT_ID_PREF = "browser.contextual-services.contextId"; 19 const CONTEXT_ID_TIMESTAMP_PREF = 20 "browser.contextual-services.contextId.timestamp-in-seconds"; 21 const CONTEXT_ID_ROTATION_DAYS_PREF = 22 "browser.contextual-services.contextId.rotation-in-days"; 23 const UUID_REGEX = 24 /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; 25 const TEST_CONTEXT_ID = "decafbad-0cd1-0cd2-0cd3-decafbad1000"; 26 const TEST_CONTEXT_ID_WITH_BRACES = "{" + TEST_CONTEXT_ID + "}"; 27 const UNIFIED_ADS_ENDPOINT = Services.prefs.getCharPref( 28 "browser.newtabpage.activity-stream.unifiedAds.endpoint", 29 "" 30 ); 31 32 do_get_profile(); 33 34 /** 35 * Resolves when the passed in ContextId instance fires the ContextId:Persisted 36 * event. 37 * 38 * @param {_ContextId} instance 39 * An instance of the _ContextId class under test. 40 * @returns {Promise<CustomEvent>} 41 */ 42 function waitForPersist(instance) { 43 return new Promise(resolve => { 44 instance.addEventListener("ContextId:Persisted", resolve, { once: true }); 45 }); 46 } 47 48 /** 49 * Resolves when the the context-id-deletion-request ping is next sent, as 50 * well as the MARS deletion request. This helper also checks that those two 51 * requests properly send the rotatedFromContextId value. 52 * 53 * @param {string} rotatedFromContextIed 54 * The context ID that was rotated away from. 55 * @param {Function} taskFn 56 * A function that will trigger the requests to be sent. This function might 57 * be async. 58 * @returns {Promise<undefined>} 59 */ 60 function waitForRotated(rotatedFromContextId, taskFn) { 61 let sandbox = sinon.createSandbox(); 62 sandbox.stub(ObliviousHTTP, "getOHTTPConfig").resolves({}); 63 64 let { promise, resolve } = Promise.withResolvers(); 65 66 sandbox 67 .stub(ObliviousHTTP, "ohttpRequest") 68 .callsFake((_url, _config, endpoint, options) => { 69 Assert.equal( 70 endpoint, 71 `${UNIFIED_ADS_ENDPOINT}v1/delete_user`, 72 "Sent to the MARS endpoint" 73 ); 74 Assert.equal(options.method, "DELETE", "Sent using DELETE"); 75 Assert.deepEqual( 76 JSON.parse(options.body), 77 { 78 context_id: rotatedFromContextId, 79 }, 80 "Sent the old context_id" 81 ); 82 83 resolve(); 84 return Promise.resolve({ 85 status: 200, 86 json: async () => [], 87 }); 88 }); 89 90 return GleanPings.contextIdDeletionRequest.testSubmission(async () => { 91 Assert.equal( 92 Glean.contextualServices.contextId.testGetValue(), 93 rotatedFromContextId, 94 "Sent the right context ID to be deleted." 95 ); 96 await promise; 97 sandbox.restore(); 98 }, taskFn); 99 } 100 101 /** 102 * Checks that when a taskFn resolves, a context ID rotation has not occurred 103 * for the instance. 104 * 105 * @param {_ContextId} instance 106 * The instance of _ContextId under test. 107 * @param {function} taskFn 108 * A function that is being tested to ensure that it does not cause rotation 109 * to occur. It can be async. 110 * @returns {Promise<undefined>} 111 */ 112 async function doesNotRotate(instance, taskFn) { 113 let controller = new AbortController(); 114 instance.addEventListener( 115 "ContextId:Rotated", 116 () => { 117 Assert.ok(false, "Saw unexpected rotation."); 118 }, 119 { signal: controller.signal } 120 ); 121 await taskFn(); 122 controller.abort(); 123 } 124 125 add_setup(() => { 126 Services.fog.initializeFOG(); 127 }); 128 129 /** 130 * Test that if there's a pre-existing contextID, we can get it, and that a 131 * timestamp will be generated for it. 132 */ 133 add_task(async function test_get_existing() { 134 // Historically, we've stored the context ID with braces, but our endpoints 135 // actually would prefer just the raw UUID. The Rust component does the 136 // work of stripping those off for us automatically. We'll test that by 137 // starting with a context ID with braces in storage, and ensuring that 138 // what gets saved and what gets returned does not have braces. 139 Services.prefs.setCharPref(CONTEXT_ID_PREF, TEST_CONTEXT_ID_WITH_BRACES); 140 Services.prefs.clearUserPref(CONTEXT_ID_TIMESTAMP_PREF); 141 Services.prefs.setIntPref(CONTEXT_ID_ROTATION_DAYS_PREF, 0); 142 143 let instance = new _ContextId(); 144 let persisted = waitForPersist(instance); 145 146 Assert.equal( 147 await instance.request(), 148 TEST_CONTEXT_ID, 149 "Should have gotten the stored context ID" 150 ); 151 152 await persisted; 153 Assert.equal( 154 typeof Services.prefs.getIntPref(CONTEXT_ID_TIMESTAMP_PREF, 0), 155 "number", 156 "We stored a timestamp for the context ID." 157 ); 158 Assert.equal( 159 Services.prefs.getCharPref(CONTEXT_ID_PREF), 160 TEST_CONTEXT_ID, 161 "We stored a the context ID without braces." 162 ); 163 164 Assert.equal( 165 await instance.request(), 166 TEST_CONTEXT_ID, 167 "Should have gotten the same stored context ID back again." 168 ); 169 }); 170 171 /** 172 * Test that if there's not a pre-existing contextID, we will generate one, and 173 * a timestamp will be generated for it. 174 */ 175 add_task(async function test_generate() { 176 Services.prefs.clearUserPref(CONTEXT_ID_PREF); 177 Services.prefs.clearUserPref(CONTEXT_ID_TIMESTAMP_PREF); 178 179 let instance = new _ContextId(); 180 let persisted = waitForPersist(instance); 181 182 const generatedContextID = await instance.request(); 183 await persisted; 184 185 Assert.ok( 186 UUID_REGEX.test(generatedContextID), 187 "Should have gotten a UUID generated for the context ID." 188 ); 189 Assert.equal( 190 typeof Services.prefs.getIntPref(CONTEXT_ID_TIMESTAMP_PREF, 0), 191 "number", 192 "We stored a timestamp for the context ID." 193 ); 194 195 Assert.equal( 196 await instance.request(), 197 generatedContextID, 198 "Should have gotten the same stored context ID back again." 199 ); 200 }); 201 202 /** 203 * Test that if we have a pre-existing context ID, with an extremely old 204 * creation date (we'll use a creation date of 1, which is back in the 1970s), 205 * but a rotation setting of 0, that we don't rotate the context ID. 206 */ 207 add_task(async function test_no_rotation() { 208 Services.prefs.setCharPref(CONTEXT_ID_PREF, TEST_CONTEXT_ID); 209 Services.prefs.setIntPref(CONTEXT_ID_TIMESTAMP_PREF, 1); 210 Services.prefs.setIntPref(CONTEXT_ID_ROTATION_DAYS_PREF, 0); 211 212 let instance = new _ContextId(); 213 Assert.ok( 214 !instance.rotationEnabled, 215 "ContextId should report that rotation is not enabled." 216 ); 217 218 await doesNotRotate(instance, async () => { 219 Assert.equal( 220 await instance.request(), 221 TEST_CONTEXT_ID, 222 "Should have gotten the stored context ID" 223 ); 224 }); 225 226 // We should be able to synchronously request the context ID in this 227 // configuration. 228 Assert.equal( 229 instance.requestSynchronously(), 230 TEST_CONTEXT_ID, 231 "Got the stored context ID back synchronously." 232 ); 233 }); 234 235 /** 236 * Test that if we have a pre-existing context ID, and if the age associated 237 * with it is greater than our rotation window, that we'll generate a new 238 * context ID and update the creation timestamp. We'll use a creation timestamp 239 * of the original context ID of 1, which is sometime in the 1970s. 240 */ 241 add_task(async function test_rotation() { 242 Services.prefs.setCharPref(CONTEXT_ID_PREF, TEST_CONTEXT_ID); 243 Services.prefs.setIntPref(CONTEXT_ID_TIMESTAMP_PREF, 1); 244 // Let's say there's a 30 day rotation window. 245 const ROTATION_DAYS = 30; 246 Services.prefs.setIntPref(CONTEXT_ID_ROTATION_DAYS_PREF, ROTATION_DAYS); 247 248 let instance = new _ContextId(); 249 Assert.ok( 250 instance.rotationEnabled, 251 "ContextId should report that rotation is enabled." 252 ); 253 254 let generatedContextID; 255 256 await waitForRotated(TEST_CONTEXT_ID, async () => { 257 let persisted = waitForPersist(instance); 258 generatedContextID = await instance.request(); 259 await persisted; 260 }); 261 262 Assert.ok( 263 UUID_REGEX.test(generatedContextID), 264 "Should have gotten a UUID generated for the context ID." 265 ); 266 267 let creationTimestamp = Services.prefs.getIntPref(CONTEXT_ID_TIMESTAMP_PREF); 268 // We should have bumped the creation timestamp. 269 Assert.greater(creationTimestamp, 1); 270 271 // We should NOT be able to synchronously request the context ID in this 272 // configuration. 273 Assert.throws(() => { 274 instance.requestSynchronously(); 275 }, /Cannot request context ID synchronously/); 276 }); 277 278 /** 279 * Test that if we have a pre-existing context ID, we can force rotation even 280 * if the expiry hasn't come up. 281 */ 282 add_task(async function test_force_rotation() { 283 Services.prefs.setCharPref(CONTEXT_ID_PREF, TEST_CONTEXT_ID); 284 Services.prefs.clearUserPref(CONTEXT_ID_TIMESTAMP_PREF); 285 // Let's say there's a 30 day rotation window. 286 const ROTATION_DAYS = 30; 287 Services.prefs.setIntPref(CONTEXT_ID_ROTATION_DAYS_PREF, ROTATION_DAYS); 288 289 let instance = new _ContextId(); 290 Assert.equal( 291 await instance.request(), 292 TEST_CONTEXT_ID, 293 "Should have gotten the stored context ID" 294 ); 295 296 await waitForRotated(TEST_CONTEXT_ID, async () => { 297 await instance.forceRotation(); 298 }); 299 300 let generatedContextID = await instance.request(); 301 302 Assert.notEqual( 303 generatedContextID, 304 TEST_CONTEXT_ID, 305 "The context ID should have been regenerated." 306 ); 307 Assert.ok( 308 UUID_REGEX.test(generatedContextID), 309 "Should have gotten a UUID generated for the context ID." 310 ); 311 312 // We should NOT be able to synchronously request the context ID in this 313 // configuration. 314 Assert.throws(() => { 315 instance.requestSynchronously(); 316 }, /Cannot request context ID synchronously/); 317 });