// META: script=/resources/testdriver.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'],
},
});