diff --git a/build-system/global-configs/experiments-config.json b/build-system/global-configs/experiments-config.json index f061431de029..62f25fecdde8 100644 --- a/build-system/global-configs/experiments-config.json +++ b/build-system/global-configs/experiments-config.json @@ -1,5 +1,11 @@ { - "experimentA": {}, + "experimentA": { + "name": "A4A No Signing RTV Experiment", + "environment": "AMP", + "issue": "https://github.com/ampproject/amphtml/issues/27189", + "expiration_date_utc": "2020-12-30", + "define_experiment_constant": "NO_SIGNING_RTV" + }, "experimentB": {}, "experimentC": {} } diff --git a/build-system/tasks/e2e/amp-driver.js b/build-system/tasks/e2e/amp-driver.js index 316515bb77f8..1b7727e69be8 100644 --- a/build-system/tasks/e2e/amp-driver.js +++ b/build-system/tasks/e2e/amp-driver.js @@ -22,7 +22,6 @@ const AmpdocEnvironment = { // AMPHTML ads environments A4A_FIE: 'a4a-fie', - A4A_FIE_NO_SIGNING: 'a4a-fie-no-signing', A4A_INABOX: 'a4a-inabox', A4A_INABOX_FRIENDLY: 'a4a-inabox-friendly', A4A_INABOX_SAFEFRAME: 'a4a-inabox-safeframe', @@ -99,21 +98,6 @@ const EnvironmentBehaviorMap = { }, }, - [AmpdocEnvironment.A4A_FIE_NO_SIGNING]: { - async ready(controller) { - return controller - .findElement('amp-ad > iframe') - .then((frame) => controller.switchToFrame(frame)); - }, - - url(url) { - const a4aUrl = url.replace(HOST, HOST + '/a4a'); - // Exp value is from extensions/amp-a4a/0.1/amp-a4a.js - // NO_SIGNING_EXP.experiment - return `${a4aUrl}?eid=a4a-no-signing:21066325`; - }, - }, - [AmpdocEnvironment.A4A_INABOX]: { async ready(controller) { return controller diff --git a/build-system/tasks/e2e/describes-e2e.js b/build-system/tasks/e2e/describes-e2e.js index 4c64d94176af..06d49b92c517 100644 --- a/build-system/tasks/e2e/describes-e2e.js +++ b/build-system/tasks/e2e/describes-e2e.js @@ -254,10 +254,6 @@ const EnvironmentVariantMap = { name: 'AMPHTML ads FIE environment', value: {environment: 'a4a-fie'}, }, - [AmpdocEnvironment.A4A_FIE_NO_SIGNING]: { - name: 'AMPHTML ads FIE environment with no-signing exp enabled', - value: {environment: 'a4a-fie-no-signing'}, - }, [AmpdocEnvironment.A4A_INABOX]: { name: 'AMPHTML ads inabox environment', value: {environment: 'a4a-inabox'}, @@ -280,7 +276,6 @@ const envPresets = { ], 'amp4ads-preset': [ AmpdocEnvironment.A4A_FIE, - AmpdocEnvironment.A4A_FIE_NO_SIGNING, AmpdocEnvironment.A4A_INABOX, AmpdocEnvironment.A4A_INABOX_FRIENDLY, AmpdocEnvironment.A4A_INABOX_SAFEFRAME, diff --git a/extensions/amp-a4a/0.1/amp-a4a.js b/extensions/amp-a4a/0.1/amp-a4a.js index 8918c320c72c..a502e0ed7b92 100644 --- a/extensions/amp-a4a/0.1/amp-a4a.js +++ b/extensions/amp-a4a/0.1/amp-a4a.js @@ -50,7 +50,6 @@ import { getConsentPolicyState, } from '../../../src/consent'; import {getContextMetadata} from '../../../src/iframe-attributes'; -import {getExperimentBranch} from '../../../src/experiments'; import {getMode} from '../../../src/mode'; import {insertAnalyticsElement} from '../../../src/extension-analytics'; import { @@ -179,15 +178,6 @@ const LIFECYCLE_STAGE_TO_ANALYTICS_TRIGGER = { 'crossDomainIframeLoaded': AnalyticsTrigger.AD_IFRAME_LOADED, }; -/** - * @const @enum {string} - */ -export const NO_SIGNING_EXP = { - id: 'a4a-no-signing', - control: '21066324', - experiment: '21066325', -}; - /** @const @enum {string} */ export const MODULE_NOMODULE_PARAMS_EXP = { ID: 'module-nomodule', @@ -870,10 +860,8 @@ export class AmpA4A extends AMP.BaseElement { * @return {boolean} */ isInNoSigningExp() { - return ( - getExperimentBranch(this.win, NO_SIGNING_EXP.id) === - NO_SIGNING_EXP.experiment - ); + // eslint-disable-next-line no-undef + return !!NO_SIGNING_RTV; } /** diff --git a/extensions/amp-a4a/0.1/test/test-amp-a4a.js b/extensions/amp-a4a/0.1/test/test-amp-a4a.js index 8b44278995b0..fbd35b152c00 100644 --- a/extensions/amp-a4a/0.1/test/test-amp-a4a.js +++ b/extensions/amp-a4a/0.1/test/test-amp-a4a.js @@ -31,7 +31,6 @@ import { DEFAULT_SAFEFRAME_VERSION, EXPERIMENT_FEATURE_HEADER_NAME, INVALID_SPSA_RESPONSE, - NO_SIGNING_EXP, RENDERING_TYPE_HEADER, SAFEFRAME_VERSION_HEADER, assignAdUrlToError, @@ -56,10 +55,6 @@ import {cancellation} from '../../../../src/error'; import {createElementWithAttributes} from '../../../../src/dom'; import {createIframePromise} from '../../../../testing/iframe'; import {dev, user} from '../../../../src/log'; -import { - forceExperimentBranch, - toggleExperiment, -} from '../../../../src/experiments'; import { incrementLoadingAds, is3pThrottled, @@ -70,1153 +65,1914 @@ import {data as testFragments} from './testdata/test_fragments'; import {toggleAmpdocFieForTesting} from '../../../../src/ampdoc-fie'; import {data as validCSSAmp} from './testdata/valid_css_at_rules_amp.reserialized'; -describes.realWin('no signing', {amp: true}, (env) => { - let doc; - let element; - let a4a; +// eslint-disable-next-line no-undef +if (NO_SIGNING_RTV) { + describes.realWin('no signing', {amp: true}, (env) => { + let doc; + let element; + let a4a; - beforeEach(() => { - doc = env.win.document; - toggleExperiment(env.win, 'a4a-no-signing', true); - forceExperimentBranch( - env.win, - NO_SIGNING_EXP.id, - NO_SIGNING_EXP.experiment - ); - element = createElementWithAttributes(env.win.document, 'amp-ad', { - 'width': '300', - 'height': '250', - 'type': 'doubleclick', - 'layout': 'fixed', + beforeEach(() => { + doc = env.win.document; + element = createElementWithAttributes(env.win.document, 'amp-ad', { + 'width': '300', + 'height': '250', + 'type': 'doubleclick', + 'layout': 'fixed', + }); + doc.body.appendChild(element); + a4a = new AmpA4A(element); + // Make the ad think it has size. + env.sandbox.stub(a4a, 'getIntersectionElementLayoutBox').returns({ + height: 250, + width: 300, + }); + env.sandbox.stub(a4a, 'getAdUrl').returns('https://adnetwork.com'); + env.fetchMock.mock( + 'begin:https://adnetwork.com', + validCSSAmp.minifiedCreative + ); + }); + + it('should contain the correct security features', async () => { + await a4a.buildCallback(); + a4a.onLayoutMeasure(); + await a4a.layoutCallback(); + const fie = doc.body.querySelector('iframe[srcdoc]'); + expect(fie.getAttribute('sandbox')).to.equal( + 'allow-forms allow-popups allow-popups-to-escape-sandbox ' + + 'allow-same-origin allow-top-navigation' + ); + const cspMeta = fie.contentDocument.querySelector( + 'meta[http-equiv=Content-Security-Policy]' + ); + expect(cspMeta).to.be.ok; + expect(cspMeta.content).to.include('img-src * data:;'); + expect(cspMeta.content).to.include('media-src *;'); + expect(cspMeta.content).to.include('font-src *;'); + expect(cspMeta.content).to.include('connect-src *;'); + expect(cspMeta.content).to.include("script-src 'none';"); + expect(cspMeta.content).to.include("object-src 'none';"); + expect(cspMeta.content).to.include("child-src 'none';"); + expect(cspMeta.content).to.include("default-src 'none';"); + expect(cspMeta.content).to.include( + 'style-src ' + + 'https://cdn.materialdesignicons.com ' + + 'https://cloud.typography.com ' + + 'https://fast.fonts.net ' + + 'https://fonts.googleapis.com ' + + 'https://maxcdn.bootstrapcdn.com https://p.typekit.net https://pro.fontawesome.com ' + + 'https://use.fontawesome.com ' + + 'https://use.typekit.net ' + + "'unsafe-inline';" + ); + }); + + it('FIE should contain with adurl', async () => { + await a4a.buildCallback(); + a4a.onLayoutMeasure(); + await a4a.layoutCallback(); + const fie = doc.body.querySelector('iframe[srcdoc]'); + const base = fie.contentDocument.querySelector('base'); + expect(base).to.be.ok; + expect(base.href).to.equal('https://adnetwork.com/'); + }); + + it('should complete the rendering FIE', async () => { + const prioritySpy = env.sandbox.spy(a4a, 'updateLayoutPriority'); + await a4a.buildCallback(); + a4a.onLayoutMeasure(); + await a4a.layoutCallback(); + const fie = doc.body.querySelector('iframe[srcdoc]'); + expect(fie).to.be.ok; + expect(fie.contentDocument.body.textContent).to.contain.string( + 'Hello, world.' + ); + expect(prioritySpy).to.be.calledWith(LayoutPriority.CONTENT); + }); + + it('should collapse on no content', async () => { + env.fetchMock.config.overwriteRoutes = true; + env.fetchMock.mock('begin:https://adnetwork.com', ''); // no content. + const iframe3pInit = env.sandbox + .stub(AmpAdXOriginIframeHandler.prototype, 'init') + .resolves(); + const collapseSpy = env.sandbox.spy(a4a, 'forceCollapse'); + + await a4a.buildCallback(); + a4a.onLayoutMeasure(); + await a4a.layoutCallback(); + expect(collapseSpy).to.be.called; + expect(iframe3pInit).not.to.be.called; + }); + + it('should fallback to x-domain without ⚡️4ads', async () => { + env.fetchMock.config.overwriteRoutes = true; + env.fetchMock.mock( + 'begin:https://adnetwork.com', + testFragments.minimalDocOneStyle + ); + const iframe3pInit = env.sandbox + .stub(AmpAdXOriginIframeHandler.prototype, 'init') + .resolves(); + await a4a.buildCallback(); + a4a.onLayoutMeasure(); + await a4a.layoutCallback(); + expect(iframe3pInit).to.be.called; }); - doc.body.appendChild(element); - a4a = new AmpA4A(element); - // Make the ad think it has size. - env.sandbox.stub(a4a, 'getIntersectionElementLayoutBox').returns({ - height: 250, - width: 300, - }); - env.sandbox.stub(a4a, 'getAdUrl').returns('https://adnetwork.com'); - env.fetchMock.mock( - 'begin:https://adnetwork.com', - validCSSAmp.minifiedCreative - ); }); +} + +describe('amp-a4a', () => { + const IFRAME_SANDBOXING_FLAGS = [ + 'allow-forms', + 'allow-modals', + 'allow-pointer-lock', + 'allow-popups', + 'allow-popups-to-escape-sandbox', + 'allow-same-origin', + 'allow-scripts', + 'allow-top-navigation-by-user-activation', + ]; + + let fetchMock; + let getSigningServiceNamesMock; + let whenVisibleMock; + let adResponse; + let onCreativeRenderSpy; + let getResourceStub; - it('should contain the correct security features', async () => { - await a4a.buildCallback(); - a4a.onLayoutMeasure(); - await a4a.layoutCallback(); - const fie = doc.body.querySelector('iframe[srcdoc]'); - expect(fie.getAttribute('sandbox')).to.equal( - 'allow-forms allow-popups allow-popups-to-escape-sandbox ' + - 'allow-same-origin allow-top-navigation' - ); - const cspMeta = fie.contentDocument.querySelector( - 'meta[http-equiv=Content-Security-Policy]' + beforeEach(() => { + fetchMock = null; + getSigningServiceNamesMock = window.sandbox.stub( + AmpA4A.prototype, + 'getSigningServiceNames' ); - expect(cspMeta).to.be.ok; - expect(cspMeta.content).to.include('img-src * data:;'); - expect(cspMeta.content).to.include('media-src *;'); - expect(cspMeta.content).to.include('font-src *;'); - expect(cspMeta.content).to.include('connect-src *;'); - expect(cspMeta.content).to.include("script-src 'none';"); - expect(cspMeta.content).to.include("object-src 'none';"); - expect(cspMeta.content).to.include("child-src 'none';"); - expect(cspMeta.content).to.include("default-src 'none';"); - expect(cspMeta.content).to.include( - 'style-src ' + - 'https://cdn.materialdesignicons.com ' + - 'https://cloud.typography.com ' + - 'https://fast.fonts.net ' + - 'https://fonts.googleapis.com ' + - 'https://maxcdn.bootstrapcdn.com https://p.typekit.net https://pro.fontawesome.com ' + - 'https://use.fontawesome.com ' + - 'https://use.typekit.net ' + - "'unsafe-inline';" + onCreativeRenderSpy = window.sandbox.spy( + AmpA4A.prototype, + 'onCreativeRender' ); + getSigningServiceNamesMock.returns(['google']); + whenVisibleMock = window.sandbox.stub(AmpDoc.prototype, 'whenFirstVisible'); + whenVisibleMock.returns(Promise.resolve()); + getResourceStub = window.sandbox.stub(AmpA4A.prototype, 'getResource'); + getResourceStub.returns({ + getUpgradeDelayMs: () => 12345, + }); + adResponse = { + headers: { + 'AMP-Fast-Fetch-Signature': validCSSAmp.signatureHeader, + }, + body: validCSSAmp.reserialized, + }; + adResponse.headers[AMP_SIGNATURE_HEADER] = validCSSAmp.signatureHeader; }); - it('FIE should contain with adurl', async () => { - await a4a.buildCallback(); - a4a.onLayoutMeasure(); - await a4a.layoutCallback(); - const fie = doc.body.querySelector('iframe[srcdoc]'); - const base = fie.contentDocument.querySelector('base'); - expect(base).to.be.ok; - expect(base.href).to.equal('https://adnetwork.com/'); + afterEach(() => { + if (fetchMock) { + fetchMock./*OK*/ restore(); + fetchMock = null; + } + resetScheduledElementForTesting(window, 'amp-a4a'); }); - it('should complete the rendering FIE', async () => { - const prioritySpy = env.sandbox.spy(a4a, 'updateLayoutPriority'); - await a4a.buildCallback(); - a4a.onLayoutMeasure(); - await a4a.layoutCallback(); - const fie = doc.body.querySelector('iframe[srcdoc]'); - expect(fie).to.be.ok; - expect(fie.contentDocument.body.textContent).to.contain.string( - 'Hello, world.' + /** + * Sets up testing by loading iframe within which test runs. + * @param {FixtureInterface} fixture + */ + function setupForAdTesting(fixture) { + expect(fetchMock).to.be.null; + fetchMock = new FetchMock(fixture.win); + fetchMock.getOnce( + 'https://cdn.ampproject.org/amp-ad-verifying-keyset.json', + { + body: validCSSAmp.publicKeyset, + status: 200, + headers: {'Content-Type': 'application/jwk-set+json'}, + } ); - expect(prioritySpy).to.be.calledWith(LayoutPriority.CONTENT); - }); + installDocService(fixture.win, /* isSingleDoc */ true); + const {doc} = fixture; + // TODO(a4a-cam@): This is necessary in the short term, until A4A is + // smarter about host document styling. The issue is that it needs to + // inherit the AMP runtime style element in order for shadow DOM-enclosed + // elements to behave properly. So we have to set up a minimal one here. + const ampStyle = doc.createElement('style'); + ampStyle.setAttribute('amp-runtime', 'scratch-fortesting'); + doc.head.appendChild(ampStyle); + } + + /** + * @param {!Document} doc + * @param {Rect=} opt_rect + * @param {Element=} opt_body + */ + function createA4aElement(doc, opt_rect, opt_body) { + const element = createElementWithAttributes(doc, 'amp-a4a', { + 'width': opt_rect ? String(opt_rect.width) : '200', + 'height': opt_rect ? String(opt_rect.height) : '50', + 'type': 'adsense', + }); + element.getAmpDoc = () => { + const ampdocService = Services.ampdocServiceFor(doc.defaultView); + return ampdocService.getAmpDoc(element); + }; + element.isBuilt = () => { + return true; + }; + element.getLayoutBox = () => { + return opt_rect || layoutRectLtwh(0, 0, 200, 50); + }; + element.getPageLayoutBox = () => { + return element.getLayoutBox.apply(element, arguments); + }; + element.getIntersectionChangeEntry = () => { + return null; + }; + const signals = new Signals(); + element.signals = () => signals; + element.renderStarted = () => { + signals.signal('render-start'); + }; + (opt_body || doc.body).appendChild(element); + return element; + } + + /** + * @param {Object=} opt_additionalInfo + * @return {string} + */ + function buildCreativeString(opt_additionalInfo) { + const baseTestDoc = testFragments.minimalDocOneStyle; + const offsets = {...(opt_additionalInfo || {})}; + offsets.ampRuntimeUtf16CharOffsets = [ + baseTestDoc.indexOf('