From 4cfd5788d24d55017a8ace53fed99082f87691cb Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Tue, 7 Jun 2022 14:14:06 +0800 Subject: [PATCH] feat(sms/email-connectors): expose third-party API request error message (#1059) --- packages/connector-aliyun-dm/src/index.ts | 58 +++++++++++-------- packages/connector-aliyun-dm/src/types.ts | 14 ++++- .../connector-aliyun-sms/src/index.test.ts | 24 +++++--- packages/connector-aliyun-sms/src/index.ts | 17 +++--- .../src/single-send-text.ts | 15 +---- packages/connector-aliyun-sms/src/types.ts | 9 ++- packages/connector-aliyun-sms/src/utils.ts | 4 +- packages/connector-sendgrid-mail/src/index.ts | 33 +++++++---- packages/connector-sendgrid-mail/src/types.ts | 16 +++-- packages/connector-twilio-sms/src/index.ts | 28 ++++++--- packages/connector-twilio-sms/src/types.ts | 9 +++ 11 files changed, 148 insertions(+), 79 deletions(-) diff --git a/packages/connector-aliyun-dm/src/index.ts b/packages/connector-aliyun-dm/src/index.ts index c46c1ddab..be3bf6668 100644 --- a/packages/connector-aliyun-dm/src/index.ts +++ b/packages/connector-aliyun-dm/src/index.ts @@ -8,11 +8,11 @@ import { GetConnectorConfig, } from '@logto/connector-types'; import { assert } from '@silverhand/essentials'; -import { Response } from 'got'; +import { HTTPError } from 'got'; import { defaultMetadata } from './constant'; import { singleSendMail } from './single-send-mail'; -import { SendEmailResponse, AliyunDmConfig, aliyunDmConfigGuard } from './types'; +import { AliyunDmConfig, aliyunDmConfigGuard } from './types'; export default class AliyunDmConnector implements EmailConnector { public metadata: ConnectorMetadata = defaultMetadata; @@ -27,11 +27,7 @@ export default class AliyunDmConnector implements EmailConnector { } }; - public sendMessage: EmailSendMessageFunction> = async ( - address, - type, - data - ) => { + public sendMessage: EmailSendMessageFunction = async (address, type, data) => { const config = await this.getConfig(this.metadata.id); await this.validateConfig(config); const { accessKeyId, accessKeySecret, accountName, fromAlias, templates } = config; @@ -45,21 +41,37 @@ export default class AliyunDmConnector implements EmailConnector { ) ); - return singleSendMail( - { - AccessKeyId: accessKeyId, - AccountName: accountName, - ReplyToAddress: 'false', - AddressType: '1', - ToAddress: address, - FromAlias: fromAlias, - Subject: template.subject, - HtmlBody: - typeof data.code === 'string' - ? template.content.replace(/{{code}}/g, data.code) - : template.content, - }, - accessKeySecret - ); + try { + return await singleSendMail( + { + AccessKeyId: accessKeyId, + AccountName: accountName, + ReplyToAddress: 'false', + AddressType: '1', + ToAddress: address, + FromAlias: fromAlias, + Subject: template.subject, + HtmlBody: + typeof data.code === 'string' + ? template.content.replace(/{{code}}/g, data.code) + : template.content, + }, + accessKeySecret + ); + } catch (error: unknown) { + if (error instanceof HTTPError) { + const { + response: { body: rawBody }, + } = error; + assert( + typeof rawBody === 'string', + new ConnectorError(ConnectorErrorCodes.InvalidResponse) + ); + + throw new ConnectorError(ConnectorErrorCodes.General, rawBody); + } + + throw error; + } }; } diff --git a/packages/connector-aliyun-dm/src/types.ts b/packages/connector-aliyun-dm/src/types.ts index 17c486491..fa9008bf8 100644 --- a/packages/connector-aliyun-dm/src/types.ts +++ b/packages/connector-aliyun-dm/src/types.ts @@ -1,7 +1,5 @@ import { z } from 'zod'; -export type { Response } from 'got'; - export type SendEmailResponse = { EnvId: string; RequestId: string }; /** @@ -51,3 +49,15 @@ export type PublicParameters = { Timestamp?: string; Version?: string; }; + +/** + * @doc https://next.api.aliyun.com/troubleshoot + */ +export const singleSendMailErrorResponseGuard = z.object({ + Code: z.string(), + Message: z.string(), + RequestId: z.string(), + HostId: z.string(), +}); + +export type SingleSendMailErrorResponse = z.infer; diff --git a/packages/connector-aliyun-sms/src/index.test.ts b/packages/connector-aliyun-sms/src/index.test.ts index bd4ba2c50..126512c96 100644 --- a/packages/connector-aliyun-sms/src/index.test.ts +++ b/packages/connector-aliyun-sms/src/index.test.ts @@ -9,7 +9,16 @@ const getConnectorConfig = jest.fn() as GetConnectorConfig; const aliyunSmsMethods = new AliyunSmsConnector(getConnectorConfig); -jest.mock('./single-send-text'); +jest.mock('./single-send-text', () => { + return { + sendSms: jest.fn(() => { + return { + body: JSON.stringify({ Code: 'OK', RequestId: 'request-id', Message: 'OK' }), + statusCode: 200, + }; + }), + }; +}); describe('validateConfig()', () => { afterEach(() => { @@ -28,28 +37,29 @@ describe('validateConfig()', () => { }); describe('sendMessage()', () => { + beforeEach(() => { + jest.spyOn(aliyunSmsMethods, 'getConfig').mockResolvedValueOnce(mockedConnectorConfig); + }); + afterEach(() => { jest.clearAllMocks(); }); it('should call singleSendMail() and replace code in content', async () => { - jest.spyOn(aliyunSmsMethods, 'getConfig').mockResolvedValueOnce(mockedConnectorConfig); await aliyunSmsMethods.sendMessage(phoneTest, 'SignIn', { code: codeTest }); - const { templates, ...credentials } = mockedConnectorConfig; expect(sendSms).toHaveBeenCalledWith( expect.objectContaining({ - AccessKeyId: credentials.accessKeyId, + AccessKeyId: mockedConnectorConfig.accessKeyId, PhoneNumbers: phoneTest, - SignName: credentials.signName, + SignName: mockedConnectorConfig.signName, TemplateCode: 'code', TemplateParam: `{"code":"${codeTest}"}`, }), - 'accessKeySecret' + mockedConnectorConfig.accessKeySecret ); }); it('throws if template is missing', async () => { - jest.spyOn(aliyunSmsMethods, 'getConfig').mockResolvedValueOnce(mockedConnectorConfig); await expect( aliyunSmsMethods.sendMessage(phoneTest, 'Register', { code: codeTest }) ).rejects.toThrow(); diff --git a/packages/connector-aliyun-sms/src/index.ts b/packages/connector-aliyun-sms/src/index.ts index 6f4a10fbc..c48ff5a38 100644 --- a/packages/connector-aliyun-sms/src/index.ts +++ b/packages/connector-aliyun-sms/src/index.ts @@ -8,11 +8,10 @@ import { GetConnectorConfig, } from '@logto/connector-types'; import { assert } from '@silverhand/essentials'; -import { Response } from 'got'; import { defaultMetadata } from './constant'; import { sendSms } from './single-send-text'; -import { aliyunSmsConfigGuard, AliyunSmsConfig, SendSmsResponse } from './types'; +import { aliyunSmsConfigGuard, AliyunSmsConfig, sendSmsResponseGuard } from './types'; export default class AliyunSmsConnector implements SmsConnector { public metadata: ConnectorMetadata = defaultMetadata; @@ -27,11 +26,7 @@ export default class AliyunSmsConnector implements SmsConnector { } }; - public sendMessage: SmsSendMessageFunction> = async ( - phone, - type, - { code } - ) => { + public sendMessage: SmsSendMessageFunction = async (phone, type, { code }) => { const config = await this.getConfig(this.metadata.id); await this.validateConfig(config); const { accessKeyId, accessKeySecret, signName, templates } = config; @@ -42,7 +37,7 @@ export default class AliyunSmsConnector implements SmsConnector { new ConnectorError(ConnectorErrorCodes.TemplateNotFound, `Cannot find template!`) ); - return sendSms( + const httpResponse = await sendSms( { AccessKeyId: accessKeyId, PhoneNumbers: phone, @@ -52,5 +47,11 @@ export default class AliyunSmsConnector implements SmsConnector { }, accessKeySecret ); + const { body: rawBody } = httpResponse; + const { Code } = sendSmsResponseGuard.parse(JSON.parse(rawBody)); + + assert(Code === 'OK', new ConnectorError(ConnectorErrorCodes.General, rawBody)); + + return httpResponse; }; } diff --git a/packages/connector-aliyun-sms/src/single-send-text.ts b/packages/connector-aliyun-sms/src/single-send-text.ts index f0c3c3242..8caa3b2de 100644 --- a/packages/connector-aliyun-sms/src/single-send-text.ts +++ b/packages/connector-aliyun-sms/src/single-send-text.ts @@ -1,19 +1,10 @@ -import { Response } from 'got'; - import { endpoint, staticConfigs } from './constant'; -import { PublicParameters, SendSms, SendSmsResponse } from './types'; +import { PublicParameters, SendSms } from './types'; import { request } from './utils'; /** * @doc https://help.aliyun.com/document_detail/101414.html */ -export const sendSms = async ( - parameters: PublicParameters & SendSms, - accessKeySecret: string -): Promise> => { - return request( - endpoint, - { Action: 'SendSms', ...staticConfigs, ...parameters }, - accessKeySecret - ); +export const sendSms = async (parameters: PublicParameters & SendSms, accessKeySecret: string) => { + return request(endpoint, { Action: 'SendSms', ...staticConfigs, ...parameters }, accessKeySecret); }; diff --git a/packages/connector-aliyun-sms/src/types.ts b/packages/connector-aliyun-sms/src/types.ts index cb536d18f..93f91df68 100644 --- a/packages/connector-aliyun-sms/src/types.ts +++ b/packages/connector-aliyun-sms/src/types.ts @@ -2,9 +2,14 @@ import { z } from 'zod'; import { SmsTemplateType } from './constant'; -export type { Response } from 'got'; +export const sendSmsResponseGuard = z.object({ + BizId: z.string().optional(), + Code: z.string(), + Message: z.string(), + RequestId: z.string(), +}); -export type SendSmsResponse = { BizId: string; Code: string; Message: string; RequestId: string }; +export type SendSmsResponse = z.infer; /** * @doc https://help.aliyun.com/document_detail/101414.html diff --git a/packages/connector-aliyun-sms/src/utils.ts b/packages/connector-aliyun-sms/src/utils.ts index 75b13014d..46c5ae802 100644 --- a/packages/connector-aliyun-sms/src/utils.ts +++ b/packages/connector-aliyun-sms/src/utils.ts @@ -37,7 +37,7 @@ export const getSignature = ( return createHmac('sha1', `${secret}&`).update(stringToSign).digest('base64'); }; -export const request = async ( +export const request = async ( url: string, parameters: PublicParameters & Record, accessKeySecret: string @@ -56,7 +56,7 @@ export const request = async ( } payload.append('Signature', signature); - return got.post({ + return got.post({ url, headers: { 'Content-Type': 'application/x-www-form-urlencoded', diff --git a/packages/connector-sendgrid-mail/src/index.ts b/packages/connector-sendgrid-mail/src/index.ts index 63ac9d343..df4b81cbb 100644 --- a/packages/connector-sendgrid-mail/src/index.ts +++ b/packages/connector-sendgrid-mail/src/index.ts @@ -7,13 +7,12 @@ import { EmailConnector, GetConnectorConfig, } from '@logto/connector-types'; -import { assert, Nullable } from '@silverhand/essentials'; -import got from 'got'; +import { assert } from '@silverhand/essentials'; +import got, { HTTPError } from 'got'; import { defaultMetadata, endpoint } from './constant'; import { sendGridMailConfigGuard, - SendEmailResponse, SendGridMailConfig, EmailData, Personalization, @@ -34,11 +33,7 @@ export default class SendGridMailConnector implements EmailConnector { } }; - public sendMessage: EmailSendMessageFunction> = async ( - address, - type, - data - ) => { + public sendMessage: EmailSendMessageFunction = async (address, type, data) => { const config = await this.getConfig(this.metadata.id); await this.validateConfig(config); const { apiKey, fromEmail, fromName, templates } = config; @@ -73,14 +68,28 @@ export default class SendGridMailConnector implements EmailConnector { content: [content], }; - return got - .post(endpoint, { + try { + return await got.post(endpoint, { headers: { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json', }, json: parameters, - }) - .json>(); + }); + } catch (error: unknown) { + if (error instanceof HTTPError) { + const { + response: { body: rawBody }, + } = error; + assert( + typeof rawBody === 'string', + new ConnectorError(ConnectorErrorCodes.InvalidResponse) + ); + + throw new ConnectorError(ConnectorErrorCodes.General, rawBody); + } + + throw error; + } }; } diff --git a/packages/connector-sendgrid-mail/src/types.ts b/packages/connector-sendgrid-mail/src/types.ts index a21b1dfe0..1679a196e 100644 --- a/packages/connector-sendgrid-mail/src/types.ts +++ b/packages/connector-sendgrid-mail/src/types.ts @@ -1,4 +1,3 @@ -import { Nullable } from '@silverhand/essentials'; import { z } from 'zod'; /** @@ -112,8 +111,17 @@ export type SendGridMailConfig = z.infer; /** * @doc https://docs.sendgrid.com/api-reference/mail-send/mail-send#responses */ -type HelpObject = Record; // Helper text or docs for troubleshooting +const helpObjectGuard = z.record(z.string(), z.unknown()); // Helper text or docs for troubleshooting -type ErrorObject = { message: string; field?: Nullable; help?: HelpObject }; +const errorObjectGuard = z.object({ + message: z.string(), + field: z.string().nullable().optional(), + help: helpObjectGuard.optional(), +}); -export type SendEmailResponse = { errors: ErrorObject; id?: string }; +export const sendEmailErrorResponseGuard = z.object({ + errors: z.array(errorObjectGuard), + id: z.string().optional(), +}); + +export type SendEmailErrorResponse = z.infer; diff --git a/packages/connector-twilio-sms/src/index.ts b/packages/connector-twilio-sms/src/index.ts index bc5d0507e..8b033322a 100644 --- a/packages/connector-twilio-sms/src/index.ts +++ b/packages/connector-twilio-sms/src/index.ts @@ -8,10 +8,10 @@ import { GetConnectorConfig, } from '@logto/connector-types'; import { assert } from '@silverhand/essentials'; -import got from 'got'; +import got, { HTTPError } from 'got'; import { defaultMetadata, endpoint } from './constant'; -import { twilioSmsConfigGuard, SendSmsResponse, TwilioSmsConfig, PublicParameters } from './types'; +import { twilioSmsConfigGuard, TwilioSmsConfig, PublicParameters } from './types'; export default class TwilioSmsConnector implements SmsConnector { public metadata: ConnectorMetadata = defaultMetadata; @@ -26,7 +26,7 @@ export default class TwilioSmsConnector implements SmsConnector { } }; - public sendMessage: EmailSendMessageFunction = async (address, type, data) => { + public sendMessage: EmailSendMessageFunction = async (address, type, data) => { const config = await this.getConfig(this.metadata.id); await this.validateConfig(config); const { accountSID, authToken, fromMessagingServiceSID, templates } = config; @@ -49,15 +49,29 @@ export default class TwilioSmsConnector implements SmsConnector { : template.content, }; - return got - .post(endpoint.replace(/{{accountSID}}/g, accountSID), { + try { + return await got.post(endpoint.replace(/{{accountSID}}/g, accountSID), { headers: { Authorization: 'Basic ' + Buffer.from([accountSID, authToken].join(':')).toString('base64'), 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams(parameters).toString(), - }) - .json(); + }); + } catch (error: unknown) { + if (error instanceof HTTPError) { + const { + response: { body: rawBody }, + } = error; + assert( + typeof rawBody === 'string', + new ConnectorError(ConnectorErrorCodes.InvalidResponse) + ); + + throw new ConnectorError(ConnectorErrorCodes.General, rawBody); + } + + throw error; + } }; } diff --git a/packages/connector-twilio-sms/src/types.ts b/packages/connector-twilio-sms/src/types.ts index bae2f6f09..c92dbef1a 100644 --- a/packages/connector-twilio-sms/src/types.ts +++ b/packages/connector-twilio-sms/src/types.ts @@ -61,3 +61,12 @@ export type SendSmsResponse = { to: string; uri: string; }; + +export const sendSmsErrorResponseGuard = z.object({ + code: z.number(), + message: z.string(), + more_info: z.string(), + status: z.number(), +}); + +export type SendSmsErrorResponse = z.infer;