From 4ce9495d4b79bd617157a80b1f3452f45d86e9bd Mon Sep 17 00:00:00 2001 From: Robert Baillie Date: Fri, 21 Jan 2022 12:45:58 +0000 Subject: [PATCH 1/3] Added framework changes from the example-app implementation of the filter and paging screen --- .../classes/criteria/fflib_Criteria.cls | 70 ++-- .../classes/ApplicationMockRegistrar.cls | 4 +- .../default/classes/FrameworkErrorCodes.cls | 3 + .../fflib-extension/ortoo_Criteria.cls | 15 +- .../fflib-extension/ortoo_SobjectSelector.cls | 51 ++- .../classes/search/ISearchConfiguration.cls | 16 + .../search/ISearchConfiguration.cls-meta.xml | 5 + .../classes/search/ISearchCriteria.cls | 7 + .../search/ISearchCriteria.cls-meta.xml | 5 + .../classes/search/ISearchCriteriaFactory.cls | 9 + .../ISearchCriteriaFactory.cls-meta.xml | 5 + .../default/classes/search/ISearchResult.cls | 6 + .../classes/search/ISearchResult.cls-meta.xml | 5 + .../classes/search/ISearchResultBuilder.cls | 8 + .../search/ISearchResultBuilder.cls-meta.xml | 5 + .../classes/search/ISearchSelector.cls | 12 + .../search/ISearchSelector.cls-meta.xml | 5 + .../classes/search/SearchConfiguration.cls | 89 +++++ .../search/SearchConfiguration.cls-meta.xml | 5 + .../classes/search/SearchController.cls | 69 ++++ .../search/SearchController.cls-meta.xml | 5 + .../default/classes/search/SearchOrderBy.cls | 60 +++ .../classes/search/SearchOrderBy.cls-meta.xml | 5 + .../default/classes/search/SearchResults.cls | 32 ++ .../classes/search/SearchResults.cls-meta.xml | 5 + .../default/classes/search/SearchWindow.cls | 53 +++ .../classes/search/SearchWindow.cls-meta.xml | 5 + .../search/tests/SearchConfigurationTest.cls | 262 +++++++++++++ .../SearchConfigurationTest.cls-meta.xml | 5 + .../search/tests/SearchControllerTest.cls | 237 ++++++++++++ .../tests/SearchControllerTest.cls-meta.xml | 5 + .../search/tests/SearchOrderByTest.cls | 294 ++++++++++++++ .../tests/SearchOrderByTest.cls-meta.xml | 5 + .../search/tests/SearchResultsTest.cls | 103 +++++ .../tests/SearchResultsTest.cls-meta.xml | 5 + .../classes/search/tests/SearchWindowTest.cls | 360 ++++++++++++++++++ .../tests/SearchWindowTest.cls-meta.xml | 5 + .../search-service/ISearchService.cls | 5 + .../ISearchService.cls-meta.xml | 5 + .../services/search-service/SearchService.cls | 17 + .../search-service/SearchService.cls-meta.xml | 5 + .../search-service/SearchServiceImpl.cls | 55 +++ .../SearchServiceImpl.cls-meta.xml | 5 + .../tests/SearchServiceImplTest.cls | 182 +++++++++ .../tests/SearchServiceImplTest.cls-meta.xml | 5 + .../tests/SearchServiceMockDomain.cls | 29 ++ .../SearchServiceMockDomain.cls-meta.xml | 5 + .../tests/SearchServiceMockSearchSelector.cls | 24 ++ ...archServiceMockSearchSelector.cls-meta.xml | 5 + .../default/classes/utils/Contract.cls | 6 +- .../default/classes/utils/StringUtils.cls | 17 + .../classes/utils/tests/StringUtilsTest.cls | 15 + ...n_Configuration.Search_Service.md-meta.xml | 17 + .../ortoo-core-CustomLabels.labels-meta.xml | 11 +- .../__tests__/datatableHelper.test.js | 232 +++++++++++ .../lwc/datatableHelper/datatableHelper.js | 59 +++ .../datatableHelper.js-meta.xml | 5 + .../__tests__/errorRenderer.test.js | 8 + .../lwc/errorRenderer/errorRenderer.js | 11 +- .../filterAndResults/filterAndResults.html | 1 - .../__tests__/paginationControls.test.js | 175 ++++----- .../paginationControls/paginationControls.css | 1 + .../paginationControls.html | 2 +- .../paginationControls/paginationControls.js | 3 +- 64 files changed, 2599 insertions(+), 146 deletions(-) create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchResult.cls create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchResult.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchSelector.cls create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchSelector.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls create mode 100644 framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/SearchController.cls create mode 100644 framework/default/ortoo-core/default/classes/search/SearchController.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls create mode 100644 framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/SearchResults.cls create mode 100644 framework/default/ortoo-core/default/classes/search/SearchResults.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/SearchWindow.cls create mode 100644 framework/default/ortoo-core/default/classes/search/SearchWindow.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchConfigurationTest.cls create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchConfigurationTest.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchControllerTest.cls create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchControllerTest.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchResultsTest.cls create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchResultsTest.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchWindowTest.cls create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchWindowTest.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/ISearchService.cls create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/ISearchService.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/SearchService.cls create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/SearchService.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/SearchServiceImpl.cls create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/SearchServiceImpl.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceImplTest.cls create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceImplTest.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockDomain.cls create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockDomain.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockSearchSelector.cls create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockSearchSelector.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/customMetadata/Application_Configuration.Search_Service.md-meta.xml create mode 100644 framework/default/ortoo-core/default/lwc/datatableHelper/__tests__/datatableHelper.test.js create mode 100644 framework/default/ortoo-core/default/lwc/datatableHelper/datatableHelper.js create mode 100644 framework/default/ortoo-core/default/lwc/datatableHelper/datatableHelper.js-meta.xml 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 fcd07f0e31d..209357e72e0 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,6 +54,12 @@ public virtual with sharing class fflib_Criteria this.type = 'AND'; } + // TODO: document and test + protected void addEvaluator( Evaluator evaluator ) + { + evaluators.add( evaluator ); + } + /** * Changes the default comparator for each criteria to OR * @@ -295,7 +301,7 @@ public virtual with sharing class fflib_Criteria } } - private class FormulaNumberEvaluator implements IFormulaEvaluator + public class FormulaNumberEvaluator implements IFormulaEvaluator { private Integer expressionNumber; @@ -737,7 +743,7 @@ public virtual with sharing class fflib_Criteria else if (operator == fflib_Operator.GREATER_THAN_OR_EQUAL_TO) return '>='; else if (operator == fflib_Operator.LIKEx) - return ' like '; + return ' LIKE '; else if (operator == fflib_Operator.INx) return ' IN '; else if (operator == fflib_Operator.NOT_IN) @@ -817,7 +823,7 @@ public virtual with sharing class fflib_Criteria return result + ')'; } - private interface Evaluator + public interface Evaluator { Boolean evaluate(Object obj); String toSOQL(); @@ -826,7 +832,7 @@ public virtual with sharing class fflib_Criteria /** * Generic criteria handler for comparing against sets */ - private virtual class FieldSetEvaluator implements Evaluator + public virtual class FieldSetEvaluator implements Evaluator { private Schema.SObjectField sObjectField; private fflib_Objects values; @@ -851,14 +857,14 @@ public virtual with sharing class fflib_Criteria public String toSOQL() { - return String.format( - '{0} {1} {2}', - new List - { - this.sObjectField.getDescribe().getName(), - operatorToString(this.operator), - toLiteral(this.values) - } + return String.join( + new List + { + this.sObjectField.getDescribe().getName(), + operatorToString(this.operator), + toLiteral(this.values) + }, + '' ); } } @@ -866,7 +872,7 @@ public virtual with sharing class fflib_Criteria /** * Generic field Evaluator */ - private class FieldEvaluator implements Evaluator + public class FieldEvaluator implements Evaluator { private Schema.SObjectField sObjectField; private Object value; @@ -903,7 +909,7 @@ public virtual with sharing class fflib_Criteria } } - private abstract class AbstractRelatedFieldEvaluator implements Evaluator + public abstract class AbstractRelatedFieldEvaluator implements Evaluator { protected String fieldName; protected fflib_Operator operator; @@ -926,7 +932,7 @@ public virtual with sharing class fflib_Criteria } - private class RelatedFieldEvaluator extends AbstractRelatedFieldEvaluator + public class RelatedFieldEvaluator extends AbstractRelatedFieldEvaluator { private Object value; @@ -950,18 +956,18 @@ public virtual with sharing class fflib_Criteria public String toSOQL() { return String.join( - new List - { - this.fieldName, - operatorToString(this.operator), - toLiteral(this.value) - }, - '' + new List + { + this.fieldName, + operatorToString(this.operator), + toLiteral(this.value) + }, + '' ); } } - private class RelatedFieldSetEvaluator extends AbstractRelatedFieldEvaluator + public class RelatedFieldSetEvaluator extends AbstractRelatedFieldEvaluator { private fflib_Objects values; @@ -984,19 +990,19 @@ public virtual with sharing class fflib_Criteria public String toSOQL() { - return String.format( - '{0} {1} {2}', - new List - { - this.fieldName, - operatorToString(this.operator), - toLiteral(this.values) - } + return String.join( + new List + { + this.fieldName, + operatorToString(this.operator), + toLiteral(this.values) + }, + '' ); } } - private class PropertyEvaluator implements Evaluator + public class PropertyEvaluator implements Evaluator { private Object property; private Object value; diff --git a/framework/default/ortoo-core/default/classes/ApplicationMockRegistrar.cls b/framework/default/ortoo-core/default/classes/ApplicationMockRegistrar.cls index 68456611c12..8e1bf17c6d7 100644 --- a/framework/default/ortoo-core/default/classes/ApplicationMockRegistrar.cls +++ b/framework/default/ortoo-core/default/classes/ApplicationMockRegistrar.cls @@ -60,7 +60,7 @@ public inherited sharing class ApplicationMockRegistrar * @param Type The domain type (class) that the mock should be generated as * @return Amoss_Instance The controller for the mock Domain */ - private static Amoss_Instance registerMockDomain( SobjectType sobjectType, Type domainType ) + public static Amoss_Instance registerMockDomain( SobjectType sobjectType, Type domainType ) { Amoss_Instance mockDomainController = new Amoss_Instance( domainType ); mockDomainController @@ -187,7 +187,7 @@ public inherited sharing class ApplicationMockRegistrar * @param Type The selector type (class) that the mock should be generated as * @return Amoss_Instance The controller for the mock selector */ - private static Amoss_Instance registerMockSelector( SobjectType sobjectType, Type selectorType ) + public static Amoss_Instance registerMockSelector( SobjectType sobjectType, Type selectorType ) { Amoss_Instance mockSelectorController = new Amoss_Instance( selectorType ); mockSelectorController diff --git a/framework/default/ortoo-core/default/classes/FrameworkErrorCodes.cls b/framework/default/ortoo-core/default/classes/FrameworkErrorCodes.cls index 70fc8349e9e..101110e471f 100644 --- a/framework/default/ortoo-core/default/classes/FrameworkErrorCodes.cls +++ b/framework/default/ortoo-core/default/classes/FrameworkErrorCodes.cls @@ -27,4 +27,7 @@ public inherited sharing class FrameworkErrorCodes { 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'; + + public final static String SELECTOR_UNBOUND_COUNT_QUERY = '0000000'; + } \ No newline at end of file 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 c46a116c452..d32ce4860a7 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 @@ -3,6 +3,19 @@ * * @group fflib Extension */ -public inherited sharing virtual class ortoo_Criteria extends fflib_Criteria // NOPMD: specified a mini-namespace to differentiate from fflib versions +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 ) ); + return this; + } + + public virtual ortoo_Criteria likeString( String relatedField, Object value ) + { + addEvaluator( new RelatedFieldEvaluator( relatedField, fflib_Operator.LIKEx, value ) ); + return this; + } + } \ No newline at end of file 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 d482ddca9d5..986d8063591 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 @@ -5,8 +5,10 @@ * * @group fflib Extension */ -public abstract inherited sharing class ortoo_SobjectSelector extends fflib_SobjectSelector // NOPMD: specified a mini-namespace to differentiate from fflib versions +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(); @@ -23,4 +25,51 @@ public abstract inherited sharing class ortoo_SobjectSelector extends fflib_Sobj 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' ); + } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls b/framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls new file mode 100644 index 00000000000..d165c78f20b --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls @@ -0,0 +1,16 @@ +/** + * Interface that defines classes that describe the configuration of searches. + * + * This includes the ability to define: + * * The fields that should be included in the result set + * * Which fields are sortable + * * How to map from the result set fields to internal SObject fields + * * Which Base SObject is used to derive a record in the result set + */ +public interface ISearchConfiguration +{ + List getRequiredFields(); + List getSortableFields(); + String getMappedSobjectField( String resultField ); + SobjectType getBaseSobjectType(); +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls b/framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls new file mode 100644 index 00000000000..6978dd4d88a --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls @@ -0,0 +1,7 @@ +/** + * Interface that defines the ability to provide a WHERE clause for a search to be performed with. + */ +public interface ISearchCriteria +{ + String toSOQL(); +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls b/framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls new file mode 100644 index 00000000000..5c0e69e2c9f --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls @@ -0,0 +1,9 @@ +/** + * Interface that defines the ability to create an instance of ISearchCriteria + * based on a Map of properties + */ +public interface ISearchCriteriaFactory +{ + ISearchCriteriaFactory setProperties( Map properties ); + ISearchCriteria build(); +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/ISearchResult.cls b/framework/default/ortoo-core/default/classes/search/ISearchResult.cls new file mode 100644 index 00000000000..9cf12cc90c5 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/ISearchResult.cls @@ -0,0 +1,6 @@ +/** + * Interface that defines the ability to represent search results + */ +public interface ISearchResult +{ +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/ISearchResult.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/ISearchResult.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/ISearchResult.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls b/framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls new file mode 100644 index 00000000000..3e0978ec32a --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls @@ -0,0 +1,8 @@ +/** + * Interface that defines the ability to build a list of search result objects + * based on the current state of that instance (e.g. by a Domain object) + */ +public interface ISearchResultBuilder +{ + List buildSearchResults( ISearchConfiguration searchConfiguration ); +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/ISearchSelector.cls b/framework/default/ortoo-core/default/classes/search/ISearchSelector.cls new file mode 100644 index 00000000000..9cdb5cb1872 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/ISearchSelector.cls @@ -0,0 +1,12 @@ +/** + * Interface that defines the ability to retrieve a list of Search Results with a total number of records + * based on: + * A Search Configuration that defines the minimum fields to return + * Search Criteria that define filters for the records to be returned + * Search Window defining which subset of the matching records should be returned + * Search Order By defining the order of the records prior to the result set being trimmed to the required window + */ +public interface ISearchSelector +{ + SearchResults selectBySearchCriteria( ISearchConfiguration searchConfiguration, ISearchCriteria criteria, SearchWindow window, SearchOrderBy orderBy ); +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/ISearchSelector.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/ISearchSelector.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/ISearchSelector.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls b/framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls new file mode 100644 index 00000000000..af46cb1943b --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls @@ -0,0 +1,89 @@ +/** + * Parent class for the definition of a Search, being a mechanism for calling 'selectBySearchCriteria' against a selector. + * This allows for a windowed and ordered result set to be returned based on an arbitrary set of search criteria. + */ +public abstract inherited sharing class SearchConfiguration implements ISearchConfiguration +{ + protected Map fieldMapping = new Map(); + private SobjectType baseSobjectType; + + /** + * Constructor, defining the Base SObject type that each resulting record represents + * + * @param SobjectType The SObject Type that each record represents + */ + public SearchConfiguration( SobjectType baseSobjectType ) + { + Contract.requires( baseSobjectType != null, 'constructor called with a null baseSobjectType' ); + + this.baseSobjectType = baseSobjectType; + } + + /** + * Returns a list of the result object's fields that are regarded as 'sortable' in the result set. + * By default, the implementation states that all mapped fields are sortable. + * This can be overridden at the concrete class level. + * + * @return List The result object's fields that can be sorted + */ + public virtual List getSortableFields() + { + Contract.assert( fieldMapping != null, 'getSortableFields when fieldMapping was null' ); + + return new List( fieldMapping.keySet() ); + } + + /** + * Returns a list of the fields that are required on the SObject in order for the results to + * be rendered correctly. + * + * @return List The source SObject's fields that are needed to build the results + */ + public List getRequiredFields() + { + Contract.assert( fieldMapping != null, 'getRequiredFields when fieldMapping was null' ); + + return fieldMapping.values(); + } + + /** + * Given a field on the result object, returns the name of the SObject field on the base object + * that the it represents + * + * @param String The name of the field on the result object + * @return String The name of the field on the SObject + */ + public String getMappedSobjectField( String resultField ) + { + Contract.requires( String.isNotBlank( resultField ), 'getMappedSobjectField called with a blank resultField' ); + Contract.assert( fieldMapping != null, 'getMappedSobjectField when fieldMapping was null' ); + + return fieldMapping.get( resultField ); + } + + /** + * The SObject Type that is the basis of the result objects for this search type. + * + * @return SObjectType The SObject Type that is the basis of the result records + */ + public SObjectType getBaseSobjectType() + { + return this.baseSobjectType; + } + + /** + * Add a mapping between a field on the result object and the SObject. + * + * @return SearchConfiguration Itself, allowing for a fluent implementation + */ + @testVisible + protected SearchConfiguration addFieldMapping( String resultField, String sobjectField ) + { + Contract.requires( String.isNotBlank( resultField ), 'addFieldMapping called with a blank resultField' ); + Contract.requires( String.isNotBlank( sobjectField ), 'addFieldMapping called with a blank sobjectField' ); + Contract.assert( fieldMapping != null, 'addFieldMapping when fieldMapping was null' ); + + this.fieldMapping.put( resultField, sobjectField ); + return this; + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/SearchController.cls b/framework/default/ortoo-core/default/classes/search/SearchController.cls new file mode 100644 index 00000000000..63e834293e6 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/SearchController.cls @@ -0,0 +1,69 @@ +/** + * A base controller that can be used for a given implementation for a given LWC (for example) + * + * A consumer implementation should instantiate an instance passing a 'searchConfigurationType' that defines the search's behaviour. + * + * In addition, before calling 'search', it should instantiate an instance of a ISearchCriteriaFactory that is used to convert the raw + * criteria into an ISearchCriteria instance. This class is likely to be defined as an inner class. + */ +public inherited sharing class SearchController +{ + private Type searchConfigurationType; + + /** + * Instructs the SearchController to be configured via the given searchConfigurationType. + * + * @param Type The type of the ISearchConfigration implementation that this search is configured by + * @return SearchController Itself, allowing for a fluent interface + */ + public SearchController setSearchConfigurationType( Type searchConfigurationType ) + { + Contract.requires( searchConfigurationType != null, 'setSearchConfigurationType called with a null searchConfigurationType' ); + + this.searchConfigurationType = searchConfigurationType; + return this; + } + + /** + * Performs a search using the SearchService, convering raw property based parameters into the appropriate objects. + * + * @param ISearchCriteriaFactory The factory that should be used to build the ISearchCriteria instance that will define + * the result set to return. + * @param Map The raw criteria that is used to define the search. This is used by the ISearchCriteriaFactory + * to configure the resulting ISearchCriteria + * @param Map The raw result set's window representation + * @param Map The raw result set's orderBy representation + * @return SearchResults The results of the given search, as returned by the service + */ + public SearchResults search( ISearchCriteriaFactory searchCriteriaFactory, Map criteria, Map window, Map orderBy ) + { + Contract.requires( searchCriteriaFactory != null, 'search called with a null searchCriteriaFactory' ); + Contract.requires( criteria != null, 'search called with a null criteria' ); + Contract.requires( window != null, 'search called with a null window' ); + Contract.requires( orderBy != null, 'search called with a null orderBy' ); + + Contract.assert( searchConfigurationType != null, 'search called when searchConfigurationType was null' ); + + ISearchCriteria criteriaObject = searchCriteriaFactory.setProperties( criteria ).build(); + + SearchWindow windowObject = ((SearchWindow)Application.APP_LOGIC.newInstance( SearchWindow.class ) ) + .configure( window ); + + SearchOrderBy orderByObject = ((SearchOrderBy)Application.APP_LOGIC.newInstance( SearchOrderBy.class ) ) + .configure( orderBy, searchConfigurationType ); + + return SearchService.search( searchConfigurationType, criteriaObject, windowObject, orderByObject ); + } + + /** + * Returns the list of sortable fields in the result set. Does so by asking the SearchService to discover it via the configuration + * + * @return List The list of fields that are searchable + */ + public List getSortableFields() + { + Contract.assert( searchConfigurationType != null, 'getSortableFields called when searchConfigurationType was null' ); + + return SearchService.getSortableFields( searchConfigurationType ); + } +} diff --git a/framework/default/ortoo-core/default/classes/search/SearchController.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/SearchController.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/SearchController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls b/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls new file mode 100644 index 00000000000..3adb699e66d --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls @@ -0,0 +1,60 @@ +/** + * Defines the Order By for a given search request. + * The request is qualified by the given search configuration type. + * + * The fieldName is mapped from the given result field name into the appropriate sobject + * field name, if one is mapped in the configuration. + */ +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 = ''; + + /** + * Configures the order by, defining its properties. + * + * @param Map The properties of the window. Should contain 'fieldName' and 'offset' properties as Strings + * @param Type The type of the SearchConfiguration object that should be used to determine the field mappings + * @return SearchOrderBy Itself, allowing for a fluent interface + */ + public SearchOrderBy configure( Map properties, Type searchConfigurationType ) + { + Contract.requires( properties != null, 'configure called with a null properties' ); + Contract.requires( searchConfigurationType != null, 'configure called with a null searchConfigurationType' ); + + ISearchConfiguration searchConfiguration; + + try + { + searchConfiguration = (ISearchConfiguration)Application.APP_LOGIC.newInstance( searchConfigurationType ); + } catch ( Exception e ) { + Contract.assert( false, 'configure called with a searchConfigurationType ('+searchConfigurationType+') that does not implement ISearchConfiguration or does not have a parameterless constructor' ); + } + + String resultsFieldName = (String)properties.get( 'fieldName' ); + String direction = (String)properties.get( 'direction' ); + + if ( String.isBlank( direction ) ) + { + direction = DIRECTION_ASCENDING; + } + + Contract.assert( direction == DIRECTION_ASCENDING || direction == DIRECTION_DESCENDING, + 'configure called with an invalid direction. Was "'+direction+'", but should be one of ['+DIRECTION_ASCENDING+','+DIRECTION_DESCENDING+']' ); + + if ( String.isNotBlank( resultsFieldName ) ) + { + String mappedFieldName = searchConfiguration.getMappedSobjectField( resultsFieldName ); + + if ( String.isNotBlank( mappedFieldName ) ) + { + this.fieldName = mappedFieldName; + this.direction = direction; + } + } + return this; + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/SearchResults.cls b/framework/default/ortoo-core/default/classes/search/SearchResults.cls new file mode 100644 index 00000000000..915747ba527 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/SearchResults.cls @@ -0,0 +1,32 @@ +/** + * Represents a window of data, being the results of a search. Is defined by the properties: + * + * * totalNumberOfRecords - The total number of records for which these results are a subset. + * Is always at least as large as the list of records + * * records - The subset of the total result record set that are being returned. + * + * The properties are public and AuraEnabled, allowing them to be viewed in Aura Components / LWCs + */ +public inherited sharing class SearchResults +{ + @AuraEnabled public Integer totalNumberOfRecords; + @AuraEnabled public List records; + + /** + * Constructor, defining the dataset + * + * @param Integer The total nmumber of records of which these records are a subset + * @param List The subset of records + */ + public SearchResults( Integer totalNumberOfRecords, List records ) + { + Contract.requires( totalNumberOfRecords != null, 'constructor called with a null totalNumberOfRecords' ); + Contract.requires( totalNumberOfRecords >= 0, 'constructor called with a negative totalNumberOfRecords' ); + Contract.requires( records != null, 'constructor called with a null records' ); + + Contract.requires( totalNumberOfRecords >= records.size(), 'constructor called with a totalNumberOfRecords that is lower than the size of records' ); + + this.totalNumberOfRecords = totalNumberOfRecords; + this.records = records; + } +} diff --git a/framework/default/ortoo-core/default/classes/search/SearchResults.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/SearchResults.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/SearchResults.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/SearchWindow.cls b/framework/default/ortoo-core/default/classes/search/SearchWindow.cls new file mode 100644 index 00000000000..6c4177f1737 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/SearchWindow.cls @@ -0,0 +1,53 @@ +/** + * Represents a definition of a window of data (though not the data itself), defined by the properties: + * * offset - the offset from the start of a result set that window starts from, zero indexed + * * length - the maximum size of the window in number of records that a results set windown contains + * + * In the case of Offset, zero is represented by a null, implying that zero is the same as 'this property is not set.' + */ +public inherited sharing class SearchWindow +{ + public Integer offset; + public Integer length; + + /** + * Configures the current instance with the given properties, defining the window + * + * @param Map The properties of the window. Should contain 'offset' and 'length' properties as Integers + */ + public SearchWindow configure( Map properties ) + { + Contract.requires( properties != null, 'configure called with a null properties' ); + + Integer offset = deriveInteger( properties, 'offset' ); + Contract.requires( offset >= 0 || offset == null, 'configure called with a negative offset' ); + + Integer length = deriveInteger( properties, 'length' ); + Contract.requires( length > 0 || length == null, 'configure called with a negative or zero length' ); + + this.offset = offset > 0 ? offset : null; + this.length = length; + + return this; + } + + private Integer deriveInteger( Map properties, String propertyName ) + { + Object rawValue = properties.get( propertyName ); + + Integer returnValue; + + if ( ! String.isBlank( String.valueOf( rawValue ) ) ) + { + try + { + returnValue = Integer.valueOf( rawValue ); + } + catch( Exception e ) + { + Contract.requires( false, 'configure called with '+propertyName+' that could not be cast into an Integer' ); + } + } + return returnValue; + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/SearchWindow.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/SearchWindow.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/SearchWindow.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/tests/SearchConfigurationTest.cls b/framework/default/ortoo-core/default/classes/search/tests/SearchConfigurationTest.cls new file mode 100644 index 00000000000..865d7fcc2a9 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/tests/SearchConfigurationTest.cls @@ -0,0 +1,262 @@ +@isTest +public inherited sharing class SearchConfigurationTest +{ + @isTest + private static void constructor_whenPassedANullSobjectType_willThrowAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + new TestableSearchConfiguration( null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'constructor called with a null baseSobjectType', exceptionMessage, 'constructor, when passed a null baseSobjectType, will throw an exception' ); + } + + @isTest + private static void getBaseSobjectType_whenCalled_returnsTheBaseType() // NOPMD: Test method name format + { + SearchConfiguration config = new TestableSearchConfiguration( Contact.SobjectType ); + + Test.startTest(); + SobjectType got = config.getBaseSobjectType(); + Test.stopTest(); + + System.assertEquals( Contact.SobjectType, got, 'getBaseSobjectType, when called, will return the base SObject type' ); + } + + @isTest + private static void getSortableFields_whenMappingsAreConfigured_returnsTheResultFieldSideOfMapping() // NOPMD: Test method name format + { + SearchConfiguration config = new TestableSearchConfiguration( Contact.SobjectType ); + + List expected = new List + { + 'resultField1', + 'resultField2', + 'resultField3' + }; + + Test.startTest(); + List got = config.getSortableFields(); + Test.stopTest(); + + System.assertEquals( expected, got, 'getSortableFields, when field mappings are configured, will return the result field side of the mapping' ); + } + + @isTest + private static void getSortableFields_whenNoMappingsAreConfigured_returnsAnEmptyList() // NOPMD: Test method name format + { + SearchConfiguration config = new EmptySearchConfiguration( Contact.SobjectType ); + + List expected = new List(); + + Test.startTest(); + List got = config.getSortableFields(); + Test.stopTest(); + + System.assertEquals( expected, got, 'getSortableFields, when no field mappings are configured, will return an empty list' ); + } + + @isTest + private static void getRequiredFields_whenMappingsAreConfigured_returnsTheSobjectFieldSideOfMapping() // NOPMD: Test method name format + { + SearchConfiguration config = new TestableSearchConfiguration( Contact.SobjectType ); + + List expected = new List + { + 'sobjectField1', + 'sobjectField2', + 'sobjectField3' + }; + + Test.startTest(); + List got = config.getRequiredFields(); + Test.stopTest(); + + System.assertEquals( expected, got, 'getRequiredFields, when field mappings are configured, will return the sobject field side of the mapping' ); + } + + @isTest + private static void getRequiredFields_whenNoMappingsAreConfigured_returnsAnEmptyList() // NOPMD: Test method name format + { + SearchConfiguration config = new EmptySearchConfiguration( Contact.SobjectType ); + + List expected = new List(); + + Test.startTest(); + List got = config.getRequiredFields(); + Test.stopTest(); + + System.assertEquals( expected, got, 'getRequiredFields, when no field mappings are configured, will return an empty list' ); + } + + @isTest + private static void getMappedSobjectField_whenGivenAFieldThatIsMapped_returnsTheSObjectField() // NOPMD: Test method name format + { + SearchConfiguration config = new TestableSearchConfiguration( Contact.SobjectType ); + + Test.startTest(); + String got = config.getMappedSobjectField( 'resultField2' ); + Test.stopTest(); + + System.assertEquals( 'sobjectField2', got, 'getMappedSobjectField, when given a result field that is mapped, will return the SObject field' ); + } + + @isTest + private static void getMappedSobjectField_whenGivenAFieldThatIsNotMapped_returnsNull() // NOPMD: Test method name format + { + SearchConfiguration config = new TestableSearchConfiguration( Contact.SobjectType ); + + Test.startTest(); + String got = config.getMappedSobjectField( 'invalid field' ); + Test.stopTest(); + + System.assertEquals( null, got, 'getMappedSobjectField, when given a result field that is not mapped, will return null' ); + } + + @isTest + private static void getMappedSobjectField_whenPassedANull_throwsAnException() // NOPMD: Test method name format + { + SearchConfiguration config = new TestableSearchConfiguration( Contact.SobjectType ); + + Test.startTest(); + String exceptionMessage; + try + { + config.getMappedSobjectField( null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'getMappedSobjectField called with a blank resultField', exceptionMessage, 'getMappedSobjectField, when passed a null result field, will throw an exception' ); + } + + @isTest + private static void getMappedSobjectField_whenPassedBlank_throwsAnException() // NOPMD: Test method name format + { + SearchConfiguration config = new TestableSearchConfiguration( Contact.SobjectType ); + + Test.startTest(); + String exceptionMessage; + try + { + config.getMappedSobjectField( ' ' ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'getMappedSobjectField called with a blank resultField', exceptionMessage, 'getMappedSobjectField, when passed a null result field, will throw an exception' ); + } + + @isTest + private static void addFieldMapping_whenPassedANullResultField_throwsAnException() // NOPMD: Test method name format + { + TestableSearchConfiguration config = new TestableSearchConfiguration( Contact.SobjectType ); + + Test.startTest(); + String exceptionMessage; + try + { + config.addFieldMapping( null, 'sobjectField' ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'addFieldMapping called with a blank resultField', exceptionMessage, 'addFieldMapping, when passed a null result field, will throw an exception' ); + } + + @isTest + private static void addFieldMapping_whenPassedABlankResultField_throwsAnException() // NOPMD: Test method name format + { + TestableSearchConfiguration config = new TestableSearchConfiguration( Contact.SobjectType ); + + Test.startTest(); + String exceptionMessage; + try + { + config.addFieldMapping( ' ', 'sobjectField' ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'addFieldMapping called with a blank resultField', exceptionMessage, 'addFieldMapping, when passed a blank result field, will throw an exception' ); + } + + @isTest + private static void addFieldMapping_whenPassedANullSobjectField_throwsAnException() // NOPMD: Test method name format + { + TestableSearchConfiguration config = new TestableSearchConfiguration( Contact.SobjectType ); + + Test.startTest(); + String exceptionMessage; + try + { + config.addFieldMapping( 'resultField', null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'addFieldMapping called with a blank sobjectField', exceptionMessage, 'addFieldMapping, when passed a null sobject field, will throw an exception' ); + } + + @isTest + private static void addFieldMapping_whenPassedABlankSobjectField_throwsAnException() // NOPMD: Test method name format + { + TestableSearchConfiguration config = new TestableSearchConfiguration( Contact.SobjectType ); + + Test.startTest(); + String exceptionMessage; + try + { + config.addFieldMapping( 'resultField', ' ' ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'addFieldMapping called with a blank sobjectField', exceptionMessage, 'addFieldMapping, when passed a blank sobject field, will throw an exception' ); + } + + class TestableSearchConfiguration extends SearchConfiguration + { + public TestableSearchConfiguration( SobjectType baseSobjectType ) + { + super( baseSobjectType ); + addFieldMapping( 'resultField1', 'sobjectField1' ); + addFieldMapping( 'resultField2', 'sobjectField2' ); + addFieldMapping( 'resultField3', 'sobjectField3' ); + } + } + + class EmptySearchConfiguration extends SearchConfiguration + { + public EmptySearchConfiguration( SobjectType baseSobjectType ) + { + super( baseSobjectType ); + } + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/tests/SearchConfigurationTest.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/tests/SearchConfigurationTest.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/tests/SearchConfigurationTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/tests/SearchControllerTest.cls b/framework/default/ortoo-core/default/classes/search/tests/SearchControllerTest.cls new file mode 100644 index 00000000000..0b2b77dce88 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/tests/SearchControllerTest.cls @@ -0,0 +1,237 @@ +@isTest +public inherited sharing class SearchControllerTest +{ + private static final Map UNIMPORTANT_CRITERIA = new Map(); + private static final Map UNIMPORTANT_WINDOW = new Map(); + private static final Map UNIMPORTANT_ORDERBY = new Map(); + + @isTest + private static void setSearchConfigurationType_whenPassedANullSearchConfigurationType_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + new SearchController().setSearchConfigurationType( null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'setSearchConfigurationType called with a null searchConfigurationType', exceptionMessage, 'setSearchConfigurationType, when passed a null searchConfigurationType, will throw an exception' ); + } + + @isTest + private static void search_whenCalled_buildObjectsAndAskTheSearchServiceToPerformASearch() // NOPMD: Test method name format + { + Type searchConfigurationType = ISearchConfiguration.class; + + Amoss_Instance searchServiceController = ApplicationMockRegistrar.registerMockService( ISearchService.class ); + Amoss_Instance searchOrderByController = ApplicationMockRegistrar.registerMockAppLogic( SearchOrderBy.class ); + Amoss_Instance searchWindowController = ApplicationMockRegistrar.registerMockAppLogic( SearchWindow.class ); + + Amoss_Instance searchCriteriaFactoryController = new Amoss_Instance( ISearchCriteriaFactory.class ); + ISearchCriteriaFactory searchCriteriaFactory = (ISearchCriteriaFactory)searchCriteriaFactoryController.generateDouble(); + + ISearchCriteria generatedSearchCriteria = (ISearchCriteria)new Amoss_Instance( ISearchCriteria.class ).generateDouble(); + + Map criteria = new Map{ + 'someField' => 'someValue' + }; + + Map window = new Map{ + 'offset' => 10, + 'length' => 20 + }; + + Map orderBy = new Map{ + 'fieldName' => 'someField', + 'direction' => 'asc' + }; + + searchCriteriaFactoryController + .expects( 'setProperties' ) + .withParameter( criteria ) + .returning( searchCriteriaFactory ) + .then() + .expects( 'build' ) + .returning( generatedSearchCriteria ); + + searchWindowController + .expects( 'configure' ) + .withParameter( window ) + .returning( searchWindowController.getDouble() ); + + searchOrderByController + .expects( 'configure' ) + .withParameter( orderBy ) + .thenParameter( searchConfigurationType ) + .returning( searchOrderByController.getDouble() ); + + SearchResults expectedResults = new SearchResults( 10, new List{ 'a result' } ); + + searchServiceController + .expects( 'search' ) + .withParameter( searchConfigurationType ) + .thenParameter( generatedSearchCriteria ) + .thenParameter( searchWindowController.getDouble() ) + .thenParameter( searchOrderByController.getDouble() ) + .returning( expectedResults ); + + SearchController searchController = new SearchController().setSearchConfigurationType( searchConfigurationType ); + + Test.startTest(); + SearchResults gotResults = searchController.search( searchCriteriaFactory, criteria, window, orderBy ); + Test.stopTest(); + + searchCriteriaFactoryController.verify(); + searchServiceController.verify(); + + System.assertEquals( expectedResults, gotResults, 'search, when called with configuration, criteria, window and orderby, will call the SearchService and return the results' ); + } + + @isTest + private static void search_whenPassedANullSearchCriteriaFactory_throwsAnException() // NOPMD: Test method name format + { + SearchController searchController = new SearchController().setSearchConfigurationType( ISearchConfiguration.class ); + + Test.startTest(); + String exceptionMessage; + try + { + searchController.search( null, UNIMPORTANT_CRITERIA, UNIMPORTANT_WINDOW, UNIMPORTANT_ORDERBY ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'search called with a null searchCriteriaFactory', exceptionMessage, 'search, when passed a null searchCriteriaFactory, will throw an exception' ); + } + + @isTest + private static void search_whenPassedANullCriteria_throwsAnException() // NOPMD: Test method name format + { + ISearchCriteriaFactory unimportantFactory = (ISearchCriteriaFactory)new Amoss_Instance( ISearchCriteriaFactory.class ).generateDouble(); + SearchController searchController = new SearchController().setSearchConfigurationType( ISearchConfiguration.class ); + + Test.startTest(); + String exceptionMessage; + try + { + searchController.search( unimportantFactory, null, UNIMPORTANT_WINDOW, UNIMPORTANT_ORDERBY ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'search called with a null criteria', exceptionMessage, 'search, when passed a null criteria, will throw an exception' ); + } + + @isTest + private static void search_whenPassedANullWindow_throwsAnException() // NOPMD: Test method name format + { + ISearchCriteriaFactory unimportantFactory = (ISearchCriteriaFactory)new Amoss_Instance( ISearchCriteriaFactory.class ).generateDouble(); + SearchController searchController = new SearchController().setSearchConfigurationType( ISearchConfiguration.class ); + + Test.startTest(); + String exceptionMessage; + try + { + searchController.search( unimportantFactory, UNIMPORTANT_CRITERIA, null, UNIMPORTANT_ORDERBY ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'search called with a null window', exceptionMessage, 'search, when passed a null window, will throw an exception' ); + } + + @isTest + private static void search_whenPassedANullOrderBy_throwsAnException() // NOPMD: Test method name format + { + ISearchCriteriaFactory unimportantFactory = (ISearchCriteriaFactory)new Amoss_Instance( ISearchCriteriaFactory.class ).generateDouble(); + SearchController searchController = new SearchController().setSearchConfigurationType( ISearchConfiguration.class ); + + Test.startTest(); + String exceptionMessage; + try + { + searchController.search( unimportantFactory, UNIMPORTANT_CRITERIA, UNIMPORTANT_WINDOW, null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'search called with a null orderBy', exceptionMessage, 'search, when passed a null orderBy, will throw an exception' ); + } + + @isTest + private static void search_whenConfigTypeNotSet_throwsAnException() // NOPMD: Test method name format + { + ISearchCriteriaFactory unimportantFactory = (ISearchCriteriaFactory)new Amoss_Instance( ISearchCriteriaFactory.class ).generateDouble(); + + Test.startTest(); + String exceptionMessage; + try + { + new SearchController().search( unimportantFactory, UNIMPORTANT_CRITERIA, UNIMPORTANT_WINDOW, UNIMPORTANT_ORDERBY ); + } + catch ( Contract.AssertException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'search called when searchConfigurationType was null', exceptionMessage, 'search, when searchConfigurationType is not configured, will throw an exception' ); + } + + @isTest + private static void getSortableFields_whenCalled_requestsFromTheSearchService() // NOPMD: Test method name format + { + Amoss_Instance searchServiceController = ApplicationMockRegistrar.registerMockService( ISearchService.class ); + + Type searchConfigurationType = ISearchConfiguration.class; + List expectedSortableFields = new List{ 'field1', 'field2' }; + + searchServiceController + .expects( 'getSortableFields' ) + .withParameter( searchConfigurationType ) + .returning( expectedSortableFields ); + + SearchController searchController = new SearchController().setSearchConfigurationType( ISearchConfiguration.class ); + + Test.startTest(); + List got = searchController.getSortableFields(); + Test.stopTest(); + + System.assertEquals( expectedSortableFields, got, 'getSortableFields, when called, will request the sortable fields from the SearchService and returns them' ); + } + + @isTest + private static void getSortableFields_whenConfigTypeNotSet_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + new SearchController().getSortableFields(); + } + catch ( Contract.AssertException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'getSortableFields called when searchConfigurationType was null', exceptionMessage, 'getSortableFields,when searchConfigurationType is not configured, will throw an exception' ); + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/tests/SearchControllerTest.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/tests/SearchControllerTest.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/tests/SearchControllerTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls b/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls new file mode 100644 index 00000000000..56c351cc4df --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls @@ -0,0 +1,294 @@ +@isTest +public inherited sharing class SearchOrderByTest +{ + @isTest + private static void configure_whenGivenProperties_mapsTheFieldNameAndSetsTheDirection() // NOPMD: Test method name format + { + Map properties = new Map{ + 'fieldName' => 'sourceFieldName', + 'direction' => 'asc' + }; + Type configurationType = ISearchConfiguration.class; + + Amoss_Instance configurationController = ApplicationMockRegistrar.registerMockAppLogic( configurationType ); + configurationController + .expects( 'getMappedSobjectField' ) + .withParameter( 'sourceFieldName' ) + .returning( 'targetFieldName' ); + + Test.startTest(); + SearchOrderBy orderBy = new SearchOrderBy().configure( properties, configurationType ); + Test.stopTest(); + + configurationController.verify(); + + 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' ); + } + + @isTest + private static void configure_whenNoFieldName_setsFieldNameAndDirectionToEmpty() // NOPMD: Test method name format + { + Map properties = new Map{ + 'direction' => 'asc' + }; + Type configurationType = ISearchConfiguration.class; + + Amoss_Instance configurationController = ApplicationMockRegistrar.registerMockAppLogic( configurationType ); + configurationController + .expectsNoCalls(); + + Test.startTest(); + SearchOrderBy orderBy = new SearchOrderBy().configure( properties, configurationType ); + Test.stopTest(); + + 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' ); + } + + @isTest + private static void configure_whenFieldNameEmpty_setsFieldNameAndDirectionToEmpty() // NOPMD: Test method name format + { + Map properties = new Map{ + 'fieldName' => '', + 'direction' => 'asc' + }; + Type configurationType = ISearchConfiguration.class; + + Amoss_Instance configurationController = ApplicationMockRegistrar.registerMockAppLogic( configurationType ); + configurationController + .expectsNoCalls(); + + Test.startTest(); + SearchOrderBy orderBy = new SearchOrderBy().configure( properties, configurationType ); + Test.stopTest(); + + 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' ); + } + + @isTest + private static void configure_whenFieldNameIsUnmapped_setsFieldNameAndDirectionToEmpty() // NOPMD: Test method name format + { + Map properties = new Map{ + 'fieldName' => 'unmapped', + 'direction' => 'asc' + }; + Type configurationType = ISearchConfiguration.class; + + Amoss_Instance configurationController = ApplicationMockRegistrar.registerMockAppLogic( configurationType ); + configurationController + .when( 'getMappedSobjectField' ) + .returns( null ); + + Test.startTest(); + SearchOrderBy orderBy = new SearchOrderBy().configure( properties, configurationType ); + Test.stopTest(); + + 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' ); + } + + @isTest + private static void configure_whenNoDirection_setsDirectionToAsc() // NOPMD: Test method name format + { + Map properties = new Map{ + 'fieldName' => 'source' + }; + Type configurationType = ISearchConfiguration.class; + + Amoss_Instance configurationController = ApplicationMockRegistrar.registerMockAppLogic( configurationType ); + configurationController + .when( 'getMappedSobjectField' ) + .returns( 'target' ); + + Test.startTest(); + SearchOrderBy orderBy = new SearchOrderBy().configure( properties, configurationType ); + Test.stopTest(); + + configurationController.verify(); + + 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' ); + } + + @isTest + private static void configure_whenDirectionAsc_setsDirectionToAsc() // NOPMD: Test method name format + { + Map properties = new Map{ + 'fieldName' => 'source', + 'direction' => 'asc' + }; + Type configurationType = ISearchConfiguration.class; + + Amoss_Instance configurationController = ApplicationMockRegistrar.registerMockAppLogic( configurationType ); + configurationController + .when( 'getMappedSobjectField' ) + .returns( 'target' ); + + Test.startTest(); + SearchOrderBy orderBy = new SearchOrderBy().configure( properties, configurationType ); + Test.stopTest(); + + configurationController.verify(); + + System.assertEquals( 'asc', orderBy.direction, 'configure, when given properties with asc direction, will set the direction to asc' ); + System.assertEquals( 'target', orderBy.fieldName, 'configure, when given properties with asc direction, will set the fieldName' ); + } + + @isTest + private static void configure_whenDirectionDesc_setsDirectionToDesc() // NOPMD: Test method name format + { + Map properties = new Map{ + 'fieldName' => 'source', + 'direction' => 'desc' + }; + Type configurationType = ISearchConfiguration.class; + + Amoss_Instance configurationController = ApplicationMockRegistrar.registerMockAppLogic( configurationType ); + configurationController + .when( 'getMappedSobjectField' ) + .returns( 'target' ); + + Test.startTest(); + SearchOrderBy orderBy = new SearchOrderBy().configure( properties, configurationType ); + Test.stopTest(); + + configurationController.verify(); + + System.assertEquals( 'desc', orderBy.direction, 'configure, when given properties with desc direction, will set the direction to desc' ); + System.assertEquals( 'target', orderBy.fieldName, 'configure, when given properties with desc direction, will set the fieldName' ); + } + + @isTest + private static void configure_whenPassedInvalidDirection_throwsAnException() // NOPMD: Test method name format + { + + Map properties = new Map{ + 'fieldName' => 'source', + 'direction' => 'invalid' + }; + Type configurationType = ISearchConfiguration.class; + + Amoss_Instance configurationController = ApplicationMockRegistrar.registerMockAppLogic( configurationType ); + configurationController + .when( 'getMappedSobjectField' ) + .returns( 'target' ); + + Test.startTest(); + String exceptionMessage; + try + { + new SearchOrderBy().configure( properties, configurationType ); + } + catch ( Contract.AssertException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with an invalid direction. Was "invalid", but should be one of [asc,desc]', exceptionMessage, 'configure, when passed an invalid direction, will throw an exception' ); + } + + @isTest + private static void configure_whenPassedANullProperties_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + new SearchOrderBy().configure( null, ISearchConfiguration.class ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with a null properties', exceptionMessage, 'configure, when passed a null properties, will throw an exception' ); + } + + @isTest + private static void configure_whenPassedANullSearchConfigurationType_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + new SearchOrderBy().configure( new Map(), null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with a null searchConfigurationType', exceptionMessage, 'configure, when passed a null searchConfigurationType, will throw an exception' ); + } + + @isTest + private static void configure_whenPassedAnInvalidSearchConfigurationType_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + new SearchOrderBy().configure( new Map(), SearchOrderByTest.class ); + } + catch ( Contract.AssertException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with a searchConfigurationType ('+SearchOrderByTest.class+') that does not implement ISearchConfiguration or does not have a parameterless constructor', exceptionMessage, 'configure, when passed an invalid searchConfigurationType, will throw an exception' ); + } + + @isTest + private static void configure_whenPassedASearchConfigurationTypeThatCannotBeConstructed_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + new SearchOrderBy().configure( new Map(), SearchOrderByTest.NoParameterlessConstructor.class ); + } + catch ( Contract.AssertException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with a searchConfigurationType ('+SearchOrderByTest.NoParameterlessConstructor.class+') that does not implement ISearchConfiguration or does not have a parameterless constructor', exceptionMessage, 'configure, when passed an invalid searchConfigurationType, will throw an exception' ); + } + + + class NoParameterlessConstructor implements ISearchConfiguration + { + public NoParameterlessConstructor( String parameter ) {} + + public List getRequiredFields() + { + return null; + } + public List getSortableFields() + { + return null; + } + public String getMappedSobjectField( String resultField ) + { + return null; + } + public SobjectType getBaseSobjectType() + { + return null; + } + } + +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/tests/SearchResultsTest.cls b/framework/default/ortoo-core/default/classes/search/tests/SearchResultsTest.cls new file mode 100644 index 00000000000..f574e8d3104 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/tests/SearchResultsTest.cls @@ -0,0 +1,103 @@ +@isTest +public inherited sharing class SearchResultsTest +{ + @isTest + private static void constructor_whenPassedANumberOfRecordsAndSomeRecords_setsTheMemberVariables() // NOPMD: Test method name format + { + Integer totalNumberOfRecords = 10; + List records = new List{ 1, 2, 3 }; + + Test.startTest(); + SearchResults results = new SearchResults( totalNumberOfRecords, records ); + Test.stopTest(); + + System.assertEquals( totalNumberOfRecords, results.totalNumberOfRecords, 'constructor, when passed a number of records and some records, will set the totalNumberOfRecords' ); + System.assertEquals( records, results.records, 'constructor, when passed a number of records and some records, will set the records' ); + } + + @isTest + private static void constructor_whenPassedZeroAndEmpty_setsTheMemberVariables() // NOPMD: Test method name format + { + Integer totalNumberOfRecords = 0; + List records = new List(); + + Test.startTest(); + SearchResults results = new SearchResults( totalNumberOfRecords, records ); + Test.stopTest(); + + System.assertEquals( totalNumberOfRecords, results.totalNumberOfRecords, 'constructor, when passed a zero number of records and no records, will set the totalNumberOfRecords to zero' ); + System.assertEquals( records, results.records, 'constructor, when passed a zero number of records and no records, will set the records to empty' ); + } + + @isTest + private static void constructor_whenPassedANullNumberOfRecords_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + new SearchResults( null, new List() ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'constructor called with a null totalNumberOfRecords', exceptionMessage, 'constructor, when passed a null totalNumberOfRecords, will throw an exception' ); + } + + @isTest + private static void constructor_whenPassedANegativeNumberOfRecords_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + new SearchResults( -1, new List() ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'constructor called with a negative totalNumberOfRecords', exceptionMessage, 'constructor, when passed a negative totalNumberOfRecords, will throw an exception' ); + } + + @isTest + private static void constructor_whenPassedANullRecords_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + new SearchResults( 10, null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'constructor called with a null records', exceptionMessage, 'constructor, when passed a null records, will throw an exception' ); + } + + @isTest + private static void constructor_whenPassedATotalNumberLowerThanPassedRecords_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + new SearchResults( 2, new List{ 1, 2, 3, 4 } ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'constructor called with a totalNumberOfRecords that is lower than the size of records', exceptionMessage, 'constructor, when passed a total number of records that is lower than the number of records, will throw an exception' ); + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/tests/SearchResultsTest.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/tests/SearchResultsTest.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/tests/SearchResultsTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/search/tests/SearchWindowTest.cls b/framework/default/ortoo-core/default/classes/search/tests/SearchWindowTest.cls new file mode 100644 index 00000000000..e5e0a44bebd --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/tests/SearchWindowTest.cls @@ -0,0 +1,360 @@ +@isTest +public inherited sharing class SearchWindowTest +{ + @isTest + private static void configure_whenCalledWithOffsetAndLengthProperties_storesThemAsMemberVariables() // NOPMD: Test method name format + { + Map properties = new Map{ + 'offset' => 10, + 'length' => 20 + }; + + Test.startTest(); + SearchWindow searchWindow = new SearchWindow().configure( properties ); + Test.stopTest(); + + System.assertEquals( 10, searchWindow.offset, 'configure, when called with offset and length properties, will store the offset as a member variable' ); + System.assertEquals( 20, searchWindow.length, 'configure, when called with offset and length properties, will store the length as a member variable' ); + } + + @isTest + private static void configure_whenPassedANullProperties_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + new SearchWindow().configure( null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with a null properties', exceptionMessage, 'configure, when passed a null properties, will throw an exception' ); + } + + @isTest + private static void configure_whenCalledWithOffsetProperty_storesThatAndLeaveLengthNull() // NOPMD: Test method name format + { + Map properties = new Map{ + 'offset' => 10 + }; + + Test.startTest(); + SearchWindow searchWindow = new SearchWindow().configure( properties ); + Test.stopTest(); + + System.assertEquals( 10, searchWindow.offset, 'configure, when called with offset and no length property, will store the offset as a member variable' ); + System.assertEquals( null, searchWindow.length, 'configure, when called with offset and no length property, will set the length to null' ); + } + + @isTest + private static void configure_whenCalledWithDecimalOffsetProperty_roundsItDown() // NOPMD: Test method name format + { + Map properties = new Map{ + 'offset' => 15.1 + }; + + Test.startTest(); + SearchWindow searchWindow = new SearchWindow().configure( properties ); + Test.stopTest(); + + System.assertEquals( 15, searchWindow.offset, 'configure, when called with a decimal offset, will round it and store the offset as a member variable' ); + } + + @isTest + private static void configure_whenCalledWithStringOffsetProperty_castsIt() // NOPMD: Test method name format + { + Map properties = new Map{ + 'offset' => '15' + }; + + Test.startTest(); + SearchWindow searchWindow = new SearchWindow().configure( properties ); + Test.stopTest(); + + System.assertEquals( 15, searchWindow.offset, 'configure, when called with a string offset, will cast it and store the offset as a member variable' ); + } + + @isTest + private static void configure_whenCalledWithEmptyLength_storesThatAsNull() // NOPMD: Test method name format + { + Map properties = new Map{ + 'offset' => 10, + 'length' => '' + }; + + Test.startTest(); + SearchWindow searchWindow = new SearchWindow().configure( properties ); + Test.stopTest(); + + System.assertEquals( 10, searchWindow.offset, 'configure, when called with offset and empty length property, will store the offset as a member variable' ); + System.assertEquals( null, searchWindow.length, 'configure, when called with offset and empty length property, will set the length to null' ); + } + + @isTest + private static void configure_whenCalledWithLengthProperty_storesThatAndLeaveOffsetNull() // NOPMD: Test method name format + { + Map properties = new Map{ + 'length' => 10 + }; + + Test.startTest(); + SearchWindow searchWindow = new SearchWindow().configure( properties ); + Test.stopTest(); + + System.assertEquals( 10, searchWindow.length, 'configure, when called with no offset and a length property, will store the length as a member variable' ); + System.assertEquals( null, searchWindow.offset, 'configure, when called with no offset and a length property, will set the offset to null' ); + } + + @isTest + private static void configure_whenCalledWithDecimalLengthProperty_roundsItDown() // NOPMD: Test method name format + { + Map properties = new Map{ + 'length' => 15.1 + }; + + Test.startTest(); + SearchWindow searchWindow = new SearchWindow().configure( properties ); + Test.stopTest(); + + System.assertEquals( 15, searchWindow.length, 'configure, when called with a decimal length, will round it and store the length as a member variable' ); + } + + @isTest + private static void configure_whenCalledWithStringLengthProperty_castsIt() // NOPMD: Test method name format + { + Map properties = new Map{ + 'length' => '15' + }; + + Test.startTest(); + SearchWindow searchWindow = new SearchWindow().configure( properties ); + Test.stopTest(); + + System.assertEquals( 15, searchWindow.length, 'configure, when called with a string length, will cast it and store the length as a member variable' ); + } + + @isTest + private static void configure_whenGivenANegativeOffset_throwsAnException() // NOPMD: Test method name format + { + Map properties = new Map{ + 'offset' => -10 + }; + + Test.startTest(); + String exceptionMessage; + try + { + SearchWindow searchWindow = new SearchWindow().configure( properties ); + } + catch ( Exception e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with a negative offset', exceptionMessage, 'configure, when given a negative offset, will throw an exception' ); + } + + @isTest + private static void configure_whenGivenANegativeStringOffset_throwsAnException() // NOPMD: Test method name format + { + Map properties = new Map{ + 'offset' => '-10' + }; + + Test.startTest(); + String exceptionMessage; + try + { + SearchWindow searchWindow = new SearchWindow().configure( properties ); + } + catch ( Exception e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with a negative offset', exceptionMessage, 'configure, when given a negative string offset, will throw an exception' ); + } + + @isTest + private static void configure_whenGivenANegativeLength_throwsAnException() // NOPMD: Test method name format + { + Map properties = new Map{ + 'length' => -10 + }; + + Test.startTest(); + String exceptionMessage; + try + { + SearchWindow searchWindow = new SearchWindow().configure( properties ); + } + catch ( Exception e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with a negative or zero length', exceptionMessage, 'configure, when given a negative length, will throw an exception' ); + } + + @isTest + private static void configure_whenGivenANegativeStringLength_throwsAnException() // NOPMD: Test method name format + { + Map properties = new Map{ + 'length' => '-10' + }; + + Test.startTest(); + String exceptionMessage; + try + { + SearchWindow searchWindow = new SearchWindow().configure( properties ); + } + catch ( Exception e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with a negative or zero length', exceptionMessage, 'configure, when given a negative string length, will throw an exception' ); + } + + @isTest + private static void configure_whenGivenAZeroLength_throwsAnException() // NOPMD: Test method name format + { + Map properties = new Map{ + 'length' => 0 + }; + + Test.startTest(); + String exceptionMessage; + try + { + SearchWindow searchWindow = new SearchWindow().configure( properties ); + } + catch ( Exception e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with a negative or zero length', exceptionMessage, 'configure, when given a zero length, will throw an exception' ); + } + + @isTest + private static void configure_whenGivenAZeroStringLength_throwsAnException() // NOPMD: Test method name format + { + Map properties = new Map{ + 'length' => '0' + }; + + Test.startTest(); + String exceptionMessage; + try + { + SearchWindow searchWindow = new SearchWindow().configure( properties ); + } + catch ( Exception e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with a negative or zero length', exceptionMessage, 'configure, when given a zero string length, will throw an exception' ); + } + + @isTest + private static void configure_whenGivenANonNumberOffset_throwsAnException() // NOPMD: Test method name format + { + Map properties = new Map{ + 'offset' => 'notanumber' + }; + + Test.startTest(); + String exceptionMessage; + try + { + SearchWindow searchWindow = new SearchWindow().configure( properties ); + } + catch ( Exception e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with offset that could not be cast into an Integer', exceptionMessage, 'configure, when given a non numeric offset, will throw an exception' ); + } + + @isTest + private static void configure_whenGivenAStringNumberThatCannotBeCastOffset_throwsAnException() // NOPMD: Test method name format + { + Map properties = new Map{ + 'offset' => '15.5' // cannot cast this + }; + + Test.startTest(); + String exceptionMessage; + try + { + SearchWindow searchWindow = new SearchWindow().configure( properties ); + } + catch ( Exception e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with offset that could not be cast into an Integer', exceptionMessage, 'configure, when given a numeric string offset that cannot be cast to an integer, will throw an exception' ); + } + + @isTest + private static void configure_whenGivenANonNumberLength_throwsAnException() // NOPMD: Test method name format + { + Map properties = new Map{ + 'length' => 'notanumber' + }; + + Test.startTest(); + String exceptionMessage; + try + { + SearchWindow searchWindow = new SearchWindow().configure( properties ); + } + catch ( Exception e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with length that could not be cast into an Integer', exceptionMessage, 'configure, when given a non numeric length, will throw an exception' ); + } + + + @isTest + private static void configure_whenGivenAStringNumberThatCannotBeCastLength_throwsAnException() // NOPMD: Test method name format + { + Map properties = new Map{ + 'length' => '15.5' // cannot cast this + }; + + Test.startTest(); + String exceptionMessage; + try + { + SearchWindow searchWindow = new SearchWindow().configure( properties ); + } + catch ( Exception e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'configure called with length that could not be cast into an Integer', exceptionMessage, 'configure, when given a numeric string length that cannot be cast to an integer, will throw an exception' ); + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/search/tests/SearchWindowTest.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/tests/SearchWindowTest.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/search/tests/SearchWindowTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/services/search-service/ISearchService.cls b/framework/default/ortoo-core/default/classes/services/search-service/ISearchService.cls new file mode 100644 index 00000000000..67289fe2850 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/services/search-service/ISearchService.cls @@ -0,0 +1,5 @@ +public interface ISearchService +{ + SearchResults search( Type searchConfigurationType, ISearchCriteria criteria, SearchWindow window, SearchOrderBy orderBy ); + List getSortableFields( Type searchConfigurationType ); +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/services/search-service/ISearchService.cls-meta.xml b/framework/default/ortoo-core/default/classes/services/search-service/ISearchService.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/services/search-service/ISearchService.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/services/search-service/SearchService.cls b/framework/default/ortoo-core/default/classes/services/search-service/SearchService.cls new file mode 100644 index 00000000000..169e0da1d32 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/services/search-service/SearchService.cls @@ -0,0 +1,17 @@ +public inherited sharing class SearchService +{ + public static SearchResults search( Type searchResultType, ISearchCriteria criteria, SearchWindow window, SearchOrderBy orderBy ) + { + return service().search( searchResultType, criteria, window, orderBy ); + } + + public static List getSortableFields( Type searchResultType ) + { + return service().getSortableFields( searchResultType ); + } + + private static ISearchService service() + { + return (ISearchService)Application.SERVICE.newInstance( ISearchService.class ); + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/services/search-service/SearchService.cls-meta.xml b/framework/default/ortoo-core/default/classes/services/search-service/SearchService.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/services/search-service/SearchService.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/services/search-service/SearchServiceImpl.cls b/framework/default/ortoo-core/default/classes/services/search-service/SearchServiceImpl.cls new file mode 100644 index 00000000000..36f7582c982 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/services/search-service/SearchServiceImpl.cls @@ -0,0 +1,55 @@ +/** + * Provides a mechanism for performing a windowed, ordered search against an object as + * defined by the passed searchConfigurationType. + */ +public with sharing class SearchServiceImpl implements ISearchService +{ + /** + * Perform the search as defined in the given configuration type, using the specified criteria. + * A window of results will be returned, as defined in the passed SearchWindow. + * The results will be ordered as per the configured SearchOrderBy + * + * @param Type The type that defines the configuration of this search. + * @param ISearchCriteria The criteria that defines the required records. + * @param SearchWindow The window of the result set that is required (offset + length). + * @param SearchOrderBy The order in which the results should be returned. + * @return SearchResults The result of the search, being a combination of the total number of records + * that match the specified criteria and the requested results that fall in the + * specified window + */ + public SearchResults search( Type searchConfigurationType, ISearchCriteria criteria, SearchWindow window, SearchOrderBy orderBy ) + { + Contract.requires( searchConfigurationType != null, 'search called with a null searchConfigurationType' ); + Contract.requires( criteria != null, 'search called with a null criteria' ); + Contract.requires( window != null, 'search called with a null window' ); + Contract.requires( orderBy != null, 'search called with a null orderBy' ); + + ISearchConfiguration searchConfiguration = buildSearchConfiguration( searchConfigurationType ); + + SobjectType sobjectType = searchConfiguration.getBaseSobjectType(); + + SearchResults searchResults = ((ISearchSelector)Application.SELECTOR.newInstance( sObjectType ) ) + .selectBySearchCriteria( searchConfiguration, criteria, window, orderBy ); + + ISearchResultBuilder searchResultsBuilder = ((ISearchResultBuilder)Application.DOMAIN.newInstance( (List)searchResults.records ) ); + + searchResults.records = searchResultsBuilder.buildSearchResults( searchConfiguration ); + + Contract.ensures( searchResults != null, 'search attempted to return with a null searchResults' ); + Contract.ensures( searchResults.records != null, 'search attempted to return with a null searchResults.records' ); + Contract.ensures( searchResults.totalNumberOfRecords != null, 'search attempted to return with a null searchResults.totalNumberOfRecords' ); + + return searchResults; + } + + public List getSortableFields( Type searchConfigurationType ) + { + Contract.requires( searchConfigurationType != null, 'getSortableFields called with a null searchConfigurationType' ); + return buildSearchConfiguration( searchConfigurationType ).getSortableFields(); + } + + private ISearchConfiguration buildSearchConfiguration( Type searchConfigurationType ) + { + return ((ISearchConfiguration)Application.APP_LOGIC.newInstance( searchConfigurationType ) ); + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/services/search-service/SearchServiceImpl.cls-meta.xml b/framework/default/ortoo-core/default/classes/services/search-service/SearchServiceImpl.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/services/search-service/SearchServiceImpl.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceImplTest.cls b/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceImplTest.cls new file mode 100644 index 00000000000..91a3fab0911 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceImplTest.cls @@ -0,0 +1,182 @@ +@isTest +public inherited sharing class SearchServiceImplTest +{ + @isTest + private static void search_whenCalled_runsASearchAndAddsDataToItReturningTheSearchResults() // NOPMD: Test method name format + { + SobjectType searchSobjectType = Contact.sobjectType; + Type searchConfigurationType = ISearchConfiguration.class; + + // Define all the mocks and register them where appropriate + ISearchCriteria criteria = (ISearchCriteria)new Amoss_Instance( ISearchCriteria.class ).generateDouble(); + SearchWindow window = (SearchWindow)new Amoss_Instance( SearchWindow.class ).generateDouble(); + SearchOrderBy orderBy = (SearchOrderBy)new Amoss_Instance( SearchOrderBy.class ).generateDouble(); + + ISearchResult finalResult = (ISearchResult)new Amoss_Instance( ISearchResult.class ).generateDouble(); + + Amoss_Instance searchConfigController = ApplicationMockRegistrar.registerMockAppLogic( searchConfigurationType ); + Amoss_Instance selectorController = ApplicationMockRegistrar.registerMockSelector( searchSobjectType, SearchServiceMockSearchSelector.class ); // the config will say this is the SobjectType we're searching for + Amoss_Instance domainController = ApplicationMockRegistrar.registerMockDomain( searchSobjectType, SearchServiceMockDomain.class ); + + SearchResults interimSearchResults = new SearchResults( 1, new List{ new Contact( FirstName = 'Interim') } ); + List adjustedObjects = new List{ finalResult }; + + // configure the expected behaviours + searchConfigController + .expects( 'getBaseSobjectType' ) + .returns( searchSobjectType ); + + selectorController + .expects( 'selectBySearchCriteria' ) + .withParameter( searchConfigController.getDouble() ) + .thenParameter( criteria ) + .thenParameter( window ) + .thenParameter( orderBy ) + .returning( interimSearchResults ); + + domainController + .expects( 'buildSearchResults' ) + .withParameter( searchConfigController.getDouble() ) + .returning( adjustedObjects ); + + Test.startTest(); + + SearchResults results = new SearchServiceImpl().search( searchConfigurationType, criteria, window, orderBy ); + + Test.stopTest(); + + searchConfigController.verify(); + selectorController.verify(); + domainController.verify(); + + System.assertEquals( interimSearchResults.totalNumberOfRecords, results.totalNumberOfRecords, 'search, when called with a config, criteria, window and order by, will request a search from the configured selector and keep the total records from there' ); + System.assertEquals( adjustedObjects, results.records, 'search, when called with a config, criteria, window and order by, will construct a domain object based on the cofig, with the results from the selector and then ask it to buildSearchResults from them' ); + } + + @isTest + private static void search_whenPassedANullSearchConfigurationType_throwsAnException() // NOPMD: Test method name format + { + ISearchCriteria criteria = (ISearchCriteria)new Amoss_Instance( ISearchCriteria.class ).generateDouble(); + SearchWindow window = (SearchWindow)new Amoss_Instance( SearchWindow.class ).generateDouble(); + SearchOrderBy orderBy = (SearchOrderBy)new Amoss_Instance( SearchOrderBy.class ).generateDouble(); + + Test.startTest(); + String exceptionMessage; + try + { + new SearchServiceImpl().search( null, criteria, window, orderBy ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'search called with a null searchConfigurationType', exceptionMessage, 'search, when passed a null searchConfigurationType, will throw an exception' ); + } + + @isTest + private static void search_whenPassedANullCriteria_throwsAnException() // NOPMD: Test method name format + { + Type searchConfigurationType = ISearchConfiguration.class; + SearchWindow window = (SearchWindow)new Amoss_Instance( SearchWindow.class ).generateDouble(); + SearchOrderBy orderBy = (SearchOrderBy)new Amoss_Instance( SearchOrderBy.class ).generateDouble(); + + Test.startTest(); + String exceptionMessage; + try + { + new SearchServiceImpl().search( searchConfigurationType, null, window, orderBy ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'search called with a null criteria', exceptionMessage, 'search, when passed a null criteria, will throw an exception' ); + } + + @isTest + private static void search_whenPassedANullWindow_throwsAnException() // NOPMD: Test method name format + { + Type searchConfigurationType = ISearchConfiguration.class; + ISearchCriteria criteria = (ISearchCriteria)new Amoss_Instance( ISearchCriteria.class ).generateDouble(); + SearchOrderBy orderBy = (SearchOrderBy)new Amoss_Instance( SearchOrderBy.class ).generateDouble(); + + Test.startTest(); + String exceptionMessage; + try + { + new SearchServiceImpl().search( searchConfigurationType, criteria, null, orderBy ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'search called with a null window', exceptionMessage, 'search, when passed a null window, will throw an exception' ); + } + + @isTest + private static void search_whenPassedANullOrderBy_throwsAnException() // NOPMD: Test method name format + { + Type searchConfigurationType = ISearchConfiguration.class; + ISearchCriteria criteria = (ISearchCriteria)new Amoss_Instance( ISearchCriteria.class ).generateDouble(); + SearchWindow window = (SearchWindow)new Amoss_Instance( SearchWindow.class ).generateDouble(); + + Test.startTest(); + String exceptionMessage; + try + { + new SearchServiceImpl().search( searchConfigurationType, criteria, window, null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'search called with a null orderBy', exceptionMessage, 'search, when passed a null orderBy, will throw an exception' ); + } + + @isTest + private static void getSortableFields_whenGivenAConfigType_getsTheSortableFieldsFromThatType() // NOPMD: Test method name format + { + Type searchConfigurationType = ISearchConfiguration.class; + List expectedSortableFields = new List{ 'expected', 'fields' }; + + Amoss_Instance searchConfigController = ApplicationMockRegistrar.registerMockAppLogic( searchConfigurationType ); + + searchConfigController + .expects( 'getSortableFields' ) + .returning( expectedSortableFields ); + + Test.startTest(); + + List got = new SearchServiceImpl().getSortableFields( searchConfigurationType ); + + Test.stopTest(); + + System.assertEquals( expectedSortableFields, got, 'getSortableFields, when given a valid config type, will call getSortableFields against that type' ); + } + + @isTest + private static void getSortableFields_whenPassedANullSearchConfigurationType_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + new SearchServiceImpl().getSortableFields( null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'getSortableFields called with a null searchConfigurationType', exceptionMessage, 'getSortableFields, when passed a null searchConfigurationType, will throw an exception' ); + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceImplTest.cls-meta.xml b/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceImplTest.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceImplTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockDomain.cls b/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockDomain.cls new file mode 100644 index 00000000000..f8f39e23051 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockDomain.cls @@ -0,0 +1,29 @@ +/** + * Class exists purely to allow an instance of ISearchResultBuilder to be mocked and registered as a fflib_ISObjectDomain + * in the application framework. Can't think of another way of doing this, but if it's possible without a class + * then that solution is welcomed. + */ +@isTest +public inherited sharing class SearchServiceMockDomain implements fflib_ISObjectDomain, ISearchResultBuilder +{ + public Object getType() + { + return null; + } + public List getObjects() + { + return null; + } + public List buildSearchResults( ISearchConfiguration searchConfiguration ) + { + return null; + } + public Schema.SObjectType sObjectType() + { + return null; + } + public List getRecords() + { + return null; + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockDomain.cls-meta.xml b/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockDomain.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockDomain.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockSearchSelector.cls b/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockSearchSelector.cls new file mode 100644 index 00000000000..36c4c8aeeba --- /dev/null +++ b/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockSearchSelector.cls @@ -0,0 +1,24 @@ +/** + * Class exists purely to allow an instance of ISearchSelector to be mocked and registered as a fflib_SobjectSelector. + * In theory we should be able to mock ortoo_SobjectSelector, since that implements ISearchSelector, but the mock + * generation fails inside the StubProvider as of API 52. + * Can't think of another way of doing this, but if it's possible without a class being defined then that solution is welcomed. + */ +@isTest +public inherited sharing class SearchServiceMockSearchSelector extends fflib_SobjectSelector implements ISearchSelector +{ + public SObjectType getSObjectType() + { + return null; + } + + public List getSObjectFieldList() + { + return null; + } + + public SearchResults selectBySearchCriteria( ISearchConfiguration searchConfiguration, ISearchCriteria criteria, SearchWindow window, SearchOrderBy orderBy ) + { + return null; + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockSearchSelector.cls-meta.xml b/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockSearchSelector.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockSearchSelector.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/utils/Contract.cls b/framework/default/ortoo-core/default/classes/utils/Contract.cls index 58ed45d8d78..e282e5fe39b 100644 --- a/framework/default/ortoo-core/default/classes/utils/Contract.cls +++ b/framework/default/ortoo-core/default/classes/utils/Contract.cls @@ -22,7 +22,7 @@ public class Contract { if ( !condition ) { - throw new RequiresException( 'Contract.requires failed: ' + message ); + throw new RequiresException( 'Contract.requires failed: ' + message ).regenerateStackTraceString( 1 ); } } @@ -38,7 +38,7 @@ public class Contract { if ( !condition ) { - throw new AssertException( 'Contract.assert failed: ' + message ); + throw new AssertException( 'Contract.assert failed: ' + message ).regenerateStackTraceString( 1 ); } } @@ -55,7 +55,7 @@ public class Contract { if ( !condition ) { - throw new EnsuresException( 'Contract.ensures failed: ' + message ); + throw new EnsuresException( 'Contract.ensures failed: ' + message ).regenerateStackTraceString( 1 ); } } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/utils/StringUtils.cls b/framework/default/ortoo-core/default/classes/utils/StringUtils.cls index 570a1de87ee..9647fbe3787 100644 --- a/framework/default/ortoo-core/default/classes/utils/StringUtils.cls +++ b/framework/default/ortoo-core/default/classes/utils/StringUtils.cls @@ -22,6 +22,23 @@ public inherited sharing class StringUtils return returnLabel; } + /** + * Given a string, will return either: + * The passed string, if the input was not null + * An empty string, if the input was null + * + * @param String The string to 'unNull' + * @return String The resulting string + */ + public static String unNull( String input ) + { + if ( input == null ) + { + return ''; + } + return input; + } + /** * Given a String that contains values that are semi-colon delimited, will split it, * trimming each entry and returning a list of strings. diff --git a/framework/default/ortoo-core/default/classes/utils/tests/StringUtilsTest.cls b/framework/default/ortoo-core/default/classes/utils/tests/StringUtilsTest.cls index 435ccb9b182..bfb41ffba1d 100644 --- a/framework/default/ortoo-core/default/classes/utils/tests/StringUtilsTest.cls +++ b/framework/default/ortoo-core/default/classes/utils/tests/StringUtilsTest.cls @@ -57,6 +57,21 @@ private without sharing class StringUtilsTest System.assertEquals( '', formattedString, 'formatLabel, when given a null string, will return an empty string' ); } + @isTest + private static void unNull_whenGivenAStringThatIsNotNull_returnsThatString() // NOPMD: Test method name format + { + String originalString = 'not null string'; + String got = StringUtils.unNull( originalString ); + System.assertEquals( originalString, got, 'unNull, when given a String that is not null, will return that String' ); + } + + @isTest + private static void unNull_whenGivenAStringThatIsNull_returnsAnEmptyString() // NOPMD: Test method name format + { + String got = StringUtils.unNull( null ); + System.assertEquals( '', got, 'unNull, when given a String that is null, will return an empty String' ); + } + @isTest private static void convertDelimitedStringToList_whenGivenAStringAndADelimiter_willSplitTheStringOnTheDelimiter() // NOPMD: Test method name format { diff --git a/framework/default/ortoo-core/default/customMetadata/Application_Configuration.Search_Service.md-meta.xml b/framework/default/ortoo-core/default/customMetadata/Application_Configuration.Search_Service.md-meta.xml new file mode 100644 index 00000000000..76080a61115 --- /dev/null +++ b/framework/default/ortoo-core/default/customMetadata/Application_Configuration.Search_Service.md-meta.xml @@ -0,0 +1,17 @@ + + + + true + + Implementation__c + SearchServiceImpl + + + Object_Type__c + ISearchService + + + Type__c + Service + + 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 3db34357c4a..f4d8c13fd74 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 @@ -145,7 +145,7 @@ en_US false The label to use on the 'First Page' button of the pagination controls. - First Page + First ortoo_core_previous_page @@ -166,7 +166,7 @@ en_US false The label to use on the 'Last Page' button of the pagination controls. - Last Page + Last ortoo_core_total_records @@ -189,4 +189,11 @@ Describes the page no. of a total. {0} - Current Page, {1} - Total Pages. Page {0} of {1} + + ortoo_core_datatable_sorting_disabled_title + en_US + false + Title of the error toast that states that datatable sorting is disabled. + Problem occured when configuring which fields are orderable. Table sorting is disabled. + \ No newline at end of file diff --git a/framework/default/ortoo-core/default/lwc/datatableHelper/__tests__/datatableHelper.test.js b/framework/default/ortoo-core/default/lwc/datatableHelper/__tests__/datatableHelper.test.js new file mode 100644 index 00000000000..fe4541fa9f5 --- /dev/null +++ b/framework/default/ortoo-core/default/lwc/datatableHelper/__tests__/datatableHelper.test.js @@ -0,0 +1,232 @@ +import DatatableHelper from 'c/datatableHelper'; +import displayError from 'c/errorRenderer'; + +jest.mock( 'c/errorRenderer' ); + +jest.mock('@salesforce/label/c.ortoo_core_datatable_sorting_disabled_title', () => { return { default: "Sorting configuration error" } }, { virtual: true } ); + +describe( 'refreshConfiguration', () => { + + beforeEach( () => { + displayError.mockClear(); + }) + + it( 'when executed against an object with the specified field as an array of objects, will clone it (shallow clone the objects in the array)', () => { + + const propertyToClone = [ + { property: 'value0', property1: 'value1', objectProperty: { property: 'subProperty' } }, + { property: 'value2', property1: 'value3' }, + ]; + + const objectToRunAgainst = { + columnsProperty: propertyToClone + }; + + DatatableHelper.refreshConfiguration.call( objectToRunAgainst, 'columnsProperty' ); + + expect( objectToRunAgainst.columnsProperty ).toEqual( propertyToClone ); + expect( objectToRunAgainst.columnsProperty ).not.toBe( propertyToClone ); + + expect( objectToRunAgainst.columnsProperty[0] ).toEqual( propertyToClone[0] ); + expect( objectToRunAgainst.columnsProperty[0] ).not.toBe( propertyToClone[0] ); + + expect( objectToRunAgainst.columnsProperty[1] ).toEqual( propertyToClone[1] ); + expect( objectToRunAgainst.columnsProperty[1] ).not.toBe( propertyToClone[1] ); + + expect( objectToRunAgainst.columnsProperty[0].objectProperty ).toBe( propertyToClone[0].objectProperty ); // at this level we no longer clone + }); + + it( 'when executed against an object with the specified field not an array, will throw', () => { + + const propertyToClone = 'not an array'; + + const objectToRunAgainst = { + columnsProperty: propertyToClone + }; + + const call = () => DatatableHelper.refreshConfiguration.call( objectToRunAgainst, 'columnsProperty' ); + + expect( call ).toThrow( 'refreshConfiguration called with a property (columnsProperty) that is not an Array' ); + }); + + it( 'when executed against an object with the specified field not an array of objects, will throw', () => { + + const propertyToClone = ['not an array of objects']; + + const objectToRunAgainst = { + columnsProperty: propertyToClone + }; + + const call = () => DatatableHelper.refreshConfiguration.call( objectToRunAgainst, 'columnsProperty' ); + + expect( call ).toThrow( 'refreshConfiguration called with a property (columnsProperty) that is not an Array of Objects.' ); + }); +}); + +describe( 'configureSortableFields', () => { + + beforeEach( () => { + displayError.mockClear(); + }) + + it( 'when bound to an object with a columns definition, the name of the right property and list of columns that are sortable, will set the sortable property on the appropriate columns', () => { + + const columns = [ + { fieldName: 'field0' }, + { fieldName: 'field1' }, + { fieldName: 'field2' }, + { fieldName: 'field3' }, + { fieldName: 'field4' }, + ]; + + const objectToRunAgainst = { + columnsProperty: columns + }; + + const sortableColumns = [ 'field0', 'field2', 'field4' ]; + + DatatableHelper.configureSortableFields.call( objectToRunAgainst, 'columnsProperty', sortableColumns, null ); + + expect( objectToRunAgainst.columnsProperty[0].sortable ).toBe( true ); + expect( objectToRunAgainst.columnsProperty[1].sortable ).toBeUndefined(); + expect( objectToRunAgainst.columnsProperty[2].sortable ).toBe( true ); + expect( objectToRunAgainst.columnsProperty[3].sortable ).toBeUndefined(); + expect( objectToRunAgainst.columnsProperty[4].sortable ).toBe( true ); + }); + + it( 'when told a column that does not exist is sortable, will skip it', () => { + + const columns = [ + { fieldName: 'field0' }, + { fieldName: 'field1' }, + { fieldName: 'field2' } + ]; + + const objectToRunAgainst = { + columnsProperty: columns + }; + + const sortableColumns = [ 'field0', 'someOtherField', 'field2' ]; + + DatatableHelper.configureSortableFields.call( objectToRunAgainst, 'columnsProperty', sortableColumns, null ); + + expect( objectToRunAgainst.columnsProperty[0].sortable ).toBe( true ); + expect( objectToRunAgainst.columnsProperty[1].sortable ).toBeUndefined(); + expect( objectToRunAgainst.columnsProperty[2].sortable ).toBe( true ); + }); + + it( 'when given an empty list of columns, will not set any as sortable', () => { + + const columns = [ + { fieldName: 'field0' }, + { fieldName: 'field1' }, + { fieldName: 'field2' } + ]; + + const objectToRunAgainst = { + columnsProperty: columns + }; + + const sortableColumns = []; + + DatatableHelper.configureSortableFields.call( objectToRunAgainst, 'columnsProperty', sortableColumns, null ); + + expect( objectToRunAgainst.columnsProperty[0].sortable ).toBeUndefined(); + expect( objectToRunAgainst.columnsProperty[1].sortable ).toBeUndefined(); + expect( objectToRunAgainst.columnsProperty[2].sortable ).toBeUndefined(); + }); + + it( 'when the column definition includes an object that does not have a fieldName, will skip it', () => { + + const columns = [ + { fieldName: 'field0' }, + { fieldName: 'field1' }, + {}, + { fieldName: 'field2' } + ]; + + const objectToRunAgainst = { + columnsProperty: columns + }; + + const sortableColumns = [ 'field1', 'field2' ]; + + DatatableHelper.configureSortableFields.call( objectToRunAgainst, 'columnsProperty', sortableColumns, null ); + + expect( objectToRunAgainst.columnsProperty[0].sortable ).toBeUndefined(); + expect( objectToRunAgainst.columnsProperty[1].sortable ).toBe( true ); + expect( objectToRunAgainst.columnsProperty[2].sortable ).toBeUndefined(); + expect( objectToRunAgainst.columnsProperty[3].sortable ).toBe( true ); + }); + + it( 'when the column property is empty, will not throw', () => { + + const columns = [ + ]; + + const objectToRunAgainst = { + columnsProperty: columns + }; + + const sortableColumns = [ 'field1', 'field2' ]; + + DatatableHelper.configureSortableFields.call( objectToRunAgainst, 'columnsProperty', sortableColumns, null ); + + expect( objectToRunAgainst.columnsProperty.length ).toBe( 0 ); + }); + + it( 'when called without being bound to an object, will throw', () => { + + const sortableColumns = [ 'field1', 'field2' ]; + + const call = () => DatatableHelper.configureSortableFields( 'columnsProperty', sortableColumns, null ); + + expect( call ).toThrow( 'configureSortableFields called with a property (columnsProperty) that does not exist. Have you bound your instance by using "bind" or "call"?' ); + }); + + it( 'when the passed column property does not exist on the bound object, will throw', () => { + + const objectToRunAgainst = {}; + + const sortableColumns = [ 'field1', 'field2' ]; + + const call = () => DatatableHelper.configureSortableFields.call( objectToRunAgainst, 'columnsProperty', sortableColumns, null ); + + expect( call ).toThrow( 'configureSortableFields called with a property (columnsProperty) that does not exist.' ); + }); + + it( 'when the passed column property is not an array, will throw', () => { + + const columns = 'this is not an array'; + + const objectToRunAgainst = { + columnsProperty: columns + }; + const sortableColumns = [ 'field1', 'field2' ]; + + const call = () => DatatableHelper.configureSortableFields.call( objectToRunAgainst, 'columnsProperty', sortableColumns, null ); + + expect( call ).toThrow( 'configureSortableFields called with a property (columnsProperty) that is not an Array.' ); + }); + + it( 'when the passed column property is not an array of objects, will throw', () => { + + const columns = [ + {}, + 'this is not an object', + {} + ]; + + const objectToRunAgainst = { + columnsProperty: columns + }; + const sortableColumns = [ 'field1', 'field2' ]; + + const call = () => DatatableHelper.configureSortableFields.call( objectToRunAgainst, 'columnsProperty', sortableColumns, null ); + + expect( call ).toThrow( 'configureSortableFields called with a property (columnsProperty) that is not an Array of Objects.' ); + }); + + // given an error, will dispatch it + // throws an exception - e.g. the object is immutable, will dispatch an error +}); diff --git a/framework/default/ortoo-core/default/lwc/datatableHelper/datatableHelper.js b/framework/default/ortoo-core/default/lwc/datatableHelper/datatableHelper.js new file mode 100644 index 00000000000..2610743fb16 --- /dev/null +++ b/framework/default/ortoo-core/default/lwc/datatableHelper/datatableHelper.js @@ -0,0 +1,59 @@ +import displayError from 'c/errorRenderer'; +import ERROR_TITLE from '@salesforce/label/c.ortoo_core_datatable_sorting_disabled_title'; + +const checkColumnProperty = function( functionName, object, columnsPropertyName ) { + + if ( object[columnsPropertyName] == undefined ) { + throw functionName + ' called with a property ('+columnsPropertyName+') that does not exist. Have you bound your instance by using "bind" or "call"?'; + } + + if ( object[columnsPropertyName].forEach == undefined ) { + throw functionName + ' called with a property ('+columnsPropertyName+') that is not an Array.'; + } + + if ( object[columnsPropertyName].find( thisElement => typeof thisElement !== 'object' ) ) { + throw functionName + ' called with a property ('+columnsPropertyName+') that is not an Array of Objects.'; + } +} + + +const refreshConfiguration = function( columnsPropertyName ) { + + checkColumnProperty( 'refreshConfiguration', this, columnsPropertyName ); + + let newConfiguration = []; + this[columnsPropertyName].forEach( thisElement => newConfiguration.push( Object.assign( {}, thisElement ) ) ); + this[columnsPropertyName] = newConfiguration; +} + +const configureSortableFields = function( columnsPropertyName, fields, error ) { + + const errorTitle = ERROR_TITLE; + + if ( error ) { + + displayError.call( this, error, errorTitle ); + + } else if ( fields ) { + + checkColumnProperty( 'configureSortableFields', this, columnsPropertyName ); + + try { + + fields.forEach( + thisSortableField => { + const referencedField = this[ columnsPropertyName ].find( thisColumn => thisColumn.fieldName == thisSortableField ); + referencedField && ( referencedField.sortable = true ); + } + ); + refreshConfiguration.call( this, columnsPropertyName ); + } catch ( e ) { + displayError.call( this, e, errorTitle ); + } + } +} + +export default { + refreshConfiguration : refreshConfiguration, + configureSortableFields : configureSortableFields +}; \ No newline at end of file diff --git a/framework/default/ortoo-core/default/lwc/datatableHelper/datatableHelper.js-meta.xml b/framework/default/ortoo-core/default/lwc/datatableHelper/datatableHelper.js-meta.xml new file mode 100644 index 00000000000..884004a97fa --- /dev/null +++ b/framework/default/ortoo-core/default/lwc/datatableHelper/datatableHelper.js-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + false + \ No newline at end of file diff --git a/framework/default/ortoo-core/default/lwc/errorRenderer/__tests__/errorRenderer.test.js b/framework/default/ortoo-core/default/lwc/errorRenderer/__tests__/errorRenderer.test.js index 3a3ec6b4cb0..b31fa31f479 100644 --- a/framework/default/ortoo-core/default/lwc/errorRenderer/__tests__/errorRenderer.test.js +++ b/framework/default/ortoo-core/default/lwc/errorRenderer/__tests__/errorRenderer.test.js @@ -95,4 +95,12 @@ describe('displayError', () => { expect( reportedError ).toBe( error ); }); + + it( 'When called without binding to an instance, will throw', () => { + + const error = 'An Error'; + const call = () => displayError( error ); + + expect( call ).toThrow( 'displayError called against an object with no "dispatchEvent" function defined. Have you bound your instance by using "bind" or "call"?' ); + }); }); \ No newline at end of file diff --git a/framework/default/ortoo-core/default/lwc/errorRenderer/errorRenderer.js b/framework/default/ortoo-core/default/lwc/errorRenderer/errorRenderer.js index 43ed94e408c..7a5e488b203 100644 --- a/framework/default/ortoo-core/default/lwc/errorRenderer/errorRenderer.js +++ b/framework/default/ortoo-core/default/lwc/errorRenderer/errorRenderer.js @@ -4,9 +4,9 @@ import ERROR_TITLE from '@salesforce/label/c.ortoo_core_error_title'; /** * When bound to a Lightning Web Component, will render the given error object. */ -const displayError = function( error ) { +const displayError = function( error, title ) { - let title = ERROR_TITLE; // should be a label + title = title ? title : ERROR_TITLE; // By default we assume we have a string for the error let message = error; @@ -23,6 +23,11 @@ const displayError = function( error ) { message = error.body.message; } + if ( this?.dispatchEvent == undefined ) { + throw 'displayError called against an object with no "dispatchEvent" function defined. Have you bound your instance by using "bind" or "call"?\n' + + 'Error was: ' + message; + } + const toastEvent = new ShowToastEvent({ title: title, message: message, @@ -31,4 +36,4 @@ const displayError = function( error ) { this.dispatchEvent( toastEvent ); } -export default displayError; \ No newline at end of file +export default displayError; diff --git a/framework/default/ortoo-core/default/lwc/filterAndResults/filterAndResults.html b/framework/default/ortoo-core/default/lwc/filterAndResults/filterAndResults.html index 9df39068462..4076ef9de05 100644 --- a/framework/default/ortoo-core/default/lwc/filterAndResults/filterAndResults.html +++ b/framework/default/ortoo-core/default/lwc/filterAndResults/filterAndResults.html @@ -44,7 +44,6 @@ - { + beforeEach(() => { + const element = createElement('c-pagination-controls', { + is: PaginationControls + }); + + document.body.appendChild( element ); + }) + afterEach(() => { // The jsdom instance is shared across test cases in a single file so reset the DOM while (document.body.firstChild) { @@ -22,11 +30,8 @@ describe( 'c-pagination-controls', () => { }); it( 'when no record properties are set, will leave all buttons as disabled', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); - document.body.appendChild( element ); + const element = document.body.querySelector( 'c-pagination-controls' ); return Promise.resolve() .then( () => { @@ -46,15 +51,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when in a middle page, defined by the currentPage, will enable all the buttons', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.currentPage = 5; - document.body.appendChild( element ); - return Promise.resolve() .then( () => { @@ -73,15 +75,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when the first button is pressed, will set the current page to 1 and issue an event', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.currentPage = 5; - document.body.appendChild( element ); - const navigateHandler = jest.fn(); element.addEventListener( 'navigate', navigateHandler ); @@ -101,15 +100,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when the previous button is pressed, will reduce the current page by 1 and issue an event', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.currentPage = 5; - document.body.appendChild( element ); - const navigateHandler = jest.fn(); element.addEventListener( 'navigate', navigateHandler ); @@ -129,15 +125,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when the next button is pressed, will increase the current page by 1 and issue an event', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.currentPage = 5; - document.body.appendChild( element ); - const navigateHandler = jest.fn(); element.addEventListener( 'navigate', navigateHandler ); @@ -157,15 +150,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when the last button is pressed, will set the current page to the last and issue an event', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.currentPage = 5; - document.body.appendChild( element ); - const navigateHandler = jest.fn(); element.addEventListener( 'navigate', navigateHandler ); @@ -185,15 +175,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when the number of records per page is changed, will issue a navigation event and set the current page so the previous first record is still shown - checking first page', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.currentPage = 1; - document.body.appendChild( element ); - const navigateHandler = jest.fn(); element.addEventListener( 'navigate', navigateHandler ); @@ -216,15 +203,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when the number of records per page is changed, will issue a navigation event and set the current page so the previous first record is still shown - checking last page', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.currentPage = 10; - document.body.appendChild( element ); - const navigateHandler = jest.fn(); element.addEventListener( 'navigate', navigateHandler ); @@ -247,15 +231,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when the number of records per page is changed, will issue a navigation event and set the current page so the previous first record is still shown - checking in the middle', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.currentPage = 7; - document.body.appendChild( element ); - const navigateHandler = jest.fn(); element.addEventListener( 'navigate', navigateHandler ); @@ -278,15 +259,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when on the first page, defined by the currentPage, but more records exist, will enable the next buttons, but not the previous ones', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.currentPage = 1; - document.body.appendChild( element ); - return Promise.resolve() .then( () => { @@ -305,15 +283,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when on the first page, defined by the offset, but more records exist, will enable the next buttons, but not the previous ones', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.offset = 0; - document.body.appendChild( element ); - return Promise.resolve() .then( () => { @@ -332,15 +307,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when on the last page, defined by the currentPage, but more records exist, will enable the previous buttons, but not the next ones', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.currentPage = 10; - document.body.appendChild( element ); - return Promise.resolve() .then( () => { @@ -359,15 +331,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when on the last page, defined by the offset, but more records exist, will enable the previous buttons, but not the next ones', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.offset = 90; - document.body.appendChild( element ); - return Promise.resolve() .then( () => { @@ -386,15 +355,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when in a middle page, defined by the offset, will enable all the buttons', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.offset = 50; - document.body.appendChild( element ); - return Promise.resolve() .then( () => { @@ -413,15 +379,12 @@ describe( 'c-pagination-controls', () => { }); it( 'when the page size is bigger than the number of records, will disable all the buttons', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 10; element.recordsPerPage = 20; element.offset = 0; - document.body.appendChild( element ); - return Promise.resolve() .then( () => { @@ -440,9 +403,8 @@ describe( 'c-pagination-controls', () => { }); it( 'when the current page is set, the offset will be set', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 20; element.currentPage = 3; @@ -451,9 +413,8 @@ describe( 'c-pagination-controls', () => { }); it( 'when the offset is set, the current page will be set', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 20; element.offset = 40; @@ -462,9 +423,8 @@ describe( 'c-pagination-controls', () => { }); it( 'when the offset is set so it is not on a page boundary, it will be rounded down to a page boundary', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 20; element.offset = 45; @@ -474,9 +434,8 @@ describe( 'c-pagination-controls', () => { }); it( 'when the offset is set to a negative number, it will be set to zero', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 20; element.offset = -45; @@ -486,9 +445,8 @@ describe( 'c-pagination-controls', () => { }); it( 'when the currentPage is set to a negative number, it will be set to 1', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 20; element.currentPage = -45; @@ -498,17 +456,14 @@ describe( 'c-pagination-controls', () => { }); it( 'when the current page is set beyond the number of records, it will still render in that position', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.currentPage = 15; // beyond the end expect( element.currentPage ).toBe( 15 ); - document.body.appendChild( element ); - return Promise.resolve() .then( () => { @@ -528,15 +483,11 @@ describe( 'c-pagination-controls', () => { it( 'will show a message containing details on the current page and the label for the page size selector, built up by labels', () => { - const element = createElement('c-pagination-controls', { - is: PaginationControls - }); + const element = document.body.querySelector( 'c-pagination-controls' ); element.numberOfRecords = 100; element.recordsPerPage = 10; element.currentPage = 5; - document.body.appendChild( element ); - return Promise.resolve() .then( () => { const info = element.shadowRoot.querySelector( INFO_SELECTOR ); @@ -544,4 +495,30 @@ describe( 'c-pagination-controls', () => { expect( info.textContent ).toBe( 'Total Records: 100 • 5 of 10 • Page Size' ); }) }); + + it( 'when recordsPerPage is set to an Integer, will set the recordsPerPage', () => { + + const element = document.body.querySelector( 'c-pagination-controls' ); + element.recordsPerPage= 100; + + expect( element.recordsPerPage ).toBe( 100 ); + }); + it( 'when recordsPerPage is set to a String representing a number, will set the recordsPerPage to an Integer', () => { + + const element = document.body.querySelector( 'c-pagination-controls' ); + element.recordsPerPage = '100'; + + expect( element.recordsPerPage ).toBe( 100 ); + }); + + it( 'when recordsPerPage is set to a String not representing a number, will not reset the recordsPerPage', () => { + + const element = document.body.querySelector( 'c-pagination-controls' ); + + const defaultRecordsPerPage = element.recordsPerPage; + + element.recordsPerPage = 'thing'; + + expect( element.recordsPerPage ).toBe( defaultRecordsPerPage ); + }); }); \ No newline at end of file diff --git a/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.css b/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.css index a5c981473e9..5c4afa831b6 100644 --- a/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.css +++ b/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.css @@ -1,3 +1,4 @@ .page-selector { display: inline-block; + max-width: 90px; } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.html b/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.html index 19f22cd41a5..93b63815e12 100644 --- a/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.html +++ b/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.html @@ -14,7 +14,7 @@ data-ortoo-elem-id={pageSizeId} class="slds-p-left_x-small slds-p-right_x-small page-selector" value={recordsPerPage} - options={pageSizeOptions.data} + options={pageSizeOptions} onchange={handleChangePageSize} >

diff --git a/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.js b/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.js index f66bbb3348d..de83812f9c8 100644 --- a/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.js +++ b/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.js @@ -19,7 +19,8 @@ export default class PaginationControls extends LightningElement { } set recordsPerPage( value ) { const previousOffset = this.offset; - this._recordsPerPage = value; + + this._recordsPerPage = isNaN( value ) ? this._recordsPerPage : parseInt( value ); this.offset = previousOffset; } From 0745ef42b96038ab42bd7ac7027a2821db050e46 Mon Sep 17 00:00:00 2001 From: Robert Baillie Date: Mon, 24 Jan 2022 11:42:13 +0000 Subject: [PATCH 2/3] Brought in more changes from the example-app implementation of filter + results * Improvements to Contract to remove stack trace entry for the Contract methods * Add ability to define like Strings to the criteria * Added testing for Sobject Selector * Added protection to public AuraEnabled properties (private setters) * Added more capabilities to SObjectUtils for examining types --- .../classes/criteria/fflib_Criteria.cls | 4 +- .../classes/common/fflib_SObjectSelector.cls | 2 +- .../exceptions/utilities/StackTrace.cls | 1 + .../fflib-extension/ortoo_Criteria.cls | 1 - .../fflib-extension/ortoo_SobjectSelector.cls | 159 ++++++----- .../tests/ortoo_CriteriaTest.cls | 33 +++ .../tests/ortoo_CriteriaTest.cls-meta.xml | 5 + .../tests/ortoo_SobjectSelectorTest.cls | 252 ++++++++++++++++++ .../default/classes/search/SearchOrderBy.cls | 14 +- .../default/classes/search/SearchWindow.cls | 4 +- .../search/tests/SearchOrderByTest.cls | 30 ++- .../default/classes/utils/SobjectUtils.cls | 78 +++++- .../classes/utils/tests/ContractTest.cls | 27 +- .../classes/utils/tests/SobjectUtilsTest.cls | 128 ++++++++- 14 files changed, 636 insertions(+), 102 deletions(-) create mode 100644 framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls create mode 100644 framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls-meta.xml 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 From 3448786d023c0760bee839e57c279057d1eea319 Mon Sep 17 00:00:00 2001 From: Robert Baillie Date: Mon, 24 Jan 2022 11:47:50 +0000 Subject: [PATCH 3/3] Fixed check on message --- .../ortoo-core/default/classes/utils/tests/ContractTest.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 997bdda706b..5c8e9f90c24 100644 --- a/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls +++ b/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls @@ -76,6 +76,6 @@ private without sharing class ContractTest Test.stopTest(); 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' ); + System.assertEquals( 'assert_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