Skip to content

Commit

Permalink
Adds TemplateValidator (#14584)
Browse files Browse the repository at this point in the history
* Adding TemplateValidator.

* Renaming test-amp-ad-templates to test-amp-ad-template-helper.

* Stash

* Adding tests.

* Moved variables tocloser to where they're actually used.

* NON_AMP case fix.

* lint

* Header renamed.

* Actually renamed header this time.

* Supporting two header names + whitelisted for presubmit.

* Undoing lint fix to unrelated file.
  • Loading branch information
glevitzky authored and lannka committed May 21, 2018
1 parent 0526d4e commit 6af752b
Show file tree
Hide file tree
Showing 10 changed files with 357 additions and 19 deletions.
1 change: 1 addition & 0 deletions build-system/tasks/presubmit-checks.js
Expand Up @@ -710,6 +710,7 @@ const forbiddenTermsSrcInclusive = {
'src/services.js',
'extensions/amp-ad/0.1/amp-ad.js',
'extensions/amp-a4a/0.1/amp-a4a.js',
'extensions/amp-a4a/0.1/template-validator.js',
'extensions/amp-ad-network-adsense-impl/0.1/amp-ad-network-adsense-impl.js', // eslint-disable-line max-len
'extensions/amp-ad-network-doubleclick-impl/0.1/amp-ad-network-doubleclick-impl.js', // eslint-disable-line max-len
'extensions/amp-lightbox-gallery/0.1/amp-lightbox-gallery.js',
Expand Down
Expand Up @@ -34,7 +34,7 @@ const TEMPLATE_CORS_CONFIG = {
credentials: 'omit',
};

export class AmpAdTemplates {
export class AmpAdTemplateHelper {

/**
* @param {!Window} win
Expand Down
3 changes: 1 addition & 2 deletions extensions/amp-a4a/0.1/cryptographic-validator.js
Expand Up @@ -21,15 +21,14 @@ import {
} from './amp-ad-type-defs';
import {SignatureVerifier, VerificationStatus} from './signature-verifier';
import {getAmpAdMetadata} from './amp-ad-utils';
import {getDefaultBootstrapBaseUrl} from '../../../src/3p-frame';
import {signingServerURLs} from '../../../ads/_a4a-config';
import {user} from '../../../src/log';
import {utf8Decode} from '../../../src/utils/bytes';

export const SIGNATURE_VERIFIER_PROPERTY_NAME =
'AMP_FAST_FETCH_SIGNATURE_VERIFIER_';

const TAG = 'amp-ad-render';
const TAG = 'amp-ad-cryptographic-validator';

export class CryptographicValidator extends Validator {
/** @param {!Window} win */
Expand Down
105 changes: 105 additions & 0 deletions extensions/amp-a4a/0.1/template-validator.js
@@ -0,0 +1,105 @@
/**
* Copyright 2018 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
AdResponseType,
Validator,
ValidatorResult,
} from './amp-ad-type-defs';
import {AmpAdTemplateHelper} from '../../amp-a4a/0.1/amp-ad-template-helper';
import {Services} from '../../../src/services';
import {getAmpAdMetadata} from './amp-ad-utils';
import {pushIfNotExist} from '../../../src/utils/array';
import {tryParseJson} from '../../../src/json';
import {utf8Decode} from '../../../src/utils/bytes';

/** @const {string} */
export const AMP_TEMPLATED_CREATIVE_HEADER_NAME = 'AMP-Ad-Template-Extension';
export const DEPRECATED_AMP_TEMPLATED_CREATIVE_HEADER_NAME =
'AMP-template-amp-creative';

/** {?AmpAdTemplateHelper} */
let ampAdTemplateHelper;

/**
* Returns the global template helper.
* @param {!Window} win
* @return {!AmpAdTemplateHelper}
* @visibleForTesting
*/
export function getAmpAdTemplateHelper(win) {
return ampAdTemplateHelper ||
(ampAdTemplateHelper = new AmpAdTemplateHelper(win));
}

/**
* Validator for Template ads.
*/
export class TemplateValidator extends Validator {
/** @override */
validate(context, unvalidatedBytes, headers) {

const body = utf8Decode(/** @type {!ArrayBuffer} */ (unvalidatedBytes));

// If we're missing the relevant header, or headers altogether, we cannot
// proceed. In this case, we return a NON_AMP response, since we cannot
// ensure this template will be valid AMP. We will pass the body of the
// response as the creative, and downstream renderers may attempt to render
// it as a non-AMP creative within a cross-domain iframe.
if (!headers ||
(headers.get(AMP_TEMPLATED_CREATIVE_HEADER_NAME) !== 'amp-mustache' &&
headers.get(DEPRECATED_AMP_TEMPLATED_CREATIVE_HEADER_NAME) !==
'amp-mustache')) {
return Promise.resolve(
/** @type {!./amp-ad-type-defs.ValidatorOutput} */ ({
creativeData: {
creative: body,
},
adResponseType: AdResponseType.TEMPLATE,
type: ValidatorResult.NON_AMP,
}));
}

const parsedResponseBody =
/** @type {!./amp-ad-type-defs.AmpTemplateCreativeDef} */ (
tryParseJson(body) || {});
return getAmpAdTemplateHelper()
.fetch(parsedResponseBody.templateUrl)
.then(template => {
const creativeMetadata = getAmpAdMetadata(template);
if (parsedResponseBody.analytics) {
pushIfNotExist(
creativeMetadata['customElementExtensions'], 'amp-analytics');
}
pushIfNotExist(
creativeMetadata['customElementExtensions'], 'amp-mustache');

const extensions = Services.extensionsFor(context.win);
creativeMetadata.customElementExtensions.forEach(
extensionId => extensions./*OK*/preloadExtension(extensionId));
// TODO(levitzky) Add preload logic for fonts / images.
return Promise.resolve(
/** @type {!./amp-ad-type-defs.ValidatorOutput} */ ({
creativeData: {
templateData: parsedResponseBody,
creativeMetadata,
},
adResponseType: AdResponseType.TEMPLATE,
type: ValidatorResult.AMP,
}));
});
}
}
Expand Up @@ -14,27 +14,27 @@
* limitations under the License.
*/

import {AmpAdTemplates} from '../amp-ad-templates';
import {AmpAdTemplateHelper} from '../amp-ad-template-helper';
import {AmpMustache} from '../../../amp-mustache/0.1/amp-mustache';
import {Xhr} from '../../../../src/service/xhr-impl';


describes.fakeWin('amp-ad-templates', {amp: true}, env => {
describes.fakeWin('AmpAdTemplateHelper', {amp: true}, env => {

const cdnUrl = 'https://adserver-com.cdn.ampproject.org/ad/s/' +
'adserver.com/amp_template_1';
const canonicalUrl = 'https://adserver.com/amp_template_1';

let win, doc;
let fetchTextMock;
let ampAdTemplates;
let ampAdTemplateHelper;

beforeEach(() => {
win = env.win;
win.AMP_MODE = {localDev: false};
doc = win.document;
fetchTextMock = sandbox.stub(Xhr.prototype, 'fetchText');
ampAdTemplates = new AmpAdTemplates(win);
ampAdTemplateHelper = new AmpAdTemplateHelper(win);
});

it('should return a promise resolving to a string template', () => {
Expand All @@ -52,16 +52,17 @@ describes.fakeWin('amp-ad-templates', {amp: true}, env => {
headers: {},
text: () => template,
}));
return ampAdTemplates.fetch(canonicalUrl)
return ampAdTemplateHelper.fetch(canonicalUrl)
.then(fetchedTemplate => expect(fetchedTemplate).to.equal(template));
});

it('should use CDN url if one is supplied', () => {
expect(ampAdTemplates.getTemplateProxyUrl_(cdnUrl)).to.equal(cdnUrl);
expect(ampAdTemplateHelper.getTemplateProxyUrl_(cdnUrl)).to.equal(cdnUrl);
});

it('should convert canonical to CDN', () => {
expect(ampAdTemplates.getTemplateProxyUrl_(canonicalUrl)).to.equal(cdnUrl);
expect(ampAdTemplateHelper.getTemplateProxyUrl_(canonicalUrl))
.to.equal(cdnUrl);
});

it('should render a template with correct values', () => {
Expand All @@ -74,7 +75,7 @@ describes.fakeWin('amp-ad-templates', {amp: true}, env => {
parentDiv./*OK*/innerHTML =
'<template type="amp-mustache"><p>{{foo}}</p></template>';
doc.body.appendChild(parentDiv);
return ampAdTemplates.render({foo: 'bar'}, parentDiv).then(result => {
return ampAdTemplateHelper.render({foo: 'bar'}, parentDiv).then(result => {
expect(result).to.not.be.null;
expect(result./*OK*/innerHTML).to.equal('bar');
});
Expand All @@ -93,7 +94,7 @@ describes.fakeWin('amp-ad-templates', {amp: true}, env => {
}, {
'type': 'googleanalytics',
}];
ampAdTemplates.insertAnalytics(parentDiv, analytics);
ampAdTemplateHelper.insertAnalytics(parentDiv, analytics);
expect(parentDiv.childNodes.length).to.equal(3);
expect(parentDiv.innerHTML).to.equal('<p>123</p>' +
'<amp-analytics config="remoteUrl">' +
Expand Down
185 changes: 185 additions & 0 deletions extensions/amp-a4a/0.1/test/test-template-validator.js
@@ -0,0 +1,185 @@
/**
* Copyright 2018 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as sinon from 'sinon';
import {
AMP_TEMPLATED_CREATIVE_HEADER_NAME,
DEPRECATED_AMP_TEMPLATED_CREATIVE_HEADER_NAME,
TemplateValidator,
getAmpAdTemplateHelper,
} from '../template-validator';
import {AdResponseType, ValidatorResult} from '../amp-ad-type-defs';
import {data} from './testdata/valid_css_at_rules_amp.reserialized';
import {utf8Encode} from '../../../../src/utils/bytes';

const realWinConfig = {
amp: {},
ampAdCss: true,
allowExternalResources: true,
};

describes.realWin('TemplateValidator', realWinConfig, env => {

const templateUrl = 'https://adnetwork.com/amp-template.html';
const headers = {
get: name => {
if (name == AMP_TEMPLATED_CREATIVE_HEADER_NAME) {
return 'amp-mustache';
}
},
};
let validator;

beforeEach(() => {
validator = new TemplateValidator();
});

describe('AMP Result', () => {

let sandbox;
let validatorPromise;

beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(getAmpAdTemplateHelper(env.win), 'fetch').callsFake(url => {
expect(url).to.equal(templateUrl);
return Promise.resolve(data.adTemplate);
});

validatorPromise = validator.validate({win: env.win},
utf8Encode(JSON.stringify({
templateUrl,
data: {url: 'https://buy.com/buy-1'},
analytics: {foo: 'bar'},
})), headers);
});

afterEach(() => sandbox.restore());

it('should have AMP validator result', () => {
return validatorPromise.then(validatorOutput => {
expect(validatorOutput).to.be.ok;
expect(validatorOutput.type).to.equal(ValidatorResult.AMP);
});
});

it('should have AMP validator result w/ deprecated header name', () => {
validator.validate({win: env.win},
utf8Encode(JSON.stringify({
templateUrl,
data: {url: 'https://buy.com/buy-1'},
analytics: {foo: 'bar'},
})), {
get: name => {
if (name == DEPRECATED_AMP_TEMPLATED_CREATIVE_HEADER_NAME) {
return 'amp-mustache';
}
},
}).then(validatorOutput => {
expect(validatorOutput).to.be.ok;
expect(validatorOutput.type).to.equal(ValidatorResult.AMP);
});
});

it('should have TEMPLATE ad response type', () => {
return validatorPromise.then(validatorOutput => {
expect(validatorOutput).to.be.ok;
expect(validatorOutput.adResponseType).to.equal(
AdResponseType.TEMPLATE);
});
});

it('should have creativeData with minified creative in metadata', () => {
return validatorPromise.then(validatorOutput => {
expect(validatorOutput).to.be.ok;
expect(validatorOutput.creativeData).to.be.ok;
const {creativeMetadata} = validatorOutput.creativeData;
expect(creativeMetadata.minifiedCreative)
.to.equal(data.minifiedTemplateCreative);
});
});

it('should have amp-analytics and mustache in customElementExtensions',
() => {
return validatorPromise.then(validatorOutput => {
expect(validatorOutput).to.be.ok;
expect(validatorOutput.creativeData).to.be.ok;
const {creativeMetadata} = validatorOutput.creativeData;
expect(creativeMetadata.customElementExtensions)
.to.deep.equal(['amp-analytics', 'amp-mustache']);
});
});
});

describe('Non-AMP Result', () => {
it('should have NON_AMP validator result due to lack of headers', () => {
return validator.validate({win: env.win},
utf8Encode(JSON.stringify({
templateUrl,
data: {url: 'https://buy.com/buy-1'},
analytics: {foo: 'bar'},
}))).then(validatorOutput => {
expect(validatorOutput).to.be.ok;
expect(validatorOutput.type).to.equal(ValidatorResult.NON_AMP);
});
});

it('should have NON_AMP validator result due to lack of mustache header',
() => {
return validator.validate({win: env.win},
utf8Encode(JSON.stringify({
templateUrl,
data: {url: 'https://buy.com/buy-1'},
analytics: {foo: 'bar'},
})),
{
get: () => null,
}).then(validatorOutput => {
expect(validatorOutput).to.be.ok;
expect(validatorOutput.type).to.equal(ValidatorResult.NON_AMP);
});
});

it('should have TEMPLATE ad response type', () => {
return validator.validate({win: env.win},
utf8Encode(JSON.stringify({
templateUrl,
data: {url: 'https://buy.com/buy-1'},
analytics: {foo: 'bar'},
}))).then(validatorOutput => {
expect(validatorOutput).to.be.ok;
expect(validatorOutput.adResponseType)
.to.equal(AdResponseType.TEMPLATE);
});
});

it('should have the response body as the creative in creativeData', () => {
return validator.validate({win: env.win},
utf8Encode(JSON.stringify({templateUrl})),
{
get: () => null,
}).then(validatorOutput => {
expect(validatorOutput).to.be.ok;
expect(validatorOutput.creativeData).to.be.ok;
const {creativeData} = validatorOutput;
expect(creativeData).to.be.ok;
expect(creativeData.creative).to.deep.equal(
'{"templateUrl":"https://adnetwork.com/amp-template.html"}');
});
});
});
});

0 comments on commit 6af752b

Please sign in to comment.