diff --git a/.github/workflows/create-org-and-deploy.yaml b/.github/workflows/create-org-and-deploy.yaml index 3f560140563..511229cb859 100644 --- a/.github/workflows/create-org-and-deploy.yaml +++ b/.github/workflows/create-org-and-deploy.yaml @@ -68,7 +68,7 @@ jobs: # Run All Unit Tests - name: Run All Unit Tests - run: sfdx force:apex:test:run -r human -u "${{env.ORG_ALIAS_PREFIX}}${{github.run_number}}" --wait 20 | grep -v ' Pass '; test ${PIPESTATUS[0]} -eq 0 + run: sfdx force:apex:test:run -r human -u "${{env.ORG_ALIAS_PREFIX}}${{github.run_number}}" --codecoverage --wait 20 | grep -v ' Pass '; test ${PIPESTATUS[0]} -eq 0 # Delete Scratch Org diff --git a/TODO.txt b/TODO.txt index 8126376cab6..6492b4af21e 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,15 +6,14 @@ Licenses that are needed with the source code and binary: Look at the use of 'MockDatabase' in fflib -* To finalise the core architecture: - * Decide on FLS standards - * Do we need to have a non all-or-nothing version of commitWork? +Test: + Unit of Work: elevatedContext -Add reference to disabling individual trigger events in tests: - https://andyinthecloud.com/2016/04/13/disabling-trigger-events-in-apex-enterprise-patterns/ +Look at: + SobjectUtils.getSobjectName -Add to Framework plan - * Ability to configure different factory mappings per user / permission set / custom permission / whatever +* To finalise the core architecture: + * Do we need to have a non all-or-nothing version of commitWork? Add to documentation * Wrapping exceptions on the way out of services @@ -28,24 +27,23 @@ Add to documentation * Do not do domain logic in them * Using the Mock Registarar - * Describe the Application Factories From Utilities, things that may be useful: -* getReferenceObjectAPIName -* getObjName - get the object name from an Id -* getLabel / getObjectLabel - get the label for an sobject -* getFieldLabel -* delimitedStringToSet and reverse - * escaping single quotes - in both directions? -* unitsBetweenDateTime -* emailAddressIsValid / emailAddressListIsValid -* sObjectIsCustom / sObjectIsCustomfromAPIName -* IsfieldFilterable -* isFieldCustom -* idIsValid -* getCrossObjectAPIName -* objectFieldExist -* sortSelectOptions - complete re-write + * getReferenceObjectAPIName + * getObjName - get the object name from an Id + * getLabel / getObjectLabel - get the label for an sobject + * getFieldLabel + * delimitedStringToSet and reverse + * escaping single quotes - in both directions? + * unitsBetweenDateTime + * emailAddressIsValid / emailAddressListIsValid + * sObjectIsCustom / sObjectIsCustomfromAPIName + * IsfieldFilterable + * isFieldCustom + * idIsValid + * getCrossObjectAPIName + * objectFieldExist + * sortSelectOptions - complete re-write Write tests for the SOQL generation in the criteria library @@ -72,10 +70,6 @@ Bad Smells - strung out calls to describe methods - put them into SobjectUtils * Question: How do we handle Constants - where are they defined? * Question: How do we handle Exceptions: -* Question: How do we handle FLS - what are the rules? - - * Where are they defined? - * Do we want to pass a single type of exception (e.g. ServiceException back to the client)? * Do we handle individual types of exception in the Service? * Do we pass any of the domain exceptions back @@ -86,22 +80,11 @@ Bad Smells - strung out calls to describe methods - put them into SobjectUtils * Question: Do you need Heap size management rules - - * Produce test-case for lack of clickthrough and raise bug on VSCode * How do we handle constants? * Dynamic building of the Unit Of Work for processing data -Standards? -* Selector name - singular or plural? -* When you have a class that wraps Sobject, what are the naming convensions - which one is called 'xxxObject'? -* Creating a service that performs DML - always provide an unwrapped version that takes a UOW -* Interfaces start with an I - Notes: -* Services should always be designed by seniors - at least which services exist - -What's missing: -* Custom metadata driven execution of trigger code (register multiple trigger handlers) -* How does QueryHandler drop into this? - Maintenance of change of state \ No newline at end of file +* Services should always be designed by seniors - at least which services exist \ No newline at end of file diff --git a/framework/default/fflib/default/classes/common/fflib_SObjectSelector.cls b/framework/default/fflib/default/classes/common/fflib_SObjectSelector.cls index 4f981bf2a3a..00bc12d448d 100644 --- a/framework/default/fflib/default/classes/common/fflib_SObjectSelector.cls +++ b/framework/default/fflib/default/classes/common/fflib_SObjectSelector.cls @@ -52,7 +52,7 @@ public abstract with sharing class fflib_SObjectSelector /** * Enforce FLS Security **/ - private Boolean m_enforceFLS = false; + protected Boolean m_enforceFLS = false; /** * Enforce CRUD Security diff --git a/framework/default/ortoo-core/default/classes/FrameworkErrorCodes.cls b/framework/default/ortoo-core/default/classes/FrameworkErrorCodes.cls index 470b25b8d5b..70fc8349e9e 100644 --- a/framework/default/ortoo-core/default/classes/FrameworkErrorCodes.cls +++ b/framework/default/ortoo-core/default/classes/FrameworkErrorCodes.cls @@ -21,4 +21,10 @@ public inherited sharing class FrameworkErrorCodes { public final static String CONFIGURATION_WITH_INVALID_TYPE = 'APP-00001'; public final static String CONFIGURATION_WITH_INVALID_CLASS = 'APP-00002'; public final static String CONFIGURATION_WITH_INVALID_SOBJECT_TYPE = 'APP-00003'; + + public final static String DML_ON_INACCESSIBLE_FIELDS = '00000'; + public final static String DML_INSERT_NOT_ALLOWED = '00001'; + public final static String DML_UPDATE_NOT_ALLOWED = '00002'; + public final static String DML_DELETE_NOT_ALLOWED = '00003'; + public final static String DML_PUBLISH_NOT_ALLOWED = '00004'; } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/exceptions/ortoo_Exception.cls b/framework/default/ortoo-core/default/classes/exceptions/ortoo_Exception.cls index 8d67e5d33b7..690c0ea88a4 100644 --- a/framework/default/ortoo-core/default/classes/exceptions/ortoo_Exception.cls +++ b/framework/default/ortoo-core/default/classes/exceptions/ortoo_Exception.cls @@ -175,11 +175,27 @@ public virtual class ortoo_Exception extends Exception implements IRenderableMes return returnList; } + /** + * Will regenerate the stack trace string that is used for generating the StackTrace object. + * + * Is useful when you want to raise an exception in one method and have it report as being thrown by + * a parent method - for example in a handler that has been delegated to throw exceptions. + * See SecureDml.ErrorOnFlsViolationHandler.handleInaccessibleFields as an example. + * + * @param Integer The number of levels upwards in the stack trace to skip. + * @return ortoo_Exception Itself, allowing for a fluent interface + */ + public ortoo_Exception regenerateStackTraceString( Integer levelsToSkip ) + { + stackTraceString = createStackTraceString( levelsToSkip + 1 ); + return this; + } + private String createStackTraceString( Integer levelsToSkip ) { - return new StackTrace( levelsToSkip+1 ).getFullStackTraceString(); // Since custom exceptions have some problems getting their stack trace strings set, - // we need to get the Stack Trace string from the generic utility, stating that the top - // level method (this one) should be ignored. + return new StackTrace( levelsToSkip + 1 ).getFullStackTraceString(); // Since custom exceptions have some problems getting their stack trace strings set, + // we need to get the Stack Trace string from the generic utility, stating that the top + // level method (this one) should be ignored. } /** diff --git a/framework/default/ortoo-core/default/classes/exceptions/tests/ortoo_ExceptionTest.cls b/framework/default/ortoo-core/default/classes/exceptions/tests/ortoo_ExceptionTest.cls index 10a9246bf22..da557620d1d 100644 --- a/framework/default/ortoo-core/default/classes/exceptions/tests/ortoo_ExceptionTest.cls +++ b/framework/default/ortoo-core/default/classes/exceptions/tests/ortoo_ExceptionTest.cls @@ -118,6 +118,50 @@ public with sharing class ortoo_ExceptionTest { Amoss_Asserts.assertContains( 'ortoo_ExceptionTest.getStackTraceString_whenSubclassedAndMultipleMethods_willReturnTheStackTrace', stackTrace, 'getStackTraceString, when the exception is subclassed and there are multiple layers of methods, will return the Stack Trace based on where the exception was raised - with each level (method 3)' ); } + @isTest + private static void regenerateStackTraceString_willCreateANewStackTraceFromTheGivenPoint() + { + String stackTrace; + Test.startTest(); + try + { + throwASubclassedException(); + } + catch ( SubclassedException e ) + { + e.regenerateStackTraceString( 0 ); + stackTrace = e.getStackTraceString(); + } + Test.stopTest(); + + Amoss_Asserts.assertDoesNotContain( '', stackTrace, 'regenerateStackTraceString, when called, will create a new stack trace from the given point' ); + Amoss_Asserts.assertDoesNotContain( 'ortoo_ExceptionTest.throwASubclassedExceptionInnerMethodCall', stackTrace, 'regenerateStackTraceString, when called, will create a new stack trace from the given point (method 1)' ); + Amoss_Asserts.assertDoesNotContain( 'ortoo_ExceptionTest.throwASubclassedException', stackTrace, 'regenerateStackTraceString, when called, will create a new stack trace from the given point (method 2)' ); + Amoss_Asserts.assertContains( 'ortoo_ExceptionTest.regenerateStackTraceString_willCreateANewStackTraceFromTheGivenPoint', stackTrace, 'regenerateStackTraceString, when called, will create a new stack trace from the given point (actual spot called)' ); + } + + @isTest + private static void regenerateStackTraceString_whenPassedANumber_willCreateANewStackTraceWithLevelsSkipped() + { + Integer levelsToSkip = 1; + + String stackTrace; + Test.startTest(); + try + { + throwASubclassedException( levelsToSkip ); + } + catch ( SubclassedException e ) + { + stackTrace = e.getStackTraceString(); + } + Test.stopTest(); + + Amoss_Asserts.assertDoesNotContain( 'ortoo_ExceptionTest.throwASubclassedExceptionInnerMethodCall', stackTrace, 'regenerateStackTraceString, when called with a number of levels to skipp, will create a new stack trace from the given point, skipping the specified number of levels (1st level is skipped)' ); + Amoss_Asserts.assertContains( 'ortoo_ExceptionTest.throwASubclassedException', stackTrace, 'regenerateStackTraceString, when called with a number of levels to skipp, will create a new stack trace from the given point, skipping the specified number of levels (2nd level is not skipped)' ); + Amoss_Asserts.assertContains( 'ortoo_ExceptionTest.regenerateStackTraceString_whenPassedANumber_willCreateANewStackTraceWithLevelsSkipped', stackTrace, 'regenerateStackTraceString, when called with a number of levels to skipp, will create a new stack trace from the given point, skipping the specified number of levels (3rd level is not skipped)' ); + } + @isTest private static void addContext_whenCalled_willAddItToTheExceptionWithStackInfo() // NOPMD: Test method name format { @@ -261,6 +305,48 @@ public with sharing class ortoo_ExceptionTest { System.assertEquals( true, exceptionCaught, 'next, when there are no entries in the list, will throw a NoSuchElementException exception' ); } + @isTest + private static void getMessageDetails_whenMessageDetailsAdded_willReturnThoseDetails() // NOPMD: Test method name format + { + ortoo_Exception exceptionUnderTest = new ortoo_Exception( 'message' ); + + List objectContexts = new List + { + new Contact( Id = TestIdUtils.generateId( Contact.sobjectType ) ), + new Contact( Id = TestIdUtils.generateId( Contact.sobjectType ) ), + new Contact( Id = TestIdUtils.generateId( Contact.sobjectType ) ) + }; + + List messageDetails = new List + { + new MessageDetail( objectContexts[0], 'message1' ), + new MessageDetail( objectContexts[1], 'message2' ), + new MessageDetail( objectContexts[1], 'message3' ), + new MessageDetail( objectContexts[0], 'message4' ) + }; + + Test.startTest(); + exceptionUnderTest.setMessageDetails( messageDetails ); + List returnedMessageDetails = exceptionUnderTest.getMessageDetails(); + + + Test.stopTest(); + + System.assertEquals( messageDetails, returnedMessageDetails, 'getMessageDetails, when some message details have been added, will return those details' ); + } + + @isTest + private static void getMessageDetails_whenNoMessageDetailsAdded_willReturnAnEmptyList() // NOPMD: Test method name format + { + ortoo_Exception exceptionUnderTest = new ortoo_Exception( 'message' ); + + Test.startTest(); + List returnedMessageDetails = exceptionUnderTest.getMessageDetails(); + Test.stopTest(); + + System.assertEquals( 0, returnedMessageDetails.size(), 'getMessageDetails, when no message details have been added, will return an empty list' ); + } + @isTest private static void getObjectContext_whenMessageDetailsAdded_willReturnTheSobjects() // NOPMD: Test method name format { @@ -364,11 +450,21 @@ public with sharing class ortoo_ExceptionTest { throw new SubclassedException(); } + private static void throwASubclassedExceptionInnerMethodCall( Integer levelsToSkip ) + { + throw new SubclassedException().regenerateStackTraceString( levelsToSkip ); + } + private static void throwASubclassedException() { throwASubclassedExceptionInnerMethodCall(); } + private static void throwASubclassedException( Integer levelsToSkip ) + { + throwASubclassedExceptionInnerMethodCall( levelsToSkip); + } + private static String getClassName() { return String.valueOf( ortoo_ExceptionTest.class ); diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/SecureDml.cls b/framework/default/ortoo-core/default/classes/fflib-extension/SecureDml.cls new file mode 100644 index 00000000000..6900822e2ae --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/SecureDml.cls @@ -0,0 +1,628 @@ +/** + * Is an implementation of the IDml interface used to manage the DML operations in an SObject Unit of Work. + * + * Implementation is secure by default, ensuring that FLS and CRUD security is adhered to. + * + * Allows ther turning off of security at multiple levels: + * FLS checking for a given field + * FLS checking for a given SObject Type + * FLS checking for all SObjects + * CRUD checking for a given SObject Type + * CRUD checking for all SObjects + */ +public inherited sharing virtual class SecureDml extends fflib_SobjectUnitOfWork.SimpleDML implements fflib_SobjectUnitOfWork.IDml +{ + public inherited sharing class SecureDmlException extends ortoo_Exception + { + protected virtual override String resolveErrorCode( String errorCode ) + { + return 'DML-' + errorCode; + } + } + + /** + * Interface that defines a handler for if and when a CRUD violation occurs. + */ + public interface CrudViolationHandler + { + void handleUnableToInsertRecords( List objList ); + void handleUnableToUpdateRecords( List objList ); + void handleUnableToDeleteRecords( List objList ); + void handleUnableToPublishEvents( List objList ); + } + + /** + * Interface that defines a handler for if and when an FLS violation occurs. + */ + public interface FlsViolationHandler + { + void handleInaccessibleFields( AccessType mode, SobjectType sobjectType, Set fieldsInViolation ); + } + + FlsViolationHandler flsViolationHandler; + CrudViolationHandler crudViolationHandler; + + Boolean ignoreCrud = false; + Set ignoreCrudForSobjectTypes = new Set(); + + Boolean ignoreFls = false; + Set ignoreFlsForSobjectTypes = new Set(); + Map> ignoreFlsForFields = new Map>(); + + /** + * Default constructor that ensures that security violations result in Exceptions being thrown + */ + public SecureDml() + { + setFlsViolationHandler( new ErrorOnFlsViolationHandler() ); + setCrudViolationHandler( new ErrorOnCrudViolationHandler() ); + } + + /** + * Set the Handler that should be used when an FLS violoation occurs + * + * @param FlsViolationHandler The FLS violation handler to use + * @return SecureDml Itself, allowing for a fluent interface + */ + public SecureDml setFlsViolationHandler( FlsViolationHandler flsViolationHandler ) + { + this.flsViolationHandler = flsViolationHandler; + return this; + } + + /** + * Set the Handler that should be used when an CRUD violation occurs + * + * @param FlsViolationHandler The FLS violation handler to use + * @return SecureDml Itself, allowing for a fluent interface + */ + public SecureDml setCrudViolationHandler( CrudViolationHandler crudViolationHandler ) + { + this.crudViolationHandler = crudViolationHandler; + return this; + } + + /** + * Disables FLS checking for this instance + * + * @return SecureDml Itself, allowing for a fluent interface + */ + public SecureDml ignoreFls() + { + ignoreFls = true; + return this; + } + + /** + * Disables FLS checking for all records of the given SObject Type + * + * @param SobjectType The Sobject Type for which to disable FLS + * @return SecureDml Itself, allowing for a fluent interface + */ + public SecureDml ignoreFlsFor( SobjectType type ) + { + Contract.requires( type != null, 'ignoreFlsFor called with a null type' ); + + ignoreFlsForSobjectTypes.add( type ); + return this; + } + + /** + * Disables FLS checking for all records of the given SObject Field + * + * @param SobjectType The Sobject Type for which to disable FLS + * @param SobjectField The Sobject Field for which to disable FLS + * @return SecureDml Itself, allowing for a fluent interface + */ + public SecureDml ignoreFlsFor( SobjectType type, SobjectField field ) + { + Contract.requires( type != null, 'ignoreFlsFor called with a null type' ); + Contract.requires( field != null, 'ignoreFlsFor called with a null field' ); + + if ( ! ignoreFlsForFields.containsKey( type ) ) + { + ignoreFlsForFields.put( type, new Set() ); + } + ignoreFlsForFields.get( type ).add( field.getDescribe().getName() ); // SobjectUtils? + return this; + } + + /** + * Disables CRUD settings checking for this instance + * + * @return SecureDml Itself, allowing for a fluent interface + */ + public SecureDml ignoreCrud() + { + ignoreCrud = true; + return this; + } + + /** + * Disables CRUD settings checking for all records of the given SObject Type + * + * @param SobjectType The Sobject Type for which to disable CRUD checking + * @return SecureDml Itself, allowing for a fluent interface + */ + public SecureDml ignoreCrudFor( SobjectType type ) + { + Contract.requires( type != null, 'ignoreCrudFor called with a null type' ); + + ignoreCrudForSobjectTypes.add( type ); + return this; + } + + /** + * Performs the DML Insert, whilst also checking CRUD and FLS rights, based on the configuration. + * + * @param List The list of records to insert + */ + public override void dmlInsert( List objList ) + { + if ( objList.isEmpty() ) + { + return; + } + + SobjectType type = SobjectUtils.getSobjectType( objList[0] ); + + if ( shouldCheckCrud( type ) && ! userCanCreate( objList[0] ) ) + { + crudViolationHandler.handleUnableToInsertRecords( objList ); + return; + } + + if ( shouldCheckFls( type ) ) + { + checkFls( objList, AccessType.CREATABLE ); + } + + doInsert( objList ); + } + + protected virtual void doInsert( List objList ) + { + insert objList; + } + + /** + * Performs the DML Update, whilst also checking CRUD and FLS rights, based on the configuration. + * + * @param List The list of records to update + */ + public override void dmlUpdate( List objList ) + { + if ( objList.isEmpty() ) + { + return; + } + + SobjectType type = SobjectUtils.getSobjectType( objList[0] ); + + if ( shouldCheckCrud( type ) && ! userCanUpdate( objList[0] ) ) + { + crudViolationHandler.handleUnableToUpdateRecords( objList ); + return; + } + + if ( shouldCheckFls( type ) ) + { + checkFls( objList, AccessType.UPDATABLE ); + } + doUpdate( objList ); + } + + protected virtual void doUpdate( List objList ) + { + update objList; + } + + /** + * Performs the DML Delete, whilst also checking CRUD and FLS rights, based on the configuration. + * + * @param List The list of records to insert + */ + public override void dmlDelete( List objList ) + { + if ( objList.isEmpty() ) + { + return; + } + + SobjectType type = SobjectUtils.getSobjectType( objList[0] ); + + if ( shouldCheckCrud( type ) && ! userCanDelete( objList[0] ) ) + { + crudViolationHandler.handleUnableToDeleteRecords( objList ); + return; + } + doDelete( objList ); + } + + protected virtual void doDelete( List objList ) + { + delete objList; + } + + /** + * Performs the DML Publish, whilst also checking CRUD and FLS rights, based on the configuration. + * + * @param List The list of records to insert + */ + public override void eventPublish( List objList ) + { + if ( objList.isEmpty() ) + { + return; + } + + SobjectType type = SobjectUtils.getSobjectType( objList[0] ); + + if ( shouldCheckCrud( type ) && ! userCanPublish( objList[0] ) ) + { + crudViolationHandler.handleUnableToPublishEvents( objList ); + return; + } + + doPublish( objList ); + } + + protected virtual void doPublish( List objList ) + { + EventBus.publish( objList ); + } + + /** + * Checks the FLS for the given Sobject, in the given mode. + * In the case of violation, report it to the flsViolationHandler. + * + * @param List The list of records for which to to check the FLS + * @param AccessType The access type that needs to be checked + */ + private void checkFls( List objList, AccessType mode ) + { + String sobjectTypeName = SobjectUtils.getSobjectName( objList[0] ); + SecurityDecision securityDecision = stripInaccessible( mode, objList ); + + if ( securityDecision.fieldsWereRemoved( sobjectTypeName ) ) + { + Set removedFields = securityDecision.getRemovedFieldsFor( sobjectTypeName ); + List strippedRecords = securityDecision.getRecords(); + + removedFields = unstripAccessible( removedFields, objList, strippedRecords ); + + if ( ! removedFields.isEmpty() ) + { + flsViolationHandler.handleInaccessibleFields( mode, SobjectUtils.getSobjectType( objList[0] ), removedFields ); + replaceList( objList, strippedRecords ); + } + } + } + + /** + * Reviews the configured 'ignoreFlsFor' fields for the given records and puts back any populated fields that should have been ignored. + * + * Is needed since we can't tell stripInaccessible to skip checking of certain fields. + * + * Will mutate the stripped records so that they now contain the specified field values again. + * + * @param Set The fields that were previously removed from the records + * @param List The original list of records, prior to field values being stripped + * @param List The new list of records, after the field values were stripped + * @return Set The new, potentially reduced list of 'removed fields + */ + @testVisible + private Set unstripAccessible( Set removedFields, List originalRecords, List strippedRecords ) + { + SobjectType type = SobjectUtils.getSobjectType( originalRecords[0] ); + + Set ignoredFlsFields = getIgnoredFlsFields( type ); + + Set fieldsToUnstrip = removedFields.clone(); + fieldsToUnstrip.retainAll( ignoredFlsFields ); + + Set remainingRemovedFields = removedFields.clone(); + remainingRemovedFields.removeAll( fieldsToUnstrip ); + + if ( remainingRemovedFields.isEmpty() ) // nothing should have been stripped, so the original records are OK + { + replaceList( strippedRecords, originalRecords ); + return remainingRemovedFields; + } + + if ( fieldsToUnstrip.isEmpty() ) // nothing needs to be put back, so the original stripped records are OK + { + return remainingRemovedFields; + } + + for ( Integer i=0; i < originalRecords.size(); i++ ) + { + Sobject thisOriginalRecord = originalRecords[i]; + Sobject thisStrippedRecord = strippedRecords[i]; + + Map populatedFields = thisOriginalRecord.getPopulatedFieldsAsMap(); + + for ( String thisFieldToUnstrip : fieldsToUnstrip ) + { + if ( populatedFields.containsKey( thisFieldToUnstrip ) ) + { + thisStrippedRecord.put( thisFieldToUnstrip, thisOriginalRecord.get( thisFieldToUnstrip ) ); + } + } + } + + return remainingRemovedFields; + } + + /** + * States if CRUD settings should be checked for the given SObject type + * + * @param SobjectType The type for which to ascertain if CRUD should be checked + * @return Boolean Should CRUD be checked + */ + private Boolean shouldCheckCrud( SobjectType type ) + { + if ( ignoreCrud ) + { + return false; + } + if ( ignoreCrudForSobjectTypes.isEmpty() ) + { + return true; + } + return ! ignoreCrudForSobjectTypes.contains( type ); + } + + /** + * States if FLS settings should be checked for the given SObject type + * + * @param SobjectType The type for which to ascertain if FLS should be checked + * @return Boolean Should FLS be checked + */ + private Boolean shouldCheckFls( SobjectType type ) + { + if ( ignoreFls ) + { + return false; + } + if ( ignoreFlsForSobjectTypes.isEmpty() ) + { + return true; + } + return ! ignoreFlsForSobjectTypes.contains( type ); + } + + /** + * Returns the Set of fields that should have FLS ignored, for the given SObject type + * + * @param SobjectType The type for which the ignored fields should be returned + * @return Set The fields to ignore the FLS of + */ + private Set getIgnoredFlsFields( SobjectType type ) + { + Set fieldsToIgnore = ignoreFlsForFields.get( type ); + + if ( fieldsToIgnore == null ) + { + fieldsToIgnore = new Set(); + } + return fieldsToIgnore; + } + + /** + * Given two lists, will update the first so that is contains the values in the second list. + * + * Allows parameters to be mutated with new values easily. + * + * Requires that the lists be of the same length. + * + * @param List The original list to be replaced + * @param List The new list to replace the original list with + */ + private static void replaceList( List originalList, List newList ) + { + Contract.requires( originalList != null, 'replaceList called with a null originalList' ); + Contract.requires( newList != null, 'replaceList called with a null newList' ); + Contract.requires( originalList.size() == newList.size(), 'replaceList called with lists that are different sizes' ); + + for ( Integer i=0; i < originalList.size(); i++ ) + { + originalList[i] = newList[i]; + } + } + + // This method cannot be reliably unit tested in a framework that does not inculde profiles and suchlike + // Is implemented as virtual so tests can override it and drive behaviour + protected virtual Boolean userCanCreate( Sobject record ) + { + return SobjectUtils.isCreateable( record ); + } + + // This method cannot be reliably unit tested in a framework that does not inculde profiles and suchlike + // Is implemented as virtual so tests can override it and drive behaviour + protected virtual Boolean userCanPublish( Sobject event ) + { + return SobjectUtils.isCreateable( event ); + } + + // This method cannot be reliably unit tested in a framework that does not inculde profiles and suchlike + // Is implemented as virtual so tests can override it and drive behaviour + protected virtual Boolean userCanUpdate( Sobject record ) + { + return SobjectUtils.isUpdateable( record ); + } + + // This method cannot be reliably unit tested in a framework that does not inculde profiles and suchlike + // Is implemented as virtual so tests can override it and drive behaviour + protected virtual Boolean userCanDelete( Sobject record ) + { + return SobjectUtils.isUpdateable( record ); + } + + // This method cannot be reliably unit tested in a framework that does not inculde profiles and suchlike. + // Is implemented as virtual so tests can override it and drive behaviour + protected virtual SecurityDecision stripInaccessible( AccessType mode, List objList ) + { + return new SecurityDecision( Security.stripInaccessible( mode, objList, false ) ); + } + + /** + * Wrapper for SObjectAccessDecision, the result of a Security.stripInaccessible call. + * Allows tests to change the behaviour of the FLS call without requiring any FLS to be set up. + * That is, SObjectAccessDecision cannot be created and configured, but the SecurityDecision + * inner class can. + */ + @testVisible + private inherited sharing class SecurityDecision + { + Map> removedFields; + List records; + + /** + * Constructs the SecurityDecision with the result from a Security.stripInaccessible call. + * + * @param SObjectAccessDecision The decision to wrap + */ + public SecurityDecision( SObjectAccessDecision securityDecision ) + { + this( securityDecision.getRemovedFields(), securityDecision.getRecords() ); + } + + /** + * Constructs explicitly defined remove field lists and transformed records + * + * @param Map> The fields that were removed, indexed by the SObject Name + * @param List The records with the specified fields removed + */ + @testVisible + private SecurityDecision( Map> removedFields, List records ) + { + this.removedFields = removedFields; + this.records = records; + } + + /** + * States if fields were removed from the given Sobject Type + * + * @param String The Name of the Sobject Type to check + * @return Boolean Were fields removed + */ + public Boolean fieldsWereRemoved( String sobjectTypeName ) + { + return !removedFields.isEmpty() && removedFields.containsKey( sobjectTypeName ); + } + + /** + * Returns the new version of the records with the inaccessible fields stripped + * + * @return List The new version of the records + */ + public List getRecords() + { + return records; + } + + /** + * Returns the fields that were removed from the Sobjects of the given type + * + * @param String The Sobject Type name to get the removed fields for + * @return Set The names of the removed fields + */ + public Set getRemovedFieldsFor( String sobjectTypeName ) + { + if ( removedFields.containsKey( sobjectTypeName ) ) + { + return removedFields.get( sobjectTypeName ); + } + return new Set(); + } + } + + /** + * CrudViolationHandler that ensures that exceptions are thrown when CRUD violations occur + */ + public inherited sharing virtual class ErrorOnCrudViolationHandler implements CrudViolationHandler + { + /** + * Throws an exception describing the insert CRUD violation + * + * @param List The list of SObjects that caused the violation. + */ + public void handleUnableToInsertRecords( List objList ) + { + throwUnableException( FrameworkErrorCodes.DML_INSERT_NOT_ALLOWED, Label.ortoo_core_crud_insert_violation, objList ); + } + + /** + * Throws an exception describing the update CRUD violation + * + * @param List The list of SObjects that caused the violation. + */ + public void handleUnableToUpdateRecords( List objList ) + { + throwUnableException( FrameworkErrorCodes.DML_UPDATE_NOT_ALLOWED, Label.ortoo_core_crud_update_violation, objList ); + } + + /** + * Throws an exception describing the delete CRUD violation + * + * @param List The list of SObjects that caused the violation. + */ + public void handleUnableToDeleteRecords( List objList ) + { + throwUnableException( FrameworkErrorCodes.DML_DELETE_NOT_ALLOWED, Label.ortoo_core_crud_delete_violation, objList ); + } + + /** + * Throws an exception describing the publish CRUD violation + * + * @param List The list of SObjects that caused the violation. + */ + public void handleUnableToPublishEvents( List objList ) + { + throwUnableException( FrameworkErrorCodes.DML_PUBLISH_NOT_ALLOWED, Label.ortoo_core_crud_publish_violation, objList ); + } + + private void throwUnableException( String errorCode, String label, List objList ) + { + String sobjectTypeName = SobjectUtils.getSobjectName( objList[0] ); + throw new SecureDmlException( StringUtils.formatLabel( label, new List{ sobjectTypeName } ) ) + .setErrorCode( errorCode ) + .addContext( 'sobjectTypeName', sobjectTypeName ) + .addContext( 'records', objList ) + .regenerateStackTraceString( 2 ); // push the stack trace string into the point that called the hander, rather than the handler itself + } + } + + /** + * FlsViolationHandler that ensures that exceptions are thrown when FLS violations occur + */ + public inherited sharing virtual class ErrorOnFlsViolationHandler implements FlsViolationHandler + { + /** + * Throws an exception describing the FLS violation + * + * @param AccessType The mode of the operation that was being performed when the violation occurred + * @param SobjectType The Sobject Type that the violation occurred against + * @param Set The names of the fields that violated FLS + */ + public void handleInaccessibleFields( AccessType mode, SobjectType sobjectType, Set fieldsInViolation ) + { + Map descriptionByMode = new Map{ + AccessType.CREATABLE => Label.ortoo_core_insert, + AccessType.UPDATABLE => Label.ortoo_core_update, + AccessType.UPSERTABLE => Label.ortoo_core_upsert + }; + + String label = Label.ortoo_core_fls_violation; + String modeDescription = descriptionByMode.get( mode ); + String sobjectTypeName = SobjectUtils.getSobjectName( sobjectType ); + + throw new SecureDmlException( StringUtils.formatLabel( label, new List{ modeDescription, sobjectTypeName, fieldsInViolation.toString() } ) ) + .setErrorCode( FrameworkErrorCodes.DML_ON_INACCESSIBLE_FIELDS ) + .addContext( 'sobjectTypeName', sobjectTypeName ) + .addContext( 'fieldsInViolation', fieldsInViolation ) + .regenerateStackTraceString( 2 ); // push the stack trace string into the point that called the hander, rather than the handler itself + } + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/SecureDml.cls-meta.xml b/framework/default/ortoo-core/default/classes/fflib-extension/SecureDml.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/SecureDml.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/UnsecureDml.cls b/framework/default/ortoo-core/default/classes/fflib-extension/UnsecureDml.cls new file mode 100644 index 00000000000..59f638e6e1e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/UnsecureDml.cls @@ -0,0 +1,4 @@ +/** + * Class is a rename of FFLIB's SimpleDml to be clear about what it actually does - it provides Unsecure access to DML operations + */ +public inherited sharing class UnsecureDml extends fflib_SobjectUnitOfWork.SimpleDML implements Idml {} // NOPMD: intentionally left blank diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/UnsecureDml.cls-meta.xml b/framework/default/ortoo-core/default/classes/fflib-extension/UnsecureDml.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/UnsecureDml.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectSelector.cls b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectSelector.cls index aa745e1c189..d482ddca9d5 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectSelector.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectSelector.cls @@ -1,8 +1,26 @@ /** - * Is an extension of the provided fflib version of SobjectSelector in order to try to limit the impact of changes + * Is an extension of the provided fflib version of SobjectSelector. + * + * Reverses the default secruity setup for FLS, being that it defaults to enforcing FLS. * * @group fflib Extension */ public abstract inherited sharing class ortoo_SobjectSelector extends fflib_SobjectSelector // NOPMD: specified a mini-namespace to differentiate from fflib versions { + public ortoo_SobjectSelector() + { + super(); + enforceFLS(); + } + + /** + * Configure this instance to ignore FLS when selecting data. + * + * @return fflib_SObjectSelector Itself, allowing for a fluent interface. + */ + public fflib_SObjectSelector ignoreFls() + { + m_enforceFLS = false; + return this; + } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectUnitOfWork.cls b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectUnitOfWork.cls index 43879177c60..02f3fd76d93 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectUnitOfWork.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectUnitOfWork.cls @@ -30,6 +30,41 @@ public inherited sharing class ortoo_SobjectUnitOfWork extends fflib_SObjectUnit super( sObjectTypes, dml ); } + /** + * Provides an elevated context for calls to the Unit of Work. + * + * Specifically, removes sharing from the call meaning that the Unit of Work can commit + * records that the current user would not normally have write access to. + * + * @return ortoo_SobjectUnitOfWork.ElevatedContext The elevated context against which a commit can be issued + */ + public ElevatedContext elevatedContext() + { + return new ElevatedContext( this ); + } + + /** + * Provides the ability to remove sharing rules from the current context + */ + public without sharing class ElevatedContext + { + @testVisible + ortoo_SobjectUnitOfWork uow; + + public ElevatedContext( ortoo_SobjectUnitOfWork uow ) + { + this.uow = uow; + } + + /** + * Commit the currently queued work, not applying sharing rules when that is done. + */ + public void commitWork() + { + uow.commitWork(); + } + } + /** * Returns the number of DML rows that are pending for this Unit of Work * diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_UnitOfWorkFactory.cls b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_UnitOfWorkFactory.cls index c346bd79dae..3fe86e80884 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_UnitOfWorkFactory.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_UnitOfWorkFactory.cls @@ -1,5 +1,6 @@ public inherited sharing class ortoo_UnitOfWorkFactory extends fflib_Application.UnitOfWorkFactory // NOPMD: specified a mini-namespace to differentiate from fflib versions { + private static fflib_SobjectUnitOfWork.Idml defaultIdml = new SecureDml(); /* * Constructs a Unit Of Work factory with no default configuration * @@ -31,7 +32,7 @@ public inherited sharing class ortoo_UnitOfWorkFactory extends fflib_Application { return m_mockUow; } - return new ortoo_SObjectUnitOfWork( m_objectTypes ); + return new ortoo_SObjectUnitOfWork( m_objectTypes, defaultIdml ); } /** @@ -39,7 +40,7 @@ public inherited sharing class ortoo_UnitOfWorkFactory extends fflib_Application * SObjectType list provided in the constructor, returns a Mock implementation * if set via the setMock method **/ - public override fflib_ISObjectUnitOfWork newInstance( fflib_SObjectUnitOfWork.IDML dml ) + public override fflib_ISObjectUnitOfWork newInstance( fflib_SObjectUnitOfWork.Idml dml ) { if ( m_mockUow != null ) { @@ -62,7 +63,7 @@ public inherited sharing class ortoo_UnitOfWorkFactory extends fflib_Application { return m_mockUow; } - return new ortoo_SObjectUnitOfWork( objectTypes ); + return new ortoo_SObjectUnitOfWork( objectTypes, defaultIdml ); } /** @@ -73,7 +74,7 @@ public inherited sharing class ortoo_UnitOfWorkFactory extends fflib_Application * @remark If mock is set, the list of SObjectType in the mock could be different * then the list of SObjectType specified in this method call **/ - public override fflib_ISObjectUnitOfWork newInstance( List objectTypes, fflib_SObjectUnitOfWork.IDML dml ) + public override fflib_ISObjectUnitOfWork newInstance( List objectTypes, fflib_SObjectUnitOfWork.Idml dml ) { if ( m_mockUow != null ) { diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/SecureDmlTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/tests/SecureDmlTest.cls new file mode 100644 index 00000000000..94bdf85596c --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/SecureDmlTest.cls @@ -0,0 +1,1659 @@ +@isTest +private without sharing class SecureDmlTest +{ + private static final Integer LOTS_OF_RECORDS = 10000; + + @isTest + private static void dmlInsert_whenTheUserCanCreateTheRecords_willInsertTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + dml.dmlInsert( accounts ); + + Test.stopTest(); + + System.assertNotEquals( null, accounts[0].Id, 'dmlInsert, when the user can create the records, will insert the records, setting the Id on them (0)' ); + System.assertNotEquals( null, accounts[1].Id, 'dmlInsert, when the user can create the records, will insert the records, setting the Id on them (1)' ); + + List createdAccounts = getAccountsInserted(); + + System.assertEquals( 'Account1', createdAccounts[0].Name, 'dmlInsert, when the user can create the records, will insert the records, setting the fields on the records that are created (0)' ); + System.assertEquals( 'Account2', createdAccounts[1].Name, 'dmlInsert, when the user can create the records, will insert the records, setting the fields on the records that are created (1)' ); + } + + @isTest + private static void dmlInsert_whenAskedToCreateALotOfRecords_willInsertTheRecords() // NOPMD: Test method name format + { + List accounts = new List(); + for ( Integer i=0; i < LOTS_OF_RECORDS; i++ ) + { + accounts.add( new Account( Name = 'Account' + i ) ); + } + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + dml.dmlInsert( accounts ); + + Test.stopTest(); + + List createdAccounts = getAccountsInserted(); + + System.assertEquals( LOTS_OF_RECORDS, createdAccounts.size(), 'dmlInsert, when the user can create the records, will insert the records without blowing CPU or Heap size limits' ); + } + + @isTest + private static void dmlInsert_whenTheUserCannotCreateRecords_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml(); + ((TestableSecureDml)dml).canCreate = false; + + dml.dmlInsert( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + System.assertNotEquals( null, thrownException, 'dmlInsert, when the user cannot create records, will throw an exception' ); + + ortoo_Exception.Contexts contexts = thrownException.getContexts(); + ortoo_Exception.Context context; + + context = contexts.next(); + System.assertEquals( 'sobjectTypeName', context.getName(), 'dmlInsert, when the user cannot create records, will throw an exception with a context named sobjectTypeName' ); + System.assertEquals( Account.getSObjectType().getDescribe().getName(), context.getValue(), 'dmlInsert, when the user cannot create records, will throw an exception with a context named sobjectTypeName set to the name of the SObject' ); + + context = contexts.next(); + System.assertEquals( 'records', context.getName(), 'dmlInsert, when the user cannot create records, will throw an exception with a context named records' ); + System.assertEquals( accounts, context.getValue(), 'dmlInsert, when the user cannot create records, will throw an exception with a context named records set to the records that where sent' ); + + System.assertEquals( 'dmlInsert', thrownException.getStackTrace().getInnermostMethodName(), 'dmlInsert, when the user cannot create records, will throw an exception with the stack trace pointing to the insert method' ); + } + + @isTest + private static void dmlInsert_whenTheUserCanNotCreateButCrudOff_willInsertTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + + ((TestableSecureDml)dml).canCreate = false; // mimics not having write access + + dml.ignoreCrud(); + dml.dmlInsert( accounts ); + + Test.stopTest(); + + System.assertNotEquals( null, accounts[0].Id, 'dmlInsert, when the user cannot create the records but crud switched off, will insert the records, setting the Id on them (0)' ); + System.assertNotEquals( null, accounts[1].Id, 'dmlInsert, when the user cannot create the records but crud switched off, will insert the records, setting the Id on them (1)' ); + } + + @isTest + private static void dmlInsert_whenTheUserCanNotCreateButCrudOffForThatObject_willInsertTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreCrudFor( Account.SobjectType ); + + ((TestableSecureDml)dml).canCreate = false; // mimics not having write access + + dml.dmlInsert( accounts ); + + Test.stopTest(); + + System.assertNotEquals( null, accounts[0].Id, 'dmlInsert, when the user cannot create the records but crud switched off for that sobject type, will insert the records, setting the Id on them (0)' ); + System.assertNotEquals( null, accounts[1].Id, 'dmlInsert, when the user cannot create the records but crud switched off for that sobject type, will insert the records, setting the Id on them (1)' ); + } + + @isTest + private static void dmlInsert_whenTheUserCanNotCreateButCrudOffForMultipleObjects_willInsertTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreCrudFor( Account.SobjectType ) + .ignoreCrudFor( Contact.SobjectType ); + + ((TestableSecureDml)dml).canCreate = false; // mimics not having write access + + dml.dmlInsert( accounts ); + + Test.stopTest(); + + System.assertNotEquals( null, accounts[0].Id, 'dmlInsert, when the user cannot create the records but crud switched off for multiple sobject types, including that one, will insert the records, setting the Id on them (0)' ); + System.assertNotEquals( null, accounts[1].Id, 'dmlInsert, when the user cannot create the records but crud switched off for multiple sobject types, including that one, will insert the records, setting the Id on them (1)' ); + } + + @isTest + private static void dmlInsert_whenTheUserCannotCreateRecordsAndCrudOfForOtherObjects_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml() + .ignoreCrudFor( Contact.sobjectType ); + + ((TestableSecureDml)dml).canCreate = false; + + dml.dmlInsert( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + System.assertNotEquals( null, thrownException, 'dmlInsert, when the user cannot create records and crud is off for other objects, will still throw an exception' ); + } + + @isTest + private static void dmlInsert_whenGivenAnEmptyList_willDoNothing() // NOPMD: Test method name format + { + List emptyList = new List(); + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + dml.dmlInsert( emptyList ); + + Test.stopTest(); + + System.assertEquals( null, getAccountsInserted(), 'dmlInsert, when given an empty list, will not issue any DML' ); + } + + @isTest + private static void dmlInsert_whenGivenAnEmptyListAndCreateIsNotAllowed_willDoNothing() // NOPMD: Test method name format + { + List emptyList = new List(); + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + + ((TestableSecureDml)dml).canCreate = false; + + dml.dmlInsert( emptyList ); + + Test.stopTest(); + + System.assertEquals( null, getAccountsInserted(), 'dmlInsert, when given an empty list of objects the user cannot create, will not issue any DML or throw an exception' ); + } + + @isTest + private static void dmlInsert_whenTheUserCannotCreateRecordsDueToFls_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml(); + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlInsert( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'NumberOfEmployees', thrownException.getMessage(), 'dmlInsert, when the user cannot create records because of FLS, will throw an exception' ); + + ortoo_Exception.Contexts contexts = thrownException.getContexts(); + ortoo_Exception.Context context; + + context = contexts.next(); + System.assertEquals( 'sobjectTypeName', context.getName(), 'dmlInsert, when the user cannot create records because of FLS, will throw an exception with a context named sobjectTypeName' ); + System.assertEquals( Account.getSObjectType().getDescribe().getName(), context.getValue(), 'dmlInsert, when the user cannot create records because of FLS, will throw an exception with a context named sobjectTypeName set to the name of the SObject' ); + + context = contexts.next(); + System.assertEquals( 'fieldsInViolation', context.getName(), 'dmlInsert, when the user cannot create records because of FLS, will throw an exception with a context named fieldsInViolation' ); + System.assertEquals( accountFieldsBlockedByFls, context.getValue(), 'dmlInsert, when the user cannot create records because of FLS, will throw an exception with a context named fieldsInViolation set to the fields that were in violation' ); + + System.assertEquals( 'dmlInsert', thrownException.getStackTrace().getInnermostMethodName(), 'dmlInsert, when the user cannot create records because of FLS, will throw an exception with the stack trace pointing to the insert method' ); + } + + @isTest + private static void dmlInsert_whenTheUserCanNotCreateButFlsCheckOff_willInsertTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreFls(); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlInsert( accounts ); + + Test.stopTest(); + + System.assertNotEquals( null, accounts[0].Id, 'dmlInsert, when the user cannot create the records, but fls switched off, will insert the records, setting the Id on them (0)' ); + + List createdAccounts = getAccountsInserted(); + + System.assertEquals( 1, createdAccounts[0].NumberOfEmployees, 'dmlInsert, when the user can create the records, but fls switched off, will insert the records, setting the fields on the records that are created (0)' ); + } + + @isTest + private static void dmlInsert_whenTheUserCanNotCreateButFlsCheckOffForSobject_willInsertTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreFlsFor( Account.sobjectType ); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlInsert( accounts ); + + Test.stopTest(); + + System.assertNotEquals( null, accounts[0].Id, 'dmlInsert, when the user cannot create the records, but fls switched off for that sobject type, will insert the records, setting the Id on them (0)' ); + System.assertEquals( 1, accounts[0].NumberOfEmployees, 'dmlInsert, when the user can create the records, but fls switched off for that sobject type, will insert the records, leaving the field set on the original record' ); + + List createdAccounts = getAccountsInserted(); + + System.assertEquals( 1, createdAccounts[0].NumberOfEmployees, 'dmlInsert, when the user can create the records, but fls switched off for that sobject type, will insert the records, setting the fields on the records that are created (0)' ); + } + + @isTest + private static void dmlInsert_whenTheUserCanNotCreateButFlsCheckOffForSobjectField_willInsertTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreFlsFor( Account.sobjectType, Account.NumberOfEmployees ); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlInsert( accounts ); + + Test.stopTest(); + + System.assertNotEquals( null, accounts[0].Id, 'dmlInsert, when the user cannot create the records, but fls switched off for that field, will insert the records, setting the Id on them (0)' ); + System.assertEquals( 1, accounts[0].NumberOfEmployees, 'dmlInsert, when the user can create the records, but fls switched off for that field, will insert the records, leaving the field set on the original record' ); + + List createdAccounts = getAccountsInserted(); + + System.assertEquals( 1, createdAccounts[0].NumberOfEmployees, 'dmlInsert, when the user can create the records, but fls switched off for that field, will insert the records, setting the fields on the records that are created (0)' ); + } + + @isTest + private static void dmlInsert_whenTheUserCannotCreateRecordsDueToFlsAndOtherObjectSwitchedOff_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml() + .ignoreFlsFor( Contact.sobjectType ); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlInsert( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'NumberOfEmployees', thrownException.getMessage(), 'dmlInsert, when the user cannot create records because of FLS and FLS switched off for a different object, will throw an exception' ); + } + + @isTest + private static void dmlInsert_whenTheUserCannotCreateRecordsDueToFlsAndOtherFieldSwitchedOff_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml() + .ignoreFlsFor( Account.sobjectType, Account.Name ); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlInsert( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'NumberOfEmployees', thrownException.getMessage(), 'dmlInsert, when the user cannot create records because of FLS and FLS switched off for a different field, will throw an exception' ); + } + + @isTest + private static void dmlInsert_whenTheUserCannotCreateRecordsDueToFlsAndNotAllFieldsOff_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account(), + new Account() + }; + + Set accountFieldsBlockedByFls = new Set{ 'Name', 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml() + .ignoreFlsFor( Account.sobjectType, Account.NumberOfEmployees ); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlInsert( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'Name', thrownException.getMessage(), 'dmlInsert, when the user cannot create records because of FLS and not all fields are switched off, will throw an exception naming the fields in violation' ); + Amoss_Asserts.assertDoesNotContain( 'NumberOfEmployees', thrownException.getMessage(), 'dmlInsert, when the user cannot create records because of FLS and not all fields are switched off, will throw an exception, not naming the fields that are supressed' ); + + ortoo_Exception.Contexts contexts = thrownException.getContexts(); + ortoo_Exception.Context context; + + context = contexts.next(); + System.assertEquals( 'sobjectTypeName', context.getName(), 'dmlInsert, when the user cannot create records because of FLS and not all fields are switched off, will throw an exception with a context named sobjectTypeName' ); + System.assertEquals( Account.getSObjectType().getDescribe().getName(), context.getValue(), 'dmlInsert, when the user cannot create records because of FLS and not all fields are switched off, will throw an exception with a context named sobjectTypeName set to the name of the SObject' ); + + context = contexts.next(); + System.assertEquals( 'fieldsInViolation', context.getName(), 'dmlInsert, when the user cannot create records because of FLS and not all fields are switched off, will throw an exception with a context named fieldsInViolation' ); + System.assertEquals( new Set{ 'Name' }, context.getValue(), 'dmlInsert, when the user cannot create records because of FLS and not all fields are switched off, will throw an exception with a context named fieldsInViolation set to the fields that were in violation minus those switched off' ); + } + + @isTest + private static void dmlInsert_whenTheUserCannotCreateRecordsDueToFlsAndNullHandler_willStripFields() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account(), + new Account() + }; + + Set accountFieldsBlockedByFls = new Set{ 'Name', 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + SecureDml dml = new TestableSecureDml() + .ignoreFlsFor( Account.sobjectType, Account.NumberOfEmployees ) + .setFlsViolationHandler( new SwallowOnFlsViolationHandler() ); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlInsert( accounts ); + Test.stopTest(); + + System.assertEquals( null, accounts[0].Name, 'dmlInsert, when the user cannot create records because of FLS and handler does not throw exception, will update the records to remove the fields in violation' ); + System.assertEquals( 1, accounts[0].NumberOfEmployees, 'dmlInsert, when the user cannot create records because of FLS and handler does not throw exception, will ensure the supressed fields keep their values' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCanUpdateTheRecords_willUpdateTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1' ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + dml.dmlUpdate( accounts ); + + Test.stopTest(); + + List updatedAccounts = getAccountsUpdated(); + + System.assertEquals( 'Account1', updatedAccounts[0].Name, 'dmlUpdate, when the user can update the records, will update the records, setting the fields on the records that are updated (0)' ); + System.assertEquals( 'Account2', updatedAccounts[1].Name, 'dmlUpdate, when the user can update the records, will update the records, setting the fields on the records that are updated (1)' ); + } + + @isTest + private static void dmlUpdate_whenAskedToUpdateALotOfRecords_willUpdateTheRecords() // NOPMD: Test method name format + { + List accounts = new List(); + for ( Integer i=0; i < LOTS_OF_RECORDS; i++ ) + { + accounts.add( new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account' + i ) ); + } + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + dml.dmlUpdate( accounts ); + + Test.stopTest(); + + List updatedAccounts = getAccountsUpdated(); + + System.assertEquals( LOTS_OF_RECORDS, updatedAccounts.size(), 'dmlUpdate, when the user can update the records, will update the records without blowing CPU or Heap size limits' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCannotUpdateRecords_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1' ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2' ) + }; + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml(); + ((TestableSecureDml)dml).canUpdate = false; + + dml.dmlUpdate( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + System.assertNotEquals( null, thrownException, 'dmlUpdate, when the user cannot update records, will throw an exception' ); + + ortoo_Exception.Contexts contexts = thrownException.getContexts(); + ortoo_Exception.Context context; + + context = contexts.next(); + System.assertEquals( 'sobjectTypeName', context.getName(), 'dmlUpdate, when the user cannot update records, will throw an exception with a context named sobjectTypeName' ); + System.assertEquals( Account.getSObjectType().getDescribe().getName(), context.getValue(), 'dmlUpdate, when the user cannot update records, will throw an exception with a context named sobjectTypeName set to the name of the SObject' ); + + context = contexts.next(); + System.assertEquals( 'records', context.getName(), 'dmlUpdate, when the user cannot update records, will throw an exception with a context named records' ); + System.assertEquals( accounts, context.getValue(), 'dmlUpdate, when the user cannot update records, will throw an exception with a context named records set to the records that where sent' ); + + System.assertEquals( 'dmlUpdate', thrownException.getStackTrace().getInnermostMethodName(), 'dmlUpdate, when the user cannot update records, will throw an exception with the stack trace pointing to the update method' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCanNotUpdateButCrudOff_willUpdateTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1' ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + + ((TestableSecureDml)dml).canUpdate = false; // mimics not having write access + + dml.ignoreCrud(); + dml.dmlUpdate( accounts ); + + Test.stopTest(); + + List updatedAccounts = getAccountsUpdated(); + + System.assertEquals( updatedAccounts[0].Id, accounts[0].Id, 'dmlUpdate, when the user cannot update the records but crud switched off, will update the records' ); + System.assertEquals( updatedAccounts[1].Id, accounts[1].Id, 'dmlUpdate, when the user cannot update the records but crud switched off, will update the records' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCanNotUpdateButCrudOffForThatObject_willUpdateTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1' ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreCrudFor( Account.SobjectType ); + + ((TestableSecureDml)dml).canUpdate = false; // mimics not having write access + + dml.dmlUpdate( accounts ); + + Test.stopTest(); + + List updatedAccounts = getAccountsUpdated(); + + System.assertEquals( accounts[0].Id, updatedAccounts[0].Id, 'dmlUpdate, when the user cannot update the records but crud switched off for that sobject type, will update the records (0)' ); + System.assertEquals( accounts[1].Id, updatedAccounts[1].Id, 'dmlUpdate, when the user cannot update the records but crud switched off for that sobject type, will update the records (1)' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCanNotUpdateButCrudOffForMultipleObjects_willUpdateTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1' ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreCrudFor( Account.SobjectType ) + .ignoreCrudFor( Contact.SobjectType ); + + ((TestableSecureDml)dml).canUpdate = false; // mimics not having write access + + dml.dmlUpdate( accounts ); + + Test.stopTest(); + + List updatedAccounts = getAccountsUpdated(); + + System.assertEquals( accounts[0].Id, updatedAccounts[0].Id, 'dmlUpdate, when the user cannot update the records but crud switched off for multiple sobject types, including that one, will update the records (0)' ); + System.assertEquals( accounts[1].Id, updatedAccounts[1].Id, 'dmlUpdate, when the user cannot update the records but crud switched off for multiple sobject types, including that one, will update the records (1)' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCannotUpdateRecordsAndCrudOfForOtherObjects_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1' ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2' ) + }; + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml() + .ignoreCrudFor( Contact.sobjectType ); + + ((TestableSecureDml)dml).canUpdate = false; + + dml.dmlUpdate( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + System.assertNotEquals( null, thrownException, 'dmlUpdate, when the user cannot update records and crud is off for other objects, will still throw an exception' ); + } + + @isTest + private static void dmlUpdate_whenGivenAnEmptyList_willDoNothing() // NOPMD: Test method name format + { + List emptyList = new List(); + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + dml.dmlUpdate( emptyList ); + + Test.stopTest(); + + System.assertEquals( null, getAccountsUpdated(), 'dmlUpdate, when given an empty list, will not issue any DML' ); + } + + @isTest + private static void dmlUpdate_whenGivenAnEmptyListAndUpdateIsNotAllowed_willDoNothing() // NOPMD: Test method name format + { + List emptyList = new List(); + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + + ((TestableSecureDml)dml).canUpdate = false; + + dml.dmlUpdate( emptyList ); + + Test.stopTest(); + + System.assertEquals( null, getAccountsUpdated(), 'dmlUpdate, when given an empty list of objects the user cannot update, will not issue any DML or throw an exception' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCannotUpdateRecordsDueToFls_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Id = accounts[0].Id, Name = 'Account1' ), + new Account( Id = accounts[1].Id, Name = 'Account2' ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml(); + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlUpdate( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'NumberOfEmployees', thrownException.getMessage(), 'dmlUpdate, when the user cannot update records because of FLS, will throw an exception' ); + + ortoo_Exception.Contexts contexts = thrownException.getContexts(); + ortoo_Exception.Context context; + + context = contexts.next(); + System.assertEquals( 'sobjectTypeName', context.getName(), 'dmlUpdate, when the user cannot update records because of FLS, will throw an exception with a context named sobjectTypeName' ); + System.assertEquals( Account.getSObjectType().getDescribe().getName(), context.getValue(), 'dmlUpdate, when the user cannot update records because of FLS, will throw an exception with a context named sobjectTypeName set to the name of the SObject' ); + + context = contexts.next(); + System.assertEquals( 'fieldsInViolation', context.getName(), 'dmlUpdate, when the user cannot update records because of FLS, will throw an exception with a context named fieldsInViolation' ); + System.assertEquals( accountFieldsBlockedByFls, context.getValue(), 'dmlUpdate, when the user cannot update records because of FLS, will throw an exception with a context named fieldsInViolation set to the fields that were in violation' ); + + System.assertEquals( 'dmlUpdate', thrownException.getStackTrace().getInnermostMethodName(), 'dmlUpdate, when the user cannot update records because of FLS, will throw an exception with the stack trace pointing to the update method' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCanNotUpdateButFlsCheckOff_willUpdateTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Id = accounts[0].id, Name = 'Account1' ), + new Account( Id = accounts[1].id, Name = 'Account2' ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreFls(); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlUpdate( accounts ); + + Test.stopTest(); + + System.assertEquals( 1, accounts[0].NumberOfEmployees, 'dmlUpdate, when the user cannot update the records, but fls switched off, will not strip the value from the record (0)' ); + + List updatedAccounts = getAccountsUpdated(); + + System.assertEquals( 1, updatedAccounts[0].NumberOfEmployees, 'dmlUpdate, when the user can update the records, but fls switched off, will update the records, setting the fields on the records that are updated (0)' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCanNotUpdateButFlsCheckOffForSobject_willUpdateTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Id = accounts[0].Id, Name = 'Account1' ), + new Account( Id = accounts[1].Id, Name = 'Account2' ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreFlsFor( Account.sobjectType ); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlUpdate( accounts ); + + Test.stopTest(); + + System.assertEquals( 1, accounts[0].NumberOfEmployees, 'dmlUpdate, when the user can update the records, but fls switched off for that sobject type, will update the records, leaving the field set on the original record' ); + + List updatedAccounts = getAccountsUpdated(); + + System.assertEquals( 1, updatedAccounts[0].NumberOfEmployees, 'dmlUpdate, when the user can update the records, but fls switched off for that sobject type, will update the records, setting the fields on the records that are updated (0)' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCanNotUpdateButFlsCheckOffForSobjectField_willUpdateTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Id = accounts[0].Id, Name = 'Account1' ), + new Account( Id = accounts[1].Id, Name = 'Account2' ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreFlsFor( Account.sobjectType, Account.NumberOfEmployees ); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlUpdate( accounts ); + + Test.stopTest(); + + System.assertEquals( 1, accounts[0].NumberOfEmployees, 'dmlUpdate, when the user can update the records, but fls switched off for that field, will update the records, leaving the field set on the original record' ); + + List updatedAccounts = getAccountsUpdated(); + + System.assertEquals( 1, updatedAccounts[0].NumberOfEmployees, 'dmlUpdate, when the user can update the records, but fls switched off for that field, will update the records, setting the fields on the records that are updated (0)' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCannotUpdateRecordsDueToFlsAndOtherObjectSwitchedOff_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Id = accounts[0].id, Name = 'Account1' ), + new Account( Id = accounts[1].id, Name = 'Account2' ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml() + .ignoreFlsFor( Contact.sobjectType ); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlUpdate( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'NumberOfEmployees', thrownException.getMessage(), 'dmlUpdate, when the user cannot update records because of FLS and FLS switched off for a different object, will throw an exception' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCannotUpdateRecordsDueToFlsAndOtherFieldSwitchedOff_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Id = accounts[0].Id, Name = 'Account1' ), + new Account( Id = accounts[1].Id, Name = 'Account2' ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml() + .ignoreFlsFor( Account.sobjectType, Account.Name ); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlUpdate( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'NumberOfEmployees', thrownException.getMessage(), 'dmlUpdate, when the user cannot update records because of FLS and FLS switched off for a different field, will throw an exception' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCannotUpdateRecordsDueToFlsAndNotAllFieldsOff_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Id = accounts[0].Id ), + new Account( Id = accounts[1].Id ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'Name', 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml() + .ignoreFlsFor( Account.sobjectType, Account.NumberOfEmployees ); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlUpdate( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'Name', thrownException.getMessage(), 'dmlUpdate, when the user cannot update records because of FLS and not all fields are switched off, will throw an exception naming the fields in violation' ); + Amoss_Asserts.assertDoesNotContain( 'NumberOfEmployees', thrownException.getMessage(), 'dmlUpdate, when the user cannot update records because of FLS and not all fields are switched off, will throw an exception, not naming the fields that are supressed' ); + + ortoo_Exception.Contexts contexts = thrownException.getContexts(); + ortoo_Exception.Context context; + + context = contexts.next(); + System.assertEquals( 'sobjectTypeName', context.getName(), 'dmlUpdate, when the user cannot update records because of FLS and not all fields are switched off, will throw an exception with a context named sobjectTypeName' ); + System.assertEquals( Account.getSObjectType().getDescribe().getName(), context.getValue(), 'dmlUpdate, when the user cannot update records because of FLS and not all fields are switched off, will throw an exception with a context named sobjectTypeName set to the name of the SObject' ); + + context = contexts.next(); + System.assertEquals( 'fieldsInViolation', context.getName(), 'dmlUpdate, when the user cannot update records because of FLS and not all fields are switched off, will throw an exception with a context named fieldsInViolation' ); + System.assertEquals( new Set{ 'Name' }, context.getValue(), 'dmlUpdate, when the user cannot update records because of FLS and not all fields are switched off, will throw an exception with a context named fieldsInViolation set to the fields that were in violation minus those switched off' ); + } + + @isTest + private static void dmlUpdate_whenTheUserCannotUpdateRecordsDueToFlsAndNullHandler_willStripFields() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account1', NumberOfEmployees = 1 ), + new Account( Id = TestIdUtils.generateId( Account.sobjectType ), Name = 'Account2', NumberOfEmployees = 2 ) + }; + + List strippedAccounts = new List + { + new Account( Id = accounts[0].Id ), + new Account( Id = accounts[1].Id ) + }; + + Set accountFieldsBlockedByFls = new Set{ 'Name', 'NumberOfEmployees' }; + Map> removedFields = new Map>{ 'Account' => accountFieldsBlockedByFls }; + SecureDml.SecurityDecision stripsNumberOfEmployees = new SecureDml.SecurityDecision( removedFields, strippedAccounts ); + + Test.startTest(); + SecureDml dml = new TestableSecureDml() + .ignoreFlsFor( Account.sobjectType, Account.NumberOfEmployees ) + .setFlsViolationHandler( new SwallowOnFlsViolationHandler() ); + + ((TestableSecureDml)dml).stripInaccessibleSecurityDecision = stripsNumberOfEmployees; + + dml.dmlUpdate( accounts ); + Test.stopTest(); + + System.assertEquals( null, accounts[0].Name, 'dmlUpdate, when the user cannot update records because of FLS and handler does not throw exception, will update the records to remove the fields in violation' ); + System.assertEquals( 1, accounts[0].NumberOfEmployees, 'dmlUpdate, when the user cannot update records because of FLS and handler does not throw exception, will ensure the supressed fields keep their values' ); + } + + @isTest + private static void dmlDelete_whenTheUserCanDeleteTheRecords_willDeleteTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + dml.dmlDelete( accounts ); + + Test.stopTest(); + + List deletedAccounts = getAccountsDeleted(); + + System.assertEquals( 'Account1', deletedAccounts[0].Name, 'dmlDelete, when the user can delete the records, will delete the records (0)' ); + System.assertEquals( 'Account2', deletedAccounts[1].Name, 'dmlDelete, when the user can delete the records, will delete the records (1)' ); + } + + @isTest + private static void dmlDelete_whenAskedToDeleteALotOfRecords_willDeleteTheRecords() // NOPMD: Test method name format + { + List accounts = new List(); + for ( Integer i=0; i < LOTS_OF_RECORDS; i++ ) + { + accounts.add( new Account( Name = 'Account' + i ) ); + } + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + dml.dmlDelete( accounts ); + + Test.stopTest(); + + List deletedAccounts = getAccountsDeleted(); + + System.assertEquals( LOTS_OF_RECORDS, deletedAccounts.size(), 'dmlDelete, when the user can delete the records, will delete the records without blowing CPU or Heap size limits' ); + } + + @isTest + private static void dmlDelete_whenTheUserCannotDeleteRecords_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml(); + ((TestableSecureDml)dml).canDelete = false; + + dml.dmlDelete( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + System.assertNotEquals( null, thrownException, 'dmlDelete, when the user cannot delete records, will throw an exception' ); + + ortoo_Exception.Contexts contexts = thrownException.getContexts(); + ortoo_Exception.Context context; + + context = contexts.next(); + System.assertEquals( 'sobjectTypeName', context.getName(), 'dmlDelete, when the user cannot delete records, will throw an exception with a context named sobjectTypeName' ); + System.assertEquals( Account.getSObjectType().getDescribe().getName(), context.getValue(), 'dmlDelete, when the user cannot delete records, will throw an exception with a context named sobjectTypeName set to the name of the SObject' ); + + context = contexts.next(); + System.assertEquals( 'records', context.getName(), 'dmlDelete, when the user cannot delete records, will throw an exception with a context named records' ); + System.assertEquals( accounts, context.getValue(), 'dmlDelete, when the user cannot delete records, will throw an exception with a context named records set to the records that where sent' ); + + System.assertEquals( 'dmlDelete', thrownException.getStackTrace().getInnermostMethodName(), 'dmlDelete, when the user cannot delete records, will throw an exception with the stack trace pointing to the delete method' ); + } + + @isTest + private static void dmlDelete_whenTheUserCanNotDeleteButCrudOff_willDeleteTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + + ((TestableSecureDml)dml).canDelete = false; // mimics not having write access + + dml.ignoreCrud(); + dml.dmlDelete( accounts ); + + Test.stopTest(); + + List deletedAccounts = getAccountsDeleted(); + + System.assertEquals( accounts[0], deletedAccounts[0], 'dmlDelete, when the user cannot delete the records but crud switched off, will delete the records - 0' ); + System.assertEquals( accounts[1], deletedAccounts[1], 'dmlDelete, when the user cannot delete the records but crud switched off, will delete the records - 1' ); + } + + @isTest + private static void dmlDelete_whenTheUserCanNotDeleteButCrudOffForThatObject_willDeleteTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreCrudFor( Account.SobjectType ); + + ((TestableSecureDml)dml).canDelete = false; // mimics not having write access + + dml.dmlDelete( accounts ); + + Test.stopTest(); + + List deletedAccounts = getAccountsDeleted(); + + System.assertEquals( accounts[0], deletedAccounts[0], 'dmlDelete, when the user cannot delete the records but crud switched off for that sobject type, will delete the records (0)' ); + System.assertEquals( accounts[1], deletedAccounts[1], 'dmlDelete, when the user cannot delete the records but crud switched off for that sobject type, will delete the records (1)' ); + } + + @isTest + private static void dmlDelete_whenTheUserCanNotDeleteButCrudOffForMultipleObjects_willDeleteTheRecords() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreCrudFor( Account.SobjectType ) + .ignoreCrudFor( Contact.SobjectType ); + + ((TestableSecureDml)dml).canDelete = false; // mimics not having write access + + dml.dmlDelete( accounts ); + + Test.stopTest(); + + List deletedAccounts = getAccountsDeleted(); + + System.assertEquals( accounts[0], deletedAccounts[0], 'dmlDelete, when the user cannot delete the records but crud switched off for multiple sobject types, including that one, will delete the records (0)' ); + System.assertEquals( accounts[1], deletedAccounts[1], 'dmlDelete, when the user cannot delete the records but crud switched off for multiple sobject types, including that one, will delete the records (1)' ); + } + + @isTest + private static void dmlDelete_whenTheUserCannotDeleteRecordsAndCrudOfForOtherObjects_willThrowAnException() // NOPMD: Test method name format + { + List accounts = new List + { + new Account( Name = 'Account1' ), + new Account( Name = 'Account2' ) + }; + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml() + .ignoreCrudFor( Contact.sobjectType ); + + ((TestableSecureDml)dml).canDelete = false; + + dml.dmlDelete( accounts ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + System.assertNotEquals( null, thrownException, 'dmlDelete, when the user cannot delete records and crud is off for other objects, will still throw an exception' ); + } + + @isTest + private static void dmlDelete_whenGivenAnEmptyList_willDoNothing() // NOPMD: Test method name format + { + List emptyList = new List(); + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + dml.dmlDelete( emptyList ); + + Test.stopTest(); + + System.assertEquals( null, getAccountsDeleted(), 'dmlDelete, when given an empty list, will not issue any DML' ); + } + + @isTest + private static void dmlDelete_whenGivenAnEmptyListAndDeleteIsNotAllowed_willDoNothing() // NOPMD: Test method name format + { + List emptyList = new List(); + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + + ((TestableSecureDml)dml).canDelete = false; + + dml.dmlDelete( emptyList ); + + Test.stopTest(); + + System.assertEquals( null, getAccountsDeleted(), 'dmlDelete, when given an empty list of objects the user cannot delete, will not issue any DML or throw an exception' ); + } + + @isTest + private static void eventPublish_whenTheUserCanPublishTheEvents_willPublishTheEvents() // NOPMD: Test method name format + { + List fakeEvents = new List // Events are just SObjects, but no standard ones exist. So we use Accounts as fake events + { + new Account( Name = 'Event1' ), + new Account( Name = 'Event2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + dml.eventPublish( fakeEvents ); + + Test.stopTest(); + + List eventsPublished = getEventsPublished(); + + System.assertEquals( 'Event1', eventsPublished[0].Name, 'eventPublish, when the user can publish the events, will publish the events (0)' ); + System.assertEquals( 'Event2', eventsPublished[1].Name, 'eventPublish, when the user can publish the events, will publish the events (1)' ); + } + + @isTest + private static void eventPublish_whenAskedToPublishALotOfEvents_willPublishTheEvents() // NOPMD: Test method name format + { + List fakeEvents = new List(); // Events are just SObjects, but no standard ones exist. So we use Accounts as fake events + for ( Integer i=0; i < LOTS_OF_RECORDS; i++ ) + { + fakeEvents.add( new Account( Name = 'Event' + i ) ); + } + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + dml.eventPublish( fakeEvents ); + + Test.stopTest(); + + List eventsPublished = getEventsPublished(); + + System.assertEquals( LOTS_OF_RECORDS, eventsPublished.size(), 'eventPublish, when the user can publish the events, will publish the events without blowing CPU or Heap size limits' ); + } + + @isTest + private static void eventPublish_whenTheUserCannotPublishEvents_willThrowAnException() // NOPMD: Test method name format + { + List fakeEvents = new List // Events are just SObjects, but no standard ones exist. So we use Accounts as fake events + { + new Account( Name = 'Event1' ), + new Account( Name = 'Event2' ) + }; + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml(); + ((TestableSecureDml)dml).canPublish = false; + + dml.eventPublish( fakeEvents ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + System.assertNotEquals( null, thrownException, 'eventPublish, when the user cannot publish events, will throw an exception' ); + + ortoo_Exception.Contexts contexts = thrownException.getContexts(); + ortoo_Exception.Context context; + + context = contexts.next(); + System.assertEquals( 'sobjectTypeName', context.getName(), 'eventPublish, when the user cannot publish events, will throw an exception with a context named sobjectTypeName' ); + System.assertEquals( Account.getSObjectType().getDescribe().getName(), context.getValue(), 'eventPublish, when the user cannot publish events, will throw an exception with a context named sobjectTypeName set to the name of the SObject' ); + + context = contexts.next(); + System.assertEquals( 'records', context.getName(), 'eventPublish, when the user cannot publish events, will throw an exception with a context named records' ); + System.assertEquals( fakeEvents, context.getValue(), 'eventPublish, when the user cannot publish events, will throw an exception with a context named records set to the records that where sent' ); + + System.assertEquals( 'eventPublish', thrownException.getStackTrace().getInnermostMethodName(), 'eventPublish, when the user cannot publish events, will throw an exception with the stack trace pointing to the delete method' ); + } + + @isTest + private static void eventPublish_whenTheUserCanNotPublishButCrudOff_willPublishTheEvents() // NOPMD: Test method name format + { + List fakeEvents = new List // Events are just SObjects, but no standard ones exist. So we use Accounts as fake events + { + new Account( Name = 'Event1' ), + new Account( Name = 'Event2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + + ((TestableSecureDml)dml).canPublish = false; // mimics not having publish access + + dml.ignoreCrud(); + dml.eventPublish( fakeEvents ); + + Test.stopTest(); + + List eventsPublished = getEventsPublished(); + + System.assertEquals( fakeEvents[0], eventsPublished[0], 'eventPublish, when the user cannot publish the events but crud switched off, will publish the events - 0' ); + System.assertEquals( fakeEvents[1], eventsPublished[1], 'eventPublish, when the user cannot publish the events but crud switched off, will publish the events - 1' ); + } + + @isTest + private static void eventPublish_whenTheUserCanNotPublishButCrudOffForThatObject_willPublishTheEvents() // NOPMD: Test method name format + { + List fakeEvents = new List // Events are just SObjects, but no standard ones exist. So we use Accounts as fake events + { + new Account( Name = 'Event1' ), + new Account( Name = 'Event2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreCrudFor( Account.SobjectType ); + + ((TestableSecureDml)dml).canPublish = false; // mimics not having publish access + + dml.eventPublish( fakeEvents ); + + Test.stopTest(); + + List eventsPublished = getEventsPublished(); + + System.assertEquals( fakeEvents[0], eventsPublished[0], 'eventPublish, when the user cannot publish the events but crud switched off for that sobject type, will publish the events (0)' ); + System.assertEquals( fakeEvents[1], eventsPublished[1], 'eventPublish, when the user cannot publish the events but crud switched off for that sobject type, will publish the events (1)' ); + } + + @isTest + private static void eventPublish_whenTheUserCanNotPublishButCrudOffForMultipleObjects_willPublishTheEvents() // NOPMD: Test method name format + { + List fakeEvents = new List // Events are just SObjects, but no standard ones exist. So we use Accounts as fake events + { + new Account( Name = 'Event1' ), + new Account( Name = 'Event2' ) + }; + + Test.startTest(); + + SecureDml dml = new TestableSecureDml() + .ignoreCrudFor( Account.SobjectType ) + .ignoreCrudFor( Contact.SobjectType ); + + ((TestableSecureDml)dml).canPublish = false; // mimics not having publish access + + dml.eventPublish( fakeEvents ); + + Test.stopTest(); + + List eventsPublished = getEventsPublished(); + + System.assertEquals( fakeEvents[0], eventsPublished[0], 'eventPublish, when the user cannot publish the events but crud switched off for multiple sobject types, including that one, will publish the events (0)' ); + System.assertEquals( fakeEvents[1], eventsPublished[1], 'eventPublish, when the user cannot publish the events but crud switched off for multiple sobject types, including that one, will publish the events (1)' ); + } + + @isTest + private static void eventPublish_whenTheUserCannotPublishEventsAndCrudOfForOtherObjects_willThrowAnException() // NOPMD: Test method name format + { + List fakeEvents = new List // Events are just SObjects, but no standard ones exist. So we use Accounts as fake events + { + new Account( Name = 'Event1' ), + new Account( Name = 'Event2' ) + }; + + Test.startTest(); + ortoo_Exception thrownException; + try + { + SecureDml dml = new TestableSecureDml() + .ignoreCrudFor( Contact.sobjectType ); + + ((TestableSecureDml)dml).canPublish = false; + + dml.eventPublish( fakeEvents ); + } + catch ( SecureDml.SecureDmlException e ) + { + thrownException = e; + } + Test.stopTest(); + + System.assertNotEquals( null, thrownException, 'eventPublish, when the user cannot publish events and crud is off for other objects, will still throw an exception' ); + } + + @isTest + private static void eventPublish_whenGivenAnEmptyList_willDoNothing() // NOPMD: Test method name format + { + List emptyList = new List(); // Events are just SObjects, but no standard ones exist. So we use Accounts as fake events + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + dml.eventPublish( emptyList ); + + Test.stopTest(); + + System.assertEquals( null, getEventsPublished(), 'eventPublish, when given an empty list, will not issue any DML' ); + } + + @isTest + private static void eventPublish_whenGivenAnEmptyListAndPublishIsNotAllowed_willDoNothing() // NOPMD: Test method name format + { + List emptyList = new List(); // Events are just SObjects, but no standard ones exist. So we use Accounts as fake events + + Test.startTest(); + + SecureDml dml = new TestableSecureDml(); + + ((TestableSecureDml)dml).canPublish = false; + + dml.eventPublish( emptyList ); + + Test.stopTest(); + + System.assertEquals( null, getEventsPublished(), 'eventPublish, when given an empty list of objects the user cannot delete, will not issue any DML or throw an exception' ); + } + + + private static List getAccountsInserted() + { + return recordsInserted; + } + + private static List getAccountsUpdated() + { + return recordsUpdated; + } + + private static List getAccountsDeleted() + { + return recordsDeleted; + } + + private static List getEventsPublished() + { + return recordsPublished; + } + + private static List recordsInserted; + private static List recordsUpdated; + private static List recordsDeleted; + private static List recordsPublished; + + // version of SecureDml that allows us to override the checks on whether + // the current user can create / update / delete as well as the actual DML + // Overriding the DML allows the tests to run in any environment + private inherited sharing class TestableSecureDml extends SecureDml + { + public Boolean canCreate = true; + public Boolean canUpdate = true; + public Boolean canDelete = true; + public Boolean canPublish = true; + + public SecureDml.SecurityDecision stripInaccessibleSecurityDecision; + + private void assignIds( List records ) + { + for ( Sobject thisRecord : records ) + { + thisRecord.put( 'Id', TestIdUtils.generateId( thisRecord.getSObjectType() ) ); + } + } + + protected override void doInsert( List records ) + { + assignIds( records ); + recordsInserted = records; + } + + protected override void doUpdate( List records ) + { + assignIds( records ); + recordsUpdated = records; + } + + protected override void doDelete( List records ) + { + assignIds( records ); + recordsDeleted = records; + } + + protected override void doPublish( List records ) + { + assignIds( records ); + recordsPublished = records; + } + + protected override Boolean userCanCreate( Sobject record ) + { + return canCreate; + } + + protected override Boolean userCanPublish( Sobject record ) + { + return canPublish; + } + + protected override Boolean userCanUpdate( Sobject record ) + { + return canUpdate; + } + + protected override Boolean userCanDelete( Sobject record ) + { + return canDelete; + } + + protected override SecureDml.SecurityDecision stripInaccessible( AccessType mode, List objList ) + { + if ( stripInaccessibleSecurityDecision != null ) + { + return stripInaccessibleSecurityDecision; + } + return new SecureDml.SecurityDecision( new Map>(), objList.clone() ); + } + } + + private inherited sharing class SwallowOnFlsViolationHandler implements SecureDml.FlsViolationHandler + { + public void handleInaccessibleFields( AccessType mode, SobjectType sobjectType, Set fieldsInViolation ) {} // NOPMD: intentionally left blank + } + + private inherited sharing class SwallowOnCrudViolationHandler implements SecureDml.CrudViolationHandler + { + public void handleUnableToInsertRecords( List objList ) {} // NOPMD: intentionally left blank + public void handleUnableToUpdateRecords( List objList ) {} // NOPMD: intentionally left blank + public void handleUnableToDeleteRecords( List objList ) {} // NOPMD: intentionally left blank + public void handleUnableToPublishEvents( List objList ) {} // NOPMD: intentionally left blank + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/SecureDmlTest.cls-meta.xml b/framework/default/ortoo-core/default/classes/fflib-extension/tests/SecureDmlTest.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/SecureDmlTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/UnsecureDmlTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/tests/UnsecureDmlTest.cls new file mode 100644 index 00000000000..bb5f4d76fe6 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/UnsecureDmlTest.cls @@ -0,0 +1,13 @@ + +@isTest +private without sharing class UnsecureDmlTest +{ + @isTest + private static void constructor_whenCalled_willCreateAnIdmlImplementation() // NOPMD: Test method name format + { + Object unsecureDml = new UnsecureDml(); + + System.assert( unsecureDml instanceOf fflib_SobjectUnitOfWork.Idml, 'constructor, when called, will create an Idml implementation' ); + System.assert( unsecureDml instanceOf fflib_SobjectUnitOfWork.SimpleDml, 'constructor, when called, will create a SimpleDml implementation' ); + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/UnsecureDmlTest.cls-meta.xml b/framework/default/ortoo-core/default/classes/fflib-extension/tests/UnsecureDmlTest.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/UnsecureDmlTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectSelectorTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectSelectorTest.cls new file mode 100644 index 00000000000..1eafb7f047c --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectSelectorTest.cls @@ -0,0 +1,34 @@ +@isTest +private without sharing class ortoo_SobjectSelectorTest +{ + @isTest + private static void constructor_whenCalled_willCreateASelectorThatHasSecurityEnabled() // NOPMD: Test method name format + { + ortoo_SobjectSelector selector = new TestableSelector(); + + System.assertEquals( true, selector.isEnforcingFls(), 'constructor, when called, will create a selector that has FLS enabled' ); + System.assertEquals( true, selector.isEnforcingCrud(), 'constructor, when called, will create a selector that has CRUD security enabled' ); + } + + @isTest + private static void ignoreFls_whenCalled_willDisableFlsChecking() // NOPMD: Test method name format + { + ortoo_SobjectSelector selector = new TestableSelector(); + selector.ignoreFls(); + + System.assertEquals( false, selector.isEnforcingFls(), 'ignoreFls, when called, will disable FLS checking' ); + } + + class TestableSelector extends ortoo_SobjectSelector + { + public List getSObjectFieldList() { + return new List { + Account.Name + }; + } + + public Schema.SObjectType getSObjectType() { + return Account.sObjectType; + } + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectSelectorTest.cls-meta.xml b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectSelectorTest.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectSelectorTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectUnitOfWorkTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectUnitOfWorkTest.cls index 86597697447..68b8fb63374 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectUnitOfWorkTest.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectUnitOfWorkTest.cls @@ -1,6 +1,17 @@ @isTest private without sharing class ortoo_SobjectUnitOfWorkTest { + @isTest + private static void elevatedContext_whenCalled_willReturnAnElevatedContext() // NOPMD: Test method name format + { + ortoo_SobjectUnitOfWork uow = new ortoo_SobjectUnitOfWork( new List() ); + + ortoo_SobjectUnitOfWork.ElevatedContext elevatedUow = uow.elevatedContext(); + + System.assertEquals( uow, elevatedUow.uow, 'elevatedContext, when called, will return an elevated context with the UOW set to "this"' ); + } + + @isTest private static void getNumberOfPendingDmlRows_whenNothingHasBeenSentToTheUow_willReturnOne() // NOPMD: Test method name format { diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_UnitOfWorkFactoryTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_UnitOfWorkFactoryTest.cls index bd860916b06..14936a32489 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_UnitOfWorkFactoryTest.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_UnitOfWorkFactoryTest.cls @@ -29,11 +29,11 @@ private without sharing class ortoo_UnitOfWorkFactoryTest // NOPMD: mini-namespa List defaultOrder = new List{ Account.sobjectType, Contact.sobjectType }; ortoo_UnitOfWorkFactory factory = new ortoo_UnitOfWorkFactory( defaultOrder ); - RegisterableIdml idml = new RegisterableIdml(); - ortoo_SobjectUnitOfWork uow = (ortoo_SobjectUnitOfWork)factory.newInstance( idml ); + RegisterableIdml IDml = new RegisterableIdml(); + ortoo_SobjectUnitOfWork uow = (ortoo_SobjectUnitOfWork)factory.newInstance( IDml ); - System.assertEquals( defaultOrder, uow.getSobjectOrder(), 'newInstance, when given no object order and an idml, will create an instance with the default order' ); - System.assertEquals( idml, uow.getIdml(), 'newInstance, when given no object order and an idml, will create an instance with the idml' ); + System.assertEquals( defaultOrder, uow.getSobjectOrder(), 'newInstance, when given no object order and an IDml, will create an instance with the default order' ); + System.assertEquals( IDml, uow.getIdml(), 'newInstance, when given no object order and an IDml, will create an instance with the IDml' ); } @isTest @@ -43,11 +43,11 @@ private without sharing class ortoo_UnitOfWorkFactoryTest // NOPMD: mini-namespa List passedOrder = new List{ Contact.sobjectType, Account.sobjectType }; ortoo_UnitOfWorkFactory factory = new ortoo_UnitOfWorkFactory( defaultOrder ); - RegisterableIdml idml = new RegisterableIdml(); - ortoo_SobjectUnitOfWork uow = (ortoo_SobjectUnitOfWork)factory.newInstance( passedOrder, idml ); + RegisterableIdml IDml = new RegisterableIdml(); + ortoo_SobjectUnitOfWork uow = (ortoo_SobjectUnitOfWork)factory.newInstance( passedOrder, IDml ); - System.assertEquals( passedOrder, uow.getSobjectOrder(), 'newInstance, when given an idml and object order, will create an instance with that order' ); - System.assertEquals( idml, uow.getIdml(), 'newInstance, when given an idml and object order and an idml, will create an instance with the idml' ); + System.assertEquals( passedOrder, uow.getSobjectOrder(), 'newInstance, when given an IDml and object order, will create an instance with that order' ); + System.assertEquals( IDml, uow.getIdml(), 'newInstance, when given an IDml and object order and an IDml, will create an instance with the IDml' ); } @isTest @@ -55,8 +55,8 @@ private without sharing class ortoo_UnitOfWorkFactoryTest // NOPMD: mini-namespa { ortoo_UnitOfWorkFactory factory = new ortoo_UnitOfWorkFactory(); - RegisterableIdml idml = new RegisterableIdml(); - ortoo_SobjectUnitOfWork uow = (ortoo_SobjectUnitOfWork)factory.newInstance( idml ); + RegisterableIdml IDml = new RegisterableIdml(); + ortoo_SobjectUnitOfWork uow = (ortoo_SobjectUnitOfWork)factory.newInstance( IDml ); System.assertEquals( 0, uow.getSobjectOrder().size(), 'newInstance, when given no parameters, will create a version with no object order' ); } @@ -97,8 +97,8 @@ private without sharing class ortoo_UnitOfWorkFactoryTest // NOPMD: mini-namespa Test.startTest(); factory.setMock( mockUow ); - RegisterableIdml idml = new RegisterableIdml(); - ortoo_SobjectUnitOfWork returnedUow = (ortoo_SobjectUnitOfWork)factory.newInstance( idml ); + RegisterableIdml IDml = new RegisterableIdml(); + ortoo_SobjectUnitOfWork returnedUow = (ortoo_SobjectUnitOfWork)factory.newInstance( IDml ); Test.stopTest(); System.assertEquals( mockUow, returnedUow, 'setMock, when new instance is called without an order, will return the mock' ); @@ -112,8 +112,8 @@ private without sharing class ortoo_UnitOfWorkFactoryTest // NOPMD: mini-namespa Test.startTest(); factory.setMock( mockUow ); - RegisterableIdml idml = new RegisterableIdml(); - ortoo_SobjectUnitOfWork returnedUow = (ortoo_SobjectUnitOfWork)factory.newInstance( new List{ Contact.sobjectType }, idml ); + RegisterableIdml IDml = new RegisterableIdml(); + ortoo_SobjectUnitOfWork returnedUow = (ortoo_SobjectUnitOfWork)factory.newInstance( new List{ Contact.sobjectType }, IDml ); Test.stopTest(); System.assertEquals( mockUow, returnedUow, 'setMock, when new instance is called without an order, will return the mock' ); diff --git a/framework/default/ortoo-core/default/classes/utils/Contract.cls b/framework/default/ortoo-core/default/classes/utils/Contract.cls index 5651b25973a..58ed45d8d78 100644 --- a/framework/default/ortoo-core/default/classes/utils/Contract.cls +++ b/framework/default/ortoo-core/default/classes/utils/Contract.cls @@ -51,7 +51,6 @@ public class Contract * @param Boolean The condition that must be true * @param String The message to emit when the condition is not true */ - public static void ensures( Boolean condition, String message ) { if ( !condition ) diff --git a/framework/default/ortoo-core/default/classes/utils/SobjectUtils.cls b/framework/default/ortoo-core/default/classes/utils/SobjectUtils.cls index 4b716233391..04ef72522d1 100644 --- a/framework/default/ortoo-core/default/classes/utils/SobjectUtils.cls +++ b/framework/default/ortoo-core/default/classes/utils/SobjectUtils.cls @@ -1,5 +1,15 @@ /** - * Utility class that provides extra capabilities related to SObjects + * Utility class that provides extra capabilities related to SObjects. + * + * Many of the methods provide shortcuts to pre-existing Salesforce methods. + * For example: record.getSObjectType().getDescribe().isCreateable(); + * These methods exist in order to: + * 1 - Make the code in the app that performs these checks simpler and easier to read. + * 2 - Provide the ability to make performance improvements as and when the + * relative performance of different methods change. + * This may include the ability to cache certain results. + * + * These methods should always be used to ask questions of SObjects. * * @group Utils */ @@ -39,6 +49,20 @@ public inherited sharing class SobjectUtils return ((SObject)Type.forName( sobjectName )?.newInstance())?.getSObjectType(); } + /** + * Given an instance of an SObject, will return its SobjectType + * + * @param Sobject The SObject for which to return the SobjectType + * @return SobjectType The SobjectType of the SObject + */ + public static SobjectType getSobjectType( Sobject record ) + { + Contract.requires( record != null, 'getSobjectType called with a null record' ); + + return record.getSObjectType(); + } + + // TODO: use the describe on the object? /** * Given an instance of an SObject, will return the developer / API name of the SObject (e.g. Contact) * @@ -70,4 +94,61 @@ public inherited sharing class SobjectUtils Contract.ensures( sobjectName != null, 'getSobjectName returned with a null sobjectName' ); return sobjectName; } + + /** + * States if the given SObject is of a type that the current user is allowed to insert + * + * @param SObject The SObject to check the type of + * @return Boolean States if the user can insert records of this type + */ + public static Boolean isCreateable( Sobject record ) + { + Contract.requires( record != null, 'isCreateable called with a null record' ); + + return getSObjectDescribeResult( record ).isCreateable(); + } + + /** + * States if the given SObject is of a type that the current user is allowed to update + * + * @param SObject The SObject to check the type of + * @return Boolean States if the user can update records of this type + */ + public static Boolean isUpdateable( Sobject record ) + { + Contract.requires( record != null, 'isUpdateable called with a null record' ); + + return getSObjectDescribeResult( record ).isUpdateable(); + } + + /** + * States if the given SObject is of a type that the current user is allowed to delete + * + * @param SObject The SObject to check the type of + * @return Boolean States if the user can delete records of this type + */ + public static Boolean isDeletable( Sobject record ) + { + Contract.requires( record != null, 'isDeletable called with a null record' ); + + return getSObjectDescribeResult( record ).isDeletable(); + } + + /** + * Given an SObject record, will return the DescrideSObjectResult for it. + * + * Generally shouldn't be used by external methods. Instead the question of the + * describe result should be asked of SobjectUtils. + * + * For example, see 'isCreateable'. + * + * @param SObject The SObject to get the describe for + * @return DescribeSObjectResult The passed SObject's describe + */ + private static DescribeSObjectResult getSobjectDescribeResult( Sobject record ) + { + Contract.requires( record != null, 'getSobjectDescribeResult called with a null record' ); + + return record.getSObjectType().getDescribe(); + } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/utils/tests/SetUtilsTest.cls b/framework/default/ortoo-core/default/classes/utils/tests/SetUtilsTest.cls index cbfe15f8c58..755d191a9d9 100644 --- a/framework/default/ortoo-core/default/classes/utils/tests/SetUtilsTest.cls +++ b/framework/default/ortoo-core/default/classes/utils/tests/SetUtilsTest.cls @@ -69,12 +69,11 @@ private without sharing class SetUtilsTest TestIdUtils.generateId( Account.getSObjectType(), 1 ) // this is a duplicate }; + Set expectedSet = new Set{ listToConvert[0], listToConvert[1], listToConvert[2] }; + Set convertedSet = SetUtils.convertToSetOfIds( listToConvert ); - System.assertEquals( 3, convertedSet.size(), 'convertToSetOfIds, when given a List of Ids, will return a List with an entry for each of the original unique list records' ); - System.assert( convertedSet.contains( listToConvert[0] ), 'convertToSetOfIds, when given a List of Ids, will return a List with an entry for each of the original unique list records - checking for ' + listToConvert[0] ); - System.assert( convertedSet.contains( listToConvert[1] ), 'convertToSetOfIds, when given a List of Ids, will return a List with an entry for each of the original unique list records - checking for ' + listToConvert[1] ); - System.assert( convertedSet.contains( listToConvert[2] ), 'convertToSetOfIds, when given a List of Ids, will return a List with an entry for each of the original unique list records - checking for ' + listToConvert[2] ); + System.assertEquals( expectedSet, convertedSet, 'convertToSetOfIds, when given a List of Ids, will return a List with an entry for each of the original unique list records' ); } @isTest diff --git a/framework/default/ortoo-core/default/classes/utils/tests/SobjectUtilsTest.cls b/framework/default/ortoo-core/default/classes/utils/tests/SobjectUtilsTest.cls index 61b13985147..cafe13595ac 100644 --- a/framework/default/ortoo-core/default/classes/utils/tests/SobjectUtilsTest.cls +++ b/framework/default/ortoo-core/default/classes/utils/tests/SobjectUtilsTest.cls @@ -42,6 +42,46 @@ private without sharing class SobjectUtilsTest Amoss_Asserts.assertContains( 'Attempted to get the name of a null SObject', exceptionMessage, 'getSobjectType, when given a null Sobject, will throw an exception' ); } + @isTest + private static void getSobjectType_whenGivenARecord_willReturnItsType() // NOPMD: Test method name format + { + SobjectType expectedType = Contact.getSObjectType(); + SobjectType actualType = SobjectUtils.getSobjectType( new Contact() ); + + System.assertEquals( expectedType, actualType, 'getSobjectType, when given an SObject record, will its type' ); + } + + @isTest + private static void getSobjectType_whenGivenARecordHeldInAGenericVariable_willReturnItsType() // NOPMD: Test method name format + { + SobjectType expectedType = Contact.getSObjectType(); + + Sobject contactRecord = new Contact(); + SobjectType actualType = SobjectUtils.getSobjectType( contactRecord ); + + System.assertEquals( expectedType, actualType, 'getSobjectType, when given an SObject record held in a generic variable, will its type' ); + } + + @isTest + private static void getSobjectType_whenPassedANullRecord_willThrowAnException() // NOPMD: Test method name format + { + Sobject nullRecord = null; + + Test.startTest(); + String exceptionMessage; + try + { + SobjectUtils.getSobjectType( nullRecord ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'getSobjectType called with a null record', exceptionMessage, 'getSobjectType, when passed a null record, will throw an exception' ); + } + @isTest private static void getSobjectName_whenGivenAnSobject_willReturnTheApiNameOfIt() // NOPMD: Test method name format { @@ -95,4 +135,91 @@ private without sharing class SobjectUtilsTest Amoss_Asserts.assertContains( 'Attempted to get the name of a null SObjectType', exceptionMessage, 'getSobjectName, when given a null SobjectType, will throw an exception' ); } + + @isTest + private static void isCreateable_whenCalled_willReturnIsCreatableOfThatSobject() // NOPMD: Test method name format + { + SObject record = new Contact(); + + Boolean expectedIsCreateable = record.getSObjectType().getDescribe().isCreateable(); + Boolean actualIsCreateable = SobjectUtils.isCreateable( record ); + + System.assertEquals( expectedIsCreateable, actualIsCreateable, 'isCreatable, when called with an SObject, will return if that SObject Type is createable by the current user' ); + } + + @isTest + private static void isCreateable_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + SobjectUtils.isCreateable( null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'isCreateable called with a null record', exceptionMessage, 'isCreateable, when given a null record, will throw an exception' ); + } + + @isTest + private static void isUpdateable_whenCalled_willReturnIsUpdateableOfThatSobject() // NOPMD: Test method name format + { + SObject record = new Contact(); + + Boolean expectedIsUpdateable = record.getSObjectType().getDescribe().isUpdateable(); + Boolean actualIsUpdateable = SobjectUtils.isUpdateable( record ); + + System.assertEquals( expectedIsUpdateable, actualIsUpdateable, 'isUpdateable, when called with an SObject, will return if that SObject Type is updateable by the current user' ); + } + + @isTest + private static void isUpdateable_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + SobjectUtils.isUpdateable( null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'isUpdateable called with a null record', exceptionMessage, 'isUpdateable, when given a null record, will throw an exception' ); + } + + @isTest + private static void isDeletable_whenCalled_willReturnIsUpdateableOfThatSobject() // NOPMD: Test method name format + { + SObject record = new Contact(); + + Boolean expectedIsDeletable = record.getSObjectType().getDescribe().isDeletable(); + Boolean actualIsDeletable = SobjectUtils.isDeletable( record ); + + System.assertEquals( expectedIsDeletable, actualIsDeletable, 'isDeletable, when called with an SObject, will return if that SObject Type is deletable by the current user' ); + } + + @isTest + private static void isDeletable_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + SobjectUtils.isDeletable( null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'isDeletable called with a null record', exceptionMessage, 'isDeletable, when given a null record, will throw an exception' ); + } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/labels/ortoo-core-CustomLabels.labels-meta.xml b/framework/default/ortoo-core/default/labels/ortoo-core-CustomLabels.labels-meta.xml index e71bc8d9ff0..7c5e114962a 100644 --- a/framework/default/ortoo-core/default/labels/ortoo-core-CustomLabels.labels-meta.xml +++ b/framework/default/ortoo-core/default/labels/ortoo-core-CustomLabels.labels-meta.xml @@ -7,4 +7,60 @@ Message when validation errors caught by the SObject Validator structures occur. Validation Errors Occurred + + ortoo_core_insert + en_US + false + The word 'insert'. + insert + + + ortoo_core_update + en_US + false + The word 'update'. + update + + + ortoo_core_upsert + en_US + false + The word 'upsert'. + upsert + + + ortoo_core_fls_violation + en_US + false + Message when FLS violation occurs on a DML operation. + Attempted to {0} {1} with fields that are not accessible: {2} + + + ortoo_core_crud_insert_violation + en_US + false + Message when CRUD violation occurs on an insert. {0} is the name of the SObject. + Attempted to insert {0} records without the required permission + + + ortoo_core_crud_update_violation + en_US + false + Message when CRUD violation occurs on an update. {0} is the name of the SObject. + Attempted to update {0} records without the required permission + + + ortoo_core_crud_delete_violation + en_US + false + Message when CRUD violation occurs on a delete. {0} is the name of the SObject. + Attempted to delete {0} records without the required permission + + + ortoo_core_crud_publish_violation + en_US + false + Message when CRUD violation occurs on a publish. {0} is the name of the Event. + Attempted to publish {0} events without the required permission + \ No newline at end of file diff --git a/framework/default/sobject-fabricator/classes/ortoo_FabricatedSObjectRegister.cls b/framework/default/sobject-fabricator/classes/ortoo_FabricatedSObjectRegister.cls index d770da338e0..12761b57f7f 100644 --- a/framework/default/sobject-fabricator/classes/ortoo_FabricatedSObjectRegister.cls +++ b/framework/default/sobject-fabricator/classes/ortoo_FabricatedSObjectRegister.cls @@ -104,7 +104,7 @@ public class ortoo_FabricatedSObjectRegister { public void persist() { - ortoo_SObjectUnitOfWork uow = (ortoo_SObjectUnitOfWork)Application.UNIT_OF_WORK.newInstance( getOrderOfInserts() ); + ortoo_SObjectUnitOfWork uow = (ortoo_SObjectUnitOfWork)Application.UNIT_OF_WORK.newInstance( getOrderOfInserts(), new UnsecureDml() ); buildObjectsByFabricated(); registerInserts( uow );