• Nicholas Sewitz 9
  • NEWBIE
  • 0 Points
  • Member since 2016

  • Chatter
    Feed
  • 0
    Best Answers
  • 0
    Likes Received
  • 0
    Likes Given
  • 4
    Questions
  • 3
    Replies
Hi!

I have an child object `Lifecycle_History__c` to `Contact`. Everytime the field`Contact.lifecycle__c` is changed I create a new `Lifecycle_History__c` object that captures some data from the `Contact`. Essentially it is a snapshot object for reporting purposes. 

I wrote some Apex for this and when I change the lifecycle on a contact in the sandbox everything works as expected. For example if a create a new contact with a `lifecycle__c` then there should be 1 related `Lifecycle_History__c` object.  If I edit that contact and change the `lifecycle__c` field, then there are 2 `Lifecycle_History__c` objects.

However in my Apex Test `testcontactUpdate()` my assertion that there should be 3 `Lifecycle_History__c` is failing and saying there is only 1. When I debug I can see that when I run the test it's the `lifecycle__c` on `c` is not updating even after I call update and in turn not triggering the creation of a new `Lifecycle_History__c`.

Apex Trigger:
 
trigger ContactTrigger on Contact (after insert, after update,after delete){
    TriggerSupport__c  objCustomSetting= TriggerSupport__c.getInstance('ContactTrigger');
    if(objCustomSetting.TriggerOff__c == false) {
        return;
    }
    else if(Trigger.isAfter)
    {
        if(Trigger.isUpdate){
            if(LeadTriggerHandler.isFirstTimeAfter){
                LeadTriggerHandler.isFirstTimeAfter = false;
                System.debug('inafterupdatetrigger');
                ContactTriggerHandler.afterUpdate(Trigger.New, Trigger.oldMap); }
        }
        else if(Trigger.isInsert){
            ContactTriggerHandler.afterInsert(Trigger.New);
        }
        else if (Trigger.isDelete){
            ContactTriggerHandler.afterDelete(Trigger.Old);
        }
    }
}



Apex Class:
public with sharing class ContactTriggerHandler {
 //   public static Boolean isFirstTimeAfter = true;
/**
* after delete handler for merging patrons
 */  
    
    public static void afterDelete(List<Contact> deletedcontacts) {
        Map<Id,Contact> contactidmap = new Map<Id,Contact>();    // this is a map of deleted contact id to merged contact map

        
        
        for (Contact c : deletedcontacts) {
            
            Database.DMLOptions dlo = new Database.DMLOptions();
            dlo.EmailHeader.triggerUserEmail = false;
            dlo.EmailHeader.triggerAutoResponseEmail = false;
            c.setOptions(dlo);   
            
            
            if (c.MasterRecordId != null) {
                // this is a merge, setup a map of old, new id
                contactidmap.put(c.MasterRecordId,c);
                System.debug('contact map' + contactidmap);
                System.debug('contact id' + c.Id );
                System.debug('master record' + c.MasterRecordId);
            }
        }
        
        if (!contactidmap.keySet().isEmpty()) {
            // now loop through all the patrons looking up to the old contact and reassign to new contact
            List<Patron__c> patlist = [select Id,Contact__c from Patron__c where Contact__c in :contactidmap.keySet()];
           // List<Lifecycle_History__c> mqllist = [select Id,Contact__c from Lifecycle_History__c where Contact__c in :contactidmap.keySet()];

            
            for (Patron__c pat : patlist) {
                
                Database.DMLOptions dlo = new Database.DMLOptions();
                dlo.EmailHeader.triggerUserEmail = false;
                dlo.EmailHeader.triggerAutoResponseEmail = false;
                pat.setOptions(dlo);   

                pat.Contact__c = contactidmap.get(pat.Contact__c).MasterRecordId;
               
            }
            
         /*   for (Lifecycle_History__c mql : mqllist) {
                
                Database.DMLOptions dlo = new Database.DMLOptions();
                dlo.EmailHeader.triggerUserEmail = false;
                dlo.EmailHeader.triggerAutoResponseEmail = false;
                mql.setOptions(dlo);   

                mql.Contact__c = contactidmap.get(mql.Contact__c).MasterRecordId;
               
            }*/
            System.debug('patron list ' + patlist);
            update patlist;
           // System.debug('patron list ' + mqllist);
        //    update mqllist;
            dupcheck.dc3Api api = new dupcheck.dc3Api(); 
            api.domerge(patlist);
    }
    }
   
   
   
    /**
* after insert handler for attaching patron and applications
* @param newcons [description]
*/
    public static void afterInsert(List<Contact> newcons) {
        Set<Id> cons = new Set<Id>();    // contacts
        for (Contact c : newcons) {
            cons.add(c.Id);
        }
        
        // query more information on contacts for patron
        // loop through contacts and create patrons
        List<Patron__c> newpatlist = new List<Patron__c>();
        List<Lifecycle_History__c> newlifecyclehistorylist = new List<Lifecycle_History__c>();
        for (Contact c : [Select Id,
                          Marketing_Qualified_Detail_Most_Recent__c,
                          Marketing_Qualified_Reason_Most_Recent__c,
                          Datetime_Marketing_Qualified_Most_Recent__c,
                          Datetime_Sales_Accepted_Most_Recent__c,
                          Datetime_Sales_Qualified_Most_Recent__c,
                          Datetime_Recycled_Most_Recent__c,
                          Datetime_Disqualified_Most_Recent__c,
                          Datetime_Won_Most_Recent__c,
                          Datetime_Lost_Most_Recent__c,
                          Recycled_Reason__c,
                          Recycle_Until_Date__c,
                          Disqualified_Reason__c,
                          Account.AccountSource,
                          utm_source_Most_Recent__c,
                          utm_campaign_Most_Recent__c,
                          utm_content_Most_Recent__c,
                          utm_medium_Most_Recent__c,
                          utm_term_Most_Recent__c,
                          OwnerId,
                          Email,
                          Phone,
                          Count_of_Patron_Contacts__c,
                          Created_via_Conversion_Process__c,
                          lifecycle__c,
                          Abbreviated_Channel__c,
                          Anything_else__c,
                          Application_Mode__c,
                          Account.Name,
                          How_did_you_learn_about_Artsy__c,
                          Interests__c,
                          AccountId,
                          Learned_from_detail__c,                                                                   
                          Name,
                          Datetime_of_Partnership_Application__c
                          from Contact where Id in :cons])
            if (c.Created_via_Conversion_Process__c == FALSE) {
               
                Patron__c pat = new Patron__c();
                PatronUtil.populatePatronfromContact(pat, c);
                newpatlist.add(pat);
                
            if(c.lifecycle__c != null) {
                    Lifecycle_History__c lifecyclehistory = new Lifecycle_History__c();
                    LifecycleUtil.populatelifecyclefromContact(lifecyclehistory, c);
                    newlifecyclehistorylist.add(lifecyclehistory);
                }
        }
        
        if (!newpatlist.isEmpty()) {
            insert newpatlist;
        }
        
        if (!newlifecyclehistorylist.isEmpty()) {
            insert newlifecyclehistorylist;
            System.debug('inclassinsert');
            System.debug(JSON.serializePretty(newlifecyclehistorylist));
        }
        
    }
    
    /*
* update children patron records after contact updates
* @param newconmap [description]
* @param oldmap [description]
*/
    public static void afterUpdate(List<Contact> updatedcons, Map<Id,Contact> oldmap) {
        System.debug('inafterupdatehandler');
        // select all necessary data
        Map<Id, Contact> updatedconmap = new Map<Id,Contact>([Select  Id,
                                                                      Marketing_Qualified_Detail_Most_Recent__c,
                                                                    Marketing_Qualified_Reason_Most_Recent__c,
                                                                    Datetime_Marketing_Qualified_Most_Recent__c,
                                                                    Datetime_Sales_Accepted_Most_Recent__c,
                                                                    Datetime_Sales_Qualified_Most_Recent__c,
                                                                    Datetime_Recycled_Most_Recent__c,
                                                                    Datetime_Disqualified_Most_Recent__c,
                                                                      Datetime_Won_Most_Recent__c,
                                                                      Datetime_Lost_Most_Recent__c,
                                                                    Recycled_Reason__c,
                                                                      Recycle_Until_Date__c,
                                                                    Disqualified_Reason__c,
                                                                    Account.AccountSource,
                                                                    utm_source_Most_Recent__c,
                                                                    utm_campaign_Most_Recent__c,
                                                                    utm_content_Most_Recent__c,
                                                                    utm_medium_Most_Recent__c,
                                                                    utm_term_Most_Recent__c,
                                                                    OwnerId,
                                                                    Email,
                                                                    Phone,
                                                                    Count_of_Patron_Contacts__c,
                                                                      Created_via_Conversion_Process__c,
                                                                    lifecycle__c,
                                                                    Abbreviated_Channel__c,
                                                                    Anything_else__c,
                                                                    Application_Mode__c,
                                                                    Account.Name,
                                                                    How_did_you_learn_about_Artsy__c,
                                                                    Interests__c,
                                                                    AccountId,
                                                                    Learned_from_detail__c,                                                                   
                                                                    Name,
                                                                    Datetime_of_Partnership_Application__c
                                                                    from Contact where Id in :updatedcons]);

        // find patrons
        List<Patron__c> patstoupdate = [Select Contact__c from Patron__c where Contact__c in :updatedconmap.keySet()];
        
        for (Patron__c pat : patstoupdate) {
            
            PatronUtil.populatePatronfromContact(pat, updatedconmap.get(pat.Contact__c));
        }
        
        update patstoupdate;
       
        
        for (Contact c : updatedcons) {         
            System.debug('inafterupdatehandlerloopbeginning');
            System.debug(c.lifecycle__c);
            System.debug(oldMap.get(c.Id).lifecycle__c);
            if (c.lifecycle__c != oldMap.get(c.Id).lifecycle__c) {
                // find lifecycles
                List<Lifecycle_History__c> lifecyclestoinsert = new List<Lifecycle_History__c>();
                System.debug('inafterupdatehandlerloop');
                for (Id contactid : updatedconmap.keySet()) {
                    Lifecycle_History__c lifecyclehistory = new Lifecycle_History__c();
                    LifecycleUtil.populateLifecyclefromContact(lifecyclehistory, updatedconmap.get(contactid));
                    lifecyclestoinsert.add(lifecyclehistory);
                    System.debug('inafterupdatehandlerloop2');
                }
                if (!lifecyclestoinsert.isEmpty()) {
                    insert lifecyclestoinsert;
                    System.debug('inclassupdate');
                    System.debug(JSON.serializePretty(lifecyclestoinsert));
                        
                }
            }
            
        }             
    }
}


Apex Test:
@isTest
private class ContactTriggerHandlerTest {
    
        @isTest static void testContactUpdate() {
        // Create custom setting
        TriggerSupport__c ts = new TriggerSupport__c();
        ts.Name='ContactTrigger';
        ts.TriggerOff__c= TRUE; 
        insert ts;
        Account a = new Account(Name='my account');
        insert a;
        Contact c = new Contact(FirstName='test',LastName='contact',AccountId=a.Id, Email='test@testdlkfjsdlkfj.com', Phone='8189432003', Abbreviated_Channel__c='Inbound ', lifecycle__c='Marketing Qualified', Created_via_Conversion_Process__c=False,Datetime_of_Partnership_Application__c=Date.today(), DateTime_Marketing_Qualified_Most_Recent__c=Date.today());
        insert c;
        
        Test.startTest();
        c.lifecycle__c = 'Sales Accepted';
        update c;
        Test.stopTest();
        List<Lifecycle_History__c> lifecyclelist1 = [SELECT Id, Contact__c FROM Lifecycle_History__c];
        List<Contact> contactlist1 = [SELECT Id, lifecycle__c FROM Contact];
        System.debug(c.lifecycle__c);
        System.debug(c.Id);            
        System.debug(JSON.serializePretty(lifecyclelist1));
        System.debug(JSON.serializePretty(contactlist1));
        c.lifecycle__c = 'Known';
        update c;
        System.debug(c.lifecycle__c);
        

            
        List<Lifecycle_History__c> lifecyclelist = [SELECT Id FROM Lifecycle_History__c WHERE Contact__c = :c.Id]; 
        System.assertEquals(3,lifecyclelist.size());
    } 
}
Hi I have a batch matching job running in salesforce. It was working and then I did something and now it has stopped functioning as I intended. I've tried debugging but for the life of me can't figure out what is going on. The goal of the script is to take one list of opportunities that match a certain criteria and another list of opportunities on the same account that match another criteria, map them, and then merge them.

It seems like it properly brings in the two lists of opportunities but when it comes time to match and merge it works on 1 mapped records in the entire list. So If there are 12 opps in each list, one of them will be merge the rest will remain untouched.
 
/**
       * called by AccountOpptyMatchingScheduler scheduler to perform opportunity matching and consolidation
       *
       * 1. get all the opportunities inserted by redshift today (with subscription id) and is not a matched opportunity (Matched_Opportunity__c = false)
       * 2. look for opportunities under the same account without subscription id (salesforce manually inserted)
       * 3. sf opportunity with the highest attempt__c and most recent createdDate and same Type and same recordtype will be the match
       * 4. if sf oppty with highest attempt__c is not the most recent or most recent does not have highest attempt, email nicholas
       * 5. otherwise, perform merge
       */
      global class OpportunityMatchingBatch implements Database.Batchable<sObject>, Database.Stateful {
          String query;
          List<Opportunity> matchedopptys;    // holds all the oppty matched today
          List<String> erroropptys;    // holds all the opptys that isn't highest attempt or most recent

          Date cdate;

          global OpportunityMatchingBatch() {
              this(null);
          }

          global OpportunityMatchingBatch(Date cdate) {
              this.cdate = cdate;
              String datestr = 'CreatedDate = LAST_N_DAYS:14';
              if (cdate != null) datestr = 'CreatedDate >= :cdate';

       /* This was what it was like before tried to slim down....
       * 
       * Date_of_Subscription_Start__c,Date_of_Subscription_End__c,'
                  + 'Total_Value__c,Virtual_MRR__c,Is_Last_Subscription__c,Active_Subscription_Number__c,'
                  + 'Number_of_Failed_Charges__c,Relative_Size__c,Is_Active_Subscription__c,Type,'
                  + 'Duration__c,Payment_Frequency__c,Ended_Early__c,Months_Paid_Up_Front__c,Total_Charges__c,'
                  + 'Last_Subscription_MRR__c,Partner_Slug__c,Pending_Charges__c,Pending_Charges_Dollar_Value__c,'
                  + 'Completed_Charges__c,Completed_Charges_Dollar_Value__c,Partner_Subscription_ID__c,RecordTypeId '
      */
              query = 'SELECT AccountId,Name,Type,RecordTypeId,Partner_Subscription_ID__c '
                  + 'FROM Opportunity WHERE ' + datestr + ' AND Partner_Subscription_ID__c <> null AND Matched_Opportunity__c = false';
              matchedopptys = new List<Opportunity>();
              erroropptys = new List<String>();
              System.debug(Logginglevel.INFO, 'Step 1');
          }

          global Database.QueryLocator start(Database.BatchableContext BC) {
              System.debug(Logginglevel.INFO, 'Step 2');
              return Database.getQueryLocator(query);
              
          }

          /**
           * record comes in one at a time
           */
          global void execute(Database.BatchableContext BC, List<Opportunity> opplist) {
              Opportunity redshifta = opplist[0];
              System.debug(Logginglevel.INFO, 'Step 3');

              // should only match with the highest attempt and most recently created sf opportunity
              for (Opportunity opp : [SELECT Name, CreatedDate, AccountId FROM Opportunity
                  WHERE AccountId = :redshifta.AccountId AND Partner_Subscription_ID__c = null ORDER BY CreatedDate DESC LIMIT 1]) {

                  // got here, it's okay to write soql in here because we are only matching with one opportunity, there won't be a loop
                  // get the highst attempt and most recent createddate from this account
                  //String highestattempt = [SELECT Attempt__c FROM Opportunity WHERE AccountId = :opp.AccountId AND Type = :redshifta.Type AND RecordTypeId // = :redshifta.RecordTypeId AND Partner_Subscription_ID__c = null ORDER BY Attempt__c DESC LIMIT 1].Attempt__c;
                  Datetime mostrecentdate = [SELECT CreatedDate FROM Opportunity WHERE AccountId = :opp.AccountId AND Type = :redshifta.Type AND Partner_Subscription_ID__c = null ORDER BY CreatedDate DESC LIMIT 1].CreatedDate;

                  // only merge if it is highest attempt and most recent
                  if (mostrecentdate == opp.CreatedDate) {
                      // create match result reason
                      String matchedon = null;

                      opp.Matching_Result__c = matchedon;

                      // merge fields
                      opp.Name = redshifta.Name;
                      /*opp.Date_of_Subscription_Start__c = redshifta.Date_of_Subscription_Start__c;
                      opp.Date_of_Subscription_End__c = redshifta.Date_of_Subscription_End__c;
                      opp.Total_Value__c = redshifta.Total_Value__c;
                      opp.Virtual_MRR__c = redshifta.Virtual_MRR__c;
                      opp.Is_Last_Subscription__c = redshifta.Is_Last_Subscription__c;
                      opp.Active_Subscription_Number__c = redshifta.Active_Subscription_Number__c;
                      opp.Number_of_Failed_Charges__c = redshifta.Number_of_Failed_Charges__c;
                      opp.Relative_Size__c = redshifta.Relative_Size__c;
                      opp.Is_Active_Subscription__c = redshifta.Is_Active_Subscription__c;
                      opp.Type = redshifta.Type;
                      opp.Duration__c = redshifta.Duration__c;
                      opp.Payment_Frequency__c = redshifta.Payment_Frequency__c;
                      opp.Ended_Early__c = redshifta.Ended_Early__c;
                      opp.Months_Paid_Up_Front__c = redshifta.Months_Paid_Up_Front__c;
                      opp.Total_Charges__c = redshifta.Total_Charges__c;
                      opp.Last_Subscription_MRR__c = redshifta.Last_Subscription_MRR__c;
                      opp.Partner_Slug__c = redshifta.Partner_Slug__c;
                      opp.Pending_Charges__c = redshifta.Pending_Charges__c;
                      opp.Pending_Charges_Dollar_Value__c = redshifta.Pending_Charges_Dollar_Value__c;
                      opp.Completed_Charges__c = redshifta.Completed_Charges__c;
                      opp.Completed_Charges_Dollar_Value__c = redshifta.Completed_Charges_Dollar_Value__c;*/
                      opp.Partner_Subscription_ID__c = redshifta.Partner_Subscription_ID__c;
                      opp.Matched_Opportunity__c = true;

                      update opp;
                      delete redshifta;
                      matchedopptys.add(opp);
                  } else {
                      // error
                      erroropptys.add('SF Name: ' + opp.Name + ', SF Id: ' + opp.Id + ', Partner Sub Name: ' + redshifta.Name + ', Redshift Id: ' + redshifta.Id
                          + ' SF CreatedDate: ' + opp.createdDate.format('MM/dd/YYYY HH:mm:ss')
                          + ' Most Recent CreatedDate: ' + mostrecentdate.format('MM/dd/YYYY HH:mm:ss'));
                  }

              }

          }

          /**
           * after batch is done, email nicholas two reports, one is the opportunity ids merged, one is the error report
           * @param  BC [description]
           * @return    [description]
           */
          global void finish(Database.BatchableContext BC) {
              List<Messaging.SingleEmailMessage> emailList = new List<Messaging.SingleEmailMessage>();

              // send matched opportunity email
              if (!matchedopptys.isEmpty()) {
                  Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                  mail.setToAddresses(new String[] {'nicholas@artsy.net','matt@gsdcompany.com'});
                  mail.setSenderDisplayName('Artsy Matching Agent');
                  mail.setUseSignature(false);
                  mail.setSubject('Opportunity Matching Result');
                  String body = Datetime.now().format('MM/dd/YYYY') + ' opportunity match result:\n\n';
                  for (Opportunity opp : matchedopptys) {
                      body += 'Name: ' + opp.Name + ' | SF ID: ' + opp.Id + ' | Partner Subscription ID: ' + opp.Partner_Subscription_ID__c + '\n';
                  }
                  mail.setPlainTextBody(body);
                  emailList.add(mail);
              }

              // send error email
              if (!erroropptys.isEmpty()) {
                  Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                  //'nicholas@artsy.net',
                  mail.setToAddresses(new String[] {'nicholas@artsy.net','matt@gsdcompany.com'});
                  mail.setSenderDisplayName('Artsy Matching Agent');
                  mail.setUseSignature(false);
                  mail.setSubject('Opportunity Match Errors (Not Highest Attempt or Not Most Recent');
                  String body = Datetime.now().format('MM/dd/YYYY') + ' Opportunity Match Errors (Not Highest Attempt or Not Most Recent):\n\n';
                  for (String e : erroropptys) {
                      body += e + '\n\n';
                  }
                  mail.setPlainTextBody(body);
                  emailList.add(mail);
              }

              Messaging.sendEmail(emailList);
          }
      }

 
Hey I am trying to write a test class for my google Authentication Controller. Even though it compiles when I try and run it, it doesn't work. I may be way off here to begin with if I could please get some help.

Controller Class
 
public with sharing class googleAuthorization_Controller {
	public string googleEmail {get;set;}
	//to store our code for dynamic rendering
	public string code {get;set;} 
	//to store our user record
	public User u {get;set;}
	public googleAuthorization_Controller() {
		googleEmail = userInfo.getUserEmail();
	}

	//page action
	public pagereference doOnLoad(){
		//retrieve current page
		Pagereference p = ApexPages.currentPage();
		//does it have a code as parameter?
		code = p.getParameters().get('code');
		//no? then stop
		if (string.isBlank(code)) return null;
		//it had one! get the state, aka email we passed
		//note you don't want to use googleEmail here
		//since we came back to the page, it reloaded and
		//the controller was reinstantiated, overriding our
		//input with the user's email
		string passedEmail = p.getParameters().get('state');

		//query for the user, with token fields so we can modify
		u = [select id, Google_Access_Token__c, Google_Refresh_Token__c from User where id = :userInfo.getUserId()];

		//call our api method to get tokens parsed into user
		u = googleCalendar_API.obtainAccessToken(u, code, googleCalendar_API.SF_AUTH_PAGE);

		//if we had no error
		if (u.Google_Access_Token__c != 'error'){
			//set the google email
			u.google_email__c = passedEmail;
			//update the user and display success message
			update u;
			ApexPages.addMessage(new ApexPages.message(ApexPages.severity.confirm,'Authorized Successfully!'));
		}
		else{
			//had an error? well then let us know <sadface>
			ApexPages.addMessage(new ApexPages.message(ApexPages.severity.error,'Authorization Error.'));	
		}
		//stay here, not going anywhere!
		return null;
	}

	public pagereference requestAuthorization(){
		return googleCalendar_API.loginRequestPage(
			googleCalendar_API.SF_AUTH_PAGE,
			googleEmail);
	}
}

Test Controller
 
@istest
public class googleControllerTest {

	
    public String body { get; set; }
    public String method { get; set; }
    public String postParam { get; set; }
    public String url { get; set; }

    public String message { get; set; }
    
    public List<SelectOption> methodList { 
    	get {
    		if(methodList==null) {
    			methodList = new List<SelectOption>();
    			methodList.add(new SelectOption('GET','GET'));    			
    			methodList.add(new SelectOption('POST','POST'));
    			methodList.add(new SelectOption('PUT','PUT'));
    		}
    		return methodList;
    	}
    	set;
    }
    private Map<String,User> oauthServices {
    	get {
    		if(oauthServices==null) {
    			oauthServices = new Map<String,User>(); 
    			for(User u : 
    					[ SELECT name, id, Google_Access_Token__c, Google_Refresh_Token__c
    					 FROM User]) {
    				oauthServices.put(u.name,u);
    			}
    		}
    		return oauthServices;
    	}
    	set;
    }

    public String selectedService { 
    	get {
    		if(selectedService==null && oauthServices.size()>0) {
    			selectedService = oauthServices.values()[0].name;
    		}
    		return selectedService;
    	}
    	set; 
    }

    public List<SelectOption> services {
        get {
            services = new List<SelectOption>();
	        for(User obj : oauthServices.values()) {
                services.add(new SelectOption(obj.name,obj.name));
            }
            return services;
        }
        set;
    }

    public PageReference execute() {
        System.debug('Method: '+method+', Service: '+selectedService+'. URL: '+url);
        Http h = new Http();
        HttpRequest req = new HttpRequest();
        req.setMethod(method);
        req.setEndpoint(url);
        if(method=='POST' || method=='PUT') {
        	if(postParam!=null & postParam!='') {
	        	req.setBody(postParam);
				req.setHeader('Content-Type','application/x-www-form-urlencoded');
			} else {
				req.setBody(body);
			}
        }
        System.debug('Sending request...');
        HttpResponse res = h.send(req);
        body = res.getBody();
        System.debug('Received response ('+res.getStatusCode()+' '+res.getStatus()+')');
        message = '';
        return null;
    }
}

 
Hey I am trying to implement a batch system that sends salesforce events to google calendar's api. I have successfully implemented this process in sandbox but am having trouble getting code coverage.

Below is my Callout Class followed by my batch class as well as my google api authorization controller. I have test coverage for none. At the bottom is my attempt at writing test coverage which essentially follows Salesforce's documentation. I seem to be having particular trouble because my HTTP CALLOUT is a POST.


Callout Class
 
public with sharing class googleCalendar_API {

/********************** START CONSTANTS ***************************/
static String GOOGLE_API_CLIENT_ID = '555540635024-5kincbt5uhpfh4g8faq6atmj4hmmbb3h.apps.googleusercontent.com';
static String GOOGLE_API_CLIENT_SECRET = 'W5G3H0qkpNi0ac1kvfsOzkWK';

static String GOOGLE_CALENDAR_SCOPE = 'https://www.googleapis.com/auth/calendar';
static String GOOGLE_CALENDAR_BASE_URL = 'https://www.googleapis.com/calendar/v3/calendars/';
static String GOOGLE_CALENDAR_EVENTS_PATH = '/events';

public static String SF_AUTH_PAGE =
    'https://-------artdev--c.cs62.visual.force.com/apex/googleAuthorization';

static Map<String,String> operationMap = new Map<String,String>{'INSERT'=>'POST','UPDATE'=>'PATCH','DELETE'=>'DELETE'};
static map<id,User> userMap = new map<id,User>([select id, name, google_Email__c, Google_Access_Token__c, Google_Refresh_Token__c from User where isActive=true]);

//carriage return
static String cr = '\r\n';
/********************** END CONSTANTS ***************************/

static TimeZone tz = UserInfo.getTimeZone();
public static String convertDateTimeToString(DateTime dt){
    Integer x = tz.getOffset(dt)/3600000;
    String z = '';
    if ( x > 0 ) z += '+';
    else z += '-';

    if ( x > 9 || x < -9 ) z += math.abs(x);
    else z += '0'+math.abs(x);

    z += ':00';

    return dt.format('yyyy-MM-dd\'T\'HH:mm:ss'+z);
}

public static httpResponse callGoogle(String endpoint, String method, String body){
    HttpRequest req = new HttpRequest();
    req.setEndpoint(endpoint);
    req.setMethod(method);
    req.setCompressed(false);
    req.setHeader('User-Agent','learnApex API');
    req.setHeader('Encoding','iso-8859-1');
    req.setHeader('Content-Type','application/x-www-form-urlencoded');
    req.setTimeout(120000);
    if( body != null ){
        req.setBody(body);
        req.setHeader('Content-length',string.valueOf(body.length()));
    }
    HttpResponse res = new http().send(req);

    system.debug(res.getBody());
    return res;
}

public static User parseGoogleAuth(String body, User u){
    jsonParser parser = json.createParser(body);
    while ( parser.nextToken() != null ){
        if ( parser.getCurrentToken() == JSONToken.FIELD_NAME && parser.getText() != null && parser.getText() == 'access_token' ){
            parser.nextToken();
            u.Google_Access_Token__c = parser.getText();
        } else
        if ( parser.getCurrentToken() == JSONToken.FIELD_NAME && parser.getText() != null && parser.getText() == 'refresh_token' ){
            parser.nextToken();
            u.Google_Refresh_Token__c = parser.getText();
        }
    }
    return u;
}

public static PageReference loginRequestPage
    (String redirectURI, String state){
        PageReference p =
            new PageReference('https://accounts.google.com/o/oauth2/auth');
    p.getParameters().put('response_type','code');  //Determines if the Google Authorization Server returns an authorization code (code), or an opaque access token (token)
    p.getParameters().put('client_id',GOOGLE_API_CLIENT_ID);
    p.getParameters().put('redirect_uri',redirectURI);
    p.getParameters().put('approval_prompt','force');
    p.getParameters().put('scope',GOOGLE_CALENDAR_SCOPE);
    p.getParameters().put('state',state);   //This optional parameter indicates any state which may be useful to your application upon receipt of the response. The Google Authorization Server roundtrips this parameter, so your application receives the same value it sent. Possible uses include redirecting the user to the correct resource in your site, nonces, and cross-site-request-forgery mitigations.
    p.getParameters().put('access_type','offline');
    return p;
}

public static User obtainAccessToken(User u, String code, String redirectURL){
    PageReference p = new PageReference('https://accounts.google.com/o/oauth2/auth');
    p.getParameters().put('client_id',GOOGLE_API_CLIENT_ID);
    p.getParameters().put('client_secret',GOOGLE_API_CLIENT_SECRET);
    p.getParameters().put('scope','');
    p.getParameters().put('redirect_uri',redirectURL);
    p.getParameters().put('grant_type','authorization_code');
    p.getParameters().put('code',code);
    String body = p.getURL();
    body = body.subStringAfter('?');
    httpResponse googleAuth = callGoogle('https://accounts.google.com/o/oauth2/token','POST',body);
    if ( googleAuth.getStatusCode() == 200 ){
        u = parseGoogleAuth(googleAuth.getBody(), u);
    }
    else u.Google_Access_Token__c ='error';
    return u;
}

public static User refreshToken(User u){
    PageReference p = new PageReference('https://accounts.google.com/o/oauth2/auth');
    p.getParameters().put('client_id',GOOGLE_API_CLIENT_ID);
    p.getParameters().put('client_secret',GOOGLE_API_CLIENT_SECRET);
    p.getParameters().put('refresh_token',u.Google_Refresh_Token__c);
    p.getParameters().put('grant_type','refresh_token');
    String body = p.getURL();
    body = body.subStringAfter('?');
    httpResponse googleAuth = callGoogle('https://accounts.google.com/o/oauth2/token','POST',body);
    if ( googleAuth.getStatusCode() == 200 ){
        u = parseGoogleAuth(googleAuth.getBody(), u);
    }
    return u;
}

public class calloutWrapper{
    public String body {get;set;}
    public String endpoint {get;set;}
    public String googleCalendarEmail {get;set;}
    public String googleEventId {get;set;}
    public String method {get;set;}
    public String ownerName {get;set;}
    public Id salesforceEventId {get;set;}
    public Id salesforceOwnerId {get;set;}

    public calloutWrapper(Event e){
        ownerName = usermap.get(e.OwnerId).Name;
        googleCalendarEmail = usermap.get(e.ownerid).google_Email__c;
        salesforceOwnerId = e.OwnerId;
        salesforceEventId = e.Id;
        if ( string.isNotBlank(e.Google_Id__c) ){
            googleEventId = e.Google_Id__c;
        }
        body = compileBodyFromEvent(e);
    }
}

public static String compileBodyFromEvent(Event e){
    //we’re building a JSON body manually!
    String body = '{'+cr+' "end": {'+cr;
    if (e.isalldayevent){
        body += ' "date": "'+ e.StartDateTime.formatgmt('yyyy-MM-dd') +'"'+cr;
    }
    else {
        body += ' "dateTime": "'+ convertDateTimeToString(e.EndDateTime) +'"'+cr;
    }
    body += ' },'+cr+' "start": {'+cr;
    if (e.isalldayevent){
        body += ' "date": "'+ e.StartDateTime.formatgmt('yyyy-MM-dd') +'"'+cr;
    }
    else{
        body += ' "dateTime": "'+ convertDateTimeToString(e.StartDateTime) +'"'+cr;
    }
    body += ' },'+cr;
    if ( string.isNotBlank(e.Subject) ){
        body += ' "summary": "'+ e.Subject +'",'+cr;
    }
    if ( string.isNotBlank(e.Description) ){
        body += ' "description": "'+ e.Description.replace('\n','\\n').replace('\r','\\r') +'",'+cr;
    }
    if ( string.isNotBlank( e.Location ) ){
        body += ' "location": "'+ e.Location +'",'+cr;
    }
    //we've been blindly adding returns
    body = body.subStringBeforeLast(',');
    body += '}'+cr;
    return body;
}

public static void processEventList(list<Event> eventList, boolean deleting){
    //generate a map of all events by ownerid
    //we'll need this because Google only lets us work with 1 user at a time
    map<String, list<calloutWrapper>> eventsByOwnerId = wrapEventsByOwner(eventlist, deleting);

    //list to collect events for update
    List<Event> eventUpdates = new List<Event>();

    for (string userId : eventsByOwnerId.keyset()){
        //refresh user Credentials, and store in map
        userMap.put(userid,refreshToken(usermap.get(userid)));

        //send the request in one fel swoop
        httpResponse res = new http().send(buildRequest(userMap.get(userid), eventsByOwnerId.get(userid)));
        //retrieve response body for work
        String resBody = res.getBody();
        //debug the response
        system.debug(resbody);
        //what's the boundary Google is using?
        String googBoundary = resBody.subStringBefore('Content-Type:');
        system.debug(googBoundary);
        //use that boundary to split the response
        List<String> parts = resBody.split(googBoundary);

        //for every split part of the response by boundary
        for ( String p : parts ){
            //if this is an event response
            if ( p.contains('Content-ID: <response-') ){
                //add event to list for update with it's new Google Id
                Event e = new Event(Id=p.subStringBetween('Content-ID: <response-','>'));
                e.Google_Id__c = p.subStringBetween('"id": "','"');
                eventUpdates.add(e);
            }
        }
        //if we were inserting events.
        if (!eventUpdates.isEmpty() && !deleting) update eventUpdates;
    }
}

public static map<String, list<calloutWrapper>> wrapEventsByOwner(List<Event> eventList, boolean deleting){
    map<String, list<calloutWrapper>> ownerMap = new map<String, list<calloutWrapper>>();
    for ( Event e : eventList ){
        if ( e.StartDateTime != null && e.EndDateTime != null ){
            calloutWrapper w = new calloutWrapper(e);
            w.Method = (string.isnotBlank(w.googleEventId))?((deleting)?'DELETE':'PATCH'):'POST';

            if ( ownerMap.containsKey(e.OwnerId))
                ownerMap.get(e.OwnerId).add(w);
            else ownerMap.put(e.OwnerId, new list<calloutWrapper>{w});
        }
    }
    return ownerMap;
}

public static HttpRequest buildRequest(User u, list<calloutWrapper> eventList){
    httpRequest req = new httpRequest();
    //boundary to be used to denote individual events in our batch
    //this can be anything you like, but since this is a use case, foobar :)
    String boundary = '______________batch_foobarbaz';
    //let Google know what our boundary is so it knows when to break things up
    req.setHeader('Content-Type','multipart/mixed; boundary='+boundary);
    //add the access token as our authentication
    req.setHeader('Authorization','Bearer '+u.Google_Access_Token__c);
    req.setMethod('POST');
    //we're sending a batch request, so we have a special endpoint
    req.setEndpoint('https://www.googleapis.com/batch');
    //max timeout
    req.setTimeout(120000);
    //construct our body
    String reqBody = '';
    //for every wrapped event
    for ( calloutWrapper e : eventList ){
        //start every event with a boundary
        reqBody += '--'+boundary+cr;
        //define type
        reqBody += 'Content-Type: application/http'+cr;
        //identify with our Salesforce id
        reqBody += 'Content-ID: <'+e.salesforceEventId+'>'+cr+cr;
        //what are we doing to this event? insert,update,delete?
        //aka post,patch,delete
        reqBody += e.Method+' ';
        //identify the calendar
        reqBody += '/calendar/v3/calendars/'+encodingUtil.urlEncode(u.google_email__c,'UTF-8');
        //add in the path for events on this calendar (static variable from documentation)
        reqBody += GOOGLE_CALENDAR_EVENTS_PATH;
        //if we're updating or deleting the Google event... we need to provide its id
        if ( string.isNotBlank(e.GoogleEventId) && (e.Method == 'PATCH' || e.Method == 'DELETE')){
            reqBody += '/'+e.googleEventId;
        }
        reqBody += cr+'Content-Type: application/json; charset=UTF-8'+cr;
        //delete requests don't need these
        if ( e.method != 'DELETE' ){
            reqBody += 'Content-Length: '+e.Body.length()+cr;
            reqBody += cr;
            reqBody += e.Body;
        }
        reqBody += cr;
    }
    //close off our batch request with a boundary
    reqBody += '--'+boundary+'--';
    // for debugging, let's see what we've got
    system.debug(reqBody);
    //set the body
    req.setBody(reqBody);
    //be good and set required length header
    req.setHeader('Content-Length',string.valueOf(reqBody.length()));
    return req;
}
}

Batch Class
 
global class batch_GoogleCalendar_Sync implements 
Database.Batchable<sObject>, Database.AllowsCallouts{
//class variables for use during processing
global final string queryString;
global final boolean deleting;
global final dateTime lastSync;
global final dateTime lastDelete;

//constructor taking in our infamous deletion boolean
global batch_GoogleCalendar_Sync(boolean del) {
    //retrieve our custom setting for last sync/delete times
    GoogleCalendar__c gcBatchSync = GoogleCalendar__c.getInstance('BatchSync');
    lastSync = gcBatchSync.LastSync__c;
    lastDelete = gcBatchSync.LastDelete__c;  

    //if there has never been a sync/deletion set a 
    //time long, long ago, in a galaxy far, far away
    if (lastSync==null) lastSync = dateTime.newinstance(2016,1,1);
    if (lastDelete==null) lastDelete = dateTime.newinstance(2016,1,1);        


    //just copying our constructor instance variable to //class level
    deleting = del;

    //construct the query string to include necessary fields
    //this is the same as our execute anonymous
    if (string.isBlank(queryString)){
        string temp = 'Select   Subject, StartDateTime, OwnerId, Location, IsAllDayEvent, Id, EndDateTime, DurationInMinutes, Description, ActivityDateTime, ActivityDate, google_id__c From Event';
        //if deleting is true, our query is different        //we have to add the isDeleted attribute
        if (deleting){
            temp += ' where lastModifiedDate > :lastDelete AND isDeleted = true';
            //and the query ALL ROWS flag
            //which enables us to query deleted records in the //Recycle Bin; if they have been removed from the
            //Recycle Bin, we can't query them anymore
            temp += ' ALL ROWS';
        }
        //if not deleting, just get modified date
        else temp += ' where lastModifiedDate > :lastSync';

        //this will become clearer in chapter 9
        if (test.isRunningTest()) temp += ' limit 1';

        //assign the query string and debug for debug…
        queryString = temp;
        system.debug(queryString);
    }
    //set lastSync / lastDelete based on operation
    if(deleting) gcBatchSync.lastDelete__c = system.now();
    else gcBatchSync.lastSync__c = system.now();
    //update our custom setting to preserve latest times
    update gcBatchSync;
}
//batch functional method to get next chunk
global Database.QueryLocator start(Database.BatchableContext bc){
    return Database.getQueryLocator(queryString);
}
//the execute method where we do our logic for every chunk
global void execute(Database.BatchableContext bc, list<Event> scope){
    //call our handy Google API method to process the events
    //passing in our trusty deleting boolean
    googleCalendar_API.processEventList(scope, deleting);
}

//batch functional method when we're done with the entirety of
//the batch; we're going to use this method to cause our batch //to run infinitely; deletes should run instantly after syncs, //and then pause before the next sync
global void finish(Database.BatchableContext bc){
    GoogleCalendar__c gcBatchSync = GoogleCalendar__c.getInstance('BatchSync');
    decimal delayMin = gcBatchSync.frequency_min__c;
    if (delayMin == null || delayMin < 0) delayMin = 0;
    if(deleting) startBatchDelay(false,integer.valueof(delayMin));
    else startBatch(true);
}
//utility method for starting the batch instantly with //deleting boolean
global static void startBatch(boolean d){    
    batch_GoogleCalendar_Sync job = new batch_GoogleCalendar_Sync(d);
    database.executeBatch(job,5);
}

//utility method for starting the batch on a delay
//with deleting boolean; specify delay in whole integer //minutes
global static void startBatchDelay(boolean d, integer min){
    batch_GoogleCalendar_Sync job = new batch_GoogleCalendar_Sync(d);
    system.scheduleBatch(  job,
                         'GoogleCalendarSync-'+((d)?'del':'upsert'),min,50);
}
}


Authorization Controller
 
public with sharing class googleAuthorization_Controller {
public string googleEmail {get;set;}
//to store our code for dynamic rendering
public string code {get;set;} 
//to store our user record
public User u {get;set;}
public googleAuthorization_Controller() {
    googleEmail = userInfo.getUserEmail();
}

//page action
public pagereference doOnLoad(){
    //retrieve current page
    Pagereference p = ApexPages.currentPage();
    //does it have a code as parameter?
    code = p.getParameters().get('code');
    //no? then stop
    if (string.isBlank(code)) return null;
    //it had one! get the state, aka email we passed
    //note you don't want to use googleEmail here
    //since we came back to the page, it reloaded and
    //the controller was reinstantiated, overriding our
    //input with the user's email
    string passedEmail = p.getParameters().get('state');

    //query for the user, with token fields so we can modify
    u = [select id, Google_Access_Token__c, Google_Refresh_Token__c from User where id = :userInfo.getUserId()];

    //call our api method to get tokens parsed into user
    u = googleCalendar_API.obtainAccessToken(u, code, googleCalendar_API.SF_AUTH_PAGE);

    //if we had no error
    if (u.Google_Access_Token__c != 'error'){
        //set the google email
        u.google_email__c = passedEmail;
        //update the user and display success message
        update u;
        ApexPages.addMessage(new ApexPages.message(ApexPages.severity.confirm,'Authorized Successfully!'));
    }
    else{
        //had an error? well then let us know <sadface>
        ApexPages.addMessage(new ApexPages.message(ApexPages.severity.error,'Authorization Error.'));   
    }
    //stay here, not going anywhere!
    return null;
}

public pagereference requestAuthorization(){
    return googleCalendar_API.loginRequestPage(
        googleCalendar_API.SF_AUTH_PAGE,
        googleEmail);
}
}


Mock Callout Class
 
@istest
global class GoogleCalendarHTTPRequestMock implements HttpCalloutMock {
// Implement this interface method
global HTTPResponse respond(HTTPRequest req) {
    // Optionally, only send a mock response for a specific endpoint
    // and method.
    System.assertEquals('https://www.googleapis.com/batch', req.getEndpoint());
    System.assertEquals('POST', req.getMethod());

    // Create a fake response
    HttpResponse res = new HttpResponse();
    res.setHeader('Content-Type', 'application/json');
    res.setBody('{"foo":"bar"}');
    res.setStatusCode(200);
    return res;
}
}

Test Class
 
@isTest
 class googleCalendarBatchTest {
@isTest static void initViewLoadTest() {
// Set mock callout class
Test.setMock(HttpCalloutMock.class, new GoogleCalendarHTTPRequestMock()); 

// Call method to test.
// This causes a fake response to be sent
// from the class that implements HttpCalloutMock. 
HttpResponse res = googleCalendar_API.httpRequest();

// Verify response received contains fake values
String contentType = res.getHeader('Content-Type');
System.assert(contentType == 'application/json');
String actualValue = res.getBody();
String expectedValue = '{"foo":"bar"}';
System.assertEquals(actualValue, expectedValue);
System.assertEquals(200, res.getStatusCode());
}
}


 
Hi I have a batch matching job running in salesforce. It was working and then I did something and now it has stopped functioning as I intended. I've tried debugging but for the life of me can't figure out what is going on. The goal of the script is to take one list of opportunities that match a certain criteria and another list of opportunities on the same account that match another criteria, map them, and then merge them.

It seems like it properly brings in the two lists of opportunities but when it comes time to match and merge it works on 1 mapped records in the entire list. So If there are 12 opps in each list, one of them will be merge the rest will remain untouched.
 
/**
       * called by AccountOpptyMatchingScheduler scheduler to perform opportunity matching and consolidation
       *
       * 1. get all the opportunities inserted by redshift today (with subscription id) and is not a matched opportunity (Matched_Opportunity__c = false)
       * 2. look for opportunities under the same account without subscription id (salesforce manually inserted)
       * 3. sf opportunity with the highest attempt__c and most recent createdDate and same Type and same recordtype will be the match
       * 4. if sf oppty with highest attempt__c is not the most recent or most recent does not have highest attempt, email nicholas
       * 5. otherwise, perform merge
       */
      global class OpportunityMatchingBatch implements Database.Batchable<sObject>, Database.Stateful {
          String query;
          List<Opportunity> matchedopptys;    // holds all the oppty matched today
          List<String> erroropptys;    // holds all the opptys that isn't highest attempt or most recent

          Date cdate;

          global OpportunityMatchingBatch() {
              this(null);
          }

          global OpportunityMatchingBatch(Date cdate) {
              this.cdate = cdate;
              String datestr = 'CreatedDate = LAST_N_DAYS:14';
              if (cdate != null) datestr = 'CreatedDate >= :cdate';

       /* This was what it was like before tried to slim down....
       * 
       * Date_of_Subscription_Start__c,Date_of_Subscription_End__c,'
                  + 'Total_Value__c,Virtual_MRR__c,Is_Last_Subscription__c,Active_Subscription_Number__c,'
                  + 'Number_of_Failed_Charges__c,Relative_Size__c,Is_Active_Subscription__c,Type,'
                  + 'Duration__c,Payment_Frequency__c,Ended_Early__c,Months_Paid_Up_Front__c,Total_Charges__c,'
                  + 'Last_Subscription_MRR__c,Partner_Slug__c,Pending_Charges__c,Pending_Charges_Dollar_Value__c,'
                  + 'Completed_Charges__c,Completed_Charges_Dollar_Value__c,Partner_Subscription_ID__c,RecordTypeId '
      */
              query = 'SELECT AccountId,Name,Type,RecordTypeId,Partner_Subscription_ID__c '
                  + 'FROM Opportunity WHERE ' + datestr + ' AND Partner_Subscription_ID__c <> null AND Matched_Opportunity__c = false';
              matchedopptys = new List<Opportunity>();
              erroropptys = new List<String>();
              System.debug(Logginglevel.INFO, 'Step 1');
          }

          global Database.QueryLocator start(Database.BatchableContext BC) {
              System.debug(Logginglevel.INFO, 'Step 2');
              return Database.getQueryLocator(query);
              
          }

          /**
           * record comes in one at a time
           */
          global void execute(Database.BatchableContext BC, List<Opportunity> opplist) {
              Opportunity redshifta = opplist[0];
              System.debug(Logginglevel.INFO, 'Step 3');

              // should only match with the highest attempt and most recently created sf opportunity
              for (Opportunity opp : [SELECT Name, CreatedDate, AccountId FROM Opportunity
                  WHERE AccountId = :redshifta.AccountId AND Partner_Subscription_ID__c = null ORDER BY CreatedDate DESC LIMIT 1]) {

                  // got here, it's okay to write soql in here because we are only matching with one opportunity, there won't be a loop
                  // get the highst attempt and most recent createddate from this account
                  //String highestattempt = [SELECT Attempt__c FROM Opportunity WHERE AccountId = :opp.AccountId AND Type = :redshifta.Type AND RecordTypeId // = :redshifta.RecordTypeId AND Partner_Subscription_ID__c = null ORDER BY Attempt__c DESC LIMIT 1].Attempt__c;
                  Datetime mostrecentdate = [SELECT CreatedDate FROM Opportunity WHERE AccountId = :opp.AccountId AND Type = :redshifta.Type AND Partner_Subscription_ID__c = null ORDER BY CreatedDate DESC LIMIT 1].CreatedDate;

                  // only merge if it is highest attempt and most recent
                  if (mostrecentdate == opp.CreatedDate) {
                      // create match result reason
                      String matchedon = null;

                      opp.Matching_Result__c = matchedon;

                      // merge fields
                      opp.Name = redshifta.Name;
                      /*opp.Date_of_Subscription_Start__c = redshifta.Date_of_Subscription_Start__c;
                      opp.Date_of_Subscription_End__c = redshifta.Date_of_Subscription_End__c;
                      opp.Total_Value__c = redshifta.Total_Value__c;
                      opp.Virtual_MRR__c = redshifta.Virtual_MRR__c;
                      opp.Is_Last_Subscription__c = redshifta.Is_Last_Subscription__c;
                      opp.Active_Subscription_Number__c = redshifta.Active_Subscription_Number__c;
                      opp.Number_of_Failed_Charges__c = redshifta.Number_of_Failed_Charges__c;
                      opp.Relative_Size__c = redshifta.Relative_Size__c;
                      opp.Is_Active_Subscription__c = redshifta.Is_Active_Subscription__c;
                      opp.Type = redshifta.Type;
                      opp.Duration__c = redshifta.Duration__c;
                      opp.Payment_Frequency__c = redshifta.Payment_Frequency__c;
                      opp.Ended_Early__c = redshifta.Ended_Early__c;
                      opp.Months_Paid_Up_Front__c = redshifta.Months_Paid_Up_Front__c;
                      opp.Total_Charges__c = redshifta.Total_Charges__c;
                      opp.Last_Subscription_MRR__c = redshifta.Last_Subscription_MRR__c;
                      opp.Partner_Slug__c = redshifta.Partner_Slug__c;
                      opp.Pending_Charges__c = redshifta.Pending_Charges__c;
                      opp.Pending_Charges_Dollar_Value__c = redshifta.Pending_Charges_Dollar_Value__c;
                      opp.Completed_Charges__c = redshifta.Completed_Charges__c;
                      opp.Completed_Charges_Dollar_Value__c = redshifta.Completed_Charges_Dollar_Value__c;*/
                      opp.Partner_Subscription_ID__c = redshifta.Partner_Subscription_ID__c;
                      opp.Matched_Opportunity__c = true;

                      update opp;
                      delete redshifta;
                      matchedopptys.add(opp);
                  } else {
                      // error
                      erroropptys.add('SF Name: ' + opp.Name + ', SF Id: ' + opp.Id + ', Partner Sub Name: ' + redshifta.Name + ', Redshift Id: ' + redshifta.Id
                          + ' SF CreatedDate: ' + opp.createdDate.format('MM/dd/YYYY HH:mm:ss')
                          + ' Most Recent CreatedDate: ' + mostrecentdate.format('MM/dd/YYYY HH:mm:ss'));
                  }

              }

          }

          /**
           * after batch is done, email nicholas two reports, one is the opportunity ids merged, one is the error report
           * @param  BC [description]
           * @return    [description]
           */
          global void finish(Database.BatchableContext BC) {
              List<Messaging.SingleEmailMessage> emailList = new List<Messaging.SingleEmailMessage>();

              // send matched opportunity email
              if (!matchedopptys.isEmpty()) {
                  Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                  mail.setToAddresses(new String[] {'nicholas@artsy.net','matt@gsdcompany.com'});
                  mail.setSenderDisplayName('Artsy Matching Agent');
                  mail.setUseSignature(false);
                  mail.setSubject('Opportunity Matching Result');
                  String body = Datetime.now().format('MM/dd/YYYY') + ' opportunity match result:\n\n';
                  for (Opportunity opp : matchedopptys) {
                      body += 'Name: ' + opp.Name + ' | SF ID: ' + opp.Id + ' | Partner Subscription ID: ' + opp.Partner_Subscription_ID__c + '\n';
                  }
                  mail.setPlainTextBody(body);
                  emailList.add(mail);
              }

              // send error email
              if (!erroropptys.isEmpty()) {
                  Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                  //'nicholas@artsy.net',
                  mail.setToAddresses(new String[] {'nicholas@artsy.net','matt@gsdcompany.com'});
                  mail.setSenderDisplayName('Artsy Matching Agent');
                  mail.setUseSignature(false);
                  mail.setSubject('Opportunity Match Errors (Not Highest Attempt or Not Most Recent');
                  String body = Datetime.now().format('MM/dd/YYYY') + ' Opportunity Match Errors (Not Highest Attempt or Not Most Recent):\n\n';
                  for (String e : erroropptys) {
                      body += e + '\n\n';
                  }
                  mail.setPlainTextBody(body);
                  emailList.add(mail);
              }

              Messaging.sendEmail(emailList);
          }
      }

 
Hey I am trying to implement a batch system that sends salesforce events to google calendar's api. I have successfully implemented this process in sandbox but am having trouble getting code coverage.

Below is my Callout Class followed by my batch class as well as my google api authorization controller. I have test coverage for none. At the bottom is my attempt at writing test coverage which essentially follows Salesforce's documentation. I seem to be having particular trouble because my HTTP CALLOUT is a POST.


Callout Class
 
public with sharing class googleCalendar_API {

/********************** START CONSTANTS ***************************/
static String GOOGLE_API_CLIENT_ID = '555540635024-5kincbt5uhpfh4g8faq6atmj4hmmbb3h.apps.googleusercontent.com';
static String GOOGLE_API_CLIENT_SECRET = 'W5G3H0qkpNi0ac1kvfsOzkWK';

static String GOOGLE_CALENDAR_SCOPE = 'https://www.googleapis.com/auth/calendar';
static String GOOGLE_CALENDAR_BASE_URL = 'https://www.googleapis.com/calendar/v3/calendars/';
static String GOOGLE_CALENDAR_EVENTS_PATH = '/events';

public static String SF_AUTH_PAGE =
    'https://-------artdev--c.cs62.visual.force.com/apex/googleAuthorization';

static Map<String,String> operationMap = new Map<String,String>{'INSERT'=>'POST','UPDATE'=>'PATCH','DELETE'=>'DELETE'};
static map<id,User> userMap = new map<id,User>([select id, name, google_Email__c, Google_Access_Token__c, Google_Refresh_Token__c from User where isActive=true]);

//carriage return
static String cr = '\r\n';
/********************** END CONSTANTS ***************************/

static TimeZone tz = UserInfo.getTimeZone();
public static String convertDateTimeToString(DateTime dt){
    Integer x = tz.getOffset(dt)/3600000;
    String z = '';
    if ( x > 0 ) z += '+';
    else z += '-';

    if ( x > 9 || x < -9 ) z += math.abs(x);
    else z += '0'+math.abs(x);

    z += ':00';

    return dt.format('yyyy-MM-dd\'T\'HH:mm:ss'+z);
}

public static httpResponse callGoogle(String endpoint, String method, String body){
    HttpRequest req = new HttpRequest();
    req.setEndpoint(endpoint);
    req.setMethod(method);
    req.setCompressed(false);
    req.setHeader('User-Agent','learnApex API');
    req.setHeader('Encoding','iso-8859-1');
    req.setHeader('Content-Type','application/x-www-form-urlencoded');
    req.setTimeout(120000);
    if( body != null ){
        req.setBody(body);
        req.setHeader('Content-length',string.valueOf(body.length()));
    }
    HttpResponse res = new http().send(req);

    system.debug(res.getBody());
    return res;
}

public static User parseGoogleAuth(String body, User u){
    jsonParser parser = json.createParser(body);
    while ( parser.nextToken() != null ){
        if ( parser.getCurrentToken() == JSONToken.FIELD_NAME && parser.getText() != null && parser.getText() == 'access_token' ){
            parser.nextToken();
            u.Google_Access_Token__c = parser.getText();
        } else
        if ( parser.getCurrentToken() == JSONToken.FIELD_NAME && parser.getText() != null && parser.getText() == 'refresh_token' ){
            parser.nextToken();
            u.Google_Refresh_Token__c = parser.getText();
        }
    }
    return u;
}

public static PageReference loginRequestPage
    (String redirectURI, String state){
        PageReference p =
            new PageReference('https://accounts.google.com/o/oauth2/auth');
    p.getParameters().put('response_type','code');  //Determines if the Google Authorization Server returns an authorization code (code), or an opaque access token (token)
    p.getParameters().put('client_id',GOOGLE_API_CLIENT_ID);
    p.getParameters().put('redirect_uri',redirectURI);
    p.getParameters().put('approval_prompt','force');
    p.getParameters().put('scope',GOOGLE_CALENDAR_SCOPE);
    p.getParameters().put('state',state);   //This optional parameter indicates any state which may be useful to your application upon receipt of the response. The Google Authorization Server roundtrips this parameter, so your application receives the same value it sent. Possible uses include redirecting the user to the correct resource in your site, nonces, and cross-site-request-forgery mitigations.
    p.getParameters().put('access_type','offline');
    return p;
}

public static User obtainAccessToken(User u, String code, String redirectURL){
    PageReference p = new PageReference('https://accounts.google.com/o/oauth2/auth');
    p.getParameters().put('client_id',GOOGLE_API_CLIENT_ID);
    p.getParameters().put('client_secret',GOOGLE_API_CLIENT_SECRET);
    p.getParameters().put('scope','');
    p.getParameters().put('redirect_uri',redirectURL);
    p.getParameters().put('grant_type','authorization_code');
    p.getParameters().put('code',code);
    String body = p.getURL();
    body = body.subStringAfter('?');
    httpResponse googleAuth = callGoogle('https://accounts.google.com/o/oauth2/token','POST',body);
    if ( googleAuth.getStatusCode() == 200 ){
        u = parseGoogleAuth(googleAuth.getBody(), u);
    }
    else u.Google_Access_Token__c ='error';
    return u;
}

public static User refreshToken(User u){
    PageReference p = new PageReference('https://accounts.google.com/o/oauth2/auth');
    p.getParameters().put('client_id',GOOGLE_API_CLIENT_ID);
    p.getParameters().put('client_secret',GOOGLE_API_CLIENT_SECRET);
    p.getParameters().put('refresh_token',u.Google_Refresh_Token__c);
    p.getParameters().put('grant_type','refresh_token');
    String body = p.getURL();
    body = body.subStringAfter('?');
    httpResponse googleAuth = callGoogle('https://accounts.google.com/o/oauth2/token','POST',body);
    if ( googleAuth.getStatusCode() == 200 ){
        u = parseGoogleAuth(googleAuth.getBody(), u);
    }
    return u;
}

public class calloutWrapper{
    public String body {get;set;}
    public String endpoint {get;set;}
    public String googleCalendarEmail {get;set;}
    public String googleEventId {get;set;}
    public String method {get;set;}
    public String ownerName {get;set;}
    public Id salesforceEventId {get;set;}
    public Id salesforceOwnerId {get;set;}

    public calloutWrapper(Event e){
        ownerName = usermap.get(e.OwnerId).Name;
        googleCalendarEmail = usermap.get(e.ownerid).google_Email__c;
        salesforceOwnerId = e.OwnerId;
        salesforceEventId = e.Id;
        if ( string.isNotBlank(e.Google_Id__c) ){
            googleEventId = e.Google_Id__c;
        }
        body = compileBodyFromEvent(e);
    }
}

public static String compileBodyFromEvent(Event e){
    //we’re building a JSON body manually!
    String body = '{'+cr+' "end": {'+cr;
    if (e.isalldayevent){
        body += ' "date": "'+ e.StartDateTime.formatgmt('yyyy-MM-dd') +'"'+cr;
    }
    else {
        body += ' "dateTime": "'+ convertDateTimeToString(e.EndDateTime) +'"'+cr;
    }
    body += ' },'+cr+' "start": {'+cr;
    if (e.isalldayevent){
        body += ' "date": "'+ e.StartDateTime.formatgmt('yyyy-MM-dd') +'"'+cr;
    }
    else{
        body += ' "dateTime": "'+ convertDateTimeToString(e.StartDateTime) +'"'+cr;
    }
    body += ' },'+cr;
    if ( string.isNotBlank(e.Subject) ){
        body += ' "summary": "'+ e.Subject +'",'+cr;
    }
    if ( string.isNotBlank(e.Description) ){
        body += ' "description": "'+ e.Description.replace('\n','\\n').replace('\r','\\r') +'",'+cr;
    }
    if ( string.isNotBlank( e.Location ) ){
        body += ' "location": "'+ e.Location +'",'+cr;
    }
    //we've been blindly adding returns
    body = body.subStringBeforeLast(',');
    body += '}'+cr;
    return body;
}

public static void processEventList(list<Event> eventList, boolean deleting){
    //generate a map of all events by ownerid
    //we'll need this because Google only lets us work with 1 user at a time
    map<String, list<calloutWrapper>> eventsByOwnerId = wrapEventsByOwner(eventlist, deleting);

    //list to collect events for update
    List<Event> eventUpdates = new List<Event>();

    for (string userId : eventsByOwnerId.keyset()){
        //refresh user Credentials, and store in map
        userMap.put(userid,refreshToken(usermap.get(userid)));

        //send the request in one fel swoop
        httpResponse res = new http().send(buildRequest(userMap.get(userid), eventsByOwnerId.get(userid)));
        //retrieve response body for work
        String resBody = res.getBody();
        //debug the response
        system.debug(resbody);
        //what's the boundary Google is using?
        String googBoundary = resBody.subStringBefore('Content-Type:');
        system.debug(googBoundary);
        //use that boundary to split the response
        List<String> parts = resBody.split(googBoundary);

        //for every split part of the response by boundary
        for ( String p : parts ){
            //if this is an event response
            if ( p.contains('Content-ID: <response-') ){
                //add event to list for update with it's new Google Id
                Event e = new Event(Id=p.subStringBetween('Content-ID: <response-','>'));
                e.Google_Id__c = p.subStringBetween('"id": "','"');
                eventUpdates.add(e);
            }
        }
        //if we were inserting events.
        if (!eventUpdates.isEmpty() && !deleting) update eventUpdates;
    }
}

public static map<String, list<calloutWrapper>> wrapEventsByOwner(List<Event> eventList, boolean deleting){
    map<String, list<calloutWrapper>> ownerMap = new map<String, list<calloutWrapper>>();
    for ( Event e : eventList ){
        if ( e.StartDateTime != null && e.EndDateTime != null ){
            calloutWrapper w = new calloutWrapper(e);
            w.Method = (string.isnotBlank(w.googleEventId))?((deleting)?'DELETE':'PATCH'):'POST';

            if ( ownerMap.containsKey(e.OwnerId))
                ownerMap.get(e.OwnerId).add(w);
            else ownerMap.put(e.OwnerId, new list<calloutWrapper>{w});
        }
    }
    return ownerMap;
}

public static HttpRequest buildRequest(User u, list<calloutWrapper> eventList){
    httpRequest req = new httpRequest();
    //boundary to be used to denote individual events in our batch
    //this can be anything you like, but since this is a use case, foobar :)
    String boundary = '______________batch_foobarbaz';
    //let Google know what our boundary is so it knows when to break things up
    req.setHeader('Content-Type','multipart/mixed; boundary='+boundary);
    //add the access token as our authentication
    req.setHeader('Authorization','Bearer '+u.Google_Access_Token__c);
    req.setMethod('POST');
    //we're sending a batch request, so we have a special endpoint
    req.setEndpoint('https://www.googleapis.com/batch');
    //max timeout
    req.setTimeout(120000);
    //construct our body
    String reqBody = '';
    //for every wrapped event
    for ( calloutWrapper e : eventList ){
        //start every event with a boundary
        reqBody += '--'+boundary+cr;
        //define type
        reqBody += 'Content-Type: application/http'+cr;
        //identify with our Salesforce id
        reqBody += 'Content-ID: <'+e.salesforceEventId+'>'+cr+cr;
        //what are we doing to this event? insert,update,delete?
        //aka post,patch,delete
        reqBody += e.Method+' ';
        //identify the calendar
        reqBody += '/calendar/v3/calendars/'+encodingUtil.urlEncode(u.google_email__c,'UTF-8');
        //add in the path for events on this calendar (static variable from documentation)
        reqBody += GOOGLE_CALENDAR_EVENTS_PATH;
        //if we're updating or deleting the Google event... we need to provide its id
        if ( string.isNotBlank(e.GoogleEventId) && (e.Method == 'PATCH' || e.Method == 'DELETE')){
            reqBody += '/'+e.googleEventId;
        }
        reqBody += cr+'Content-Type: application/json; charset=UTF-8'+cr;
        //delete requests don't need these
        if ( e.method != 'DELETE' ){
            reqBody += 'Content-Length: '+e.Body.length()+cr;
            reqBody += cr;
            reqBody += e.Body;
        }
        reqBody += cr;
    }
    //close off our batch request with a boundary
    reqBody += '--'+boundary+'--';
    // for debugging, let's see what we've got
    system.debug(reqBody);
    //set the body
    req.setBody(reqBody);
    //be good and set required length header
    req.setHeader('Content-Length',string.valueOf(reqBody.length()));
    return req;
}
}

Batch Class
 
global class batch_GoogleCalendar_Sync implements 
Database.Batchable<sObject>, Database.AllowsCallouts{
//class variables for use during processing
global final string queryString;
global final boolean deleting;
global final dateTime lastSync;
global final dateTime lastDelete;

//constructor taking in our infamous deletion boolean
global batch_GoogleCalendar_Sync(boolean del) {
    //retrieve our custom setting for last sync/delete times
    GoogleCalendar__c gcBatchSync = GoogleCalendar__c.getInstance('BatchSync');
    lastSync = gcBatchSync.LastSync__c;
    lastDelete = gcBatchSync.LastDelete__c;  

    //if there has never been a sync/deletion set a 
    //time long, long ago, in a galaxy far, far away
    if (lastSync==null) lastSync = dateTime.newinstance(2016,1,1);
    if (lastDelete==null) lastDelete = dateTime.newinstance(2016,1,1);        


    //just copying our constructor instance variable to //class level
    deleting = del;

    //construct the query string to include necessary fields
    //this is the same as our execute anonymous
    if (string.isBlank(queryString)){
        string temp = 'Select   Subject, StartDateTime, OwnerId, Location, IsAllDayEvent, Id, EndDateTime, DurationInMinutes, Description, ActivityDateTime, ActivityDate, google_id__c From Event';
        //if deleting is true, our query is different        //we have to add the isDeleted attribute
        if (deleting){
            temp += ' where lastModifiedDate > :lastDelete AND isDeleted = true';
            //and the query ALL ROWS flag
            //which enables us to query deleted records in the //Recycle Bin; if they have been removed from the
            //Recycle Bin, we can't query them anymore
            temp += ' ALL ROWS';
        }
        //if not deleting, just get modified date
        else temp += ' where lastModifiedDate > :lastSync';

        //this will become clearer in chapter 9
        if (test.isRunningTest()) temp += ' limit 1';

        //assign the query string and debug for debug…
        queryString = temp;
        system.debug(queryString);
    }
    //set lastSync / lastDelete based on operation
    if(deleting) gcBatchSync.lastDelete__c = system.now();
    else gcBatchSync.lastSync__c = system.now();
    //update our custom setting to preserve latest times
    update gcBatchSync;
}
//batch functional method to get next chunk
global Database.QueryLocator start(Database.BatchableContext bc){
    return Database.getQueryLocator(queryString);
}
//the execute method where we do our logic for every chunk
global void execute(Database.BatchableContext bc, list<Event> scope){
    //call our handy Google API method to process the events
    //passing in our trusty deleting boolean
    googleCalendar_API.processEventList(scope, deleting);
}

//batch functional method when we're done with the entirety of
//the batch; we're going to use this method to cause our batch //to run infinitely; deletes should run instantly after syncs, //and then pause before the next sync
global void finish(Database.BatchableContext bc){
    GoogleCalendar__c gcBatchSync = GoogleCalendar__c.getInstance('BatchSync');
    decimal delayMin = gcBatchSync.frequency_min__c;
    if (delayMin == null || delayMin < 0) delayMin = 0;
    if(deleting) startBatchDelay(false,integer.valueof(delayMin));
    else startBatch(true);
}
//utility method for starting the batch instantly with //deleting boolean
global static void startBatch(boolean d){    
    batch_GoogleCalendar_Sync job = new batch_GoogleCalendar_Sync(d);
    database.executeBatch(job,5);
}

//utility method for starting the batch on a delay
//with deleting boolean; specify delay in whole integer //minutes
global static void startBatchDelay(boolean d, integer min){
    batch_GoogleCalendar_Sync job = new batch_GoogleCalendar_Sync(d);
    system.scheduleBatch(  job,
                         'GoogleCalendarSync-'+((d)?'del':'upsert'),min,50);
}
}


Authorization Controller
 
public with sharing class googleAuthorization_Controller {
public string googleEmail {get;set;}
//to store our code for dynamic rendering
public string code {get;set;} 
//to store our user record
public User u {get;set;}
public googleAuthorization_Controller() {
    googleEmail = userInfo.getUserEmail();
}

//page action
public pagereference doOnLoad(){
    //retrieve current page
    Pagereference p = ApexPages.currentPage();
    //does it have a code as parameter?
    code = p.getParameters().get('code');
    //no? then stop
    if (string.isBlank(code)) return null;
    //it had one! get the state, aka email we passed
    //note you don't want to use googleEmail here
    //since we came back to the page, it reloaded and
    //the controller was reinstantiated, overriding our
    //input with the user's email
    string passedEmail = p.getParameters().get('state');

    //query for the user, with token fields so we can modify
    u = [select id, Google_Access_Token__c, Google_Refresh_Token__c from User where id = :userInfo.getUserId()];

    //call our api method to get tokens parsed into user
    u = googleCalendar_API.obtainAccessToken(u, code, googleCalendar_API.SF_AUTH_PAGE);

    //if we had no error
    if (u.Google_Access_Token__c != 'error'){
        //set the google email
        u.google_email__c = passedEmail;
        //update the user and display success message
        update u;
        ApexPages.addMessage(new ApexPages.message(ApexPages.severity.confirm,'Authorized Successfully!'));
    }
    else{
        //had an error? well then let us know <sadface>
        ApexPages.addMessage(new ApexPages.message(ApexPages.severity.error,'Authorization Error.'));   
    }
    //stay here, not going anywhere!
    return null;
}

public pagereference requestAuthorization(){
    return googleCalendar_API.loginRequestPage(
        googleCalendar_API.SF_AUTH_PAGE,
        googleEmail);
}
}


Mock Callout Class
 
@istest
global class GoogleCalendarHTTPRequestMock implements HttpCalloutMock {
// Implement this interface method
global HTTPResponse respond(HTTPRequest req) {
    // Optionally, only send a mock response for a specific endpoint
    // and method.
    System.assertEquals('https://www.googleapis.com/batch', req.getEndpoint());
    System.assertEquals('POST', req.getMethod());

    // Create a fake response
    HttpResponse res = new HttpResponse();
    res.setHeader('Content-Type', 'application/json');
    res.setBody('{"foo":"bar"}');
    res.setStatusCode(200);
    return res;
}
}

Test Class
 
@isTest
 class googleCalendarBatchTest {
@isTest static void initViewLoadTest() {
// Set mock callout class
Test.setMock(HttpCalloutMock.class, new GoogleCalendarHTTPRequestMock()); 

// Call method to test.
// This causes a fake response to be sent
// from the class that implements HttpCalloutMock. 
HttpResponse res = googleCalendar_API.httpRequest();

// Verify response received contains fake values
String contentType = res.getHeader('Content-Type');
System.assert(contentType == 'application/json');
String actualValue = res.getBody();
String expectedValue = '{"foo":"bar"}';
System.assertEquals(actualValue, expectedValue);
System.assertEquals(200, res.getStatusCode());
}
}


 
Hi all,

Here is some very basic before insert code I am using in an account trigger - my intent is to copy the OwnerId (or OwnerName) to a text field called Original_Account_Owner__c. This field would retain a historical record of who the original account owner was so that we can always report on that data even if the current owner is changed.
 
if (Trigger.isBefore && Trigger.isInsert) {
        for(Account acct : Trigger.new){
            // Check that the owner is a user (not a queue)
            if( ((String)acct.OwnerId).substring(0,3) == '005' && acct.Original_Account_Owner__c == null ){
            acct.Original_Account_Owner__c = acct.OwnerId;
            }
            else{
            // In case of Queue (which shouldn't happen), System Debug.
            System.debug('found a queue when we shouldn't have...' + acct.OwnerId);
            }
        }

This works perfectly if I convert a lead for myself, with myself set as the Record Owner during the conversion process...

However:
If I have two employees (Emp A and Emp B) and Emp A is converting a lead but during the conversion process he/she sets the Record Owner to Emp B, the end result after my trigger runs is that the "Original Account Owner" is showing Emp A and the "Account Owner" is showing Emp B when in reality I want the "Original Account Owner" and the "Account Owner" to BOTH be Emp B because that was the Record Owner selected during the conversion.

My assumption was that if the record owner is selected during conversion, it would be the one that the new account record is created with - so my trigger should just pick up the Account Owner prior to insert and set it on my custom field... instead, it looks like it assumes that I am the Record Owner during insert and then quickly changes it afterwards?

Is there any way I can combat this and get the end result I am looking for, or am I stuck because of the nature of the account creation/reassignment process during conversion?

Many thanks for your input everyone!