diff --git a/README.md b/README.md index 116d5c1..f91bdae 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,52 @@ FFLib Apex Common Sample -======================== +===================================== ![Push Source and Run Apex Tests](https://github.com/apex-enterprise-patterns/fflib-apex-common-samplecode/workflows/Create%20a%20Scratch%20Org,%20Push%20Source%20and%20Run%20Apex%20Tests/badge.svg) **Dependencies:** Must deploy [Apex Mocks](https://github.com/apex-enterprise-patterns/fflib-apex-mocks) and [Apex Common](https://github.com/apex-enterprise-patterns/fflib-apex-common) before deploying this library -Deploy Apex Mocks - - Deploy to Salesforce - - -Deploy Apex Common - - Deploy to Salesforce - - -Deploy Apex Common Sample Code - - Deploy to Salesforce - +| Library | Deploy | +|---------|--------| +| Apex Mocks | Deploy to Salesforce | +| Apex Common | Deploy to Salesforce | +| Apex Common Sample Code | Deploy to Salesforce | Sample Application ================== -This repository contains a sample application illustrating the Apex Enterprise Patterns library. The aim is to illustrate fullly working sample application to better illustrate the patterns in presentations and articles. You can see a [demo of this application in the Dreamforce 2013 session](http://www.youtube.com/watch?v=qlq46AEAlLI#t=572) presented by @afawcett +This repository contains a sample application illustrating the Apex Enterprise Patterns library. The aim is to illustrate a fully working sample application that demonstrates the patterns. **NOTE:** The supporting **Apex Common** library can be found [here](https://github.com/apex-enterprise-patterns/fflib-apex-common). -![Alt text](/images/sampleappoverview.png "Optional title") +| Platform Feature | Patterns Used | +|------------------|---------------| +| Custom Buttons | Building **UI** logic and calling **Service Layer** code from Controllers | +| Batch Apex | Reusing **Service** and **Selector Layer** code from within a Batch context | +| Integration API | Exposing an Integration API via **Service Layer** using Apex and REST | +| Apex Triggers | Factoring your Apex Trigger logic via the **Domain Layer** (wrappers) | + +User Mode and CRUD/FLS +---------------------- + +This sample enforces field-level security (FLS) by using fflib user mode throughout. Selectors pass `fflib_SObjectSelector.DataAccess.USER_MODE` into the `super(...)` constructor for FLS on queries; the UnitOfWork uses `UserModeDML()` via an inner `UserModeUnitOfWorkFactory` in `Application.cls` for FLS on DML. Tests that exercise USER_MODE code use `TestDataFactory` to create a Standard User with the `ApexEnterprisePatternsSampleApp` permission set, then run under `System.runAs(getRunAsUser())`; data setup requiring elevated permissions (e.g. PricebookEntry insert) runs in system context. + +| Area | Approach | +|------|----------| +| **Selectors** | `super(false, fflib_SObjectSelector.DataAccess.USER_MODE)` in selector constructors (and `includeFieldSetFields` overload where present) | +| **UnitOfWork** | Inner `UserModeUnitOfWorkFactory` in Application.cls; uses `UserModeDML()` | +| **Tests** | `TestDataFactory`; `@TestSetup` + `System.runAs(getRunAsUser())` for DML/selector tests | +| **Permission set** | `ApexEnterprisePatternsSampleApp` grants field-level access for USER_MODE tests | Application Enterprise Patterns on Salesforce Lightning Platform ================================================================ Design patterns are an invaluable tool for developers and architects looking to build enterprise solutions. Here are presented some tried and tested enterprise application engineering patterns that have been used in other platforms and languages. We will discuss and illustrate how patterns such as Data Mapper, Service Layer, Unit of Work and of course Model View Controller can be applied to Force.com. Applying these patterns can help manage governed resources (such as DML) better, encourage better separation-of-concerns in your logic and enforce Force.com coding best practices. -Dreamforce Session and Slides ------------------------------ - -- View slides for the **Dreamforce 2013** session [here](https://docs.google.com/file/d/0B6brfGow3cD8RVVYc1dCX2s0S1E/edit) -- Video recording of the **Dreamforce 2014** advanced session [here](https://www.youtube.com/watch?v=BLXp0ZP0cF0). -- View slides for the **Dreamforce 2015** session [here](http://www.slideshare.net/andyinthecloud/building-strong-foundations-apex-enterprise-patterns) - More Information on Trailhead -------------------------------------------- There are two Trailhead Modules for Apex Enterprise Patterns: +- [Apex Enterprise Patterns - Separation of Concerns](https://trailhead.salesforce.com/en/content/learn/modules/apex_patterns_sl/apex_patterns_sl_soc) - [Apex Enterprise Patterns - Service Layer](https://trailhead.salesforce.com/en/content/learn/modules/apex_patterns_sl) - - [Separation of Concerns](https://trailhead.salesforce.com/en/content/learn/modules/apex_patterns_sl/apex_patterns_sl_soc) - [Apex Enterprise Patterns - Domain and Selector Layer](https://trailhead.salesforce.com/en/content/learn/modules/apex_patterns_dsl) diff --git a/images/sampleappoverview.png b/images/sampleappoverview.png deleted file mode 100644 index 42bf8a6..0000000 Binary files a/images/sampleappoverview.png and /dev/null differ diff --git a/sfdx-source/apex-common-samplecode/main/classes/Application.cls b/sfdx-source/apex-common-samplecode/main/classes/Application.cls index b4d8882..609e7eb 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/Application.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/Application.cls @@ -26,9 +26,9 @@ public class Application { - // Configure and create the UnitOfWorkFactory for this Application - public static final fflib_Application.UnitOfWorkFactory UnitOfWork = - new fflib_Application.UnitOfWorkFactory( + // Configure and create the UnitOfWorkFactory for this Application (UserMode DML for FLS) + public static final fflib_Application.UnitOfWorkFactory UnitOfWork = + new UserModeUnitOfWorkFactory( new List { Account.SObjectType, Invoice__c.SObjectType, @@ -40,7 +40,7 @@ public class Application WorkOrder__c.SObjectType, DeveloperWorkItem__c.SObjectType, TrainingWorkItem__c.SObjectType, - DesignWorkItem__c.SObjectType }); + DesignWorkItem__c.SObjectType }); // Configure and create the ServiceFactory for this Application public static final fflib_Application.ServiceFactory Service = @@ -71,4 +71,13 @@ public class Application OpportunityLineItem.SObjectType => OpportunityLineItems.Constructor.class, Account.SObjectType => Accounts.Constructor.class, DeveloperWorkItem__c.SObjectType => DeveloperWorkItems.class }); + + // Custom UnitOfWork factory that uses UserModeDML for FLS enforcement + private class UserModeUnitOfWorkFactory extends fflib_Application.UnitOfWorkFactory { + public UserModeUnitOfWorkFactory(List objectTypes) { super(objectTypes); } + public override fflib_ISObjectUnitOfWork newInstance() { + if (m_mockUow != null) return m_mockUow; + return new fflib_SObjectUnitOfWork(m_objectTypes, new fflib_SObjectUnitOfWork.UserModeDML()); + } + } } diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls index ea2b8e7..a3ce517 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls @@ -63,12 +63,11 @@ public with sharing class OpportunityLineItems extends fflib_SObjectDomain continue; } } - - // Adjust UnitPrice - line.UnitPrice = line.UnitPrice * factor; - - // Register this record as dirty with the unit of work - uow.registerDirty(line); + + // Minimal record (Id + UnitPrice only): USER_MODE DML enforces FLS on every field in the record; queried records carry OpportunityId, PricebookEntryId, etc. that trigger "fields being inaccessible". + OpportunityLineItem updateOli = new OpportunityLineItem(Id = line.Id); + updateOli.UnitPrice = line.UnitPrice * factor; + uow.registerDirty(updateOli); } } diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls index 4979727..1dd1876 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls @@ -34,7 +34,12 @@ public class AccountsSelector extends fflib_SObjectSelector { return (IAccountsSelector) Application.Selector.newInstance(Account.SObjectType); } - + + public AccountsSelector() + { + super(false, fflib_SObjectSelector.DataAccess.USER_MODE); + } + public List getSObjectFieldList() { return new List { diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls index 9cbfe83..f79980f 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls @@ -31,7 +31,12 @@ public class OpportunitiesSelector extends fflib_SObjectSelector { return (IOpportunitiesSelector) Application.Selector.newInstance(Opportunity.SObjectType); } - + + public OpportunitiesSelector() + { + super(false, fflib_SObjectSelector.DataAccess.USER_MODE); + } + public List getSObjectFieldList() { return new List { diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls index 83492b5..bd799a4 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls @@ -31,7 +31,12 @@ public class OpportunityLineItemsSelector extends fflib_SObjectSelector { return (IOpportunityLineItemsSelector) Application.Selector.newInstance(OpportunityLineItem.SObjectType); } - + + public OpportunityLineItemsSelector() + { + super(false, fflib_SObjectSelector.DataAccess.USER_MODE); + } + public List getSObjectFieldList() { return new List { diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls index 138998c..c010aff 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls @@ -31,7 +31,12 @@ public class PricebookEntriesSelector extends fflib_SObjectSelector { return (IPricebookEntriesSelector) Application.Selector.newInstance(PricebookEntry.SObjectType); } - + + public PricebookEntriesSelector() + { + super(false, fflib_SObjectSelector.DataAccess.USER_MODE); + } + public List getSObjectFieldList() { return new List { diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls index 694679f..20594c2 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls @@ -31,7 +31,12 @@ public class PricebooksSelector extends fflib_SObjectSelector { return (IPricebooksSelector) Application.Selector.newInstance(Pricebook2.SObjectType); } - + + public PricebooksSelector() + { + super(false, fflib_SObjectSelector.DataAccess.USER_MODE); + } + public List getSObjectFieldList() { return new List { diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls index ae7018c..8d8b60a 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls @@ -34,12 +34,12 @@ public class ProductsSelector extends fflib_SObjectSelector public ProductsSelector() { - super(false); + super(false, fflib_SObjectSelector.DataAccess.USER_MODE); } - + public ProductsSelector(Boolean includeFieldSetFields) { - super(includeFieldSetFields); + super(includeFieldSetFields, fflib_SObjectSelector.DataAccess.USER_MODE); } public List getSObjectFieldList() diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls index 6308574..36dd2c3 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls @@ -34,7 +34,12 @@ public class UsersSelector extends fflib_SObjectSelector { return (IUsersSelector) Application.Selector.newInstance(User.SObjectType); } - + + public UsersSelector() + { + super(false, fflib_SObjectSelector.DataAccess.USER_MODE); + } + public List getSObjectFieldList() { // Current app requirements driven solely by getUsersEmail at this stage diff --git a/sfdx-source/apex-common-samplecode/main/permissionSets/ApexEnterprisePatternsSampleApp.permissionset-meta.xml b/sfdx-source/apex-common-samplecode/main/permissionSets/ApexEnterprisePatternsSampleApp.permissionset-meta.xml new file mode 100644 index 0000000..3ade872 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/permissionSets/ApexEnterprisePatternsSampleApp.permissionset-meta.xml @@ -0,0 +1,350 @@ + + + Grants access to custom fields and objects used by Apex Enterprise Patterns sample app tests (USER_MODE enforcement) + + true + DeveloperWorkItem__c.CodeReviewingHours__c + true + + + true + DeveloperWorkItem__c.CodingHours__c + true + + + true + DeveloperWorkItem__c.DeveloperCost__c + true + + + true + DeveloperWorkItem__c.TechnicalDesignHours__c + true + + + true + TrainingWorkItem__c.DeliveryHours__c + true + + + true + TrainingWorkItem__c.PreparationHours__c + true + + + true + TrainingWorkItem__c.TrainingCost__c + true + + + true + WorkOrder__c.Account__c + true + + + true + InvoiceLine__c.Description__c + true + + + true + InvoiceLine__c.Product__c + true + + + true + InvoiceLine__c.Quantity__c + true + + + true + InvoiceLine__c.UnitPrice__c + true + + + true + Invoice__c.Account__c + true + + + true + Invoice__c.Description__c + true + + + true + Invoice__c.InvoiceDate__c + true + + + true + Opportunity.AccountId + true + + + true + Opportunity.Amount + true + + + true + Opportunity.CampaignId + true + + + true + Opportunity.ContractId + true + + + true + Opportunity.Description + true + + + true + Opportunity.DiscountType__c + true + + + true + Opportunity.ExpectedRevenue + true + + + true + Opportunity.InvoicedStatus__c + true + + + true + Opportunity.IsPrivate + true + + + true + Opportunity.LeadSource + true + + + true + Opportunity.NextStep + true + + + true + Opportunity.Probability + true + + + true + Opportunity.TotalOpportunityQuantity + true + + + true + Opportunity.Type + true + + + true + OpportunityLineItem.Description + true + + + true + OpportunityLineItem.Discount + true + + + true + OpportunityLineItem.ListPrice + true + + + true + OpportunityLineItem.ProductCode + true + + + true + OpportunityLineItem.ServiceDate + true + + + true + OpportunityLineItem.Subtotal + true + + + true + OpportunityLineItem.TotalPrice + true + + + true + Product2.Description + true + + + true + Product2.DiscountingApproved__c + true + + + true + Product2.DisplayUrl + true + + + true + Product2.ExternalDataSourceId + true + + + true + Product2.ExternalId + true + + + true + Product2.Family + true + + + true + Product2.ProductCode + true + + + true + Product2.QuantityUnitOfMeasure + true + + + true + Product2.SellerId + true + + + true + Product2.SourceProductId + true + + + true + Product2.StockKeepingUnit + true + + + true + Product2.SubscriberField__c + true + + + true + Product2.TransferRecordMode + true + + false + + Salesforce + + true + false + true + true + false + Account + false + false + + + true + false + true + true + false + DeveloperWorkItem__c + false + false + + + true + false + true + true + false + InvoiceLine__c + false + false + + + true + false + true + true + false + Invoice__c + false + false + + + true + true + true + true + false + Opportunity + false + false + + + false + false + false + true + false + Pricebook2 + false + false + + + true + false + true + true + false + Product2 + false + false + + + true + false + true + true + false + TrainingWorkItem__c + false + false + + + true + false + true + true + false + WorkOrder__c + false + false + + + standard-Opportunity + Visible + + + standard-Product2 + Visible + + diff --git a/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls b/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls new file mode 100644 index 0000000..f404b89 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls @@ -0,0 +1,46 @@ +/** + * Shared test data factory. Creates a Standard User with ApexEnterprisePatternsSampleApp + * permission set for runAs tests that require field-level access (USER_MODE enforcement). + */ +@IsTest +public class TestDataFactory +{ + private static final String RUN_AS_USER_LASTNAME = 'SampleApp'; + + /** + * Creates a Standard User with ApexEnterprisePatternsSampleApp permission set. + * Call from @TestSetup. In test methods, use getRunAsUser() with System.runAs(). + * @return The created user + */ + public static User createUserWithSampleAppPermissions() + { + List salesProfiles = [SELECT Id FROM Profile WHERE Name = 'Sales User' AND UserLicense.Name = 'Salesforce' LIMIT 1]; + Profile p = salesProfiles.isEmpty() + ? [SELECT Id FROM Profile WHERE Name = 'Standard User' AND UserLicense.Name = 'Salesforce' LIMIT 1] + : salesProfiles[0]; + String uniqueName = 'sampleapp_' + DateTime.now().getTime() + '@test.org'; + User u = new User( + Alias = 'sampapp', + Email = 'sampleapp@test.org', + EmailEncodingKey = 'UTF-8', + LastName = RUN_AS_USER_LASTNAME, + LanguageLocaleKey = 'en_US', + LocaleSidKey = 'en_US', + ProfileId = p.Id, + TimeZoneSidKey = 'America/Los_Angeles', + UserName = uniqueName + ); + insert u; + + PermissionSet ps = [SELECT Id FROM PermissionSet WHERE Name = 'ApexEnterprisePatternsSampleApp' LIMIT 1]; + insert new PermissionSetAssignment(AssigneeId = u.Id, PermissionSetId = ps.Id); + + return u; + } + + /** Returns the runAs user created by createUserWithSampleAppPermissions(). Use with System.runAs(). */ + public static User getRunAsUser() + { + return [SELECT Id FROM User WHERE LastName = :RUN_AS_USER_LASTNAME LIMIT 1]; + } +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls-meta.xml new file mode 100644 index 0000000..5f399c3 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls-meta.xml @@ -0,0 +1,5 @@ + + + 63.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/test/classes/domains/DeveloperWorkItemsTest.cls b/sfdx-source/apex-common-samplecode/test/classes/domains/DeveloperWorkItemsTest.cls index 98c0a4c..d76f55c 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/domains/DeveloperWorkItemsTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/domains/DeveloperWorkItemsTest.cls @@ -24,25 +24,34 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -@isTest -private class DeveloperWorkItemsTest { +@IsTest +private class DeveloperWorkItemsTest +{ + @TestSetup + static void setup() + { + TestDataFactory.createUserWithSampleAppPermissions(); + } - static testMethod void whenInsertingDeveloperWorkItemsThenCalcCost() { - - // Given - fflib_ISObjectUnitOfWOrk uow = Application.UnitOfWork.newInstance(); - WorkOrder__c wo = new WorkOrder__c(); - DeveloperWorkItem__c wi = new DeveloperWorkItem__c(); - wi.CodingHours__c = 20; - wi.CodeReviewingHours__c = 1; - wi.TechnicalDesignHours__c = 10; - uow.registerNew(wo); - uow.registerNew(wi, DeveloperWorkItem__c.WorkOrder__c, wo); - - // When - uow.commitWork(); - - // Then - System.assertEquals(3100, [select DeveloperCost__c from DeveloperWorkItem__c where Id = :wi.Id].DeveloperCost__c); - } + @IsTest + private static void whenInsertingDeveloperWorkItemsThenCalcCost() + { + // Given - data setup in system context, DML in runAs for USER_MODE + WorkOrder__c wo = new WorkOrder__c(); + DeveloperWorkItem__c wi = new DeveloperWorkItem__c(); + wi.CodingHours__c = 20; + wi.CodeReviewingHours__c = 1; + wi.TechnicalDesignHours__c = 10; + + System.runAs(TestDataFactory.getRunAsUser()) + { + fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance(); + uow.registerNew(wo); + uow.registerNew(wi, DeveloperWorkItem__c.WorkOrder__c, wo); + uow.commitWork(); + } + + // Then + System.assertEquals(3100, [SELECT DeveloperCost__c FROM DeveloperWorkItem__c WHERE Id = :wi.Id].DeveloperCost__c); + } } \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls b/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls index f4cc1ec..d59223d 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls @@ -30,6 +30,12 @@ @IsTest private class OpportunitiesTest { + @TestSetup + static void setup() + { + TestDataFactory.createUserWithSampleAppPermissions(); + } + @IsTest private static void callingApplyDiscountShouldCalcDiscountAndRegisterDirty() { @@ -65,36 +71,46 @@ private class OpportunitiesTest @IsTest private static void testApplyDiscountWithoutOpportunityLines() { - Opportunity opp = new Opportunity ( - Name = 'Test', + Opportunity opp = new Opportunity( + Name = 'Test', Type = 'New Account', StageName = 'Open', - CloseDate = System.today().addMonths(1), - Amount = 100 ); - insert opp; - fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork( new Schema.SObjectType[] { Opportunity.SObjectType }); - Opportunities opps = new Opportunities(new List { opp }); - opps.applyDiscount(10, uow); - uow.commitWork(); - Opportunity assertOpp = [select Amount from Opportunity where Id = :opp.Id]; - System.assertEquals(90, assertOpp.Amount); + CloseDate = System.today().addMonths(1), + Amount = 100 + ); + insert opp; + + System.runAs(TestDataFactory.getRunAsUser()) + { + fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance(); + IOpportunities opps = Opportunities.newInstance(new List { opp }); + opps.applyDiscount(10, uow); + uow.commitWork(); + } + + Opportunity assertOpp = [SELECT Amount FROM Opportunity WHERE Id = :opp.Id]; + System.assertEquals(90, assertOpp.Amount); } @IsTest private static void testUpdateAccountOnInsertOfOpportunity() { - Account account = new Account( - Name = 'Test', - Description = null ); + Account account = new Account(Name = 'Test', Description = null); insert account; - Opportunity opp = new Opportunity ( - Name = 'Test', - Type = 'Existing Account', - StageName = 'Open', - CloseDate = System.today().addMonths(1), - AccountId = account.Id ); - insert opp; - Account assertAccount = [select Description from Account where Id = :account.Id]; - System.assertEquals('Last Opportunity Raised ' + System.today(), assertAccount.Description); + + System.runAs(TestDataFactory.getRunAsUser()) + { + Opportunity opp = new Opportunity( + Name = 'Test', + Type = 'Existing Account', + StageName = 'Open', + CloseDate = System.today().addMonths(1), + AccountId = account.Id + ); + insert opp; + } + + Account assertAccount = [SELECT Description FROM Account WHERE Id = :account.Id]; + System.assertEquals('Last Opportunity Raised ' + System.today(), assertAccount.Description); } } \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/test/classes/selectors/ProductsSelectorTest.cls b/sfdx-source/apex-common-samplecode/test/classes/selectors/ProductsSelectorTest.cls index 5298527..fa859c5 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/selectors/ProductsSelectorTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/selectors/ProductsSelectorTest.cls @@ -27,49 +27,57 @@ @IsTest private class ProductsSelectorTest { + @TestSetup + static void setup() + { + TestDataFactory.createUserWithSampleAppPermissions(); + } + @IsTest private static void testSelectById() { - // Test data - Product2 product = new Product2(); - product.Description = 'Something cool'; - product.Name = 'CoolItem'; - product.IsActive = true; - product.SubscriberField__c = 'My Text Field'; + Product2 product = new Product2( + Description = 'Something cool', + Name = 'CoolItem', + IsActive = true, + SubscriberField__c = 'My Text Field' + ); insert product; - - // Query - List products = - new ProductsSelector(true).selectById(new Set { product.Id }); - - // Assert - System.assertEquals('Something cool', products[0].Description); - System.assertEquals('CoolItem', products[0].Name); - System.assertEquals(true, products[0].IsActive); - System.assertEquals('My Text Field', products[0].SubscriberField__c); + + System.runAs(TestDataFactory.getRunAsUser()) + { + List products = + new ProductsSelector(true).selectById(new Set { product.Id }); + + System.assertEquals('Something cool', products[0].Description); + System.assertEquals('CoolItem', products[0].Name); + System.assertEquals(true, products[0].IsActive); + System.assertEquals('My Text Field', products[0].SubscriberField__c); + } } @IsTest private static void testQueryLocatorById() { - // Test data - Product2 product = new Product2(); - product.Description = 'Something cool'; - product.Name = 'CoolItem'; - product.IsActive = true; - product.SubscriberField__c = 'My Text Field'; + Product2 product = new Product2( + Description = 'Something cool', + Name = 'CoolItem', + IsActive = true, + SubscriberField__c = 'My Text Field' + ); insert product; - - // Query - Database.QueryLocator queryLocator = - new ProductsSelector(true).queryLocatorById(new Set { product.Id }); - - // Assert - Database.QueryLocatorIterator productsIterator = queryLocator.iterator(); - Product2 queriedProduct = (Product2) productsIterator.next(); - System.assertEquals('Something cool', queriedProduct.Description); - System.assertEquals('CoolItem', queriedProduct.Name); - System.assertEquals(true, queriedProduct.IsActive); - System.assertEquals('My Text Field', queriedProduct.SubscriberField__c); + + System.runAs(TestDataFactory.getRunAsUser()) + { + Database.QueryLocator queryLocator = + new ProductsSelector(true).queryLocatorById(new Set { product.Id }); + Database.QueryLocatorIterator productsIterator = queryLocator.iterator(); + Product2 queriedProduct = (Product2) productsIterator.next(); + + System.assertEquals('Something cool', queriedProduct.Description); + System.assertEquals('CoolItem', queriedProduct.Name); + System.assertEquals(true, queriedProduct.IsActive); + System.assertEquals('My Text Field', queriedProduct.SubscriberField__c); + } } } \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/InvoicingServiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/InvoicingServiceTest.cls index fa0de4c..a7c15d3 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/InvoicingServiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/InvoicingServiceTest.cls @@ -27,27 +27,33 @@ @IsTest private class InvoicingServiceTest { + @TestSetup + static void setup() + { + TestDataFactory.createUserWithSampleAppPermissions(); + } + @IsTest private static void testService() { - // Create Test Data + // Given: create data in system context (admin runs setup; PricebookEntry insert works) fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance(); Opportunity opp = new Opportunity(); opp.Name = 'Opportunity'; opp.StageName = 'Open'; opp.CloseDate = System.today(); - uow.registerNew(opp); - for(Integer i=0; i<5; i++) - { + uow.registerNew(opp); + for (Integer i = 0; i < 5; i++) + { Product2 product = new Product2(); product.Name = opp.Name + ' : Product : ' + i; - uow.registerNew(product); + uow.registerNew(product); PricebookEntry pbe = new PricebookEntry(); pbe.UnitPrice = 10; pbe.IsActive = true; pbe.UseStandardPrice = false; pbe.Pricebook2Id = Test.getStandardPricebookId(); - uow.registerNew(pbe, PricebookEntry.Product2Id, product); + uow.registerNew(pbe, PricebookEntry.Product2Id, product); OpportunityLineItem oppLineItem = new OpportunityLineItem(); oppLineItem.Quantity = 1; oppLineItem.TotalPrice = 10; @@ -56,10 +62,11 @@ private class InvoicingServiceTest } uow.commitWork(); - // Call Service - List invoiceIds = InvoicingService.generate(new List { opp.Id }); - - // Assert Invoices - System.assertEquals(1, invoiceIds.size()); + // When: run service as permission-set user (USER_MODE selectors/DML) + System.runAs(TestDataFactory.getRunAsUser()) + { + List invoiceIds = InvoicingService.generate(new List { opp.Id }); + System.assertEquals(1, invoiceIds.size(), 'Should generate one invoice'); + } } } \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls index e75a019..1ebec2b 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls @@ -27,10 +27,16 @@ @IsTest private class OpportunitiesServiceIntegrationTest { + @TestSetup + static void setup() + { + TestDataFactory.createUserWithSampleAppPermissions(); + } + @IsTest private static void testServiceClass() { - // Create Opportunity, Lines, Pricebooks and Products + // Create Opportunity, Lines, Pricebooks and Products (system context for inserts) List opps = new List(); List> productsByOpp = new List>(); List> pricebookEntriesByOpp = new List>(); @@ -94,12 +100,15 @@ private class OpportunitiesServiceIntegrationTest } insert allOppLineItems; - // Test - Test.startTest(); - OpportunitiesService.applyDiscounts(new Set { opp.Id }, 10); - Test.stopTest(); + // Test - applyDiscounts uses USER_MODE selectors/DML, must run as user with permission set + System.runAs(TestDataFactory.getRunAsUser()) + { + Test.startTest(); + OpportunitiesService.applyDiscounts(new Set { opp.Id }, 10); + Test.stopTest(); + } // Assert - System.assertEquals(90, [select Amount from Opportunity limit 1][0].Amount); + System.assertEquals(90, [SELECT Amount FROM Opportunity LIMIT 1][0].Amount); } } \ No newline at end of file