зеркало из https://github.com/mozilla/fxa.git
Merge pull request #11085 from mozilla/feat/issue-10826
feat(auth): include validation details for product/plan
This commit is contained in:
Коммит
a6d791ed8f
|
@ -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(
|
||||
|
|
Загрузка…
Ссылка в новой задаче