Merge branch 'master' into train-141-merge

This commit is contained in:
Shane Tomlinson 2019-07-17 20:20:29 +01:00
Родитель 02a8574849 4b8c093c6d
Коммит cd377a5bc8
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 09D4F897B87A2D19
60 изменённых файлов: 2199 добавлений и 1259 удалений

Просмотреть файл

@ -99,7 +99,7 @@ If you get an `error` status for any of the servers please verify that you insta
> [libgmp](https://gmplib.org/),
> [graphicsmagick](http://www.graphicsmagick.org/),
> [docker](https://docs.docker.com/),
> [grunt](https://github.com/gruntjs/grunt-cli)
> [grunt](https://github.com/gruntjs/grunt-cli),
> [gcloud CLI](https://cloud.google.com/sdk/)
##### OS X (with [Brew](http://brew.sh/)):
@ -170,7 +170,7 @@ Download from [java.com/en/download/](https://www.java.com/en/download/)
> Rust Nightly is used for the fxa-email-service
#### Ubuntu
##### Ubuntu and OS X
```
curl https://sh.rustup.rs -sSf | sh
@ -213,7 +213,19 @@ Available options:
**The following requires [the JDK](http://www.oracle.com/technetwork/java/javase/downloads/index-jsp-138363.html#javasejdk) and tests the local servers only.**
**Use `npm test` - all functional tests**
To run all functional tests:
```
npm test
```
Note that as of 2019-07-08, running this command at the project root will fail ([see issue #725](https://github.com/mozilla/fxa/issues/725)). Instead, run the command in the server that needs to be tested.
To run a specific test or tests whose name matches part of a search string:
```
node tests/intern.js --suites=all --grep="Test string to search for"
```
---
@ -289,7 +301,7 @@ Once services have started, you can start MailDev on port 9999. You might have t
sudo maildev -s 9999
```
All emails sent can be viewed from `http://localhost:1080`.
All emails sent can be viewed from [http://localhost:1080](http://localhost:1080).
---

Просмотреть файл

@ -53,20 +53,6 @@
"max_restarts": "1",
"min_uptime": "2m"
},
{
"name": "fxa-basket-proxy PORT 1114",
"script": "./bin/basket-proxy-server.js",
"cwd": "packages/fxa-basket-proxy",
"max_restarts": "1",
"min_uptime": "2m"
},
{
"name": "fxa-basket-proxy fake basket server PORT 10140",
"script": "./bin/fake-basket-server.js",
"cwd": "packages/fxa-basket-proxy",
"max_restarts": "1",
"min_uptime": "2m"
},
{
"name": "oauth-server PORT 9010",
"script": "../../../_scripts/oauth_mysql.sh",

1590
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -29,7 +29,7 @@
"fxa-dev-launcher": "github:vladikoff/fxa-dev-launcher",
"husky": "^2.5.0",
"internal-ip": "1.2.0",
"lerna": "^3.13.1",
"lerna": "^3.15.0",
"lint-staged": "^8.2.1",
"madr": "^2.1.2",
"node-fetch": "^2.3.0",

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -158,7 +158,7 @@ module.exports.generateTokens = async function generateTokens(grant) {
}
// Maybe also generate an idToken?
if (grant.scope && grant.scope.contains(SCOPE_OPENID)) {
result.id_token = await generateIdToken(grant, access);
result.id_token = await generateIdToken(grant, result.access_token);
}
amplitude('token.created', {
@ -169,7 +169,7 @@ module.exports.generateTokens = async function generateTokens(grant) {
return result;
};
async function generateIdToken(grant, access) {
async function generateIdToken(grant, accessToken) {
var now = Math.floor(Date.now() / 1000);
var claims = {
sub: await sub(grant.userId, grant.clientId, grant.ppidSeed),
@ -177,7 +177,7 @@ async function generateIdToken(grant, access) {
//iss set in jwt.sign
iat: now,
exp: now + ID_TOKEN_EXPIRATION,
at_hash: util.generateTokenHash(access.token),
at_hash: util.generateTokenHash(accessToken),
};
if (grant.amr) {
claims.amr = grant.amr;

Просмотреть файл

@ -29,15 +29,15 @@ const base64URLEncode = function base64URLEncode(buf) {
* Generates a hash of the access token based on
* http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
*
* This value is the hash of the ascii value of the access token, then the base64url
* This hash of the access token, then the base64url
* value of the left half.
*
* @param {Buffer} accessTokenBuf
* @param {Buffer} accessToken The access token as seen by the client (hex form)
* @returns {String}
* @api public
*/
const generateTokenHash = function generateTokenHash(accessTokenBuf) {
const hash = encrypt.hash(accessTokenBuf.toString('ascii'));
const generateTokenHash = function generateTokenHash(accessToken) {
const hash = encrypt.hash(accessToken);
return base64URLEncode(hash.slice(0, hash.length / 2));
};

Просмотреть файл

@ -2272,9 +2272,7 @@ describe('/v1', function() {
assert.equal(claims.acr, ACR);
assert.equal(claims['fxa-aal'], AAL);
const at_hash = util.generateTokenHash(
Buffer.from(res.result.access_token, 'hex')
);
const at_hash = util.generateTokenHash(res.result.access_token);
assert.equal(claims.at_hash, at_hash);
});
});

Просмотреть файл

@ -63,6 +63,10 @@ To change the default auth server edit `server/config/*.json` on your deployed i
**Note that testing with Selenium via Docker does _not_ work at present, so all testing must be carried out via your normal operating system's npm & Java tooling.**
### Unit Tests
If you want to test only the unit tests (not Selenium/function tests) you can visit http://127.0.0.1:3030/tests/index.html and you can select specific tests with something like http://127.0.0.1:3030/tests/index.html?grep=fxa-client
---
## Grunt Commands

Просмотреть файл

@ -0,0 +1,15 @@
<svg width="141" height="18" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(3 3)" fill="none" fill-rule="evenodd">
<circle stroke="#E0E0E6" stroke-width="3" cx="6" cy="6" r="7.5"/>
<circle fill="#0090ED" cx="67.75" cy="6" r="3.75"/>
<circle stroke="#E0E0E6" stroke-width="3" cx="67.5" cy="6" r="7.5"/>
<circle stroke="#E0E0E6" stroke-width="3" cx="129" cy="6" r="7.5"/>
<path d="M13.5 6.375H60M75 6.375h46.5" stroke="#E0E0E6" stroke-width="3"/>
<g transform="translate(123)">
<circle stroke="#005CE3" stroke-width="3" fill="#005CE3" cx="6" cy="6" r="7.5"/>
<path
d="M4.746 9.75a.626.626 0 0 1-.442-.184L2.426 7.69a.625.625 0 0 1 .885-.884l1.35 1.349 3.953-5.643a.626.626 0 0 1 1.026.717L5.26 9.482a.626.626 0 0 1-.458.267.54.54 0 0 1-.055 0z"
fill="#FFF" fill-rule="nonzero"/>
</g>
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 927 B

Просмотреть файл

@ -0,0 +1,22 @@
<svg width="141" height="18" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(3 3)" fill="none" fill-rule="evenodd">
<circle fill="#0090ED" cx="6" cy="6" r="3.75"/>
<circle stroke="#E0E0E6" stroke-width="3" cx="6" cy="6" r="7.5"/>
<circle stroke="#E0E0E6" stroke-width="3" cx="67.5" cy="6" r="7.5"/>
<circle stroke="#E0E0E6" stroke-width="3" cx="129" cy="6" r="7.5"/>
<path d="M13.5 6.375H60" stroke="#E0E0E6" stroke-width="3"/>
<path d="M75 6.375h46.5" stroke="#E0E0E6" stroke-width="3"/>
<g transform="translate(62)">
<circle stroke="#005CE3" stroke-width="3" fill="#005CE3" cx="6" cy="6" r="7.5"/>
<path
d="M4.746 9.75a.626.626 0 0 1-.442-.184L2.426 7.69a.625.625 0 0 1 .885-.884l1.35 1.349 3.953-5.643a.626.626 0 0 1 1.026.717L5.26 9.482a.626.626 0 0 1-.458.267.54.54 0 0 1-.055 0z"
fill="#FFF" fill-rule="nonzero"/>
</g>
<g transform="translate(123)">
<circle stroke="#005CE3" stroke-width="3" fill="#005CE3" cx="6" cy="6" r="7.5"/>
<path
d="M4.746 9.75a.626.626 0 0 1-.442-.184L2.426 7.69a.625.625 0 0 1 .885-.884l1.35 1.349 3.953-5.643a.626.626 0 0 1 1.026.717L5.26 9.482a.626.626 0 0 1-.458.267.54.54 0 0 1-.055 0z"
fill="#FFF" fill-rule="nonzero"/>
</g>
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

Просмотреть файл

@ -362,6 +362,7 @@ Start.prototype = {
oAuthClientId: this._config.oAuthClientId,
profileClient: this._profileClient,
sentryMetrics: this._sentryMetrics,
subscriptionsConfig: this._config.subscriptions,
storage: this._getUserStorageInstance(),
uniqueUserId: this._getUniqueUserId(),
}));

Просмотреть файл

@ -1274,6 +1274,29 @@ FxaClientWrapper.prototype = {
* - `keyRotationTimestamp`
*/
getOAuthScopedKeyData: createClientDelegate('getOAuthScopedKeyData'),
/**
* Get a list of active subscriptions with an OAuth access token.
*
* @param {String} token A token from the OAuth server.
* @returns {Promise} A promise that will be fulfilled with a list of active
* subscriptions.
*/
getActiveSubscriptions: createClientDelegate('getActiveSubscriptions'),
/**
* Create a support ticket.
*
* @param {String} token A token from the OAuth server.
* @param {Object} [supportTicket={}]
* @param {String} [supportTicket.topic]
* @param {String} [supportTicket.subject] Optional subject
* @param {String} [supportTicket.message]
* @returns {Promise} A promise that will be fulfilled with:
* - `success`
* - `ticket` OR `error`
*/
createSupportTicket: createClientDelegate('createSupportTicket'),
};
export default FxaClientWrapper;

Просмотреть файл

@ -0,0 +1,30 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const PaymentServer = {
navigateToPaymentServer(view, subscriptionsConfig, redirectPath) {
const {
managementClientId,
managementScopes,
managementTokenTTL,
managementUrl,
} = subscriptionsConfig;
return view
.getSignedInAccount()
.createOAuthToken(managementClientId, {
scope: managementScopes,
ttl: managementTokenTTL,
})
.then(accessToken => {
const url = `${managementUrl}/${redirectPath}#accessToken=${encodeURIComponent(
accessToken.get('token')
)}`;
view.navigateAway(url);
});
},
};
export default PaymentServer;

Просмотреть файл

@ -51,6 +51,7 @@ import SignUpView from '../views/sign_up';
import SmsSendView from '../views/sms_send';
import SmsSentView from '../views/sms_sent';
import Storage from './storage';
import SubscriptionsProductRedirectView from '../views/subscriptions_product_redirect';
import TwoStepAuthenticationView from '../views/settings/two_step_authentication';
import VerificationReasons from './verification-reasons';
import WhyConnectAnotherDeviceView from '../views/why_connect_another_device';
@ -256,6 +257,9 @@ const Router = Backbone.Router.extend({
WhyConnectAnotherDeviceView,
SmsSendView
),
'subscriptions/products/:productId': createViewHandler(
SubscriptionsProductRedirectView
),
'verify_email(/)': createViewHandler(CompleteSignUpView, {
type: VerificationReasons.SIGN_UP,
}),

Просмотреть файл

@ -97,6 +97,7 @@ const Account = Backbone.Model.extend(
this._metrics = options.metrics;
this._notifier = options.notifier;
this._sentryMetrics = options.sentryMetrics;
this._subscriptionsConfig = options.subscriptionsConfig;
// upgrade old `grantedPermissions` to the new `permissions`.
this._upgradeGrantedPermissions();
@ -984,6 +985,59 @@ const Account = Backbone.Model.extend(
return this._fxaClient.attachedClients(this.get('sessionToken'));
},
/**
* Fetch the account's list of active subscriptions.
*
* @returns {Promise} - resolves with a list of subscription objects.
*/
fetchActiveSubscriptions() {
return this._fetchShortLivedSubscriptionsOAuthToken().then(
accessToken => {
return this._fxaClient.getActiveSubscriptions(
accessToken.get('token')
);
}
);
},
/**
* Fetch the account's list of active subscriptions.
*
* @param {Object} [supportTicket={}]
* @param {String} [supportTicket.topic]
* @param {String} [supportTicket.subject] Optional subject
* @param {String} [supportTicket.message]
* @returns {Promise} - resolves with:
* - `success`
* - `ticket` OR `error`
*/
createSupportTicket(supportTicket) {
return this._fetchShortLivedSubscriptionsOAuthToken().then(
accessToken => {
return this._fxaClient.createSupportTicket(
accessToken.get('token'),
supportTicket
);
}
);
},
/**
* Fetch an access token with subscription management scopes and a lifetime
* of 30 seconds.
*
* @returns {Promise<OAuthToken>}
*/
_fetchShortLivedSubscriptionsOAuthToken() {
return this.createOAuthToken(
this._subscriptionsConfig.managementClientId,
{
scope: this._subscriptionsConfig.managementScopes,
ttl: 30,
}
);
},
/**
* Disconnect a client from the account
*

Просмотреть файл

@ -27,6 +27,7 @@ var User = Backbone.Model.extend({
this._fxaClient = options.fxaClient;
this._metrics = options.metrics;
this._notifier = options.notifier;
this._subscriptionsConfig = options.subscriptionsConfig;
this._storage = options.storage || Storage.factory();
this.sentryMetrics = options.sentryMetrics;
@ -139,6 +140,7 @@ var User = Backbone.Model.extend({
oAuthClientId: this._oAuthClientId,
profileClient: this._profileClient,
sentryMetrics: this.sentryMetrics,
subscriptionsConfig: this._subscriptionsConfig,
});
},

Просмотреть файл

@ -0,0 +1,5 @@
<div id="main-content" class="card loading subscriptions-redirect">
<section>
<div class="spinner"></div>
</section>
</div>

Просмотреть файл

@ -8,6 +8,7 @@ import Cocktail from 'cocktail';
import FormView from '../form';
import SettingsPanelMixin from '../mixins/settings-panel-mixin';
import Template from 'templates/settings/subscription.mustache';
import PaymentServer from '../../lib/payment-server';
const View = FormView.extend({
template: Template,
@ -19,30 +20,18 @@ const View = FormView.extend({
},
initialize(options) {
this._config = {};
this._subscriptionsConfig = {};
if (options && options.config && options.config.subscriptions) {
this._config = options.config.subscriptions;
this._subscriptionsConfig = options.config.subscriptions;
}
},
submit() {
const {
managementClientId,
managementScopes,
managementTokenTTL,
managementUrl,
} = this._config;
return this.getSignedInAccount()
.createOAuthToken(managementClientId, {
scope: managementScopes,
ttl: managementTokenTTL,
})
.then(accessToken => {
const url = `${managementUrl}/subscriptions#accessToken=${encodeURIComponent(
accessToken.get('token')
)}`;
this.navigateAway(url);
});
return PaymentServer.navigateToPaymentServer(
this,
this._subscriptionsConfig,
'subscriptions'
);
},
});

Просмотреть файл

@ -0,0 +1,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import FormView from './form';
import Template from 'templates/subscriptions_redirect.mustache';
import PaymentServer from '../lib/payment-server';
class SubscriptionsProductRedirectView extends FormView {
mustAuth = true;
template = Template;
initialize(options) {
this._currentPage = options.currentPage;
this._subscriptionsConfig = {};
if (options && options.config && options.config.subscriptions) {
this._subscriptionsConfig = options.config.subscriptions;
}
}
afterRender() {
const searchQuery = this.window.location.search;
const productId = this._currentPage.split('/').pop();
const redirectPath = `products/${productId}${searchQuery}`;
return PaymentServer.navigateToPaymentServer(
this,
this._subscriptionsConfig,
redirectPath
);
}
}
export default SubscriptionsProductRedirectView;

Просмотреть файл

@ -275,14 +275,26 @@ a[data-visible-url^='http']::after {
.step-1 {
background-image: image-url('trailhead/step-1.svg');
html[dir='rtl'] & {
transform: scaleX(-1);
}
}
.step-2 {
background-image: image-url('trailhead/step-2.svg');
html[dir='rtl'] & {
background-image: image-url('trailhead/step-2-rtl.svg');
}
}
.step-3 {
background-image: image-url('trailhead/step-3.svg');
html[dir='rtl'] & {
background-image: image-url('trailhead/step-3-rtl.svg');
}
}
.step-4 {

Просмотреть файл

@ -48,7 +48,7 @@ button {
background: $button-background-color-primary;
color: $button-text-color-primary;
&:hover {
&:hover:not:disabled {
background: $button-background-color-primary-hover;
}

Просмотреть файл

@ -0,0 +1,65 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { assert } from 'chai';
import sinon from 'sinon';
import Account from 'models/account';
import PaymentServer from 'lib/payment-server';
describe('lib/payment-server-redirect', () => {
var account;
var view;
var tokenMock;
var config;
beforeEach(function() {
account = new Account();
config = {
subscriptions: {
managementClientId: 'MOCK_CLIENT_ID',
managementScopes: 'MOCK_SCOPES',
managementTokenTTL: 900,
managementUrl: 'http://example.com',
},
};
tokenMock = {
get: () => 'MOCK_TOKEN',
};
sinon
.stub(account, 'createOAuthToken')
.callsFake(() => Promise.resolve(tokenMock));
view = {
getSignedInAccount: sinon.stub().callsFake(() => account),
navigateAway: sinon.spy(),
};
});
it('redirects as expected', () => {
const REDIRECT_PATH = 'example/path';
PaymentServer.navigateToPaymentServer(
view,
config.subscriptions,
REDIRECT_PATH
).then(() => {
assert.lengthOf(view.getSignedInAccount.args, 1);
assert.deepEqual(
account.createOAuthToken.args[0],
[
config.subscriptions.managementScopes,
{
//eslint-disable-next-line camelcase
client_id: config.subscriptions.managementClientId,
ttl: config.subscriptions.managementTokenTTL,
},
],
'should make the correct call to account.createOAuthToken'
);
assert.deepEqual(
view.navigateAway.args[0][0],
`${config.subscriptions.managementUrl}/${REDIRECT_PATH}#accessToken=MOCK_TOKEN`,
'should make the correct call to navigateAway'
);
});
});
});

Просмотреть файл

@ -28,6 +28,7 @@ describe('models/account', function() {
var oAuthClient;
var profileClient;
var relier;
var subscriptionsConfig;
var CLIENT_ID = 'client_id';
var EMAIL = 'user@example.domain';
@ -63,6 +64,10 @@ describe('models/account', function() {
oAuthClient = new OAuthClient();
profileClient = new ProfileClient();
relier = new Relier();
subscriptionsConfig = {
managementClientId: 'foxkeh',
managementScopes: 'quux',
};
account = new Account(
{
@ -77,6 +82,7 @@ describe('models/account', function() {
oAuthClientId: CLIENT_ID,
profileClient: profileClient,
sentryMetrics: sentryMetrics,
subscriptionsConfig,
}
);
});
@ -2864,4 +2870,59 @@ describe('models/account', function() {
);
});
});
describe('_fetchShortLivedSubscriptionsOAuthToken', () => {
it('calls createOAuthToken with the correct arguments', () => {
const createOAuthTokenStub = sinon.stub(account, 'createOAuthToken');
account._fetchShortLivedSubscriptionsOAuthToken();
assert.isTrue(createOAuthTokenStub.calledOnce);
assert.isTrue(
createOAuthTokenStub.calledWith(
subscriptionsConfig.managementClientId,
{ scope: subscriptionsConfig.managementScopes, ttl: 30 }
)
);
});
});
describe('fetchActiveSubscriptions', () => {
it('delegates to the fxa-client', () => {
const token = 'tickettoride';
const subs = [{ sid: 'foo' }];
sinon.stub(account, 'createOAuthToken').callsFake(function() {
return Promise.resolve(new OAuthToken({ token }));
});
const subsStub = sinon
.stub(fxaClient, 'getActiveSubscriptions')
.callsFake(function() {
return Promise.resolve(subs);
});
account.fetchActiveSubscriptions().then(resp => {
assert.isTrue(subsStub.calledWith(token));
assert.deepEqual(resp, subs);
});
});
});
describe('createSupportTicket', () => {
it('delegates to the fxa-client', () => {
const token = 'tickettoride';
const ticket = { topic: 'TESTO', message: 'testo?' };
const ticketResp = { success: true, ticket: 123 };
sinon.stub(account, 'createOAuthToken').callsFake(function() {
return Promise.resolve(new OAuthToken({ token }));
});
const ticketStub = sinon
.stub(fxaClient, 'createSupportTicket')
.callsFake(function() {
return Promise.resolve(ticketResp);
});
account.createSupportTicket(ticket).then(resp => {
assert.isTrue(ticketStub.calledWith(token, ticket));
assert.deepEqual(resp, ticketResp);
});
});
});
});

Просмотреть файл

@ -3,19 +3,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import $ from 'jquery';
import Account from 'models/account';
import { assert } from 'chai';
import sinon from 'sinon';
import User from 'models/user';
import View from 'views/settings/subscription';
import WindowMock from '../../../mocks/window';
import PaymentServer from 'lib/payment-server';
describe('views/settings/subscription', function() {
var account;
var user;
var view;
var tokenMock;
var windowMock;
var config;
function render() {
@ -23,9 +17,6 @@ describe('views/settings/subscription', function() {
}
beforeEach(function() {
user = new User();
account = new Account();
windowMock = new WindowMock();
config = {
subscriptions: {
managementClientId: 'MOCK_CLIENT_ID',
@ -34,23 +25,18 @@ describe('views/settings/subscription', function() {
managementUrl: 'http://example.com',
},
};
tokenMock = {
get: () => 'MOCK_TOKEN',
};
view = new View({ config });
sinon
.stub(account, 'createOAuthToken')
.callsFake(() => Promise.resolve(tokenMock));
view = new View({ config, user, window: windowMock });
sinon.stub(view, 'getSignedInAccount').callsFake(() => account);
sinon.stub(view, 'navigateAway');
.stub(PaymentServer, 'navigateToPaymentServer')
.callsFake(() => Promise.resolve(true));
return render();
});
afterEach(function() {
PaymentServer.navigateToPaymentServer.restore();
$(view.el).remove();
view.destroy();
view = null;
@ -63,27 +49,10 @@ describe('views/settings/subscription', function() {
});
describe('submit', () => {
it('creates an OAuth token on submit', () => {
it('calls PaymentServer.navigateToPaymentServer as expected', () => {
return view.submit().then(() => {
assert.lengthOf(view.getSignedInAccount.args, 1);
assert.deepEqual(
account.createOAuthToken.args[0],
[
config.subscriptions.managementClientId,
{
scope: config.subscriptions.managementScopes,
ttl: config.subscriptions.managementTokenTTL,
},
],
'should make the correct call to account.createOAuthToken'
);
assert.deepEqual(
view.navigateAway.args[0],
[
`${config.subscriptions.managementUrl}/subscriptions#accessToken=MOCK_TOKEN`,
],
'should make the correct call to navigateAway'
);
const args = PaymentServer.navigateToPaymentServer.getCall(0).args;
assert.deepEqual(args, [view, config.subscriptions, 'subscriptions']);
});
});
});

Просмотреть файл

@ -0,0 +1,83 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import $ from 'jquery';
import Account from 'models/account';
import { assert } from 'chai';
import sinon from 'sinon';
import User from 'models/user';
import View from 'views/subscriptions_product_redirect';
import Notifier from 'lib/channels/notifier';
import WindowMock from '../../mocks/window';
import PaymentServer from 'lib/payment-server';
const PRODUCT_ID = 'pk_8675309';
const SEARCH_QUERY = '?plan_id=plk_12345';
describe('views/subscriptions_product_redirect', function() {
let account;
let user;
let view;
let windowMock;
let config;
let notifier;
function render() {
return view.render().then(() => view.afterVisible());
}
beforeEach(function() {
user = new User();
account = new Account();
notifier = new Notifier();
windowMock = new WindowMock();
windowMock.location.search = SEARCH_QUERY;
config = {
subscriptions: {
managementClientId: 'MOCK_CLIENT_ID',
managementScopes: 'MOCK_SCOPES',
managementTokenTTL: 900,
managementUrl: 'http://example.com',
},
};
sinon.stub(user, 'sessionStatus').callsFake(() => Promise.resolve(account));
view = new View({
config,
currentPage: `subscriptions/product/${PRODUCT_ID}`,
notifier,
user,
window: windowMock,
});
sinon.stub(view, 'getSignedInAccount').callsFake(() => account);
sinon
.stub(PaymentServer, 'navigateToPaymentServer')
.callsFake(() => Promise.resolve(true));
return render();
});
afterEach(function() {
PaymentServer.navigateToPaymentServer.restore();
$(view.el).remove();
view.destroy();
view = null;
});
describe('render', () => {
it('renders correctly', () => {
assert.lengthOf(view.$('.subscriptions-redirect'), 1);
});
it('calls PaymentServer.navigateToPaymentServer as expected', () => {
assert.deepEqual(PaymentServer.navigateToPaymentServer.args, [
[view, config.subscriptions, `products/${PRODUCT_ID}${SEARCH_QUERY}`],
]);
});
});
});

Просмотреть файл

@ -58,6 +58,7 @@ require('./spec/lib/null-storage');
require('./spec/lib/oauth-client');
require('./spec/lib/oauth-errors');
require('./spec/lib/pairing-channel-client');
require('./spec/lib/payment-server');
require('./spec/lib/profile-client');
require('./spec/lib/router');
require('./spec/lib/screen-info');
@ -255,6 +256,7 @@ require('./spec/views/sign_up_password');
require('./spec/views/sms_send');
require('./spec/views/sms_sent');
require('./spec/views/sub_panels');
require('./spec/views/subscriptions_product_redirect');
require('./spec/views/tooltip');
require('./spec/views/tos');
require('./spec/views/why_connect_another_device');

60
packages/fxa-content-server/npm-shrinkwrap.json сгенерированный
Просмотреть файл

@ -287,9 +287,9 @@
}
},
"@babel/helpers": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.2.tgz",
"integrity": "sha512-NDkkTqDvgFUeo8djXBOiwO/mFjownznOWvmP9hvNdfiFUmx0nwNOqxuaTTbxjH744eQsD9M5ubC7gdANBvIWPw==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.4.tgz",
"integrity": "sha512-6LJ6xwUEJP51w0sIgKyfvFMJvIb9mWAfohJp0+m6eHJigkFdcH8duZ1sfhn0ltJRzwUIT/yqqhdSfRpCpL7oow==",
"requires": {
"@babel/template": "^7.4.4",
"@babel/traverse": "^7.5.0",
@ -340,9 +340,9 @@
}
},
"@babel/plugin-proposal-object-rest-spread": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.2.tgz",
"integrity": "sha512-C/JU3YOx5J4d9s0GGlJlYXVwsbd5JmqQ0AvB7cIDAx7nN57aDTnlJEsZJPuSskeBtMGFWSWU5Q+piTiDe0s7FQ==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.4.tgz",
"integrity": "sha512-KCx0z3y7y8ipZUMAEEJOyNi11lMb/FOPUjjB113tfowgw0c16EGYos7worCKBcUAh2oG+OBnoUhsnTSoLpV9uA==",
"requires": {
"@babel/helper-plugin-utils": "^7.0.0",
"@babel/plugin-syntax-object-rest-spread": "^7.2.0"
@ -823,9 +823,9 @@
}
},
"@babel/runtime": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.2.tgz",
"integrity": "sha512-9M29wrrP7//JBGX70+IrDuD1w4iOYhUGpJNMQJVNAXue+cFeFlMTqBECouIziXPUphlgrfjcfiEpGX4t0WGK4g==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.4.tgz",
"integrity": "sha512-Na84uwyImZZc3FKf4aUF1tysApzwf3p2yuFBIyBfbzT5glzKTdvYI4KVW4kcgjrzoGUjC7w3YyCHcJKaRxsr2Q==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
@ -1191,9 +1191,9 @@
"dev": true
},
"@types/node": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.0.tgz",
"integrity": "sha512-dVeOVH/lhZ2Cki5Emh0aKeXUcWG1+EDTkqyzdgPe0ZjzgvBhzSFlogc6rm8uUd0I+XGK5fcp9DsMv5Wofe0/3w==",
"version": "12.6.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.2.tgz",
"integrity": "sha512-gojym4tX0FWeV2gsW4Xmzo5wxGjXGm550oVUII7f7G5o4BV6c7DBdiG1RRQd+y1bvqRyYtPfMK85UM95vsapqQ==",
"dev": true
},
"@types/platform": {
@ -2829,9 +2829,9 @@
"integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
},
"caniuse-lite": {
"version": "1.0.30000981",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000981.tgz",
"integrity": "sha512-JTByHj4DQgL2crHNMK6PibqAMrqqb/Vvh0JrsTJVSWG4VSUrT16EklkuRZofurlMjgA9e+zlCM4Y39F3kootMQ=="
"version": "1.0.30000983",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000983.tgz",
"integrity": "sha512-/llD1bZ6qwNkt41AsvjsmwNOoA4ZB+8iqmf5LVyeSXuBODT/hAMFNVOh84NdUzoiYiSKqo5vQ3ZzeYHSi/olDQ=="
},
"capture-stack-trace": {
"version": "1.0.1",
@ -5792,9 +5792,9 @@
"dev": true
},
"fxa-common-password-list": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/fxa-common-password-list/-/fxa-common-password-list-0.0.2.tgz",
"integrity": "sha512-xFhM7lu0A6drtSbXKgs6GsgUbfq6uqY1PhNdZMU5P6jBjLLADEmUuE4LTAL+8F/hQD8GtoMDGflhHzJFJwW43w==",
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/fxa-common-password-list/-/fxa-common-password-list-0.0.3.tgz",
"integrity": "sha512-lRQX4EfMuiNACW4/L6Yr7gsAB2E1kSZBiP5qDV/l+2RkFazpDdtqBEN5piQYyECr1qw18VLt/+M3agF3ZDFtLA==",
"requires": {
"bloomfilter": "0.0.18"
}
@ -5830,9 +5830,9 @@
}
},
"fxa-js-client": {
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/fxa-js-client/-/fxa-js-client-1.0.14.tgz",
"integrity": "sha512-Yh5OhC+/WS3fAdWpLC/4zlRGDXRaB86lom72+aqUqMQ2/Rz+0vhdS1dWQKfofkfg5IYkPTULump+r36e/pGvMw==",
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/fxa-js-client/-/fxa-js-client-1.0.15.tgz",
"integrity": "sha512-BEcPZbOB/Yr5zlO+fJgoNKR9T4kk+GO1Ai0X304i0H2/vP0RTzn3Cnq2Ew568M/ltPpcDUfQK0D5p0kl1hi5Lg==",
"requires": {
"es6-promise": "4.1.1",
"sjcl": "git://github.com/bitwiseshiftleft/sjcl.git#a03ea8e",
@ -8351,9 +8351,9 @@
}
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.13.tgz",
"integrity": "sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA=="
},
"lodash.assign": {
"version": "4.2.0",
@ -8404,9 +8404,9 @@
"dev": true
},
"lodash.merge": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz",
"integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ=="
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
"lodash.omit": {
"version": "4.5.0",
@ -12514,9 +12514,9 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typescript": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz",
"integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA=="
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz",
"integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g=="
},
"ua-parser-js": {
"version": "git://github.com/vladikoff/ua-parser-js.git#643d1698aef5bed095e1264ae258902bf346175c",

Просмотреть файл

@ -61,10 +61,10 @@
"express": "4.16.2",
"extract-loader": "2.0.1",
"file-loader": "1.1.11",
"fxa-common-password-list": "0.0.2",
"fxa-common-password-list": "^0.0.3",
"fxa-crypto-relier": "2.3.0",
"fxa-geodb": "1.0.4",
"fxa-js-client": "^1.0.14",
"fxa-js-client": "^1.0.15",
"fxa-mustache-loader": "0.0.2",
"fxa-pairing-channel": "1.0.1",
"fxa-shared": "1.0.26",
@ -97,7 +97,7 @@
"jsxgettext-recursive-next": "1.1.0",
"legal-docs": "git://github.com/mozilla/legal-docs.git#master",
"load-grunt-tasks": "3.5.2",
"lodash": "4.17.11",
"lodash": "4.17.13",
"mailcheck": "1.1.1",
"mkdirp": "0.5.1",
"mocha": "4.0.1",

Просмотреть файл

@ -73,6 +73,7 @@ module.exports = function() {
'sms/sent',
'sms/sent/why',
'sms/why',
'subscriptions/products/[\\w_]+',
'verify_email',
'verify_primary_email',
'verify_secondary_email',

Просмотреть файл

@ -75,6 +75,7 @@ var routes = {
'/signup_confirmed': { statusCode: 200 },
'/signup_permissions': { statusCode: 200 },
'/signup_verified': { statusCode: 200 },
'/subscriptions/products/123doneProProduct': { statusCode: 200 },
'/sms': { statusCode: 200 },
'/sms/sent': { statusCode: 200 },
'/sms/sent/why': { statusCode: 200 },

6
packages/fxa-customs-server/npm-shrinkwrap.json сгенерированный
Просмотреть файл

@ -3171,9 +3171,9 @@
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"lodash.merge": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz",
"integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU="
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
"lodash.snakecase": {
"version": "4.1.1",

Просмотреть файл

@ -30,7 +30,7 @@
"ip": "1.1.3",
"ip-reputation-js-client": "4.1.0",
"lodash.isequal": "4.5.0",
"lodash.merge": "4.6.0",
"lodash.merge": "4.6.2",
"memcached": "2.2.1",
"newrelic": "4.1.0",
"raven": "2.3.0",

12
packages/fxa-email-event-proxy/package-lock.json сгенерированный
Просмотреть файл

@ -751,9 +751,9 @@
"dev": true
},
"js-yaml": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
"integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
@ -789,9 +789,9 @@
}
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"version": "4.17.14",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
"integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==",
"dev": true
},
"lodash.get": {

Просмотреть файл

@ -16,9 +16,11 @@ USER app
COPY package.json package.json
COPY package-lock.json package-lock.json
RUN npm ci --production && rm -rf ~app/.npm /tmp/*
RUN npm ci && rm -rf ~app/.npm /tmp/*
COPY . /app
RUN npm run build
USER root
RUN chown app:app /app/config

Просмотреть файл

@ -51,7 +51,28 @@ async function main() {
logger,
webhookService
);
await server.start();
try {
await server.start();
} catch (err) {
logger.error('startup', { err });
process.exit(1);
}
process.on('uncaughtException', err => {
logger.error('uncaughtException', { err });
process.exit(8);
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('unhandledRejection', { error: reason });
process.exit();
});
process.on('SIGINT', shutdown);
function shutdown() {
server.stop({ timeout: 10_000 }).then(() => {
process.exit(0);
});
}
}
main();

Просмотреть файл

@ -17,6 +17,10 @@ export default class ProxyController {
private readonly jwt: JWT
) {}
public async heartbeat(request: hapi.Request, h: hapi.ResponseToolkit) {
return h.response({}).code(200);
}
public async proxyDelivery(request: hapi.Request, h: hapi.ResponseToolkit) {
const webhookData = this.webhookService.serviceData();
const clientId = request.params.clientId;
@ -66,6 +70,8 @@ export default class ProxyController {
let resp = h.response(response.body).code(response.statusCode);
this.logger.debug('proxyDeliverSuccess', { statusCode: response.statusCode });
// Copy the headers over to our hapi Response
Object.entries(response.headers).map(([name, value]) => {
if (!value) {

Просмотреть файл

@ -12,6 +12,7 @@ export const proxyPayloadValidator = joi
data: joi.string().required(),
messageId: joi.string().required()
})
.unknown(true)
.required(),
subscription: joi.string().required()
})

Просмотреть файл

@ -20,15 +20,22 @@ export default function(
const proxyController = new ProxyController(logger, webhookService, jwt);
server.bind(proxyController);
server.route({
method: 'POST',
options: {
auth: 'pubsub',
handler: proxyController.proxyDelivery,
validate: {
payload: proxyPayloadValidator as hapiJoi.ObjectSchema
}
server.route([
{
handler: proxyController.heartbeat,
method: 'GET',
path: '/__lbheartbeat__'
},
path: '/v1/proxy/{clientId}'
});
{
method: 'POST',
options: {
auth: 'pubsub',
handler: proxyController.proxyDelivery,
validate: {
payload: proxyPayloadValidator as hapiJoi.ObjectSchema
}
},
path: '/v1/proxy/{clientId}'
}
]);
}

Просмотреть файл

@ -26,6 +26,8 @@ const BASE_MESSAGE_SCHEMA = joi
.unknown(true)
.required();
type baseMessageSchema = joi.Literal<typeof BASE_MESSAGE_SCHEMA>;
const LOGIN_SCHEMA = joi
.object()
.keys({
@ -46,6 +48,8 @@ const LOGIN_SCHEMA = joi
.unknown(true)
.required();
type loginSchema = joi.Literal<typeof LOGIN_SCHEMA>;
const SUBSCRIPTION_UPDATE_SCHEMA = joi
.object()
.keys({
@ -65,6 +69,8 @@ const SUBSCRIPTION_UPDATE_SCHEMA = joi
.unknown(true)
.required();
type subscriptionUpdateSchema = joi.Literal<typeof SUBSCRIPTION_UPDATE_SCHEMA>;
class ServiceNotificationProcessor {
public readonly app: Consumer;
private readonly db: Datastore;
@ -126,16 +132,34 @@ class ServiceNotificationProcessor {
private async handleMessage(sqsMessage: SQS.Message) {
const body = JSON.parse(sqsMessage.Body || '{}');
const message = joi.attempt(body, BASE_MESSAGE_SCHEMA);
let message: baseMessageSchema;
try {
message = joi.attempt(body, BASE_MESSAGE_SCHEMA);
} catch (err) {
this.logger.error('badBaseMessage', { err });
return;
}
switch (message.event) {
case LOGIN_EVENT: {
const loginMessage = joi.attempt(message, LOGIN_SCHEMA);
let loginMessage: loginSchema;
try {
loginMessage = joi.attempt(message, LOGIN_SCHEMA);
} catch (err) {
this.logger.error('badLoginMessage', { err });
return;
}
await this.db.storeLogin(loginMessage.uid, loginMessage.clientId);
this.logger.debug('sqs.loginEvent', loginMessage);
return;
}
case SUBSCRIPTION_UPDATE_EVENT: {
const subMessage = joi.attempt(message, SUBSCRIPTION_UPDATE_SCHEMA);
let subMessage: subscriptionUpdateSchema;
try {
subMessage = joi.attempt(message, SUBSCRIPTION_UPDATE_SCHEMA);
} catch (err) {
this.logger.error('badSubscriptionUpdateMessage', { err });
return;
}
const clientIds = await this.db.fetchClientIds(subMessage.uid);
this.logger.debug('sqs.subEvent', subMessage);

Просмотреть файл

@ -96,6 +96,14 @@ describe('Proxy Controller', () => {
await server.stop();
});
it('has a heartbeat', async () => {
const result = await server.inject({
method: 'GET',
url: '/__lbheartbeat__'
});
cassert.equal(result.statusCode, 200);
});
it('notifies successfully on subscription state change', async () => {
mockWebhook();
const message = createValidMessage();

Просмотреть файл

@ -124,22 +124,22 @@ describe('ServiceNotificationProcessor', () => {
assert.calledWith(db.fetchClientIds as SinonSpy);
});
it('throws an error on invalid login message', async () => {
it('logs an error on invalid login message', async () => {
updateStubMessage(Object.assign({}, { ...baseLoginMessage, email: false }));
consumer.start();
await pEvent(consumer.app, 'processing_error');
await pEvent(consumer.app, 'message_processed');
consumer.stop();
assert.calledOnce(logger.error as SinonSpy);
cassert.equal((logger.error as SinonSpy).getCalls()[0].args[0], 'processingError');
cassert.equal((logger.error as SinonSpy).getCalls()[0].args[0], 'badLoginMessage');
});
it('throws an error on invalid subscription message', async () => {
it('logs an error on invalid subscription message', async () => {
updateStubMessage(Object.assign({}, { ...baseSubscriptionUpdateMessage, productName: false }));
consumer.start();
await pEvent(consumer.app, 'processing_error');
await pEvent(consumer.app, 'message_processed');
consumer.stop();
assert.calledOnce(logger.error as SinonSpy);
assert.calledWithMatch(logger.error as SinonSpy, 'processingError');
assert.calledWithMatch(logger.error as SinonSpy, 'badSubscriptionUpdateMessage');
});
it('logs on message its not interested in', async () => {

Просмотреть файл

@ -2528,6 +2528,69 @@ define([
});
};
/**
* Get a user's list of active subscriptions.
*
* @param {String} token A token from the OAuth server.
* @returns {Promise} A promise that will be fulfilled with a list of active
* subscriptions.
*/
FxAccountClient.prototype.getActiveSubscriptions = function(token) {
var self = this;
return Promise.resolve().then(function() {
required(token, 'token');
const requestOptions = {
headers: {
Authorization: `Bearer ${token}`,
},
};
return self.request.send(
'/oauth/subscriptions/active',
'GET',
null,
null,
requestOptions
);
});
};
/**
* Submit a support ticket.
*
* @param {String} authorizationHeader A token from the OAuth server.
* @param {Object} [supportTicket={}]
* @param {String} [supportTicket.topic]
* @param {String} [supportTicket.subject] Optional subject
* @param {String} [supportTicket.message]
* @returns {Promise} A promise that will be fulfilled with:
* - `success`
* - `ticket` OR `error`
*/
FxAccountClient.prototype.createSupportTicket = function(
token,
supportTicket
) {
var self = this;
return Promise.resolve().then(function() {
required(token, 'token');
required(supportTicket, 'supportTicket');
const requestOptions = {
headers: {
Authorization: `Bearer ${token}`,
},
};
return self.request.send(
'/support/ticket',
'POST',
null,
supportTicket,
requestOptions
);
});
};
/**
* Check for a required argument. Exposed for unit testing.
*

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "fxa-js-client",
"version": "1.0.14",
"version": "1.0.15",
"description": "Web client that talks to the Firefox Accounts API server",
"author": "Mozilla",
"license": "MPL-2.0",

Просмотреть файл

@ -25,6 +25,7 @@ define([
'tests/lib/signIn',
'tests/lib/signinCodes',
'tests/lib/signUp',
'tests/lib/subscriptions',
'tests/lib/totp',
'tests/lib/tokenCodes',
'tests/lib/sms',

Просмотреть файл

@ -0,0 +1,102 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
define([
'intern!tdd',
'intern/chai!assert',
'tests/addons/environment',
], function(tdd, assert, Environment) {
var env = new Environment();
if (env.useRemoteServer) {
return;
}
with (tdd) {
suite('subscriptions', function() {
var accountHelper;
var respond;
var client;
var RequestMocks;
beforeEach(function() {
env = new Environment();
accountHelper = env.accountHelper;
respond = env.respond;
client = env.client;
RequestMocks = env.RequestMocks;
});
test('#getActiveSubscriptions - missing token', function() {
return accountHelper
.newVerifiedAccount()
.then(function(account) {
return respond(
client.getActiveSubscriptions(),
RequestMocks.getActiveSubscriptions
);
})
.then(assert.notOk, function(error) {
assert.include(error.message, 'Missing token');
});
});
test('#getActiveSubscriptions', function() {
return accountHelper
.newVerifiedAccount()
.then(function(account) {
return respond(
client.getActiveSubscriptions('saynomore'),
RequestMocks.getActiveSubscriptions
);
})
.then(function(resp) {
assert.ok(resp);
}, assert.notOk);
});
test('#createSupportTicket - missing token', function() {
return accountHelper
.newVerifiedAccount()
.then(function(account) {
return respond(
client.createSupportTicket(),
RequestMocks.createSupportTicket
);
})
.then(assert.notOk, function(error) {
assert.include(error.message, 'Missing token');
});
});
test('#createSupportTicket - missing supportTicket', function() {
return accountHelper
.newVerifiedAccount()
.then(function(account) {
return respond(
client.createSupportTicket('redpandas'),
RequestMocks.createSupportTicket
);
})
.then(assert.notOk, function(error) {
assert.include(error.message, 'Missing supportTicket');
});
});
test('#createSupportTicket', function() {
return accountHelper
.newVerifiedAccount()
.then(function(account) {
return respond(
client.createSupportTicket('redpandas', {
topic: 'Species',
subject: 'Cute & Rare',
message: 'Need moar',
}),
RequestMocks.createSupportTicket
);
})
.then(function(resp) {
assert.ok(resp);
}, assert.notOk);
});
});
}
});

Просмотреть файл

@ -509,5 +509,13 @@ define(['client/lib/errors', 'tests/lib/push-constants'], function(
status: 200,
body: '{"exists": true}',
},
getActiveSubscriptions: {
status: 200,
body: '[{"subscriptionId": 9},{"subscriptionId": 12}]',
},
createSupportTicket: {
status: 200,
body: '{"success": true, "ticket": "abc123xyz"}',
},
};
});

Просмотреть файл

@ -8,6 +8,10 @@ This project uses [Storybook](https://storybook.js.org/) to show each screen wit
You can view the Storybook built from the most recent master at http://mozilla.github.io/fxa/fxa-payments-server/
## Installation notes
On Mac OS, `npm run test` may trigger an `EMFILE` error. In this case, to get tests running, you may need to `brew install watchman`. (If the watchman postinstall step fails, follow the instructions [here](https://stackoverflow.com/a/41320226) to change `/usr/local` ownership from root to your user account.)
## License
MPL-2.0

Двоичные данные
packages/fxa-payments-server/public/favicon.ico

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 66 KiB

После

Ширина:  |  Высота:  |  Размер: 15 KiB

Просмотреть файл

@ -1,6 +1,6 @@
#dialogs {
.modal {
padding: 64px 27px;
padding: 25px 27px;
text-align: center;
}

Просмотреть файл

@ -16,7 +16,8 @@ function init() {
.add('in progress', () => <Subject inProgress={true} />)
.add('all invalid', () => {
const state = mockValidatorState();
// HACK: pre-seed with some error messages for display purposes
state.fields.name.valid = false;
state.fields.name.error = 'Please enter your name';
state.fields.zip.valid = false;
@ -24,10 +25,10 @@ function init() {
state.fields.creditCardNumber.valid = false;
state.fields.creditCardNumber.error = 'Your card number is incomplete';
state.fields.expDate.valid = false;
state.fields.expDate.error = 'Your card\'s expiration date is incomplete.';
state.fields.expDate.error = 'Your card\'s expiration date is incomplete';
state.fields.cvc.valid = false;
state.fields.cvc.error = 'Your card\'s security code is incomplete.';
state.fields.cvc.error = 'Your card\'s security code is incomplete';
return<Subject validatorInitialState={state} />;
});
}

Просмотреть файл

@ -22,8 +22,9 @@ const STRIPE_ELEMENT_STYLES = {
base: {
//TODO: Figure out what this really should be - I just copied it from computed styles because CSS can't apply through the iframe
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
fontSize: '16px',
lineHeight: '48px',
fontSize: '13px',
fontWeight: '500',
lineHeight: '45px',
}
};
@ -32,7 +33,7 @@ export type PaymentFormProps = {
confirm?: boolean,
plan?: Plan,
onCancel?: () => void,
onPayment: (tokenResponse: stripe.TokenResponse) => void,
onPayment: (tokenResponse: stripe.TokenResponse, name: string) => void,
onPaymentError: (error: any) => void,
validatorInitialState?: ValidatorState,
validatorMiddlewareReducer?: ValidatorMiddlewareReducer,
@ -63,7 +64,7 @@ export const PaymentForm = ({
if (stripe) {
stripe
.createToken({ name, address_zip: zip })
.then(onPayment)
.then((tokenResponse: stripe.TokenResponse) => onPayment(tokenResponse, name))
.catch(onPaymentError);
}
}, [ validator, onPayment, onPaymentError, stripe ]);
@ -85,28 +86,27 @@ export const PaymentForm = ({
<FieldGroup>
<StripeElement component={CardNumberElement}
name="creditCardNumber" label="Credit Card Number"
name="creditCardNumber" label="Card number"
style={STRIPE_ELEMENT_STYLES}
className="input-row input-row--xl" required />
<StripeElement component={CardExpiryElement}
name="expDate" label="Exp. Date"
name="expDate" label="Exp. date"
style={STRIPE_ELEMENT_STYLES} required />
<StripeElement component={CardCVCElement}
name="cvc" label="CVC"
style={STRIPE_ELEMENT_STYLES} required />
<Input type="number" name="zip" label="Zip Code" maxLength={5} required
<Input type="number" name="zip" label="Zip code" maxLength={5} required
data-testid="zip"
onValidate={value => {
let error = null;
if (value !== null) {
value = ('' + value).substr(0, 5);
if (! value) {
if (!value) {
error = 'Zip code is required';
}
if (value.length !== 5) {
} else if (value.length !== 5) {
error = 'Zip code is too short';
}
}

Просмотреть файл

@ -157,7 +157,13 @@ export const StripeElement = (props: StripeElementProps) => {
(value: stripe.elements.ElementChangeResponse) => {
if (value !== null) {
if (value.error && value.error.message) {
validator.updateField({ name, value, valid: false, error: value.error.message });
let error = value.error.message;
// Issue #1718 - remove periods from error messages from Stripe
// for consistency with our own errors
if (error.endsWith('.')) {
error = error.slice(0, -1);
}
validator.updateField({ name, value, valid: false, error });
} else if (value.complete) {
validator.updateField({ name, value, valid: true });
}

Просмотреть файл

@ -0,0 +1,11 @@
import 'jest-dom/extend-expect';
import { getErrorMessage, BASIC_ERROR, PAYMENT_ERROR_1 } from './errors';
it('returns the basic error text if not predefined error type', () => {
expect(getErrorMessage("NON_PREDEFINED_ERROR")).toEqual(BASIC_ERROR);
});
it('returns the payment error text for the correct error type', () => {
expect(getErrorMessage("approve_with_id")).toEqual(PAYMENT_ERROR_1);
});

Просмотреть файл

@ -1,9 +1,110 @@
// ref: fxa-auth-server/lib/error.js
export const AuthServerErrno = {
const AuthServerErrno = {
UNKNOWN_SUBSCRIPTION_CUSTOMER: 176,
UNKNOWN_SUBSCRIPTION: 177,
UNKNOWN_SUBSCRIPTION_PLAN: 178,
REJECTED_SUBSCRIPTION_PAYMENT_TOKEN: 179,
SUBSCRIPTION_ALREADY_CANCELLED: 180,
REJECTED_CUSTOMER_UPDATE: 181,
};
};
/*
* Todos:
* - L10N
* - handle General SubHub subscription creation failure on submit
* (network issue, server error)
* - handle Payment token not valid
*/
const BASIC_ERROR = "Hmm, we're having trouble with our system. We're working on fixing it for you and apologize for the inconvenience. Please try again later.";
const PAYMENT_ERROR_1 = "Hmm. There was a problem authorizing your payment. Try again or get in touch with your card issuer.";
const PAYMENT_ERROR_2 = "Hmm. There was a problem authorizing your payment. Get in touch with your card issuer.";
let errorMessageIndex: { [key: string]: string } = {
"expired_card": "It looks like your credit card has expired. Try another card.",
"insufficient_funds": "It looks like your card has insufficient funds. Try another card.",
"withdrawal_count_limit_exceeded": "It looks like this transaction will put you over your credit limit. Try another card.",
"charge_exceeds_source_limit": "It looks like this transaction will put you over your daily credit limit. Try another card or in 24 hours.",
"instant_payouts_unsupported": "It looks like your debit card isn't setup for instant payments. Try another debit or credit card.",
"duplicate_transaction": "Hmm. Looks like an identical transaction was just sent. Check your payment history.",
"coupon_expired": "It looks like that promo code has expired.",
// todo: handle "parameters_exclusive": "Your already subscribed to _product_"
};
const basicErrors = ["api_key_expired",
"platform_api_key_expired",
"rate_limit",
"UNKNOWN", // TODO: General SubHub subscription creation failure on submit (network issue, server error)
"api_connection_error",
"api_error",
"invalid_request_error",
"UNKNOWN", // TODO: Payment token not valid
"state_unsupported",
"invalid_source_usage",
"invoice_no_customer_line_items",
"invoice_no_subscription_line_items",
"invoice_not_editable",
"invoice_upcoming_none",
"missing",
"order_creation_failed",
"order_required_settings",
"order_status_invalid",
"order_upstream_timeout",
"payment_intent_incompatible_payment_method",
"payment_intent_unexpected_state",
"payment_method_unactivated",
"payment_method_unexpected_state",
"payouts_not_allowed",
"resource_already_exists",
"resource_missing",
"secret_key_required",
"sepa_unsupported_account",
"shipping_calculation_failed",
"tax_id_invalid",
"taxes_calculation_failed",
"tls_version_unsupported",
"token_already_used",
"token_in_use",
"transfers_not_allowed"];
const paymentErrors1 = ["approve_with_id",
"issuer_not_available",
"processing_error",
"reenter_transaction",
"try_again_later",
"payment_intent_authentication_failure",
"processing_error",];
const paymentErrors2 = ["call_issuer",
"card_not_supported",
"card_velocity_exceeded",
"do_not_honor",
"do_not_try_again",
"fraudulent",
"generic_decline",
"invalid_account",
"lost_card",
"merchant_blacklist",
"new_account_information_available",
"no_action_taken",
"not_permitted",
"pickup_card",
"restricted_card",
"revocation_of_all_authorizations",
"revocation_of_authorization",
"security_violation",
"service_not_allowed",
"stolen_card",
"stop_payment_order",
"transaction_not_allowed",];
basicErrors.forEach(k => errorMessageIndex[k] = BASIC_ERROR);
paymentErrors1.forEach(k => errorMessageIndex[k] = PAYMENT_ERROR_1);
paymentErrors2.forEach(k => errorMessageIndex[k] = PAYMENT_ERROR_2);
function getErrorMessage(type: string) {
return errorMessageIndex[type] ? errorMessageIndex[type] : BASIC_ERROR;
}
// BASIC_ERROR and PAYMENT_ERROR_1 are exported for errors.test.tsx
export { AuthServerErrno, getErrorMessage, BASIC_ERROR, PAYMENT_ERROR_1 };

Просмотреть файл

@ -1,6 +1,6 @@
import React, { useEffect, useState, useCallback, useContext } from 'react';
import { connect } from 'react-redux';
import { AuthServerErrno } from '../../lib/errors';
import { AuthServerErrno, getErrorMessage } from '../../lib/errors';
import { actions, selectors } from '../../store';
import { AppContext } from '../../lib/AppContext';
import { LoadingOverlay } from '../../components/LoadingOverlay';
@ -76,7 +76,7 @@ export const Product = ({
activated: accountActivated = false
} = queryParams;
const [ createTokenError, setCreateTokenError ] = useState({ message: null });
const [ createTokenError, setCreateTokenError ] = useState({ type: "", error: false });
// Fetch plans on initial render, change in product ID, or auth change.
useEffect(() => {
@ -97,22 +97,23 @@ export const Product = ({
selectedPlan = productPlans[0];
}
const onPayment = useCallback((tokenResponse: stripe.TokenResponse) => {
const onPayment = useCallback((tokenResponse: stripe.TokenResponse, name: string) => {
if (tokenResponse && tokenResponse.token) {
createSubscription(accessToken, {
paymentToken: tokenResponse.token.id,
planId: selectedPlan.plan_id,
displayName: profile.result ? profile.result.displayName : '',
displayName: name,
});
} else {
// This shouldn't happen with a successful createToken() call, but let's
// display an error in case it does.
const error: any = { message: 'No token response received from Stripe' };
const error: any = { type: 'api_error', error: true };
setCreateTokenError(error);
}
}, [ accessToken, selectedPlan, createSubscription, setCreateTokenError ]);
const onPaymentError = useCallback((error: any) => {
error.error = true;
setCreateTokenError(error);
}, [ setCreateTokenError ]);
@ -188,16 +189,16 @@ export const Product = ({
error={createSubscriptionStatus.error} />
)}
{createTokenError.message && (
{createTokenError.error && (
<DialogMessage
className="dialog-error"
onDismiss={() => {
resetCreateSubscriptionError();
setCreateTokenError({ message: null });
setCreateTokenError({ type: "", error: false });
}}
>
<h4>Payment submission failed</h4>
<p>{createTokenError.message}</p>
<p>{getErrorMessage(createTokenError.type)}</p>
</DialogMessage>
)}

Просмотреть файл

@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react';
import dayjs from 'dayjs';
import { useBooleanState } from '../../lib/hooks';
import { getErrorMessage } from '../../lib/errors';
import {
Customer,
UpdatePaymentFetchState,
@ -31,7 +32,7 @@ export const PaymentUpdateForm = ({
plan,
}: PaymentUpdateFormProps) => {
const [ updateRevealed, revealUpdate, hideUpdate ] = useBooleanState();
const [ createTokenError, setCreateTokenError ] = useState({ message: null });
const [ createTokenError, setCreateTokenError ] = useState({ type: "", error: false });
const onRevealUpdateClick = useCallback(() => {
resetUpdatePayment();
revealUpdate();
@ -45,17 +46,18 @@ export const PaymentUpdateForm = ({
} else {
// This shouldn't happen with a successful createToken() call, but let's
// display an error in case it does.
const error: any = { message: 'No token response received from Stripe' };
const error: any = { type: 'api_error', error: true };
setCreateTokenError(error);
}
}, [ accessToken, updatePayment, setCreateTokenError ]);
const onPaymentError = useCallback((error: any) => {
error.error = true;
setCreateTokenError(error);
}, [ setCreateTokenError ]);
const onTokenErrorDismiss = useCallback(() => {
setCreateTokenError({ message: null });
setCreateTokenError({ type: "", error: false });
}, [ setCreateTokenError ]);
const inProgress =
@ -76,10 +78,10 @@ export const PaymentUpdateForm = ({
return (
<div className="payment-update">
{createTokenError.message && (
{createTokenError.error && (
<DialogMessage className="dialog-error" onDismiss={onTokenErrorDismiss}>
<h4>Payment submission failed</h4>
<p>{createTokenError.message}</p>
<p>{getErrorMessage(createTokenError.type)}</p>
</DialogMessage>
)}

10
packages/fxa-shared/package-lock.json сгенерированный
Просмотреть файл

@ -65,7 +65,7 @@
"dependencies": {
"acorn": {
"version": "3.3.0",
"resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
"integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
"dev": true
}
@ -161,7 +161,7 @@
"dependencies": {
"chalk": {
"version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
@ -916,9 +916,9 @@
}
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"version": "4.17.14",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
"integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==",
"dev": true
},
"lolex": {

Просмотреть файл

@ -1,4 +1,4 @@
#!/bin/sh
#!/usr/bin/env bash
set -e
IFS=$'\n'

Просмотреть файл

@ -45,20 +45,6 @@
"max_restarts": "1",
"min_uptime": "2m"
},
{
"name": "fxa-basket-proxy PORT 1114",
"script": "./bin/basket-proxy-server.js",
"cwd": "packages/fxa-basket-proxy",
"max_restarts": "1",
"min_uptime": "2m"
},
{
"name": "fxa-basket-proxy fake basket server PORT 10140",
"script": "./bin/fake-basket-server.js",
"cwd": "packages/fxa-basket-proxy",
"max_restarts": "1",
"min_uptime": "2m"
},
{
"name": "oauth-server PORT 9010",
"script": "./bin/server.js",