interest-group-update.https.window.js (15387B)
1 // META: script=/resources/testdriver.js 2 // META: script=/resources/testdriver-vendor.js 3 // META: script=/common/utils.js 4 // META: script=resources/fledge-util.sub.js 5 // META: script=/common/subset-tests.js 6 // META: timeout=long 7 // META: variant=?1-4 8 // META: variant=?5-9 9 // META: variant=?10-14 10 // META: variant=?15-19 11 // META: variant=?20-last 12 13 "use strict"; 14 15 // This test repeatedly runs auctions to verify an update. A modified bidding script 16 // continuously throws errors until it detects the expected change in the interest group 17 // field. This update then stops the auction cycle. 18 const makeTestForUpdate = ({ 19 // Test name 20 name, 21 // fieldname that is getting updated 22 interestGroupFieldName, 23 // This is used to check if update has happened. 24 expectedValue, 25 // This is used to create the update response, by default it will always send 26 // back the `expectedValue`. Extra steps to make a deep copy. 27 responseOverride = expectedValue, 28 // Overrides to the interest group. 29 interestGroupOverrides = {}, 30 // Overrides to the auction config. 31 auctionConfigOverrides = {}, 32 }) => { 33 subsetTest(promise_test, async test => { 34 const uuid = generateUuid(test); 35 let extraBiddingLogic = ``; 36 37 let replacePlaceholders = (ads) => ads.forEach(element => { 38 element.renderURL = element.renderURL.replace(`UUID-PLACEHOLDER`, uuid); 39 }); 40 41 // Testing 'ads' requires some additional setup due to it's reliance 42 // on createRenderURL, specifically the bidding script used checks to make 43 // sure the `uuid` is the correct one for the test. We use a renderURL 44 // with a placeholder 'UUID-PLACEHOLDER' and make sure to replace it 45 // before moving on to the test. 46 if (interestGroupFieldName === `ads`) { 47 if (interestGroupFieldName in interestGroupOverrides) { 48 replacePlaceholders(interestGroupOverrides[interestGroupFieldName]); 49 } 50 replacePlaceholders(responseOverride); 51 replacePlaceholders(expectedValue); 52 } 53 // When checking the render URL, both the deprecated 'renderUrl' and the updated 'renderURL' might exist 54 // in the interest group simultaneously, so this test deletes the 'renderUrl' to ensure a 55 // clean comparison with deepEquals. 56 if (interestGroupFieldName === `ads` || interestGroupFieldName === `adComponents`) { 57 extraBiddingLogic = ` 58 interestGroup.${interestGroupFieldName}.forEach(element => { 59 delete element.renderUrl; 60 });` 61 } 62 63 let expectedValueJSON = JSON.stringify(expectedValue); 64 // When the update has not yet been seen, throw an error which will cause the 65 // auction not to have a result. 66 interestGroupOverrides.biddingLogicURL = createBiddingScriptURL({ 67 generateBid: ` 68 ${extraBiddingLogic} 69 if (!deepEquals(interestGroup.${interestGroupFieldName}, ${expectedValueJSON})) { 70 throw '${interestGroupFieldName} is ' + 71 JSON.stringify(interestGroup.${interestGroupFieldName}) + 72 ' instead of ' + '${expectedValueJSON}'; 73 }` 74 }); 75 76 let responseBody = {}; 77 responseBody[interestGroupFieldName] = responseOverride; 78 let updateParams = { 79 body: JSON.stringify(responseBody), 80 uuid: uuid 81 }; 82 interestGroupOverrides.updateURL = createUpdateURL(updateParams); 83 await joinInterestGroup(test, uuid, interestGroupOverrides); 84 if (interestGroupFieldName === `ads`) { 85 let interestGroup = createInterestGroupForOrigin( 86 uuid, window.location.origin, interestGroupOverrides); 87 interestGroup.ads = responseOverride; 88 await makeInterestGroupKAnonymous(interestGroup); 89 } 90 91 // Run an auction until there's a winner, which means update occurred. 92 let auctionResult = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides); 93 expectNoWinner(auctionResult); 94 while (!auctionResult) { 95 auctionResult = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides); 96 } 97 }, name); 98 }; 99 100 // In order to test the update process does not update certain fields, this test uses two interest groups: 101 102 // * `failedUpdateGroup`: Receives an invalid update, and will continue to throw errors until the update 103 // occurs (which shouldn't happen). This group will have a high bid to ensure if 104 // there was ever a tie, it would win. 105 // * `successUpdateGroup`: A hard-coded interest group that receives a update and will signal the change 106 // by throwing an error. 107 108 // By tracking render URLs, this test guarantees that only the URL associated with the correct update 109 // (`goodUpdateRenderURL`) is used, and the incorrect URL (`badUpdateRenderURL`) isn't. The test runs 110 // auctions repeatedly until the update in `successUpdateGroup` stops an auction from producing a winner. 111 // It then will run one final auction. If there's still no winner, it can infer that `failedUpdateGroup` 112 // would have received the update if it were propagating correctly. 113 114 // If there was a bug in the implementation, a possible case can occur and manifest as a flaky test. 115 // In this scenerio with the current structure of the Protected Audience API, the `successUpdateGroup` 116 // updates, and so does the `failedUpdateGroup`, but the `failedUpdateGroup` update happens significantly 117 // after the `successUpdateGroup`'s update. In an effort to combat this, after the while loop we run 118 // another auction to ensure there is no winner (both cases should throw), but depending how slow the 119 // update takes, this flaky issue still can **possibly** occur. 120 const makeTestForNoUpdate = ({ 121 // Test name 122 name, 123 // fieldname that is should not be getting updated 124 interestGroupFieldName, 125 // this is used to create the update response and check if it did not happen. 126 responseOverride, 127 // Overrides to the auction config. 128 auctionConfigOverrides = {}, 129 // Overrides to the interest group. 130 failedUpdateGroup = {}, 131 }) => { 132 subsetTest(promise_test, async test => { 133 const uuid = generateUuid(test); 134 // successUpdateGroup 135 136 // These are used in `successUpdateGroup` in order to get a proper update. 137 let successUpdateGroup = {}; 138 let successUpdateField = `userBiddingSignals`; 139 let successUpdateFieldExpectedValue = { 'test': 20 }; 140 141 const goodUpdateRenderURL = createTrackerURL(window.location.origin, uuid, 'track_get', 'good_update'); 142 successUpdateGroup.ads = [{ 'renderURL': goodUpdateRenderURL }]; 143 successUpdateGroup.biddingLogicURL = createBiddingScriptURL({ 144 generateBid: ` 145 if (deepEquals(interestGroup.${successUpdateField}, ${JSON.stringify(successUpdateFieldExpectedValue)})){ 146 throw '${successUpdateField} has updated and is ' + 147 '${JSON.stringify(successUpdateFieldExpectedValue)}.' 148 }`, 149 bid: 5 150 }); 151 152 let successResponseBody = {}; 153 successResponseBody[successUpdateField] = successUpdateFieldExpectedValue; 154 let successUpdateParams = { 155 body: JSON.stringify(successResponseBody), 156 uuid: uuid 157 }; 158 successUpdateGroup.updateURL = createUpdateURL(successUpdateParams); 159 await joinInterestGroup(test, uuid, successUpdateGroup); 160 ///////////////////////// successUpdateGroup 161 162 // failedUpdateGroup 163 const badUpdateRenderURL = createTrackerURL(window.location.origin, uuid, `track_get`, `bad_update`); 164 // Name needed so we don't have two IGs with same name. 165 failedUpdateGroup.name = failedUpdateGroup.name ? failedUpdateGroup.name : `IG name` 166 failedUpdateGroup.ads = [{ 'renderURL': badUpdateRenderURL }]; 167 failedUpdateGroup.biddingLogicURL = createBiddingScriptURL({ 168 generateBid: ` 169 if (!deepEquals(interestGroup.${interestGroupFieldName}, ${JSON.stringify(responseOverride)})){ 170 throw '${interestGroupFieldName} is as expected: '+ 171 JSON.stringify(interestGroup.${interestGroupFieldName}); 172 }`, 173 bid: 1000 174 }); 175 let failedResponseBody = {}; 176 failedResponseBody[interestGroupFieldName] = responseOverride; 177 178 let failedUpdateParams = { 179 body: JSON.stringify(failedResponseBody), 180 uuid: uuid 181 }; 182 183 failedUpdateGroup.updateURL = createUpdateURL(failedUpdateParams); 184 await joinInterestGroup(test, uuid, failedUpdateGroup); 185 ///////////////////////// failedUpdateGroup 186 187 // First result should be not be null, `successUpdateGroup` throws when update is detected so until then, 188 // run and observe the requests to ensure only `goodUpdateRenderURL` is fetched. 189 let auctionResult = await runBasicFledgeTestExpectingWinner(test, uuid, auctionConfigOverrides); 190 while (auctionResult) { 191 createAndNavigateFencedFrame(test, auctionResult); 192 await waitForObservedRequests( 193 uuid, 194 [goodUpdateRenderURL, createSellerReportURL(uuid)]); 195 await fetch(createCleanupURL(uuid)); 196 auctionResult = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides); 197 } 198 // Re-run to ensure null because: 199 // `successUpdateGroup` should be throwing since update occurred. 200 // `failedUpdateGroup` should be throwing since update did not occur. 201 await runBasicFledgeTestExpectingNoWinner(test, uuid, auctionConfigOverrides); 202 }, name); 203 }; 204 205 // Helper to eliminate rewriting a long call to createRenderURL(). 206 // Only thing to change would be signalParams to differentiate between URLs. 207 const createTempRenderURL = (signalsParams = null) => { 208 return createRenderURL(/*uuid=*/`UUID-PLACEHOLDER`,/*script=*/ null,/*signalParams=*/ signalsParams,/*origin=*/ null); 209 }; 210 211 makeTestForUpdate({ 212 name: 'userBiddingSignals update overwrites everything in the field.', 213 interestGroupFieldName: 'userBiddingSignals', 214 expectedValue: { 'test': 20 }, 215 interestGroupOverrides: { 216 userBiddingSignals: { 'test': 10, 'extra_value': true }, 217 } 218 }); 219 220 makeTestForUpdate({ 221 name: 'userBiddingSignals updated multi-type', 222 interestGroupFieldName: 'userBiddingSignals', 223 expectedValue: { 'test': 20, 5: [1, [false, false, true], 3, 'Hello'] }, 224 interestGroupOverrides: { 225 userBiddingSignals: { 'test': 10 }, 226 } 227 }); 228 229 makeTestForUpdate({ 230 name: 'userBiddingSignals updated to non object', 231 interestGroupFieldName: 'userBiddingSignals', 232 expectedValue: 5, 233 interestGroupOverrides: { 234 userBiddingSignals: { 'test': 10 }, 235 } 236 }); 237 238 makeTestForUpdate({ 239 name: 'userBiddingSignals updated to null', 240 interestGroupFieldName: 'userBiddingSignals', 241 expectedValue: null, 242 interestGroupOverrides: { 243 userBiddingSignals: { 'test': 10 }, 244 } 245 }); 246 247 makeTestForUpdate({ 248 name: 'trustedBiddingSignalsKeys updated correctly', 249 interestGroupFieldName: 'trustedBiddingSignalsKeys', 250 expectedValue: ['new_key', 'old_key'], 251 interestGroupOverrides: { 252 trustedBiddingSignalsKeys: ['old_key'], 253 } 254 }); 255 256 makeTestForUpdate({ 257 name: 'trustedBiddingSignalsKeys updated to empty array.', 258 interestGroupFieldName: 'trustedBiddingSignalsKeys', 259 expectedValue: [], 260 interestGroupOverrides: { 261 trustedBiddingSignalsKeys: ['old_key'], 262 } 263 }); 264 265 266 makeTestForUpdate({ 267 name: 'trustedBiddingSignalsSlotSizeMode updated to slot-size', 268 interestGroupFieldName: 'trustedBiddingSignalsSlotSizeMode', 269 expectedValue: 'slot-size', 270 interestGroupOverrides: { 271 trustedBiddingSignalsKeys: ['key'], 272 trustedBiddingSignalsSlotSizeMode: 'none', 273 } 274 }); 275 276 makeTestForUpdate({ 277 name: 'trustedBiddingSignalsSlotSizeMode updated to all-slots-requested-sizes', 278 interestGroupFieldName: 'trustedBiddingSignalsSlotSizeMode', 279 expectedValue: 'all-slots-requested-sizes', 280 interestGroupOverrides: { 281 trustedBiddingSignalsKeys: ['key'], 282 trustedBiddingSignalsSlotSizeMode: 'slot-size', 283 } 284 }); 285 286 makeTestForUpdate({ 287 name: 'trustedBiddingSignalsSlotSizeMode updated to none', 288 interestGroupFieldName: 'trustedBiddingSignalsSlotSizeMode', 289 expectedValue: 'none', 290 interestGroupOverrides: { 291 trustedBiddingSignalsKeys: ['key'], 292 trustedBiddingSignalsSlotSizeMode: 'slot-size', 293 } 294 }); 295 296 makeTestForUpdate({ 297 name: 'trustedBiddingSignalsSlotSizeMode updated to unknown, defaults to none', 298 interestGroupFieldName: 'trustedBiddingSignalsSlotSizeMode', 299 expectedValue: 'none', 300 responseOverride: 'unknown-type', 301 interestGroupOverrides: { 302 trustedBiddingSignalsKeys: ['key'], 303 trustedBiddingSignalsSlotSizeMode: 'slot-size', 304 } 305 }); 306 307 makeTestForUpdate({ 308 name: 'ads updated from 2 ads to 1.', 309 interestGroupFieldName: 'ads', 310 expectedValue: [ 311 { renderURL: createTempRenderURL('new_url1'), metadata: 'test1-new' }, 312 ], 313 interestGroupOverrides: { 314 ads: [{ renderURL: createTempRenderURL() }, 315 { renderURL: createTempRenderURL() }] 316 } 317 }); 318 319 makeTestForUpdate({ 320 name: 'ads updated from 1 ad to 2.', 321 interestGroupFieldName: 'ads', 322 expectedValue: [{ renderURL: createTempRenderURL('new_url1'), metadata: 'test1-new' }, 323 { renderURL: createTempRenderURL('new_url2'), metadata: 'test2-new' }], 324 interestGroupOverrides: { 325 ads: [{ renderURL: createTempRenderURL() }] 326 } 327 }); 328 329 makeTestForUpdate({ 330 name: 'adComponents updated from 1 adComponent to 2.', 331 interestGroupFieldName: 'adComponents', 332 expectedValue: [{ renderURL: createTempRenderURL('new_url1'), metadata: 'test1-new' }, 333 { renderURL: createTempRenderURL('new_url2'), metadata: 'test2' }], 334 interestGroupOverrides: { 335 adComponents: [{ renderURL: createTempRenderURL(), metadata: 'test1' }] 336 }, 337 }); 338 339 makeTestForUpdate({ 340 name: 'adComponents updated from 2 adComponents to 1.', 341 interestGroupFieldName: 'adComponents', 342 expectedValue: [{ renderURL: createTempRenderURL('new_url1'), metadata: 'test1-new' }], 343 interestGroupOverrides: { 344 adComponents: [{ renderURL: createTempRenderURL() }, 345 { renderURL: createTempRenderURL() }] 346 }, 347 }); 348 349 makeTestForUpdate({ 350 name: 'executionMode updated to frozen context', 351 interestGroupFieldName: 'executionMode', 352 expectedValue: 'frozen-context', 353 interestGroupOverrides: { 354 executionMode: 'compatibility', 355 } 356 }); 357 358 makeTestForUpdate({ 359 name: 'executionMode updated to compatibility', 360 interestGroupFieldName: 'executionMode', 361 expectedValue: 'compatibility', 362 interestGroupOverrides: { 363 executionMode: 'frozen-context', 364 } 365 }); 366 367 makeTestForUpdate({ 368 name: 'executionMode updated to group by origin', 369 interestGroupFieldName: 'executionMode', 370 expectedValue: 'group-by-origin', 371 interestGroupOverrides: { 372 executionMode: 'compatibility', 373 } 374 }); 375 376 makeTestForNoUpdate({ 377 name: 'executionMode updated with invalid input', 378 interestGroupFieldName: 'executionMode', 379 responseOverride: 'unknown-type', 380 }); 381 382 makeTestForNoUpdate({ 383 name: 'owner cannot be updated.', 384 interestGroupFieldName: 'owner', 385 responseOverride: OTHER_ORIGIN1, 386 auctionConfigOverrides: { 387 interestGroupBuyers: [OTHER_ORIGIN1, window.location.origin] 388 } 389 }); 390 391 makeTestForNoUpdate({ 392 name: 'name cannot be updated.', 393 interestGroupFieldName: 'name', 394 responseOverride: 'new_name', 395 failedUpdateGroup: { name: 'name2' }, 396 }); 397 398 makeTestForNoUpdate({ 399 name: 'executionMode not updated when unknown type.', 400 interestGroupFieldName: 'executionMode', 401 responseOverride: 'unknown-type', 402 failedUpdateGroup: { executionMode: 'compatibility' }, 403 }); 404 405 makeTestForNoUpdate({ 406 name: 'trustedBiddingSignalsKeys not updated when bad value.', 407 interestGroupFieldName: 'trustedBiddingSignalsKeys', 408 responseOverride: 5, 409 failedUpdateGroup: { 410 trustedBiddingSignalsKeys: ['key'], 411 }, 412 });