Skip to content

Commit

Permalink
Started to test SecureDml
Browse files Browse the repository at this point in the history
Fix issue where CUD would be checked by stripInaccessible even when we said to not check CUD
  • Loading branch information
rob-baillie-ortoo committed Dec 9, 2021
1 parent 98d3472 commit 4b2968e
Show file tree
Hide file tree
Showing 3 changed files with 253 additions and 6 deletions.
1 change: 1 addition & 0 deletions TODO.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Test:

Look at:
SobjectUtils.getSobjectName
Do we need to add 'emptyRecycleBin' too?

Test Manually:
Implementation os SecureDml in general - does it actually work
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// TODO: test
/**
* Is an implementation of the IDml interface used to manage the DML operations in an SObject Unit of Work.
*
Expand Down Expand Up @@ -268,7 +267,7 @@ public inherited sharing virtual class SecureDml extends fflib_SobjectUnitOfWork
private void checkFls( List<Sobject> objList, AccessType mode )
{
String sobjectTypeName = SobjectUtils.getSobjectName( objList[0] );
SObjectAccessDecision securityDecision = Security.stripInaccessible( mode, objList );
SObjectAccessDecision securityDecision = Security.stripInaccessible( mode, objList, false );
Set<String> removedFields = securityDecision.getRemovedFields().get( sobjectTypeName );

if ( removedFields != null && !removedFields.isEmpty() )
Expand Down Expand Up @@ -420,17 +419,20 @@ public inherited sharing virtual class SecureDml extends fflib_SobjectUnitOfWork
}
}

private virtual Boolean userCanCreate( Sobject record )
// This method cannot be reliably unit tested in a framework that does not inculde profiles and suchlike
protected virtual Boolean userCanCreate( Sobject record )
{
return SobjectUtils.isCreateable( record );
}

private virtual Boolean userCanUpdate( Sobject record )
// This method cannot be reliably unit tested in a framework that does not inculde profiles and suchlike
protected virtual Boolean userCanUpdate( Sobject record )
{
return SobjectUtils.isUpdateable( record );
}

private virtual Boolean userCanDelete( Sobject record )
// This method cannot be reliably unit tested in a framework that does not inculde profiles and suchlike
protected virtual Boolean userCanDelete( Sobject record )
{
return SobjectUtils.isUpdateable( record );
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,249 @@

// If there is any risk that this test would stop working in a given App implementation, then it should be rewritten or removed.
// It is written in a way that assumes that Accounts and Contacts are configured to allow for the most trivial records to be created
@isTest
private without sharing class SecureDmlTest
{
@isTest
private static void dmlInsert_whenTheUserCanCreateTheRecords_willInsertTheRecords() // NOPMD: Test method name format
{
List<Account> accounts = new List<Account>
{
(Account)new FAccount().name( 'Account1' ).toPersistableSobject(),
(Account)new FAccount().name( 'Account2' ).toPersistableSobject()
};

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<Account> createdAccounts = getAccounts();

System.assertEquals( 'Account1', accounts[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', accounts[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_whenTheUserCannotCreateRecords_willThrowAnException() // NOPMD: Test method name format
{
List<Account> accounts = new List<Account>
{
(Account)new FAccount().name( 'Account1' ).toPersistableSobject(),
(Account)new FAccount().name( 'Account2' ).toPersistableSobject()
};

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_whenTheUserCanNotCreateButCudOff_willInsertTheRecords() // NOPMD: Test method name format
{
List<Account> accounts = new List<Account>
{
(Account)new FAccount().name( 'Account1' ).toPersistableSobject(),
(Account)new FAccount().name( 'Account2' ).toPersistableSobject()
};

Test.startTest();

SecureDml dml = new TestableSecureDml();

((TestableSecureDml)dml).canCreate = false; // mimics not having write access

dml.ignoreCudSettings();
dml.dmlInsert( accounts );

Test.stopTest();

System.assertNotEquals( null, accounts[0].Id, 'dmlInsert, when the user cannot create the records but cud 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 cud switched off, will insert the records, setting the Id on them (1)' );
}

@isTest
private static void dmlInsert_whenTheUserCanNotCreateButCudOffForThatObject_willInsertTheRecords() // NOPMD: Test method name format
{
List<Account> accounts = new List<Account>
{
(Account)new FAccount().name( 'Account1' ).toPersistableSobject(),
(Account)new FAccount().name( 'Account2' ).toPersistableSobject()
};

Test.startTest();

SecureDml dml = new TestableSecureDml()
.ignoreCudSettingsFor( 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 cud 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 cud switched off for that sobject type, will insert the records, setting the Id on them (1)' );
}

@isTest
private static void dmlInsert_whenTheUserCanNotCreateButCudOffForMultipleObjects_willInsertTheRecords() // NOPMD: Test method name format
{
List<Account> accounts = new List<Account>
{
(Account)new FAccount().name( 'Account1' ).toPersistableSobject(),
(Account)new FAccount().name( 'Account2' ).toPersistableSobject()
};

Test.startTest();

SecureDml dml = new TestableSecureDml()
.ignoreCudSettingsFor( Account.SobjectType )
.ignoreCudSettingsFor( 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 cud 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 cud switched off for multiple sobject types, including that one, will insert the records, setting the Id on them (1)' );
}

@isTest
private static void dmlInsert_whenTheUserCannotCreateRecordsAndCudOfForOtherObjects_willThrowAnException() // NOPMD: Test method name format
{
List<Account> accounts = new List<Account>
{
(Account)new FAccount().name( 'Account1' ).toPersistableSobject(),
(Account)new FAccount().name( 'Account2' ).toPersistableSobject()
};

Test.startTest();
ortoo_Exception thrownException;
try
{
SecureDml dml = new TestableSecureDml()
.ignoreCudSettingsFor( 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 cud is off for other objects, will still throw an exception' );
}

@isTest
private static void dmlInsert_whenGivenAnEmptyList_willDoNothing() // NOPMD: Test method name format
{
List<Account> emptyList = new List<Account>();

Test.startTest();

SecureDml dml = new TestableSecureDml();
dml.dmlInsert( emptyList );

Test.stopTest();

System.assertEquals( 0, Limits.getDmlStatements(), 'dmlInsert, when given an empty list, will not issue any DML' );
}

@isTest
private static void dmlInsert_whenGivenAnEmptyListAndCreateIsNotAllowed_willDoNothing() // NOPMD: Test method name format
{
List<Account> emptyList = new List<Account>();

Test.startTest();

SecureDml dml = new TestableSecureDml();

((TestableSecureDml)dml).canCreate = false;

dml.dmlInsert( emptyList );

Test.stopTest();

System.assertEquals( 0, Limits.getDmlStatements(), 'dmlInsert, when given an empty list of objects the user cannot create, will not issue any DML or throw an exception' );
}

private static List<Account> getAccounts()
{
return [SELECT Name FROM Account ORDER BY Name];
}

// version of SecureDml that allows us to override the checks on whether
// the current user can create / update / delete
private inherited sharing class TestableSecureDml extends SecureDml
{
public Boolean canCreate = true;
public Boolean canUpdate = true;
public Boolean canDelete = true;

protected override Boolean userCanCreate( Sobject record )
{
return canCreate;
}

protected override Boolean userCanUpdate( Sobject record )
{
return canUpdate;
}

protected override Boolean userCanDelete( Sobject record )
{
return canDelete;
}
}

private inherited sharing class FAccount extends sfab_FabricatedSobject
{
public FAccount()
{
super( Account.class );
name( 'Default Name' );
}

public FAccount name( String name )
{
set( Account.Name, name );
return this;
}
}
}

0 comments on commit 4b2968e

Please sign in to comment.