From 65a675de2f92d0d52149dfd81e24f865957c4f3a Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:08:20 +0200 Subject: [PATCH] DEVEXP-380: Support plugins addition in SinchClient parameters (#52) --- .../src/rest/v1/conversation-domain-api.ts | 17 +- .../src/rest/v1/conversation-service.ts | 2 +- packages/fax/src/rest/v3/fax-domain-api.ts | 17 +- .../numbers/src/rest/v1/numbers-domain-api.ts | 18 +- .../src/api/api-client-options-helper.ts | 124 +++++--- packages/sdk-client/src/api/api-client.ts | 8 +- packages/sdk-client/src/api/api-interface.ts | 7 - packages/sdk-client/src/api/index.ts | 34 +- .../src/client/api-client-helpers.ts | 3 +- .../client/api-client-pagination-helper.ts | 2 +- .../sdk-client/src/client/api-fetch-client.ts | 14 +- .../sdk-client/src/domain/domain-interface.ts | 11 +- .../plugins/exception/exception.response.ts | 2 +- .../src/plugins/oauth2/oauth2-api.ts | 3 +- .../plugins/oauth2/oauth2-token.request.ts | 4 +- .../src/utils/authorization.helper.ts | 2 +- .../api/api-client-options-helper.test.ts | 300 ++++++++++++++++++ packages/sms/src/rest/v1/sms-domain-api.ts | 38 +-- .../src/rest/v1/verification-domain-api.ts | 14 +- .../voice/src/rest/v1/voice-domain-api.ts | 17 +- packages/voice/src/rest/v1/voice-service.ts | 11 +- .../voice/tests/rest/v1/voice-service.test.ts | 2 +- 22 files changed, 478 insertions(+), 172 deletions(-) create mode 100644 packages/sdk-client/tests/api/api-client-options-helper.test.ts diff --git a/packages/conversation/src/rest/v1/conversation-domain-api.ts b/packages/conversation/src/rest/v1/conversation-domain-api.ts index cd5d3fd7..c5bf45a4 100644 --- a/packages/conversation/src/rest/v1/conversation-domain-api.ts +++ b/packages/conversation/src/rest/v1/conversation-domain-api.ts @@ -1,10 +1,9 @@ import { Api, ApiClient, - ApiClientOptions, ApiFetchClient, + buildOAuth2ApiClientOptions, ConversationRegion, - Oauth2TokenRequest, Region, SinchClientParameters, UnifiedCredentials, @@ -81,25 +80,13 @@ export class ConversationDomainApi implements Api { if(!Object.values(ConversationRegion).includes((region as unknown) as ConversationRegion)) { console.warn(`The region '${region}' is not supported for the Conversation API`); } - const apiClientOptions = this.buildApiClientOptions(this.sinchClientParameters); + const apiClientOptions = buildOAuth2ApiClientOptions(this.sinchClientParameters, 'Conversation'); this.client = new ApiFetchClient(apiClientOptions); this.client.apiClientOptions.hostname = this.buildHostname(region); } return this.client; } - private buildApiClientOptions(params: SinchClientParameters): ApiClientOptions { - if (!params.projectId || !params.keyId || !params.keySecret) { - throw new Error('Invalid configuration for the Conversation API: ' - + '"projectId", "keyId" and "keySecret" values must be provided'); - } - return { - projectId: params.projectId, - requestPlugins: [new Oauth2TokenRequest( params.keyId, params.keySecret)], - useServicePlanId: false, - }; - } - private buildHostname(region: Region) { switch (this.apiName) { case 'TemplatesV1Api': diff --git a/packages/conversation/src/rest/v1/conversation-service.ts b/packages/conversation/src/rest/v1/conversation-service.ts index 34c22349..1815f0ed 100644 --- a/packages/conversation/src/rest/v1/conversation-service.ts +++ b/packages/conversation/src/rest/v1/conversation-service.ts @@ -53,7 +53,7 @@ export class ConversationService { /** * Update the default hostname for the Templates API - * @param {string} hostname - The new hostname to use for all the APIs. + * @param {string} hostname - The new hostname to use for the Templates APIs. */ public setTemplatesHostname(hostname: string) { this.templatesV1.setHostname(hostname); diff --git a/packages/fax/src/rest/v3/fax-domain-api.ts b/packages/fax/src/rest/v3/fax-domain-api.ts index 2e1cd81e..1b70d802 100644 --- a/packages/fax/src/rest/v3/fax-domain-api.ts +++ b/packages/fax/src/rest/v3/fax-domain-api.ts @@ -1,11 +1,10 @@ import { Api, ApiClient, - ApiClientOptions, ApiFetchClient, SinchClientParameters, - Oauth2TokenRequest, UnifiedCredentials, + buildOAuth2ApiClientOptions, } from '@sinch/sdk-client'; export class FaxDomainApi implements Api { @@ -59,23 +58,11 @@ export class FaxDomainApi implements Api { */ public getSinchClient(): ApiClient { if (!this.client) { - const apiClientOptions = this.buildApiClientOptions(this.sinchClientParameters); + const apiClientOptions = buildOAuth2ApiClientOptions(this.sinchClientParameters, 'Fax'); this.client = new ApiFetchClient(apiClientOptions); this.client.apiClientOptions.hostname = this.sinchClientParameters.faxHostname ?? 'https://fax.api.sinch.com'; } return this.client; } - private buildApiClientOptions(params: SinchClientParameters): ApiClientOptions { - if (!params.projectId || !params.keyId || !params.keySecret) { - throw new Error('Invalid configuration for the Fax API: ' - + '"projectId", "keyId" and "keySecret" values must be provided'); - } - return { - projectId: params.projectId, - requestPlugins: [new Oauth2TokenRequest( params.keyId, params.keySecret)], - useServicePlanId: false, - }; - } - } diff --git a/packages/numbers/src/rest/v1/numbers-domain-api.ts b/packages/numbers/src/rest/v1/numbers-domain-api.ts index edbea46f..255ea28c 100644 --- a/packages/numbers/src/rest/v1/numbers-domain-api.ts +++ b/packages/numbers/src/rest/v1/numbers-domain-api.ts @@ -1,9 +1,10 @@ import { Api, - ApiClient, ApiClientOptions, + ApiClient, ApiFetchClient, + buildOAuth2ApiClientOptions, SinchClientParameters, - Oauth2TokenRequest, UnifiedCredentials, + UnifiedCredentials, } from '@sinch/sdk-client'; export class NumbersDomainApi implements Api { @@ -57,22 +58,11 @@ export class NumbersDomainApi implements Api { */ public getSinchClient(): ApiClient { if (!this.client) { - const apiClientOptions = this.buildApiClientOptions(this.sinchClientParameters); + const apiClientOptions = buildOAuth2ApiClientOptions(this.sinchClientParameters, 'Numbers'); this.client = new ApiFetchClient(apiClientOptions); this.client.apiClientOptions.hostname = this.sinchClientParameters.numbersHostname ?? 'https://numbers.api.sinch.com'; } return this.client; } - private buildApiClientOptions(params: SinchClientParameters): ApiClientOptions { - if (!params.projectId || !params.keyId || !params.keySecret) { - throw new Error('Invalid configuration for the Numbers API: ' - + '"projectId", "keyId" and "keySecret" values must be provided'); - } - return { - projectId: params.projectId, - requestPlugins: [new Oauth2TokenRequest( params.keyId, params.keySecret)], - useServicePlanId: false, - }; - } } diff --git a/packages/sdk-client/src/api/api-client-options-helper.ts b/packages/sdk-client/src/api/api-client-options-helper.ts index 68c3264b..17a9cc10 100644 --- a/packages/sdk-client/src/api/api-client-options-helper.ts +++ b/packages/sdk-client/src/api/api-client-options-helper.ts @@ -1,37 +1,87 @@ -// import { ApiTokenRequest, Oauth2TokenRequest, SigningRequest, XTimestampRequest } from '../plugins'; -// // import { ApiClientOptions } from './api-client-options'; -// -// export const buildApiClientOptionsForProjectId = ( -// projectId: string, -// keyId: string, -// keySecret: string, -// ) => { -// return { -// projectId, -// requestPlugins: [new Oauth2TokenRequest(keyId, keySecret)], -// useServicePlanId: false, -// }; -// }; -// -// export const buildApiClientOptionForServicePlanId = ( -// servicePlanId: string, -// apiToken: string, -// ) => { -// return { -// projectId: servicePlanId, -// requestPlugins: [new ApiTokenRequest(apiToken)], -// useServicePlanId: true, -// }; -// }; -// -// export const buildApiClientOptionForApplication = ( -// applicationKey: string, -// applicationSecret: string, -// ) => { -// return { -// requestPlugins: [ -// new XTimestampRequest(), -// new SigningRequest(applicationKey, applicationSecret), -// ], -// }; -// }; +import { Region, SinchClientParameters } from '../domain'; +import { ApiClientOptions } from './api-client-options'; +import { + ApiTokenRequest, + Oauth2TokenRequest, + SigningRequest, + XTimestampRequest, +} from '../plugins'; + +export const buildOAuth2ApiClientOptions = (params: SinchClientParameters, apiName: string): ApiClientOptions => { + if (!params.projectId || !params.keyId || !params.keySecret) { + throw new Error(`Invalid configuration for the ${apiName} API: "projectId", "keyId" and "keySecret" values must be provided`); + } + const apiClientOptions: ApiClientOptions = { + projectId: params.projectId, + requestPlugins: [new Oauth2TokenRequest(params.keyId, params.keySecret, params.authHostname)], + useServicePlanId: false, + }; + addPlugins(apiClientOptions, params); + return apiClientOptions; +}; + +export const buildApplicationSignedApiClientOptions = ( + params: SinchClientParameters, apiName: string, +): ApiClientOptions => { + if (!params.applicationKey || !params.applicationSecret) { + throw new Error(`Invalid configuration for the ${apiName} API: "applicationKey" and "applicationSecret" values must be provided`); + } + const apiClientOptions: ApiClientOptions = { + requestPlugins: [ + new XTimestampRequest(), + new SigningRequest(params.applicationKey, params.applicationSecret), + ], + }; + addPlugins(apiClientOptions, params); + return apiClientOptions; +}; + +export const buildFlexibleOAuth2OrApiTokenApiClientOptions = ( + params: SinchClientParameters, region: Region, apiName: string, +): ApiClientOptions => { + let apiClientOptions: ApiClientOptions | undefined; + // Check the region: if US or EU, try to use the OAuth2 authentication with the access key / secret under the project Id + if (!params.forceServicePlanIdUsageForSmsApi + && (region === Region.UNITED_STATES || region === Region.EUROPE)) { + // Let's check the required parameters for OAuth2 authentication + if (params.projectId && params.keyId && params.keySecret) { + apiClientOptions = { + projectId: params.projectId, + requestPlugins: [new Oauth2TokenRequest(params.keyId, params.keySecret, params.authHostname)], + useServicePlanId: false, + }; + } + } + if (!apiClientOptions) { + // The API client options couldn't be initialized for with the projectId unified authentication. + // Let's try with the servicePlanId + if (params.servicePlanId && params.apiToken) { + apiClientOptions = { + projectId: params.servicePlanId, + requestPlugins: [new ApiTokenRequest(params.apiToken)], + useServicePlanId: true, + }; + } + } + if (!apiClientOptions) { + throw new Error(`Invalid parameters for the ${apiName} API: check your configuration`); + } + addPlugins(apiClientOptions, params); + return apiClientOptions; +}; + +const addPlugins = (apiClientOptions: ApiClientOptions, params: SinchClientParameters) => { + if (params.requestPlugins && params.requestPlugins.length > 0) { + if (!apiClientOptions.requestPlugins) { + apiClientOptions.requestPlugins = []; + } + apiClientOptions.requestPlugins.push(...params.requestPlugins); + } + if (params.responsePlugins && params.responsePlugins.length > 0) { + if (!apiClientOptions.responsePlugins) { + apiClientOptions.responsePlugins = []; + } + apiClientOptions.responsePlugins.push(...params.responsePlugins); + } + return apiClientOptions; +}; diff --git a/packages/sdk-client/src/api/api-client.ts b/packages/sdk-client/src/api/api-client.ts index 2c2cbc92..6d3193d0 100644 --- a/packages/sdk-client/src/api/api-client.ts +++ b/packages/sdk-client/src/api/api-client.ts @@ -2,7 +2,6 @@ import { RequestBody, RequestOptions } from '../plugins/core/request-plugin'; import { ApiClientOptions } from './api-client-options'; import { Headers } from 'node-fetch'; import FormData = require('form-data'); -import { FileBuffer } from './api-interface'; export enum PaginationEnum { NONE, @@ -51,6 +50,13 @@ export interface PaginatedApiProperties { dataKey: string, } +export interface FileBuffer { + /** Name of the file extracted from the 'content-disposition' header */ + fileName: string; + /** File content as Buffer */ + buffer: Buffer; +} + /** * API Client used to call the server */ diff --git a/packages/sdk-client/src/api/api-interface.ts b/packages/sdk-client/src/api/api-interface.ts index 16968b99..51d4370a 100644 --- a/packages/sdk-client/src/api/api-interface.ts +++ b/packages/sdk-client/src/api/api-interface.ts @@ -7,10 +7,3 @@ export interface Api { /** API Client used to process the calls to the API */ client?: ApiClient; } - -export interface FileBuffer { - /** Name of the file extracted from the 'content-disposition' header */ - fileName: string; - /** File content as Buffer */ - buffer: Buffer; -} diff --git a/packages/sdk-client/src/api/index.ts b/packages/sdk-client/src/api/index.ts index 2580cf8b..ca6b44f9 100644 --- a/packages/sdk-client/src/api/index.ts +++ b/packages/sdk-client/src/api/index.ts @@ -1,6 +1,28 @@ -export * from './api-client-options'; -// export * from './api-client-options-helper'; -export * from './api-client'; -export * from './api-errors'; -export * from './api-interface'; -export * from './callback-webhooks-interface'; +export { + ApiClientOptions, +} from './api-client-options'; +export { + buildOAuth2ApiClientOptions, + buildApplicationSignedApiClientOptions, + buildFlexibleOAuth2OrApiTokenApiClientOptions, +} from './api-client-options-helper'; +export { + ApiClient, + ApiListPromise, + FileBuffer, + PageResult, + PaginatedApiProperties, + PaginationEnum, +} from './api-client'; +export { + GenericError, + RequestFailedError, + EmptyResponseError, + ResponseJSONParseError, +} from './api-errors'; +export { + Api, +} from './api-interface'; +export { + CallbackProcessor, +} from './callback-webhooks-interface'; diff --git a/packages/sdk-client/src/client/api-client-helpers.ts b/packages/sdk-client/src/client/api-client-helpers.ts index 5a11d2e5..84a7acf1 100644 --- a/packages/sdk-client/src/client/api-client-helpers.ts +++ b/packages/sdk-client/src/client/api-client-helpers.ts @@ -3,7 +3,8 @@ import { RequestPlugin, RequestPluginEnum, } from '../plugins/core/request-plugin'; -import { ApiCallParameters, ApiCallParametersWithPagination, ErrorContext, GenericError } from '../api'; +import { ApiCallParameters, ApiCallParametersWithPagination } from '../api/api-client'; +import { ErrorContext, GenericError } from '../api/api-errors'; export const manageExpiredToken = async ( apiCallParameters: ApiCallParameters | ApiCallParametersWithPagination, diff --git a/packages/sdk-client/src/client/api-client-pagination-helper.ts b/packages/sdk-client/src/client/api-client-pagination-helper.ts index 564d8f62..18900e44 100644 --- a/packages/sdk-client/src/client/api-client-pagination-helper.ts +++ b/packages/sdk-client/src/client/api-client-pagination-helper.ts @@ -6,7 +6,7 @@ import { PageResult, PaginatedApiProperties, PaginationEnum, -} from '../api'; +} from '../api/api-client'; import { RequestOptions } from '../plugins/core/request-plugin'; class SinchIterator implements AsyncIterator { diff --git a/packages/sdk-client/src/client/api-fetch-client.ts b/packages/sdk-client/src/client/api-fetch-client.ts index bde6fe8d..6663ec6d 100644 --- a/packages/sdk-client/src/client/api-fetch-client.ts +++ b/packages/sdk-client/src/client/api-fetch-client.ts @@ -4,16 +4,20 @@ import { ExceptionResponse } from '../plugins/exception'; import { TimezoneResponse } from '../plugins/timezone'; import { ApiClient, + ApiCallParameters, + ApiCallParametersWithPagination, + PageResult, + FileBuffer, +} from '../api/api-client'; +import { ApiClientOptions, +} from '../api/api-client-options'; +import { EmptyResponseError, ErrorContext, GenericError, - ApiCallParameters, ResponseJSONParseError, - ApiCallParametersWithPagination, - PageResult, - FileBuffer, -} from '../api'; +} from '../api/api-errors'; import fetch, { Response, Headers } from 'node-fetch'; import FormData = require('form-data'); import { buildErrorContext, manageExpiredToken, reviveDates } from './api-client-helpers'; diff --git a/packages/sdk-client/src/domain/domain-interface.ts b/packages/sdk-client/src/domain/domain-interface.ts index d630c7ac..c26040f5 100644 --- a/packages/sdk-client/src/domain/domain-interface.ts +++ b/packages/sdk-client/src/domain/domain-interface.ts @@ -1,8 +1,12 @@ +import { RequestPlugin } from '../plugins/core/request-plugin'; +import { ResponsePlugin } from '../plugins/core/response-plugin'; + export interface SinchClientParameters extends Partial, Partial, Partial, - ApiHostname {} + ApiHostname, + ApiPlugins {} export interface UnifiedCredentials { /** The project ID associated with the API Client. You can find this on your [Dashboard](https://dashboard.sinch.com/account/access-keys). */ @@ -47,6 +51,11 @@ export interface ApiHostname { voiceApplicationManagementHostname?: string; } +export interface ApiPlugins { + requestPlugins?: RequestPlugin[]; + responsePlugins?: ResponsePlugin[]; +} + export const isUnifiedCredentials = (credentials: any): credentials is UnifiedCredentials => { const candidate = (credentials) as UnifiedCredentials; return candidate.projectId !== undefined diff --git a/packages/sdk-client/src/plugins/exception/exception.response.ts b/packages/sdk-client/src/plugins/exception/exception.response.ts index f82d616f..f8836bd0 100644 --- a/packages/sdk-client/src/plugins/exception/exception.response.ts +++ b/packages/sdk-client/src/plugins/exception/exception.response.ts @@ -1,4 +1,4 @@ -import { EmptyResponseError, RequestFailedError } from '../../api'; +import { EmptyResponseError, RequestFailedError } from '../../api/api-errors'; import { PluginRunner } from '../core'; import { ResponsePlugin, ResponsePluginContext } from '../core/response-plugin'; diff --git a/packages/sdk-client/src/plugins/oauth2/oauth2-api.ts b/packages/sdk-client/src/plugins/oauth2/oauth2-api.ts index 92fbeec6..f87800d8 100644 --- a/packages/sdk-client/src/plugins/oauth2/oauth2-api.ts +++ b/packages/sdk-client/src/plugins/oauth2/oauth2-api.ts @@ -1,4 +1,5 @@ -import { Api, ApiClient } from '../../api'; +import { Api } from '../../api/api-interface'; +import { ApiClient } from '../../api/api-client'; export interface Response { access_token: string; diff --git a/packages/sdk-client/src/plugins/oauth2/oauth2-token.request.ts b/packages/sdk-client/src/plugins/oauth2/oauth2-token.request.ts index 5d8fe179..95c64ff4 100644 --- a/packages/sdk-client/src/plugins/oauth2/oauth2-token.request.ts +++ b/packages/sdk-client/src/plugins/oauth2/oauth2-token.request.ts @@ -1,10 +1,10 @@ import { PluginRunner } from '../core'; import { RequestOptions, RequestPlugin, RequestPluginEnum } from '../core/request-plugin'; import { AdditionalHeadersRequest } from '../additional-headers'; -import { ApiClient } from '../../api'; +import { ApiClient } from '../../api/api-client'; import { OAuth2Api } from './oauth2-api'; import { BasicAuthenticationRequest } from '../basicAuthentication'; -import { ApiFetchClient } from '../../client'; +import { ApiFetchClient } from '../../client/api-fetch-client'; export class Oauth2TokenRequest implements RequestPlugin { private readonly apiClient: ApiClient; diff --git a/packages/sdk-client/src/utils/authorization.helper.ts b/packages/sdk-client/src/utils/authorization.helper.ts index 60e8ef1d..ef4b1dfc 100644 --- a/packages/sdk-client/src/utils/authorization.helper.ts +++ b/packages/sdk-client/src/utils/authorization.helper.ts @@ -1,6 +1,6 @@ import crypto from 'crypto'; import { IncomingHttpHeaders } from 'http'; -import { RequestBody } from '../plugins'; +import { RequestBody } from '../plugins/core/request-plugin'; import * as console from 'console'; /** diff --git a/packages/sdk-client/tests/api/api-client-options-helper.test.ts b/packages/sdk-client/tests/api/api-client-options-helper.test.ts new file mode 100644 index 00000000..9dead3e5 --- /dev/null +++ b/packages/sdk-client/tests/api/api-client-options-helper.test.ts @@ -0,0 +1,300 @@ +import { + ApiTokenRequest, + buildApplicationSignedApiClientOptions, + buildFlexibleOAuth2OrApiTokenApiClientOptions, + buildOAuth2ApiClientOptions, + Oauth2TokenRequest, + PluginRunner, + Region, + SigningRequest, + SinchClientParameters, + XTimestampRequest, +} from '../../src'; +import { RequestOptions, RequestPlugin } from '../../src/plugins/core/request-plugin'; +import { ResponsePlugin, ResponsePluginContext } from '../../src/plugins/core/response-plugin'; + +const dummyRequestPlugin: RequestPlugin = { + getName(): string { + return 'dummy-request-plugin'; + }, + load(): PluginRunner { + return { + transform(data: RequestOptions): Promise | RequestOptions { + return data; + }, + }; + }, +}; + +const dummyResponsePlugin: ResponsePlugin = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + load(_context: ResponsePluginContext): PluginRunner { + return { + transform(data: { [p: string]: any } | undefined): any { + return data; + }, + }; + }, +}; + +describe('API Client Options helper', () => { + + describe('buildOAuth2ApiClientOptions', () => { + it('should build some API client options to perform OAuth2 authentication', () => { + // Given + const params: SinchClientParameters = { + projectId: 'PROJECT_ID', + keyId: 'KEY_ID', + keySecret: 'KEY_SECRET', + }; + + // When + const apiClientOptions = buildOAuth2ApiClientOptions(params, 'foo'); + + // Then + expect(apiClientOptions).toBeDefined(); + expect(apiClientOptions.requestPlugins?.length).toBe(1); + expect(apiClientOptions.requestPlugins?.[0]).toBeInstanceOf(Oauth2TokenRequest); + expect(apiClientOptions.responsePlugins).toBeUndefined(); + }); + + it('should build some API client options with additional plugins', () => { + // Given + const params: SinchClientParameters = { + projectId: 'PROJECT_ID', + keyId: 'KEY_ID', + keySecret: 'KEY_SECRET', + requestPlugins: [dummyRequestPlugin], + responsePlugins: [dummyResponsePlugin], + }; + + // When + const apiClientOptions = buildOAuth2ApiClientOptions(params, 'foo'); + + // Then + expect(apiClientOptions.requestPlugins?.length).toBe(2); + expect(apiClientOptions.responsePlugins?.length).toBe(1); + }); + + it('should throw an exception when some credentials are missing', () => { + // Given + const params: SinchClientParameters = { + projectId: 'PROJECT_ID', + }; + + // When + const buildApiClientOptionsFunction = () => buildOAuth2ApiClientOptions(params, 'foo'); + + // Then + expect(buildApiClientOptionsFunction).toThrow( + 'Invalid configuration for the foo API: "projectId", "keyId" and "keySecret" values must be provided'); + }); + }); + + describe('buildApplicationSignedApiClientOptions', () => { + it('should build some API client options to perform application-signed authentication', () => { + // Given + const params: SinchClientParameters = { + applicationKey: 'APPLICATION_KEY', + applicationSecret: 'APPLICATION_SECRET', + }; + + // When + const apiClientOptions = buildApplicationSignedApiClientOptions(params, 'foo'); + + // Then + expect(apiClientOptions).toBeDefined(); + expect(apiClientOptions.requestPlugins?.length).toBe(2); + const requestPlugins = apiClientOptions.requestPlugins!; + const xTimestampRequestPlugin = requestPlugins.filter((plugin) => plugin instanceof XTimestampRequest); + expect(xTimestampRequestPlugin.length).toBe(1); + const signingRequestPlugin = requestPlugins.filter((plugin) => plugin instanceof SigningRequest); + expect(signingRequestPlugin.length).toBe(1); + expect(apiClientOptions.responsePlugins).toBeUndefined(); + }); + + it('should build some API client options with additional plugins', () => { + // Given + const params: SinchClientParameters = { + applicationKey: 'APPLICATION_KEY', + applicationSecret: 'APPLICATION_SECRET', + requestPlugins: [dummyRequestPlugin], + responsePlugins: [dummyResponsePlugin], + }; + + // When + const apiClientOptions = buildApplicationSignedApiClientOptions(params, 'foo'); + + // Then + expect(apiClientOptions.requestPlugins?.length).toBe(3); + expect(apiClientOptions.responsePlugins?.length).toBe(1); + }); + + it('should throw an exception when some credentials are missing', () => { + // Given + const params: SinchClientParameters = { + applicationKey: 'APPLICATION_KEY', + }; + + // When + const buildApiClientOptionsFunction = () => buildApplicationSignedApiClientOptions(params, 'foo'); + + // Then + expect(buildApiClientOptionsFunction).toThrow( + 'Invalid configuration for the foo API: "applicationKey" and "applicationSecret" values must be provided'); + }); + }); + + + describe('buildFlexibleOAuth2OrApiTokenApiClientOptions', () => { + // eslint-disable-next-line max-len + it('should build some API client options to perform OAuth2 authentication when the credentials are present and the region is supported', () => { + // Given + const params: SinchClientParameters = { + projectId: 'PROJECT_ID', + keyId: 'KEY_ID', + keySecret: 'KEY_SECRET', + servicePlanId: 'SERVICE_PLAN_ID', + apiToken: 'API_TOKEN', + }; + const region = Region.EUROPE; + + // When + const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + + // Then + expect(apiClientOptions).toBeDefined(); + expect(apiClientOptions.requestPlugins?.length).toBe(1); + expect(apiClientOptions.requestPlugins?.[0]).toBeInstanceOf(Oauth2TokenRequest); + expect(apiClientOptions.responsePlugins).toBeUndefined(); + }); + + // eslint-disable-next-line max-len + it('should build some API client options to perform API token authentication when the credentials are present and the region is not supported by OAuth2 authentication', () => { + // Given + const params: SinchClientParameters = { + projectId: 'PROJECT_ID', + keyId: 'KEY_ID', + keySecret: 'KEY_SECRET', + servicePlanId: 'SERVICE_PLAN_ID', + apiToken: 'API_TOKEN', + }; + const region = Region.CANADA; + + // When + const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + + // Then + expect(apiClientOptions).toBeDefined(); + expect(apiClientOptions.requestPlugins?.length).toBe(1); + expect(apiClientOptions.requestPlugins?.[0]).toBeInstanceOf(ApiTokenRequest); + expect(apiClientOptions.responsePlugins).toBeUndefined(); + }); + + it('should force the usage of API token authentication even when the region supports OAuth2 authentication', () => { + // Given + const params: SinchClientParameters = { + projectId: 'PROJECT_ID', + keyId: 'KEY_ID', + keySecret: 'KEY_SECRET', + servicePlanId: 'SERVICE_PLAN_ID', + apiToken: 'API_TOKEN', + forceServicePlanIdUsageForSmsApi: true, + }; + const region = Region.EUROPE; + + // When + const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + + // Then + expect(apiClientOptions).toBeDefined(); + expect(apiClientOptions.requestPlugins?.length).toBe(1); + expect(apiClientOptions.requestPlugins?.[0]).toBeInstanceOf(ApiTokenRequest); + expect(apiClientOptions.responsePlugins).toBeUndefined(); + }); + + it('should build some API client options with additional plugins', () => { + // Given + const params: SinchClientParameters = { + servicePlanId: 'SERVICE_PLAN_ID', + apiToken: 'API_TOKEN', + requestPlugins: [dummyRequestPlugin], + responsePlugins: [dummyResponsePlugin], + }; + const region = Region.CANADA; + + // When + const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + + // Then + expect(apiClientOptions.requestPlugins?.length).toBe(2); + expect(apiClientOptions.responsePlugins?.length).toBe(1); + }); + + // eslint-disable-next-line max-len + it('should throw an exception when the parameters are inconsistent: missing params for OAuth2 authentication', () => { + // Given + const params: SinchClientParameters = { + projectId: 'PROJECT_ID', + }; + const region = Region.EUROPE; + + // When + const buildApiClientOptionsFunction = () => buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + + // Then + expect(buildApiClientOptionsFunction).toThrow('Invalid parameters for the foo API: check your configuration'); + }); + + // eslint-disable-next-line max-len + it('should throw an exception when the parameters are inconsistent: missing params for API Token authentication', () => { + // Given + const params: SinchClientParameters = { + servicePlanId: 'SERVICE_PLAN_ID', + }; + const region = Region.EUROPE; + + // When + const buildApiClientOptionsFunction = () => buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + + // Then + expect(buildApiClientOptionsFunction).toThrow('Invalid parameters for the foo API: check your configuration'); + }); + + // eslint-disable-next-line max-len + it('should throw an exception when the parameters are inconsistent: unsupported region for OAuth2 authentication', () => { + // Given + const params: SinchClientParameters = { + projectId: 'PROJECT_ID', + keyId: 'KEY_ID', + keySecret: 'KEY_SECRET', + }; + const region = Region.CANADA; + + // When + const buildApiClientOptionsFunction = () => buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + + // Then + expect(buildApiClientOptionsFunction).toThrow('Invalid parameters for the foo API: check your configuration'); + }); + + // eslint-disable-next-line max-len + it('should throw an exception when the parameters are inconsistent: missing parameters when forcing API Token authentication', () => { + // Given + const params: SinchClientParameters = { + projectId: 'PROJECT_ID', + keyId: 'KEY_ID', + keySecret: 'KEY_SECRET', + forceServicePlanIdUsageForSmsApi: true, + }; + const region = Region.EUROPE; + + // When + const buildApiClientOptionsFunction = () => buildFlexibleOAuth2OrApiTokenApiClientOptions(params, region, 'foo'); + + // Then + expect(buildApiClientOptionsFunction).toThrow('Invalid parameters for the foo API: check your configuration'); + }); + }); + +}); diff --git a/packages/sms/src/rest/v1/sms-domain-api.ts b/packages/sms/src/rest/v1/sms-domain-api.ts index c4c7527f..bf8e68aa 100644 --- a/packages/sms/src/rest/v1/sms-domain-api.ts +++ b/packages/sms/src/rest/v1/sms-domain-api.ts @@ -1,12 +1,12 @@ import { Api, ApiClient, - ApiClientOptions, ApiFetchClient, + buildFlexibleOAuth2OrApiTokenApiClientOptions, SinchClientParameters, Region, - ApiTokenRequest, - Oauth2TokenRequest, UnifiedCredentials, ServicePlanIdCredentials, + UnifiedCredentials, + ServicePlanIdCredentials, } from '@sinch/sdk-client'; export class SmsDomainApi implements Api { @@ -66,41 +66,11 @@ export class SmsDomainApi implements Api { public getSinchClient(): ApiClient { if (!this.client) { const region = this.sinchClientParameters.region || Region.UNITED_STATES; - const apiClientOptions = this.buildApiClientOptions(this.sinchClientParameters, region); + const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(this.sinchClientParameters, region, 'SMS'); this.client = new ApiFetchClient(apiClientOptions); this.client.apiClientOptions.hostname = this.sinchClientParameters.smsHostname ?? `https://${apiClientOptions.useServicePlanId?'':'zt.'}${region}.sms.api.sinch.com`; } return this.client; } - private buildApiClientOptions(params: SinchClientParameters, region: Region): ApiClientOptions { - let apiClientOptions: ApiClientOptions | undefined; - // Check the region: if US or EU, try to use the OAuth2 authentication with the access key / secret under the project Id - if (!params.forceServicePlanIdUsageForSmsApi - && (region === Region.UNITED_STATES || region === Region.EUROPE)) { - // Let's check the required parameters for OAuth2 authentication - if (params.projectId && params.keyId && params.keySecret) { - apiClientOptions = { - projectId: params.projectId, - requestPlugins: [new Oauth2TokenRequest(params.keyId, params.keySecret)], - useServicePlanId: false, - }; - } - } - if (!apiClientOptions) { - // The API client options couldn't be initialized for with the projectId unified authentication. - // Let's try with the servicePlanId - if (params.servicePlanId && params.apiToken) { - apiClientOptions = { - projectId: params.servicePlanId, - requestPlugins: [new ApiTokenRequest(params.apiToken)], - useServicePlanId: true, - }; - } - } - if (!apiClientOptions) { - throw new Error('Invalid parameters for the SMS API: check your configuration'); - } - return apiClientOptions; - } } diff --git a/packages/verification/src/rest/v1/verification-domain-api.ts b/packages/verification/src/rest/v1/verification-domain-api.ts index c5c84a99..7b000970 100644 --- a/packages/verification/src/rest/v1/verification-domain-api.ts +++ b/packages/verification/src/rest/v1/verification-domain-api.ts @@ -2,8 +2,9 @@ import { Api, ApiClient, ApiFetchClient, + buildApplicationSignedApiClientOptions, SinchClientParameters, - SigningRequest, XTimestampRequest, ApplicationCredentials, + ApplicationCredentials, } from '@sinch/sdk-client'; export class VerificationDomainApi implements Api { @@ -62,16 +63,7 @@ export class VerificationDomainApi implements Api { */ public getSinchClient(): ApiClient { if (!this.client) { - if (!this.sinchClientParameters.applicationKey || !this.sinchClientParameters.applicationSecret) { - throw new Error('Invalid configuration for the Verification API: ' - + '"applicationKey" and "applicationSecret" values must be provided'); - } - const apiClientOptions = { - requestPlugins: [ - new XTimestampRequest(), - new SigningRequest(this.sinchClientParameters.applicationKey, this.sinchClientParameters.applicationSecret), - ], - }; + const apiClientOptions = buildApplicationSignedApiClientOptions(this.sinchClientParameters, 'Verification'); this.client = new ApiFetchClient(apiClientOptions); this.client.apiClientOptions.hostname = this.sinchClientParameters.verificationHostname ?? 'https://verification.api.sinch.com'; } diff --git a/packages/voice/src/rest/v1/voice-domain-api.ts b/packages/voice/src/rest/v1/voice-domain-api.ts index d01f33c7..50d0e6eb 100644 --- a/packages/voice/src/rest/v1/voice-domain-api.ts +++ b/packages/voice/src/rest/v1/voice-domain-api.ts @@ -1,11 +1,11 @@ import { Api, ApiClient, - ApiFetchClient, ApplicationCredentials, - SigningRequest, + ApiFetchClient, + ApplicationCredentials, + buildApplicationSignedApiClientOptions, SinchClientParameters, VoiceRegion, - XTimestampRequest, } from '@sinch/sdk-client'; export class VoiceDomainApi implements Api { @@ -75,16 +75,7 @@ export class VoiceDomainApi implements Api { */ public getSinchClient(): ApiClient { if (!this.client) { - if (!this.sinchClientParameters.applicationKey || !this.sinchClientParameters.applicationSecret) { - throw new Error('Invalid configuration for the Verification API: ' - + '"applicationKey" and "applicationSecret" values must be provided'); - } - const apiClientOptions = { - requestPlugins: [ - new XTimestampRequest(), - new SigningRequest(this.sinchClientParameters.applicationKey, this.sinchClientParameters.applicationSecret), - ], - }; + const apiClientOptions = buildApplicationSignedApiClientOptions(this.sinchClientParameters, 'Voice'); this.client = new ApiFetchClient(apiClientOptions); const region: VoiceRegion = this.sinchClientParameters.voiceRegion || VoiceRegion.DEFAULT; this.client.apiClientOptions.hostname = this.buildHostname(region); diff --git a/packages/voice/src/rest/v1/voice-service.ts b/packages/voice/src/rest/v1/voice-service.ts index 46a07f89..28eef344 100644 --- a/packages/voice/src/rest/v1/voice-service.ts +++ b/packages/voice/src/rest/v1/voice-service.ts @@ -18,9 +18,8 @@ export class VoiceService { } /** - * Update the default hostname for each API - * - * @param {string} hostname - The new hostname to use for all the APIs. + * Update the default hostname for each API except Applications + * @param {string} hostname - The new hostname to use for all the APIs except Applications. */ public setHostname(hostname: string) { this.conferences.setHostname(hostname); @@ -28,7 +27,11 @@ export class VoiceService { this.callouts.setHostname(hostname); } - public setApplicationManagementHostname(hostname: string) { + /** + * Update the default hostname for the Applications API + * @param {string} hostname - The new hostname to use for the Applications API. + */ + public setApplicationsManagementHostname(hostname: string) { this.applications.setHostname(hostname); } diff --git a/packages/voice/tests/rest/v1/voice-service.test.ts b/packages/voice/tests/rest/v1/voice-service.test.ts index 2322f419..786b9472 100644 --- a/packages/voice/tests/rest/v1/voice-service.test.ts +++ b/packages/voice/tests/rest/v1/voice-service.test.ts @@ -51,7 +51,7 @@ describe('Voice Service', () => { // When const voiceService = new VoiceService(params); - voiceService.setApplicationManagementHostname(CUSTOM_HOSTNAME_APPLICATIONS); + voiceService.setApplicationsManagementHostname(CUSTOM_HOSTNAME_APPLICATIONS); // Then expect(voiceService.callouts.getSinchClient().apiClientOptions.hostname).toBe(DEFAULT_HOSTNAME);