S2S Connection through SOAP Web Services as solution to FF c2g Accounting bug related to Connection User and Account Trigger logic

There is a bug in the new version of Financial Force Accounting package which breaks standard Salesforce to Salesforce functionality.

There is likely some trigger(s) on Account insert or update inside of this managed package which checks current user scope. However S2S Connection insert or update is made under fake “Connection User” which doesn’t have an actual User record in database and this breaks each insert or update operation. If only this package were unmanaged we could edit trigger logic and fix that bug or if only this logic were present in some validation rule that we could turn off, then there would be a chance to turn off FF Accounting trigger logic temporarily until they fix it or there would be a chance to fix it on my own.

However, this FF Accounting and managed package and its account trigger logic can not be modified or configured, so there is no simple obvious way to fix this.

This is workaround to the insert part of this bug inside of  the new version of Financial Force Accounting package. We can disable “auto-accept” for Accounts, and we can accept records manually on the organization with FF package installed. When we manually accept records, current user is our user which has record in the database and account record is successfully inserted. However, there is no simple workaround for update part. Since we can’t turn off auto-update S2S functionality.

If you know any workaround for update part or more elegant workaround for insert part, please comment under this post.

Actually we have been working on a different solution to this problem, trying to establish an SOAP Web Services Analogue of S2S connection.

There are a lot of articles on the web describing a way how to SOAP Web Services Analogue of S2S connection, like build this one on SF SE, where answer, chosen as accepted, leads to CookBook receipt, which sounds a little bit obsolete, since now you don’t have to rename method like “delete” to “delete_x” or “deleteSObjects” since they are already named as  “delete_x” when you import a WDSL file.

Anyway, after studying all available materials I have collected all the best practices and advises from different forums, posts and articles and made my own conclusion.

It is really convenient to use classes generated from Partner WDSL or Enterprise WDSL, however, they use a lot of code and might be too lengthy if you have a big organization with many objects. Also it is hard to use it for some generic solution. So for me it looks like more reasonable to have some different class which would contain limited functionality which I need only.

This is listing to my (final?) version of my class implementing the approach described above.

global class WebS2SServices {
   
 /**
 * This method enqueues asynchronous job to share records from account trigger, 
 * upf means abbreviation of Update-Patch future
 * 
 * @param SObject[] scope Account records list to be shared through Web S2S Connection Tool
 * @returns void
 */
 webservice static void upf(SObject[] scope) {
 if ( scope.size() == 0 )
 return;
 //System.enqueueJob(new WebS2SShareQueueable(scope));
 // There are some SF Platform bugs related to testing Queueable jobs https://success.salesforce.com/issues_view?id=a1p300000008ZJ0AAM
 // Refactoring this as a future method
 WebS2SServices.upcsf(new List<Id>( new Map<Id, SObject>( scope ).keySet() ) );
   
 }
   
  
/* future method-version of queueable job*/
 @future(callout=true)
 public static void upcsf(Id[] ids) {
 Map<String, WebS2SFieldCopy__c> fieldsMap = WebS2SFieldCopy__c.getAll();
 if ( fieldsMap.isEmpty() ) {
 throw new WebS2SConfigException('Custom setting WebS2SFieldCopy__c is not setup');
 }
 //List<Account> acct = new List<Account>();
 String sql = 'SELECT FF_Account_Id__c,' + String.join( new List<String>( fieldsMap.keySet() ), ',') + ' FROM Account WHERE ID IN (' + '\'' + String.join( ids , '\',\'') + '\')';
 System.debug(LoggingLevel.ERROR, '@@@ sql: ' + sql );
 upcs( Database.Query( sql ) );
 }
   
 /**
 * This method is called from asynchronous job to share records,
 * upcs means abbreviation of Update-Patch using Custom Setting
 * 
 * @param SObject[] scope Account records list to be shared through Web S2S Connection Tool
 * @returns void
 */
 webservice static void upcs(SObject[] scope) {
 Map<String, WebS2SFieldCopy__c> fieldsMap = WebS2SFieldCopy__c.getAll();
 if ( fieldsMap.isEmpty() ) {
 throw new WebS2SConfigException('Custom setting WebS2SFieldCopy__c is not setup');
 }
 List<Account> acct = (Account[]) scope;
 List<Map<String, String>> fieldsValuesMaps = new List<Map<String, String>>();
 for ( Account a: acct ) {
 Map<String, String> fieldsValuesMap = new Map<String, String>();
 fieldsValuesMap.put('Id', a.FF_Account_Id__c);
 for ( WebS2SFieldCopy__c webS2SFieldCopy: fieldsMap.values() ) {
 fieldsValuesMap.put( webS2SFieldCopy.Partner_Field_Name__c, String.valueOf( a.get( webS2SFieldCopy.Name ) ) );
 }
 fieldsValuesMaps.add( fieldsValuesMap );
 }
 System.debug(LoggingLevel.ERROR, '@@@ fieldsValuesMaps '+ fieldsValuesMaps);
 String result = upd('Account', fieldsValuesMaps);
 System.debug(LoggingLevel.ERROR, '@@@ result '+ result);
 Boolean success = getValueFromXMLString(result, 'success') == 'true'; 
   
 if ( !success ) {
 String s = getValueFromXMLString(result, 'message');
 throw new WebS2SDMLException(s);
 }
 }
   
 /**
 * Generic method to perform Web S2S Update DML Operation.
 * Takes SObject Type and fieldsMap as parameters.
 * 
 * @param String soType : SObject Type (usually equals to 'Account')
 * @param List<Map<String, String>> fieldsMaps : List of values to be updated;
 * each element describes field name/values map for each record
 * Example: [{Id='001000000000001', Name='Account 1'},{Id='001000000000002', Name='Account 2'}]
 * 
 * @returns void
 */
 public static String upd(String soType, List<Map<String, String>> fieldsMaps ) {
 String fieldTemplate = '<{0} xmlns="urn:sobject.partner.soap.sforce.com">{1}</{0}>';
 String sobjectTemplate = '<sObjects><type xmlns="urn:sobject.partner.soap.sforce.com">{0}</type>{1}</sObjects>';
 String scope = '';
 for ( Map<String, String> fieldsMap: fieldsMaps ) {
 String fieldValues = '';
 for ( String key: fieldsMap.keySet() ) {
 String value = fieldsMap.get( key );
 fieldValues += String.format( fieldTemplate, new String[]{ key, value } );
 }
 scope += String.format( sobjectTemplate, new String[]{ soType, fieldValues } );
 }
 String[] susi = login();
 String template = '<?xml version="1.0" encoding="UTF-8"?><env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><env:Header><SessionHeader xmlns="urn:partner.soap.sforce.com"><sessionId>{0}</sessionId></SessionHeader></env:Header><env:Body><update xmlns="urn:partner.soap.sforce.com">{1}</update></env:Body></env:Envelope>';
   
 System.debug(LoggingLevel.ERROR, '@@@ xml request: ' + String.format(template, new String[]{susi[1], scope}) );
 return makeHTTPPatchCall( susi[0], susi[1], String.format( template, new String[]{ susi[1], scope } ) );
 }
   
 /**
 * Web SOAP Login call
 * 
 * @returns String[]: returned list always contains two values, the first one is serverURL and the second one is SessionId
 */
 public static String[] login() {
 Map<String, WebS2SFieldCopy__c> fieldsMap = WebS2SFieldCopy__c.getAll();
 String SFLName;
 for ( WebS2SFieldCopy__c webS2SFieldCopy: fieldsMap.values() ) {
 if ( !String.isBlank( webS2SFieldCopy.SF_Login_Name__c ) ) {
 SFLName = webS2SFieldCopy.SF_Login_Name__c;
 break;
 }
 }
 if ( String.isBlank( SFLName ) ) {
 throw new WebS2SConfigException('Custom setting WebS2SFieldCopy__c does not save SF_Login_Name__c ');
 }
 Salesforce_Login__c sfl = [ SELECT Username__c, Password_Series__c, Current_Security_Token__c FROM Salesforce_Login__c WHERE Name = :SFLName LIMIT 1 ];
   
 string bodyToSendLogin;
 string outCallResultLogin;
 string userName;
 string password;
 String loginURL;
 String loginTemplate = '<?xml version="1.0" encoding="UTF-8"?><env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><env:Header /><env:Body><login xmlns="urn:partner.soap.sforce.com"><username>{0}</username><password>{1}</password></login></env:Body></env:Envelope>';
 userName = sfl.Username__c;//'bdovhan@corevalue.net';
 password = sfl.Password_Series__c + sfl.Current_Security_Token__c;//'InGodWeTrust100$X6RgBXCKzwPOthooGqs3AhMHR';
 bodyToSendLogin = String.format(loginTemplate, new String[]{userName, password});
 System.debug(LoggingLevel.ERROR, '@@@ userName: ' + userName );
 System.debug(LoggingLevel.ERROR, '@@@ password: ' + password );
 loginURL = 'https://' + ( Utils.isProductionEnvironment() ? 'login' : 'test' ) + '.salesforce.com/services/Soap/u/26.0';
 outCallResultLogin = makeHTTPCall( loginURL, bodyToSendLogin );
 System.debug(LoggingLevel.ERROR, '@@@ outCallResultLogin: ' + outCallResultLogin );
 System.assertEquals('', getValueFromXMLString(outCallResultLogin, 'sf:exceptionMessage'), 'Login credentials should be current of active user');
 System.assertEquals('', getValueFromXMLString(outCallResultLogin, 'sf:exceptionCode'), 'Login credentials should be current of active user');
 System.assertEquals('', getValueFromXMLString(outCallResultLogin, 'sf:faultstring'), 'Login credentials should be current of active user');
 System.assertEquals('', getValueFromXMLString(outCallResultLogin, 'sf:faultcode'), 'Login credentials should be current of active user');
   
   
 String serverUrl = getValueFromXMLString(outCallResultLogin, 'serverUrl'); 
 String session = getValueFromXMLString(outCallResultLogin, 'sessionId'); 
 System.assertNotEquals('', serverUrl, 'Login credentials should be current of active user');
 System.assertNotEquals('', session, 'Login credentials should be current of active user');
 return new String[]{serverUrl, session};
 }
  
/**
 * Generic method to make unauthorized Web S2S SOAP Callouts
 * 
 * @param String endPoint Server URL\Endpoint for the call
 * @param String soapBody SOAP Message Body
 * 
 * @returns String XML-based string containing SOAP Response Body
 */
 private static String makeHTTPCall(String endPoint, String soapBody){
 Http hLLogin = new Http();
 HttpRequest reqLLogin = new HttpRequest();
 reqLLogin.setTimeout(60000);
 reqLLogin.setEndpoint(endPoint); 
 reqLLogin.setMethod('POST');
 reqLLogin.setHeader('SFDC_STACK_DEPTH', '1');
 reqLLogin.setHeader('SOAPAction', 'DoesNotMatter'); 
 reqLLogin.setHeader('Accept', 'text/xml'); 
 reqLLogin.setHeader('Content-type','text/xml'); 
 reqLLogin.setHeader('charset','UTF-8');
 reqLLogin.setBody(soapBody); 
 HttpResponse resLLogin = hLLogin.send(reqLLogin);
 return resLLogin.getBody();
 }
  
/**
 * String-utility method to parse XML String and retrieve a single value from it
 * 
 * @param String xmlString Source XML-based String
 * @param String keyField Name of the key of field to be extracted
 * 
 * @returns String Value of the key of field being extracted
 */
 private static String getValueFromXMLString(String xmlString, String keyField){
 String openingTag = '<' + keyField + '>';
 String closingTag = '</' + keyField + '>';
 return ( xmlString.contains( openingTag ) && xmlString.contains( closingTag ) ) ?
 xmlString.substringBetween(openingTag, closingTag) : '';
 }
   
 /**
 * Generic method to make authorized Web S2S SOAP Callouts
 * 
 * @param String endPoint Server URL\Endpoint for the call
 * @param String sessionId Session Id for authorization
 * @param String soapBody SOAP Message Body
 * 
 * Example of SOAP Body
 * 
 * <?xml version="1.0" encoding="UTF-8"?>
 * <env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 * <env:Header><SessionHeader xmlns="urn:partner.soap.sforce.com">
 * <sessionId>00D3B000000D3aq!ARcAQPUjRpxFie0CIxKGWNlEfWvUT34HkI85kMyrqqHZ.Ct0ql7z20fnpUYN79.VWXMtAEzaX3zUYyQmpVXpt8enSdniJMN9</sessionId>
 * </SessionHeader></env:Header>
 * <env:Body><update xmlns="urn:partner.soap.sforce.com"><sObjects>
 * <type xmlns="urn:sobject.partner.soap.sforce.com">Account</type>
 * <Name xmlns="urn:sobject.partner.soap.sforce.com">Account 0 0</Name>
 * <Phone xmlns="urn:sobject.partner.soap.sforce.com">0 0</Phone>
 * <Website xmlns="urn:sobject.partner.soap.sforce.com">0.0.0.0 0</Website>
 * <Id xmlns="urn:sobject.partner.soap.sforce.com">0013B000002KzYiQAK</Id>
 * </sObjects><sObjects>
 * <type xmlns="urn:sobject.partner.soap.sforce.com">Account</type>
 * <Name xmlns="urn:sobject.partner.soap.sforce.com">Account 1 1</Name>
 * <Phone xmlns="urn:sobject.partner.soap.sforce.com">1 1</Phone>
 * <Website xmlns="urn:sobject.partner.soap.sforce.com">1.1.1.1 1</Website>
 * <Id xmlns="urn:sobject.partner.soap.sforce.com">0013B000002KyXPQA0</Id>
 * </sObjects><sObjects>
 * <type xmlns="urn:sobject.partner.soap.sforce.com">Account</type>
 * <Name xmlns="urn:sobject.partner.soap.sforce.com">Account 2 2</Name>
 * <Phone xmlns="urn:sobject.partner.soap.sforce.com">2 2</Phone>
 * <Website xmlns="urn:sobject.partner.soap.sforce.com">2.2.2.2 2</Website>
 * <Id xmlns="urn:sobject.partner.soap.sforce.com">0013B000002KzcBQAS</Id>
 * </sObjects></update></env:Body></env:Envelope>
 * 
 * @returns String XML-based string containing SOAP Response Body
 */
 private static String makeHTTPPatchCall(String endPoint, String sessionId, String soapBody){
 Http hLLogin = new Http();
 HttpRequest reqLLogin = new HttpRequest();
 reqLLogin.setTimeout(60000);
 reqLLogin.setEndpoint(endPoint); 
 reqLLogin.setMethod('POST');
 reqLLogin.setHeader('SFDC_STACK_DEPTH', '1');
 reqLLogin.setHeader('Authorization', 'Bearer ' + sessionId );
 reqLLogin.setHeader('SOAPAction','DoesNotMatter'); 
 reqLLogin.setHeader('Accept','text/xml'); 
 reqLLogin.setHeader('Content-type','text/xml'); 
 reqLLogin.setHeader('charset','UTF-8'); 
 reqLLogin.setBody(soapBody); 
 HttpResponse resLLogin = hLLogin.send(reqLLogin);
 return resLLogin.getBody();
 }
   
 // Custom exception class for wrong Web S2S configuration
 public class WebS2SConfigException extends Exception {}
 // Custom exception class for runtime DML Exception during Web S2S call
 public class WebS2SDMLException extends Exception {}
}
 * @returns String XML-based string containing SOAP Response Body
 */
 private static String makeHTTPPatchCall(String endPoint, String sessionId, String soapBody){
 Http hLLogin = new Http();
 HttpRequest reqLLogin = new HttpRequest();
 reqLLogin.setTimeout(60000);
 reqLLogin.setEndpoint(endPoint); 
 reqLLogin.setMethod('POST');
 reqLLogin.setHeader('SFDC_STACK_DEPTH', '1');
 reqLLogin.setHeader('Authorization', 'Bearer ' + sessionId );
 reqLLogin.setHeader('SOAPAction','DoesNotMatter'); 
 reqLLogin.setHeader('Accept','text/xml'); 
 reqLLogin.setHeader('Content-type','text/xml'); 
 reqLLogin.setHeader('charset','UTF-8'); 
 reqLLogin.setBody(soapBody); 
 HttpResponse resLLogin = hLLogin.send(reqLLogin);
 return resLLogin.getBody();
 }
   
 // Custom exception class for wrong Web S2S configuration
 public class WebS2SConfigException extends Exception {}
 // Custom exception class for runtime DML Exception during Web S2S call
 public class WebS2SDMLException extends Exception {}
}

I was going to use new features of Salesforce platform: Queueable jobs instead of future methods, however, looks like there are still some bugs related to testing of Queueable jobs, so in final version I will use future method. For sake of code completeness, I would still present the code of my Queueable job here.


public class WebS2SShareQueueable implements Queueable, Database.AllowsCallouts {
private SObject[] sc;
public WebS2SShareQueueable(SObject[] scope) {
sc = scope;
}
public void execute(QueueableContext qc){
WebS2SServices.upcs(sc);
}

}

 

This is test class covering WebS2SServices


@isTest
private class TestWebS2SServices {
private static Integer NUMBER_TO_CREATE = 1;
private static String SF_LOGIN_NAME = '1043';
private static String USERNAME = 'UKROP@UKROP.com';
private static String PASSWORD = 'SECURITY';
private static String SECURITYTOKEN = 'TOKEN';
private static String MSG_FAILED = 'Test should fall';
private static String MSG_CALLOUT_FAILED = 'Callout loop not allowed';
private static String MSG_SERVER_URL = 'Server_down';
private static String MSG_SESSIONID = 'Session_closed';
private static String XML_STRING_FOR_TEST = '<?xml version="1.0" encoding="UTF-8"?><env:Envelope xmlns:env="http:////schemas.xmlsoap.org//soap//envelope//" xmlns:xsd="http:////www.w3.org//2001//XMLSchema" xmlns:xsi="http:////www.w3.org//2001//XMLSchema-instance"><env:Header><SessionHeader xmlns="urn:partner.soap.sforce.com"><sessionId><//sessionId><//SessionHeader><//env:Header><env:Body><update xmlns="urn:partner.soap.sforce.com"><sObjects><type xmlns="urn:sobject.partner.soap.sforce.com">Account<//type><Id xmlns="urn:sobject.partner.soap.sforce.com">null<//Id><SalesForce_Customer_ID__c xmlns="urn:sobject.partner.soap.sforce.com">001L000000l61jBIAQ<//SalesForce_Customer_ID__c><Name xmlns="urn:sobject.partner.soap.sforce.com">Account UK<//Name><Phone xmlns="urn:sobject.partner.soap.sforce.com">1234567890<//Phone><Website xmlns="urn:sobject.partner.soap.sforce.com">www.ukr.net<//Website><c2g__CODABankCity__c xmlns="urn:sobject.partner.soap.sforce.com">Lviv<//c2g__CODABankCity__c><BillingCountry xmlns="urn:sobject.partner.soap.sforce.com">Ukraine<//BillingCountry><BillingPostalCode xmlns="urn:sobject.partner.soap.sforce.com">1111<//BillingPostalCode><BillingState xmlns="urn:sobject.partner.soap.sforce.com">2222<//BillingState><BillingStreet xmlns="urn:sobject.partner.soap.sforce.com">3333<//BillingStreet><//sObjects><//update><//env:Body><//env:Envelope>';
private static String XML_STRING_SUCCESS = '<success>true</success><serverUrl>'+MSG_SERVER_URL+'</serverUrl><sessionId>'+MSG_SESSIONID+'</sessionId>';
private static String XML_STRING_FAILED = '<success>false</success><message>'+MSG_FAILED+'</message><serverUrl>'+MSG_SERVER_URL+'</serverUrl><sessionId>'+MSG_SESSIONID+'</sessionId>';
private static Map<String, String> FIELDNAMES = new Map<String, String>{
'Id' => 'SalesForce_Customer_ID__c',
'Name' => 'Name',
'Phone' => 'Phone',
'Website' => 'Website',
'BillingCity' => 'c2g__CODABankCity__c',
'BillingCountry' => 'BillingCountry',
'BillingPostalCode' => 'BillingPostalCode',
'BillingState' => 'BillingState',
'BillingStreet' => 'BillingStreet'
};
@isTest static void testUpfFailedExistingAccount() {
String msg = '';
Salesforce_Login__c sfl = TestingUtils.createSalesforceLogins(new List<Project__c>{new Project__c()}, false)[0];
sfl.Username__c = USERNAME;
sfl.Password_Series__c = PASSWORD;
sfl.Current_Security_Token__c = SECURITYTOKEN;
insert sfl;

sfl = [ SELECT Name, Username__c, Password_Series__c, Current_Security_Token__c FROM Salesforce_Login__c LIMIT 1 ];
SF_LOGIN_NAME = sfl.Name;

List<WebS2SFieldCopy__c> webS2SFieldCopy = new List<WebS2SFieldCopy__c>();
for(String key: FIELDNAMES.keySet()){
webS2SFieldCopy.add(new WebS2SFieldCopy__c(Name = key, Partner_Field_Name__c = FIELDNAMES.get(key), SF_Login_Name__c = SF_LOGIN_NAME));
}
insert webS2SFieldCopy;

List<Account> testAccounts = TestingUtils.createAccounts( NUMBER_TO_CREATE, true );
for(Account acc: testAccounts){
acc.Name = 'Account UK';
acc.Phone = '1234567890';
acc.Website = 'www.ukr.net';
acc.BillingCity = 'Lviv';
acc.BillingCountry = 'Ukraine';
acc.BillingPostalCode = '1111';
acc.BillingState = '2222';
acc.BillingStreet = '3333';
}
SingleRequestMock mock = new SingleRequestMock( 200, 'OK', XML_STRING_SUCCESS);

Test.setMock(HttpCalloutMock.class, mock);
Test.startTest();

update testAccounts;

Test.stopTest();

System.assertEquals( NUMBER_TO_CREATE, testAccounts.size(), 'Count Accounts should match created accounts' );
System.assertEquals( FIELDNAMES .size(), webS2SFieldCopy.size(), 'Count fields account to update should match created fields' );
System.assertEquals( 2, mock.howManyTimesCalled(), 'Mock should be called twice since one callout is made from trigger and another from unit test method call.' );

}

@isTest static void testUpcsSuccess() {
String msg = '';
Salesforce_Login__c sfl = TestingUtils.createSalesforceLogins(new List<Project__c>{new Project__c()}, false)[0];
sfl.Username__c = USERNAME;
sfl.Password_Series__c = PASSWORD;
sfl.Current_Security_Token__c = SECURITYTOKEN;
insert sfl;

sfl = [ SELECT Name, Username__c, Password_Series__c, Current_Security_Token__c FROM Salesforce_Login__c LIMIT 1 ];
SF_LOGIN_NAME = sfl.Name;

List<WebS2SFieldCopy__c> webS2SFieldCopy = new List<WebS2SFieldCopy__c>();
for(String key: FIELDNAMES.keySet()){
webS2SFieldCopy.add(new WebS2SFieldCopy__c(Name = key, Partner_Field_Name__c = FIELDNAMES.get(key), SF_Login_Name__c = SF_LOGIN_NAME));
}
insert webS2SFieldCopy;

List<Account> testAccounts = TestingUtils.createAccounts( NUMBER_TO_CREATE, true );
for(Account acc: testAccounts){
acc.Name = 'Account UK';
acc.Phone = '1234567890';
acc.Website = 'www.ukr.net';
acc.BillingCity = 'Lviv';
acc.BillingCountry = 'Ukraine';
acc.BillingPostalCode = '1111';
acc.BillingState = '2222';
acc.BillingStreet = '3333';
}
SingleRequestMock mock = new SingleRequestMock( 200, 'OK', XML_STRING_SUCCESS);

Test.setMock(HttpCalloutMock.class, mock);
Test.startTest();
try{
WebS2SServices.upcs(testAccounts);
}
catch(Exception ex){
msg = ex.getMessage();
}
Test.stopTest();

System.assertEquals( NUMBER_TO_CREATE, testAccounts.size(), 'Count Accounts should match created accounts' );
System.assertEquals( FIELDNAMES.size(), webS2SFieldCopy.size(), 'Count fields account to update should match created fields' );
System.assertEquals( 2, mock.howManyTimesCalled(), 'Mock should be called twice since one callout is made from trigger and another from unit test method call.' );
String endPoint = mock.getLastRequestEndpoint();
System.debug(LoggingLevel.ERROR, '@@@ endPoint '+ endPoint);

System.assertEquals( '', msg, 'We should not get error message');
}

@isTest static void testUpcsFailed() {
String msg = '';
Salesforce_Login__c sfl = TestingUtils.createSalesforceLogins(new List<Project__c>{new Project__c()}, false)[0];
sfl.Username__c = USERNAME;
sfl.Password_Series__c = PASSWORD;
sfl.Current_Security_Token__c = SECURITYTOKEN;
insert sfl;

sfl = [ SELECT Name, Username__c, Password_Series__c, Current_Security_Token__c FROM Salesforce_Login__c LIMIT 1 ];
SF_LOGIN_NAME = sfl.Name;

List<WebS2SFieldCopy__c> webS2SFieldCopy = new List<WebS2SFieldCopy__c>();
for(String key: FIELDNAMES.keySet()){
webS2SFieldCopy.add(new WebS2SFieldCopy__c(Name = key, Partner_Field_Name__c = FIELDNAMES.get(key), SF_Login_Name__c = SF_LOGIN_NAME));
}
insert webS2SFieldCopy;

List<Account> testAccounts = TestingUtils.createAccounts( NUMBER_TO_CREATE, true );
for(Account acc: testAccounts){
acc.Name = 'Account UK';
acc.Phone = '1234567890';
acc.Website = 'www.ukr.net';
acc.BillingCity = 'Lviv';
acc.BillingCountry = 'Ukraine';
acc.BillingPostalCode = '1111';
acc.BillingState = '2222';
acc.BillingStreet = '3333';
}
SingleRequestMock mock = new SingleRequestMock( 200, 'OK', XML_STRING_FAILED);

Test.setMock(HttpCalloutMock.class, mock);
Test.startTest();
try{
WebS2SServices.upcs(testAccounts);
}
catch(Exception ex){
msg = ex.getMessage();
}
Test.stopTest();
System.assertEquals( NUMBER_TO_CREATE, testAccounts.size(), 'Count Accounts should match created accounts' );
System.assertEquals( FIELDNAMES.size(), webS2SFieldCopy.size(), 'Count fields account to update should match created fields' );
System.assertEquals( 2, mock.howManyTimesCalled(), 'Mock should be called twice since one callout is made from trigger and another from unit test method call.' );
String endPoint = mock.getLastRequestEndpoint();
System.assertEquals( MSG_FAILED, msg, 'We should get corect error message');
}

@isTest static void testUpcsWithoutWebS2SFieldCopy() {
String msg = '';
List<Account> testAccounts = TestingUtils.createAccounts( NUMBER_TO_CREATE, true );

Test.startTest();
try{
WebS2SServices.upcs(testAccounts);
}
catch(Exception ex){
msg = ex.getMessage();
}
Test.stopTest();

System.assertEquals( NUMBER_TO_CREATE, testAccounts.size(), 'Count Accounts should match created accounts' );
System.assertEquals( 'Custom setting WebS2SFieldCopy__c is not setup', msg, 'We should get corect error message');
}

@isTest static void testUpcsWithoutSalesforceLogin() {
String msg = '';
List<WebS2SFieldCopy__c> webS2SFieldCopy = new List<WebS2SFieldCopy__c>();
for(String key: FIELDNAMES.keySet()){
webS2SFieldCopy.add(new WebS2SFieldCopy__c(Name = key, Partner_Field_Name__c = FIELDNAMES.get(key) ));
}
insert webS2SFieldCopy;

List<Account> testAccounts = TestingUtils.createAccounts( NUMBER_TO_CREATE, true );

Test.startTest();
try{
WebS2SServices.upcs(testAccounts);
}
catch(Exception ex){
msg = ex.getMessage();
}
Test.stopTest();

System.assertEquals( NUMBER_TO_CREATE, testAccounts.size(), 'Count Accounts should match created accounts' );
System.assertEquals( 'Custom setting WebS2SFieldCopy__c does not save SF_Login_Name__c ', msg, 'We should get corect error message');
}
}

We use for testing callouts typical SingleRequestMock


@isTest
public class SingleRequestMock implements HttpCalloutMock, Database.Stateful {
protected Integer code;
protected String status;
protected String bodyAsString;
protected Integer counter;
protected HTTPRequest lastRequest;

public SingleRequestMock( Integer code, String status, String body ) {
this.code = code;
this.status = status;
this.bodyAsString = body;
this.counter = 0;
}

public HTTPResponse respond(HTTPRequest req) {
this.lastRequest = req;
this.counter++;
HttpResponse resp = new HttpResponse();
resp.setStatusCode(code);
resp.setStatus(status);
resp.setBody(bodyAsString);
return resp;
}

public Integer howManyTimesCalled() {
return this.counter;
}

public String getLastRequestEndpoint() {
return this.lastRequest.getEndpoint();
}
}

This is account trigger (custom one)


trigger AccountTrigger on Account (after update) {
WebS2SServices.upf(AccountServices.filterAccountToBeShared(Trigger.new, Trigger.oldMap));
}

 


public class AccountServices {

public static List<Account> filterAccountToBeShared(List<Account> newAccList, Map<Id, Account> oldMap) {
List<Account> filteredList = new List<Account>();
List<String> fields = new List<String>( WebS2SFieldCopy__c.getAll().keySet() );
for ( Account a: newAccList ) {
Account oldOne = oldMap.get( a.Id );
for ( String field: fields ) {
if ( a.get( field ) != oldOne.get(field) ) {
filteredList.add( a );
break;
}
}

}
return filteredList;
}
}

We have some custom object which contains login credentials to the partner org with FF package installed. Also we have custom setting which contains name of the login credentials record with the current security token and list of field names on Account for both current and partner org.
Hope this may be helpful for someone with the same issue.

In this article I have discussed and shown code for sharing (updating) records automatically by trigger. I would like also to discuss another approach which requires manual interaction (clicking button on account record), but probably I would discuss this in the next article.
Please like if you find this post super-awesome and please comment if you have what to say.

Advertisements
This entry was posted in apex, FF Accounting, S2S, SOAP, Webservices, WSDL and tagged , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s