From f6f9d35250a1abe03fc39fc808a4847c9f539333 Mon Sep 17 00:00:00 2001 From: Piotr Srebniak Date: Tue, 12 Dec 2023 21:26:32 +0100 Subject: [PATCH] feat: #60 add unit tests --- API.md | 13 ++ src/salesforce/destination.ts | 2 +- src/salesforce/source.ts | 2 +- test/salesforce/destination.test.ts | 243 ++++++++++++++++++++++++++++ test/salesforce/profile.test.ts | 218 +++++++++++++++++++++++++ test/salesforce/source.test.ts | 232 ++++++++++++++++++++++++++ 6 files changed, 708 insertions(+), 2 deletions(-) create mode 100644 test/salesforce/destination.test.ts create mode 100644 test/salesforce/profile.test.ts create mode 100644 test/salesforce/source.test.ts diff --git a/API.md b/API.md index 9f89afad..66ce922b 100644 --- a/API.md +++ b/API.md @@ -7960,6 +7960,7 @@ const salesforceSourceProps: SalesforceSourceProps = { ... } | object | string | *No description.* | | profile | SalesforceConnectorProfile | *No description.* | | apiVersion | string | *No description.* | +| dataTransferApi | SalesforceDataTransferApi | Specifies which Salesforce API is used by Amazon AppFlow when your flow transfers data from Salesforce. | | enableDynamicFieldUpdate | boolean | *No description.* | | includeDeletedRecords | boolean | *No description.* | @@ -7995,6 +7996,18 @@ public readonly apiVersion: string; --- +##### `dataTransferApi`Optional + +```typescript +public readonly dataTransferApi: SalesforceDataTransferApi; +``` + +- *Type:* SalesforceDataTransferApi + +Specifies which Salesforce API is used by Amazon AppFlow when your flow transfers data from Salesforce. + +--- + ##### `enableDynamicFieldUpdate`Optional ```typescript diff --git a/src/salesforce/destination.ts b/src/salesforce/destination.ts index d2283c74..db324088 100644 --- a/src/salesforce/destination.ts +++ b/src/salesforce/destination.ts @@ -5,6 +5,7 @@ SPDX-License-Identifier: Apache-2.0 import { CfnFlow } from 'aws-cdk-lib/aws-appflow'; import { IConstruct } from 'constructs'; import { SalesforceConnectorProfile } from './profile'; +import { SalesforceDataTransferApi } from './salesforce-data-transfer-api'; import { SalesforceConnectorType } from './type'; import { AppFlowPermissionsManager } from '../core/appflow-permissions-manager'; import { ConnectorType } from '../core/connectors/connector-type'; @@ -12,7 +13,6 @@ import { ErrorHandlingConfiguration } from '../core/error-handling'; import { IFlow } from '../core/flows'; import { IDestination } from '../core/vertices/destination'; import { WriteOperation } from '../core/write-operation'; -import { SalesforceDataTransferApi } from './salesforce-data-transfer-api'; export interface SalesforceDestinationProps { diff --git a/src/salesforce/source.ts b/src/salesforce/source.ts index 1ce45f98..b561b827 100644 --- a/src/salesforce/source.ts +++ b/src/salesforce/source.ts @@ -5,11 +5,11 @@ SPDX-License-Identifier: Apache-2.0 import { CfnFlow } from 'aws-cdk-lib/aws-appflow'; import { IConstruct } from 'constructs'; import { SalesforceConnectorProfile } from './profile'; +import { SalesforceDataTransferApi } from './salesforce-data-transfer-api'; import { SalesforceConnectorType } from './type'; import { ConnectorType } from '../core/connectors/connector-type'; import { IFlow } from '../core/flows'; import { ISource } from '../core/vertices/source'; -import { SalesforceDataTransferApi } from './salesforce-data-transfer-api'; export interface SalesforceSourceProps { readonly profile: SalesforceConnectorProfile; diff --git a/test/salesforce/destination.test.ts b/test/salesforce/destination.test.ts new file mode 100644 index 00000000..f21c46a0 --- /dev/null +++ b/test/salesforce/destination.test.ts @@ -0,0 +1,243 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Stack } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; + +import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { + Mapping, + OnDemandFlow, + S3InputFileType, + S3Source, + SalesforceConnectorProfile, + SalesforceConnectorType, + SalesforceDataTransferApi, + SalesforceDestination, + WriteOperation, +} from '../../src'; + +describe('SalesforceDestination', () => { + + test('Destination with only connector name', () => { + const stack = new Stack(undefined, 'TestStack'); + const destination = new SalesforceDestination({ + profile: SalesforceConnectorProfile.fromConnectionProfileName(stack, 'TestProfile', 'dummy-profile'), + object: 'Account', + operation: WriteOperation.insert(), + }); + + const expectedConnectorType = SalesforceConnectorType.instance; + expect(destination.connectorType.asProfileConnectorLabel).toEqual(expectedConnectorType.asProfileConnectorLabel); + expect(destination.connectorType.asProfileConnectorType).toEqual(expectedConnectorType.asProfileConnectorType); + expect(destination.connectorType.asTaskConnectorOperatorOrigin).toEqual(expectedConnectorType.asTaskConnectorOperatorOrigin); + expect(destination.connectorType.isCustom).toEqual(expectedConnectorType.isCustom); + }); + + test('Destination in a Flow is in the stack', () => { + const stack = new Stack(undefined, 'TestStack'); + + const s3Bucket = new Bucket(stack, 'TestBucket', {}); + const source = new S3Source({ + bucket: s3Bucket, + prefix: '', + format: { + type: S3InputFileType.JSON, + }, + }); + + const destination = new SalesforceDestination({ + profile: SalesforceConnectorProfile.fromConnectionProfileName(stack, 'TestProfile', 'dummy-profile'), + dataTransferApi: SalesforceDataTransferApi.REST_SYNC, + object: 'Account', + operation: WriteOperation.insert(), + }); + + new OnDemandFlow(stack, 'TestFlow', { + source: source, + destination: destination, + mappings: [Mapping.mapAll()], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::AppFlow::Flow', { + DestinationFlowConfigList: [ + { + ConnectorProfileName: 'dummy-profile', + ConnectorType: 'Salesforce', + DestinationConnectorProperties: { + Salesforce: { + DataTransferApi: 'REST_SYNC', + Object: 'Account', + WriteOperationType: 'INSERT', + }, + }, + }, + ], + FlowName: 'TestFlow', + SourceFlowConfig: { + ConnectorType: 'S3', + SourceConnectorProperties: { + S3: { + BucketName: { + Ref: 'TestBucket560B80BC', + }, + BucketPrefix: '', + S3InputFormatConfig: { + S3InputFileType: 'JSON', + }, + }, + }, + }, + Tasks: [ + { + ConnectorOperator: { + S3: 'NO_OP', + }, + SourceFields: [], + TaskProperties: [ + { + Key: 'EXCLUDE_SOURCE_FIELDS_LIST', + Value: '[]', + }, + ], + TaskType: 'Map_all', + }, + ], + TriggerConfig: { + TriggerType: 'OnDemand', + }, + }); + }); + + test('Destination for dummy-profile in a Flow is in the stack', () => { + const stack = new Stack(undefined, 'TestStack'); + + const secret = Secret.fromSecretNameV2(stack, 'TestSecret', 'appflow/salesforce/client'); + const profile = new SalesforceConnectorProfile(stack, 'TestProfile', { + oAuth: { + accessToken: 'accessToken', + flow: { + refreshTokenGrant: { + refreshToken: 'refreshToken', + client: secret, + }, + }, + }, + instanceUrl: 'https://instance-id.develop.my.salesforce.com', + }); + + + const s3Bucket = new Bucket(stack, 'TestBucket', {}); + const source = new S3Source({ + bucket: s3Bucket, + prefix: '', + format: { + type: S3InputFileType.JSON, + }, + }); + + const destination = new SalesforceDestination({ + profile: profile, + dataTransferApi: SalesforceDataTransferApi.REST_SYNC, + object: 'Account', + operation: WriteOperation.insert(), + }); + + new OnDemandFlow(stack, 'TestFlow', { + source: source, + destination: destination, + mappings: [Mapping.mapAll()], + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::AppFlow::ConnectorProfile', { + ConnectionMode: 'Public', + ConnectorProfileConfig: { + ConnectorProfileCredentials: { + Salesforce: { + AccessToken: 'accessToken', + ClientCredentialsArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':secretsmanager:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':secret:appflow/salesforce/client', + ], + ], + }, + RefreshToken: 'refreshToken', + }, + }, + ConnectorProfileProperties: { + Salesforce: { + InstanceUrl: 'https://instance-id.develop.my.salesforce.com', + }, + }, + }, + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + }); + + template.hasResourceProperties('AWS::AppFlow::Flow', { + DestinationFlowConfigList: [ + { + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + DestinationConnectorProperties: { + Salesforce: { + DataTransferApi: 'REST_SYNC', + Object: 'Account', + WriteOperationType: 'INSERT', + }, + }, + }, + ], + FlowName: 'TestFlow', + SourceFlowConfig: { + ConnectorType: 'S3', + SourceConnectorProperties: { + S3: { + BucketName: { + Ref: 'TestBucket560B80BC', + }, + BucketPrefix: '', + S3InputFormatConfig: { + S3InputFileType: 'JSON', + }, + }, + }, + }, + Tasks: [ + { + ConnectorOperator: { + S3: 'NO_OP', + }, + SourceFields: [], + TaskProperties: [ + { + Key: 'EXCLUDE_SOURCE_FIELDS_LIST', + Value: '[]', + }, + ], + TaskType: 'Map_all', + }, + ], + TriggerConfig: { + TriggerType: 'OnDemand', + }, + }); + }); +}); diff --git a/test/salesforce/profile.test.ts b/test/salesforce/profile.test.ts new file mode 100644 index 00000000..31c2518d --- /dev/null +++ b/test/salesforce/profile.test.ts @@ -0,0 +1,218 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Stack } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { Key } from 'aws-cdk-lib/aws-kms'; +import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; + +import { SalesforceConnectorProfile } from '../../src'; + +describe('SalesforceConnectorProfileProps', () => { + + test('OAuth2 profile with direct client credentials exists in the stack', () => { + const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' } }); + + const clientSecret = new Secret(stack, 'TestSecret'); + + new SalesforceConnectorProfile(stack, 'TestProfile', { + oAuth: { + accessToken: 'accessToken', + flow: { + refreshTokenGrant: { + refreshToken: 'refreshToken', + client: clientSecret, + }, + }, + }, + instanceUrl: 'https://instance-id.develop.my.salesforce.com', + }); + Template.fromStack(stack).hasResourceProperties('AWS::AppFlow::ConnectorProfile', { + ConnectionMode: 'Public', + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + ConnectorProfileConfig: { + ConnectorProfileCredentials: { + Salesforce: { + AccessToken: 'accessToken', + ClientCredentialsArn: { + Ref: 'TestSecret16AF87B1', + }, + RefreshToken: 'refreshToken', + }, + }, + ConnectorProfileProperties: { + Salesforce: { + InstanceUrl: 'https://instance-id.develop.my.salesforce.com', + }, + }, + }, + }); + }); + + test('OAuth2 profile with client credentials as secret elements exists in the stack', () => { + const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' } }); + + const secret = new Secret(stack, 'TestSecret'); + + new SalesforceConnectorProfile(stack, 'TestProfile', { + oAuth: { + accessToken: secret.secretValueFromJson('accessToken').toString(), + flow: { + refreshTokenGrant: { + refreshToken: secret.secretValueFromJson('refreshToken').toString(), + client: secret, + }, + }, + }, + instanceUrl: secret.secretValueFromJson('instanceUrl').toString(), + }); + Template.fromStack(stack).hasResourceProperties('AWS::AppFlow::ConnectorProfile', { + ConnectionMode: 'Public', + ConnectorProfileConfig: { + ConnectorProfileCredentials: { + Salesforce: { + AccessToken: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'TestSecret16AF87B1', + }, + ':SecretString:accessToken::}}', + ], + ], + }, + ClientCredentialsArn: { + Ref: 'TestSecret16AF87B1', + }, + RefreshToken: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'TestSecret16AF87B1', + }, + ':SecretString:refreshToken::}}', + ], + ], + }, + }, + }, + ConnectorProfileProperties: { + Salesforce: { + InstanceUrl: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'TestSecret16AF87B1', + }, + ':SecretString:instanceUrl::}}', + ], + ], + }, + }, + }, + }, + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + }); + }); + + + test('OAuth2 profile with a dedicated KMS key and client credentials as secret elements exists in the stack', () => { + const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' } }); + + const key = new Key(stack, 'TestKey'); + + const secret = new Secret(stack, 'TestSecret'); + + new SalesforceConnectorProfile(stack, 'TestProfile', { + key: key, + oAuth: { + accessToken: secret.secretValueFromJson('accessToken').toString(), + flow: { + refreshTokenGrant: { + refreshToken: secret.secretValueFromJson('refreshToken').toString(), + client: secret, + }, + }, + }, + instanceUrl: secret.secretValueFromJson('instanceUrl').toString(), + }); + + Template.fromStack(stack).hasResource('AWS::AppFlow::ConnectorProfile', { + DependsOn: [ + 'TestKey4CACAF33', + 'TestSecret16AF87B1', + ], + Properties: { + ConnectionMode: 'Public', + ConnectorProfileConfig: { + ConnectorProfileCredentials: { + Salesforce: { + AccessToken: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'TestSecret16AF87B1', + }, + ':SecretString:accessToken::}}', + ], + ], + }, + ClientCredentialsArn: { + Ref: 'TestSecret16AF87B1', + }, + RefreshToken: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'TestSecret16AF87B1', + }, + ':SecretString:refreshToken::}}', + ], + ], + }, + }, + }, + ConnectorProfileProperties: { + Salesforce: { + InstanceUrl: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'TestSecret16AF87B1', + }, + ':SecretString:instanceUrl::}}', + ], + ], + }, + }, + }, + }, + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + KMSArn: { + 'Fn::GetAtt': [ + 'TestKey4CACAF33', + 'Arn', + ], + }, + }, + Type: 'AWS::AppFlow::ConnectorProfile', + }); + }); + +}); diff --git a/test/salesforce/source.test.ts b/test/salesforce/source.test.ts new file mode 100644 index 00000000..c9a7a7eb --- /dev/null +++ b/test/salesforce/source.test.ts @@ -0,0 +1,232 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Stack } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; + +import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { + Mapping, + OnDemandFlow, + S3Destination, + SalesforceConnectorProfile, + SalesforceDataTransferApi, + SalesforceSource, + SalesforceConnectorType, +} from '../../src'; + +describe('SalesforceSource', () => { + + test('Source with only connector name', () => { + const stack = new Stack(undefined, 'TestStack'); + const source = new SalesforceSource({ + profile: SalesforceConnectorProfile.fromConnectionProfileName(stack, 'TestProfile', 'dummy-profile'), + dataTransferApi: SalesforceDataTransferApi.REST_SYNC, + enableDynamicFieldUpdate: true, + apiVersion: '1', + includeDeletedRecords: true, + object: 'Account', + }); + + const expectedConnectorType = SalesforceConnectorType.instance; + expect(source.connectorType.asProfileConnectorLabel).toEqual(expectedConnectorType.asProfileConnectorLabel); + expect(source.connectorType.asProfileConnectorType).toEqual(expectedConnectorType.asProfileConnectorType); + expect(source.connectorType.asTaskConnectorOperatorOrigin).toEqual(expectedConnectorType.asTaskConnectorOperatorOrigin); + expect(source.connectorType.isCustom).toEqual(expectedConnectorType.isCustom); + }); + + test('Source in a Flow is in the stack', () => { + const stack = new Stack(undefined, 'TestStack'); + const source = new SalesforceSource({ + profile: SalesforceConnectorProfile.fromConnectionProfileName(stack, 'TestProfile', 'dummy-profile'), + dataTransferApi: SalesforceDataTransferApi.REST_SYNC, + enableDynamicFieldUpdate: true, + apiVersion: '1', + includeDeletedRecords: true, + object: 'Account', + }); + + const destination = new S3Destination({ + location: { bucket: new Bucket(stack, 'TestBucket') }, + }); + + new OnDemandFlow(stack, 'TestFlow', { + source: source, + destination: destination, + mappings: [Mapping.mapAll()], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::AppFlow::Flow', { + DestinationFlowConfigList: [ + { + ConnectorType: 'S3', + DestinationConnectorProperties: { + S3: { + BucketName: { + Ref: 'TestBucket560B80BC', + }, + }, + }, + }, + ], + FlowName: 'TestFlow', + SourceFlowConfig: { + ApiVersion: '1', + ConnectorProfileName: 'dummy-profile', + ConnectorType: 'Salesforce', + SourceConnectorProperties: { + Salesforce: { + DataTransferApi: 'REST_SYNC', + EnableDynamicFieldUpdate: true, + IncludeDeletedRecords: true, + Object: 'Account', + }, + }, + }, + Tasks: [ + { + ConnectorOperator: { + Salesforce: 'NO_OP', + }, + SourceFields: [], + TaskProperties: [ + { + Key: 'EXCLUDE_SOURCE_FIELDS_LIST', + Value: '[]', + }, + ], + TaskType: 'Map_all', + }, + ], + TriggerConfig: { + TriggerType: 'OnDemand', + }, + }); + }); + + test('Source for dummy-profile in a Flow is in the stack', () => { + const stack = new Stack(undefined, 'TestStack'); + + const secret = Secret.fromSecretNameV2(stack, 'TestSecret', 'appflow/salesforce/client'); + const profile = new SalesforceConnectorProfile(stack, 'TestProfile', { + oAuth: { + accessToken: 'accessToken', + flow: { + refreshTokenGrant: { + refreshToken: 'refreshToken', + client: secret, + }, + }, + }, + instanceUrl: 'https://instance-id.develop.my.salesforce.com', + }); + + const source = new SalesforceSource({ + profile: profile, + dataTransferApi: SalesforceDataTransferApi.REST_SYNC, + enableDynamicFieldUpdate: true, + apiVersion: '1', + includeDeletedRecords: true, + object: 'Account', + }); + + const destination = new S3Destination({ + location: { bucket: new Bucket(stack, 'TestBucket') }, + }); + + new OnDemandFlow(stack, 'TestFlow', { + source: source, + destination: destination, + mappings: [Mapping.mapAll()], + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::AppFlow::ConnectorProfile', { + ConnectionMode: 'Public', + ConnectorProfileConfig: { + ConnectorProfileCredentials: { + Salesforce: { + AccessToken: 'accessToken', + ClientCredentialsArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':secretsmanager:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':secret:appflow/salesforce/client', + ], + ], + }, + RefreshToken: 'refreshToken', + }, + }, + ConnectorProfileProperties: { + Salesforce: { + InstanceUrl: 'https://instance-id.develop.my.salesforce.com', + }, + }, + }, + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + }); + + template.hasResourceProperties('AWS::AppFlow::Flow', { + DestinationFlowConfigList: [ + { + ConnectorType: 'S3', + DestinationConnectorProperties: { + S3: { + BucketName: { + Ref: 'TestBucket560B80BC', + }, + }, + }, + }, + ], + FlowName: 'TestFlow', + SourceFlowConfig: { + ApiVersion: '1', + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + SourceConnectorProperties: { + Salesforce: { + DataTransferApi: 'REST_SYNC', + EnableDynamicFieldUpdate: true, + IncludeDeletedRecords: true, + Object: 'Account', + }, + }, + }, + Tasks: [ + { + ConnectorOperator: { + Salesforce: 'NO_OP', + }, + SourceFields: [], + TaskProperties: [ + { + Key: 'EXCLUDE_SOURCE_FIELDS_LIST', + Value: '[]', + }, + ], + TaskType: 'Map_all', + }, + ], + TriggerConfig: { + TriggerType: 'OnDemand', + }, + }); + }); +});