diff --git a/.vscode/settings.json b/.vscode/settings.json index 76decfb..ef14c7f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,9 @@ "**/node_modules": true, "**/bower_components": true, "**/.sfdx": true + }, + "workbench.colorCustomizations": { + "statusBar.background": null, + "activityBar.background": null } } diff --git a/README.md b/README.md index c1b2ded..b8d0768 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Open in Visual Studio Code](https://classroom.github.com/assets/open-in-vscode-718a45dd9cf7e7f842a935f5ebbe5719a5e09af4491e668f4dbf3b35d5cca122.svg)](https://classroom.github.com/online_ide?assignment_repo_id=14992435&assignment_repo_type=AssignmentRepo) # Developer Kickstart: Integrations This repository is a pivotal element of the Developer Kickstart curriculum at Cloud Code Academy. Crafted for aspiring Salesforce developers, this module delves into the intricate domain of integrations, spotlighting the principles, tools, and best practices necessary to effectively connect Salesforce with external systems and platforms. diff --git a/force-app/main/default/classes/ContactTriggerHandler.cls b/force-app/main/default/classes/ContactTriggerHandler.cls new file mode 100644 index 0000000..5dbdc67 --- /dev/null +++ b/force-app/main/default/classes/ContactTriggerHandler.cls @@ -0,0 +1,57 @@ +/** + * Key Behaviors: + * 1. When a new Contact is inserted and doesn't have a value for the DummyJSON_Id__c field, the trigger generates a random number between 0 and 100 for it. + * 2. Upon insertion, if the generated or provided DummyJSON_Id__c value is less than or equal to 100, the trigger initiates the getDummyJSONUserFromId API call. + * 3. If a Contact record is updated and the DummyJSON_Id__c value is greater than 100, the trigger initiates the postCreateDummyJSONUser API call. + * + * Best Practices for Callouts in Triggers: + * + * 1. Avoid Direct Callouts: Triggers do not support direct HTTP callouts. Instead, use asynchronous methods like @future or Queueable to make the callout. + * 2. Bulkify Logic: Ensure that the trigger logic is bulkified so that it can handle multiple records efficiently without hitting governor limits. + * 3. Avoid Recursive Triggers: Ensure that the callout logic doesn't result in changes that re-invoke the same trigger, causing a recursive loop. + */ + +public with sharing class ContactTriggerHandler extends TriggerHandler { + + private List newConts; + + /** + * Constructor. Set instance variables. + */ + public ContactTriggerHandler() { + this.newConts = (List) Trigger.new; + } + + /** + * Before Insert method. + * + * When a contact is inserted + * if DummyJSON_Id__c is null, generate a random number between 0 and 100 and set this as the contact's DummyJSON_Id__c value + * if DummyJSON_Id__c is less than or equal to 100, call the getDummyJSONUserFromId API + */ + public override void beforeInsert() { + List dummyJSONIdList = new List(); + for (Contact cont : newConts) { + if (cont.DummyJSON_Id__c == null) { + cont.DummyJSON_Id__c = String.valueOf(Math.round(Math.random() * 100)); + } + if (Integer.valueOf(cont.DummyJSON_Id__c) <= 100 && !System.isFuture()) { + DummyJSONCallout.getDummyJSONUserFromId(cont.DummyJSON_Id__c); + } + } + } + + /** + * After Update method. + * + * When a contact is updated + * if DummyJSON_Id__c is greater than 100, call the postCreateDummyJSONUser API + */ + public override void afterUpdate() { + for (Contact cont : newConts) { + if (Integer.valueOf(cont.DummyJSON_Id__c) > 100 && !System.isFuture()) { + DummyJSONCallout.postCreateDummyJSONUser(cont.Id); + } + } + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/ContactTriggerHandler.cls-meta.xml b/force-app/main/default/classes/ContactTriggerHandler.cls-meta.xml new file mode 100644 index 0000000..019e850 --- /dev/null +++ b/force-app/main/default/classes/ContactTriggerHandler.cls-meta.xml @@ -0,0 +1,5 @@ + + + 59.0 + Active + \ No newline at end of file diff --git a/force-app/main/default/classes/DummyJSONCallout.cls b/force-app/main/default/classes/DummyJSONCallout.cls index 9485a82..8981103 100644 --- a/force-app/main/default/classes/DummyJSONCallout.cls +++ b/force-app/main/default/classes/DummyJSONCallout.cls @@ -19,6 +19,8 @@ * * For more detailed information on HTTP callouts in Apex, refer to the official Salesforce documentation: * https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_classes_restful_http_httprequest.htm + * + * Implemented by Oxana Suvorova */ public with sharing class DummyJSONCallout { @@ -33,19 +35,26 @@ public with sharing class DummyJSONCallout { * * @param dummyUserId The ID of the user in the external system to fetch data for. */ - + @future(callout = true) public static void getDummyJSONUserFromId(String dummyUserId) { // Create HTTP request to send. - + HttpRequest request = new HttpRequest(); // Set the endpoint URL. Use direct URL or for best practices use Named Credential. - + request.setEndpoint('callout:DummyJsonUser' + '/' + dummyUserId); // Set the HTTP method to GET. - + request.setMethod('GET'); // Send the HTTP request and get the response. - - // If the HTTP response code is successful, parse the JSON response and update the contact. - Contact cont = null; - upsert cont DummyJSON_Id__c; //insert/update from the JSON response using the external id (dummyUserId) + try { + Http http = new Http(); + HttpResponse response = http.send(request); + // If the HTTP response code is successful, parse the JSON response and update the contact. + if (response.getStatusCode() == 200) { + Contact cont = createContactFromJson(response.getBody()); + upsert cont DummyJSON_Id__c; // insert/update from the JSON response using the external id (dummyUserId) + } + } catch (Exception ex) { + System.debug('Error: ' + ex.getMessage()); + } } /* @@ -66,16 +75,27 @@ public with sharing class DummyJSONCallout { @TestVisible // Allows test class to see this method. Since it is private, it would not be visible otherwise. private static Contact createContactFromJson(String jsonResponse) { // Deserialize the JSON string into map of primitive data types. - + Map userDataMap = (Map) JSON.deserializeUntyped(jsonResponse); // Create a new contact from the JSON response. - + Contact cont = new Contact(); // Set the contact fields from the JSON response. - + cont.FirstName = (String) userDataMap.get('firstName'); + cont.LastName = (String) userDataMap.get('lastName'); + cont.Email = (String) userDataMap.get('email'); + cont.Phone = (String) userDataMap.get('phone'); + cont.Birthdate = Date.valueOf((String) userDataMap.get('birthDate')); + cont.DummyJSON_Id__c = String.valueOf(userDataMap.get('id')); + // Deserialize the address from the JSON response. - + Map addressMap = (Map) userDataMap.get('address'); // Set the address fields on the contact. + cont.MailingStreet = (String) addressMap.get('address'); + cont.MailingCity = (String) addressMap.get('city'); + cont.MailingPostalCode = (String) addressMap.get('postalCode'); + cont.MailingState = (String) addressMap.get('state'); + cont.MailingCountry = (String) addressMap.get('country'); - return null; + return cont; } /* @@ -91,19 +111,31 @@ public with sharing class DummyJSONCallout { * * @param contactId The Salesforce Contact ID used to generate the JSON payload for the external system. */ - + @future(callout = true) public static void postCreateDummyJSONUser(String contactId) { // Create HTTP request to send. - + HttpRequest request = new HttpRequest(); // Set the endpoint URL. Use direct URL or for best practices use Named Credential. - + request.setEndpoint('callout:DummyJsonUser/add'); // Set the HTTP method to POST. - + request.setMethod('POST'); // Set the body using generateDummyJsonUserPayload method. - + // String jsonBody = generateDummyJsonUserPayload(); + // if (jsonBody.length() > 0) + request.setBody(generateDummyJsonUserPayload(contactId)); + // Send the HTTP request and get the response. - + Http http = new Http(); + HttpResponse response = http.send(request); // If the HTTP response code is successful, update the contact. + if (response.getStatusCode() >= 200 || response.getStatusCode() <= 299) { + Contact cont = [ + SELECT Id, DummyJSON_Last_Updated__c + FROM Contact + WHERE Id = :contactId]; + cont.DummyJSON_Last_Updated__c = Datetime.now(); + update cont; + } } /* @@ -124,13 +156,23 @@ public with sharing class DummyJSONCallout { @TestVisible // Allows test class to see this method. Since it is private, it would not be visible otherwise. private static String generateDummyJsonUserPayload(String contactId) { // Query the contact to get the field values to generate the JSON payload. - + Contact cont = [ + SELECT Id, FirstName, LastName, Email, Phone, DummyJSON_Id__c + FROM Contact + WHERE Id = :contactId + ]; // Create a map of the field values. - + Map valuesMap = new Map(); + valuesMap.put('salesforceId', contactId); + valuesMap.put('firstName', String.isNotBlank(cont.FirstName) ? cont.FirstName : 'unknown'); + valuesMap.put('lastName', String.isNotBlank(cont.LastName) ? cont.LastName : 'unknown'); + valuesMap.put('email', String.isNotBlank(cont.Email) ? cont.Email : 'unknown'); + valuesMap.put('phone', String.isNotBlank(cont.Phone) ? cont.Phone : 'unknown'); // Serialize the map into a JSON string. + String json = JSON.serialize(valuesMap); // Make sure to check that required contacts fields have a value. Default the value to unknown if it does not exists. // Integration data can change over time. It is a best practice to add safeguards/validation to ensure the integration does not break. - return null; + return json; } } \ No newline at end of file diff --git a/force-app/main/default/classes/DummyJSONCallout.cls-meta.xml b/force-app/main/default/classes/DummyJSONCallout.cls-meta.xml index 754ecb1..f5e18fd 100644 --- a/force-app/main/default/classes/DummyJSONCallout.cls-meta.xml +++ b/force-app/main/default/classes/DummyJSONCallout.cls-meta.xml @@ -1,5 +1,5 @@ - 57.0 + 60.0 Active diff --git a/force-app/main/default/classes/DummyJSONCalloutTest.cls b/force-app/main/default/classes/DummyJSONCalloutTest.cls index 4d8b9e3..dbc6043 100644 --- a/force-app/main/default/classes/DummyJSONCalloutTest.cls +++ b/force-app/main/default/classes/DummyJSONCalloutTest.cls @@ -1,3 +1,7 @@ +/* + * Update the ContactTrigger.trigger to do a callout to the DummyJSONCallout class to retrieve/send user data from the Dummy JSON API. + * Implement the DummyJSONCallout class to handle the callouts to the Dummy JSON API. + */ @IsTest public with sharing class DummyJSONCalloutTest { // This test method will test the getDummyJSONUserFromId future method with a mock HTTP response. @@ -129,28 +133,35 @@ public with sharing class DummyJSONCalloutTest { @IsTest static void testContactInsert() { - Contact cont = new Contact(FirstName = 'Test', LastName = 'User'); + //Create 100 contact + List contacts = new List(); + for (Integer i = 0; i < 1; i++) { + contacts.add(new Contact(FirstName = 'Test', LastName = 'User' + i)); + } // Register the mock callout class Test.setMock(HttpCalloutMock.class, new DummyJSONCalloutMockGenerator()); // As this is a future method, we need to enclose it in Test.startTest() and Test.stopTest() to ensure it's executed in the test context. Test.startTest(); - insert cont; + insert contacts; Test.stopTest(); // After the stopTest, the future method will have run. Now we can check if the contact was created correctly. - cont = [ - SELECT Email, Phone, Birthdate, MailingStreet, MailingCity, MailingPostalCode, MailingState, MailingCountry + contacts = [ + SELECT DummyJSON_Id__c, Email, Phone, Birthdate, MailingStreet, MailingCity, MailingPostalCode, MailingState, MailingCountry FROM Contact - WHERE Id = :cont.Id + WHERE Id IN :contacts AND DummyJSON_Id__c != null ]; - System.assertEquals('test@example.com', cont.Email, 'Email does not match your value: ' + cont.Email); - System.assertEquals('+123456789', cont.Phone, 'Phone does not match your value: ' + cont.Phone); - System.assertEquals( - cont.Birthdate, - Date.valueOf('1990-01-01'), - 'Birthdate does not match your value: ' + cont.Birthdate - ); + Assert.isTrue(!contacts.isEmpty(), 'No contacts were created'); + + for (Contact cont : contacts) { + Assert.isTrue(cont.DummyJSON_Id__c.isNumeric(), 'DummyJSON_Id is not numeric string'); + Integer dummyJSONId = Integer.valueOf(cont.DummyJSON_Id__c); + Assert.isTrue( + dummyJSONId >=0 && dummyJSONId <= 100, + 'Expected range of DummyJSON_Id is [0-100]' + ); + } } } \ No newline at end of file diff --git a/force-app/main/default/classes/TriggerHandler.cls b/force-app/main/default/classes/TriggerHandler.cls new file mode 100644 index 0000000..57c4774 --- /dev/null +++ b/force-app/main/default/classes/TriggerHandler.cls @@ -0,0 +1,240 @@ +public virtual class TriggerHandler { + + // static map of handlername, times run() was invoked + private static Map loopCountMap; + private static Set bypassedHandlers; + + // the current context of the trigger, overridable in tests + @TestVisible + private TriggerContext context; + + // the current context of the trigger, overridable in tests + @TestVisible + private Boolean isTriggerExecuting; + + // static initialization + static { + loopCountMap = new Map(); + bypassedHandlers = new Set(); + } + + // constructor + public TriggerHandler() { + this.setTriggerContext(); + } + + /*************************************** + * public instance methods + ***************************************/ + + // main method that will be called during execution + public void run() { + + if(!validateRun()) { + return; + } + + addToLoopCount(); + + // dispatch to the correct handler method + switch on this.context { + when BEFORE_INSERT { + this.beforeInsert(); + } + when BEFORE_UPDATE { + this.beforeUpdate(); + } + when BEFORE_DELETE { + this.beforeDelete(); + } + when AFTER_INSERT { + this.afterInsert(); + } + when AFTER_UPDATE { + this.afterUpdate(); + } + when AFTER_DELETE { + this.afterDelete(); + } + when AFTER_UNDELETE { + this.afterUndelete(); + } + } + } + + public void setMaxLoopCount(Integer max) { + String handlerName = getHandlerName(); + if(!TriggerHandler.loopCountMap.containsKey(handlerName)) { + TriggerHandler.loopCountMap.put(handlerName, new LoopCount(max)); + } else { + TriggerHandler.loopCountMap.get(handlerName).setMax(max); + } + } + + public void clearMaxLoopCount() { + this.setMaxLoopCount(-1); + } + + /*************************************** + * public static methods + ***************************************/ + + public static void bypass(String handlerName) { + TriggerHandler.bypassedHandlers.add(handlerName); + } + + public static void clearBypass(String handlerName) { + TriggerHandler.bypassedHandlers.remove(handlerName); + } + + public static Boolean isBypassed(String handlerName) { + return TriggerHandler.bypassedHandlers.contains(handlerName); + } + + public static void clearAllBypasses() { + TriggerHandler.bypassedHandlers.clear(); + } + + /*************************************** + * private instancemethods + ***************************************/ + + @TestVisible + private void setTriggerContext() { + this.setTriggerContext(null, false); + } + + @TestVisible + private void setTriggerContext(String ctx, Boolean testMode) { + if(!Trigger.isExecuting && !testMode) { + this.isTriggerExecuting = false; + return; + } else { + this.isTriggerExecuting = true; + } + + if((Trigger.isExecuting && Trigger.isBefore && Trigger.isInsert) || + (ctx != null && ctx == 'before insert')) { + this.context = TriggerContext.BEFORE_INSERT; + } else if((Trigger.isExecuting && Trigger.isBefore && Trigger.isUpdate) || + (ctx != null && ctx == 'before update')){ + this.context = TriggerContext.BEFORE_UPDATE; + } else if((Trigger.isExecuting && Trigger.isBefore && Trigger.isDelete) || + (ctx != null && ctx == 'before delete')) { + this.context = TriggerContext.BEFORE_DELETE; + } else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isInsert) || + (ctx != null && ctx == 'after insert')) { + this.context = TriggerContext.AFTER_INSERT; + } else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isUpdate) || + (ctx != null && ctx == 'after update')) { + this.context = TriggerContext.AFTER_UPDATE; + } else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isDelete) || + (ctx != null && ctx == 'after delete')) { + this.context = TriggerContext.AFTER_DELETE; + } else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isUndelete) || + (ctx != null && ctx == 'after undelete')) { + this.context = TriggerContext.AFTER_UNDELETE; + } + } + + // increment the loop count + @TestVisible + private void addToLoopCount() { + String handlerName = getHandlerName(); + if(TriggerHandler.loopCountMap.containsKey(handlerName)) { + Boolean exceeded = TriggerHandler.loopCountMap.get(handlerName).increment(); + if(exceeded) { + Integer max = TriggerHandler.loopCountMap.get(handlerName).max; + throw new TriggerHandlerException('Maximum loop count of ' + String.valueOf(max) + ' reached in ' + handlerName); + } + } + } + + // make sure this trigger should continue to run + @TestVisible + private Boolean validateRun() { + if(!this.isTriggerExecuting || this.context == null) { + throw new TriggerHandlerException('Trigger handler called outside of Trigger execution'); + } + return !TriggerHandler.bypassedHandlers.contains(getHandlerName()); + } + + @TestVisible + private String getHandlerName() { + return this.toString().substringBefore(':'); + } + + /*************************************** + * context methods + ***************************************/ + + // context-specific methods for override + @TestVisible + protected virtual void beforeInsert(){} + @TestVisible + protected virtual void beforeUpdate(){} + @TestVisible + protected virtual void beforeDelete(){} + @TestVisible + protected virtual void afterInsert(){} + @TestVisible + protected virtual void afterUpdate(){} + @TestVisible + protected virtual void afterDelete(){} + @TestVisible + protected virtual void afterUndelete(){} + + /*************************************** + * inner classes + ***************************************/ + + // inner class for managing the loop count per handler + @TestVisible + private class LoopCount { + private Integer max; + private Integer count; + + public LoopCount() { + this.max = 5; + this.count = 0; + } + + public LoopCount(Integer max) { + this.max = max; + this.count = 0; + } + + public Boolean increment() { + this.count++; + return this.exceeded(); + } + + public Boolean exceeded() { + return this.max >= 0 && this.count > this.max; + } + + public Integer getMax() { + return this.max; + } + + public Integer getCount() { + return this.count; + } + + public void setMax(Integer max) { + this.max = max; + } + } + + // possible trigger contexts + @TestVisible + private enum TriggerContext { + BEFORE_INSERT, BEFORE_UPDATE, BEFORE_DELETE, + AFTER_INSERT, AFTER_UPDATE, AFTER_DELETE, + AFTER_UNDELETE + } + + // exception class + public class TriggerHandlerException extends Exception {} + +} diff --git a/force-app/main/default/classes/TriggerHandler.cls-meta.xml b/force-app/main/default/classes/TriggerHandler.cls-meta.xml new file mode 100644 index 0000000..230482e --- /dev/null +++ b/force-app/main/default/classes/TriggerHandler.cls-meta.xml @@ -0,0 +1,10 @@ + + + 55.0 + + 3 + 5 + geopointe + + Active + diff --git a/force-app/main/default/classes/TriggerHandler_Test.cls b/force-app/main/default/classes/TriggerHandler_Test.cls new file mode 100644 index 0000000..da35855 --- /dev/null +++ b/force-app/main/default/classes/TriggerHandler_Test.cls @@ -0,0 +1,277 @@ +@isTest +private class TriggerHandler_Test { + + private static final String TRIGGER_CONTEXT_ERROR = 'Trigger handler called outside of Trigger execution'; + + private static String lastMethodCalled; + + private static TriggerHandler_Test.TestHandler handler; + + static { + handler = new TriggerHandler_Test.TestHandler(); + // override its internal trigger detection + handler.isTriggerExecuting = true; + } + + /*************************************** + * unit tests + ***************************************/ + + // contexts tests + + @isTest + static void testBeforeInsert() { + beforeInsertMode(); + handler.run(); + System.assertEquals('beforeInsert', lastMethodCalled, 'last method should be beforeInsert'); + } + + @isTest + static void testBeforeUpdate() { + beforeUpdateMode(); + handler.run(); + System.assertEquals('beforeUpdate', lastMethodCalled, 'last method should be beforeUpdate'); + } + + @isTest + static void testBeforeDelete() { + beforeDeleteMode(); + handler.run(); + System.assertEquals('beforeDelete', lastMethodCalled, 'last method should be beforeDelete'); + } + + @isTest + static void testAfterInsert() { + afterInsertMode(); + handler.run(); + System.assertEquals('afterInsert', lastMethodCalled, 'last method should be afterInsert'); + } + + @isTest + static void testAfterUpdate() { + afterUpdateMode(); + handler.run(); + System.assertEquals('afterUpdate', lastMethodCalled, 'last method should be afterUpdate'); + } + + @isTest + static void testAfterDelete() { + afterDeleteMode(); + handler.run(); + System.assertEquals('afterDelete', lastMethodCalled, 'last method should be afterDelete'); + } + + @isTest + static void testAfterUndelete() { + afterUndeleteMode(); + handler.run(); + System.assertEquals('afterUndelete', lastMethodCalled, 'last method should be afterUndelete'); + } + + @isTest + static void testNonTriggerContext() { + try{ + handler.run(); + System.assert(false, 'the handler ran but should have thrown'); + } catch(TriggerHandler.TriggerHandlerException te) { + System.assertEquals(TRIGGER_CONTEXT_ERROR, te.getMessage(), 'the exception message should match'); + } catch(Exception e) { + System.assert(false, 'the exception thrown was not expected: ' + e.getTypeName() + ': ' + e.getMessage()); + } + } + + // test bypass api + + @isTest + static void testBypassAPI() { + afterUpdateMode(); + + // test a bypass and run handler + TriggerHandler.bypass('TestHandler'); + handler.run(); + System.assertEquals(null, lastMethodCalled, 'last method should be null when bypassed'); + System.assertEquals(true, TriggerHandler.isBypassed('TestHandler'), 'test handler should be bypassed'); + resetTest(); + + // clear that bypass and run handler + TriggerHandler.clearBypass('TestHandler'); + handler.run(); + System.assertEquals('afterUpdate', lastMethodCalled, 'last method called should be afterUpdate'); + System.assertEquals(false, TriggerHandler.isBypassed('TestHandler'), 'test handler should be bypassed'); + resetTest(); + + // test a re-bypass and run handler + TriggerHandler.bypass('TestHandler'); + handler.run(); + System.assertEquals(null, lastMethodCalled, 'last method should be null when bypassed'); + System.assertEquals(true, TriggerHandler.isBypassed('TestHandler'), 'test handler should be bypassed'); + resetTest(); + + // clear all bypasses and run handler + TriggerHandler.clearAllBypasses(); + handler.run(); + System.assertEquals('afterUpdate', lastMethodCalled, 'last method called should be afterUpdate'); + System.assertEquals(false, TriggerHandler.isBypassed('TestHandler'), 'test handler should be bypassed'); + resetTest(); + } + + // instance method tests + + @isTest + static void testLoopCount() { + beforeInsertMode(); + + // set the max loops to 2 + handler.setMaxLoopCount(2); + + // run the handler twice + handler.run(); + handler.run(); + + // clear the tests + resetTest(); + + try { + // try running it. This should exceed the limit. + handler.run(); + System.assert(false, 'the handler should throw on the 3rd run when maxloopcount is 3'); + } catch(TriggerHandler.TriggerHandlerException te) { + // we're expecting to get here + System.assertEquals(null, lastMethodCalled, 'last method should be null'); + } catch(Exception e) { + System.assert(false, 'the exception thrown was not expected: ' + e.getTypeName() + ': ' + e.getMessage()); + } + + // clear the tests + resetTest(); + + // now clear the loop count + handler.clearMaxLoopCount(); + + try { + // re-run the handler. We shouldn't throw now. + handler.run(); + System.assertEquals('beforeInsert', lastMethodCalled, 'last method should be beforeInsert'); + } catch(TriggerHandler.TriggerHandlerException te) { + System.assert(false, 'running the handler after clearing the loop count should not throw'); + } catch(Exception e) { + System.assert(false, 'the exception thrown was not expected: ' + e.getTypeName() + ': ' + e.getMessage()); + } + } + + @isTest + static void testLoopCountClass() { + TriggerHandler.LoopCount lc = new TriggerHandler.LoopCount(); + System.assertEquals(5, lc.getMax(), 'max should be five on init'); + System.assertEquals(0, lc.getCount(), 'count should be zero on init'); + + lc.increment(); + System.assertEquals(1, lc.getCount(), 'count should be 1'); + System.assertEquals(false, lc.exceeded(), 'should not be exceeded with count of 1'); + + lc.increment(); + lc.increment(); + lc.increment(); + lc.increment(); + System.assertEquals(5, lc.getCount(), 'count should be 5'); + System.assertEquals(false, lc.exceeded(), 'should not be exceeded with count of 5'); + + lc.increment(); + System.assertEquals(6, lc.getCount(), 'count should be 6'); + System.assertEquals(true, lc.exceeded(), 'should not be exceeded with count of 6'); + } + + // private method tests + + @isTest + static void testGetHandlerName() { + System.assertEquals('TestHandler', handler.getHandlerName(), 'handler name should match class name'); + } + + // test virtual methods + + @isTest + static void testVirtualMethods() { + TriggerHandler h = new TriggerHandler(); + h.beforeInsert(); + h.beforeUpdate(); + h.beforeDelete(); + h.afterInsert(); + h.afterUpdate(); + h.afterDelete(); + h.afterUndelete(); + } + + /*************************************** + * testing utilities + ***************************************/ + + private static void resetTest() { + lastMethodCalled = null; + } + + // modes for testing + + private static void beforeInsertMode() { + handler.setTriggerContext('before insert', true); + } + + private static void beforeUpdateMode() { + handler.setTriggerContext('before update', true); + } + + private static void beforeDeleteMode() { + handler.setTriggerContext('before delete', true); + } + + private static void afterInsertMode() { + handler.setTriggerContext('after insert', true); + } + + private static void afterUpdateMode() { + handler.setTriggerContext('after update', true); + } + + private static void afterDeleteMode() { + handler.setTriggerContext('after delete', true); + } + + private static void afterUndeleteMode() { + handler.setTriggerContext('after undelete', true); + } + + // test implementation of the TriggerHandler + + private class TestHandler extends TriggerHandler { + + public override void beforeInsert() { + TriggerHandler_Test.lastMethodCalled = 'beforeInsert'; + } + + public override void beforeUpdate() { + TriggerHandler_Test.lastMethodCalled = 'beforeUpdate'; + } + + public override void beforeDelete() { + TriggerHandler_Test.lastMethodCalled = 'beforeDelete'; + } + + public override void afterInsert() { + TriggerHandler_Test.lastMethodCalled = 'afterInsert'; + } + + public override void afterUpdate() { + TriggerHandler_Test.lastMethodCalled = 'afterUpdate'; + } + + public override void afterDelete() { + TriggerHandler_Test.lastMethodCalled = 'afterDelete'; + } + + public override void afterUndelete() { + TriggerHandler_Test.lastMethodCalled = 'afterUndelete'; + } + + } + +} diff --git a/force-app/main/default/classes/TriggerHandler_Test.cls-meta.xml b/force-app/main/default/classes/TriggerHandler_Test.cls-meta.xml new file mode 100644 index 0000000..3b90cb4 --- /dev/null +++ b/force-app/main/default/classes/TriggerHandler_Test.cls-meta.xml @@ -0,0 +1,4 @@ + + + 55.0 + diff --git a/force-app/main/default/triggers/ContactTrigger.trigger b/force-app/main/default/triggers/ContactTrigger.trigger index 407c4bf..d9946be 100644 --- a/force-app/main/default/triggers/ContactTrigger.trigger +++ b/force-app/main/default/triggers/ContactTrigger.trigger @@ -16,13 +16,6 @@ * * Optional Challenge: Use a trigger handler class to implement the trigger logic. */ -trigger ContactTrigger on Contact(before insert) { - // When a contact is inserted - // if DummyJSON_Id__c is null, generate a random number between 0 and 100 and set this as the contact's DummyJSON_Id__c value - - //When a contact is inserted - // if DummyJSON_Id__c is less than or equal to 100, call the getDummyJSONUserFromId API - - //When a contact is updated - // if DummyJSON_Id__c is greater than 100, call the postCreateDummyJSONUser API +trigger ContactTrigger on Contact(before insert, after insert, before update, after update) { + new ContactTriggerHandler().run(); } \ No newline at end of file