I have a batch class which makes multiple http callouts to an HR system and then with the absences returned checks for existing absences and upserts. However when I try and run test class for the batch class, I get the error "Script-thrown exception"
My batch class (please note I have not included the wrapper calsses CiphrWrapper, Ciphr and CiphrAbsenceDetail)
global class CiphrBatchJob implements Database.Batchable<sObject>, Database.AllowsCallouts{
String query;
//final ID UKLog = Schema.getGlobalDescribe().get('Integration_Log__c').getDescribe().getRecordTypeInfosByName().get('UK Log').getRecordTypeId();
String Endpoint = 'https://****/***/absence?EmployeeNumber=[in]';
String DetailEndpoint;
private String API_Key;
private date cutOffDate= Date.newInstance(2010,01,01); //Change date to current year
global Database.QueryLocator start(Database.BatchableContext bc){
query = 'Select KC_Employee_Reference__c from KimbleOne__Resource__c where KC_Employee_Reference__c <> null';
return Database.getQueryLocator(query);
}
global void execute(Database.BatchableContext BC, List<KimbleOne__Resource__c> scope){
for(Integer i=0;i<scope.size();i++){
if(i==scope.size()-1){
Endpoint += scope[i].KC_Employee_Reference__c;
}else{
Endpoint += scope[i].KC_Employee_Reference__c+',';
}
}
Http http = new Http();
HttpRequest request = new HttpRequest();
API_Key = getApiKey();
request.setEndpoint(Endpoint);
request.setMethod('GET');
request.setHeader('Accept', 'application/json');
request.setHeader('Authorization', 'apikey '+API_key);
request.setTimeout(60000);
CiphrWrapper result;
/*List<Integration_log__c> LogList = new List<Integration_Log__c>();
Integration_log__c log = new Integration_log__c();
log.recordTypeID = UKLog;
log.Job_Name__c = 'Ciphr Get Absence Request';
log.Request_Body__c = 'Get '+Endpoint;*/
try{
HttpResponse response = http.send(request);
result = CiphrWrapper.parse(response.getBody());
/*if(response.getBody().length() >= 131001){
log.Response_Body__c = String.valueOf(response.getBody()).subString(0,131000);
}else{
log.Response_Body__c = response.getBody();
}
log.Status__c = response.getStatus();
log.Status_Code__c = response.getStatusCode();*/
}catch(Exception e){
System.debug('Exception during call--> '+e.getMessage());
//log.Exception__c = e.getMessage();
sendEmail('Call to Ciphr failed','Exception Occurred',e.getMessage(),true,Endpoint);
}
//LogList.add(log);
Map<String,Ciphr.Resource> CRMap = new Map<String,Ciphr.Resource>();
Map<String,Ciphr.AbsenceDetail> CADMap = new Map<String,Ciphr.AbsenceDetail>();
Map<Integer,Ciphr.Absence> CAMap = new Map<Integer,Ciphr.Absence>();
DateTime dt; Date d;
if(result <> null){
//Populate Resources
for(CiphrWrapper.Absence ab: result.Absence){
dt = DateTime.ValueOf(ab.Start.replace('T',' '));
d = Date.newinstance(dT.year(), dT.month(), dT.day());
if(d >= /*System.today()*/ cutOffDate){ //<---- Change Date here
Ciphr.Resource CR = new Ciphr.Resource();
CR.employeeNumber = ab.EmployeeNumber;
CR.absences = new List<Ciphr.Absence>();
CRMap.put(CR.employeeNumber, CR);
}
}
//Populate Absences in Resources
for(CiphrWrapper.Absence ab: result.Absence){
dt = DateTime.ValueOf(ab.Start.replace('T',' '));
d = Date.newinstance(dT.year(), dT.month(), dT.day());
if(CRMap.containsKey(ab.employeeNumber) && d >= /*System.today()*/ cutOffDate){ //<-- Change date here
Ciphr.Absence CA = new Ciphr.Absence();
CA.AbsenceID = Integer.valueOf(ab.ID);
CA.employeeNumber = ab.EmployeeNumber;
CA.absenceDetails = new List<Ciphr.AbsenceDetail>();
CAMap.put(CA.AbsenceID, CA);
CRMap.get(CA.employeeNumber).absences.add(CA);
}
}
System.debug('CRMap: '+CRMap);
//Calling Absences
for(Ciphr.Resource cr: CRMap.values()){
DetailEndpoint = 'https://****.***/***/absenceDetail?AbsenceID=[in]';
Integer i = 0;
for(Ciphr.Absence ca: cr.absences){
if(i==cr.absences.size()-1){
DetailEndpoint += String.valueOf(ca.AbsenceID);
}else{
DetailEndpoint += String.valueOf(ca.AbsenceID)+',';
}
i++;
}
System.debug('Details Endpoint for emp: '+cr.employeeNumber+' is:'+DetailEndpoint);
HttpRequest requestAbsenceDetail = new HttpRequest();
requestAbsenceDetail.setEndpoint(DetailEndpoint);
requestAbsenceDetail.setMethod('GET');
requestAbsenceDetail.setHeader('Accept', 'application/json');
requestAbsenceDetail.setHeader('Authorization', 'apikey '+API_Key);
CiphrAbsenceDetail resultAbsenceDetails;
/*Integration_Log__c DetailLog = new Integration_log__c();
DetailLog.RecordTypeId = UKLog;
DetailLog.Job_Name__c = 'Ciphr Get Absence Detail Request';
DetailLog.Request_Body__c = 'Get '+DetailEndpoint;*/
try{
HttpResponse responseDetails = http.send(requestAbsenceDetail);
resultAbsenceDetails = CiphrAbsenceDetail.parse(responseDetails.getBody());
/*DetailLog.Response_Body__c = responseDetails.getBody();
DetailLog.Status__c = responseDetails.getStatus();
DetailLog.Status_Code__c = responseDetails.getStatusCode();*/
}catch(Exception e){
System.debug('Exception during Absence detail call for EmployeeNumber --> '+cr.employeeNumber+' '+e.getMessage());
//DetailLog.Exception__c = e.getMessage();
sendEmail('SF Ciphr Detail Call failed','Exception Occurred in Detail Call',e.getMessage(),true,DetailEndpoint);
}//LogList.add(DetailLog);
if(resultAbsenceDetails<> null){
for(Ciphr.Absence ca: cr.absences){
for(CiphrAbsenceDetail.AbsenceDetail cad: resultAbsenceDetails.AbsenceDetail){
if(ca.AbsenceID == cad.AbsenceID){
Ciphr.AbsenceDetail cad1 = new Ciphr.AbsenceDetail();
cad1.employeeNumber = ca.employeeNumber;
cad1.AbsenceID = cad.AbsenceID;
cad1.End_Z = cad.End_Z;
cad1.Start = cad.Start;
cad1.ID = Integer.valueOf(cad.AbsenceID)+'_'+Integer.valueOf(cad.ID);
cad1.Hours = cad.Hours;
CADMap.put(cad1.ID, cad1);
CAMap.get(ca.AbsenceID).absenceDetails.add(cad1);
ca.AbsenceDetails.add(cad1);
}
}
}
}
System.debug('CAMap: '+CAMap);
}
System.debug('CRMap: '+CRMap);
}
//Insert Logs //Insert LogList;
//Retrieve current absences greater than the cutoffdate from Kimble
List<KimbleOne__TimeEntryImportLine__c> kimbleCurrentAbsences = [SELECT KimbleOne__ExternalId__c, KimbleOne__Resource__r.KC_Employee_Reference__c, KimbleOne__EntryUnits__c, KimbleOne__EntryDate__c FROM KimbleOne__TimeEntryImportLine__c where KimbleOne__EntryDate__c >=: cutOffDate And KimbleOne__Resource__r.KC_Employee_Reference__c in:CRMap.keyset()];
Map<String, KimbleOne__TimeEntryImportLine__c> KimbleCurrentAbsencesMap = new Map<String, KimbleOne__TimeEntryImportLine__c>();
List<Map<String,Object>> PayLoad = new List<Map<String,Object>>();
if(KimbleCurrentAbsences.size()>0){
for(KimbleOne__TimeEntryImportLine__c kab: kimbleCurrentAbsences){
KimbleCurrentAbsencesMap.put(kab.KimbleOne__ExternalId__c, kab);
}
}
//Check and remove absence details if they exist in Kimble with same absence detail Id and same no. of hours
for(Ciphr.AbsenceDetail cad: CADMap.Values()){
if(KimbleCurrentAbsencesMap.containsKey(cad.id)){
if(cad.hours == KimbleCurrentAbsencesMap.get(cad.id).KimbleOne__EntryUnits__c)
{
CADMap.remove(cad.id);
KimbleCurrentAbsencesMap.remove(cad.id);
}
}
}
//Check if there are any absence details in Kimble but are not Ciphr i.e. absence details that were deleted in Ciphr so add a 0 against the absence deleted in Ciphr
for(KimbleOne__TimeEntryImportLine__c kab: KimbleCurrentAbsencesMap.values()){
if(!CadMap.containsKey(kab.KimbleOne__ExternalId__c)){
Ciphr.AbsenceDetail cad = new Ciphr.AbsenceDetail();
cad.employeeNumber = KimbleCurrentAbsencesMap.get(kab.KimbleOne__ExternalId__c).KimbleOne__Resource__r.KC_Employee_Reference__c;
cad.Start = KimbleCurrentAbsencesMap.get(kab.KimbleOne__ExternalId__c).KimbleOne__EntryDate__c.format();
cad.ID = kab.KimbleOne__ExternalId__c;
cad.Hours = 0;
CADMap.put(kab.KimbleOne__ExternalId__c,cad);
}
}
//Prepare the payload
for(Ciphr.AbsenceDetail cad: CADMap.Values()){
Map<String,Object> MaptoSerialize = new Map<String,Object>{'resource' => cad.employeeNumber,'activity' => 'a5z1w0000004CH5AAM', 'date' => Date.valueOf(cad.start), 'hours' => cad.Hours, 'CIPHR_Id' => cad.ID};
PayLoad.add(MaptoSerialize);
}
String responseOfPayloadPush = SendPayload(PayLoad);
System.debug('*******************Payload******************\n'+json.serializePretty(PayLoad));
System.debug('Response of the payload push: '+responseOfPayloadPush); //Uncomment for testing
//Below only for testing
Attachment att = new Attachment();
att.parentID = '001w000001W37mWAAR'; //Attaches to account "jon_singer_test_UK_SOHO"
att.ContentType = 'text/plain';
att.name = 'Payload '+Date.today()+'.txt';
if(String.valueOf(json.serializePretty(PayLoad)).length()>131000){
att.body = Blob.valueOf(String.valueOf(json.serializePretty(PayLoad)).substring(0,131000));
}else{
att.body = Blob.valueOf(String.valueOf(json.serializePretty(PayLoad)));
}
insert att;
Attachment att1 = new Attachment();
att1.parentID = '001w000001W37mWAAR'; //Attaches to account "jon_singer_test_UK_SOHO"
att1.ContentType = 'text/plain';
att1.name = 'Payload Push Response'+Date.today()+'.txt';
att1.body = Blob.valueOf(responseOfPayloadPush);
insert att1;
//Above for Testing only
}
global void finish(Database.BatchableContext BC){
sendEmail('SF Ciphr Records Processed','SF Ciphr Batch Process Completed','SF Ciphr Batch Process Completed.',false,'');
}
private String getApiKey(){
Ciphr_Kimble__c ck = Ciphr_Kimble__c.getInstance();
String key = ck.EncryptionKey__c;
String APIKey = ck.EncryptedCiphrKey__c;
Blob decrypted = Crypto.decryptWithManagedIV('AES128', EncodingUtil.base64Decode(key), EncodingUtil.base64Decode(APIKey));
APIKey = decrypted.toString();
//System.debug('API Key getApiKey '+APIKey);
return APIKey;
}
private void sendEmail(string displayName, string subject, String messageBody, boolean exceptionOccurred, String endpoint){
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(new String[] {'****@***.***'});
//mail.setToAddresses(new String[] {'*****@****.***'});
mail.setReplyTo('[email protected]');
mail.setSenderDisplayName(displayName);
mail.setSubject(subject);
if(exceptionOccurred){
mail.setPlainTextBody('Exception: '+messageBody+'\n\nFor URL: '+endpoint);
}else{
mail.setPlainTextBody(messageBody);
}
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
private String SendPayload(List<Map<String,Object>> payload){
String sfdcURL = URL.getSalesforceBaseUrl().toExternalForm();
String restAPIURL = sfdcURL + '/services/apexrest/***/**/****/***';
HttpRequest httpRequest = new HttpRequest();
httpRequest.setEndpoint(restAPIURL);
httpRequest.setMethod('POST');
httprequest.setHeader('Content-Type', 'application/json');
httprequest.setHeader('Accept','application/json');
httpRequest.setHeader('Authorization', 'OAuth ' + UserInfo.getSessionId());
httpRequest.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionID());
httpRequest.setBody(json.serializePretty(PayLoad));
String response = '';
try {
Http http = new Http();
HttpResponse httpResponse = http.send(httpRequest);
System.debug('>> Response of payload >> '+httpResponse.getStatusCode());
response = 'Status Code: '+httpResponse.getStatusCode()+' Response: '+httpResponse.getBody();
} catch(Exception e) {
System.debug('ERROR: '+ e.getMessage());
response = e.getMessage();
sendEmail('Payload Sending to Kimble failed','Exception Occurred Payload Push',e.getMessage(),true,restAPIURL);
}
System.debug(' ** response ** : ' + response );
return response;
}}
My Test class
@isTest(SeeAllData = True) //In progress
public class CiphrBatchJobTest {
static testmethod void doTest(){
Test.startTest();
CiphrBatchJob c = new CiphrBatchJob();
Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator_Ciphr());
ID BatchProcessID = Database.executeBatch(c,50);
Test.stopTest();
}
}
My http mock class:
@isTest
global class MockHttpResponseGenerator_Ciphr implements HttpCalloutMock{
// Implement this interface method
global HTTPResponse respond(HTTPRequest req) {
// Create a fake response
if(req.getBody().contains('EmployeeNumber')){
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
res.setBody('{"Absence": [{"ID": "1463","Start": "1995-08-08T00:00:00","End": "1995-08-20T00:00:00","EmployeeNumber": "10187","Forenames": "Jonathan","Surname": "Christopher"}]}');
res.setStatusCode(200);
return res;
}else if(req.getBody().contains('AbsenceID')){
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
res.setBody('{"AbsenceDetail": [{"AbsenceID": "1463","Start": "1995-08-08T00:00:00","End": "1995-08-20T00:00:00","ID": 8763 ,"Hours": 7.40}]}');
res.setStatusCode(200);
return res;
}else{
return null;
}
}
}
I know that seeAllData=true is a bad practice but I am not able to insert into the KimbleOne__Resource__c object and subsequently KimbleOne__TimeEntryImportLine__c as these are 3rd party objects and we were advised not to insert as there are 30+ objects that would need inserted etc (basically it's too complex)
Are you able to help please, as I'm unsure on how to go about it?