0

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?

1

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.

| improve this answer | |
  • Thanks for pointing it out I have added but the issue persists albeit another so edited my question! – SFDC_Dev Sep 24 '18 at 14:56
  • For Script-thrown exception you'd need to actually look in and see what line it's pointing to and debug from there. I've seen that message come up when my code contains an ApexPages.addMessage for Visualforce errors but it can have other causes too. – Charles T Sep 24 '18 at 15:49
  • Thanks mate, I added debug logs and tweaked my mock class MockHttpResponseGenerator_Ciphr with the expected date and it worked out. – SFDC_Dev Sep 24 '18 at 21:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.