diff --git a/examples/article-access.amp.html b/examples/article-access.amp.html
index ffe4ce38ebb4..2a1f0013c300 100644
--- a/examples/article-access.amp.html
+++ b/examples/article-access.amp.html
@@ -5,11 +5,12 @@
Lorem Ipsum | PublisherName
+
diff --git a/extensions/amp-access/0.1/amp-access-client.js b/extensions/amp-access/0.1/amp-access-client.js
new file mode 100644
index 000000000000..d7da52892ae4
--- /dev/null
+++ b/extensions/amp-access/0.1/amp-access-client.js
@@ -0,0 +1,107 @@
+/**
+ * Copyright 2016 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 {assertHttpsUrl} from '../../../src/url';
+import {dev, user} from '../../../src/log';
+import {timer} from '../../../src/timer';
+import {xhrFor} from '../../../src/xhr';
+
+/** @const {string} */
+const TAG = 'amp-access-client';
+
+/** @const {number} */
+const AUTHORIZATION_TIMEOUT = 3000;
+
+
+/** @implements {AccessTypeAdapterDef} */
+export class AccessClientAdapter {
+
+ /**
+ * @param {!Window} win
+ * @param {!JSONObject} configJson
+ * @param {!AccessTypeAdapterContextDef} context
+ */
+ constructor(win, configJson, context) {
+ /** @const {!Window} */
+ this.win = win;
+
+ /** @const @private {!AccessTypeAdapterContextDef} */
+ this.context_ = context;
+
+ /** @const @private {string} */
+ this.authorizationUrl_ = user.assert(configJson['authorization'],
+ '"authorization" URL must be specified');
+ assertHttpsUrl(this.authorizationUrl_, '"authorization"');
+
+ /** @const @private {string} */
+ this.pingbackUrl_ = user.assert(configJson['pingback'],
+ '"pingback" URL must be specified');
+ assertHttpsUrl(this.pingbackUrl_, '"pingback"');
+
+ /** @const @private {!Xhr} */
+ this.xhr_ = xhrFor(win);
+
+ /** @const @private {!Timer} */
+ this.timer_ = timer;
+ }
+
+ /** @override */
+ getConfig() {
+ return {
+ 'authorizationUrl': this.authorizationUrl_,
+ 'pingbackUrl': this.pingbackUrl_,
+ };
+ }
+
+ /** @override */
+ isAuthorizationEnabled() {
+ return true;
+ }
+
+ /** @override */
+ authorize() {
+ dev.fine(TAG, 'Start authorization via ', this.authorizationUrl_);
+ const urlPromise = this.context_.buildUrl(this.authorizationUrl_,
+ /* useAuthData */ false);
+ return urlPromise.then(url => {
+ dev.fine(TAG, 'Authorization URL: ', url);
+ return this.timer_.timeoutPromise(
+ AUTHORIZATION_TIMEOUT,
+ this.xhr_.fetchJson(url, {
+ credentials: 'include',
+ requireAmpResponseSourceOrigin: true,
+ }));
+ });
+ }
+
+ /** @override */
+ pingback() {
+ const promise = this.context_.buildUrl(this.pingbackUrl_,
+ /* useAuthData */ true);
+ return promise.then(url => {
+ dev.fine(TAG, 'Pingback URL: ', url);
+ return this.xhr_.sendSignal(url, {
+ method: 'POST',
+ credentials: 'include',
+ requireAmpResponseSourceOrigin: true,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: '',
+ });
+ });
+ }
+}
diff --git a/extensions/amp-access/0.1/amp-access-other.js b/extensions/amp-access/0.1/amp-access-other.js
new file mode 100644
index 000000000000..52b9a848c724
--- /dev/null
+++ b/extensions/amp-access/0.1/amp-access-other.js
@@ -0,0 +1,74 @@
+/**
+ * Copyright 2016 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 {dev} from '../../../src/log';
+import {isProxyOrigin} from '../../../src/url';
+
+/** @const {string} */
+const TAG = 'amp-access-other';
+
+
+/** @implements {AccessTypeAdapterDef} */
+export class AccessOtherAdapter {
+
+ /**
+ * @param {!Window} win
+ * @param {!JSONObject} configJson
+ * @param {!AccessTypeAdapterContextDef} context
+ */
+ constructor(win, configJson, context) {
+ /** @const {!Window} */
+ this.win = win;
+
+ /** @const @private {!AccessTypeAdapterContextDef} */
+ this.context_ = context;
+
+ /** @private {?JSONObject} */
+ this.authorizationResponse_ =
+ configJson['authorizationFallbackResponse'] || null;
+
+ /** @const @private {boolean} */
+ this.isProxyOrigin_ = isProxyOrigin(win.location);
+ }
+
+ /** @override */
+ getConfig() {
+ return {
+ 'authorizationResponse': this.authorizationResponse_,
+ };
+ }
+
+ /** @override */
+ isAuthorizationEnabled() {
+ // The `type=other` is allowed to use the authorization fallback, but
+ // only if it's not on `cdn.ampproject.org`.
+ return (!!this.authorizationResponse_ && !this.isProxyOrigin_);
+ }
+
+ /** @override */
+ authorize() {
+ dev.fine(TAG, 'Use the authorization fallback for type=other');
+ // Only allowed for proxy origin (`cdn.ampproject.org`).
+ dev.assert(!this.isProxyOrigin_);
+ return Promise.resolve(dev.assert(this.authorizationResponse_));
+ }
+
+ /** @override */
+ pingback() {
+ dev.fine(TAG, 'Ignore pingback');
+ return Promise.resolve();
+ }
+}
diff --git a/extensions/amp-access/0.1/amp-access.js b/extensions/amp-access/0.1/amp-access.js
index f51098b644c2..669711552806 100644
--- a/extensions/amp-access/0.1/amp-access.js
+++ b/extensions/amp-access/0.1/amp-access.js
@@ -14,10 +14,12 @@
* limitations under the License.
*/
+import {AccessClientAdapter} from './amp-access-client';
+import {AccessOtherAdapter} from './amp-access-other';
import {CSS} from '../../../build/amp-access-0.1.css';
import {actionServiceFor} from '../../../src/action';
import {analyticsFor} from '../../../src/analytics';
-import {assertHttpsUrl, getSourceOrigin, isProxyOrigin} from '../../../src/url';
+import {assertHttpsUrl, getSourceOrigin} from '../../../src/url';
import {cancellation} from '../../../src/error';
import {cidFor} from '../../../src/cid';
import {evaluateAccessExpr} from './access-expr';
@@ -37,25 +39,10 @@ import {urlReplacementsFor} from '../../../src/url-replacements';
import {viewerFor} from '../../../src/viewer';
import {viewportFor} from '../../../src/viewport';
import {vsyncFor} from '../../../src/vsync';
-import {xhrFor} from '../../../src/xhr';
-/**
- * The configuration properties are:
- * - type: The type of access workflow: client, server or other.
- * - authorization: The URL of the Authorization endpoint.
- * - pingback: The URL of the Pingback endpoint.
- * - loginMap: The URL of the Login Page or a map of URLs.
- *
- * @typedef {{
- * type: !AccessType,
- * authorization: (string|undefined),
- * pingback: (string|undefined),
- * loginMap: !Object,
- * authorizationFallbackResponse: !JSONObject
- * }}
- */
-let AccessConfigDef;
+/** @const */
+const TAG = 'amp-access';
/**
* The type of access flow.
@@ -67,12 +54,6 @@ const AccessType = {
OTHER: 'other',
};
-/** @const */
-const TAG = 'amp-access';
-
-/** @const {number} */
-const AUTHORIZATION_TIMEOUT = 3000;
-
/** @const {number} */
const VIEW_TIMEOUT = 2000;
@@ -103,24 +84,35 @@ export class AccessService {
/** @const @private {!Element} */
this.accessElement_ = accessElement;
- /** @const @private {!AccessConfigDef} */
- this.config_ = this.buildConfig_();
+ let configJson;
+ try {
+ configJson = JSON.parse(this.accessElement_.textContent);
+ } catch (e) {
+ throw user.createError('Failed to parse "amp-access" JSON: ' + e);
+ }
+
+ /** @const @private {!AccessType} */
+ this.type_ = this.buildConfigType_(configJson);
+
+ /** @const @private {!Object} */
+ this.loginConfig_ = this.buildConfigLoginMap_(configJson);
+
+ /** @const @private {!JSONObject} */
+ this.authorizationFallbackResponse_ =
+ configJson['authorizationFallbackResponse'];
+
+ /** @const @private {!AccessTypeAdapterDef} */
+ this.adapter_ = this.createAdapter_(configJson);
/** @const @private {string} */
this.pubOrigin_ = getSourceOrigin(win.location);
- /** @const @private {boolean} */
- this.isProxyOrigin_ = isProxyOrigin(win.location);
-
/** @const @private {!Timer} */
this.timer_ = timer;
/** @const @private {!Vsync} */
this.vsync_ = vsyncFor(win);
- /** @const @private {!Xhr} */
- this.xhr_ = xhrFor(win);
-
/** @const @private {!UrlReplacements} */
this.urlReplacements_ = urlReplacementsFor(win);
@@ -178,57 +170,42 @@ export class AccessService {
}
/**
- * @return {!AccessConfigDef}
+ * @param {!JSONObject} configJson
+ * @return {!AccessTypeAdapterDef}
* @private
*/
- buildConfig_() {
- let configJson;
- try {
- configJson = JSON.parse(this.accessElement_.textContent);
- } catch (e) {
- throw user.createError('Failed to parse "amp-access" JSON: ' + e);
+ createAdapter_(configJson) {
+ const context = /** @type {!AccessTypeAdapterContextDef} */ ({
+ buildUrl: this.buildUrl_.bind(this),
+ });
+ switch (this.type_) {
+ case AccessType.CLIENT:
+ case AccessType.SERVER:
+ return new AccessClientAdapter(this.win, configJson, context);
+ case AccessType.OTHER:
+ return new AccessOtherAdapter(this.win, configJson, context);
}
+ throw dev.createError('Unsuported access type: ', this.type_);
+ }
- // Access type.
+ /**
+ * @param {!JSONObject} configJson
+ * @return {!AccessType}
+ */
+ buildConfigType_(configJson) {
const type = configJson['type'] ?
user.assertEnumValue(AccessType, configJson['type'], 'access type') :
AccessType.CLIENT;
- const config = {
- type: type,
- authorization: configJson['authorization'],
- pingback: configJson['pingback'],
- loginMap: this.buildConfigLoginMap_(configJson['login']),
- authorizationFallbackResponse:
- configJson['authorizationFallbackResponse'],
- };
-
- // Check that all URLs are valid.
- if (config.authorization) {
- assertHttpsUrl(config.authorization);
- }
- if (config.pingback) {
- assertHttpsUrl(config.pingback);
- }
- for (const k in config.loginMap) {
- assertHttpsUrl(config.loginMap[k]);
- }
-
- // Validate type = client/server.
- if (type == AccessType.CLIENT || type == AccessType.SERVER) {
- user.assert(config.authorization,
- '"authorization" URL must be specified');
- user.assert(config.pingback, '"pingback" URL must be specified');
- user.assert(Object.keys(config.loginMap).length > 0,
- 'At least one "login" URL must be specified');
- }
- return config;
+ return type;
}
/**
+ * @param {!JSONObject} configJson
* @return {?Object}
* @private
*/
- buildConfigLoginMap_(loginConfig) {
+ buildConfigLoginMap_(configJson) {
+ const loginConfig = configJson['login'];
const loginMap = {};
if (!loginConfig) {
// Ignore: in some cases login config is not necessary.
@@ -242,6 +219,11 @@ export class AccessService {
user.assert(false,
'"login" must be either a single URL or a map of URLs');
}
+
+ // Check that all URLs are valid.
+ for (const k in loginMap) {
+ assertHttpsUrl(loginMap[k]);
+ }
return loginMap;
}
@@ -277,7 +259,8 @@ export class AccessService {
/** @private */
startInternal_() {
- dev.fine(TAG, 'config:', this.config_);
+ dev.fine(TAG, 'config:', this.type_, this.loginConfig_,
+ this.adapter_.getConfig());
actionServiceFor(this.win).installActionHandler(
this.accessElement_, this.handleAction_.bind(this));
@@ -362,48 +345,24 @@ export class AccessService {
* @private
*/
runAuthorization_(opt_disableFallback) {
- if (this.config_.type == AccessType.OTHER &&
- (!this.config_.authorizationFallbackResponse || this.isProxyOrigin_)) {
- // The `type=other` is allowed to use the authorization fallback, but
- // only if it's not on `cdn.ampproject.org`.
- dev.fine(TAG, 'Ignore authorization due to type=other');
+ if (!this.adapter_.isAuthorizationEnabled()) {
+ dev.fine(TAG, 'Ignore authorization for type=', this.type_);
this.firstAuthorizationResolver_();
return Promise.resolve();
}
this.toggleTopClass_('amp-access-loading', true);
- let responsePromise;
- if (this.config_.authorization) {
- dev.fine(TAG, 'Start authorization via ', this.config_.authorization);
- const urlPromise = this.buildUrl_(
- this.config_.authorization, /* useAuthData */ false);
- responsePromise = urlPromise.then(url => {
- dev.fine(TAG, 'Authorization URL: ', url);
- return this.timer_.timeoutPromise(
- AUTHORIZATION_TIMEOUT,
- this.xhr_.fetchJson(url, {
- credentials: 'include',
- requireAmpResponseSourceOrigin: true,
- }));
- }).catch(error => {
- this.analyticsEvent_('access-authorization-failed');
- if (this.config_.authorizationFallbackResponse &&
- !opt_disableFallback) {
- // Use fallback.
- user.error(TAG, 'Authorization failed: ', error);
- return this.config_.authorizationFallbackResponse;
- } else {
- // Rethrow the error, it will be processed in the bottom `catch`.
- throw error;
- }
- });
- } else {
- dev.fine(TAG, 'Use the authorization fallback for type=other');
- dev.assert(this.config_.type == AccessType.OTHER);
- dev.assert(!this.isProxyOrigin_);
- responsePromise = Promise.resolve(dev.assert(
- this.config_.authorizationFallbackResponse));
- }
+ const responsePromise = this.adapter_.authorize().catch(error => {
+ this.analyticsEvent_('access-authorization-failed');
+ if (this.authorizationFallbackResponse_ && !opt_disableFallback) {
+ // Use fallback.
+ user.error(TAG, 'Authorization failed: ', error);
+ return this.authorizationFallbackResponse_;
+ } else {
+ // Rethrow the error, it will be processed in the bottom `catch`.
+ throw error;
+ }
+ });
const promise = responsePromise.then(response => {
dev.fine(TAG, 'Authorization response: ', response);
this.setAuthResponse_(response);
@@ -684,24 +643,7 @@ export class AccessService {
* @private
*/
reportViewToServer_() {
- if (!this.config_.pingback) {
- dev.fine(TAG, 'Ignore pingback');
- return Promise.resolve();
- }
- const promise = this.buildUrl_(
- this.config_.pingback, /* useAuthData */ true);
- return promise.then(url => {
- dev.fine(TAG, 'Pingback URL: ', url);
- return this.xhr_.sendSignal(url, {
- method: 'POST',
- credentials: 'include',
- requireAmpResponseSourceOrigin: true,
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- body: '',
- });
- }).then(() => {
+ return this.adapter_.pingback().then(() => {
dev.fine(TAG, 'Pingback complete');
this.analyticsEvent_('access-pingback-sent');
}).catch(error => {
@@ -762,7 +704,7 @@ export class AccessService {
}
dev.fine(TAG, 'Start login: ', type);
- user.assert(this.config_.loginMap[type],
+ user.assert(this.loginConfig_[type],
'Login URL is not configured: %s', type);
// Login URL should always be available at this time.
const loginUrl = user.assert(this.loginUrlMap_[type],
@@ -820,23 +762,58 @@ export class AccessService {
* @private
*/
buildLoginUrls_() {
- const loginMap = this.config_.loginMap;
- if (Object.keys(loginMap).length == 0) {
+ if (Object.keys(this.loginConfig_).length == 0) {
return null;
}
const promises = [];
- for (const k in loginMap) {
+ for (const k in this.loginConfig_) {
promises.push(
- this.buildUrl_(loginMap[k], /* useAuthData */ true).then(url => {
- this.loginUrlMap_[k] = url;
- return {type: k, url: url};
- }));
+ this.buildUrl_(this.loginConfig_[k], /* useAuthData */ true)
+ .then(url => {
+ this.loginUrlMap_[k] = url;
+ return {type: k, url: url};
+ }));
}
return Promise.all(promises);
}
}
+/**
+ * @typedef {{
+ * buildUrl: function(url:string, useAuthData:boolean):!Promise
+ * }}
+ */
+let AccessTypeAdapterContextDef;
+
+
+/**
+ * @interface
+ */
+class AccessTypeAdapterDef {
+
+ /**
+ * @return {!JSONObject}
+ */
+ getConfig() {}
+
+ /**
+ * @return {boolean}
+ */
+ isAuthorizationEnabled() {}
+
+ /**
+ * @return {!Promise}
+ */
+ authorize() {}
+
+ /**
+ * @return {!Promise<>}
+ */
+ pingback() {}
+}
+
+
/**
* @param {!Window} win
* @return {!AccessService}
diff --git a/extensions/amp-access/0.1/test/test-amp-access-client.js b/extensions/amp-access/0.1/test/test-amp-access-client.js
new file mode 100644
index 000000000000..5234de088c05
--- /dev/null
+++ b/extensions/amp-access/0.1/test/test-amp-access-client.js
@@ -0,0 +1,221 @@
+/**
+ * Copyright 2016 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 {AccessClientAdapter} from '../amp-access-client';
+import * as sinon from 'sinon';
+
+describe('AccessClientAdapter', () => {
+
+ let sandbox;
+ let clock;
+ let validConfig;
+ let context;
+ let contextMock;
+
+ beforeEach(() => {
+ sandbox = sinon.sandbox.create();
+ clock = sandbox.useFakeTimers();
+
+ validConfig = {
+ 'authorization': 'https://acme.com/a?rid=READER_ID',
+ 'pingback': 'https://acme.com/p?rid=READER_ID',
+ };
+
+ context = {
+ buildUrl: () => {},
+ };
+ contextMock = sandbox.mock(context);
+ });
+
+ afterEach(() => {
+ contextMock.verify();
+ sandbox.restore();
+ });
+
+
+ describe('config', () => {
+ it('should load valid config', () => {
+ const adapter = new AccessClientAdapter(window, validConfig, context);
+ expect(adapter.authorizationUrl_).to
+ .equal('https://acme.com/a?rid=READER_ID');
+ expect(adapter.pingbackUrl_).to
+ .equal('https://acme.com/p?rid=READER_ID');
+ expect(adapter.getConfig()).to.deep.equal({
+ authorizationUrl: 'https://acme.com/a?rid=READER_ID',
+ pingbackUrl: 'https://acme.com/p?rid=READER_ID',
+ });
+ expect(adapter.isAuthorizationEnabled()).to.be.true;
+ });
+
+ it('should fail if config authorization is missing or malformed', () => {
+ delete validConfig['authorization'];
+ expect(() => {
+ new AccessClientAdapter(window, validConfig, context);
+ }).to.throw(/"authorization" URL must be specified/);
+
+ validConfig['authorization'] = 'http://acme.com/a';
+ expect(() => {
+ new AccessClientAdapter(window, validConfig, context);
+ }).to.throw(/"authorization".*https\:/);
+ });
+
+ it('should fail if config pingback is missing or malformed', () => {
+ delete validConfig['pingback'];
+ expect(() => {
+ new AccessClientAdapter(window, validConfig, context);
+ }).to.throw(/"pingback" URL must be specified/);
+
+ validConfig['pingback'] = 'http://acme.com/p';
+ expect(() => {
+ new AccessClientAdapter(window, validConfig, context);
+ }).to.throw(/"pingback".*https\:/);
+ });
+ });
+
+
+ describe('runtime', () => {
+
+ let adapter;
+ let xhrMock;
+
+ beforeEach(() => {
+ adapter = new AccessClientAdapter(window, validConfig, context);
+ xhrMock = sandbox.mock(adapter.xhr_);
+ });
+
+ afterEach(() => {
+ xhrMock.verify();
+ });
+
+ describe('authorize', () => {
+ it('should issue XHR fetch', () => {
+ contextMock.expects('buildUrl')
+ .withExactArgs(
+ 'https://acme.com/a?rid=READER_ID',
+ /* useAuthData */ false)
+ .returns(Promise.resolve('https://acme.com/a?rid=reader1'))
+ .once();
+ xhrMock.expects('fetchJson')
+ .withExactArgs('https://acme.com/a?rid=reader1', {
+ credentials: 'include',
+ requireAmpResponseSourceOrigin: true,
+ })
+ .returns(Promise.resolve({access: 'A'}))
+ .once();
+ return adapter.authorize().then(response => {
+ expect(response).to.exist;
+ expect(response.access).to.equal('A');
+ });
+ });
+
+ it('should fail when XHR fails', () => {
+ contextMock.expects('buildUrl')
+ .withExactArgs(
+ 'https://acme.com/a?rid=READER_ID',
+ /* useAuthData */ false)
+ .returns(Promise.resolve('https://acme.com/a?rid=reader1'))
+ .once();
+ xhrMock.expects('fetchJson')
+ .withExactArgs('https://acme.com/a?rid=reader1', {
+ credentials: 'include',
+ requireAmpResponseSourceOrigin: true,
+ })
+ .returns(Promise.reject('intentional'))
+ .once();
+ return adapter.authorize().then(() => {
+ throw new Error('must never happen');
+ }, error => {
+ expect(error).to.match(/intentional/);
+ });
+ });
+
+ it('should time out XHR fetch', () => {
+ contextMock.expects('buildUrl')
+ .withExactArgs(
+ 'https://acme.com/a?rid=READER_ID',
+ /* useAuthData */ false)
+ .returns(Promise.resolve('https://acme.com/a?rid=reader1'))
+ .once();
+ xhrMock.expects('fetchJson')
+ .withExactArgs('https://acme.com/a?rid=reader1', {
+ credentials: 'include',
+ requireAmpResponseSourceOrigin: true,
+ })
+ .returns(new Promise(() => {})) // Never resolved.
+ .once();
+ const promise = adapter.authorize();
+ return Promise.resolve().then(() => {
+ clock.tick(3001);
+ return promise;
+ }).then(() => {
+ throw new Error('must never happen');
+ }, error => {
+ expect(error).to.match(/timeout/);
+ });
+ });
+ });
+
+ describe('pingback', () => {
+ it('should send POST pingback', () => {
+ contextMock.expects('buildUrl')
+ .withExactArgs(
+ 'https://acme.com/p?rid=READER_ID',
+ /* useAuthData */ true)
+ .returns(Promise.resolve('https://acme.com/p?rid=reader1'))
+ .once();
+ xhrMock.expects('sendSignal')
+ .withExactArgs('https://acme.com/p?rid=reader1',
+ sinon.match(init => {
+ return (init.method == 'POST' &&
+ init.credentials == 'include' &&
+ init.requireAmpResponseSourceOrigin == true &&
+ init.body == '' &&
+ init.headers['Content-Type'] ==
+ 'application/x-www-form-urlencoded');
+ }))
+ .returns(Promise.resolve())
+ .once();
+ return adapter.pingback();
+ });
+
+ it('should fail when POST fails', () => {
+ contextMock.expects('buildUrl')
+ .withExactArgs(
+ 'https://acme.com/p?rid=READER_ID',
+ /* useAuthData */ true)
+ .returns(Promise.resolve('https://acme.com/p?rid=reader1'))
+ .once();
+ xhrMock.expects('sendSignal')
+ .withExactArgs('https://acme.com/p?rid=reader1',
+ sinon.match(init => {
+ return (init.method == 'POST' &&
+ init.credentials == 'include' &&
+ init.requireAmpResponseSourceOrigin == true &&
+ init.body == '' &&
+ init.headers['Content-Type'] ==
+ 'application/x-www-form-urlencoded');
+ }))
+ .returns(Promise.reject('intentional'))
+ .once();
+ return adapter.pingback().then(() => {
+ throw new Error('must never happen');
+ }, error => {
+ expect(error).to.match(/intentional/);
+ });
+ });
+ });
+ });
+});
diff --git a/extensions/amp-access/0.1/test/test-amp-access-other.js b/extensions/amp-access/0.1/test/test-amp-access-other.js
new file mode 100644
index 000000000000..8375dc4fb241
--- /dev/null
+++ b/extensions/amp-access/0.1/test/test-amp-access-other.js
@@ -0,0 +1,127 @@
+/**
+ * Copyright 2016 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 {AccessOtherAdapter} from '../amp-access-other';
+import * as sinon from 'sinon';
+
+describe('AccessOtherAdapter', () => {
+
+ let sandbox;
+ let validConfig;
+ let context;
+ let contextMock;
+
+ beforeEach(() => {
+ sandbox = sinon.sandbox.create();
+
+ validConfig = {};
+
+ context = {
+ buildUrl: () => {},
+ };
+ contextMock = sandbox.mock(context);
+ });
+
+ afterEach(() => {
+ contextMock.verify();
+ sandbox.restore();
+ });
+
+
+ describe('config', () => {
+ it('should load valid config', () => {
+ const adapter = new AccessOtherAdapter(window, validConfig, context);
+ expect(adapter.authorizationResponse_).to.be.null;
+ expect(adapter.getConfig()).to.deep.equal({
+ authorizationResponse: null,
+ });
+ expect(adapter.isProxyOrigin_).to.be.false;
+ });
+
+ it('should load valid config with fallback object', () => {
+ const obj = {'access': 'A'};
+ validConfig['authorizationFallbackResponse'] = obj;
+ const adapter = new AccessOtherAdapter(window, validConfig, context);
+ expect(adapter.authorizationResponse_).to.be.equal(obj);
+ expect(adapter.getConfig()).to.deep.equal({
+ authorizationResponse: obj,
+ });
+ expect(adapter.isProxyOrigin_).to.be.false;
+ });
+ });
+
+
+ describe('runtime', () => {
+ let adapter;
+
+ beforeEach(() => {
+ adapter = new AccessOtherAdapter(window, {}, context);
+ });
+
+ afterEach(() => {
+ });
+
+ it('should disable authorization without fallback object', () => {
+ adapter.authorizationResponse_ = null;
+
+ adapter.isProxyOrigin_ = false;
+ expect(adapter.isAuthorizationEnabled()).to.be.false;
+
+ adapter.isProxyOrigin_ = true;
+ expect(adapter.isAuthorizationEnabled()).to.be.false;
+ });
+
+ it('should disable authorization on proxy', () => {
+ adapter.isProxyOrigin_ = true;
+
+ adapter.authorizationResponse_ = null;
+ expect(adapter.isAuthorizationEnabled()).to.be.false;
+
+ adapter.authorizationResponse_ = {};
+ expect(adapter.isAuthorizationEnabled()).to.be.false;
+ });
+
+ it('should enable authorization when not on proxy and with auth', () => {
+ adapter.isProxyOrigin_ = false;
+ adapter.authorizationResponse_ = {};
+ expect(adapter.isAuthorizationEnabled()).to.be.true;
+ });
+
+ it('should fail authorization on proxy', () => {
+ adapter.isProxyOrigin_ = true;
+ adapter.authorizationResponse_ = {};
+ contextMock.expects('buildUrl').never();
+ expect(() => {
+ adapter.authorize();
+ }).to.throw();
+ });
+
+ it('should respond to authorization when not on proxy proxy', () => {
+ adapter.isProxyOrigin_ = false;
+ const obj = {'access': 'A'};
+ adapter.authorizationResponse_ = obj;
+ contextMock.expects('buildUrl').never();
+ return adapter.authorize().then(response => {
+ expect(response).to.equal(obj);
+ });
+ });
+
+ it('should short-circuit pingback flow', () => {
+ contextMock.expects('buildUrl').never();
+ return adapter.pingback();
+ });
+ });
+});
diff --git a/extensions/amp-access/0.1/test/test-amp-access.js b/extensions/amp-access/0.1/test/test-amp-access.js
index e8be957a8558..c7f1bc48b186 100644
--- a/extensions/amp-access/0.1/test/test-amp-access.js
+++ b/extensions/amp-access/0.1/test/test-amp-access.js
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import {AccessClientAdapter} from '../amp-access-client';
+import {AccessOtherAdapter} from '../amp-access-other';
import {AccessService} from '../amp-access';
import {Observable} from '../../../../src/observable';
import {installCidService} from '../../../../src/service/cid-impl';
@@ -50,7 +52,6 @@ describe('AccessService', () => {
const service = new AccessService(window);
expect(service.isEnabled()).to.be.false;
expect(service.accessElement_).to.be.undefined;
- expect(service.config_).to.be.undefined;
});
it('should fail if config is malformed', () => {
@@ -59,51 +60,21 @@ describe('AccessService', () => {
}).to.throw(Error);
});
- it('should fail if config authorization is missing or malformed', () => {
- const config = {
- 'login': 'https://acme.org/l',
- };
+ it('should default to "client" and fail if authorization is missing', () => {
+ const config = {};
element.textContent = JSON.stringify(config);
expect(() => {
new AccessService(window);
}).to.throw(/"authorization" URL must be specified/);
-
- config['authorization'] = 'http://acme.com/a';
- element.textContent = JSON.stringify(config);
- expect(() => {
- new AccessService(window);
- }).to.throw(/https\:/);
});
- it('should fail if config pingback is missing or malformed', () => {
- const config = {
- 'authorization': 'https://acme.com/a',
- 'login': 'https://acme.org/l',
- };
- element.textContent = JSON.stringify(config);
- expect(() => {
- new AccessService(window);
- }).to.throw(/"pingback" URL must be specified/);
-
- config['pingback'] = 'http://acme.com/p';
- element.textContent = JSON.stringify(config);
- expect(() => {
- new AccessService(window);
- }).to.throw(/https\:/);
- });
-
- it('should fail if config login is missing or malformed', () => {
+ it('should fail if config login is malformed', () => {
const config = {
'authorization': 'https://acme.com/a',
'pingback': 'https://acme.com/p',
+ 'login': 'http://acme.com/l',
};
element.textContent = JSON.stringify(config);
- expect(() => {
- new AccessService(window);
- }).to.throw(/At least one "login" URL must be specified/);
-
- config['login'] = 'http://acme.com/l';
- element.textContent = JSON.stringify(config);
expect(() => {
new AccessService(window);
}).to.throw(/https\:/);
@@ -119,9 +90,10 @@ describe('AccessService', () => {
const service = new AccessService(window);
expect(service.isEnabled()).to.be.true;
expect(service.accessElement_).to.equal(element);
- expect(service.config_.authorization).to.equal('https://acme.com/a');
- expect(service.config_.pingback).to.equal('https://acme.com/p');
- expect(service.config_.loginMap).to.deep.equal({'': 'https://acme.com/l'});
+ expect(service.type_).to.equal('client');
+ expect(service.loginConfig_).to.deep.equal({'': 'https://acme.com/l'});
+ expect(service.adapter_).to.be.instanceOf(AccessClientAdapter);
+ expect(service.adapter_.authorizationUrl_).to.equal('https://acme.com/a');
});
it('should parse multiple login URLs', () => {
@@ -136,40 +108,40 @@ describe('AccessService', () => {
element.textContent = JSON.stringify(config);
const service = new AccessService(window);
expect(service.isEnabled()).to.be.true;
- expect(service.config_.loginMap).to.deep.equal({
+ expect(service.loginConfig_).to.deep.equal({
'login1': 'https://acme.com/l1',
'login2': 'https://acme.com/l2',
});
});
- it('should default type to "client"', () => {
+ it('should parse type', () => {
const config = {
'authorization': 'https://acme.com/a',
'pingback': 'https://acme.com/p',
'login': 'https://acme.com/l',
};
element.textContent = JSON.stringify(config);
- const service = new AccessService(window);
- expect(service.config_.type).to.equal('client');
- });
+ expect(new AccessService(window).type_).to.equal('client');
+ expect(new AccessService(window).adapter_).to.be
+ .instanceOf(AccessClientAdapter);
- it('should parse type', () => {
- const config = {
- 'type': 'client',
- 'authorization': 'https://acme.com/a',
- 'pingback': 'https://acme.com/p',
- 'login': 'https://acme.com/l',
- };
+ config['type'] = 'client';
element.textContent = JSON.stringify(config);
- expect(new AccessService(window).config_.type).to.equal('client');
+ expect(new AccessService(window).type_).to.equal('client');
+ expect(new AccessService(window).adapter_).to.be
+ .instanceOf(AccessClientAdapter);
config['type'] = 'server';
element.textContent = JSON.stringify(config);
- expect(new AccessService(window).config_.type).to.equal('server');
+ expect(new AccessService(window).type_).to.equal('server');
+ expect(new AccessService(window).adapter_).to.be
+ .instanceOf(AccessClientAdapter);
config['type'] = 'other';
element.textContent = JSON.stringify(config);
- expect(new AccessService(window).config_.type).to.equal('other');
+ expect(new AccessService(window).type_).to.equal('other');
+ expect(new AccessService(window).adapter_).to.be
+ .instanceOf(AccessOtherAdapter);
});
it('should fail if type is unknown', () => {
@@ -236,20 +208,97 @@ describe('AccessService', () => {
'authorizationFallbackResponse': {'error': true},
});
const service = new AccessService(window);
- expect(service.config_.authorizationFallbackResponse).to.deep.equal(
+ expect(service.authorizationFallbackResponse_).to.deep.equal(
{'error': true});
});
});
+describe('AccessService adapter context', () => {
+
+ let sandbox;
+ let clock;
+ let configElement;
+ let service;
+ let context;
+
+ beforeEach(() => {
+ sandbox = sinon.sandbox.create();
+ clock = sandbox.useFakeTimers();
+ clock.tick(0);
+
+ markElementScheduledForTesting(window, 'amp-analytics');
+ installCidService(window);
+
+ configElement = document.createElement('script');
+ configElement.setAttribute('id', 'amp-access');
+ configElement.setAttribute('type', 'application/json');
+ configElement.textContent = JSON.stringify({
+ 'authorization': 'https://acme.com/a?rid=READER_ID',
+ 'pingback': 'https://acme.com/p?rid=READER_ID',
+ });
+ document.body.appendChild(configElement);
+
+ service = new AccessService(window);
+ service.readerIdPromise_ = Promise.resolve('reader1');
+ context = service.adapter_.context_;
+ });
+
+ afterEach(() => {
+ if (configElement.parentElement) {
+ configElement.parentElement.removeChild(configElement);
+ }
+ sandbox.restore();
+ });
+
+ it('should resolve URL without auth response and no authdata vars', () => {
+ return context.buildUrl('?rid=READER_ID&type=AUTHDATA(child.type)',
+ /* useAuthData */ false).then(url => {
+ expect(url).to.equal('?rid=reader1&type=');
+ });
+ });
+
+ it('should resolve URL without auth response and with authdata vars', () => {
+ return context.buildUrl('?rid=READER_ID&type=AUTHDATA(child.type)',
+ /* useAuthData */ true).then(url => {
+ expect(url).to.equal('?rid=reader1&type=');
+ });
+ });
+
+ it('should resolve URL with auth response and no authdata vars', () => {
+ service.setAuthResponse_({child: {type: 'premium'}});
+ return context.buildUrl('?rid=READER_ID&type=AUTHDATA(child.type)',
+ /* useAuthData */ false).then(url => {
+ expect(url).to.equal('?rid=reader1&type=');
+ });
+ });
+
+ it('should resolve URL with auth response and with authdata vars', () => {
+ service.setAuthResponse_({child: {type: 'premium'}});
+ return context.buildUrl('?rid=READER_ID&type=AUTHDATA(child.type)',
+ /* useAuthData */ true).then(url => {
+ expect(url).to.equal('?rid=reader1&type=premium');
+ });
+ });
+
+ it('should resolve URL with unknown authdata var', () => {
+ service.setAuthResponse_({child: {type: 'premium'}});
+ return context.buildUrl('?rid=READER_ID&type=AUTHDATA(child.type2)',
+ /* useAuthData */ true).then(url => {
+ expect(url).to.equal('?rid=reader1&type=');
+ });
+ });
+});
+
+
describe('AccessService authorization', () => {
let sandbox;
let clock;
let configElement, elementOn, elementOff, elementError;
- let xhrMock;
let cidMock;
let analyticsMock;
+ let adapterMock;
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -285,6 +334,14 @@ describe('AccessService authorization', () => {
service = new AccessService(window);
+ const adapter = {
+ getConfig: () => {},
+ isAuthorizationEnabled: () => true,
+ authorize: () => {},
+ };
+ service.adapter_ = adapter;
+ adapterMock = sandbox.mock(adapter);
+
sandbox.stub(service.resources_, 'mutateElement',
(unusedElement, mutator) => {
mutator();
@@ -299,7 +356,6 @@ describe('AccessService authorization', () => {
return Promise.resolve();
},
};
- xhrMock = sandbox.mock(service.xhr_);
const cid = {
get: () => {},
};
@@ -326,6 +382,7 @@ describe('AccessService authorization', () => {
if (elementError.parentElement) {
elementError.parentElement.removeChild(elementError);
}
+ adapterMock.verify();
analyticsMock.verify();
sandbox.restore();
});
@@ -339,13 +396,32 @@ describe('AccessService authorization', () => {
.once();
}
+ it('should short-circuit authorization flow when disabled', () => {
+ adapterMock.expects('isAuthorizationEnabled')
+ .withExactArgs()
+ .returns(false)
+ .once();
+ adapterMock.expects('authorize').never();
+ cidMock.expects('get').never();
+ const promise = service.runAuthorization_();
+ expect(document.documentElement).not.to.have.class('amp-access-loading');
+ expect(document.documentElement).not.to.have.class('amp-access-error');
+ return promise.then(() => {
+ expect(document.documentElement).not.to.have.class('amp-access-loading');
+ expect(document.documentElement).not.to.have.class('amp-access-error');
+ expect(service.firstAuthorizationPromise_).to.exist;
+ return service.firstAuthorizationPromise_;
+ }).then(() => {
+ expect(service.lastAuthorizationPromise_).to.equal(
+ service.firstAuthorizationPromise_);
+ expect(service.authResponse_).to.be.null;
+ });
+ });
+
it('should run authorization flow', () => {
expectGetReaderId('reader1');
- xhrMock.expects('fetchJson')
- .withExactArgs('https://acme.com/a?rid=reader1', {
- credentials: 'include',
- requireAmpResponseSourceOrigin: true,
- })
+ adapterMock.expects('authorize')
+ .withExactArgs()
.returns(Promise.resolve({access: true}))
.once();
service.buildLoginUrls_ = sandbox.spy();
@@ -372,11 +448,8 @@ describe('AccessService authorization', () => {
it('should recover from authorization failure', () => {
expectGetReaderId('reader1');
- xhrMock.expects('fetchJson')
- .withExactArgs('https://acme.com/a?rid=reader1', {
- credentials: 'include',
- requireAmpResponseSourceOrigin: true,
- })
+ adapterMock.expects('authorize')
+ .withExactArgs()
.returns(Promise.reject('intentional'))
.once();
const promise = service.runAuthorization_();
@@ -392,11 +465,8 @@ describe('AccessService authorization', () => {
it('should NOT resolve last promise until first success', () => {
expectGetReaderId('reader1');
- xhrMock.expects('fetchJson')
- .withExactArgs('https://acme.com/a?rid=reader1', {
- credentials: 'include',
- requireAmpResponseSourceOrigin: true,
- })
+ adapterMock.expects('authorize')
+ .withExactArgs()
.returns(Promise.reject('intentional'))
.once();
const promise = service.runAuthorization_();
@@ -422,42 +492,13 @@ describe('AccessService authorization', () => {
});
});
- it('should time out authorization flow', () => {
- expectGetReaderId('reader1');
- xhrMock.expects('fetchJson')
- .withExactArgs('https://acme.com/a?rid=reader1', {
- credentials: 'include',
- requireAmpResponseSourceOrigin: true,
- })
- .returns(new Promise(() => {}))
- .once();
- service.buildLoginUrls_ = sandbox.spy();
- let actualTimeoutDelay;
- sandbox.stub(service.timer_, 'delay', (callback, delay) => {
- actualTimeoutDelay = delay;
- callback();
- });
- const promise = service.runAuthorization_();
- expect(document.documentElement).to.have.class('amp-access-loading');
- expect(document.documentElement).not.to.have.class('amp-access-error');
- return promise.then(() => {
- expect(document.documentElement).not.to.have.class('amp-access-loading');
- expect(document.documentElement).to.have.class('amp-access-error');
- expect(service.authResponse_).to.not.exist;
- expect(actualTimeoutDelay).to.equal(3000);
- });
- });
-
it('should use fallback on authorization failure when available', () => {
expectGetReaderId('reader1');
- xhrMock.expects('fetchJson')
- .withExactArgs('https://acme.com/a?rid=reader1', {
- credentials: 'include',
- requireAmpResponseSourceOrigin: true,
- })
+ adapterMock.expects('authorize')
+ .withExactArgs()
.returns(Promise.reject('intentional'))
.once();
- service.config_.authorizationFallbackResponse = {'error': true};
+ service.authorizationFallbackResponse_ = {'error': true};
const promise = service.runAuthorization_();
expect(document.documentElement).to.have.class('amp-access-loading');
expect(document.documentElement).not.to.have.class('amp-access-error');
@@ -472,14 +513,11 @@ describe('AccessService authorization', () => {
it('should NOT fallback on authorization failure when disabled', () => {
expectGetReaderId('reader1');
- xhrMock.expects('fetchJson')
- .withExactArgs('https://acme.com/a?rid=reader1', {
- credentials: 'include',
- requireAmpResponseSourceOrigin: true,
- })
+ adapterMock.expects('authorize')
+ .withExactArgs()
.returns(Promise.reject('intentional'))
.once();
- service.config_.authorizationFallbackResponse = {'error': true};
+ service.authorizationFallbackResponse_ = {'error': true};
const promise = service.runAuthorization_(/* disableFallback */ true);
expect(document.documentElement).to.have.class('amp-access-loading');
expect(document.documentElement).not.to.have.class('amp-access-error');
@@ -490,11 +528,8 @@ describe('AccessService authorization', () => {
it('should resolve first-authorization promise after success', () => {
expectGetReaderId('reader1');
- xhrMock.expects('fetchJson')
- .withExactArgs('https://acme.com/a?rid=reader1', {
- credentials: 'include',
- requireAmpResponseSourceOrigin: true,
- })
+ adapterMock.expects('authorize')
+ .withExactArgs()
.returns(Promise.resolve({access: true}))
.once();
analyticsMock.expects('triggerEvent')
@@ -508,11 +543,8 @@ describe('AccessService authorization', () => {
it('should NOT resolve first-authorization promise after failure', () => {
expectGetReaderId('reader1');
- xhrMock.expects('fetchJson')
- .withExactArgs('https://acme.com/a?rid=reader1', {
- credentials: 'include',
- requireAmpResponseSourceOrigin: true,
- })
+ adapterMock.expects('authorize')
+ .withExactArgs()
.returns(Promise.reject('intentional'))
.once();
analyticsMock.expects('triggerEvent')
@@ -703,7 +735,7 @@ describe('AccessService pingback', () => {
let sandbox;
let clock;
let configElement;
- let xhrMock;
+ let adapterMock;
let cidMock;
let analytics;
let analyticsMock;
@@ -730,7 +762,11 @@ describe('AccessService pingback', () => {
service = new AccessService(window);
- xhrMock = sandbox.mock(service.xhr_);
+ const adapter = {
+ pingback: () => {},
+ };
+ service.adapter_ = adapter;
+ adapterMock = sandbox.mock(adapter);
const cid = {
get: () => {},
@@ -769,6 +805,7 @@ describe('AccessService pingback', () => {
if (configElement.parentElement) {
configElement.parentElement.removeChild(configElement);
}
+ adapterMock.verify();
analyticsMock.verify();
sandbox.restore();
});
@@ -974,16 +1011,8 @@ describe('AccessService pingback', () => {
it('should send POST pingback', () => {
expectGetReaderId('reader1');
- xhrMock.expects('sendSignal')
- .withExactArgs('https://acme.com/p?rid=reader1&type=',
- sinon.match(init => {
- return (init.method == 'POST' &&
- init.credentials == 'include' &&
- init.requireAmpResponseSourceOrigin == true &&
- init.body == '' &&
- init.headers['Content-Type'] ==
- 'application/x-www-form-urlencoded');
- }))
+ adapterMock.expects('pingback')
+ .withExactArgs()
.returns(Promise.resolve())
.once();
analyticsMock.expects('triggerEvent')
@@ -998,34 +1027,10 @@ describe('AccessService pingback', () => {
});
});
- it('should resolve AUTH vars in POST pingback', () => {
- expectGetReaderId('reader1');
- service.setAuthResponse_({child: {type: 'premium'}});
- xhrMock.expects('sendSignal')
- .withArgs('https://acme.com/p?rid=reader1&type=premium')
- .returns(Promise.resolve())
- .once();
- return service.reportViewToServer_().then(() => {
- return 'SUCCESS';
- }, error => {
- return 'ERROR ' + error;
- }).then(result => {
- expect(result).to.equal('SUCCESS');
- });
- });
-
it('should NOT send analytics event if postback failed', () => {
expectGetReaderId('reader1');
- xhrMock.expects('sendSignal')
- .withExactArgs('https://acme.com/p?rid=reader1&type=',
- sinon.match(init => {
- return (init.method == 'POST' &&
- init.credentials == 'include' &&
- init.requireAmpResponseSourceOrigin == true &&
- init.body == '' &&
- init.headers['Content-Type'] ==
- 'application/x-www-form-urlencoded');
- }))
+ adapterMock.expects('pingback')
+ .withExactArgs()
.returns(Promise.reject('intentional'))
.once();
analyticsMock.expects('triggerEvent')
@@ -1149,7 +1154,7 @@ describe('AccessService login', () => {
});
it('should build multiple login url', () => {
- service.config_.loginMap = {
+ service.loginConfig_ = {
'login1': 'https://acme.com/l1?rid=READER_ID',
'login2': 'https://acme.com/l2?rid=READER_ID',
};
@@ -1185,7 +1190,7 @@ describe('AccessService login', () => {
});
it('should build login url with RETURN_URL', () => {
- service.config_.loginMap[''] =
+ service.loginConfig_[''] =
'https://acme.com/l?rid=READER_ID&ret=RETURN_URL';
cidMock.expects('get')
.withExactArgs(
@@ -1315,7 +1320,7 @@ describe('AccessService login', () => {
});
it('should succeed login with success=true with multiple logins', () => {
- service.config_.loginMap = {
+ service.loginConfig_ = {
'login1': 'https://acme.com/l1?rid=READER_ID',
'login2': 'https://acme.com/l2?rid=READER_ID',
};
@@ -1488,122 +1493,3 @@ describe('AccessService analytics', () => {
});
});
});
-
-
-describe('AccessService type=other', () => {
-
- let sandbox;
- let configElement;
- let xhrMock;
- let cidMock;
-
- beforeEach(() => {
- sandbox = sinon.sandbox.create();
-
- markElementScheduledForTesting(window, 'amp-analytics');
- installCidService(window);
-
- configElement = document.createElement('script');
- configElement.setAttribute('id', 'amp-access');
- configElement.setAttribute('type', 'application/json');
- configElement.textContent = JSON.stringify({'type': 'other'});
- document.body.appendChild(configElement);
- document.documentElement.classList.remove('amp-access-error');
-
- service = new AccessService(window);
-
- service.vsync_ = {
- mutate: callback => {
- callback();
- },
- mutatePromise: callback => {
- callback();
- return Promise.resolve();
- },
- };
- xhrMock = sandbox.mock(service.xhr_);
- const cid = {
- get: () => {},
- };
- cidMock = sandbox.mock(cid);
- service.cid_ = Promise.resolve(cid);
- });
-
- afterEach(() => {
- if (configElement.parentElement) {
- configElement.parentElement.removeChild(configElement);
- }
- sandbox.restore();
- });
-
- it('should short-circuit authorization flow', () => {
- cidMock.expects('get').never();
- xhrMock.expects('fetchJson').never();
- const promise = service.runAuthorization_();
- expect(document.documentElement).not.to.have.class('amp-access-loading');
- expect(document.documentElement).not.to.have.class('amp-access-error');
- return promise.then(() => {
- expect(document.documentElement).not.to.have.class('amp-access-loading');
- expect(document.documentElement).not.to.have.class('amp-access-error');
- expect(service.firstAuthorizationPromise_).to.exist;
- return service.firstAuthorizationPromise_;
- }).then(() => {
- expect(service.lastAuthorizationPromise_).to.equal(
- service.firstAuthorizationPromise_);
- expect(service.authResponse_).to.be.null;
- });
- });
-
- it('should ignore fallback in a proxy case', () => {
- expect(service.isProxyOrigin_).to.be.false; // Normally `false` in tests.
- service.isProxyOrigin_ = true;
- const fallback = {};
- service.config_.authorizationFallbackResponse = fallback;
- cidMock.expects('get').never();
- xhrMock.expects('fetchJson').never();
- const promise = service.runAuthorization_();
- expect(document.documentElement).not.to.have.class('amp-access-loading');
- expect(document.documentElement).not.to.have.class('amp-access-error');
- return promise.then(() => {
- expect(document.documentElement).not.to.have.class('amp-access-loading');
- expect(document.documentElement).not.to.have.class('amp-access-error');
- expect(service.firstAuthorizationPromise_).to.exist;
- return service.firstAuthorizationPromise_;
- }).then(() => {
- expect(service.lastAuthorizationPromise_).to.equal(
- service.firstAuthorizationPromise_);
- expect(service.authResponse_).to.be.null;
- });
- });
-
- it('should allow fallback use in a non-proxy case', () => {
- service.isProxyOrigin_ = false;
- const fallback = {};
- service.config_.authorizationFallbackResponse = fallback;
- cidMock.expects('get').never();
- xhrMock.expects('fetchJson').never();
- const promise = service.runAuthorization_();
- expect(document.documentElement).to.have.class('amp-access-loading');
- expect(document.documentElement).not.to.have.class('amp-access-error');
- const lastPromise = service.lastAuthorizationPromise_;
- return promise.then(() => {
- expect(document.documentElement).not.to.have.class('amp-access-loading');
- expect(document.documentElement).not.to.have.class('amp-access-error');
- expect(service.firstAuthorizationPromise_).to.exist;
- return service.firstAuthorizationPromise_;
- }).then(() => {
- expect(service.lastAuthorizationPromise_).to.equal(lastPromise);
- expect(service.authResponse_).to.equal(fallback);
- });
- });
-
- it('should short-circuit pingback flow', () => {
- cidMock.expects('get').never();
- xhrMock.expects('fetchJson').never();
- return service.reportViewToServer_();
- });
-
- it('should short-circuit login flow', () => {
- expect(() => service.login('')).to.throw(/Login URL is not configured/);
- });
-});