diff --git a/framework/default/fflib-apex-extensions/default/classes/criteria/fflib_Criteria.cls b/framework/default/fflib-apex-extensions/default/classes/criteria/fflib_Criteria.cls index 209357e72e0..151beba5855 100755 --- a/framework/default/fflib-apex-extensions/default/classes/criteria/fflib_Criteria.cls +++ b/framework/default/fflib-apex-extensions/default/classes/criteria/fflib_Criteria.cls @@ -54,7 +54,9 @@ public virtual with sharing class fflib_Criteria this.type = 'AND'; } - // TODO: document and test + /** + * Adds the given evalutor onto the formula stack + */ protected void addEvaluator( Evaluator evaluator ) { evaluators.add( evaluator ); diff --git a/framework/default/fflib/default/classes/common/fflib_SObjectSelector.cls b/framework/default/fflib/default/classes/common/fflib_SObjectSelector.cls index 00bc12d448d..be8c0ef8a18 100644 --- a/framework/default/fflib/default/classes/common/fflib_SObjectSelector.cls +++ b/framework/default/fflib/default/classes/common/fflib_SObjectSelector.cls @@ -91,7 +91,7 @@ public abstract with sharing class fflib_SObjectSelector * Implement this method to inform the base class of the SObject (custom or standard) to be queried **/ abstract Schema.SObjectType getSObjectType(); - + /** * Implement this method to inform the base class of the common fields to be queried or listed by the base class methods **/ diff --git a/framework/default/ortoo-core/default/classes/exceptions/utilities/StackTrace.cls b/framework/default/ortoo-core/default/classes/exceptions/utilities/StackTrace.cls index 95dee22cf53..5e7f37e6119 100644 --- a/framework/default/ortoo-core/default/classes/exceptions/utilities/StackTrace.cls +++ b/framework/default/ortoo-core/default/classes/exceptions/utilities/StackTrace.cls @@ -176,6 +176,7 @@ public class StackTrace { return stackTraceEntries[0]; } + @testVisible private StackTraceEntry getEntryStackTraceEntry() { if ( stackTraceEntries.isEmpty() ) diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_Criteria.cls b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_Criteria.cls index d32ce4860a7..bbdf47d27cc 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_Criteria.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_Criteria.cls @@ -5,7 +5,6 @@ */ public inherited sharing virtual class ortoo_Criteria extends fflib_Criteria implements ISearchCriteria // NOPMD: specified a mini-namespace to differentiate from fflib versions { - public virtual ortoo_Criteria likeString( Schema.SObjectField field, Object value ) { addEvaluator( new FieldEvaluator( field, fflib_Operator.LIKEx, value ) ); 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 986d8063591..e7047275ab4 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 @@ -7,69 +7,98 @@ */ public abstract inherited sharing class ortoo_SobjectSelector extends fflib_SobjectSelector implements ISearchSelector // NOPMD: specified a mini-namespace to differentiate from fflib versions { - class UnboundCountQueryException extends Exceptions.SelectorException {} - - 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; - } - - // TODO: document - // TODO: test - // TODO: security - public SearchResults selectBySearchCriteria( ISearchConfiguration searchConfiguration, ISearchCriteria criteria, SearchWindow window, SearchOrderBy orderBy ) - { - Contract.requires( criteria != null, 'selectByCriteria called with a null criteria' ); - - Integer countOfRecords = getCountOfRecords( criteria ); - - List resultsRecords = new List(); - - if ( countOfRecords > 0 ) - { - fflib_QueryFactory queryFactory = newQueryFactory().setCondition( criteria.toSOQL() ); - - queryFactory.selectFields( searchConfiguration.getRequiredFields() ); - queryFactory.setOffset( window.offset ); - queryFactory.setLimit( window.length ); - queryFactory.setOrdering( orderBy.fieldName , orderBy.direction == 'desc' ? fflib_QueryFactory.SortOrder.DESCENDING : fflib_QueryFactory.SortOrder.ASCENDING ); - - resultsRecords = Database.query( queryFactory.toSOQL() ); - } - - return new SearchResults( countOfRecords, resultsRecords ); - } - - // TODO: document - // TODO: test - protected Integer getCountOfRecords( ISearchCriteria soqlCriteria ) - { - // TODO: security - String whereClause = soqlCriteria.toSOQL(); - - if ( String.isBlank( whereClause ) ) - { - throw new UnboundCountQueryException( 'Attempted to perform a count on an unbound query against ' + getSObjectName() ) - .setErrorCode( FrameworkErrorCodes.SELECTOR_UNBOUND_COUNT_QUERY ) - .addContext( 'SObjectType', getSObjectName() ); - } - - String query = 'SELECT COUNT(Id) recordCount FROM ' + getSObjectName() + ' WHERE ' + whereClause; - - AggregateResult result = (AggregateResult)Database.query( query ); // NOPMD: variables come from a trusted source - - return (Integer)result.get( 'recordCount' ); - } + public class UnboundCountQueryException extends Exceptions.SelectorException {} + + 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; + } + + /** + * Given a configuration, search criteria, a window and an order by, will perform the defined search. + * + * The results includes a total record count as well as the windowed search results. + * + * WIll perform a maximum of 2 SOQL queries. + * + * @param ISearchConfiguration The configuration defining the additional fields that should be returned. + * @param ISearchCriteria The criteria defining the records that should be returned. + * @param SearchWindow Defines the subset of the full result set that should be returned. + * @param SearchOrderBy The order in which the records should be returned (is applied prior to the window). + * @return SearchResults The resulting total record count and result set. + */ + public SearchResults selectBySearchCriteria( ISearchConfiguration searchConfiguration, ISearchCriteria criteria, SearchWindow window, SearchOrderBy orderBy ) + { + Contract.requires( searchConfiguration != null, 'selectBySearchCriteria called with a null searchConfiguration' ); + Contract.requires( criteria != null, 'selectBySearchCriteria called with a null criteria' ); + Contract.requires( window != null, 'selectBySearchCriteria called with a null window' ); + Contract.requires( orderBy != null, 'selectBySearchCriteria called with a null orderBy' ); + + Integer countOfRecords = getCountOfRecords( criteria ); + + List resultsRecords = new List(); + + if ( countOfRecords > 0 ) + { + fflib_QueryFactory queryFactory = newQueryFactory().setCondition( criteria.toSOQL() ); + + queryFactory.selectFields( searchConfiguration.getRequiredFields() ); + queryFactory.setOffset( window.offset ); + queryFactory.setLimit( window.length ); + + if ( orderBy.isConfigured() ) + { + queryFactory.setOrdering( orderBy.fieldName , orderBy.direction == 'desc' ? fflib_QueryFactory.SortOrder.DESCENDING : fflib_QueryFactory.SortOrder.ASCENDING ); + } + + resultsRecords = Database.query( queryFactory.toSOQL() ); + } + + return new SearchResults( countOfRecords, resultsRecords ); + } + + /** + * Given a set of search criteria, will return the number of records that meet that criteria + * + * Will perform a maximum of 1 SOQL query + * + * @param ISearchCriteria The criteria defining the records that should be returned. + * @return Integer The total number of records that match the criteria + */ + protected Integer getCountOfRecords( ISearchCriteria criteria ) + { + Contract.requires( criteria != null, 'getCountOfRecords called with a null criteria' ); + + String whereClause = criteria.toSOQL(); + + if ( String.isBlank( whereClause ) ) + { + throw new UnboundCountQueryException( 'Attempted to perform the count of an unbound query against ' + getSObjectName() ) + .setErrorCode( FrameworkErrorCodes.SELECTOR_UNBOUND_COUNT_QUERY ) + .addContext( 'SObjectType', getSObjectName() ); + } + + if ( isEnforcingCRUD() && ! SobjectUtils.isAccessible( sObjectType() ) ) + { + throw new fflib_SObjectDomain.DomainException( 'Permission to access ' + getSObjectName() + ' denied.' ); // to match the exception thrown by fflib + } + + String query = 'SELECT COUNT(Id) recordCount FROM ' + getSObjectName() + ' WHERE ' + whereClause; + + AggregateResult result = (AggregateResult)Database.query( query ); // NOPMD: variables come from a trusted source + + return (Integer)result.get( 'recordCount' ); + } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls new file mode 100644 index 00000000000..b7e4cff2d93 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls @@ -0,0 +1,33 @@ +@isTest +private without sharing class ortoo_CriteriaTest +{ + @isTest + private static void likeString_field_whenCalled_addsALikeToTheGeneratedSoql() // NOPMD: Test method name format + { + ortoo_Criteria criteria = new ortoo_Criteria(); + + Test.startTest(); + criteria.likeString( Contact.Name, 'thing%' ); + Test.stopTest(); + + String expected = 'Name LIKE \'thing%\''; + String got = criteria.toSOQL(); + + System.assertEquals( expected, got, 'likeString, when called with a field, will add a LIKE to the Generated SOQL' ); + } + + @isTest + private static void likeString_stringName_whenCalled_addsALikeToTheGeneratedSoql() // NOPMD: Test method name format + { + ortoo_Criteria criteria = new ortoo_Criteria(); + + Test.startTest(); + criteria.likeString( 'Account.Name', 'thing%' ); + Test.stopTest(); + + String expected = 'Account.Name LIKE \'thing%\''; + String got = criteria.toSOQL(); + + System.assertEquals( expected, got, 'likeString, when called with a string name for a field will add a LIKE to the Generated SOQL' ); + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls-meta.xml b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.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 index 1eafb7f047c..b2978c5adc6 100644 --- 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 @@ -19,6 +19,258 @@ private without sharing class ortoo_SobjectSelectorTest System.assertEquals( false, selector.isEnforcingFls(), 'ignoreFls, when called, will disable FLS checking' ); } + @isTest + private static void selectBySearchCriteria_whenGivenCriteria_returnsRecordsThatMatchWithACount() // NOPMD: Test method name format + { + List accountList = new List + { + new Account( Name = 'Acc1', AnnualRevenue = 123 ), + new Account( Name = 'Acc2', AnnualRevenue = 234 ), + new Account( Name = 'Acc3', AnnualRevenue = 345 ), + new Account( Name = 'Acc4', AnnualRevenue = 456 ), + new Account( Name = 'Acc5', AnnualRevenue = 567 ) + }; + insert accountList; + + Amoss_Instance configController = ApplicationMockRegistrar.registerMockAppLogic( ISearchConfiguration.class ); + Amoss_Instance criteriaController = new Amoss_Instance( ISearchCriteria.class ); + + ISearchConfiguration config = (ISearchConfiguration)configController.generateDouble(); + configController + .expects( 'getRequiredFields' ) + .returns( new List{ 'AnnualRevenue' } ) + .also() + .when( 'getMappedSobjectField' ) + .returns( 'Name' ); + + ISearchCriteria criteria = (ISearchCriteria)criteriaController.generateDouble(); + criteriaController + .when( 'toSOQL' ) + .returns( 'AnnualRevenue > 200' ); + + SearchWindow window = new SearchWindow().configure( new Map{ 'offset' => 1, 'length' => 2 } ); + SearchOrderBy orderBy = new SearchOrderBy().configure( new Map{ 'fieldName' => 'Name', 'direction' => 'desc' }, ISearchConfiguration.class ); + + Test.startTest(); + SearchResults got = new TestableSelector().selectBySearchCriteria( config, criteria, window, orderBy ); + Test.stopTest(); + + configController.verify(); + + System.assertEquals( 4, got.totalNumberOfRecords, 'selectBySearchCriteria, when given config, criteria, window and orderby, will return the total number of records that match' ); + + List returnedAccounts = (List)got.records; + System.assertEquals( 2, returnedAccounts.size(), 'selectBySearchCriteria, when given config, criteria, window and orderby, will return records that match, limited by the window size' ); + + System.assertEquals( 'Acc4', returnedAccounts[0].Name, 'selectBySearchCriteria, when given config, criteria, window and orderby, will return records that match, ordered by the orderBy, with the base selector fields included (1)' ); + System.assertEquals( 'Acc3', returnedAccounts[1].Name, 'selectBySearchCriteria, when given config, criteria, window and orderby, will return records that match, ordered by the orderBy, with the base selector fields included (2)' ); + + System.assertEquals( 456, returnedAccounts[0].AnnualRevenue, 'selectBySearchCriteria, when given config, criteria, window and orderby, will return records that match, ordered by the orderBy, with the passed config fields included (2)' ); + System.assertEquals( 345, returnedAccounts[1].AnnualRevenue, 'selectBySearchCriteria, when given config, criteria, window and orderby, will return records that match, ordered by the orderBy, with the passed config fields included (2)' ); + } + + @isTest + private static void selectBySearchCriteria_whenNoWindowIsSet_willReturnAllTheResults() // NOPMD: Test method name format + { + List accountList = new List + { + new Account( Name = 'Acc1', AnnualRevenue = 123 ), + new Account( Name = 'Acc2', AnnualRevenue = 234 ), + new Account( Name = 'Acc3', AnnualRevenue = 345 ), + new Account( Name = 'Acc4', AnnualRevenue = 456 ), + new Account( Name = 'Acc5', AnnualRevenue = 567 ) + }; + insert accountList; + + Amoss_Instance configController = ApplicationMockRegistrar.registerMockAppLogic( ISearchConfiguration.class ); + Amoss_Instance criteriaController = new Amoss_Instance( ISearchCriteria.class ); + + ISearchConfiguration config = (ISearchConfiguration)configController.generateDouble(); + configController + .when( 'getRequiredFields' ) + .returns( new List() ) + .also() + .when( 'getMappedSobjectField' ) + .returns( 'Name' ); + + ISearchCriteria criteria = (ISearchCriteria)criteriaController.generateDouble(); + criteriaController + .when( 'toSOQL' ) + .returns( 'AnnualRevenue > 200' ); + + SearchWindow emptyWindow = new SearchWindow(); + SearchOrderBy orderBy = new SearchOrderBy().configure( new Map{ 'fieldName' => 'Name', 'direction' => 'desc' }, ISearchConfiguration.class ); + + Test.startTest(); + SearchResults got = new TestableSelector().selectBySearchCriteria( config, criteria, emptyWindow, orderBy ); + Test.stopTest(); + + configController.verify(); + + System.assertEquals( 4, got.totalNumberOfRecords, 'selectBySearchCriteria, when given config, criteria, window and orderby, will return the total number of records that match' ); + + List returnedAccounts = (List)got.records; + System.assertEquals( 4, returnedAccounts.size(), 'selectBySearchCriteria, when given an empty window, will return all the records that match' ); + } + + @isTest + private static void selectBySearchCriteria_whenGivenEmptyOrderBy_stillReturnsResults() // NOPMD: Test method name format + { + List accountList = new List + { + new Account( Name = 'Acc1', AnnualRevenue = 123 ), + new Account( Name = 'Acc2', AnnualRevenue = 234 ), + new Account( Name = 'Acc3', AnnualRevenue = 345 ), + new Account( Name = 'Acc4', AnnualRevenue = 456 ), + new Account( Name = 'Acc5', AnnualRevenue = 567 ) + }; + insert accountList; + + Amoss_Instance configController = ApplicationMockRegistrar.registerMockAppLogic( ISearchConfiguration.class ); + Amoss_Instance criteriaController = new Amoss_Instance( ISearchCriteria.class ); + + ISearchConfiguration config = (ISearchConfiguration)configController.generateDouble(); + configController + .when( 'getRequiredFields' ) + .returns( new List() ); + + ISearchCriteria criteria = (ISearchCriteria)criteriaController.generateDouble(); + criteriaController + .when( 'toSOQL' ) + .returns( 'AnnualRevenue > 200' ); + + SearchWindow window = new SearchWindow().configure( new Map{ 'offset' => 1, 'length' => 2 } ); + SearchOrderBy emptyOrderBy = new SearchOrderBy(); + + Test.startTest(); + SearchResults got = new TestableSelector().selectBySearchCriteria( config, criteria, window, emptyOrderBy ); + Test.stopTest(); + + System.assertEquals( 4, got.totalNumberOfRecords, 'selectBySearchCriteria, when given an empty order by, will return the total number of records that match' ); + + List returnedAccounts = (List)got.records; + System.assertEquals( 2, returnedAccounts.size(), 'selectBySearchCriteria, when given an empty order by, will return records that match, limited by the window size' ); + } + + @isTest + private static void selectBySearchCriteria_whenGivenEmptyCriteria_throwsAnException() // NOPMD: Test method name format + { + Amoss_Instance configController = ApplicationMockRegistrar.registerMockAppLogic( ISearchConfiguration.class ); + Amoss_Instance criteriaController = new Amoss_Instance( ISearchCriteria.class ); + + ISearchConfiguration config = (ISearchConfiguration)configController.generateDouble(); + + SearchWindow window = new SearchWindow(); + SearchOrderBy orderBy = new SearchOrderBy(); + + ISearchCriteria criteria = (ISearchCriteria)criteriaController.generateDouble(); + criteriaController + .when( 'toSOQL' ) + .returns( '' ); + + Test.startTest(); + String exceptionMessage; + try + { + new TestableSelector().selectBySearchCriteria( config, criteria, window, orderBy ); + } + catch ( ortoo_SobjectSelector.UnboundCountQueryException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'Attempted to perform the count of an unbound query against ' + Account.sobjectType.getDescribe().getName(), exceptionMessage, 'selectBySearchCriteria, when given empty criteria, will throw an exception' ); + } + + @isTest + private static void selectBySearchCriteria_whenPassedANullConfig_throwsAnException() // NOPMD: Test method name format + { + ISearchCriteria criteria = (ISearchCriteria)new Amoss_Instance( ISearchCriteria.class ).generateDouble(); + SearchWindow window = new SearchWindow(); + SearchOrderBy orderBy = new SearchOrderBy(); + + Test.startTest(); + String exceptionMessage; + try + { + new TestableSelector().selectBySearchCriteria( null, criteria, window, orderBy ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'selectBySearchCriteria called with a null searchConfiguration', exceptionMessage, 'selectBySearchCriteria, when passed a null searchConfiguration, will throw an exception' ); + } + + @isTest + private static void selectBySearchCriteria_whenPassedANullCriteria_throwsAnException() // NOPMD: Test method name format + { + ISearchConfiguration config = (ISearchConfiguration)new Amoss_Instance( ISearchConfiguration.class ).generateDouble(); + SearchWindow window = new SearchWindow(); + SearchOrderBy orderBy = new SearchOrderBy(); + + Test.startTest(); + String exceptionMessage; + try + { + new TestableSelector().selectBySearchCriteria( config, null, window, orderBy ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'selectBySearchCriteria called with a null criteria', exceptionMessage, 'selectBySearchCriteria, when passed a null criteria, will throw an exception' ); + } + + @isTest + private static void selectBySearchCriteria_whenPassedANullWindow_throwsAnException() // NOPMD: Test method name format + { + ISearchConfiguration config = (ISearchConfiguration)new Amoss_Instance( ISearchConfiguration.class ).generateDouble(); + ISearchCriteria criteria = (ISearchCriteria)new Amoss_Instance( ISearchCriteria.class ).generateDouble(); + SearchOrderBy orderBy = new SearchOrderBy(); + + Test.startTest(); + String exceptionMessage; + try + { + new TestableSelector().selectBySearchCriteria( config, criteria, null, orderBy ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'selectBySearchCriteria called with a null window', exceptionMessage, 'selectBySearchCriteria, when passed a null window, will throw an exception' ); + } + + @isTest + private static void selectBySearchCriteria_whenPassedANullOrderBy_throwsAnException() // NOPMD: Test method name format + { + ISearchConfiguration config = (ISearchConfiguration)new Amoss_Instance( ISearchConfiguration.class ).generateDouble(); + ISearchCriteria criteria = (ISearchCriteria)new Amoss_Instance( ISearchCriteria.class ).generateDouble(); + SearchWindow window = new SearchWindow(); + + Test.startTest(); + String exceptionMessage; + try + { + new TestableSelector().selectBySearchCriteria( config, criteria, window, null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'selectBySearchCriteria called with a null orderBy', exceptionMessage, 'selectBySearchCriteria, when passed a null orderBy, will throw an exception' ); + } + class TestableSelector extends ortoo_SobjectSelector { public List getSObjectFieldList() { diff --git a/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls b/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls index 3adb699e66d..8863b71c719 100644 --- a/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls +++ b/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls @@ -10,8 +10,8 @@ public inherited sharing class SearchOrderBy private static final String DIRECTION_ASCENDING = 'asc'; private static final String DIRECTION_DESCENDING = 'desc'; - public String fieldName = ''; - public String direction = ''; + public String fieldName {get; private set;} + public String direction {get; private set;} /** * Configures the order by, defining its properties. @@ -57,4 +57,14 @@ public inherited sharing class SearchOrderBy } return this; } + + /** + * States if the OrderBy object has been configured with a fieldName that makes sense. + * + * @return Boolean Is this object configured + */ + public Boolean isConfigured() + { + return String.isNotBlank( fieldName ); + } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/SearchWindow.cls b/framework/default/ortoo-core/default/classes/search/SearchWindow.cls index 6c4177f1737..f6266f34f5f 100644 --- a/framework/default/ortoo-core/default/classes/search/SearchWindow.cls +++ b/framework/default/ortoo-core/default/classes/search/SearchWindow.cls @@ -7,8 +7,8 @@ */ public inherited sharing class SearchWindow { - public Integer offset; - public Integer length; + public Integer offset {get; private set;} + public Integer length {get; private set;} /** * Configures the current instance with the given properties, defining the window diff --git a/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls b/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls index 56c351cc4df..3c9ed4faa44 100644 --- a/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls +++ b/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls @@ -24,10 +24,12 @@ public inherited sharing class SearchOrderByTest System.assertEquals( 'targetFieldName', orderBy.fieldName, 'configure, when given properties with a fieldName and direction, will map the fieldName using the configuration and set the member variable' ); System.assertEquals( 'asc', orderBy.direction, 'configure, when given properties with a fieldName and direction, will set the direction member variable' ); + + System.assert( orderBy.isConfigured(), 'configure, when given properties with a fieldName and direction, will set the orderBy to configured' ); } @isTest - private static void configure_whenNoFieldName_setsFieldNameAndDirectionToEmpty() // NOPMD: Test method name format + private static void configure_whenNoFieldName_setsFieldNameAndDirectionToNull() // NOPMD: Test method name format { Map properties = new Map{ 'direction' => 'asc' @@ -44,12 +46,14 @@ public inherited sharing class SearchOrderByTest configurationController.verify(); - System.assertEquals( '', orderBy.fieldName, 'configure, when given properties with no fieldName and direction, will set the fieldName to an empty string' ); - System.assertEquals( '', orderBy.direction, 'configure, when given properties with no fieldName and direction, will set the direction to an empty string' ); + System.assertEquals( null, orderBy.fieldName, 'configure, when given properties with no fieldName and direction, will set the fieldName to null' ); + System.assertEquals( null, orderBy.direction, 'configure, when given properties with no fieldName and direction, will set the direction to null' ); + + System.assert( ! orderBy.isConfigured(), 'configure, when given properties with no fieldName and direction, will set the orderBy to not configured' ); } @isTest - private static void configure_whenFieldNameEmpty_setsFieldNameAndDirectionToEmpty() // NOPMD: Test method name format + private static void configure_whenFieldNameEmpty_setsFieldNameAndDirectionToNull() // NOPMD: Test method name format { Map properties = new Map{ 'fieldName' => '', @@ -67,12 +71,14 @@ public inherited sharing class SearchOrderByTest configurationController.verify(); - System.assertEquals( '', orderBy.fieldName, 'configure, when given properties with empty fieldName and direction, will set the fieldName to an empty string' ); - System.assertEquals( '', orderBy.direction, 'configure, when given properties with empty fieldName and direction, will set the fieldName to an empty string' ); + System.assertEquals( null, orderBy.fieldName, 'configure, when given properties with empty fieldName and direction, will set the fieldName to null' ); + System.assertEquals( null, orderBy.direction, 'configure, when given properties with empty fieldName and direction, will set the fieldName to null' ); + + System.assert( ! orderBy.isConfigured(), 'configure, when given properties with empty fieldName and direction, will set the orderBy to not configured' ); } @isTest - private static void configure_whenFieldNameIsUnmapped_setsFieldNameAndDirectionToEmpty() // NOPMD: Test method name format + private static void configure_whenFieldNameIsUnmapped_setsFieldNameAndDirectionToNull() // NOPMD: Test method name format { Map properties = new Map{ 'fieldName' => 'unmapped', @@ -91,8 +97,10 @@ public inherited sharing class SearchOrderByTest configurationController.verify(); - System.assertEquals( '', orderBy.fieldName, 'configure, when given properties with unmapped fieldName, will set the fieldName to an empty string' ); - System.assertEquals( '', orderBy.direction, 'configure, when given properties with unmapped fieldName, will set the direction to an empty string' ); + System.assertEquals( null, orderBy.fieldName, 'configure, when given properties with unmapped fieldName, will set the fieldName to null' ); + System.assertEquals( null, orderBy.direction, 'configure, when given properties with unmapped fieldName, will set the direction to null' ); + + System.assert( ! orderBy.isConfigured(), 'configure, when given properties with unmapped fieldName, will set the orderBy to not configured' ); } @isTest @@ -116,6 +124,8 @@ public inherited sharing class SearchOrderByTest System.assertEquals( 'asc', orderBy.direction, 'configure, when given properties with no direction, will set the direction to asc' ); System.assertEquals( 'target', orderBy.fieldName, 'configure, when given properties with no direction, will set the fieldName' ); + + System.assert( orderBy.isConfigured(), 'configure, when given properties with no direction, will set the orderBy to configured' ); } @isTest @@ -169,7 +179,6 @@ public inherited sharing class SearchOrderByTest @isTest private static void configure_whenPassedInvalidDirection_throwsAnException() // NOPMD: Test method name format { - Map properties = new Map{ 'fieldName' => 'source', 'direction' => 'invalid' @@ -290,5 +299,4 @@ public inherited sharing class SearchOrderByTest return null; } } - } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/utils/SobjectUtils.cls b/framework/default/ortoo-core/default/classes/utils/SobjectUtils.cls index 04ef72522d1..4667e1b899c 100644 --- a/framework/default/ortoo-core/default/classes/utils/SobjectUtils.cls +++ b/framework/default/ortoo-core/default/classes/utils/SobjectUtils.cls @@ -98,7 +98,7 @@ public inherited sharing class SobjectUtils /** * 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 + * @param SObject The SObject to check 'isCreatable' of * @return Boolean States if the user can insert records of this type */ public static Boolean isCreateable( Sobject record ) @@ -111,7 +111,7 @@ public inherited sharing class SobjectUtils /** * 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 + * @param SObject The SObject to check 'isUpdateable' of * @return Boolean States if the user can update records of this type */ public static Boolean isUpdateable( Sobject record ) @@ -124,7 +124,7 @@ public inherited sharing class SobjectUtils /** * 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 + * @param SObject The SObject to check 'isDeletable' of * @return Boolean States if the user can delete records of this type */ public static Boolean isDeletable( Sobject record ) @@ -134,6 +134,58 @@ public inherited sharing class SobjectUtils return getSObjectDescribeResult( record ).isDeletable(); } + /** + * States if the given SObjectType is one that the current user is allowed to insert + * + * @param SobjectType The SObject to check 'isCreateable' of + * @return Boolean States if the user can insert records of this type + */ + public static Boolean isCreateable( SobjectType type ) + { + Contract.requires( type != null, 'isCreateable called with a null type' ); + + return getSObjectDescribeResult( type ).isCreateable(); + } + + /** + * States if the given SObjectType is one that the current user is allowed to update + * + * @param SobjectType The SObject to check 'isUpdateable' of + * @return Boolean States if the user can update records of this type + */ + public static Boolean isUpdateable( SobjectType type ) + { + Contract.requires( type != null, 'isUpdateable called with a null type' ); + + return getSObjectDescribeResult( type ).isUpdateable(); + } + + /** + * States if the given SObjectType is one that the current user is allowed to delete + * + * @param SobjectType The SObject to check 'isDeletable' of + * @return Boolean States if the user can delete records of this type + */ + public static Boolean isDeletable( SobjectType type ) + { + Contract.requires( type != null, 'isDeletable called with a null type' ); + + return getSObjectDescribeResult( type ).isDeletable(); + } + + /** + * States if the given SObjectType is one that the current user is allowed to access + * + * @param SobjectType The SObject to check 'isAccessible' of + * @return Boolean States if the user can delete records of this type + */ + public static Boolean isAccessible( SobjectType type ) + { + Contract.requires( type != null, 'isAccessible called with a null type' ); + + return getSObjectDescribeResult( type ).isAccessible(); + } + /** * Given an SObject record, will return the DescrideSObjectResult for it. * @@ -149,6 +201,24 @@ public inherited sharing class SobjectUtils { Contract.requires( record != null, 'getSobjectDescribeResult called with a null record' ); - return record.getSObjectType().getDescribe(); + return getSobjectDescribeResult( record.getSObjectType() ); + } + + /** + * Given an SObject Type 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 SObjectType The SObjectType to get the describe for + * @return DescribeSObjectResult The passed SObject's describe + */ + private static DescribeSObjectResult getSobjectDescribeResult( SobjectType type ) + { + Contract.requires( type != null, 'getSobjectDescribeResult called with a null type' ); + + return type.getDescribe(); } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls b/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls index ad18c22b50c..997bdda706b 100644 --- a/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls +++ b/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls @@ -12,18 +12,19 @@ private without sharing class ContractTest private static void requires_whenGivenAFalseCondition_willThrowAnException() // NOPMD: Test method name format { Test.startTest(); - String exceptionMessage; + ortoo_Exception exceptionThrown; try { Contract.requires( false, 'will throw' ); } - catch ( Exception e ) + catch ( ortoo_Exception e ) { - exceptionMessage = e.getMessage(); + exceptionThrown = e; } Test.stopTest(); - Amoss_Asserts.assertContains( 'Contract.requires failed: will throw', exceptionMessage, 'requires, when given a false condition, will throw an exception' ); + Amoss_Asserts.assertContains( 'Contract.requires failed: will throw', exceptionThrown.getMessage(), 'requires, when given a false condition, will throw an exception' ); + System.assertEquals( 'requires_whenGivenAFalseCondition_willThrowAnException', exceptionThrown.getStackTrace().getInnermostMethodName(), 'requires, when given a false condition, will throw an exception with the stack trace set to the caller' ); } @isTest @@ -37,18 +38,19 @@ private without sharing class ContractTest private static void ensures_whenGivenAFalseCondition_willThrowAnException() // NOPMD: Test method name format { Test.startTest(); - String exceptionMessage; + ortoo_Exception exceptionThrown; try { Contract.ensures( false, 'will throw' ); } - catch ( Exception e ) + catch ( ortoo_Exception e ) { - exceptionMessage = e.getMessage(); + exceptionThrown = e; } Test.stopTest(); - Amoss_Asserts.assertContains( 'Contract.ensures failed: will throw', exceptionMessage, 'ensures, when given a false condition, will throw an exception' ); + Amoss_Asserts.assertContains( 'Contract.ensures failed: will throw', exceptionThrown.getMessage(), 'ensures, when given a false condition, will throw an exception' ); + System.assertEquals( 'ensures_whenGivenAFalseCondition_willThrowAnException', exceptionThrown.getStackTrace().getInnermostMethodName(), 'ensures, when given a false condition, will throw an exception with the stack trace set to the caller' ); } @isTest @@ -62,17 +64,18 @@ private without sharing class ContractTest private static void assert_whenGivenAFalseCondition_willThrowAnException() // NOPMD: Test method name format { Test.startTest(); - String exceptionMessage; + ortoo_Exception exceptionThrown; try { Contract.assert( false, 'will throw' ); } - catch ( Exception e ) + catch ( ortoo_Exception e ) { - exceptionMessage = e.getMessage(); + exceptionThrown = e; } Test.stopTest(); - Amoss_Asserts.assertContains( 'Contract.assert failed: will throw', exceptionMessage, 'assert, when given a false condition, will throw an exception' ); + Amoss_Asserts.assertContains( 'Contract.assert failed: will throw', exceptionThrown.getMessage(), 'assert, when given a false condition, will throw an exception' ); + System.assertEquals( 'ensures_whenGivenAFalseCondition_willThrowAnException', exceptionThrown.getStackTrace().getInnermostMethodName(), 'assert, when given a false condition, will throw an exception with the stack trace set to the caller' ); } } \ No newline at end of file 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 cafe13595ac..d3d958118a7 100644 --- a/framework/default/ortoo-core/default/classes/utils/tests/SobjectUtilsTest.cls +++ b/framework/default/ortoo-core/default/classes/utils/tests/SobjectUtilsTest.cls @@ -150,11 +150,13 @@ private without sharing class SobjectUtilsTest @isTest private static void isCreateable_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format { + SObject nullRecord = null; + Test.startTest(); String exceptionMessage; try { - SobjectUtils.isCreateable( null ); + SobjectUtils.isCreateable( nullRecord ); } catch ( Contract.RequiresException e ) { @@ -179,11 +181,13 @@ private without sharing class SobjectUtilsTest @isTest private static void isUpdateable_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format { + SObject nullRecord = null; + Test.startTest(); String exceptionMessage; try { - SobjectUtils.isUpdateable( null ); + SobjectUtils.isUpdateable( nullRecord ); } catch ( Contract.RequiresException e ) { @@ -208,11 +212,13 @@ private without sharing class SobjectUtilsTest @isTest private static void isDeletable_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format { + SObject nullRecord = null; + Test.startTest(); String exceptionMessage; try { - SobjectUtils.isDeletable( null ); + SobjectUtils.isDeletable( nullRecord ); } catch ( Contract.RequiresException e ) { @@ -222,4 +228,120 @@ private without sharing class SobjectUtilsTest Amoss_Asserts.assertContains( 'isDeletable called with a null record', exceptionMessage, 'isDeletable, when given a null record, will throw an exception' ); } + + @isTest + private static void isCreateable_sobjectType_whenCalled_willReturnIsCreatableOfThatSobjectType() // NOPMD: Test method name format + { + Boolean expectedIsCreateable = Contact.sObjectType.getDescribe().isCreateable(); + Boolean actualIsCreateable = SobjectUtils.isCreateable( Contact.sObjectType ); + + System.assertEquals( expectedIsCreateable, actualIsCreateable, 'isCreatable, when called with an SObjectType, will return if that SObject Type is createable by the current user' ); + } + + @isTest + private static void isCreateable_sobjectType_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format + { + SobjectType nullType = null; + + Test.startTest(); + String exceptionMessage; + try + { + SobjectUtils.isCreateable( nullType ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'isCreateable called with a null type', exceptionMessage, 'isCreateable, when given a null SObjectType, will throw an exception' ); + } + + @isTest + private static void isUpdateable_sobjectType_whenCalled_willReturnIsUpdateableOfThatSobject() // NOPMD: Test method name format + { + Boolean expectedIsUpdateable = Contact.sObjectType.getDescribe().isUpdateable(); + Boolean actualIsUpdateable = SobjectUtils.isUpdateable( Contact.sObjectType ); + + System.assertEquals( expectedIsUpdateable, actualIsUpdateable, 'isUpdateable, when called with an SObjectType, will return if that SObject Type is updateable by the current user' ); + } + + @isTest + private static void isUpdateable_sobjectType_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format + { + SobjectType nullType = null; + + Test.startTest(); + String exceptionMessage; + try + { + SobjectUtils.isUpdateable( nullType ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'isUpdateable called with a null type', exceptionMessage, 'isUpdateable, when given a null SObjectType, will throw an exception' ); + } + + @isTest + private static void isDeletable_sobjectType_whenCalled_willReturnIsUpdateableOfThatSobject() // NOPMD: Test method name format + { + Boolean expectedIsDeletable = Contact.sObjectType.getDescribe().isDeletable(); + Boolean actualIsDeletable = SobjectUtils.isDeletable( Contact.sobjectType ); + + System.assertEquals( expectedIsDeletable, actualIsDeletable, 'isDeletable, when called with an SObjectType, will return if that SObject Type is deletable by the current user' ); + } + + @isTest + private static void isDeletable_sobjectType_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format + { + SobjectType nullType = null; + + Test.startTest(); + String exceptionMessage; + try + { + SobjectUtils.isDeletable( nullType ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'isDeletable called with a null type', exceptionMessage, 'isDeletable, when given a null SObjectType, will throw an exception' ); + } + + @isTest + private static void isAccessible_sobjectType_whenCalled_willReturnIsUpdateableOfThatSobject() // NOPMD: Test method name format + { + Boolean expectedIsAccessible = Contact.sObjectType.getDescribe().isAccessible(); + Boolean actualIsAccessible = SobjectUtils.isAccessible( Contact.sobjectType ); + + System.assertEquals( expectedIsAccessible, actualIsAccessible, 'isAccessible, when called with an SObjectType, will return if that SObject Type is accessible by the current user' ); + } + + @isTest + private static void isAccessible_sobjectType_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format + { + SobjectType nullType = null; + + Test.startTest(); + String exceptionMessage; + try + { + SobjectUtils.isAccessible( nullType ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'isAccessible called with a null type', exceptionMessage, 'isAccessible, when given a null SObjectType, will throw an exception' ); + } } \ No newline at end of file