| // META: script=/resources/testdriver.js |
| // META: script=/resources/testdriver-vendor.js |
| // META: script=/common/utils.js |
| // META: script=resources/fledge-util.sub.js |
| // META: script=/common/subset-tests.js |
| // META: timeout=long |
| // META: variant=?1-4 |
| // META: variant=?5-9 |
| // META: variant=?10-14 |
| // META: variant=?15-19 |
| // META: variant=?20-last |
| |
| "use strict;" |
| |
| // This test repeatedly runs auctions to verify an update. A modified bidding script |
| // continuously throws errors until it detects the expected change in the interest group |
| // field. This update then stops the auction cycle. |
| const makeTestForUpdate = ({ |
| // Test name |
| name, |
| // fieldname that is getting updated |
| interestGroupFieldName, |
| // This is used to check if update has happened. |
| expectedValue, |
| // This is used to create the update response, by default it will always send |
| // back the `expectedValue`. Extra steps to make a deep copy. |
| responseOverride = expectedValue, |
| // Overrides to the interest group. |
| interestGroupOverrides = {}, |
| // Overrides to the auction config. |
| auctionConfigOverrides = {}, |
| }) => { |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| extraBiddingLogic = ``; |
| |
| let replacePlaceholders = (ads) => ads.forEach(element => { |
| element.renderURL = element.renderURL.replace(`UUID-PLACEHOLDER`, uuid); |
| }); |
| |
| // Testing 'ads' requires some additional setup due to it's reliance |
| // on createRenderURL, specifically the bidding script used checks to make |
| // sure the `uuid` is the correct one for the test. We use a renderURL |
| // with a placeholder 'UUID-PLACEHOLDER' and make sure to replace it |
| // before moving on to the test. |
| if (interestGroupFieldName === `ads`) { |
| if (interestGroupFieldName in interestGroupOverrides) { |
| replacePlaceholders(interestGroupOverrides[interestGroupFieldName]); |
| } |
| replacePlaceholders(responseOverride); |
| replacePlaceholders(expectedValue); |
| } |
| // When checking the render URL, both the deprecated 'renderUrl' and the updated 'renderURL' might exist |
| // in the interest group simultaneously, so this test deletes the 'renderUrl' to ensure a |
| // clean comparison with deepEquals. |
| if (interestGroupFieldName === `ads` || interestGroupFieldName === `adComponents`) { |
| extraBiddingLogic = ` |
| interestGroup.${interestGroupFieldName}.forEach(element => { |
| delete element.renderUrl; |
| });` |
| } |
| |
| let expectedValueJSON = JSON.stringify(expectedValue); |
| // When the update has not yet been seen, throw an error which will cause the |
| // auction not to have a result. |
| interestGroupOverrides.biddingLogicURL = createBiddingScriptURL({ |
| generateBid: ` |
| ${extraBiddingLogic} |
| if (!deepEquals(interestGroup.${interestGroupFieldName}, ${expectedValueJSON})) { |
| throw '${interestGroupFieldName} is ' + |
| JSON.stringify(interestGroup.${interestGroupFieldName}) + |
| ' instead of ' + '${expectedValueJSON}'; |
| }` |
| }); |
| |
| let responseBody = {}; |
| responseBody[interestGroupFieldName] = responseOverride; |
| let updateParams = { |
| body: JSON.stringify(responseBody), |
| uuid: uuid |
| }; |
| interestGroupOverrides.updateURL = createUpdateURL(updateParams); |
| await joinInterestGroup(test, uuid, interestGroupOverrides); |
| |
| // Run an auction until there's a winner, which means update occurred. |
| let auctionResult = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides); |
| expectNoWinner(auctionResult); |
| while (!auctionResult) { |
| auctionResult = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides); |
| } |
| }, name); |
| }; |
| |
| // In order to test the update process does not update certain fields, this test uses two interest groups: |
| |
| // * `failedUpdateGroup`: Receives an invalid update, and will continue to throw errors until the update |
| // occurs (which shouldn't happen). This group will have a high bid to ensure if |
| // there was ever a tie, it would win. |
| // * `successUpdateGroup`: A hard-coded interest group that receives a update and will signal the change |
| // by throwing an error. |
| |
| // By tracking render URLs, this test guarantees that only the URL associated with the correct update |
| // (`goodUpdateRenderURL`) is used, and the incorrect URL (`badUpdateRenderURL`) isn't. The test runs |
| // auctions repeatedly until the update in `successUpdateGroup` stops an auction from producing a winner. |
| // It then will run one final auction. If there's still no winner, it can infer that `failedUpdateGroup` |
| // would have received the update if it were propagating correctly. |
| |
| // If there was a bug in the implementation, a possible case can occur and manifest as a flaky test. |
| // In this scenerio with the current structure of the Protected Audience API, the `successUpdateGroup` |
| // updates, and so does the `failedUpdateGroup`, but the `failedUpdateGroup` update happens significantly |
| // after the `successUpdateGroup`'s update. In an effort to combat this, after the while loop we run |
| // another auction to ensure there is no winner (both cases should throw), but depending how slow the |
| // update takes, this flaky issue still can **possibly** occur. |
| const makeTestForNoUpdate = ({ |
| // Test name |
| name, |
| // fieldname that is should not be getting updated |
| interestGroupFieldName, |
| // this is used to create the update response and check if it did not happen. |
| responseOverride, |
| // Overrides to the auction config. |
| auctionConfigOverrides = {}, |
| // Overrides to the interest group. |
| failedUpdateGroup = {}, |
| }) => { |
| subsetTest(promise_test, async test => { |
| const uuid = generateUuid(test); |
| // successUpdateGroup |
| |
| // These are used in `successUpdateGroup` in order to get a proper update. |
| let successUpdateGroup = {}; |
| let successUpdateField = `userBiddingSignals`; |
| let successUpdateFieldExpectedValue = { 'test': 20 }; |
| |
| const goodUpdateRenderURL = createTrackerURL(window.location.origin, uuid, 'track_get', 'good_update'); |
| successUpdateGroup.ads = [{ 'renderURL': goodUpdateRenderURL }]; |
| successUpdateGroup.biddingLogicURL = createBiddingScriptURL({ |
| generateBid: ` |
| if (deepEquals(interestGroup.${successUpdateField}, ${JSON.stringify(successUpdateFieldExpectedValue)})){ |
| throw '${successUpdateField} has updated and is ' + |
| '${JSON.stringify(successUpdateFieldExpectedValue)}.' |
| }`, |
| bid: 5 |
| }); |
| |
| let successResponseBody = {}; |
| successResponseBody[successUpdateField] = successUpdateFieldExpectedValue; |
| let successUpdateParams = { |
| body: JSON.stringify(successResponseBody), |
| uuid: uuid |
| }; |
| successUpdateGroup.updateURL = createUpdateURL(successUpdateParams); |
| await joinInterestGroup(test, uuid, successUpdateGroup); |
| ///////////////////////// successUpdateGroup |
| |
| // failedUpdateGroup |
| const badUpdateRenderURL = createTrackerURL(window.location.origin, uuid, `track_get`, `bad_update`); |
| // Name needed so we don't have two IGs with same name. |
| failedUpdateGroup.name = failedUpdateGroup.name ? failedUpdateGroup.name : `IG name` |
| failedUpdateGroup.ads = [{ 'renderURL': badUpdateRenderURL }]; |
| failedUpdateGroup.biddingLogicURL = createBiddingScriptURL({ |
| generateBid: ` |
| if (!deepEquals(interestGroup.${interestGroupFieldName}, ${JSON.stringify(responseOverride)})){ |
| throw '${interestGroupFieldName} is as expected: '+ |
| JSON.stringify(interestGroup.${interestGroupFieldName}); |
| }`, |
| bid: 1000 |
| }); |
| let failedResponseBody = {}; |
| failedResponseBody[interestGroupFieldName] = responseOverride; |
| |
| let failedUpdateParams = { |
| body: JSON.stringify(failedResponseBody), |
| uuid: uuid |
| }; |
| |
| failedUpdateGroup.updateURL = createUpdateURL(failedUpdateParams); |
| await joinInterestGroup(test, uuid, failedUpdateGroup); |
| ///////////////////////// failedUpdateGroup |
| |
| // First result should be not be null, `successUpdateGroup` throws when update is detected so until then, |
| // run and observe the requests to ensure only `goodUpdateRenderURL` is fetched. |
| let auctionResult = await runBasicFledgeTestExpectingWinner(test, uuid, auctionConfigOverrides); |
| while (auctionResult) { |
| createAndNavigateFencedFrame(test, auctionResult); |
| await waitForObservedRequests( |
| uuid, |
| [goodUpdateRenderURL, createSellerReportURL(uuid)]); |
| await fetch(createCleanupURL(uuid)); |
| auctionResult = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides); |
| } |
| // Re-run to ensure null because: |
| // `successUpdateGroup` should be throwing since update occurred. |
| // `failedUpdateGroup` should be throwing since update did not occur. |
| await runBasicFledgeTestExpectingNoWinner(test, uuid, auctionConfigOverrides); |
| }, name); |
| }; |
| |
| // Helper to eliminate rewriting a long call to createRenderURL(). |
| // Only thing to change would be signalParams to differentiate between URLs. |
| const createTempRenderURL = (signalsParams = null) => { |
| return createRenderURL(/*uuid=*/`UUID-PLACEHOLDER`,/*script=*/ null,/*signalParams=*/ signalsParams,/*origin=*/ null); |
| }; |
| |
| makeTestForUpdate({ |
| name: 'userBiddingSignals update overwrites everything in the field.', |
| interestGroupFieldName: 'userBiddingSignals', |
| expectedValue: { 'test': 20 }, |
| interestGroupOverrides: { |
| userBiddingSignals: { 'test': 10, 'extra_value': true }, |
| } |
| }); |
| |
| makeTestForUpdate({ |
| name: 'userBiddingSignals updated multi-type', |
| interestGroupFieldName: 'userBiddingSignals', |
| expectedValue: { 'test': 20, 5: [1, [false, false, true], 3, 'Hello'] }, |
| interestGroupOverrides: { |
| userBiddingSignals: { 'test': 10 }, |
| } |
| }); |
| |
| makeTestForUpdate({ |
| name: 'userBiddingSignals updated to non object', |
| interestGroupFieldName: 'userBiddingSignals', |
| expectedValue: 5, |
| interestGroupOverrides: { |
| userBiddingSignals: { 'test': 10 }, |
| } |
| }); |
| |
| makeTestForUpdate({ |
| name: 'userBiddingSignals updated to null', |
| interestGroupFieldName: 'userBiddingSignals', |
| expectedValue: null, |
| interestGroupOverrides: { |
| userBiddingSignals: { 'test': 10 }, |
| } |
| }); |
| |
| makeTestForUpdate({ |
| name: 'trustedBiddingSignalsKeys updated correctly', |
| interestGroupFieldName: 'trustedBiddingSignalsKeys', |
| expectedValue: ['new_key', 'old_key'], |
| interestGroupOverrides: { |
| trustedBiddingSignalsKeys: ['old_key'], |
| } |
| }); |
| |
| makeTestForUpdate({ |
| name: 'trustedBiddingSignalsKeys updated to empty array.', |
| interestGroupFieldName: 'trustedBiddingSignalsKeys', |
| expectedValue: [], |
| interestGroupOverrides: { |
| trustedBiddingSignalsKeys: ['old_key'], |
| } |
| }); |
| |
| |
| makeTestForUpdate({ |
| name: 'trustedBiddingSignalsSlotSizeMode updated to slot-size', |
| interestGroupFieldName: 'trustedBiddingSignalsSlotSizeMode', |
| expectedValue: 'slot-size', |
| interestGroupOverrides: { |
| trustedBiddingSignalsKeys: ['key'], |
| trustedBiddingSignalsSlotSizeMode: 'none', |
| } |
| }); |
| |
| makeTestForUpdate({ |
| name: 'trustedBiddingSignalsSlotSizeMode updated to all-slots-requested-sizes', |
| interestGroupFieldName: 'trustedBiddingSignalsSlotSizeMode', |
| expectedValue: 'all-slots-requested-sizes', |
| interestGroupOverrides: { |
| trustedBiddingSignalsKeys: ['key'], |
| trustedBiddingSignalsSlotSizeMode: 'slot-size', |
| } |
| }); |
| |
| makeTestForUpdate({ |
| name: 'trustedBiddingSignalsSlotSizeMode updated to none', |
| interestGroupFieldName: 'trustedBiddingSignalsSlotSizeMode', |
| expectedValue: 'none', |
| interestGroupOverrides: { |
| trustedBiddingSignalsKeys: ['key'], |
| trustedBiddingSignalsSlotSizeMode: 'slot-size', |
| } |
| }); |
| |
| makeTestForUpdate({ |
| name: 'trustedBiddingSignalsSlotSizeMode updated to unknown, defaults to none', |
| interestGroupFieldName: 'trustedBiddingSignalsSlotSizeMode', |
| expectedValue: 'none', |
| responseOverride: 'unknown-type', |
| interestGroupOverrides: { |
| trustedBiddingSignalsKeys: ['key'], |
| trustedBiddingSignalsSlotSizeMode: 'slot-size', |
| } |
| }); |
| |
| makeTestForUpdate({ |
| name: 'ads updated from 2 ads to 1.', |
| interestGroupFieldName: 'ads', |
| expectedValue: [ |
| { renderURL: createTempRenderURL('new_url1'), metadata: 'test1-new' }, |
| ], |
| interestGroupOverrides: { |
| ads: [{ renderURL: createTempRenderURL() }, |
| { renderURL: createTempRenderURL() }] |
| } |
| }); |
| |
| makeTestForUpdate({ |
| name: 'ads updated from 1 ad to 2.', |
| interestGroupFieldName: 'ads', |
| expectedValue: [{ renderURL: createTempRenderURL('new_url1'), metadata: 'test1-new' }, |
| { renderURL: createTempRenderURL('new_url2'), metadata: 'test2-new' }], |
| interestGroupOverrides: { |
| ads: [{ renderURL: createTempRenderURL() }] |
| } |
| }); |
| |
| makeTestForUpdate({ |
| name: 'adComponents updated from 1 adComponent to 2.', |
| interestGroupFieldName: 'adComponents', |
| expectedValue: [{ renderURL: createTempRenderURL('new_url1'), metadata: 'test1-new' }, |
| { renderURL: createTempRenderURL('new_url2'), metadata: 'test2' }], |
| interestGroupOverrides: { |
| adComponents: [{ renderURL: createTempRenderURL(), metadata: 'test1' }] |
| }, |
| }); |
| |
| makeTestForUpdate({ |
| name: 'adComponents updated from 2 adComponents to 1.', |
| interestGroupFieldName: 'adComponents', |
| expectedValue: [{ renderURL: createTempRenderURL('new_url1'), metadata: 'test1-new' }], |
| interestGroupOverrides: { |
| adComponents: [{ renderURL: createTempRenderURL() }, |
| { renderURL: createTempRenderURL() }] |
| }, |
| }); |
| |
| makeTestForUpdate({ |
| name: 'executionMode updated to frozen context', |
| interestGroupFieldName: 'executionMode', |
| expectedValue: 'frozen-context', |
| interestGroupOverrides: { |
| executionMode: 'compatibility', |
| } |
| }); |
| |
| makeTestForUpdate({ |
| name: 'executionMode updated to compatibility', |
| interestGroupFieldName: 'executionMode', |
| expectedValue: 'compatibility', |
| interestGroupOverrides: { |
| executionMode: 'frozen-context', |
| } |
| }); |
| |
| makeTestForUpdate({ |
| name: 'executionMode updated to group by origin', |
| interestGroupFieldName: 'executionMode', |
| expectedValue: 'group-by-origin', |
| interestGroupOverrides: { |
| executionMode: 'compatibility', |
| } |
| }); |
| |
| makeTestForNoUpdate({ |
| name: 'executionMode updated with invalid input', |
| interestGroupFieldName: 'executionMode', |
| responseOverride: 'unknown-type', |
| }); |
| |
| makeTestForNoUpdate({ |
| name: 'owner cannot be updated.', |
| interestGroupFieldName: 'owner', |
| responseOverride: OTHER_ORIGIN1, |
| auctionConfigOverrides: { |
| interestGroupBuyers: [OTHER_ORIGIN1, window.location.origin] |
| } |
| }); |
| |
| makeTestForNoUpdate({ |
| name: 'name cannot be updated.', |
| interestGroupFieldName: 'name', |
| responseOverride: 'new_name', |
| failedUpdateGroup: { name: 'name2' }, |
| }); |
| |
| makeTestForNoUpdate({ |
| name: 'executionMode not updated when unknown type.', |
| interestGroupFieldName: 'executionMode', |
| responseOverride: 'unknown-type', |
| failedUpdateGroup: { executionMode: 'compatibility' }, |
| }); |
| |
| makeTestForNoUpdate({ |
| name: 'trustedBiddingSignalsKeys not updated when bad value.', |
| interestGroupFieldName: 'trustedBiddingSignalsKeys', |
| responseOverride: 5, |
| failedUpdateGroup: { |
| trustedBiddingSignalsKeys: ['key'], |
| }, |
| }); |
| |