diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 387478f1..637a6f76 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -181,7 +181,11 @@ jobs: # Import sample data - name: 'Import sample data' - run: sf data tree import -p data/data-plan.json -p data/data-plan2.json + run: sf data tree import -p data/data-plan.json + + # Import sample data 2 + - name: 'Import sample data 2' + run: sf data tree import -p data/data-plan2.json # Run Apex tests in scratch org - name: 'Run Apex tests' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e56aefe3..3db6831f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,7 +120,11 @@ jobs: # Import sample data - name: 'Import sample data' - run: sf data tree import -p data/data-plan.json -p data/data-plan2.json + run: sf data tree import -p data/data-plan.json + + # Import sample data 2 + - name: 'Import sample data 2' + run: sf data tree import -p data/data-plan2.json # Run Apex tests in scratch org - name: 'Run Apex tests' diff --git a/force-app/main/default/classes/Integration Recipes/NamedCredentialRecipes.cls b/force-app/main/default/classes/Integration Recipes/NamedCredentialRecipes.cls new file mode 100644 index 00000000..43f5d0b0 --- /dev/null +++ b/force-app/main/default/classes/Integration Recipes/NamedCredentialRecipes.cls @@ -0,0 +1,130 @@ +/** + * @description Demonstrates how to manage named credentials from Apex + * @group Integration Recipes + */ +public with sharing class NamedCredentialRecipes { + public static final String NAMED_CREDENTIAL_MASTER_LABEL = 'GoogleBooksAPI (created with Apex)'; + public static final String NAMED_CREDENTIAL_DEVELOPER_NAME = 'googleBooksAPIApex'; + public static final ConnectApi.NamedCredentialType NAMED_CREDENTIAL_TYPE = ConnectApi.NamedCredentialType.SecuredEndpoint; + public static final String NAMED_CREDENTIAL_CALLOUT_URL = 'https://www.googleapis.com/books/v1'; + public static final Boolean NAMED_CREDENTIAL_ALLOW_MERGE_FIELDS_IN_BODY = false; + public static final Boolean NAMED_CREDENTIAL_ALLOW_MERGE_FIELDS_IN_HEADER = false; + public static final Boolean NAMED_CREDENTIAL_GENERATE_AUTH_HEADER = true; + public static final String EXTERNAL_CREDENTIAL_MASTER_LABEL = 'GoogleBooksAPI (created with Apex)'; + public static final String EXTERNAL_CREDENTIAL_DEVELOPER_NAME = 'googleBooksAPIApexExternal'; + public static final ConnectApi.CredentialAuthenticationProtocol EXTERNAL_CREDENTIAL_AUTHENTICATION_PROTOCOL = ConnectApi.CredentialAuthenticationProtocol.Custom; + public static final String PRINCIPAL_NAME = 'Developer Access'; + public static final ConnectApi.CredentialPrincipalType PRINCIPAL_TYPE = ConnectApi.CredentialPrincipalType.NamedPrincipal; + public static final Integer PRINCIPAL_SEQUENCE_NUMBER = 1; + + /** + * @description Demonstrates how create a named credential from Apex. + * @param connectApiWrapper instance of ConnectApiWrapper, created to allow mocking + * @param permissionSetName name of the permission set that will have access to the external credential + * @return ConnectApi.NamedCredential The created named credential + * @example + * System.debug(NamedCredentialRecipes.createNamedCredential(new ConnectApiWrapper(), 'Apex_Recipes')); + * HttpResponse response = RestClient.makeApiCall( + * NAMED_CREDENTIAL_DEVELOPER_NAME, + * RestClient.HttpVerb.GET, + * '/volumes?q=salesforce' + * ); + * System.debug(response.getBody()); + **/ + public static ConnectApi.NamedCredential createNamedCredential( + ConnectApiWrapper connectApiWrapper, + String permissionSetName + ) { + // Create an external credential (you could use an existing one) + ConnectApi.ExternalCredential externalCredential = NamedCredentialRecipes.createExternalCredential( + connectApiWrapper, + permissionSetName + ); + + // Create a list of external credential inputs and add the external credential name + List externalCredentials = new List(); + ConnectApi.ExternalCredentialInput externalCredentialInput = new ConnectApi.ExternalCredentialInput(); + externalCredentialInput.developerName = externalCredential.DeveloperName; + externalCredentials.add(externalCredentialInput); + + // Create a named credential input and setup the required fields + ConnectApi.NamedCredentialInput namedCredentialInput = new ConnectApi.NamedCredentialInput(); + namedCredentialInput.developerName = NAMED_CREDENTIAL_DEVELOPER_NAME; + namedCredentialInput.masterLabel = NAMED_CREDENTIAL_MASTER_LABEL; + namedCredentialInput.type = NAMED_CREDENTIAL_TYPE; + namedCredentialInput.calloutUrl = NAMED_CREDENTIAL_CALLOUT_URL; + namedCredentialInput.externalCredentials = externalCredentials; + + // Configure the named credential callout options + ConnectApi.NamedCredentialCalloutOptionsInput calloutOptions = new ConnectApi.NamedCredentialCalloutOptionsInput(); + calloutOptions.allowMergeFieldsInBody = NAMED_CREDENTIAL_ALLOW_MERGE_FIELDS_IN_BODY; + calloutOptions.allowMergeFieldsInHeader = NAMED_CREDENTIAL_ALLOW_MERGE_FIELDS_IN_HEADER; + calloutOptions.generateAuthorizationHeader = NAMED_CREDENTIAL_GENERATE_AUTH_HEADER; + namedCredentialInput.calloutOptions = calloutOptions; + + // Create the named credential! + return connectApiWrapper.createNamedCredential(namedCredentialInput); + } + + /** + * @description This example shows how to create an external credential in Apex. + * An external credential contains the authentication and authorization information for the callout, + * and needs to be linked to a named credential in order to be used. + * @param connectApiWrapper instance of ConnectApiWrapper, created to allow mocking + * @param permissionSetName name of the permission set that will have access to the external credential + * @return ConnectApi.ExternalCredential The created external credential + * @example + * System.debug(NamedCredentialRecipes.createExternalCredential(new ConnectApiWrapper(), 'Apex_Recipes')); + **/ + private static ConnectApi.ExternalCredential createExternalCredential( + ConnectApiWrapper connectApiWrapper, + String permissionSetName + ) { + ConnectApi.ExternalCredentialInput externalCredentialInput = new ConnectApi.ExternalCredentialInput(); + externalCredentialInput.developerName = EXTERNAL_CREDENTIAL_DEVELOPER_NAME; + externalCredentialInput.masterLabel = EXTERNAL_CREDENTIAL_MASTER_LABEL; + externalCredentialInput.authenticationProtocol = EXTERNAL_CREDENTIAL_AUTHENTICATION_PROTOCOL; + + // Populate principals to connect the external credential to permissions + ConnectApi.ExternalCredentialPrincipalInput principalInput = new ConnectApi.ExternalCredentialPrincipalInput(); + principalInput.principalName = PRINCIPAL_NAME; + principalInput.principalType = PRINCIPAL_TYPE; + principalInput.sequenceNumber = PRINCIPAL_SEQUENCE_NUMBER; + + externalCredentialInput.principals = new List{ + principalInput + }; + + // Create external credential + ConnectApi.ExternalCredential externalCredential = connectApiWrapper.createExternalCredential( + externalCredentialInput + ); + + if (!Test.isRunningTest()) { + // Tests should skip giving permission set access, as principal doesn't really exist + // Reload principal to get its id + List principals = connectApiWrapper.getExternalCredential( + EXTERNAL_CREDENTIAL_DEVELOPER_NAME + ) + .principals; + + PermissionSet permissionSet = [ + SELECT Id + FROM PermissionSet + WHERE Name = :permissionSetName + WITH USER_MODE + LIMIT 1 + ]; + + if (permissionSet != null) { + // Give access to named principal on permission set + insert as user new SetupEntityAccess( + ParentId = permissionSet.Id, + SetupEntityId = principals[0].Id + ); + } + } + + return externalCredential; + } +} diff --git a/force-app/main/default/classes/Integration Recipes/NamedCredentialRecipes.cls-meta.xml b/force-app/main/default/classes/Integration Recipes/NamedCredentialRecipes.cls-meta.xml new file mode 100644 index 00000000..c14e405d --- /dev/null +++ b/force-app/main/default/classes/Integration Recipes/NamedCredentialRecipes.cls-meta.xml @@ -0,0 +1,5 @@ + + + 59.0 + Active + diff --git a/force-app/main/default/classes/Shared Code/ConnectApiWrapper.cls b/force-app/main/default/classes/Shared Code/ConnectApiWrapper.cls new file mode 100644 index 00000000..b18efd49 --- /dev/null +++ b/force-app/main/default/classes/Shared Code/ConnectApiWrapper.cls @@ -0,0 +1,32 @@ +/** + * @description Most Connect in Apex methods require access to real organization data, + * and fail unless used in test methods marked @IsTest(SeeAllData=true). + * An alternative to that, is mocking the calls to the ConnectAPI class. + * This class can be used to inject the ConnectAPI dependency, + * allowing its methods to be mocked in test classes. + * @group Shared Code + * @see NamedCredentialRecipesTest + */ +public with sharing class ConnectApiWrapper { + public ConnectApi.ExternalCredential createExternalCredential( + ConnectApi.ExternalCredentialInput externalCredentialInput + ) { + return ConnectApi.NamedCredentials.createExternalCredential( + externalCredentialInput + ); + } + + public ConnectApi.NamedCredential createNamedCredential( + ConnectApi.NAmedCredentialInput namedCredentialInput + ) { + return ConnectApi.NamedCredentials.createNamedCredential( + namedCredentialInput + ); + } + + public ConnectApi.ExternalCredential getExternalCredential( + String developerName + ) { + return ConnectApi.NamedCredentials.getExternalCredential(developerName); + } +} diff --git a/force-app/main/default/classes/Shared Code/ConnectApiWrapper.cls-meta.xml b/force-app/main/default/classes/Shared Code/ConnectApiWrapper.cls-meta.xml new file mode 100644 index 00000000..c14e405d --- /dev/null +++ b/force-app/main/default/classes/Shared Code/ConnectApiWrapper.cls-meta.xml @@ -0,0 +1,5 @@ + + + 59.0 + Active + diff --git a/force-app/main/default/staticresources/documentation/ConnectApiWrapper.md b/force-app/main/default/staticresources/documentation/ConnectApiWrapper.md new file mode 100644 index 00000000..f8332ce0 --- /dev/null +++ b/force-app/main/default/staticresources/documentation/ConnectApiWrapper.md @@ -0,0 +1,19 @@ +# ConnectApiWrapper + +Most Connect in Apex methods require access to real organization data, +and fail unless used in test methods marked + + +**IsTest** (SeeAllData=true). An alternative to that, is mocking the calls to the ConnectAPI class. This class can be used to inject the ConnectAPI dependency, allowing its methods to be mocked in test classes. + + +**Group** Shared Code + + +**See** [NamedCredentialRecipesTest](NamedCredentialRecipesTest) + +## Methods +### `public ConnectApi createExternalCredential(ConnectApi externalCredentialInput)` +### `public ConnectApi createNamedCredential(ConnectApi namedCredentialInput)` +### `public ConnectApi getExternalCredential(String developerName)` +--- diff --git a/force-app/main/default/staticresources/documentation/NamedCredentialRecipes.md b/force-app/main/default/staticresources/documentation/NamedCredentialRecipes.md new file mode 100644 index 00000000..d5a2538e --- /dev/null +++ b/force-app/main/default/staticresources/documentation/NamedCredentialRecipes.md @@ -0,0 +1,103 @@ +# NamedCredentialRecipes + +Demonstrates how to manage named credentials from Apex + + +**Group** Integration Recipes + +## Fields + +### `public EXTERNAL_CREDENTIAL_AUTHENTICATION_PROTOCOL` → `ConnectApi` + + +### `public EXTERNAL_CREDENTIAL_DEVELOPER_NAME` → `String` + + +### `public EXTERNAL_CREDENTIAL_MASTER_LABEL` → `String` + + +### `public NAMED_CREDENTIAL_ALLOW_MERGE_FIELDS_IN_BODY` → `Boolean` + + +### `public NAMED_CREDENTIAL_ALLOW_MERGE_FIELDS_IN_HEADER` → `Boolean` + + +### `public NAMED_CREDENTIAL_CALLOUT_URL` → `String` + + +### `public NAMED_CREDENTIAL_DEVELOPER_NAME` → `String` + + +### `public NAMED_CREDENTIAL_GENERATE_AUTH_HEADER` → `Boolean` + + +### `public NAMED_CREDENTIAL_MASTER_LABEL` → `String` + + +### `public NAMED_CREDENTIAL_TYPE` → `ConnectApi` + + +### `public PRINCIPAL_NAME` → `String` + + +### `public PRINCIPAL_SEQUENCE_NUMBER` → `Integer` + + +### `public PRINCIPAL_TYPE` → `ConnectApi` + + +--- +## Methods +### `public static ConnectApi createNamedCredential(ConnectApiWrapper connectApiWrapper, String permissionSetName)` + +Demonstrates how create a named credential from Apex. + +#### Parameters + +|Param|Description| +|---|---| +|`connectApiWrapper`|instance of ConnectApiWrapper, created to allow mocking| +|`permissionSetName`|name of the permission set that will have access to the external credential| + +#### Returns + +|Type|Description| +|---|---| +|`ConnectApi`|ConnectApi.NamedCredential The created named credential| + +#### Example +```apex +System.debug(NamedCredentialRecipes.createNamedCredential(new ConnectApiWrapper(), 'Apex_Recipes')); +HttpResponse response = RestClient.makeApiCall( + NAMED_CREDENTIAL_DEVELOPER_NAME, + RestClient.HttpVerb.GET, + '/volumes?q=salesforce' +); +System.debug(response.getBody()); +``` + + +### `private static ConnectApi createExternalCredential(ConnectApiWrapper connectApiWrapper, String permissionSetName)` + +This example shows how to create an external credential in Apex. An external credential contains the authentication and authorization information for the callout, and needs to be linked to a named credential in order to be used. + +#### Parameters + +|Param|Description| +|---|---| +|`connectApiWrapper`|instance of ConnectApiWrapper, created to allow mocking| +|`permissionSetName`|name of the permission set that will have access to the external credential| + +#### Returns + +|Type|Description| +|---|---| +|`ConnectApi`|ConnectApi.ExternalCredential The created external credential| + +#### Example +```apex +System.debug(NamedCredentialRecipes.createExternalCredential(new ConnectApiWrapper(), 'Apex_Recipes')); +``` + + +--- diff --git a/force-app/tests/Integration Recipes/NamedCredentialRecipesTest.cls b/force-app/tests/Integration Recipes/NamedCredentialRecipesTest.cls new file mode 100644 index 00000000..e8dd1b9a --- /dev/null +++ b/force-app/tests/Integration Recipes/NamedCredentialRecipesTest.cls @@ -0,0 +1,103 @@ +@isTest +private with sharing class NamedCredentialRecipesTest { + @isTest + static void createNamedCredential() { + // GIVEN + ConnectApiWrapper connectApiWrapper = (ConnectApiWrapper) Test.createStub( + ConnectApiWrapper.class, + new ConnectApiWrapperMock() + ); + + // WHEN + Test.startTest(); + ConnectApi.NamedCredential namedCredential = NamedCredentialRecipes.createNamedCredential( + connectApiWrapper, + 'Apex_Recipes' + ); + Test.stopTest(); + + // THEN + // Check named credential + Assert.areEqual( + NamedCredentialRecipes.NAMED_CREDENTIAL_DEVELOPER_NAME, + namedCredential.developerName, + 'Expected a different developerName value' + ); + Assert.areEqual( + NamedCredentialRecipes.NAMED_CREDENTIAL_MASTER_LABEL, + namedCredential.masterLabel, + 'Expected a different masterLabel value' + ); + Assert.areEqual( + NamedCredentialRecipes.NAMED_CREDENTIAL_TYPE, + namedCredential.type, + 'Expected a different type value' + ); + Assert.areEqual( + NamedCredentialRecipes.NAMED_CREDENTIAL_CALLOUT_URL, + namedCredential.calloutUrl, + 'Expected a different calloutUrl value' + ); + Assert.areEqual( + NamedCredentialRecipes.NAMED_CREDENTIAL_ALLOW_MERGE_FIELDS_IN_BODY, + namedCredential.calloutOptions.allowMergeFieldsInBody, + 'Expected a different allowMergeFieldsInBody value' + ); + Assert.areEqual( + NamedCredentialRecipes.NAMED_CREDENTIAL_ALLOW_MERGE_FIELDS_IN_HEADER, + namedCredential.calloutOptions.allowMergeFieldsInHeader, + 'Expected a different allowMergeFieldsInHeader value' + ); + Assert.areEqual( + NamedCredentialRecipes.NAMED_CREDENTIAL_GENERATE_AUTH_HEADER, + namedCredential.calloutOptions.generateAuthorizationHeader, + 'Expected a different generateAuthorizationHeader value' + ); + + // Check external credential + Assert.areEqual( + 1, + namedCredential.externalCredentials.size(), + 'Expected a external credential created' + ); + ConnectApi.ExternalCredential externalCredential = namedCredential.externalCredentials[0]; + Assert.areEqual( + NamedCredentialRecipes.EXTERNAL_CREDENTIAL_DEVELOPER_NAME, + externalCredential.developerName, + 'Expected a different developerName value' + ); + Assert.areEqual( + NamedCredentialRecipes.EXTERNAL_CREDENTIAL_MASTER_LABEL, + externalCredential.masterLabel, + 'Expected a different masterLabel value' + ); + Assert.areEqual( + NamedCredentialRecipes.EXTERNAL_CREDENTIAL_AUTHENTICATION_PROTOCOL, + externalCredential.authenticationProtocol, + 'Expected a different authenticationProtocol value' + ); + + // Check principal + Assert.areEqual( + 1, + externalCredential.principals.size(), + 'Expected a principal created' + ); + ConnectApi.ExternalCredentialPrincipal principal = externalCredential.principals[0]; + Assert.areEqual( + NamedCredentialRecipes.PRINCIPAL_NAME, + principal.principalName, + 'Expected a different principalName value' + ); + Assert.areEqual( + NamedCredentialRecipes.PRINCIPAL_TYPE, + principal.principalType, + 'Expected a different principalType value' + ); + Assert.areEqual( + NamedCredentialRecipes.PRINCIPAL_SEQUENCE_NUMBER, + principal.sequenceNumber, + 'Expected a different sequenceNumber value' + ); + } +} diff --git a/force-app/tests/Integration Recipes/NamedCredentialRecipesTest.cls-meta.xml b/force-app/tests/Integration Recipes/NamedCredentialRecipesTest.cls-meta.xml new file mode 100644 index 00000000..c14e405d --- /dev/null +++ b/force-app/tests/Integration Recipes/NamedCredentialRecipesTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 59.0 + Active + diff --git a/force-app/tests/Shared Code/ConnectApiWrapperMock.cls b/force-app/tests/Shared Code/ConnectApiWrapperMock.cls new file mode 100644 index 00000000..be995e36 --- /dev/null +++ b/force-app/tests/Shared Code/ConnectApiWrapperMock.cls @@ -0,0 +1,55 @@ +@isTest +public class ConnectApiWrapperMock implements System.StubProvider { + public static ConnectApi.NamedCredential namedCredential = new ConnectApi.NamedCredential(); + public static ConnectApi.ExternalCredential externalCredential = new ConnectApi.ExternalCredential(); + + @SuppressWarnings('PMD.ExcessiveParameterList') // This is just a mock! + public Object handleMethodCall( + Object stubbedObject, + String stubbedMethodName, + Type returnType, + List listOfParamTypes, + List listOfParamNames, + List listOfArgs + ) { + switch on stubbedMethodName { + when 'createExternalCredential' { + ConnectApi.ExternalCredentialInput externalCredentialInput = (ConnectApi.ExternalCredentialInput) listOfArgs[0]; + externalCredential.developerName = externalCredentialInput.developerName; + externalCredential.masterLabel = externalCredentialInput.masterLabel; + externalCredential.authenticationProtocol = externalCredentialInput.authenticationProtocol; + externalCredential.principals = new List(); + for ( + ConnectApi.ExternalCredentialPrincipalInput principalInput : externalCredentialInput.principals + ) { + ConnectApi.ExternalCredentialPrincipal principal = new ConnectApi.ExternalCredentialPrincipal(); + principal.principalName = principalInput.principalName; + principal.principalType = principalInput.principalType; + principal.sequenceNumber = principalInput.sequenceNumber; + externalCredential.principals.add(principal); + } + + return externalCredential; + } + when 'createNamedCredential' { + ConnectApi.NamedCredentialInput namedCredentialInput = (ConnectApi.NamedCredentialInput) listOfArgs[0]; + namedCredential.developerName = namedCredentialInput.developerName; + namedCredential.masterLabel = namedCredentialInput.masterLabel; + namedCredential.type = namedCredentialInput.type; + namedCredential.calloutUrl = namedCredentialInput.calloutUrl; + namedCredential.calloutOptions = new ConnectApi.NamedCredentialCalloutOptions(); + namedCredential.calloutOptions.allowMergeFieldsInBody = namedCredentialInput.calloutOptions.allowMergeFieldsInBody; + namedCredential.calloutOptions.allowMergeFieldsInHeader = namedCredentialInput.calloutOptions.allowMergeFieldsInHeader; + namedCredential.calloutOptions.generateAuthorizationHeader = namedCredentialInput.calloutOptions.generateAuthorizationHeader; + namedCredential.externalCredentials = new List{ + externalCredential + }; + return namedCredential; + } + when 'getExternalCredential' { + return externalCredential; + } + } + return null; + } +} diff --git a/force-app/tests/Shared Code/ConnectApiWrapperMock.cls-meta.xml b/force-app/tests/Shared Code/ConnectApiWrapperMock.cls-meta.xml new file mode 100644 index 00000000..c14e405d --- /dev/null +++ b/force-app/tests/Shared Code/ConnectApiWrapperMock.cls-meta.xml @@ -0,0 +1,5 @@ + + + 59.0 + Active +