[SalesForce] Script-thrown exception in Test class for Batch apex callout

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('batch@acme.com');
    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?

Best Answer

The line:

global class MockHttpResponseGenerator_Ciphr {

Should be:

global class MockHttpResponseGenerator_Ciphr implements HttpCalloutMock {

You implemented the method but failed to actually declare the interface.

Related Topic