Merge pull request #11085 from mozilla/feat/issue-10826

feat(auth): include validation details for product/plan
This commit is contained in:
Ben Bangert 2021-11-19 12:06:23 -08:00 коммит произвёл GitHub
Родитель a62532cc50 e81f5e077c
Коммит a6d791ed8f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 86 добавлений и 54 удалений

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

@ -44,6 +44,7 @@ import { ConfigType } from '../../config';
import error from '../error';
import Redis from '../redis';
import { subscriptionProductMetadataValidator } from '../routes/validators';
import { reportValidationError } from '../sentry';
import { AuthFirestore } from '../types';
import { CurrencyHelper } from './currencies';
import { SubscriptionPurchase } from './google-play/subscription-purchase';
@ -1181,22 +1182,16 @@ export class StripeHelper {
continue;
}
const result = subscriptionProductMetadataValidator.validate({
...item.product.metadata,
...item.metadata,
});
if (result?.error) {
const msg = `fetchAllPlans - Plan "${item.id}"'s metadata failed validation`;
this.log.error(msg, { error: result.error, plan: item });
Sentry.withScope((scope) => {
scope.setContext('validationError', {
error: result.error,
});
Sentry.captureMessage(msg, Sentry.Severity.Error);
const { error } =
await subscriptionProductMetadataValidator.validateAsync({
...item.product.metadata,
...item.metadata,
});
if (error) {
const msg = `fetchAllPlans - Plan "${item.id}"'s metadata failed validation`;
this.log.error(msg, { error, plan: item });
reportValidationError(msg, error as any);
continue;
}

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

@ -10,16 +10,10 @@ import { Stripe } from 'stripe';
import Container from 'typedi';
import { ConfigType } from '../../../config';
import { reportSentryError } from '../../../lib/sentry';
import { reportSentryError, reportValidationError } from '../../../lib/sentry';
import error from '../../error';
import { CapabilityService } from '../../payments/capability';
import { PayPalHelper } from '../../payments/paypal';
import { PayPalClientError } from '../../payments/paypal-client';
import {
PAYPAL_BILLING_AGREEMENT_INVALID,
PAYPAL_BILLING_TRANSACTION_WRONG_ACCOUNT,
PAYPAL_SOURCE_ERRORS,
} from '../../payments/paypal-error-codes';
import {
INVOICES_RESOURCE,
StripeHelper,
@ -407,22 +401,15 @@ export class StripeWebhookHandler extends StripeHandler {
return;
}
const result = subscriptionProductMetadataValidator.validate({
const { error } = await subscriptionProductMetadataValidator.validateAsync({
...product.metadata,
...plan.metadata,
});
if (result?.error) {
if (error) {
const msg = `handlePlanUpdatedEvent - Plan "${plan.id}"'s metadata failed validation`;
this.log.error(msg, { error: result.error, plan });
Sentry.withScope((scope) => {
scope.setContext('validationError', {
error: result.error,
});
Sentry.captureMessage(msg, Sentry.Severity.Error);
});
this.log.error(msg, { error, plan });
reportValidationError(msg, error as any);
this.stripeHelper.updateAllPlans(updatedList);
return;
}
@ -451,28 +438,23 @@ export class StripeWebhookHandler extends StripeHandler {
await this.stripeHelper.updateAllProducts(updatedList);
const plans = await this.stripeHelper.fetchPlansByProductId(product.id);
const allPlans = await this.stripeHelper.allPlans();
const allPlans = await this.stripeHelper.fetchAllPlans();
const updatedPlans = allPlans.filter(
(plan) => (plan.product as Stripe.Product).id !== product.id
);
if (event.type !== 'product.deleted') {
for (const plan of plans) {
const result = subscriptionProductMetadataValidator.validate({
...product.metadata,
...plan.metadata,
});
if (result?.error) {
const msg = `handleProductWebhookEvent - Plan "${plan.id}"'s metadata failed validation on product ${product.id} update.`;
this.log.error(msg, { error: result.error, product });
Sentry.withScope((scope) => {
scope.setContext('validationError', {
error: result.error,
});
Sentry.captureMessage(msg, Sentry.Severity.Error);
const { error } =
await subscriptionProductMetadataValidator.validateAsync({
...product.metadata,
...plan.metadata,
});
if (error) {
const msg = `handleProductWebhookEvent - Plan "${plan.id}"'s metadata failed validation on product ${product.id} update.`;
this.log.error(msg, { error, product });
reportValidationError(msg, error as any);
} else {
updatedPlans.push({ ...plan, product });
}

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

@ -448,11 +448,37 @@ module.exports.subscriptionProductMetadataValidator = {
error: 'Capability missing from metadata',
};
}
return module.exports.subscriptionProductMetadataBaseValidator.validate(
metadata
metadata,
{
abortEarly: false,
}
);
},
async validateAsync(metadata) {
const hasCapability = Object.keys(metadata).some((k) =>
capabilitiesClientIdPattern.test(k)
);
if (!hasCapability) {
return {
error: 'Capability missing from metadata',
};
}
try {
const value = await isA.validate(
metadata,
module.exports.subscriptionProductMetadataBaseValidator,
{
abortEarly: false,
}
);
return { value };
} catch (error) {
return { error };
}
},
};
module.exports.subscriptionsPlanValidator = isA.object({

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

@ -190,4 +190,29 @@ async function configureSentry(server, config, processName = 'key_server') {
}
}
module.exports = { configureSentry, reportSentryError };
/**
* Report a validation error to Sentry with validation details.
*
* @param {*} message
* @param {string | import('@hapi/joi').ValidationError} error
*/
function reportValidationError(message, error) {
const details = {};
if (typeof error === 'string') {
details.error = error;
} else {
for (const errorItem of error.details) {
const key = errorItem.path.join('.');
details[key] = {
message: errorItem.message,
type: errorItem.type,
};
}
}
Sentry.withScope((scope) => {
scope.setContext('validationError', details);
Sentry.captureMessage(message, Sentry.Severity.Error);
});
}
module.exports = { configureSentry, reportSentryError, reportValidationError };

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

@ -439,7 +439,9 @@ describe('StripeWebhookHandler', () => {
product: updatedEvent.data.object,
};
const allPlans = [...validPlanList, invalidPlan];
StripeWebhookHandlerInstance.stripeHelper.allPlans.resolves(allPlans);
StripeWebhookHandlerInstance.stripeHelper.fetchAllPlans.resolves(
allPlans
);
StripeWebhookHandlerInstance.stripeHelper.fetchPlansByProductId.resolves(
[invalidPlan]
);
@ -451,7 +453,9 @@ describe('StripeWebhookHandler', () => {
assert.calledOnce(scopeContextSpy);
assert.calledOnce(captureMessageSpy);
assert.calledOnce(StripeWebhookHandlerInstance.stripeHelper.allPlans);
assert.calledOnce(
StripeWebhookHandlerInstance.stripeHelper.fetchAllPlans
);
assert.calledOnceWithExactly(
StripeWebhookHandlerInstance.stripeHelper.fetchPlansByProductId,
updatedEvent.data.object.id
@ -468,7 +472,7 @@ describe('StripeWebhookHandler', () => {
it('does not throw a sentry error if the update event data is valid', async () => {
const updatedEvent = deepCopy(eventProductUpdated);
StripeWebhookHandlerInstance.stripeHelper.allPlans.resolves(
StripeWebhookHandlerInstance.stripeHelper.fetchAllPlans.resolves(
validPlanList
);
StripeWebhookHandlerInstance.stripeHelper.fetchPlansByProductId.resolves(
@ -490,7 +494,7 @@ describe('StripeWebhookHandler', () => {
...deepCopy(eventProductUpdated),
type: 'product.deleted',
};
StripeWebhookHandlerInstance.stripeHelper.allPlans.resolves(
StripeWebhookHandlerInstance.stripeHelper.fetchAllPlans.resolves(
validPlanList
);
StripeWebhookHandlerInstance.stripeHelper.fetchPlansByProductId.resolves(