From 851fbaf11dbd0ed836597e3d70819e420a0008f9 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 4 Feb 2021 11:48:55 -0500 Subject: [PATCH 01/24] Add UnitTest Added UnitTest.cls from GMS --- .../classes/testUtils/UnitTest/README.md | 206 +++++++ .../classes/testUtils/UnitTest/UnitTest.cls | 538 ++++++++++++++++++ .../testUtils/UnitTest/UnitTest.cls-meta.xml | 5 + .../labels/CustomLabels.labels-meta.xml | 27 + 4 files changed, 776 insertions(+) create mode 100644 force-app/main/default/classes/testUtils/UnitTest/README.md create mode 100644 force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls create mode 100644 force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls-meta.xml create mode 100644 force-app/main/default/labels/CustomLabels.labels-meta.xml diff --git a/force-app/main/default/classes/testUtils/UnitTest/README.md b/force-app/main/default/classes/testUtils/UnitTest/README.md new file mode 100644 index 0000000..90ea072 --- /dev/null +++ b/force-app/main/default/classes/testUtils/UnitTest/README.md @@ -0,0 +1,206 @@ +# UnitTest + +`UnitTest` is a unit-testing framework based on the API and use of Python's [unittest.mock](https://docs.python.org/3/library/unittest.mock.html). + +- When an instance is mocked, all of the instance's methods automatically have a mock created and associated with the instance method. +- Set and get a mock `returnValue` with a `UnitTest.Mock` property. +- Have a mocked method throw an Exception by setting its `returnValue` as an instance of an Exception or an Exception Type. +- Add custom logic of what a `UnitTest.Mock` will return when called by creating an extension of `UnitTest.Mock` and overriding the [sideEffect](#sideeffect) method. +- Assert the `UnitTest.Mock` was called or not called using assert API: + +# UnitTest.Mock + +`UnitTest.Mock` provides a core Mock class removing the need to create a host of stubs throughout your test suite. After performing an action, you can make assertions about which methods / attributes were used and arguments they were called with. You can also specify return values and set needed attributes in the normal way. + +**Attributes** + +- `Object returnValue` + - The return value of the method called. + - Default: `null` + - If the `returnValue` is an instance of an `Exception`, the exception is thrown. + - If the `returnValue` is an exception type, a new instance of the exception type is thrown. +- `Map methods` + - Map of [UnitTest.Mock](#unittestmock) by method name for stubs of this mock. + - Edit at your own risk. +- `List> calls` + - List of each call's arguments (i.e. `List`). + - If a method is [overloaded](https://salesforce.stackexchange.com/questions/165127/overloading-method-return-type-in-apex), the number of arguments may differ between calls. + +**Methods** + +- [getMethod(String methodName)](#getmethod) + - Returns the `UnitTest.Mock` associated with the `methodName`. If the `methodName` does not have an associated `UnitTest.Mock`, a new `UnitTest.Mock` is constructed for `methodName` and returned. +- [setMethod(String methodName, UnitTest.Mock mock)](#setmethod) + - Overrides the default UnitTest.Mock associated with `methodName`. Remember, if `mock` is null, [getMethod](#getmethod) will create a new UnitTest.Mock associated with methodName. +- [sideEffect(Object instance, String methodName, Type returnType, List parameterTypes, List parameterNames, List arguments)](#sideeffect) + - Returns what `handleMethodCall` will return. If `sideEffect` returns a [stub provider](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_System_StubProvider.htm), `handleMethodCall` will try return a stub of the stub provider with the `returnType` argument. Default: returns `returnValue`. + +## getMethod + +Returns the `UnitTest.Mock` associated with the `methodName`. If the `methodName` does not have an associated `UnitTest.Mock`, a new `UnitTest.Mock` is constructed for `methodName` and returned. + +### Parameters + +#### methodName + +Type: [String](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_string.htm) + +Name of method to mock. Cannot be null. + +### Return Value + +Type: [UnitTest.Mock](#unittestmock) + +UnitTest.Mock for the `methodName`. + +### Example + +```apex +public interface MyService { + + String getName(); + Integer getInteger(); + +} + +@IsTest +public with sharing class MyService_TEST { + + @IsTest + private static void stubGetName() { + final UnitTest.Mock myServiceMock = new UnitTest.Mock(); + + myServiceMock.getMethod('getName').returnValue = 'Random' + Crypto.getRandomInteger(); + + // REMEMBER: myServiceMock.getMethod('getName') is a UnitTest.Mock + // automatically created. + + Test.startTest(); + + final MyService myService = (MyService) myServiceMock.createStub(MyService.class); + + final String actualName = myService.getName(); + final Integer actualInteger = myService.getInteger(); + + Test.stopTest(); + + System.assertEquals( + myServiceMock.getMethod('getName').returnValue, + actualName, + ); + + // REMEMBER: UnitTest.Mock.return value defaults to `null`. + System.assertEquals( + null, + myServiceMock.getMethod('getInteger').returnValue + ); + + System.assertEquals( + actualInteger, + myServiceMock.getMethod('getInteger').returnValue + ); + } +} + +``` + +## setMethod + +Overrides the default UnitTest.Mock associated with `methodName`. Remember, if `mock` is null, [getMethod](#getmethod) will create a new UnitTest.Mock associated with methodName. + +### Parameters + +#### methodName + +Type: [String](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_string.htm) + +Name of method to mock. Cannot be null. + +#### mock + +Type: [UnitTest.Mock](#unittestmock) + +Mock for the method. Remember, if mock is null, getMethod will create a new UnitTest.Mock associated with methodName. + +### Return Value + +Type: void + +### Example + +```apex +public interface MyService { + + String getName(); + Integer getInteger(); + +} + +@IsTest +public with sharing class MyService_TEST { + + @IsTest + private static void stubGetName() { + final UnitTest.Mock myServiceMock = new UnitTest.Mock(); + + final UnitTest.Mock getNameMock = new UnitTest.Mock(); + + myServiceMock.setMethod(`getName`, getNameMock); + + System.assert( + getNameMock === myServiceMock.getMethod('getName'), + `The mock for getName should have the same memory location (===) as getNameMock` + ); + } +} +``` + +## sideEffect + +Virtual method that returns what `handleMethodCall` will return. If `sideEffect` returns a [stub provider](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_System_StubProvider.htm), `handleMethodCall` will try return a stub of the stub provider with the `returnType` argument. + +Default: returns `returnValue`. + +### Parameters + +#### instance + +Type: Object + +`instance` of the [handleMethodCall](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_System_StubProvider.htm#apex_System_StubProvider_handleMethodCall) + +#### methodName + +Type: [String](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_string.htm#apex_methods_system_string) + +`methodName` of the [handleMethodCall](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_System_StubProvider.htm#apex_System_StubProvider_handleMethodCall) + +#### returnType + +Type: [System.Type](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_type.htm#apex_methods_system_type) + +`returnType` of the [handleMethodCall](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_System_StubProvider.htm#apex_System_StubProvider_handleMethodCall) + +#### parameterTypes + +Type: [List](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_type.htm#apex_methods_system_type) + +`parameterTypes` of the [handleMethodCall](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_System_StubProvider.htm#apex_System_StubProvider_handleMethodCall) + +#### parameterNames + +Type: [List](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_string.htm#apex_methods_system_string) + +`parameterNames` of the [handleMethodCall](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_System_StubProvider.htm#apex_System_StubProvider_handleMethodCall) + +#### arguments + +Type: List + +`arguments` of the [handleMethodCall](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_System_StubProvider.htm#apex_System_StubProvider_handleMethodCall) + +### Return Value + +Type: Object + +What `handleMethodCall` will return. If an instance of an Exception or an Exception Type is returned, the exception instance or a new instance of the Exception Type is thrown. If a [System.StubProvider](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_System_StubProvider.htm#apex_System_StubProvider_handleMethodCall) is returned, a stub of the [System.StubProvider](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_System_StubProvider.htm#apex_System_StubProvider_handleMethodCall) is returned. Default: returns `returnValue`. diff --git a/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls b/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls new file mode 100644 index 0000000..75901f5 --- /dev/null +++ b/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls @@ -0,0 +1,538 @@ +/** + * TODO: Add documentation. + * TODO: Add UnitTest_TEST with examples showing how to UnitTest with best practices. + */ +@IsTest +public with sharing class UnitTest { + private static Integer MOCK_ID_INDEX = 0; + + /** + * Generates a mocked Id that is guaranteed to be unique within a transaction. + * @param sObjectType In tests, you can pass in a SObjectType with Schema.[SObjectType Name].SObjectType. + * @return A single mocked Id of sObjectType guaranteed to be unique within a transaction. + */ + public static Id mockId(Schema.DescribeSObjectResult describe) { + return describe.getKeyPrefix() + + String.valueOf(UnitTest.MOCK_ID_INDEX++).leftPad(12, '0'); + } + + /** + * Generates a mocked Id that is guaranteed to be unique within a transaction. + * @param sObjectType In tests, you can pass in a describe with Schema.SObjectType.[SObjectType Name]. + * @return A single mocked Id of sObjectType guaranteed to be unique within a transaction. + */ + public static Id mockId(SObjectType sObjectType) { + return UnitTest.mockId(sObjectType.getDescribe()); + } + + /** + * Generates mocked Ids that are guaranteed to be unique within a transaction. + * @param describe In tests, you can pass in a describe with Schema.SObjectType.[SObjectType Name]. + * @param size Number of Ids to generate for this sObjectType + * @return List of mocked Ids of sObjectType guaranteed to be unique within a transaction. + */ + public static List mockIds(Schema.DescribeSObjectResult describe, Integer size) { + final List mockIds = new List(); + for (Integer i = 0; i < size; i++) { + mockIds.add(UnitTest.mockId(describe)); + } + return mockIds; + } + + /** + * Generates mocked Ids that are guaranteed to be unique within a transaction. + * @param sObjectType In tests, you can pass in a SObjectType with Schema.[SObjectType Name].SObjectType. + * @param size Number of Ids to generate for this sObjectType + * @return List of mocked Ids of sObjectType guaranteed to be unique within a transaction. + */ + public static List mockIds(SObjectType sObjectType, Integer size) { + return UnitTest.mockIds(sObjectType.getDescribe(), size); + } + + /** + * An exception that can be used for testing so tests don't have to create their own Exceptions. + */ + public with sharing class TestException extends Exception { + } + + /** + * Tab delimiter when debugging. + */ + public static String DEBUG_DELIMITER = ' '; + + /** + * Unit testing magic. + * TODO: Add documentation. + */ + public with sharing virtual class Mock implements System.StubProvider { + public final Map methods = new Map(); + + // TODO: support asserting calls at a specified index + @TestVisible + public final List> calls = new List>(); + + public Object returnValue { get; set; } + + /** + * Returns the `UnitTest.Mock` associated with the `methodName`. If the + * `methodName` does not have an associated `UnitTest.Mock`, a new `UnitTest.Mock` + * is constructed for `methodName` and returned. + * @param methodName Name of method to mock. Cannot be null. + * @return UnitTest.Mock for the `methodName`. + */ + public UnitTest.Mock getMethod(String methodName) { + System.assertNotEquals( + null, + methodName, + 'methodName argument of UnitTest.Mock.getMethod() should not be null.' + ); + final String lowerMethodName = methodName.toLowerCase(); + UnitTest.Mock method = this.methods.get(lowerMethodName); + if (method == null) { + method = new UnitTest.Mock(); + this.methods.put(lowerMethodName, method); + } + return method; + } + + /** + * Overrides the default UnitTest.Mock associated with `methodName`. Remember, if `mock` is null, getMethod will create a new UnitTest.Mock associated with methodName. + * @param methodName Name of the method to mock. Cannot be null. + * @param mock Mock for the method. Remember, if mock is null, getMethod will create a new UnitTest.Mock associated with methodName. + */ + public void setMethod(String methodName, UnitTest.Mock mock) { + System.assertNotEquals( + null, + methodName, + 'methodName argument of UnitTest.Mock.getMethod() should not be null.' + ); + System.assertNotEquals(null, mock, 'Cannot set a method\'s mock as null.'); + final String lowerMethodName = methodName.toLowerCase(); + this.methods.put(lowerMethodName, mock); + } + + /** + * Virtual method that returns what `handleMethodCall` will return. If `sideEffect` returns a stub + * provider, `handleMethodCall` will try return a stub of the stub provider with + * the `returnType` argument. Default: returns `returnValue`. + * @param instance `instance` of the `handleMethodCall` + * @param methodName `methodName` of the `handleMethodCall` + * @param returnType `returnType` of the `handleMethodCall` + * @param parameterTypes `parameterTypes` of the `handleMethodCall` + * @param parameterNames `parameterNames` of the `handleMethodCall` + * @param arguments `arguments` of the `handleMethodCall` + * @return What `handleMethodCall` will return. If an instance of an Exception or an Exception Type is returned, the exception instance or a new instance of the Exception Type is thrown. If a `System.StubProvider` is returned, a stub of the `System.StubProvider` is returned. Default: returns `returnValue`. + */ + public virtual Object sideEffect( + Object instance, + String methodName, + Type returnType, + List parameterTypes, + List parameterNames, + List arguments + ) { + return this.returnValue; + } + + /** + * System.StubProvider implementation. + * @param instance Stubbed instance of the method called. We assert instance + * is an instance of the expected stubbed method. + * @param methodName Name of the called method. We assert the expected stubbed + * method's Method Name equals-ignore-case methodName. + * @param returnType Return type of the called method. We assert the expected + * stubbed method's Return Type equals returnType. + * @param parameterTypes List the called method's parameter Types. We assert the + * expected stubbed method's Return Type equals returnType. + * @param parameterNames List the called method's parameter names. In Apex, only a + * method's parameter types define the method's signature, so + * we don't assert anything regarding parameter names. + * @param arguments List of the called method's arguments. We assert the + * expected stubbed method's Expected Arguments equal + * arguments. + * @return First, we assert the method called matches the signature + * of the expected stubbed method. + * + * Then we either throw the expected stubbed method's + * Exception Thrown Before Returning Value if set + * to a non-null value, or we return the expected + * stubbed method's Return Value. + * + * If the expected stubbed method implements ModifyArguments, + * the expected stubbed method's modifyArguments(...) + * is called before returning its Return Value. This is + * useful if you absolutely need to modify a method's + * arguments before moving on in the unit test. + * Generally, this is NOT needed. + * + * NOTE: Stub.Method's getReturnValue() returns + * void.class by default which simulates a method + * with "void" Return Type. If you unexpectedly see + * void.class returned, you forgot to override + * the stubbed method's getReturnValue()! + */ + public Object handleMethodCall( + final Object instance, + final String methodName, + final Type returnType, + final List parameterTypes, + final List parameterNames, + final List arguments + ) { + final UnitTest.Mock method = this.getMethod(methodName); + method.calls.add(arguments); + + final Object returnValue = method.sideEffect( + instance, + methodName, + returnType, + parameterTypes, + parameterNames, + arguments + ); + if (returnValue instanceof System.StubProvider) { + try { + // Work-around to stubbing System-defined classes: Try wrapping in "singleton". + // See: FeatureManagement and PermissionValidator + return Test.createStub(returnType, (System.StubProvider) returnValue); + } catch (System.TypeException typeException) { + System.assert( + false, + String.join( + new List{ + '', + '', + 'The return value provided by sideEffect(...) is a System.StubProvider instance,', + 'so UnitTest.Mock tried to return a Test.createStub using the method call\'s returnType', + 'and the return value as the stub provider. However, a System.TypeException was thrown.', + '', + 'Is the returnType a system defined type? If so, you cannot stub system defined types.', + 'Try creating an extension of UnitTest.Mock overriding sideEffect(...) to track and return', + 'values that are not instances of System.StubProvider.', + '', + debug(typeException), + '', + debug( + instance, + methodName, + returnType, + parameterTypes, + parameterNames, + arguments + ), + '', + 'this:' + this + }, + '\n' + ) + ); + } + } else if (returnValue instanceof Exception) { + throw (Exception) returnValue; + } else if (returnValue instanceof Type) { + try { + Object newInstance = ((Type) returnValue).newInstance(); + if (newInstance instanceof Exception) { + throw (Exception) newInstance; + } + } catch (Exception e) { + } + } + return returnValue; + } + + /** + * Shorthand to create a stub with Test.createStub using this as the stub provider. + * @param stubType Type of Stub + * @return Test.createStub(stubType, this) + */ + public Object createStub(Type stubType) { + return Test.createStub(stubType, this); + } + + /** + * Used to show which concrete type this UnitTest.Mock instance is when debugging. + * @return Concrete type of this UnitTest.Mock instance. + */ + public virtual Type getType() { + return UnitTest.class; + } + + /** + * Used to debug a UnitTest.Mock in human readable way. + * + * NOTE: Scott P has an idea for a debugging framework to generalize this concept. + */ + private String debug(Integer tabs) { + if (4 < tabs) { + return 'DEBUG_DEPTH_REACHED: 4'; + } + final String delimiter0 = DEBUG_DELIMITER.repeat(tabs); + final String delimiter1 = DEBUG_DELIMITER.repeat(tabs + 1); + final String delimiter2 = DEBUG_DELIMITER.repeat(tabs + 2); + + final List debugs = new List{ this.getType().getName() }; + + // returnValue + if (this.returnValue instanceof UnitTest.Mock) { + debugs.add( + delimiter1 + + 'returnValue: ' + + ((UnitTest.Mock) this.returnValue).debug(tabs + 2) + + ',' + ); + } else { + debugs.add(delimiter1 + 'returnValue: ' + this.returnValue + ','); + } + + // calls + debugs.add(delimiter1 + 'calls: ['); + for (List call : this.calls) { + debugs.add( + String.format( + delimiter2 + '({0}) [{1}]', + new List{ + String.join(call, ', '), + String.valueOf(call.size()) + } + ) + ); + } + debugs.add(delimiter1 + ']'); + + // methods + debugs.add(delimiter1 + 'methods:'); + for (String methodName : this.methods.keySet()) { + debugs.add( + delimiter2 + + methodName + + ': ' + + this.methods.get(methodName).debug(tabs + 2) + ); + } + return String.join(debugs, '\n'); + } + + public override String toString() { + return this.debug(0); + } + + /** + * Assert that the mock was called at least once. + */ + public void assertCalled() { + System.assert(!this.calls.isEmpty(), this); + } + + /** + * Assert that the mock was called exactly once. + */ + public void assertCalledOnce() { + System.assertEquals(1, this.calls.size(), this); + } + + /** + * This method is a convenient way of asserting that the last call has been made in a particular way. + */ + public void assertCalledWith(final List arguments) { + this.assertCalled(); + final List expected = arguments; + final List actual = this.calls[this.calls.size() - 1]; // Last call + UnitTest.assertCallEquals(expected, actual); + } + + /** + * Assert that the mock was called exactly once and that that call was with the specified arguments. + */ + public void assertCalledOnceWith(final List arguments) { + this.assertCalledOnce(); + this.assertCalledWith(arguments); + } + + /** + * Assert the mock has been called with the specified calls. The mockCalls list is + * checked for the calls. + * + * If anyOrder is false then the calls must be sequential. There can be extra calls + * before or after the specified calls. + * + * If anyOrder is true then the calls can be in any order, but they must all appear + * in mockCalls. + * + * TODO: support Boolean anyCalls argument + */ + public void assertHasCalls(final List> mockCalls) { + UnitTest.assertCallsEquals(this.calls, mockCalls); + } + + /** + * Assert the mock was never called. + */ + public void assertNotCalled() { + System.assert( + this.calls.isEmpty(), + 'UnitTest.Mock should not have been called: ' + this + ); + } + + /** + * Clears calls. You can also clear calls manually: `mock.class.clear()` + */ + public void clear() { + this.calls.clear(); + } + } + + /** + * Asserts expectedCalls equals actualCalls with human readable debugging. + */ + public static void assertCallsEquals( + List> expectedCalls, + List> actualCalls + ) { + if (expectedCalls == null || actualCalls == null) { + System.assertEquals( + expectedCalls, + actualCalls, + 'assertCallsEquals(List> expectedCalls, List> actualCalls)' + ); + } else { + final Integer expectedSize = expectedCalls.size(); + System.assertEquals( + expectedSize, + actualCalls.size(), + 'Lists of Calls should have the same size' + ); + for (Integer callIndex = 0; callIndex < expectedSize; callIndex++) { + final List expectedCall = expectedCalls[callIndex]; + final List actualCall = actualCalls[callIndex]; + + if (expectedCall == null || actualCall == null) { + System.assertEquals( + expectedCall, + actualCall, + String.format( + 'Call at index [{0}]', + new List{ String.valueOf(callIndex) } + ) + ); + } else { + final Integer argumentsSize = expectedCall.size(); + System.assertEquals( + argumentsSize, + actualCall.size(), + String.format( + 'Call at index [{0}] should have the same size', + new List{ String.valueOf(callIndex) } + ) + ); + for ( + Integer argumentIndex = 0; + argumentIndex < argumentsSize; + argumentIndex++ + ) { + System.assertEquals( + expectedCall[argumentIndex], + actualCall[argumentIndex], + String.format( + 'Argument at index [{1}] of Call at index [{0}] should equal', + new List{ + String.valueOf(callIndex), + String.valueOf(argumentIndex) + } + ) + ); + } + } + } + } + } + + public static void assertCallEquals( + List expectedCall, + List actualCall + ) { + if (expectedCall == null || expectedCall == null) { + System.assertEquals( + expectedCall, + actualCall, + 'assertCallsEquals(List expectedCall, List actualCall)' + ); + } else { + final Integer argumentsSize = expectedCall.size(); + System.assertEquals( + argumentsSize, + actualCall.size(), + 'Call should have the same size' + ); + for ( + Integer argumentIndex = 0; argumentIndex < argumentsSize; argumentIndex++ + ) { + System.assertEquals( + expectedCall[argumentIndex], + actualCall[argumentIndex], + String.format( + 'Argument at index [{0}] of Call should equal', + new List{ String.valueOf(argumentIndex) } + ) + ); + } + } + } + + /** + * @param e + * @return Human readable message for debugging Exceptions. + */ + public static String debug(Exception e) { + if (e == null) { + return 'null'; + } + return 'Exception:\n' + + String.join( + new List{ + DEBUG_DELIMITER + + 'TypeName: ' + + e.getTypeName(), + DEBUG_DELIMITER + + 'Message: ' + + e.getMessage() + }, + '\n' + ); + } + + /** + * @return Human readable message for debugging method calls. + */ + public static String debug( + final Object instance, + final String methodName, + final Type returnType, + final List parameterTypes, + final List parameterNames, + final List arguments + ) { + return 'Method Call:\n' + + String.join( + new List{ + DEBUG_DELIMITER + + 'methodName: ' + + methodName, + DEBUG_DELIMITER + + 'returnType: ' + + returnType, + DEBUG_DELIMITER + + 'parameterTypes: ' + + parameterTypes, + DEBUG_DELIMITER + + 'parameterNames: ' + + parameterNames, + DEBUG_DELIMITER + + 'arguments: ' + + arguments + }, + '\n' + ); + } +} diff --git a/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls-meta.xml b/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls-meta.xml new file mode 100644 index 0000000..db9bf8c --- /dev/null +++ b/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 48.0 + Active + diff --git a/force-app/main/default/labels/CustomLabels.labels-meta.xml b/force-app/main/default/labels/CustomLabels.labels-meta.xml new file mode 100644 index 0000000..9c45bf4 --- /dev/null +++ b/force-app/main/default/labels/CustomLabels.labels-meta.xml @@ -0,0 +1,27 @@ + + + + DatabaseService_NoDeleteAccessException + DatabaseService,Exception + en_US + true + The user does not have delete access to the object. + You don’t have delete access to this object. Ask your Salesforce admin for help. + + + DatabaseService_NoUpsertFlsException + Error Handling + en_US + true + The user does not have insert or edit/update access to required fields. + You don’t have field-level access to insert/edit this record. Contact your system administrator for help. + + + DatabaseService_NoUpdateFlsException + Error Handling + en_US + true + The user does not have edit or update access to required fields. + You don’t have field-level access to edit this record. Contact your system administrator for help. + + From 54254ac9709c47633387294ad16eaec4d7cc0832 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 4 Feb 2021 11:49:48 -0500 Subject: [PATCH 02/24] Add DatabaseService Added DatabaseService though tests are unfinised --- .../services/Database/DatabaseService.cls | 116 ++++++++++++++++++ .../Database/DatabaseService.cls-meta.xml | 5 + .../Database/tests/DatabaseService_TEST.cls | 15 +++ .../tests/DatabaseService_TEST.cls-meta.xml | 5 + 4 files changed, 141 insertions(+) create mode 100644 force-app/main/default/classes/services/Database/DatabaseService.cls create mode 100644 force-app/main/default/classes/services/Database/DatabaseService.cls-meta.xml create mode 100644 force-app/main/default/classes/services/Database/tests/DatabaseService_TEST.cls create mode 100644 force-app/main/default/classes/services/Database/tests/DatabaseService_TEST.cls-meta.xml diff --git a/force-app/main/default/classes/services/Database/DatabaseService.cls b/force-app/main/default/classes/services/Database/DatabaseService.cls new file mode 100644 index 0000000..fc09c3f --- /dev/null +++ b/force-app/main/default/classes/services/Database/DatabaseService.cls @@ -0,0 +1,116 @@ +public with sharing class DatabaseService { + public List insertRecordsEnforceFls(List records) { + if (records == null || records.isEmpty()) { + return new List(); + } + + // Security.stripInaccessible verifies object-level permissions. + System.SObjectAccessDecision accessDecision = Security.stripInaccessible( + System.AccessType.CREATABLE, + records, + true + ); + + // Enforce all field-level permissions by checking if any fields were removed in the accessDecision implying the user does not have the proper field-level permission for this operation. + if (!accessDecision.getRemovedFields().isEmpty()) { + System.NoAccessException e = new System.NoAccessException(); + e.setMessage(System.Label.DatabaseService_NoUpdateFlsException); + } + + // Database.insert should set IDs on records. + return Database.insert(records); + } + + public List updateRecordsEnforceFls(List records) { + if (records == null || records.isEmpty()) { + return new List(); + } + + // Security.stripInaccessible verifies object-level permissions. + System.SObjectAccessDecision accessDecision = Security.stripInaccessible( + System.AccessType.UPDATABLE, + records, + true + ); + + // Enforce all field-level permissions by checking if any fields were removed in the accessDecision implying the user does not have the proper field-level permission for this operation. + if (!accessDecision.getRemovedFields().isEmpty()) { + System.NoAccessException e = new System.NoAccessException(); + e.setMessage(System.Label.DatabaseService_NoUpdateFlsException); + } + + return Database.update(accessDecision.getRecords()); + } + + public List upsertRecordsEnforceFls(List records) { + if (records == null || records.isEmpty()) { + return new List(); + } + + // Security.stripInaccessible verifies object-level permissions. + System.SObjectAccessDecision accessDecision = Security.stripInaccessible( + System.AccessType.UPSERTABLE, + records, + true + ); + + // Enforce all field-level permissions by checking if any fields were removed in the accessDecision implying the user does not have the proper field-level permission for this operation. + if (!accessDecision.getRemovedFields().isEmpty()) { + System.NoAccessException e = new System.NoAccessException(); + e.setMessage(System.Label.DatabaseService_NoUpsertFlsException); + } + + return Database.upsert(accessDecision.getRecords()); + } + + public List deleteRecords(List records) { + if (records == null || records.isEmpty()) { + return new List(); + } + + final Schema.DescribeSObjectResult describe = records.getSObjectType() + .getDescribe(); + + if (!describe.isDeletable()) { + System.NoAccessException e = new System.NoAccessException(); + e.setMessage(System.Label.DatabaseService_NoDeleteAccessException); + } + + return Database.delete(records); + } + + /** + * Stubbable "wrapper" of System.Savepoint. Instances are constructed with a + * null System.Savepoint, so instances can be safely stubbed in unit tests. + */ + public with sharing class Savepoint { + @TestVisible + private System.Savepoint savepoint; + + /** + * Private access so production code cannot construct this class. + * Production code must go through DatabaseService.Savepoint setSavepoint(). + */ + @TestVisible + private Savepoint() { + } + } + + /** + * This is the only way to get a DatabaseService.Savepoint in production code. + * @return A DatabaseService.Savepoint with its System.Savepoint set. + */ + public DatabaseService.Savepoint setSavepoint() { + final DatabaseService.Savepoint savepoint = new DatabaseService.Savepoint(); + savepoint.savepoint = System.Database.setSavepoint(); + return savepoint; + } + + /** + * Rolls back to the savepoint's System.Savepoint. + * @param savepoint Pass in a DatabaseService.Savepoint returned by setSavepoint(). + */ + public void rollback(DatabaseService.Savepoint savepoint) { + System.Database.rollback(savepoint.savepoint); + } +} diff --git a/force-app/main/default/classes/services/Database/DatabaseService.cls-meta.xml b/force-app/main/default/classes/services/Database/DatabaseService.cls-meta.xml new file mode 100644 index 0000000..541584f --- /dev/null +++ b/force-app/main/default/classes/services/Database/DatabaseService.cls-meta.xml @@ -0,0 +1,5 @@ + + + 50.0 + Active + diff --git a/force-app/main/default/classes/services/Database/tests/DatabaseService_TEST.cls b/force-app/main/default/classes/services/Database/tests/DatabaseService_TEST.cls new file mode 100644 index 0000000..43e1f20 --- /dev/null +++ b/force-app/main/default/classes/services/Database/tests/DatabaseService_TEST.cls @@ -0,0 +1,15 @@ +@isTest +public with sharing class DatabaseService_TEST { + @isTest + private static void test() { + try { + if (true) { + Exception e = new System.NoAccessException(); + e.setMessage(System.Label.DatabaseService_NoDeleteAccessException); + throw e; + } + } catch (Exception e) { + System.assert(false, e.getMessage()); + } + } +} diff --git a/force-app/main/default/classes/services/Database/tests/DatabaseService_TEST.cls-meta.xml b/force-app/main/default/classes/services/Database/tests/DatabaseService_TEST.cls-meta.xml new file mode 100644 index 0000000..541584f --- /dev/null +++ b/force-app/main/default/classes/services/Database/tests/DatabaseService_TEST.cls-meta.xml @@ -0,0 +1,5 @@ + + + 50.0 + Active + From db2049181325badced82fd1ba95306a4933fb62e Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Mon, 8 Feb 2021 17:04:12 -0500 Subject: [PATCH 03/24] Setting up services for manageExpenditures Setting up services for Apex related to the manageExpenditures LWC. - Adding `DatabaseService` with associated Custom Labels. - Adding `testUtils` services including `UnitTest`. - Adding `GauExpenditure` Services. - Moving @AuraEnabled controllers to `*/classes/controllers` --- .../GauExpendituresManager.cls | 46 ++++++++----------- .../GauExpendituresManager.cls-meta.xml | 0 .../GauLookupController.cls | 0 .../GauLookupController.cls-meta.xml | 0 .../LookupSearchResult.cls | 0 .../LookupSearchResult.cls-meta.xml | 0 .../tests}/GauExpendituresManagerTest.cls | 0 .../GauExpendituresManagerTest.cls-meta.xml | 0 .../tests}/GauLookupControllerTest.cls | 0 .../GauLookupControllerTest.cls-meta.xml | 0 .../services/Database/DatabaseService.cls | 29 +++--------- .../GauExpenditure/GauExpenditureSelector.cls | 26 +++++++++++ .../GauExpenditureSelector.cls-meta.xml | 5 ++ .../classes/testUtils/UnitTest/UnitTest.cls | 4 -- .../labels/CustomLabels.labels-meta.xml | 24 +++++----- 15 files changed, 67 insertions(+), 67 deletions(-) rename force-app/main/default/classes/{ => controllers/manageExpenditures}/GauExpendituresManager.cls (87%) rename force-app/main/default/classes/{ => controllers/manageExpenditures}/GauExpendituresManager.cls-meta.xml (100%) rename force-app/main/default/classes/{ => controllers/manageExpenditures}/GauLookupController.cls (100%) rename force-app/main/default/classes/{ => controllers/manageExpenditures}/GauLookupController.cls-meta.xml (100%) rename force-app/main/default/classes/{ => controllers/manageExpenditures}/LookupSearchResult.cls (100%) rename force-app/main/default/classes/{ => controllers/manageExpenditures}/LookupSearchResult.cls-meta.xml (100%) rename force-app/main/default/classes/{ => controllers/manageExpenditures/tests}/GauExpendituresManagerTest.cls (100%) rename force-app/main/default/classes/{ => controllers/manageExpenditures/tests}/GauExpendituresManagerTest.cls-meta.xml (100%) rename force-app/main/default/classes/{ => controllers/manageExpenditures/tests}/GauLookupControllerTest.cls (100%) rename force-app/main/default/classes/{ => controllers/manageExpenditures/tests}/GauLookupControllerTest.cls-meta.xml (100%) create mode 100644 force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls create mode 100644 force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls-meta.xml diff --git a/force-app/main/default/classes/GauExpendituresManager.cls b/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls similarity index 87% rename from force-app/main/default/classes/GauExpendituresManager.cls rename to force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls index d111b60..fa514db 100755 --- a/force-app/main/default/classes/GauExpendituresManager.cls +++ b/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls @@ -8,6 +8,9 @@ * not in that list should be deleted, since they've been deleted in the UI. */ public with sharing class GauExpendituresManager { + @TestVisible + private static GanExpenditureSelector gauExpenditureSelector = new GanExpenditureSelector(); + /***************************************************************************** * @description using a disbursementId (Passed from a lightning record page), return a wrapper for the disbursement object. * One of the properties of this wrapper is a list of wrappers for the GAU Expenditure object, with the disbursements children. @@ -20,36 +23,23 @@ public with sharing class GauExpendituresManager { */ @AuraEnabled public static DisbursementWrapper getDisbursement(String disbursementId) { - List disbursements = [ - SELECT - Id, - Name, - outfunds__Amount__c, - outfunds__Status__c, - ( - SELECT - Id, - General_Accounting_Unit__c, - General_Accounting_Unit__r.Name, - General_Accounting_Unit__r.npsp__Active__c, - Amount__c - FROM GAU_Expendatures__r - ORDER BY CreatedDate ASC - LIMIT 200 - ) - FROM outfunds__Disbursement__c - WHERE Id = :disbursementId - LIMIT 1 - ]; - outfunds__Disbursement__c disbursement = disbursements.size() == 1 - ? disbursements.get(0) - : null; + try { + List disbursements = gauExpenditureSelector.getDisbursementsById( + disbursementId + ); - if (disbursement == null) { - return null; - } + outfunds__Disbursement__c disbursement = disbursements.size() == 1 + ? disbursements.get(0) + : null; + + if (disbursement == null) { + return null; + } - return new DisbursementWrapper(disbursement); + return new DisbursementWrapper(disbursement); + } catch (Exception e) { + // throw AuraHandledException. + } } /***************************************************************************** * @description Receive stringified version of the gau expenditures, with the id of the parent. Upsert and delete children as needed. diff --git a/force-app/main/default/classes/GauExpendituresManager.cls-meta.xml b/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls-meta.xml similarity index 100% rename from force-app/main/default/classes/GauExpendituresManager.cls-meta.xml rename to force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls-meta.xml diff --git a/force-app/main/default/classes/GauLookupController.cls b/force-app/main/default/classes/controllers/manageExpenditures/GauLookupController.cls similarity index 100% rename from force-app/main/default/classes/GauLookupController.cls rename to force-app/main/default/classes/controllers/manageExpenditures/GauLookupController.cls diff --git a/force-app/main/default/classes/GauLookupController.cls-meta.xml b/force-app/main/default/classes/controllers/manageExpenditures/GauLookupController.cls-meta.xml similarity index 100% rename from force-app/main/default/classes/GauLookupController.cls-meta.xml rename to force-app/main/default/classes/controllers/manageExpenditures/GauLookupController.cls-meta.xml diff --git a/force-app/main/default/classes/LookupSearchResult.cls b/force-app/main/default/classes/controllers/manageExpenditures/LookupSearchResult.cls similarity index 100% rename from force-app/main/default/classes/LookupSearchResult.cls rename to force-app/main/default/classes/controllers/manageExpenditures/LookupSearchResult.cls diff --git a/force-app/main/default/classes/LookupSearchResult.cls-meta.xml b/force-app/main/default/classes/controllers/manageExpenditures/LookupSearchResult.cls-meta.xml similarity index 100% rename from force-app/main/default/classes/LookupSearchResult.cls-meta.xml rename to force-app/main/default/classes/controllers/manageExpenditures/LookupSearchResult.cls-meta.xml diff --git a/force-app/main/default/classes/GauExpendituresManagerTest.cls b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManagerTest.cls similarity index 100% rename from force-app/main/default/classes/GauExpendituresManagerTest.cls rename to force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManagerTest.cls diff --git a/force-app/main/default/classes/GauExpendituresManagerTest.cls-meta.xml b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManagerTest.cls-meta.xml similarity index 100% rename from force-app/main/default/classes/GauExpendituresManagerTest.cls-meta.xml rename to force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManagerTest.cls-meta.xml diff --git a/force-app/main/default/classes/GauLookupControllerTest.cls b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauLookupControllerTest.cls similarity index 100% rename from force-app/main/default/classes/GauLookupControllerTest.cls rename to force-app/main/default/classes/controllers/manageExpenditures/tests/GauLookupControllerTest.cls diff --git a/force-app/main/default/classes/GauLookupControllerTest.cls-meta.xml b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauLookupControllerTest.cls-meta.xml similarity index 100% rename from force-app/main/default/classes/GauLookupControllerTest.cls-meta.xml rename to force-app/main/default/classes/controllers/manageExpenditures/tests/GauLookupControllerTest.cls-meta.xml diff --git a/force-app/main/default/classes/services/Database/DatabaseService.cls b/force-app/main/default/classes/services/Database/DatabaseService.cls index fc09c3f..93434e2 100644 --- a/force-app/main/default/classes/services/Database/DatabaseService.cls +++ b/force-app/main/default/classes/services/Database/DatabaseService.cls @@ -14,7 +14,8 @@ public with sharing class DatabaseService { // Enforce all field-level permissions by checking if any fields were removed in the accessDecision implying the user does not have the proper field-level permission for this operation. if (!accessDecision.getRemovedFields().isEmpty()) { System.NoAccessException e = new System.NoAccessException(); - e.setMessage(System.Label.DatabaseService_NoUpdateFlsException); + e.setMessage(System.Label.DatabaseService_NoInsertFlsException); + throw e; } // Database.insert should set IDs on records. @@ -37,30 +38,10 @@ public with sharing class DatabaseService { if (!accessDecision.getRemovedFields().isEmpty()) { System.NoAccessException e = new System.NoAccessException(); e.setMessage(System.Label.DatabaseService_NoUpdateFlsException); + throw e; } - return Database.update(accessDecision.getRecords()); - } - - public List upsertRecordsEnforceFls(List records) { - if (records == null || records.isEmpty()) { - return new List(); - } - - // Security.stripInaccessible verifies object-level permissions. - System.SObjectAccessDecision accessDecision = Security.stripInaccessible( - System.AccessType.UPSERTABLE, - records, - true - ); - - // Enforce all field-level permissions by checking if any fields were removed in the accessDecision implying the user does not have the proper field-level permission for this operation. - if (!accessDecision.getRemovedFields().isEmpty()) { - System.NoAccessException e = new System.NoAccessException(); - e.setMessage(System.Label.DatabaseService_NoUpsertFlsException); - } - - return Database.upsert(accessDecision.getRecords()); + return Database.update(records); } public List deleteRecords(List records) { @@ -68,12 +49,14 @@ public with sharing class DatabaseService { return new List(); } + // TODO: What do we do if records contains more than one SobjectType? final Schema.DescribeSObjectResult describe = records.getSObjectType() .getDescribe(); if (!describe.isDeletable()) { System.NoAccessException e = new System.NoAccessException(); e.setMessage(System.Label.DatabaseService_NoDeleteAccessException); + throw e; } return Database.delete(records); diff --git a/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls b/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls new file mode 100644 index 0000000..6888b31 --- /dev/null +++ b/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls @@ -0,0 +1,26 @@ +public with sharing class GauExpenditureSelector { + public List getDisbursementsById(String disbursementId) { + return [ + SELECT + Id, + Name, + outfunds__Amount__c, + outfunds__Status__c, + ( + SELECT + Id, + General_Accounting_Unit__c, + General_Accounting_Unit__r.Name, + General_Accounting_Unit__r.npsp__Active__c, + Amount__c + FROM GAU_Expendatures__r + ORDER BY CreatedDate ASC + LIMIT 200 + ) + FROM outfunds__Disbursement__c + WHERE Id = :disbursementId + WITH SECURITY_ENFORCED + LIMIT 1 + ]; + } +} diff --git a/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls-meta.xml b/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls-meta.xml new file mode 100644 index 0000000..541584f --- /dev/null +++ b/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls-meta.xml @@ -0,0 +1,5 @@ + + + 50.0 + Active + diff --git a/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls b/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls index 75901f5..cb84f52 100644 --- a/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls +++ b/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls @@ -1,7 +1,3 @@ -/** - * TODO: Add documentation. - * TODO: Add UnitTest_TEST with examples showing how to UnitTest with best practices. - */ @IsTest public with sharing class UnitTest { private static Integer MOCK_ID_INDEX = 0; diff --git a/force-app/main/default/labels/CustomLabels.labels-meta.xml b/force-app/main/default/labels/CustomLabels.labels-meta.xml index 9c45bf4..b8be0d8 100644 --- a/force-app/main/default/labels/CustomLabels.labels-meta.xml +++ b/force-app/main/default/labels/CustomLabels.labels-meta.xml @@ -1,27 +1,27 @@ - DatabaseService_NoDeleteAccessException + DatabaseService_NoInsertFlsException DatabaseService,Exception en_US true - The user does not have delete access to the object. - You don’t have delete access to this object. Ask your Salesforce admin for help. - + The user does not have create access to fields required for this process. + You don't have field-level access to create this record. Contact your system administrator for help. + - DatabaseService_NoUpsertFlsException - Error Handling + DatabaseService_NoUpdateFlsException + DatabaseService,Exception en_US true - The user does not have insert or edit/update access to required fields. - You don’t have field-level access to insert/edit this record. Contact your system administrator for help. + The user does not have edit or update access to required fields. + You don't have field-level access to edit this record. Contact your system administrator for help. - DatabaseService_NoUpdateFlsException - Error Handling + DatabaseService_NoDeleteAccessException + DatabaseService,Exception en_US true - The user does not have edit or update access to required fields. - You don’t have field-level access to edit this record. Contact your system administrator for help. + The user does not have delete access to the object. + You don't have delete access to this object. Ask your Salesforce admin for help. From d2209bbc01f8fcc8caafe3e2568937055151a6db Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Mon, 8 Feb 2021 17:31:04 -0500 Subject: [PATCH 04/24] Using selector to query data - `GauExpenditureSelector.getDisbursementsById` - `GauExpenditureSelector.getExpendituresToDelete` Wrapping @AuraHandled components in try/catch --- .../GauExpendituresManager.cls | 164 ++++++++---------- .../GauExpenditure/GauExpenditureSelector.cls | 31 +++- 2 files changed, 102 insertions(+), 93 deletions(-) diff --git a/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls b/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls index fa514db..a8dfa04 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls +++ b/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls @@ -1,8 +1,5 @@ -/******************************************************************************* - * @author Thom Behrens - * @date 2020-01-25 - * - * @description Class made in support of the manageExpenditures lwc. selector and update method made so that as much +/** + * Class made in support of the manageExpenditures lwc. selector and update method made so that as much * logic as possible can be handle on this end, and as such, some assumptions about what is being passed from the UI * are made. E.g. when a list of GAU Expenditures is received along with a disbursement Id, we assume that any expenditures * not in that list should be deleted, since they've been deleted in the UI. @@ -11,38 +8,38 @@ public with sharing class GauExpendituresManager { @TestVisible private static GanExpenditureSelector gauExpenditureSelector = new GanExpenditureSelector(); - /***************************************************************************** - * @description using a disbursementId (Passed from a lightning record page), return a wrapper for the disbursement object. + @TestVisible + private static DatabaseService databaseService = new DatabaseService(); + + /** + * Using a disbursementId (Passed from a lightning record page), return a wrapper for the disbursement object. * One of the properties of this wrapper is a list of wrappers for the GAU Expenditure object, with the disbursements children. - * @param String disbursementId - * @return DisbursementWrapper - * @example - * GauExpendituresManager.DisbursementWrapper queriedDisbursement = GauExpendituresManager.getDisbursement( - * String.valueOf(disbursement.Id) - * ); + * @param disbursementId disbursementId description + * @return DisbursementWrapper of a outfunds__Disbursement__c. */ @AuraEnabled public static DisbursementWrapper getDisbursement(String disbursementId) { try { - List disbursements = gauExpenditureSelector.getDisbursementsById( + final List disbursements = GauExpendituresManager.gauExpenditureSelector.getDisbursementsById( disbursementId ); - outfunds__Disbursement__c disbursement = disbursements.size() == 1 - ? disbursements.get(0) - : null; - - if (disbursement == null) { + if (disbursements.isEmpty()) { return null; } - - return new DisbursementWrapper(disbursement); + return new DisbursementWrapper(disbursements[0]); } catch (Exception e) { - // throw AuraHandledException. + // Re-throw as an AuraHandledException that Lightning Components can handle. + final System.AuraHandledException auraHandledException = new System.AuraHandledException( + e.getMessage() + ); + auraHandledException.setMessage(e.getMessage()); + throw auraHandledException; } } + /***************************************************************************** - * @description Receive stringified version of the gau expenditures, with the id of the parent. Upsert and delete children as needed. + * @description * @param String expenditureString * @param String disbursementId * @return void @@ -52,82 +49,70 @@ public with sharing class GauExpendituresManager { * disbursement.Id * ); */ + /** + * Receives a JSON serialized version of the GAU Expenditures, with the id of the parent. Upsert and delete children as needed. + */ @AuraEnabled public static void upsertGauExpenditures( String expendituresString, String disbursementId ) { - List expenditureWrappers = (List) JSON.deserialize( - expendituresString, - List.class - ); - List expenditures = new List(); - for (GauExpenditureWrapper expenditureWrapper : expenditureWrappers) { - expenditures.add( - new GAU_Expenditure__c( + final DatabaseService.Savepoint savepoint = GauExpendituresManager.databaseService.setSavepoint(); + + try { + // Deserialize expendituresString and split into records to insert and update. + final List expendituresToInsert = new List(); + final Map expendituresToUpdate = new Map(); + + for ( + GauExpendituresManager.GauExpenditureWrapper expenditureWrapper : (List) JSON.deserialize( + expendituresString, + List.class + ) + ) { + final Schema.GAU_Expenditure__c expenditure = new Schema.GAU_Expenditure__c( Id = expenditureWrapper.recordId, Disbursement__c = disbursementId, General_Accounting_Unit__c = expenditureWrapper.gauId, Amount__c = expenditureWrapper.amount + ); + + if (expenditure.Id == null) { + expendituresToInsert.add(expenditure); + } else { + expendituresToUpdate.put(expenditure.Id, expenditure); + } + } + + // Delete child records not contained in expendituresString. + GauExpendituresManager.databaseService.deleteRecords( + GauExpendituresManager.gauExpenditureSelector.getExpendituresToDelete( + disbursementId ) ); - } - List expendituresToDelete = queryDeletedExpenditures( - expenditures, - disbursementId - ); - Savepoint sp = Database.setSavepoint(); - try { - delete expendituresToDelete; - upsert expenditures; + + // Upsert records in expendituresString. + GauExpendituresManager.databaseService.insertRecordsEnforceFls( + expendituresToInsert + ); + GauExpendituresManager.databaseService.updateRecordsEnforceFls( + expendituresToUpdate + ); } catch (Exception e) { - Database.rollback(sp); - if (Test.isRunningTest()) { - throw new ConstructedException(e.getMessage()); - } else { - throw new AuraHandledException(e.getMessage()); - } - } - } - /***************************************************************************** - * @description deduce which expenditures should be deleted by querying all expenditures, and checking to see which - * ones are not in the list of those to be preserved. - * @param List remainingExpenditures - * @param String disbursementId - * @return List - records to be deleted - * @example - * List expendituresToDelete = queryDeletedExpenditures( - * expenditures, - * disbursementId - * ); - */ - private static List queryDeletedExpenditures( - List remainingExpenditures, - String disbursementId - ) { - Set remainingExpenditureIds = new Set(); - for (GAU_Expenditure__c expenditure : remainingExpenditures) { - remainingExpenditureIds.add(expenditure.Id); - } - List allExpenditures = [ - SELECT Id - FROM GAU_Expenditure__c - WHERE Disbursement__c = :disbursementId - ]; - List expendituresToDelete = new List(); - for (GAU_Expenditure__c expenditure : allExpenditures) { - if (!remainingExpenditureIds.contains(expenditure.Id)) { - expendituresToDelete.add(expenditure); - } + // Rollback transaction. + GauExpendituresManager.databaseService.rollback(savepoint); + + // Re-throw as an AuraHandledException that Lightning Components can handle. + final System.AuraHandledException auraHandledException = new System.AuraHandledException( + e.getMessage() + ); + auraHandledException.setMessage(e.getMessage()); + throw auraHandledException; } - return expendituresToDelete; } - /******************************************************************************* - * @author Thom Behrens - * @date 2020-01-25 - * - * @description Wrapper class for the outfunds__Disbursement__c object + /** + * Wrapper class for the outfunds__Disbursement__c object */ public class DisbursementWrapper { @AuraEnabled @@ -152,11 +137,9 @@ public with sharing class GauExpendituresManager { } } } - /******************************************************************************* - * @author Thom Behrens - * @date 2020-01-25 - * - * @description Wrapper class for the GAU_Expenditure__c object. + + /** + * Wrapper class for the GAU_Expenditure__c object. */ public class GauExpenditureWrapper { @AuraEnabled @@ -182,7 +165,4 @@ public with sharing class GauExpendituresManager { this.rowId = rowId; } } - - public class ConstructedException extends Exception { - } } diff --git a/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls b/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls index 6888b31..08c64f7 100644 --- a/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls +++ b/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls @@ -1,5 +1,7 @@ public with sharing class GauExpenditureSelector { - public List getDisbursementsById(String disbursementId) { + public List getDisbursementsById( + String disbursementId + ) { return [ SELECT Id, @@ -23,4 +25,31 @@ public with sharing class GauExpenditureSelector { LIMIT 1 ]; } + + /** + * Deduce which expenditures should be deleted by querying all expenditures, and checking to see which ones are not in the list of those to be preserved. + * @return return description + */ + public List getExpendituresToDelete( + String disbursementId, + Set expenditureIdsToKeep + ) { + final Set normalizedExpenditureIdsToKeep = new Set(); + if (expenditureIdsToKeep != null) { + normalizedExpenditureIdsToKeep.addAll(expenditureIdsToKeep); + } + + final List expendituresToDelete = new List(); + for (Schema.GAU_Expenditure__c expenditure : [ + SELECT Id + FROM GAU_Expenditure__c + WHERE Disbursement__c = :disbursementId + WITH SECURITY_ENFORCED + ]) { + if (!normalizedExpenditureIdsToKeep.contains(expenditure.Id)) { + expendituresToDelete.add(expenditure); + } + } + return expendituresToDelete; + } } From 8e304cadfdc21eb96f527c6cefbf84aea23e4000 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Wed, 10 Feb 2021 08:42:05 -0500 Subject: [PATCH 05/24] Tested DatabaseService - Added `TestUser` to create test users and easily modify object-level and field-level permissions before a test. - Fully tested `DatabaseService`. --- .../Database/tests/DatabaseService_TEST.cls | 905 +++++++++++- .../classes/testUtils/TestUser/TestUser.cls | 1313 +++++++++++++++++ .../testUtils/TestUser/TestUser.cls-meta.xml | 5 + 3 files changed, 2214 insertions(+), 9 deletions(-) create mode 100644 force-app/main/default/classes/testUtils/TestUser/TestUser.cls create mode 100644 force-app/main/default/classes/testUtils/TestUser/TestUser.cls-meta.xml diff --git a/force-app/main/default/classes/services/Database/tests/DatabaseService_TEST.cls b/force-app/main/default/classes/services/Database/tests/DatabaseService_TEST.cls index 43e1f20..3b73e58 100644 --- a/force-app/main/default/classes/services/Database/tests/DatabaseService_TEST.cls +++ b/force-app/main/default/classes/services/Database/tests/DatabaseService_TEST.cls @@ -1,15 +1,902 @@ @isTest public with sharing class DatabaseService_TEST { - @isTest - private static void test() { - try { - if (true) { - Exception e = new System.NoAccessException(); - e.setMessage(System.Label.DatabaseService_NoDeleteAccessException); - throw e; + private static TestUser.MinimalAccessPersona minimalAccessPersona = new TestUser.MinimalAccessPersona(); + + private static Account getAccount() { + return [SELECT Id, AccountNumber FROM Account LIMIT 1][0]; + } + + @TestSetup + public static void testSetup() { + TestUser.insertPersonasInTestSetup( + new List{ DatabaseService_TEST.minimalAccessPersona } + ); + + DatabaseService_TEST.minimalAccessPersona.load(); + + insert new Account( + Name = 'Pi (approximately)', + AccountNumber = '3.14159', + // Owner will grant all sharing access to the record so we can test object-level permissions. + OwnerId = DatabaseService_TEST.minimalAccessPersona.getUser().Id + ); + } + + @IsTest + private static void testInsertRecordsEnforceFls_NoObjectLevelPermission() { + // Load TestUser.Persona and assert the object-level and field-level are as expected for this test. + DatabaseService_TEST.minimalAccessPersona.load(); + + // Set arguments. + final List records = new List{ + new Account(Name = 'Account 0', AccountNumber = '0'), + new Account(Name = 'Account 1', AccountNumber = '1') + }; + + Test.startTest(); + + Exception actualException; + + System.runAs(DatabaseService_TEST.minimalAccessPersona.getUser()) { + // Assert permissions are as expected. + System.assert( + !Schema.Account.SObjectType.getDescribe().isCreateable(), + 'DatabaseService_TEST.minimalAccessPersona should not be able to create Account.' + ); + + final DatabaseService service = new DatabaseService(); + + // Null records. + final List actualWhenNullRecords = service.insertRecordsEnforceFls( + (List) null + ); + System.assertNotEquals( + null, + actualWhenNullRecords, + 'DatabaseService.insertRecordsEnforceFls should never return null.' + ); + System.assert( + actualWhenNullRecords.isEmpty(), + 'DatabaseService.insertRecordsEnforceFls should always return an empty list whenever the records argument is null. CRUD or FLS is not checked.' + ); + + final List actualWhenEmptyRecords = service.insertRecordsEnforceFls( + new List() + ); + System.assertNotEquals( + null, + actualWhenEmptyRecords, + 'DatabaseService.insertRecordsEnforceFls should never return null.' + ); + System.assert( + actualWhenEmptyRecords.isEmpty(), + 'DatabaseService.insertRecordsEnforceFls should always return an empty list whenever the records argument is empty. CRUD or FLS is not checked.' + ); + + try { + service.insertRecordsEnforceFls(records); + } catch (Exception e) { + actualException = e; + } + } + + Test.stopTest(); + + System.assert( + actualException instanceof System.NoAccessException, + 'DatabaseService.insertRecordsEnforceFls should throw a System.NoAccessException since the user does not have Create object-level access on Account. Actual Exception: ' + + actualException + ); + } + + @IsTest + private static void testInsertRecordsEnforceFls_MissingFieldLevelPermission() { + // Load TestUser.Persona and assert the object-level and field-level are as expected for this test. + DatabaseService_TEST.minimalAccessPersona.load(); + + TestUser.PermissionSetManager permissionSetManager = DatabaseService_TEST.minimalAccessPersona.getPermissionSetManager(); + + TestUser.ObjectPermission objectPermission = permissionSetManager.getObjectPermission( + Schema.SObjectType.Account + ) + .setRead(true) + .setCreate(true); + + TestUser.FieldPermission fieldPermission = objectPermission.getFieldPermission( + Schema.SObjectType.Account.fields.AccountNumber + ) + .setRead(false) + .setEdit(false); + + permissionSetManager.save(); + + // Set arguments. + final List records = new List{ + new Account(Name = 'Account 0', AccountNumber = '0'), + new Account(Name = 'Account 1', AccountNumber = '1') + }; + + Test.startTest(); + + Exception actualException; + + System.runAs(DatabaseService_TEST.minimalAccessPersona.getUser()) { + // Assert permissions are as expected. + System.assert( + Schema.Account.SObjectType.getDescribe().isCreateable(), + 'DatabaseService_TEST.minimalAccessPersona should be able to create Account.' + ); + + System.assert( + !Schema.Account.SObjectType.fields.AccountNumber.getDescribe() + .isCreateable(), + 'DatabaseService_TEST.minimalAccessPersona should not be able to create Account.AccountNumber.' + ); + + final DatabaseService service = new DatabaseService(); + + // Null records. + final List actualWhenNullRecords = service.insertRecordsEnforceFls( + (List) null + ); + System.assertNotEquals( + null, + actualWhenNullRecords, + 'DatabaseService.insertRecordsEnforceFls should never return null.' + ); + System.assert( + actualWhenNullRecords.isEmpty(), + 'DatabaseService.insertRecordsEnforceFls should always return an empty list whenever the records argument is null. CRUD or FLS is not checked.' + ); + + final List actualWhenEmptyRecords = service.insertRecordsEnforceFls( + new List() + ); + System.assertNotEquals( + null, + actualWhenEmptyRecords, + 'DatabaseService.insertRecordsEnforceFls should never return null.' + ); + System.assert( + actualWhenEmptyRecords.isEmpty(), + 'DatabaseService.insertRecordsEnforceFls should always return an empty list whenever the records argument is empty. CRUD or FLS is not checked.' + ); + + try { + service.insertRecordsEnforceFls(records); + } catch (Exception e) { + actualException = e; + } + } + + Test.stopTest(); + + System.assert( + actualException instanceof System.NoAccessException, + 'DatabaseService.insertRecordsEnforceFls should throw a System.NoAccessException since the user does not have Edit field-level access on Account.AccountNumber. Actual Exception: ' + + actualException + ); + System.assertEquals( + System.Label.DatabaseService_NoInsertFlsException, + actualException.getMessage(), + 'DatabaseService.insertRecordsEnforceFls should throw a System.NoAccessException whose message equals System.Label.DatabaseService_NoInsertFlsException ("' + + System.Label.DatabaseService_NoInsertFlsException + + '").' + ); + } + + @IsTest + private static void testInsertRecordsEnforceFls_WithFieldLevelPermission() { + // Load TestUser.Persona and assert the object-level and field-level are as expected for this test. + DatabaseService_TEST.minimalAccessPersona.load(); + + TestUser.PermissionSetManager permissionSetManager = DatabaseService_TEST.minimalAccessPersona.getPermissionSetManager(); + + TestUser.ObjectPermission objectPermission = permissionSetManager.getObjectPermission( + Schema.SObjectType.Account + ) + .setRead(true) + .setCreate(true); + + TestUser.FieldPermission fieldPermission = objectPermission.getFieldPermission( + Schema.SObjectType.Account.fields.AccountNumber + ) + .setRead(true) + .setEdit(true); + + permissionSetManager.save(); + + // Set arguments. + final List records = new List{ + new Account(Name = 'Account 0', AccountNumber = '0'), + new Account(Name = 'Account 1', AccountNumber = '1') + }; + + Test.startTest(); + + List actual; + + System.runAs(DatabaseService_TEST.minimalAccessPersona.getUser()) { + // Assert permissions are as expected. + System.assert( + Schema.Account.SObjectType.getDescribe().isCreateable(), + 'DatabaseService_TEST.minimalAccessPersona should be able to create Account.' + ); + + System.assert( + Schema.Account.SObjectType.fields.AccountNumber.getDescribe() + .isCreateable(), + 'DatabaseService_TEST.minimalAccessPersona should be able to create Account.AccountNumber.' + ); + + final DatabaseService service = new DatabaseService(); + + // Null records. + final List actualWhenNullRecords = service.insertRecordsEnforceFls( + (List) null + ); + System.assertNotEquals( + null, + actualWhenNullRecords, + 'DatabaseService.insertRecordsEnforceFls should never return null.' + ); + System.assert( + actualWhenNullRecords.isEmpty(), + 'DatabaseService.insertRecordsEnforceFls should always return an empty list whenever the records argument is null. CRUD or FLS is not checked.' + ); + + // Empty records. + final List actualWhenEmptyRecords = service.insertRecordsEnforceFls( + new List() + ); + System.assertNotEquals( + null, + actualWhenEmptyRecords, + 'DatabaseService.insertRecordsEnforceFls should never return null.' + ); + System.assert( + actualWhenEmptyRecords.isEmpty(), + 'DatabaseService.insertRecordsEnforceFls should always return an empty list whenever the records argument is empty. CRUD or FLS is not checked.' + ); + + // Actual test. + actual = service.insertRecordsEnforceFls(records); + } + + Test.stopTest(); + + System.assertNotEquals( + null, + actual, + 'DatabaseService.insertRecordsEnforceFls should never return null.' + ); + + final Integer recordsSize = records.size(); + + System.assertEquals( + recordsSize, + actual.size(), + 'DatabaseService.insertRecordsEnforceFls should return a Database.SaveResult for each record in records.' + ); + + for (Integer i = 0; i < recordsSize; i++) { + System.assert( + actual[i].isSuccess(), + 'actual[' + + i + + '].isSuccess should be true.' + ); + System.assertNotEquals( + null, + actual[i].getId(), + 'actual[' + + i + + '].getId should not be null.' + ); + System.assertEquals( + records[i].Id, + actual[i].getId(), + 'records[' + + i + + '].Id should equal actual[' + + i + + '].getId.' + ); + } + + System.assertEquals( + recordsSize, + [SELECT Id FROM Account WHERE Id IN :records].size(), + 'All records should exist in the Database.' + ); + } + + @IsTest + private static void updateRecordsEnforceFls_NoObjectLevelPermission() { + // Load TestUser.Persona and assert the object-level and field-level are as expected for this test. + DatabaseService_TEST.minimalAccessPersona.load(); + + // Set arguments. + final Account originalRecord = DatabaseService_TEST.getAccount(); + final List records = new List{ + new Account( + Id = originalRecord.Id, + Name = 'new Name', + AccountNumber = 'new Account Number' + ) + }; + + Test.startTest(); + + Exception actualException; + + System.runAs(DatabaseService_TEST.minimalAccessPersona.getUser()) { + // Assert permissions are as expected. + System.assert( + !Schema.Account.SObjectType.getDescribe().isUpdateable(), + 'DatabaseService_TEST.minimalAccessPersona should not be able to update Account.' + ); + + final DatabaseService service = new DatabaseService(); + + // Null records. + final List actualWhenNullRecords = service.updateRecordsEnforceFls( + (List) null + ); + System.assertNotEquals( + null, + actualWhenNullRecords, + 'DatabaseService.updateRecordsEnforceFls should never return null.' + ); + System.assert( + actualWhenNullRecords.isEmpty(), + 'DatabaseService.updateRecordsEnforceFls should always return an empty list whenever the records argument is null. CRUD or FLS is not checked.' + ); + + final List actualWhenEmptyRecords = service.updateRecordsEnforceFls( + new List() + ); + System.assertNotEquals( + null, + actualWhenEmptyRecords, + 'DatabaseService.updateRecordsEnforceFls should never return null.' + ); + System.assert( + actualWhenEmptyRecords.isEmpty(), + 'DatabaseService.updateRecordsEnforceFls should always return an empty list whenever the records argument is empty. CRUD or FLS is not checked.' + ); + + try { + service.updateRecordsEnforceFls(records); + } catch (Exception e) { + actualException = e; + } + } + + Test.stopTest(); + + System.assert( + actualException instanceof System.NoAccessException, + 'DatabaseService.updateRecordsEnforceFls should throw a System.NoAccessException since the user does not have Update object-level access on Account. Actual Exception: ' + + actualException + ); + } + + @IsTest + private static void updateRecordsEnforceFls_MissingFieldLevelPermission() { + // Load TestUser.Persona and assert the object-level and field-level are as expected for this test. + DatabaseService_TEST.minimalAccessPersona.load(); + + TestUser.PermissionSetManager permissionSetManager = DatabaseService_TEST.minimalAccessPersona.getPermissionSetManager(); + + TestUser.ObjectPermission objectPermission = permissionSetManager.getObjectPermission( + Schema.SObjectType.Account + ) + .setRead(true) + .setEdit(true); + + TestUser.FieldPermission fieldPermission = objectPermission.getFieldPermission( + Schema.SObjectType.Account.fields.AccountNumber + ) + .setRead(false) + .setEdit(false); + + permissionSetManager.save(); + + // Set arguments. + final Account originalRecord = DatabaseService_TEST.getAccount(); + final List records = new List{ + new Account( + Id = originalRecord.Id, + Name = 'new Name', + AccountNumber = 'new Account Number' + ) + }; + + Test.startTest(); + + Exception actualException; + + System.runAs(DatabaseService_TEST.minimalAccessPersona.getUser()) { + // Assert permissions are as expected. + System.assert( + Schema.Account.SObjectType.getDescribe().isUpdateable(), + 'DatabaseService_TEST.minimalAccessPersona should be able to update Account.' + ); + + System.assert( + !Schema.Account.SObjectType.fields.AccountNumber.getDescribe() + .isUpdateable(), + 'DatabaseService_TEST.minimalAccessPersona should not be able to update Account.AccountNumber.' + ); + + final DatabaseService service = new DatabaseService(); + + // Null records. + final List actualWhenNullRecords = service.updateRecordsEnforceFls( + (List) null + ); + System.assertNotEquals( + null, + actualWhenNullRecords, + 'DatabaseService.updateRecordsEnforceFls should never return null.' + ); + System.assert( + actualWhenNullRecords.isEmpty(), + 'DatabaseService.updateRecordsEnforceFls should always return an empty list whenever the records argument is null. CRUD or FLS is not checked.' + ); + + final List actualWhenEmptyRecords = service.updateRecordsEnforceFls( + new List() + ); + System.assertNotEquals( + null, + actualWhenEmptyRecords, + 'DatabaseService.updateRecordsEnforceFls should never return null.' + ); + System.assert( + actualWhenEmptyRecords.isEmpty(), + 'DatabaseService.updateRecordsEnforceFls should always return an empty list whenever the records argument is empty. CRUD or FLS is not checked.' + ); + + try { + service.updateRecordsEnforceFls(records); + } catch (Exception e) { + actualException = e; + } + } + + Test.stopTest(); + + System.assert( + actualException instanceof System.NoAccessException, + 'DatabaseService.updateRecordsEnforceFls should throw a System.NoAccessException since the user does not have Edit field-level access on Account.AccountNumber. Actual Exception: ' + + actualException + ); + System.assertEquals( + System.Label.DatabaseService_NoUpdateFlsException, + actualException.getMessage(), + 'DatabaseService.updateRecordsEnforceFls should throw a System.NoAccessException whose message equals System.Label.DatabaseService_NoUpdateFlsException ("' + + System.Label.DatabaseService_NoUpdateFlsException + + '").' + ); + } + + @IsTest + private static void updateRecordsEnforceFls_WithFieldLevelPermission() { + // Load TestUser.Persona and assert the object-level and field-level are as expected for this test. + DatabaseService_TEST.minimalAccessPersona.load(); + + TestUser.PermissionSetManager permissionSetManager = DatabaseService_TEST.minimalAccessPersona.getPermissionSetManager(); + + TestUser.ObjectPermission objectPermission = permissionSetManager.getObjectPermission( + Schema.SObjectType.Account + ) + .setRead(true) + .setEdit(true); + + TestUser.FieldPermission fieldPermission = objectPermission.getFieldPermission( + Schema.SObjectType.Account.fields.AccountNumber + ) + .setRead(true) + .setEdit(true); + + permissionSetManager.save(); + + // Set arguments. + final Account originalRecord = DatabaseService_TEST.getAccount(); + final List records = new List{ + new Account( + Id = originalRecord.Id, + Name = 'new Name', + AccountNumber = 'new Account Number' + ) + }; + + Test.startTest(); + + List actual; + + System.runAs(DatabaseService_TEST.minimalAccessPersona.getUser()) { + // Assert permissions are as expected. + System.assert( + Schema.Account.SObjectType.getDescribe().isUpdateable(), + 'DatabaseService_TEST.minimalAccessPersona should be able to update Account.' + ); + + System.assert( + Schema.Account.SObjectType.fields.AccountNumber.getDescribe() + .isUpdateable(), + 'DatabaseService_TEST.minimalAccessPersona should be able to update Account.AccountNumber.' + ); + + final DatabaseService service = new DatabaseService(); + + // Null records. + final List actualWhenNullRecords = service.updateRecordsEnforceFls( + (List) null + ); + System.assertNotEquals( + null, + actualWhenNullRecords, + 'DatabaseService.updateRecordsEnforceFls should never return null.' + ); + System.assert( + actualWhenNullRecords.isEmpty(), + 'DatabaseService.updateRecordsEnforceFls should always return an empty list whenever the records argument is null. CRUD or FLS is not checked.' + ); + + // Empty records. + final List actualWhenEmptyRecords = service.updateRecordsEnforceFls( + new List() + ); + System.assertNotEquals( + null, + actualWhenEmptyRecords, + 'DatabaseService.updateRecordsEnforceFls should never return null.' + ); + System.assert( + actualWhenEmptyRecords.isEmpty(), + 'DatabaseService.updateRecordsEnforceFls should always return an empty list whenever the records argument is empty. CRUD or FLS is not checked.' + ); + + // Actual test. + actual = service.updateRecordsEnforceFls(records); + } + + Test.stopTest(); + + System.assertNotEquals( + null, + actual, + 'DatabaseService.updateRecordsEnforceFls should never return null.' + ); + + final Integer recordsSize = records.size(); + + System.assertEquals( + recordsSize, + actual.size(), + 'DatabaseService.updateRecordsEnforceFls should return a Database.SaveResult for each record in records.' + ); + + for (Integer i = 0; i < recordsSize; i++) { + System.assert( + actual[i].isSuccess(), + 'actual[' + + i + + '].isSuccess should be true.' + ); + System.assertNotEquals( + null, + actual[i].getId(), + 'actual[' + + i + + '].getId should not be null.' + ); + System.assertEquals( + records[i].Id, + actual[i].getId(), + 'records[' + + i + + '].Id should equal actual[' + + i + + '].getId.' + ); + } + + System.assertEquals( + recordsSize, + [ + SELECT Id + FROM Account + WHERE + Id = :originalRecord.Id + AND Name = :records[0].Name + AND AccountNumber = :records[0].AccountNumber + ] + .size(), + 'All records should have been updated in the Database.' + ); + } + + @IsTest + private static void deleteRecords_NoObjectLevelPermission() { + // Load TestUser.Persona and assert the object-level and field-level are as expected for this test. + DatabaseService_TEST.minimalAccessPersona.load(); + + // Set arguments. + final List records = new List{ + DatabaseService_TEST.getAccount() + }; + + Test.startTest(); + + Exception actualException; + + System.runAs(DatabaseService_TEST.minimalAccessPersona.getUser()) { + // Assert permissions are as expected. + System.assert( + !Schema.Account.SObjectType.getDescribe().isDeletable(), + 'DatabaseService_TEST.minimalAccessPersona should not be able to delete Account.' + ); + + final DatabaseService service = new DatabaseService(); + + // Null records. + final List actualWhenNullRecords = service.deleteRecords( + (List) null + ); + System.assertNotEquals( + null, + actualWhenNullRecords, + 'DatabaseService.deleteRecords should never return null.' + ); + System.assert( + actualWhenNullRecords.isEmpty(), + 'DatabaseService.deleteRecords should always return an empty list whenever the records argument is null. CRUD or FLS is not checked.' + ); + + final List actualWhenEmptyRecords = service.deleteRecords( + new List() + ); + System.assertNotEquals( + null, + actualWhenEmptyRecords, + 'DatabaseService.deleteRecords should never return null.' + ); + System.assert( + actualWhenEmptyRecords.isEmpty(), + 'DatabaseService.deleteRecords should always return an empty list whenever the records argument is empty. CRUD or FLS is not checked.' + ); + + try { + service.deleteRecords(records); + } catch (Exception e) { + actualException = e; } - } catch (Exception e) { - System.assert(false, e.getMessage()); } + + Test.stopTest(); + + System.assert( + actualException instanceof System.NoAccessException, + 'DatabaseService.deleteRecords should throw a System.NoAccessException since the user does not have Delete object-level access on Account. Actual Exception: ' + + actualException + ); + System.assertEquals( + System.Label.DatabaseService_NoDeleteAccessException, + actualException.getMessage(), + 'DatabaseService.deleteRecords should throw a System.NoAccessException whose message equals System.Label.DatabaseService_NoDeleteAccessException ("' + + System.Label.DatabaseService_NoDeleteAccessException + + '").' + ); + } + + @IsTest + private static void deleteRecords_WithObjectLevelPermission() { + // Load TestUser.Persona and assert the object-level and field-level are as expected for this test. + DatabaseService_TEST.minimalAccessPersona.load(); + + TestUser.PermissionSetManager permissionSetManager = DatabaseService_TEST.minimalAccessPersona.getPermissionSetManager(); + + TestUser.ObjectPermission objectPermission = permissionSetManager.getObjectPermission( + Schema.SObjectType.Account + ) + .setRead(true) + .setEdit(true) + .setDelete(true); + + permissionSetManager.save(); + + // Set arguments. + final List records = new List{ + DatabaseService_TEST.getAccount() + }; + + Test.startTest(); + + List actual; + + System.runAs(DatabaseService_TEST.minimalAccessPersona.getUser()) { + // Assert permissions are as expected. + System.assert( + Schema.Account.SObjectType.getDescribe().isDeletable(), + 'DatabaseService_TEST.minimalAccessPersona should be able to delete Account.' + ); + + final DatabaseService service = new DatabaseService(); + + // Null records. + final List actualWhenNullRecords = service.deleteRecords( + (List) null + ); + System.assertNotEquals( + null, + actualWhenNullRecords, + 'DatabaseService.deleteRecords should never return null.' + ); + System.assert( + actualWhenNullRecords.isEmpty(), + 'DatabaseService.deleteRecords should always return an empty list whenever the records argument is null. CRUD or FLS is not checked.' + ); + + // Empty records. + final List actualWhenEmptyRecords = service.deleteRecords( + new List() + ); + System.assertNotEquals( + null, + actualWhenEmptyRecords, + 'DatabaseService.deleteRecords should never return null.' + ); + System.assert( + actualWhenEmptyRecords.isEmpty(), + 'DatabaseService.deleteRecords should always return an empty list whenever the records argument is empty. CRUD or FLS is not checked.' + ); + + // Actual test. + actual = service.deleteRecords(records); + } + + Test.stopTest(); + + System.assertNotEquals( + null, + actual, + 'DatabaseService.deleteRecords should never return null.' + ); + + final Integer recordsSize = records.size(); + + System.assertEquals( + recordsSize, + actual.size(), + 'DatabaseService.deleteRecords should return a Database.DeleteResult for each record in records.' + ); + + for (Integer i = 0; i < recordsSize; i++) { + System.assert( + actual[i].isSuccess(), + 'actual[' + + i + + '].isSuccess should be true.' + ); + System.assertNotEquals( + null, + actual[i].getId(), + 'actual[' + + i + + '].getId should not be null.' + ); + System.assertEquals( + records[i].Id, + actual[i].getId(), + 'records[' + + i + + '].Id should equal actual[' + + i + + '].getId.' + ); + } + + System.assert( + [SELECT Id FROM Account WHERE Id IN :records].isEmpty(), + 'All records should have been deleted in the Database.' + ); + } + + @IsTest + private static void setSavepointAndRollback() { + // Load TestUser.Persona and assert the object-level and field-level are as expected for this test. + DatabaseService_TEST.minimalAccessPersona.load(); + + TestUser.PermissionSetManager permissionSetManager = DatabaseService_TEST.minimalAccessPersona.getPermissionSetManager(); + + TestUser.ObjectPermission objectPermission = permissionSetManager.getObjectPermission( + Schema.SObjectType.Account + ) + .setRead(true) + .setEdit(true) + .setDelete(true); + + permissionSetManager.save(); + + // Set arguments. + final List records = new List{ + DatabaseService_TEST.getAccount() + }; + + Test.startTest(); + + System.runAs(DatabaseService_TEST.minimalAccessPersona.getUser()) { + final DatabaseService service = new DatabaseService(); + + // Create a savepoint. + final DatabaseService.Savepoint savepoint = service.setSavepoint(); + + System.assertNotEquals( + null, + savepoint.savepoint, + 'DatabaseService.setSavepoint should set the DatabaseService.Savepoint.savepoint.' + ); + + // Delete records. + service.deleteRecords(records); + + // Rollback the transaction. + service.rollback(savepoint); + } + + Test.stopTest(); + + System.assert( + ![SELECT Id FROM Account WHERE Id IN :records LIMIT 1].isEmpty(), + 'No records should have been deleted because the transaction should have been rolled back.' + ); + } + + /** + * Tests that we can mock rolling back because DatabaseService.Savepoint has an @TestVisible empty constructor. + */ + @IsTest + private static void mockRollingBack() { + // Set mock return values. + final DatabaseService.Savepoint expected = new DatabaseService.Savepoint(); + + System.assertEquals( + null, + expected.savepoint, + '@TestVisible private Database.Savepoint() constructor should not set Database.Savepoint.savepoint. Only DatabaseService.setSavepoint should set Database.Savepoint.savepoint.' + ); + + final UnitTest.Mock databaseServiceMock = new UnitTest.Mock(); + + final UnitTest.Mock setSavepoint = databaseServiceMock.getMethod('setSavepoint'); + setSavepoint.returnValue = expected; + + final UnitTest.Mock rollbackMethod = databaseServiceMock.getMethod('rollback'); + + Test.startTest(); + + final DatabaseService service = (DatabaseService) databaseServiceMock.createStub( + DatabaseService.class + ); + + final DatabaseService.Savepoint actual = service.setSavepoint(); + + service.rollback(actual); + + Test.stopTest(); + + System.assertEquals( + expected, + actual, + 'setSavepoint should have returned exactly (===) the mocked return value.' + ); + + setSavepoint.assertCalledOnceWith(new List()); + + rollbackMethod.assertCalledOnceWith(new List{ setSavepoint.returnValue }); } } diff --git a/force-app/main/default/classes/testUtils/TestUser/TestUser.cls b/force-app/main/default/classes/testUtils/TestUser/TestUser.cls new file mode 100644 index 0000000..6f03bda --- /dev/null +++ b/force-app/main/default/classes/testUtils/TestUser/TestUser.cls @@ -0,0 +1,1313 @@ +@IsTest +public with sharing class TestUser { + public with sharing virtual class MinimalAccessPersona extends TestUser.Persona { + public MinimalAccessPersona() { + super(); + } + + public virtual override Type getType() { + return TestUser.MinimalAccessPersona.class; + } + + public virtual override String getUsername() { + return 'TestUser.MinimalAccessPersona@sfdo.example.com'; + } + + public override String getProfileName() { + return 'Minimum Access - Salesforce'; + } + + public virtual override List getPermissionSetsToAssign() { + return new List{}; + } + + public virtual override List getPermissionSetLicensesToAssign() { + return new List(); + } + } + + public static Schema.UserRole insertNewUserRole() { + Integer tries = 0; + final Integer maxTries = 10; + while (tries < maxTries) { + final String developerName = (TestUser.class.getName().replace('.', '_') + + Math.abs(Crypto.getRandomInteger())) + .left(Schema.SObjectType.UserRole.fields.DeveloperName.getLength()); + + final Schema.UserRole userRole = new Schema.UserRole( + DeveloperName = developerName, + Name = developerName + ); + try { + insert userRole; + return userRole; + } catch (Exception e) { + tries++; + } + } + System.assert( + false, + String.format( + 'A new, random UserRole could not be created in {0} tries.', + new List{ String.valueOf(maxTries) } + ) + ); + return null; + } + + private static void setUserDefaultValue( + final Schema.User user, + final Schema.DescribeFieldResult fieldDescribe, + final String defaultValue + ) { + if (String.isBlank((String) user.get(fieldDescribe.getName()))) { + user.put( + fieldDescribe.getName(), + defaultValue == null ? null : defaultValue.left(fieldDescribe.getLength()) + ); + } + } + + public static List emptyIfNull(final List value) { + return value == null ? new List() : value; + } + + public static void insertPersonasInTestSetup(List personas) { + // System.runAs the running to avoid mixed DML and license restrictions. + final Schema.User runningUser = [ + SELECT Id, UserRoleId + FROM User + WHERE Id = :UserInfo.getUserId() + ][0]; + + // To create community users, make sure the running User has a UserRole. + if (runningUser.UserRoleId == null) { + // System.runAs the runningUser avoid mixed DML later. + System.runAs(runningUser) { + runningUser.UserRoleId = TestUser.insertNewUserRole().Id; + update runningUser; + } + } + + Map personasByUsername = new Map(); + final Integer size = personas.size(); + for (Integer i = 0; i < size; i++) { + final TestUser.Persona persona = personas[i]; + System.assertNotEquals( + null, + persona, + String.format( + 'personas[{0}] should not be null.', + new List{ String.valueof(i) } + ) + ); + + final String username = persona.getUsername(); + System.assert( + !String.isBlank(username), + String.format( + 'personas[{0}].getUsername() should not return a blank string.', + new List{ String.valueof(i) } + ) + ); + + System.assertEquals( + null, + personasByUsername.get(username), + String.format( + 'personas[{0}].getUsername() should be unique, but a TestUser.Persona already has been added with username "{1}".', + new List{ String.valueOf(i), username } + ) + ); + + // Last Persona with username wins. + personasByUsername.put(username, persona); + } + + // Collect information to create users and set user required fields. + final Map usersByUsername = new Map(); + final Set profileNames = new Set(); + final Set permissionSetLicenseDeveloperNames = new Set(); + // TODO: collect Permission Set Groups to assign. + final Set permissionSetNames = new Set(); + + // System.runAs the runningUser to execute doBeforeInsertingUser() and avoid mixed DML later. + System.runAs(runningUser) { + for (String username : personasByUsername.keySet()) { + final TestUser.Persona persona = personasByUsername.get(username); + + // Collect Profile Name. + final String profileName = persona.getProfileName(); + System.assert( + String.isNotBlank(profileName), + String.format( + 'getProfileName() should not be blank for TestUser.Persona with Username "{0}".', + new List{ username } + ) + ); + profileNames.add(profileName); + + // Create User and set defaults for required fields with missing values. + persona.doBeforeInsertingUser(); + + final Schema.User user = persona.getUserForInsert(); + System.assertNotEquals( + null, + user, + String.format( + 'getUserForInsert() should not return null for TestUser.Persona with Username "{0}".', + new List{ username } + ) + ); + usersByUsername.put(username, user); + + user.Username = username; + user.IsActive = true; + + TestUser.setUserDefaultValue( + user, + Schema.SObjectType.User.fields.LastName, + username + ); + + TestUser.setUserDefaultValue( + user, + Schema.SObjectType.User.fields.Alias, + username + ); + + TestUser.setUserDefaultValue( + user, + Schema.SObjectType.User.fields.Email, + UserInfo.getUserEmail() + ); + + TestUser.setUserDefaultValue( + user, + Schema.SObjectType.User.fields.EmailEncodingKey, + 'UTF-8' + ); + + TestUser.setUserDefaultValue( + user, + Schema.SObjectType.User.fields.LanguageLocaleKey, + 'en_US' + ); + + TestUser.setUserDefaultValue( + user, + Schema.SObjectType.User.fields.LocaleSidKey, + 'en_US' + ); + + TestUser.setUserDefaultValue( + user, + Schema.SObjectType.User.fields.TimeZoneSidKey, + 'America/Los_Angeles' + ); + + // Collect Permission Set Licenses to assign. + permissionSetLicenseDeveloperNames.addAll( + TestUser.emptyIfNull(persona.getPermissionSetLicensesToAssign()) + ); + + // Collect Permission Sets to assign. + permissionSetNames.add(persona.getPermissionSetName()); + permissionSetNames.addAll( + TestUser.emptyIfNull(persona.getPermissionSetsToAssign()) + ); + } + } + + // Get Profiles, Permission Set Licenses to assign, and Permission Sets to assign. + final Map profileIdsByName = new Map(); + for (Schema.Profile profile : [ + SELECT Id, Name + FROM Profile + WHERE Name IN :profileNames + ]) { + profileIdsByName.put(profile.Name, profile.Id); + } + + final Map permissionSetLicenseIdsByDeveloperName = new Map(); + for (Schema.PermissionSetLicense permissionSetLicense : [ + SELECT Id, DeveloperName + FROM PermissionSetLicense + WHERE DeveloperName IN :permissionSetLicenseDeveloperNames + ]) { + permissionSetLicenseIdsByDeveloperName.put( + permissionSetLicense.DeveloperName, + permissionSetLicense.Id + ); + } + + // TODO: collect Permission Set Groups to assign. + + final Map permissionSetsIdsByName = new Map(); + { + final Set permissionSetNamesToInsert = new Set( + permissionSetNames + ); + for (Schema.PermissionSet permissionSet : [ + SELECT Id, Name + FROM PermissionSet + WHERE Name IN :permissionSetNames + ]) { + permissionSetsIdsByName.put(permissionSet.Name, permissionSet.Id); + permissionSetNamesToInsert.remove(permissionSet.Name); + } + + // Create Permission Sets that don't exist. + final List permissionSetsToInsert = new List(); + for (String permissionSetName : permissionSetNamesToInsert) { + permissionSetsToInsert.add( + new PermissionSet(Name = permissionSetName, Label = permissionSetName) + ); + } + // System.runAs the runningUser to insert permssions and assignments to avoid mixed DML later. + System.runAs(runningUser) { + insert permissionSetsToInsert; + } + for (Schema.PermissionSet permissionSet : permissionSetsToInsert) { + permissionSetsIdsByName.put(permissionSet.Name, permissionSet.Id); + } + } + + // Assign User Profile IDs and collect Useres. + final List users = new List(); + + for (String username : usersByUsername.keySet()) { + final Schema.User user = usersByUsername.get(username); + final TestUser.Persona persona = personasByUsername.get(username); + + final String profileName = persona.getProfileName(); + final Id profileId = profileIdsByName.get(profileName); + + System.assertNotEquals( + null, + profileId, + String.format( + 'No Profile found for Profile Name "{0}" for TestUser.Persona with Username "{1}".', + new List{ profileName, username } + ) + ); + user.ProfileId = profileId; + + users.add(user); + } + + // Insert Users and assign PSLs and Permission Sets. + // System.runAs ignores user license limits. + System.runAs(runningUser) { + insert users; + + final List permissionSetLicenseAssigns = new List(); + final Map> permissionSetLicenseAssignsByNameByUsername = new Map>(); + + final List permissionSetAssignments = new List(); + final Map> permissionSetAssignmentsByNameByUsername = new Map>(); + + for (String username : personasByUsername.keySet()) { + final Schema.User user = usersByUsername.get(username); + final TestUser.Persona persona = personasByUsername.get(username); + + // Track existing assignments so we don't try to insert duplicate assignments. + Map permissionSetLicenseAssignsByName = permissionSetLicenseAssignsByNameByUsername.get( + username + ); + if (permissionSetLicenseAssignsByName == null) { + permissionSetLicenseAssignsByName = new Map(); + permissionSetLicenseAssignsByNameByUsername.put( + username, + permissionSetLicenseAssignsByName + ); + } + + Map permissionSetAssignmentsByName = permissionSetAssignmentsByNameByUsername.get( + username + ); + if (permissionSetAssignmentsByName == null) { + permissionSetAssignmentsByName = new Map(); + permissionSetAssignmentsByNameByUsername.put( + username, + permissionSetAssignmentsByName + ); + } + + // Assign Permission Set Licenses. + for ( + String developerName : TestUser.emptyIfNull( + persona.getPermissionSetLicensesToAssign() + ) + ) { + final Id permissionSetLicenseId = permissionSetLicenseIdsByDeveloperName.get( + developerName + ); + System.assertNotEquals( + null, + permissionSetLicenseId, + String.format( + 'No Permission Set License found with DeveloperName "{0}" for TestUser.Persona with Username "{1}".', + new List{ developerName, username } + ) + ); + + if (permissionSetLicenseAssignsByName.get(developerName) == null) { + final Schema.PermissionSetLicenseAssign assign = new Schema.PermissionSetLicenseAssign( + AssigneeId = user.Id, + PermissionSetLicenseId = permissionSetLicenseId + ); + + permissionSetLicenseAssigns.add(assign); + permissionSetLicenseAssignsByName.put(developerName, assign); + } + } + + // Assign Permission Sets. + final List permissionSetsToAssign = new List{ + persona.getPermissionSetName() + }; + permissionSetsToAssign.addAll( + TestUser.emptyIfNull(persona.getPermissionSetsToAssign()) + ); + for (String name : permissionSetsToAssign) { + final Id permissionSetId = permissionSetsIdsByName.get(name); + System.assertNotEquals( + null, + permissionSetId, + String.format( + 'No Permission Set found with Name "{0}" for TestUser.Persona with Username "{1}".', + new List{ name, username } + ) + ); + + if (permissionSetAssignmentsByName.get(name) == null) { + final Schema.PermissionSetAssignment assignment = new Schema.PermissionSetAssignment( + AssigneeId = user.Id, + PermissionSetId = permissionSetId + ); + permissionSetAssignments.add(assignment); + permissionSetAssignmentsByName.put(name, assignment); + } + } + } + + insert permissionSetLicenseAssigns; + // TODO: insert Permission Set Group Assignments. + insert permissionSetAssignments; + } + } + + public with sharing abstract class Persona { + protected Schema.User user; + protected Map permissionSetManagersById; + protected PermissionSetManager permissionSetManager; + + /** + * Persona description + * @return return description + */ + public Persona() { + } + + /** + * Queries User and sets PermissionSetManagers. + */ + public void load() { + final String username = this.getUsername(); + List users = [ + SELECT + Id, + Name, + ProfileId, + Profile.Name, + Username, + IsActive, + FirstName, + LastName, + Alias, + Email, + EmailEncodingKey, + LanguageLocaleKey, + LocaleSidKey, + TimeZoneSidKey, + ContactId, + Contact.AccountId, + ( + SELECT + Id, + PermissionSetId, + PermissionSet.Name, + PermissionSet.PermissionSetGroupId, + PermissionSet.ProfileId + FROM PermissionSetAssignments + ) + FROM User + WHERE Username = :username + LIMIT 1 + ]; + System.assert( + !users.isEmpty(), + String.format( + 'No User found with Username "{0}". Is TestUser.insertPersonasInTestSetup(List personas) called in @TestSetup?', + new List{ username } + ) + ); + this.user = users[0]; + + System.assertEquals( + this.getProfileName(), + this.user.Profile.Name, + 'User.Profile.Name should equal getProfileName().' + ); + + final Set permissionSetIds = new Set(); + Id permissionSetId; + for ( + Schema.PermissionSetAssignment assignment : this.user.PermissionSetAssignments + ) { + permissionSetIds.add(assignment.PermissionSetId); + + if (assignment.PermissionSet.Name == this.getPermissionSetName()) { + permissionSetId = assignment.PermissionSetId; + } + } + + this.permissionSetManagersById = TestUser.getPermissionSetManagersById( + permissionSetIds + ); + + this.permissionSetManager = this.permissionSetManagersById.get( + permissionSetId + ); + + if (this.permissionSetManager == null) { + System.assert( + false, + String.format( + 'The default Permission Set "{0}" was not assigned to this User with Username "{1}" for TestUser.Persona "{2}"', + new List{ + this.getPermissionSetName(), + this.getUsername(), + this.getType().getName() + } + ) + ); + } + } + + /** + * @return Username of the User generated for this Persona. Should be unique within a test. + */ + public abstract String getUsername(); + + /** + * @return Name of the Profile for the User generated by this Persona. + */ + public abstract String getProfileName(); + + /** + * @return Type of the implementation. Used to generate a unique Permission Set for the implementation. + */ + public abstract Type getType(); + + /** + * @return Name of the empty Permission Set created specifically for this implementation. + */ + public String getPermissionSetName() { + return this.getType().getName().replace('.', '_'); + } + + /** + * Optional method to be executed in the TestUser.insertPersonasInTestSetup method before Users are inserted. + */ + public virtual void doBeforeInsertingUser() { + } + + /** + * Override to set optional User fields, e.g. ContactId. + * @return The User to generated for insert. Username and ProfileId are overridden, and other required User fields are set to a default if not provided. + */ + public virtual User getUserForInsert() { + return new User(); + } + + /** + * @return DeveloperNames of Permission Set Licenses to assign to the Persona's User. + */ + public virtual List getPermissionSetLicensesToAssign() { + return new List(); + } + + /** + * @return Names of Permission Sets to assign to the Persona's User. + */ + public virtual List getPermissionSetsToAssign() { + return new List(); + } + + protected virtual void assertLoaded() { + if (this.user == null) { + System.assert( + false, + String.format( + 'load() should have been called before using this TestUser.Persona "{0}".', + new List{ this.getType().getName() } + ) + ); + } + } + + public User getUser() { + this.assertLoaded(); + return this.user; + } + + public TestUser.PermissionSetManager getPermissionSetManager() { + this.assertLoaded(); + return this.permissionSetManager; + } + + public List getPermissionSetManagers() { + this.assertLoaded(); + return this.permissionSetManagersById.values(); + } + + public TestUser.PermissionSetManager getPermissionSetManager(Id permissionSetId) { + this.assertLoaded(); + return this.permissionSetManagersById.get(permissionSetId); + } + } + + public static Map getPermissionSetManagersById( + Set permissionSetIds + ) { + final Map permissionSetsById = new Map( + [ + SELECT + Id, + Name, + ProfileId, + ( + SELECT + Id, + ParentId, + SobjectType, + PermissionsRead, + PermissionsCreate, + PermissionsEdit, + PermissionsDelete, + PermissionsViewAllRecords, + PermissionsModifyAllRecords + FROM ObjectPerms // API Name: ObjectPermissions + ORDER BY SobjectType + ), + ( + SELECT + Id, + ParentId, + SobjectType, + Field, + PermissionsRead, + PermissionsEdit + FROM FieldPerms // API Name: FieldPermissions + ORDER BY Field + ) + FROM PermissionSet + WHERE Id IN :permissionSetIds + ] + ); + + final Map permissionSetManagersById = new Map(); + for (Id permissionSetId : permissionSetIds) { + final Schema.PermissionSet permissionSet = permissionSetsById.get( + permissionSetId + ); + System.assertNotEquals( + null, + permissionSet, + String.format( + 'No Permission Set found with Id "{0}".', + new List{ permissionSetId } + ) + ); + + // Don't include Permission Sets associated with Profiles. + // Modifying Permission Sets associated with Profiles throws Exceptions when trying to save(). + if (permissionSet.ProfileId == null) { + permissionSetManagersById.put( + permissionSetId, + new TestUser.PermissionSetManager(permissionSet) + ); + } + } + + return permissionSetManagersById; + } + + public with sharing class PermissionSetManager { + private final Schema.PermissionSet permissionSet; + private final Map objectPermissionRecordsBySObjectType; + private final Map> fieldPermissionRecordsBySObjectTypeByName; + private final Map objectPermissionsBySObjectType; + + private PermissionSetManager(final PermissionSet permissionSet) { + System.assertNotEquals( + null, + permissionSet, + 'PermissionSet should not be null for TestUser.' + ); + this.permissionSet = permissionSet; + + this.objectPermissionsBySObjectType = new Map(); + + // Collect ObjectPerms and FieldPerms so they can be loaded on demand. + this.objectPermissionRecordsBySObjectType = new Map(); + for (Schema.ObjectPermissions permission : permissionSet.ObjectPerms) { + this.objectPermissionRecordsBySObjectType.put( + permission.SobjectType, + permission + ); + } + + this.fieldPermissionRecordsBySObjectTypeByName = new Map>(); + for (Schema.FieldPermissions permission : permissionSet.FieldPerms) { + final String sObjectType = permission.SobjectType; + final String name = permission.Field.substringAfter('.'); + + Map objectPermissions = fieldPermissionRecordsBySObjectTypeByName.get( + sObjectType + ); + if (objectPermissions == null) { + objectPermissions = new Map(); + fieldPermissionRecordsBySObjectTypeByName.put( + sObjectType, + objectPermissions + ); + } + objectPermissions.put(name, permission); + } + } + + public String getPermissionSetName() { + return this.permissionSet.Name; + } + + public Id getPermissionSetId() { + return this.permissionSet.Id; + } + + public TestUser.ObjectPermission getObjectPermission( + Schema.DescribeSObjectResult objectDescribe + ) { + System.assertNotEquals( + null, + objectDescribe, + 'objectDescribe should not be null' + ); + if ( + this.objectPermissionsBySObjectType.get(objectDescribe.getName()) == null + ) { + // Load existing ObjectPerms and FieldPerms. + final Map fieldPermissionRecordByName = this.fieldPermissionRecordsBySObjectTypeByName.get( + objectDescribe.getName() + ); + final List fieldPermissions = new List(); + + if (fieldPermissionRecordByName != null) { + Map sObjectFieldsByName = objectDescribe.fields.getMap(); + + for (String fieldName : fieldPermissionRecordByName.keySet()) { + final Schema.FieldPermissions permission = fieldPermissionRecordByName.get( + fieldName + ); + final Schema.SObjectField sObjectField = sObjectFieldsByName.get( + fieldName + ); + + // Not every FieldPermissions's Field has a describe?? Must be some deep, standard object not normally used by ISVs. + if (sObjectField != null) { + final TestUser.FieldPermission fieldPermission = new TestUser.FieldPermission( + sObjectField.getDescribe(), + permission + ); + fieldPermissions.add(fieldPermission); + } + } + } + + TestUser.ObjectPermission objectPermission; + + final Schema.ObjectPermissions objectPermissionRecord = this.objectPermissionRecordsBySObjectType.get( + objectDescribe.getName() + ); + if (objectPermissionRecord != null) { + // There is an existing ObjectPermission record to load values from. + objectPermission = new TestUser.ObjectPermission( + objectDescribe, + objectPermissionRecord, + fieldPermissions + ); + } else { + // The PermissionSet does not have any existing permissions to objectDescribe. + objectPermission = new TestUser.ObjectPermission( + objectDescribe, + new ObjectPermissions( + ParentId = this.getPermissionSetId(), + SobjectType = objectDescribe.getName() + ), + fieldPermissions + ); + } + + this.objectPermissionsBySObjectType.put( + objectDescribe.getName(), + objectPermission + ); + } + return this.objectPermissionsBySObjectType.get(objectDescribe.getName()); + } + + public TestUser.PermissionSetManager save() { + List allPermissions = new List(); + for ( + TestUser.ObjectPermission objectPermission : this.objectPermissionsBySObjectType.values() + ) { + allPermissions.addAll(objectPermission.allPermissions); + } + TestUser.save(allPermissions); + return this; + } + } + + public abstract class Permission { + private final SObject permission; + protected Boolean isChanged = false; + + protected Permission() { + // Empty, protected constructor to prevent extensions outside of PermissionsTest + } + + /** + * @return If any permission is true + */ + protected abstract Boolean isAnyPermission(); + + public Id getId() { + return (Id) this.permission.get('Id'); + } + + protected Boolean getPermission(Schema.DescribeFieldResult fieldDescribe) { + return (Boolean) this.permission.get(fieldDescribe.getName()); + } + + protected void setPermission( + Schema.DescribeFieldResult fieldDescribe, + Boolean value + ) { + final Boolean oldValue = this.getPermission(fieldDescribe); + final Boolean newValue = value == true; + //final Boolean oldValue = (Boolean) this.permission.put(fieldDescribe.getName(), newValue); + this.permission.put(fieldDescribe.getName(), newValue); + this.isChanged = this.isChanged || newValue != oldValue; + } + + public Boolean getIsChanged() { + return this.isChanged; + } + + private Object getSaveRequest() { + if (this.getIsChanged()) { + // If at least one permission is true, update permission. + // Else if Nno permissions are true, we can't update a Permission SObject with no permissions. + // Rather, we need to delete the permission. Return the ID to delete, and clear out permission's Id. + //return this.isAnyPermission() ? this.permission : this.permission.put('Id', null); // Calling put returns the old value + if (this.isAnyPermission()) { + return this.permission; + } else { + Object permissionIdToDelete = this.permission.get('Id'); + this.permission.put('Id', null); + return permissionIdToDelete; + } + } + return null; + } + } + + public with sharing class FieldPermission extends TestUser.Permission { + final String field; + final Schema.DescribeFieldResult fieldDescribe; + + private FieldPermission( + final Schema.DescribeFieldResult fieldDescribe, + final Schema.FieldPermissions fieldPermissions + ) { + super(); + System.assertNotEquals( + null, + fieldDescribe, + 'fieldDescribe should not be null' + ); + System.assertNotEquals( + null, + fieldPermissions, + 'fieldPermissions should not be null' + ); + System.assertNotEquals( + null, + fieldPermissions.ParentId, + 'fieldPermissions.ParentId should not be null' + ); + System.assertNotEquals( + null, + fieldPermissions.SobjectType, + 'fieldPermissions.SobjectType should not be null' + ); + System.assertNotEquals( + null, + fieldPermissions.Field, + 'fieldPermissions.Field should not be null' + ); + this.fieldDescribe = fieldDescribe; + this.permission = fieldPermissions.clone(true, true, false, false); + this.field = fieldPermissions.Field.substringAfter('.'); + + // Initialize permissions so values are not null. + this.setRead(fieldPermissions.PermissionsRead); + this.setEdit(fieldPermissions.PermissionsEdit); + + this.isChanged = false; + } + + public Schema.DescribeFieldResult getFieldDescribe() { + return this.fieldDescribe; + } + + private Schema.FieldPermissions getFieldPermissions() { + return (Schema.FieldPermissions) this.permission; + } + + public Id getPermissionSetId() { + return this.getFieldPermissions().ParentId; + } + + public String getSObjectType() { + return this.getFieldPermissions().SobjectType; + } + + /** + * @return Part of FieldPermissions.Field corresponding to Field's API Name. + */ + public String getField() { + return this.field; + } + + public Boolean getRead() { + // FieldPermissions.PermissionsRead may null, i.e. not set. + return this.getPermission( + Schema.SObjectType.FieldPermissions.fields.PermissionsRead + ) == true; + } + + public Boolean getEdit() { + // FieldPermissions.PermissionsEdit may null, i.e. not set. + return this.getPermission( + Schema.SObjectType.FieldPermissions.fields.PermissionsEdit + ) == true; + } + + public override Boolean isAnyPermission() { + return this.getRead() || this.getEdit(); + } + + /** + * Sets FieldPermissions.PermissionsRead as permissionsRead if getFieldDescribe() is permissionable. + * @param permissionsRead Value to set on FieldPermissions.PermissionsRead + * @return this + */ + public TestUser.FieldPermission setRead(Boolean permissionsRead) { + if (!this.getFieldDescribe().isPermissionable()) { + throw new TestUser.FieldNotPermissionableException( + String.format( + 'Cannot call setRead on Field "{0}" since it is not permissionable for the running User.', + new List{ this.getFieldDescribe().getName() } + ) + ); + } + this.setPermission( + Schema.SObjectType.FieldPermissions.fields.PermissionsRead, + permissionsRead + ); + return this; + } + + /** + * Sets FieldPermissions.PermissionsEdit as permissionsEdit if getFieldDescribe() is permissionable and NOT calculated. + * If FieldPermissions.PermissionsEdit is set and is true, calls setRead(true) + * @param permissionsEdit Value to set on FieldPermissions.PermissionsEdit + * @return this + */ + public TestUser.FieldPermission setEdit(Boolean permissionsEdit) { + if (!this.getFieldDescribe().isPermissionable()) { + throw new TestUser.FieldNotPermissionableException( + String.format( + 'Cannot call setEdit on Field "{0}" since it is not permissionable for the running User.', + new List{ this.getFieldDescribe().getName() } + ) + ); + } + if (this.getFieldDescribe().isCalculated()) { + throw new TestUser.FieldNotPermissionableException( + String.format( + 'Cannot call setEdit on Field "{0}" since it is calculated for the running User.', + new List{ this.getFieldDescribe().getName() } + ) + ); + } + + final Boolean permission = permissionsEdit == true; + + this.setPermission( + Schema.SObjectType.FieldPermissions.fields.PermissionsEdit, + permission + ); + + // Edit requires Read + if (permission) { + this.setRead(true); + } + + return this; + } + + public void save() { + TestUser.save(new List{ this }); + } + } + + public with sharing class FieldNotPermissionableException extends Exception { + } + + public with sharing class ObjectPermission extends TestUser.Permission { + private final Schema.DescribeSObjectResult objectDescribe; + private final Map fieldPermissionsByField; + private final List allPermissions; + + private ObjectPermission( + final Schema.DescribeSObjectResult objectDescribe, + final Schema.ObjectPermissions permission + ) { + super(); + System.assertNotEquals( + null, + permission, + 'DescribeSObjectResult should not be null' + ); + System.assertNotEquals( + null, + permission, + 'ObjectPermissions should not be null' + ); + System.assertNotEquals( + null, + permission.ParentId, + 'ObjectPermissions.ParentId should not be null' + ); + System.assertNotEquals( + null, + permission.SobjectType, + 'ObjectPermissions.SobjectType should not be null' + ); + this.objectDescribe = objectDescribe; + this.permission = permission; + + // Initialize permissions so values are not null. + this.setRead(permission.PermissionsRead); + this.setCreate(permission.PermissionsCreate); + this.setEdit(permission.PermissionsEdit); + this.setDelete(permission.PermissionsDelete); + this.setViewAllRecords(permission.PermissionsViewAllRecords); + this.setModifyAllRecords(permission.PermissionsModifyAllRecords); + + this.isChanged = false; + + this.allPermissions = new List{ this }; + this.fieldPermissionsByField = new Map(); + } + + private ObjectPermission( + final Schema.DescribeSObjectResult objectDescribe, + final Schema.ObjectPermissions permission, + final List fieldPermissions + ) { + this(objectDescribe, permission); + + if (fieldPermissions != null) { + for (TestUser.FieldPermission fieldPermission : fieldPermissions) { + System.assertEquals( + this.getPermissionSetId(), + fieldPermission.getPermissionSetId(), + 'fieldPermission.getPermissionSetId() should equal getPermissionSetId()' + ); + System.assertEquals( + this.getSObjectType(), + fieldPermission.getSObjectType(), + 'fieldPermission.getSObjectType() should equal getSObjectType()' + ); + this.fieldPermissionsByField.put( + fieldPermission.getField(), + fieldPermission + ); + this.allPermissions.add(fieldPermission); + } + } + } + + public Schema.DescribeSObjectResult getObjectDescribe() { + return this.objectDescribe; + } + + private Schema.ObjectPermissions getObjectPermissions() { + return (Schema.ObjectPermissions) this.permission; + } + + public Id getPermissionSetId() { + return this.getObjectPermissions().ParentId; + } + + public String getSObjectType() { + return this.getObjectPermissions().SobjectType; + } + + public Boolean getRead() { + return this.getPermission( + Schema.SObjectType.ObjectPermissions.fields.PermissionsRead + ); + } + + public Boolean getCreate() { + return this.getPermission( + Schema.SObjectType.ObjectPermissions.fields.PermissionsCreate + ); + } + + public Boolean getEdit() { + return this.getPermission( + Schema.SObjectType.ObjectPermissions.fields.PermissionsEdit + ); + } + + public Boolean getDelete() { + return this.getPermission( + Schema.SObjectType.ObjectPermissions.fields.PermissionsDelete + ); + } + + public Boolean getViewAllRecords() { + return this.getPermission( + Schema.SObjectType.ObjectPermissions.fields.PermissionsViewAllRecords + ); + } + + public Boolean getModifyAllRecords() { + return this.getPermission( + Schema.SObjectType.ObjectPermissions.fields.PermissionsModifyAllRecords + ); + } + + public override Boolean isAnyPermission() { + return this.getRead() || + this.getCreate() || + this.getEdit() || + this.getDelete() || + this.getViewAllRecords() || + this.getModifyAllRecords(); + } + + public TestUser.ObjectPermission setRead(Boolean permissionsRead) { + this.setPermission( + Schema.SObjectType.ObjectPermissions.fields.PermissionsRead, + permissionsRead + ); + if (!this.getRead()) { + this.setCreate(false); + this.setEdit(false); + this.setDelete(false); + this.setViewAllRecords(false); + this.setModifyAllRecords(false); + } + return this; + } + + public TestUser.ObjectPermission setCreate(Boolean permissionsCreate) { + this.setPermission( + Schema.SObjectType.ObjectPermissions.fields.PermissionsCreate, + permissionsCreate + ); + if (this.getCreate()) { + this.setRead(true); + } + return this; + } + + public TestUser.ObjectPermission setEdit(Boolean permissionsEdit) { + this.setPermission( + Schema.SObjectType.ObjectPermissions.fields.PermissionsEdit, + permissionsEdit + ); + // Edit requires Read + if (this.getEdit()) { + this.setRead(true); + } else { + this.setDelete(false); + this.setModifyAllRecords(false); + } + return this; + } + + public TestUser.ObjectPermission setDelete(Boolean permissionsDelete) { + this.setPermission( + Schema.SObjectType.ObjectPermissions.fields.PermissionsDelete, + permissionsDelete + ); + if (this.getDelete()) { + this.setEdit(true); + this.setRead(true); + } else { + this.setModifyAllRecords(false); + } + return this; + } + + public TestUser.ObjectPermission setViewAllRecords( + Boolean permissionsViewAllRecords + ) { + this.setPermission( + Schema.SObjectType.ObjectPermissions.fields.PermissionsViewAllRecords, + permissionsViewAllRecords + ); + // View All requires Read + if (this.getViewAllRecords()) { + this.setRead(true); + } else { + this.setModifyAllRecords(false); + } + return this; + } + + public TestUser.ObjectPermission setModifyAllRecords( + Boolean permissionsModifyAllRecords + ) { + this.setPermission( + Schema.SObjectType.ObjectPermissions.fields.PermissionsModifyAllRecords, + permissionsModifyAllRecords + ); + // Modify All requires Read, Edit, Delete, View All + if (this.getModifyAllRecords()) { + this.setRead(true); + this.setEdit(true); + this.setDelete(true); + this.setViewAllRecords(true); + } + return this; + } + + public TestUser.ObjectPermission save() { + TestUser.save(this.allPermissions); + return this; + } + + public TestUser.FieldPermission getFieldPermission( + DescribeFieldResult fieldDescribe + ) { + System.assertNotEquals(null, 'fieldDescribe should not be null'); + if (this.fieldPermissionsByField.get(fieldDescribe.getName()) == null) { + final TestUser.FieldPermission fieldPermission = new TestUser.FieldPermission( + fieldDescribe, + new FieldPermissions( + ParentId = this.getPermissionSetId(), + SobjectType = this.getSObjectType(), + Field = this.getSObjectType() + '.' + fieldDescribe.getName() + ) + ); + this.fieldPermissionsByField.put( + fieldPermission.getField(), + fieldPermission + ); + this.allPermissions.add(fieldPermission); + } + return this.fieldPermissionsByField.get(fieldDescribe.getName()); + } + + public TestUser.ObjectPermission deleteAllFieldPermissions() { + for ( + TestUser.FieldPermission fieldPermission : this.fieldPermissionsByField.values() + ) { + fieldPermission.setRead(false); + fieldPermission.setEdit(false); + fieldPermission.permission.put('Id', null); + fieldPermission.isChanged = false; + } + Database.delete( + [ + SELECT Id + FROM FieldPermissions + WHERE + ParentId = :this.getPermissionSetId() + AND SobjectType = :this.getSObjectType() + ] + ); + return this; + } + } + + public static void save(List permissions) { + final Set idsToDelete = new Set(); + final Map> permissionsToInsertBySObjectType = new Map>(); + final Map> permissionsToUpdateBySObjectType = new Map>(); + + for (TestUser.Permission permission : permissions) { + final Object saveRequest = permission.getSaveRequest(); + if (saveRequest instanceof Id) { + idsToDelete.add((Id) saveRequest); + } else if (saveRequest instanceof SObject) { + final SObject record = (SObject) saveRequest; + + Map> permissionsMap = record.get('Id') == null + ? permissionsToInsertBySObjectType + : permissionsToUpdateBySObjectType; + + if (permissionsMap.get(record.getSObjectType()) == null) { + permissionsMap.put(record.getSObjectType(), new List()); + } + permissionsMap.get(record.getSObjectType()).add(record); + } + } + + Database.delete(new List(idsToDelete)); + for ( + List permissionsToInsert : permissionsToInsertBySObjectType.values() + ) { + Database.insert(permissionsToInsert); + } + for ( + List permissionsToUpdate : permissionsToUpdateBySObjectType.values() + ) { + Database.update(permissionsToUpdate); + } + } + + public static Boolean isRelationship(Schema.DescribeFieldResult fieldDescribe) { + return fieldDescribe != null && fieldDescribe.getType() == DisplayType.REFERENCE; + } + + public static Boolean isMasterDetail(Schema.DescribeFieldResult fieldDescribe) { + return TestUser.isRelationship(fieldDescribe) && + fieldDescribe.getRelationshipOrder() != null; + } + + public static Boolean isLookup(Schema.DescribeFieldResult fieldDescribe) { + return TestUser.isRelationship(fieldDescribe) && + !TestUser.isMasterDetail(fieldDescribe); + } +} diff --git a/force-app/main/default/classes/testUtils/TestUser/TestUser.cls-meta.xml b/force-app/main/default/classes/testUtils/TestUser/TestUser.cls-meta.xml new file mode 100644 index 0000000..8e4d11f --- /dev/null +++ b/force-app/main/default/classes/testUtils/TestUser/TestUser.cls-meta.xml @@ -0,0 +1,5 @@ + + + 49.0 + Active + From 7dfc5ad5992e3a3dae6fa85197bc648a68d49dd0 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Wed, 10 Feb 2021 08:43:42 -0500 Subject: [PATCH 06/24] Moved LookupSearchResult to GauExpendituresManager - Moved LookupSearchResult functionality to GauExpendituresManager. - Temporarily setting `dev.json` to create orgs in a prerelease pod. - Added shell test classes. --- .../GauExpendituresManager.cls | 69 +++++++++++- .../GauLookupController.cls | 54 ---------- .../manageExpenditures/LookupSearchResult.cls | 50 --------- .../LookupSearchResult.cls-meta.xml | 5 - ...st.cls => GauExpendituresManager_TEST.cls} | 2 +- .../GauExpendituresManager_TEST.cls-meta.xml} | 0 .../tests/GauLookupControllerTest.cls | 100 ------------------ .../GauLookupControllerTest.cls-meta.xml | 5 - .../GauExpenditure/GauExpenditureSelector.cls | 15 +++ .../tests/GauExpenditureSelector_TEST.cls | 3 + .../GauExpenditureSelector_TEST.cls-meta.xml} | 2 +- .../gauExpenditureRow/gauExpenditureRow.js | 3 +- force-app/main/default/lwc/lookup/lookup.js | 1 + orgs/dev.json | 1 + 14 files changed, 88 insertions(+), 222 deletions(-) delete mode 100755 force-app/main/default/classes/controllers/manageExpenditures/GauLookupController.cls delete mode 100644 force-app/main/default/classes/controllers/manageExpenditures/LookupSearchResult.cls delete mode 100644 force-app/main/default/classes/controllers/manageExpenditures/LookupSearchResult.cls-meta.xml rename force-app/main/default/classes/controllers/manageExpenditures/tests/{GauExpendituresManagerTest.cls => GauExpendituresManager_TEST.cls} (99%) rename force-app/main/default/classes/controllers/manageExpenditures/{GauLookupController.cls-meta.xml => tests/GauExpendituresManager_TEST.cls-meta.xml} (100%) mode change 100644 => 100755 delete mode 100755 force-app/main/default/classes/controllers/manageExpenditures/tests/GauLookupControllerTest.cls delete mode 100644 force-app/main/default/classes/controllers/manageExpenditures/tests/GauLookupControllerTest.cls-meta.xml create mode 100644 force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls rename force-app/main/default/classes/{controllers/manageExpenditures/tests/GauExpendituresManagerTest.cls-meta.xml => services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls-meta.xml} (80%) mode change 100755 => 100644 diff --git a/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls b/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls index a8dfa04..d7d51bb 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls +++ b/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls @@ -6,7 +6,7 @@ */ public with sharing class GauExpendituresManager { @TestVisible - private static GanExpenditureSelector gauExpenditureSelector = new GanExpenditureSelector(); + private static GauExpenditureSelector gauExpenditureSelector = new GauExpenditureSelector(); @TestVisible private static DatabaseService databaseService = new DatabaseService(); @@ -87,7 +87,8 @@ public with sharing class GauExpendituresManager { // Delete child records not contained in expendituresString. GauExpendituresManager.databaseService.deleteRecords( GauExpendituresManager.gauExpenditureSelector.getExpendituresToDelete( - disbursementId + disbursementId, + expendituresToUpdate.keySet() ) ); @@ -96,7 +97,7 @@ public with sharing class GauExpendituresManager { expendituresToInsert ); GauExpendituresManager.databaseService.updateRecordsEnforceFls( - expendituresToUpdate + expendituresToUpdate.values() ); } catch (Exception e) { // Rollback transaction. @@ -111,21 +112,55 @@ public with sharing class GauExpendituresManager { } } + /** + * Search npsp__General_Accounting_Unit__c records + * @param searchTerm String to be used to match against records + * @param selectedIds Optional List of Ids to exclude (w/ multi-select lookup) + * @return Wrappers to give to component + */ + @AuraEnabled(cacheable=true) + public static List searchActiveGeneralAccountingUnitsLikeName( + String searchTerm + ) { + final List lookupSearchResults = new List(); + for ( + Schema.npsp__General_Accounting_Unit__c generalAccountingUnit : GauExpendituresManager.gauExpenditureSelector.getActiveGeneralAccountUnitsLikeName( + searchTerm + ) + ) { + final GauExpendituresManager.LookupSearchResult lookupSearchResult = new GauExpendituresManager.LookupSearchResult(); + lookupSearchResult.id = generalAccountingUnit.Id; + lookupSearchResult.sObjectType = Schema.SObjectType.npsp__General_Accounting_Unit__c.getName(); + lookupSearchResult.icon = 'custom:custom87'; + lookupSearchResult.title = generalAccountingUnit.Name; + lookupSearchResult.subtitle = generalAccountingUnit.npsp__Description__c; + + lookupSearchResults.add(lookupSearchResult); + } + + return lookupSearchResults; + } + /** * Wrapper class for the outfunds__Disbursement__c object */ public class DisbursementWrapper { @AuraEnabled public Id recordId; + @AuraEnabled public String name; + @AuraEnabled public Decimal amount; + @AuraEnabled public String status; + @AuraEnabled public List expenditures; - public DisbursementWrapper(outfunds__Disbursement__c disbursement) { + + public DisbursementWrapper(Schema.outfunds__Disbursement__c disbursement) { this.recordId = disbursement.Id; this.name = disbursement.Name; this.amount = disbursement.outfunds__Amount__c; @@ -144,17 +179,26 @@ public with sharing class GauExpendituresManager { public class GauExpenditureWrapper { @AuraEnabled public Id recordId; + @AuraEnabled public Id gauId; + @AuraEnabled public String gauName; + @AuraEnabled public Decimal amount; + @AuraEnabled public Boolean gauIsActive; + @AuraEnabled public Integer rowId; - public GauExpenditureWrapper(GAU_Expenditure__c gauExpenditure, Integer rowId) { + + public GauExpenditureWrapper( + Schema.GAU_Expenditure__c gauExpenditure, + Integer rowId + ) { this.recordId = gauExpenditure.Id; this.gauId = gauExpenditure.General_Accounting_Unit__c; if (this.gauId != null) { @@ -165,4 +209,19 @@ public with sharing class GauExpendituresManager { this.rowId = rowId; } } + + /** + * Class used to serialize a single Lookup search result item + * The Lookup controller returns a List when sending search result back to Lightning + */ + public class LookupSearchResult { + @AuraEnabled + public Id id; + + @AuraEnabled + public String sObjectType, + icon, + title, + subtitle; + } } diff --git a/force-app/main/default/classes/controllers/manageExpenditures/GauLookupController.cls b/force-app/main/default/classes/controllers/manageExpenditures/GauLookupController.cls deleted file mode 100755 index e2936b5..0000000 --- a/force-app/main/default/classes/controllers/manageExpenditures/GauLookupController.cls +++ /dev/null @@ -1,54 +0,0 @@ -/******************************************************************************* - * @author Thom Behrens - * @date 2019-11-16 - * - * @description Class containing LWC exposed static search methods & helpers - */ -public with sharing class GauLookupController { - /***************************************************************************** - * @description dictates maximum number of results returned from query - */ - public final static Integer MAX_RESULTS = 5; - - /***************************************************************************** - * @description dictates icon to be used alongside results - see link below - * https://www.lightningdesignsystem.com/icons/ - */ - public final static String ICON_NAME = 'custom:custom87'; - - /***************************************************************************** - * @description search npsp__General_Accounting_Unit__c records - * @param searchTerm: String to be used to match against records - * @param selectedIds: Optional List of Ids to exclude (w/ multi-select lookup) - * @return List wrappers to give to component - * @example - */ - @AuraEnabled(Cacheable=true) - public static List search( - String searchTerm, - List selectedIds - ) { - searchTerm = String.escapeSingleQuotes(searchTerm) + '%'; - List gaus = [ - SELECT Name, Id, npsp__Description__c - FROM npsp__General_Accounting_Unit__c - WHERE Name LIKE :searchTerm AND npsp__Active__c = TRUE - LIMIT :MAX_RESULTS - ]; - - List lookupSearchResults = new List(); - for (npsp__General_Accounting_Unit__c eachGau : gaus) { - lookupSearchResults.add( - new lookupSearchResult( - eachGau.Id, - 'npsp__General_Accounting_Unit__c', - ICON_NAME, - eachGau.Name, - eachGau.npsp__Description__c - ) - ); - } - - return lookupSearchResults; - } -} diff --git a/force-app/main/default/classes/controllers/manageExpenditures/LookupSearchResult.cls b/force-app/main/default/classes/controllers/manageExpenditures/LookupSearchResult.cls deleted file mode 100644 index 03715c5..0000000 --- a/force-app/main/default/classes/controllers/manageExpenditures/LookupSearchResult.cls +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Class used to serialize a single Lookup search result item - * The Lookup controller returns a List when sending search result back to Lightning - */ -public class LookupSearchResult { - private Id id; - private String sObjectType; - private String icon; - private String title; - private String subtitle; - - public LookupSearchResult( - Id id, - String sObjectType, - String icon, - String title, - String subtitle - ) { - this.id = id; - this.sObjectType = sObjectType; - this.icon = icon; - this.title = title; - this.subtitle = subtitle; - } - - @AuraEnabled - public Id getId() { - return id; - } - - @AuraEnabled - public String getSObjectType() { - return sObjectType; - } - - @AuraEnabled - public String getIcon() { - return icon; - } - - @AuraEnabled - public String getTitle() { - return title; - } - - @AuraEnabled - public String getSubtitle() { - return subtitle; - } -} diff --git a/force-app/main/default/classes/controllers/manageExpenditures/LookupSearchResult.cls-meta.xml b/force-app/main/default/classes/controllers/manageExpenditures/LookupSearchResult.cls-meta.xml deleted file mode 100644 index d2105bb..0000000 --- a/force-app/main/default/classes/controllers/manageExpenditures/LookupSearchResult.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 47.0 - Active - diff --git a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManagerTest.cls b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls similarity index 99% rename from force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManagerTest.cls rename to force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls index 1d98b40..210493e 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManagerTest.cls +++ b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls @@ -1,5 +1,5 @@ @IsTest -public with sharing class GauExpendituresManagerTest { +public with sharing class GauExpendituresManager_TEST { /***************************************************************************** * @description Create records needed for test and set class-level properties */ diff --git a/force-app/main/default/classes/controllers/manageExpenditures/GauLookupController.cls-meta.xml b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls-meta.xml old mode 100644 new mode 100755 similarity index 100% rename from force-app/main/default/classes/controllers/manageExpenditures/GauLookupController.cls-meta.xml rename to force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls-meta.xml diff --git a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauLookupControllerTest.cls b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauLookupControllerTest.cls deleted file mode 100755 index 6b29e3a..0000000 --- a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauLookupControllerTest.cls +++ /dev/null @@ -1,100 +0,0 @@ -@IsTest -public with sharing class GauLookupControllerTest { - @IsTest - public static void happyPath() { - insert new npsp__General_Accounting_Unit__c(Name = 'Unit 1'); - Test.startTest(); - List lookupSearchResults = GauLookupController.search( - 'Unit 1', - new List() - ); - Test.stopTest(); - System.assertEquals( - 1, - lookupSearchResults.size(), - 'Wrong number of search results' - ); - } - - @IsTest - public static void testInactive() { - insert new npsp__General_Accounting_Unit__c( - Name = 'Unit 1', - npsp__Active__c = false - ); - Test.startTest(); - List lookupSearchResults = GauLookupController.search( - 'Unit 1', - new List() - ); - Test.stopTest(); - System.assertEquals( - 0, - lookupSearchResults.size(), - 'did not expect to receive inactive GAUs' - ); - } - - @IsTest - public static void testMaxResults() { - List gaus = new List(); - Integer tooMany = GauLookupController.MAX_RESULTS + 1; - for (Integer itr = 0; itr < tooMany; itr++) { - gaus.add( - new npsp__General_Accounting_Unit__c( - Name = 'Unit ' + String.valueof(DateTime.now().getTime()) - ) - ); - } - insert gaus; - Test.startTest(); - List lookupSearchResults = GauLookupController.search( - 'Unit', - new List() - ); - Test.stopTest(); - System.assertEquals( - GauLookupController.MAX_RESULTS, - lookupSearchResults.size(), - 'Wrong number of search results' - ); - } - - @IsTest - public static void testGetters() { - npsp__General_Accounting_Unit__c gau = new npsp__General_Accounting_Unit__c( - Name = 'Unit 1', - npsp__Description__c = 'In publishing and graphic design, Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content.' - ); - insert gau; - Test.startTest(); - List lookupSearchResults = GauLookupController.search( - 'Unit 1', - new List() - ); - Test.stopTest(); - System.assertEquals( - 1, - lookupSearchResults.size(), - 'Wrong number of search results' - ); - LookupSearchResult result = lookupSearchResults[0]; - System.assertEquals(gau.Id, result.getId(), 'getId() failed to load correct id.'); - System.assertEquals( - 'npsp__General_Accounting_Unit__c', - result.getSObjectType(), - 'Wrong SObject type provided' - ); - System.assertEquals( - GauLookupController.ICON_NAME, - result.getIcon(), - 'Wrong icon provided' - ); - System.assertEquals(gau.Name, result.getTitle(), 'Wrong title provided'); - System.assertEquals( - gau.npsp__Description__c, - result.getSubtitle(), - 'Wrong subtitle provided' - ); - } -} diff --git a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauLookupControllerTest.cls-meta.xml b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauLookupControllerTest.cls-meta.xml deleted file mode 100644 index 252fbfd..0000000 --- a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauLookupControllerTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 47.0 - Active - diff --git a/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls b/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls index 08c64f7..b34cde9 100644 --- a/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls +++ b/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls @@ -52,4 +52,19 @@ public with sharing class GauExpenditureSelector { } return expendituresToDelete; } + + public List getActiveGeneralAccountUnitsLikeName( + String nameLikeClause + ) { + final String name = + (nameLikeClause == null ? '' : String.escapeSingleQuotes(nameLikeClause)) + + '%'; + return [ + SELECT Name, Id, npsp__Description__c + FROM npsp__General_Accounting_Unit__c + WHERE Name LIKE :name AND npsp__Active__c = TRUE + WITH SECURITY_ENFORCED + LIMIT 5 + ]; + } } diff --git a/force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls b/force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls new file mode 100644 index 0000000..7168ee5 --- /dev/null +++ b/force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls @@ -0,0 +1,3 @@ +@IsTest +public with sharing class GauExpenditureSelector_TEST { +} diff --git a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManagerTest.cls-meta.xml b/force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls-meta.xml old mode 100755 new mode 100644 similarity index 80% rename from force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManagerTest.cls-meta.xml rename to force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls-meta.xml index 252fbfd..541584f --- a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManagerTest.cls-meta.xml +++ b/force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls-meta.xml @@ -1,5 +1,5 @@ - 47.0 + 50.0 Active diff --git a/force-app/main/default/lwc/gauExpenditureRow/gauExpenditureRow.js b/force-app/main/default/lwc/gauExpenditureRow/gauExpenditureRow.js index 6d32127..d1a807c 100755 --- a/force-app/main/default/lwc/gauExpenditureRow/gauExpenditureRow.js +++ b/force-app/main/default/lwc/gauExpenditureRow/gauExpenditureRow.js @@ -1,9 +1,10 @@ import { LightningElement, track, api } from "lwc"; -import apexSearch from "@salesforce/apex/GauLookupController.search"; +import apexSearch from "@salesforce/apex/GauExpendituresManager.searchActiveGeneralAccountingUnitsLikeName"; // TODO: Localize with Custom Labels. const PERCENT_SIGN = "%"; const DELETE_LABEL = "Delete"; + export default class GauExpenditureRow extends LightningElement { labels = { percentSign: PERCENT_SIGN, diff --git a/force-app/main/default/lwc/lookup/lookup.js b/force-app/main/default/lwc/lookup/lookup.js index 2383053..c7b9406 100644 --- a/force-app/main/default/lwc/lookup/lookup.js +++ b/force-app/main/default/lwc/lookup/lookup.js @@ -10,6 +10,7 @@ const SEARCH_ICON_ALTERNATIVE_TEXT = "Search icon"; const REMOVE_BUTTON_ALTERNATIVE_TEXT = "Remove selected option"; const RESULT_ICON_ALTERNATIVE_TEXT = "Result item icon"; const MULTI_ENTRY_ARIA_LABEL = "Selected Options:"; + export default class Lookup extends LightningElement { labels = { selectIcon: { diff --git a/orgs/dev.json b/orgs/dev.json index 0fc172b..7ea062d 100644 --- a/orgs/dev.json +++ b/orgs/dev.json @@ -1,4 +1,5 @@ { + "instance": "cs46", "orgName": "Outbound Funds (npsp) - Dev Org", "edition": "Developer", "settings": { From 28b6af7f5fc2160677d810dfa9a4e0fa4773e361 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Wed, 10 Feb 2021 15:32:21 -0500 Subject: [PATCH 07/24] wip got GauExpenditureSelector test --- .../tests/GauExpenditureSelector_TEST.cls | 468 ++++++++++++++++++ .../classes/testUtils/TestUser/TestUser.cls | 5 + 2 files changed, 473 insertions(+) diff --git a/force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls b/force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls index 7168ee5..d5cd59b 100644 --- a/force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls +++ b/force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls @@ -1,3 +1,471 @@ @IsTest public with sharing class GauExpenditureSelector_TEST { + private static TestUser.MinimalAccessPersona minimalAccessPersona = new TestUser.MinimalAccessPersona(); + + @TestSetup + private static void testSetup() { + TestUser.insertPersonasInTestSetup( + new List{ GauExpenditureSelector_TEST.minimalAccessPersona } + ); + + GauExpenditureSelector_TEST.minimalAccessPersona.load(); + + final outfunds__Funding_Request__c fundingRequest = new outfunds__Funding_Request__c( + // Setting Test User as Owner to grant sharing access so we can test object-level and field-level permissions. + OwnerId = GauExpenditureSelector_TEST.minimalAccessPersona.getUser().Id + ); + insert fundingRequest; + + final outfunds__Disbursement__c disbursement = new outfunds__Disbursement__c( + outfunds__Funding_Request__c = fundingRequest.Id, + outfunds__Amount__c = 1000, + outfunds__Status__c = 'Scheduled' + ); + insert disbursement; + + final List generalAccountingUnits = new List{ + new npsp__General_Accounting_Unit__c(Name = 'GAU 0', npsp__Active__c = true), + new npsp__General_Accounting_Unit__c(Name = 'GAU 1', npsp__Active__c = false), + new npsp__General_Accounting_Unit__c(Name = 'GAU 2', npsp__Active__c = true), + new npsp__General_Accounting_Unit__c(Name = 'GAU 3', npsp__Active__c = true), + new npsp__General_Accounting_Unit__c(Name = 'GAU 4', npsp__Active__c = true), + new npsp__General_Accounting_Unit__c(Name = 'GAU 5', npsp__Active__c = true) + }; + insert generalAccountingUnits; + + final List expenditures = new List{ + new GAU_Expenditure__c( + Disbursement__c = disbursement.Id, + General_Accounting_Unit__c = generalAccountingUnits[0].Id, + Amount__c = 700 + ), + new GAU_Expenditure__c( + Disbursement__c = disbursement.Id, + General_Accounting_Unit__c = generalAccountingUnits[1].Id, + Amount__c = 200 + ), + new GAU_Expenditure__c( + Disbursement__c = disbursement.Id, + General_Accounting_Unit__c = generalAccountingUnits[2].Id, + Amount__c = 100 + ) + }; + insert expenditures; + } + + public static outfunds__Disbursement__c getDisbursement() { + return [ + SELECT Id, (SELECT Id FROM GAU_Expendatures__r) + FROM outfunds__Disbursement__c + LIMIT 1 + ][0]; + } + + @IsTest + private static void getDisbursementsById_WithPermissions() { + final Id nullId; + final Id nonDisbursementId = UnitTest.mockId(Schema.SObjectType.Account); + final Id disbursementId = GauExpenditureSelector_TEST.getDisbursement().Id; + + Test.startTest(); + + // Assert has permissions to read Disbursements, GAU Expenditures, and all fields since we are running as a user who can author Apex. + System.assert(Schema.SObjectType.outfunds__Disbursement__c.isAccessible()); + System.assert( + Schema.SObjectType.outfunds__Disbursement__c.fields.Name.isAccessible() + ); + System.assert( + Schema.SObjectType.outfunds__Disbursement__c.fields.outfunds__Amount__c.isAccessible() + ); + System.assert( + Schema.SObjectType.outfunds__Disbursement__c.fields.outfunds__Status__c.isAccessible() + ); + + System.assert(Schema.SObjectType.GAU_Expenditure__c.isAccessible()); + System.assert( + Schema.SObjectType.GAU_Expenditure__c.fields.Amount__c.isAccessible() + ); + System.assert( + Schema.SObjectType.GAU_Expenditure__c.fields.General_Accounting_Unit__c.isAccessible() + ); + + System.assert(Schema.SObjectType.npsp__General_Accounting_Unit__c.isAccessible()); + System.assert( + Schema.SObjectType.npsp__General_Accounting_Unit__c.fields.Name.isAccessible() + ); + System.assert( + Schema.SObjectType.npsp__General_Accounting_Unit__c.fields.npsp__Active__c.isAccessible() + ); + + // Run the test. + final GauExpenditureSelector service = new GauExpenditureSelector(); + + final List disbursementsWhenNullId = service.getDisbursementsById( + nullId + ); + + final List disbursementsWhenNonDisbursementId = service.getDisbursementsById( + nonDisbursementId + ); + + final List actual = service.getDisbursementsById( + disbursementId + ); + + Test.stopTest(); + + // When disbursementId is null. + System.assertNotEquals( + null, + disbursementsWhenNullId, + 'GauExpenditureSelector.getDisbursementsById should never return null.' + ); + + System.assert( + disbursementsWhenNullId.isEmpty(), + 'GauExpenditureSelector.getDisbursementsById should return empty when disbursementId is null.' + ); + + // When disbursementId does not belong to a non-deleted outfunds__Disbursement__c record. + System.assertNotEquals( + null, + disbursementsWhenNonDisbursementId, + 'GauExpenditureSelector.getDisbursementsById should never return null.' + ); + + System.assert( + disbursementsWhenNonDisbursementId.isEmpty(), + 'GauExpenditureSelector.getDisbursementsById should return empty when disbursementId does not belong to a non-deleted outfunds__Disbursement__c record.' + ); + + // When disbursementId DOES belong to a non-deleted outfunds__Disbursement__c record. + System.assertNotEquals( + null, + actual, + 'GauExpenditureSelector.getDisbursementsById should never return null.' + ); + + System.assertEquals( + 1, + actual.size(), + 'GauExpenditureSelector.getDisbursementsById should return one record.' + ); + + // Assert Disbursement with disbursementId was queried and Name, Amount, and Status fields are queried. + System.assertEquals(disbursementId, actual[0].Id); + System.assertNotEquals(null, actual[0].Name); + System.assertEquals(1000, actual[0].outfunds__Amount__c); + System.assertEquals('Scheduled', actual[0].outfunds__Status__c); + + System.assertEquals(3, actual[0].GAU_Expendatures__r.size()); + + Map expendituresByGauName = new Map(); + for (GAU_Expenditure__c expenditure : actual[0].GAU_Expendatures__r) { + expendituresByGauName.put( + expenditure.General_Accounting_Unit__r.Name, + expenditure + ); + } + + { + final GAU_Expenditure__c expenditure = expendituresByGauName.get('GAU 0'); + + System.assertNotEquals(null, expenditure); + System.assertEquals( + true, + expenditure.General_Accounting_Unit__r.npsp__Active__c + ); + System.assertEquals(700, expenditure.Amount__c); + } + + { + final GAU_Expenditure__c expenditure = expendituresByGauName.get('GAU 1'); + + System.assertNotEquals(null, expenditure); + System.assertEquals( + false, + expenditure.General_Accounting_Unit__r.npsp__Active__c + ); + System.assertEquals(200, expenditure.Amount__c); + } + + { + final GAU_Expenditure__c expenditure = expendituresByGauName.get('GAU 2'); + + System.assertNotEquals(null, expenditure); + System.assertEquals( + true, + expenditure.General_Accounting_Unit__r.npsp__Active__c + ); + System.assertEquals(100, expenditure.Amount__c); + } + } + + @IsTest + private static void getDisbursementsById_MissingDisbursementCrud() { + // Set arguments. + final Id disbursementId = GauExpenditureSelector_TEST.getDisbursement().Id; + + // Configure permissions. + GauExpenditureSelector_TEST.minimalAccessPersona.load(); + + Test.startTest(); + + System.QueryException actualException; + + System.runAs(GauExpenditureSelector_TEST.minimalAccessPersona.getUser()) { + // Assert Test User has expected permissions. + System.assertEquals( + false, + Schema.outfunds__Disbursement__c.SObjectType.getDescribe().isAccessible() + ); + System.assertEquals( + false, + Schema.outfunds__Disbursement__c.SObjectType.fields.Name.getDescribe() + .isAccessible() + ); + System.assertEquals( + false, + Schema.outfunds__Disbursement__c.SObjectType.fields.outfunds__Amount__c.getDescribe() + .isAccessible() + ); + System.assertEquals( + false, + Schema.outfunds__Disbursement__c.SObjectType.fields.outfunds__Status__c.getDescribe() + .isAccessible() + ); + + System.assertEquals( + false, + Schema.GAU_Expenditure__c.SObjectType.getDescribe().isAccessible() + ); + System.assertEquals( + false, + Schema.GAU_Expenditure__c.SObjectType.fields.Amount__c.getDescribe() + .isAccessible() + ); + System.assertEquals( + false, + Schema.GAU_Expenditure__c.SObjectType.fields.General_Accounting_Unit__c.getDescribe() + .isAccessible() + ); + + System.assertEquals( + false, + Schema.npsp__General_Accounting_Unit__c.SObjectType.getDescribe() + .isAccessible() + ); + System.assertEquals( + false, + Schema.npsp__General_Accounting_Unit__c.SObjectType.fields.Name.getDescribe() + .isAccessible() + ); + System.assertEquals( + false, + Schema.npsp__General_Accounting_Unit__c.SObjectType.fields.npsp__Active__c.getDescribe() + .isAccessible() + ); + + // Run the test. + final GauExpenditureSelector service = new GauExpenditureSelector(); + + try { + service.getDisbursementsById(disbursementId); + } catch (System.QueryException e) { + actualException = e; + } + } + + Test.stopTest(); + + System.assertNotEquals( + null, + actualException, + 'A System.QueryException should have been thrown because the user cannot read outfunds__Disbursement__c.' + ); + } + + @IsTest + private static void getExpendituresToDelete_WithPermissions() { + // Set arguments and expected values. + final outfunds__Disbursement__c disbursement = GauExpenditureSelector_TEST.getDisbursement(); + + final Id nullDisbursementId; + final Id nonDisbursementId = UnitTest.mockId(Schema.SObjectType.Account); + final Id disbursementId = GauExpenditureSelector_TEST.getDisbursement(); + + final Set nullExpenditureIdsToKeep; + final Set emptyExpenditureIdsToKeep = new Set(); + final Set firstExpenditureIdsToKeep = new Set(); + + final List expected = new List(); + + { + Boolean isFirst = true; + for (GAU_Expenditure__c expenditure : disbursement.GAU_Expendatures__r) { + if (isFirst) { + firstExpenditureIdsToKeep.add(expenditure.Id); + isFirst = false; + } else { + expected.add(new GAU_Expenditure__c(Id = expenditure.Id)); + } + } + } + + Test.startTest(); + + // Assert has permissions to read GAU Expenditures. + System.assert(Schema.SObjectType.GAU_Expenditure__c.isAccessible()); + + System.assert( + Schema.SObjectType.GAU_Expenditure__c.fields.Disbursement__c.isAccessible() + ); + + // Run the test. + final GauExpenditureSelector service = new GauExpenditureSelector(); + + // An empty list should always be returned when disbursementId is null. + for ( + Set expenditureIdsToKeep : new List>{ + nullExpenditureIdsToKeep, + emptyExpenditureIdsToKeep, + firstExpenditureIdsToKeep + } + ) { + final List actual = selector.getExpendituresToDelete( + nullDisbursementId, + expenditureIdsToKeep + ); + + System.assertNotEquals( + null, + actual, + 'GauExpenditureSelector.getExpendituresToDelete should never return null.' + ); + System.assert( + actual.isEmpty(), + 'GauExpenditureSelector.getExpendituresToDelete should always return an empty list for a null disbursementId.' + ); + } + + // An empty list should always be returned when disbursementId is not related to a non-deleted Disbursement record. + for ( + Set expenditureIdsToKeep : new List>{ + nullExpenditureIdsToKeep, + emptyExpenditureIdsToKeep, + firstExpenditureIdsToKeep + } + ) { + final List actual = selector.getExpendituresToDelete( + nonDisbursementId, + expenditureIdsToKeep + ); + + System.assertNotEquals( + null, + actual, + 'GauExpenditureSelector.getExpendituresToDelete should never return null.' + ); + System.assert( + actual.isEmpty(), + 'GauExpenditureSelector.getExpendituresToDelete should always return an empty list for a disbursementId not related to a non-deleted Disbursement record.' + ); + } + + final List actualWhenNullExpenditureIdsToKeep = selector.getExpendituresToDelete( + disbursementId, + nullExpenditureIdsToKeep + ); + + final List actualWhenEmptyExpenditureIdsToKeep = selector.getExpendituresToDelete( + disbursementId, + emptyExpenditureIdsToKeep + ); + + final List actualWhenFirstExpenditureIdsToKeep = selector.getExpendituresToDelete( + disbursementId, + firstExpenditureIdsToKeep + ); + + Test.stopTest(); + + // actualWhenNullExpenditureIdsToKeep. + System.assertNotEquals( + null, + actualWhenNullExpenditureIdsToKeep, + 'GauExpenditureSelector.getExpendituresToDelete should never return null.' + ); + System.assertEquals( + disbursement.GAU_Expenditures__r, + actualWhenNullExpenditureIdsToKeep, + 'GauExpenditureSelector.getExpendituresToDelete should return all list of child GAU Expenditure.' + ); + + // actualWhenEmptyExpenditureIdsToKeep. + System.assertNotEquals( + null, + actualWhenEmptyExpenditureIdsToKeep, + 'GauExpenditureSelector.getExpendituresToDelete should never return null.' + ); + System.assertEquals( + disbursement.GAU_Expenditures__r, + actualWhenEmptyExpenditureIdsToKeep, + 'GauExpenditureSelector.getExpendituresToDelete should return all list of child GAU Expenditure.' + ); + + // actualWhenFirstExpenditureIdsToKeep. + System.assertNotEquals( + null, + actualWhenFirstExpenditureIdsToKeep, + 'GauExpenditureSelector.getExpendituresToDelete should never return null.' + ); + System.assertEquals( + expected, + actualWhenFirstExpenditureIdsToKeep, + 'GauExpenditureSelector.getExpendituresToDelete should return only the last two children GAU Expenditure.' + ); + } + + @IsTest + private static void getExpendituresToDelete_MissingDisbursementCrud() { + // Set arguments. + final Id disbursementId = GauExpenditureSelector_TEST.getDisbursement().Id; + final Set expenditureIdsToKeep = new Set(); + + // Configure permissions. + GauExpenditureSelector_TEST.minimalAccessPersona.load(); + + Test.startTest(); + + System.QueryException actualException; + + System.runAs(GauExpenditureSelector_TEST.minimalAccessPersona.getUser()) { + // Assert Test User has expected permissions. + System.assertEquals( + false, + Schema.GAU_Expenditure__c.SObjectType.getDescribe().isAccessible() + ); + + System.assert( + Schema.SObjectType.GAU_Expenditure__c.fields.Disbursement__c.isAccessible() + ); + + // Run the test. + final GauExpenditureSelector service = new GauExpenditureSelector(); + + try { + service.getExpendituresToDelete(disbursementId, expenditureIdsToKeep); + } catch (System.QueryException e) { + actualException = e; + } + } + + Test.stopTest(); + + System.assertNotEquals( + null, + actualException, + 'A System.QueryException should have been thrown because the user cannot read GAU_Expenditure__c.' + ); + } } diff --git a/force-app/main/default/classes/testUtils/TestUser/TestUser.cls b/force-app/main/default/classes/testUtils/TestUser/TestUser.cls index 6f03bda..f4aff24 100644 --- a/force-app/main/default/classes/testUtils/TestUser/TestUser.cls +++ b/force-app/main/default/classes/testUtils/TestUser/TestUser.cls @@ -573,6 +573,11 @@ public with sharing class TestUser { return this.permissionSetManagersById.values(); } + public Set getPermissionSetIds() { + this.assertLoaded(); + return this.permissionSetManagersById.keySet(); + } + public TestUser.PermissionSetManager getPermissionSetManager(Id permissionSetId) { this.assertLoaded(); return this.permissionSetManagersById.get(permissionSetId); From 613fe90d05fe4b8f774ead0913e8531aed324619 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Wed, 10 Feb 2021 16:28:31 -0500 Subject: [PATCH 08/24] Tested GauExpenditureSelector - Added tests for all methods in `GauExpenditureSelector`. - Added tests that verify all SOQL queries are made `WITH SECURITY_ENFORCED`. --- .../GauExpenditure/GauExpenditureSelector.cls | 8 +- .../tests/GauExpenditureSelector_TEST.cls | 251 ++++++++++++++++-- 2 files changed, 237 insertions(+), 22 deletions(-) diff --git a/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls b/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls index b34cde9..89ba2a8 100644 --- a/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls +++ b/force-app/main/default/classes/services/GauExpenditure/GauExpenditureSelector.cls @@ -56,11 +56,11 @@ public with sharing class GauExpenditureSelector { public List getActiveGeneralAccountUnitsLikeName( String nameLikeClause ) { - final String name = - (nameLikeClause == null ? '' : String.escapeSingleQuotes(nameLikeClause)) + - '%'; + final String name = String.isBlank(nameLikeClause) + ? '%' + : String.escapeSingleQuotes(nameLikeClause) + '%'; return [ - SELECT Name, Id, npsp__Description__c + SELECT Id, Name, npsp__Description__c FROM npsp__General_Accounting_Unit__c WHERE Name LIKE :name AND npsp__Active__c = TRUE WITH SECURITY_ENFORCED diff --git a/force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls b/force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls index d5cd59b..afed7a9 100644 --- a/force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls +++ b/force-app/main/default/classes/services/GauExpenditure/tests/GauExpenditureSelector_TEST.cls @@ -24,12 +24,42 @@ public with sharing class GauExpenditureSelector_TEST { insert disbursement; final List generalAccountingUnits = new List{ - new npsp__General_Accounting_Unit__c(Name = 'GAU 0', npsp__Active__c = true), - new npsp__General_Accounting_Unit__c(Name = 'GAU 1', npsp__Active__c = false), - new npsp__General_Accounting_Unit__c(Name = 'GAU 2', npsp__Active__c = true), - new npsp__General_Accounting_Unit__c(Name = 'GAU 3', npsp__Active__c = true), - new npsp__General_Accounting_Unit__c(Name = 'GAU 4', npsp__Active__c = true), - new npsp__General_Accounting_Unit__c(Name = 'GAU 5', npsp__Active__c = true) + new npsp__General_Accounting_Unit__c( + // Setting Test User as Owner to grant sharing access so we can test object-level and field-level permissions. + OwnerId = GauExpenditureSelector_TEST.minimalAccessPersona.getUser().Id, + Name = 'GAU 0', + npsp__Active__c = true + ), + new npsp__General_Accounting_Unit__c( + // Setting Test User as Owner to grant sharing access so we can test object-level and field-level permissions. + OwnerId = GauExpenditureSelector_TEST.minimalAccessPersona.getUser().Id, + Name = 'GAU 1', + npsp__Active__c = false + ), + new npsp__General_Accounting_Unit__c( + // Setting Test User as Owner to grant sharing access so we can test object-level and field-level permissions. + OwnerId = GauExpenditureSelector_TEST.minimalAccessPersona.getUser().Id, + Name = 'GAU 2', + npsp__Active__c = true + ), + new npsp__General_Accounting_Unit__c( + // Setting Test User as Owner to grant sharing access so we can test object-level and field-level permissions. + OwnerId = GauExpenditureSelector_TEST.minimalAccessPersona.getUser().Id, + Name = 'GAU 3', + npsp__Active__c = true + ), + new npsp__General_Accounting_Unit__c( + // Setting Test User as Owner to grant sharing access so we can test object-level and field-level permissions. + OwnerId = GauExpenditureSelector_TEST.minimalAccessPersona.getUser().Id, + Name = 'GAU 4', + npsp__Active__c = true + ), + new npsp__General_Accounting_Unit__c( + // Setting Test User as Owner to grant sharing access so we can test object-level and field-level permissions. + OwnerId = GauExpenditureSelector_TEST.minimalAccessPersona.getUser().Id, + Name = 'There\\\'s an escaped single quote in this Name.', + npsp__Active__c = true + ) }; insert generalAccountingUnits; @@ -202,7 +232,7 @@ public with sharing class GauExpenditureSelector_TEST { } @IsTest - private static void getDisbursementsById_MissingDisbursementCrud() { + private static void getDisbursementsById_MissingCrud() { // Set arguments. final Id disbursementId = GauExpenditureSelector_TEST.getDisbursement().Id; @@ -292,22 +322,29 @@ public with sharing class GauExpenditureSelector_TEST { final Id nullDisbursementId; final Id nonDisbursementId = UnitTest.mockId(Schema.SObjectType.Account); - final Id disbursementId = GauExpenditureSelector_TEST.getDisbursement(); + final Id disbursementId = GauExpenditureSelector_TEST.getDisbursement().Id; final Set nullExpenditureIdsToKeep; final Set emptyExpenditureIdsToKeep = new Set(); final Set firstExpenditureIdsToKeep = new Set(); + final List allChildren = new List(); final List expected = new List(); { Boolean isFirst = true; for (GAU_Expenditure__c expenditure : disbursement.GAU_Expendatures__r) { + final GAU_Expenditure__c child = new GAU_Expenditure__c( + Id = expenditure.Id + ); + + allChildren.add(child); + if (isFirst) { firstExpenditureIdsToKeep.add(expenditure.Id); isFirst = false; } else { - expected.add(new GAU_Expenditure__c(Id = expenditure.Id)); + expected.add(child); } } } @@ -332,7 +369,7 @@ public with sharing class GauExpenditureSelector_TEST { firstExpenditureIdsToKeep } ) { - final List actual = selector.getExpendituresToDelete( + final List actual = service.getExpendituresToDelete( nullDisbursementId, expenditureIdsToKeep ); @@ -356,7 +393,7 @@ public with sharing class GauExpenditureSelector_TEST { firstExpenditureIdsToKeep } ) { - final List actual = selector.getExpendituresToDelete( + final List actual = service.getExpendituresToDelete( nonDisbursementId, expenditureIdsToKeep ); @@ -372,17 +409,17 @@ public with sharing class GauExpenditureSelector_TEST { ); } - final List actualWhenNullExpenditureIdsToKeep = selector.getExpendituresToDelete( + final List actualWhenNullExpenditureIdsToKeep = service.getExpendituresToDelete( disbursementId, nullExpenditureIdsToKeep ); - final List actualWhenEmptyExpenditureIdsToKeep = selector.getExpendituresToDelete( + final List actualWhenEmptyExpenditureIdsToKeep = service.getExpendituresToDelete( disbursementId, emptyExpenditureIdsToKeep ); - final List actualWhenFirstExpenditureIdsToKeep = selector.getExpendituresToDelete( + final List actualWhenFirstExpenditureIdsToKeep = service.getExpendituresToDelete( disbursementId, firstExpenditureIdsToKeep ); @@ -396,7 +433,7 @@ public with sharing class GauExpenditureSelector_TEST { 'GauExpenditureSelector.getExpendituresToDelete should never return null.' ); System.assertEquals( - disbursement.GAU_Expenditures__r, + allChildren, actualWhenNullExpenditureIdsToKeep, 'GauExpenditureSelector.getExpendituresToDelete should return all list of child GAU Expenditure.' ); @@ -408,7 +445,7 @@ public with sharing class GauExpenditureSelector_TEST { 'GauExpenditureSelector.getExpendituresToDelete should never return null.' ); System.assertEquals( - disbursement.GAU_Expenditures__r, + allChildren, actualWhenEmptyExpenditureIdsToKeep, 'GauExpenditureSelector.getExpendituresToDelete should return all list of child GAU Expenditure.' ); @@ -427,7 +464,7 @@ public with sharing class GauExpenditureSelector_TEST { } @IsTest - private static void getExpendituresToDelete_MissingDisbursementCrud() { + private static void getExpendituresToDelete_MissingCrud() { // Set arguments. final Id disbursementId = GauExpenditureSelector_TEST.getDisbursement().Id; final Set expenditureIdsToKeep = new Set(); @@ -446,7 +483,8 @@ public with sharing class GauExpenditureSelector_TEST { Schema.GAU_Expenditure__c.SObjectType.getDescribe().isAccessible() ); - System.assert( + System.assertEquals( + false, Schema.SObjectType.GAU_Expenditure__c.fields.Disbursement__c.isAccessible() ); @@ -468,4 +506,181 @@ public with sharing class GauExpenditureSelector_TEST { 'A System.QueryException should have been thrown because the user cannot read GAU_Expenditure__c.' ); } + + @IsTest + private static void getActiveGeneralAccountUnitsLikeName_WithPermissions_nullName() { + // Set arguments and expected values. + final List expected = [ + SELECT Id, Name, npsp__Description__c + FROM npsp__General_Accounting_Unit__c + WHERE npsp__Active__c = TRUE + LIMIT 5 + ]; + + final String nameLikeClause; + + Test.startTest(); + + // Assert has permissions to read General Accounting Units and all fields since we are running as a user who can author Apex. + System.assert(Schema.SObjectType.npsp__General_Accounting_Unit__c.isAccessible()); + System.assert( + Schema.SObjectType.npsp__General_Accounting_Unit__c.fields.Name.isAccessible() + ); + System.assert( + Schema.SObjectType.npsp__General_Accounting_Unit__c.fields.npsp__Description__c.isAccessible() + ); + + // Execute the test. + final GauExpenditureSelector service = new GauExpenditureSelector(); + + final List actual = service.getActiveGeneralAccountUnitsLikeName( + nameLikeClause + ); + + Test.stopTest(); + + System.assertEquals( + expected, + actual, + 'The first 5 active General Accounting Units should have been returned since name is null.' + ); + } + + @IsTest + private static void getActiveGeneralAccountUnitsLikeName_WithPermissions_blankName() { + // Set arguments and expected values. + final List expected = [ + SELECT Id, Name, npsp__Description__c + FROM npsp__General_Accounting_Unit__c + WHERE npsp__Active__c = TRUE + LIMIT 5 + ]; + + final String nameLikeClause = ''; + + Test.startTest(); + + // Assert has permissions to read General Accounting Units and all fields since we are running as a user who can author Apex. + System.assert(Schema.SObjectType.npsp__General_Accounting_Unit__c.isAccessible()); + System.assert( + Schema.SObjectType.npsp__General_Accounting_Unit__c.fields.Name.isAccessible() + ); + System.assert( + Schema.SObjectType.npsp__General_Accounting_Unit__c.fields.npsp__Description__c.isAccessible() + ); + + // Execute the test. + final GauExpenditureSelector service = new GauExpenditureSelector(); + + final List actual = service.getActiveGeneralAccountUnitsLikeName( + nameLikeClause + ); + + Test.stopTest(); + + System.assertEquals( + expected, + actual, + 'The first 5 active General Accounting Units should have been returned since name is blank.' + ); + } + + @IsTest + private static void getActiveGeneralAccountUnitsLikeName_WithPermissions_NameWithEscapedSingleQuote() { + // Set arguments and expected values. + final List expected = [ + SELECT Id, Name, npsp__Description__c + FROM npsp__General_Accounting_Unit__c + WHERE + Name = 'There\\\'s an escaped single quote in this Name.' + AND npsp__Active__c = TRUE + LIMIT 1 + ]; + + System.assertEquals( + 1, + expected.size(), + 'Only one npsp__General_Accounting_Unit__c should be found with the name with an escaped single quote.' + ); + + // nameLikeClause should find expected since a wild card (%) is attached to the end of the like clause. + final String nameLikeClause = 'There\'s an escaped single quote'; + + Test.startTest(); + + // Assert has permissions to read General Accounting Units and all fields since we are running as a user who can author Apex. + System.assert(Schema.SObjectType.npsp__General_Accounting_Unit__c.isAccessible()); + System.assert( + Schema.SObjectType.npsp__General_Accounting_Unit__c.fields.Name.isAccessible() + ); + System.assert( + Schema.SObjectType.npsp__General_Accounting_Unit__c.fields.npsp__Description__c.isAccessible() + ); + + // Execute the test. + final GauExpenditureSelector service = new GauExpenditureSelector(); + + final List actual = service.getActiveGeneralAccountUnitsLikeName( + nameLikeClause + ); + + Test.stopTest(); + + System.assertEquals( + expected, + actual, + 'The first 5 active General Accounting Units should have been returned since there is only one npsp__General_Accounting_Unit__c whose Name is starts with "GAU 1".' + ); + } + + @IsTest + private static void getActiveGeneralAccountUnitsLikeName_MissingCrud() { + // nameLikeClause should find expected since a wild card (%) is attached to the end of the like clause. + final String nameLikeClause = 'There\'s an escaped single quote'; + + // Load Test User without access to npsp__General_Accounting_Unit__c. + GauExpenditureSelector_TEST.minimalAccessPersona.load(); + + Test.startTest(); + + System.QueryException actualException; + + // Assert has permissions to read General Accounting Units and all fields since we are running as a user who can author Apex. + System.runAs(GauExpenditureSelector_TEST.minimalAccessPersona.getUser()) { + System.assertEquals( + false, + Schema.npsp__General_Accounting_Unit__c.SObjectType.getDescribe() + .isAccessible() + ); + System.assertEquals( + false, + Schema.npsp__General_Accounting_Unit__c.SObjectType.fields.Name.getDescribe() + .isAccessible() + ); + System.assertEquals( + false, + Schema.npsp__General_Accounting_Unit__c.SObjectType.fields.npsp__Description__c.getDescribe() + .isAccessible() + ); + + // Execute the test. + final GauExpenditureSelector service = new GauExpenditureSelector(); + + try { + final List actual = service.getActiveGeneralAccountUnitsLikeName( + nameLikeClause + ); + } catch (System.QueryException e) { + actualException = e; + } + } + + Test.stopTest(); + + System.assertNotEquals( + null, + actualException, + 'A System.QueryException should have been thrown calling GauExpenditureSelector.getActiveGeneralAccountUnitsLikeName since the Test User does not have read access to npsp__General_Accounting_Unit__c.' + ); + } } From 23852468d3b75f2a60b47cd53b3c98ec7b735f6b Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Wed, 10 Feb 2021 16:31:13 -0500 Subject: [PATCH 09/24] Removed creating dev orgs on prerelease pod. --- orgs/dev.json | 1 - 1 file changed, 1 deletion(-) diff --git a/orgs/dev.json b/orgs/dev.json index 7ea062d..0fc172b 100644 --- a/orgs/dev.json +++ b/orgs/dev.json @@ -1,5 +1,4 @@ { - "instance": "cs46", "orgName": "Outbound Funds (npsp) - Dev Org", "edition": "Developer", "settings": { From e85d2b3f4023f6d01b9c76041c2f6afb3ced7254 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Wed, 10 Feb 2021 17:19:16 -0500 Subject: [PATCH 10/24] Added GAU Expenditure storytelling - Use the `Eager Beavers Read!` Funding Request --- datasets/data.sql | 100 ++++++++++++++++++++++++++----------------- datasets/mapping.yml | 21 +++++++++ 2 files changed, 81 insertions(+), 40 deletions(-) diff --git a/datasets/data.sql b/datasets/data.sql index 5d29647..41c5fdb 100644 --- a/datasets/data.sql +++ b/datasets/data.sql @@ -27,11 +27,12 @@ CREATE TABLE "Account" ( "Description" VARCHAR(255), PRIMARY KEY (id) ); -INSERT INTO "Account" VALUES(1,'STEPS','','','Customer - Direct','Not For Profit','','','303-555-7541','','','','','','2920 Juniper Drive','Denver','Colorado','80230','United States','','','','','',''); -INSERT INTO "Account" VALUES(2,'Hillside Elementary','','','Customer - Direct','Education','','','719-555-9914','','','','','','713 S. 8th Street','Englewood','Colorado','80110','United States','','','','','',''); -INSERT INTO "Account" VALUES(3,'Grantseeker Community: Self-Registered','','','','','','','','','','','','','','','','','','','','','','','Account initially assigned to self-registered users for the Grantseeker Community'); -INSERT INTO "Account" VALUES(4,'Takagawa Institute','','','Customer - Direct','Not For Profit','','','602-555-3542','','','','','','9833 Plateau Street','Phoenix','Arizona','85310','United States','','','','','',''); -INSERT INTO "Account" VALUES(5,'Grantwood City Council','','','Customer - Direct','Government','','','970-555-9633','','','','','','445 North Peak Road','Grantwood','Colorado','80522','United States','','','','','',''); +INSERT INTO "Account" VALUES(1,'Sample Account for Entitlements','','','','','','','','','','','','','','','','','','','','','','',''); +INSERT INTO "Account" VALUES(2,'STEPS','','','Customer - Direct','Not For Profit','','','303-555-7541','','','','','','2920 Juniper Drive','Denver','Colorado','80230','United States','','','','','',''); +INSERT INTO "Account" VALUES(3,'Hillside Elementary','','','Customer - Direct','Education','','','719-555-9914','','','','','','713 S. 8th Street','Englewood','Colorado','80110','United States','','','','','',''); +INSERT INTO "Account" VALUES(4,'Grantseeker Community: Self-Registered','','','','','','','','','','','','','','','','','','','','','','','Account initially assigned to self-registered users for the Grantseeker Community'); +INSERT INTO "Account" VALUES(5,'Takagawa Institute','','','Customer - Direct','Not For Profit','','','602-555-3542','','','','','','9833 Plateau Street','Phoenix','Arizona','85310','United States','','','','','',''); +INSERT INTO "Account" VALUES(6,'Grantwood City Council','','','Customer - Direct','Government','','','970-555-9633','','','','','','445 North Peak Road','Grantwood','Colorado','80522','United States','','','','','',''); CREATE TABLE "Contact" ( id INTEGER NOT NULL, "Salutation" VARCHAR(255), @@ -63,13 +64,32 @@ CREATE TABLE "Contact" ( reports_to_id VARCHAR(255), PRIMARY KEY (id) ); -INSERT INTO "Contact" VALUES(1,'Ms.','Ellen','Perez','Program Coordinator','ellen.perez@steps.example.com','','','','303-555-7541','','','','','','2920 Juniper Drive','Denver','Colorado','80230','United States','','','','','','','1','2'); -INSERT INTO "Contact" VALUES(2,'Ms.','Grace','Walker','Development Director','grace.walker@steps.example.com','','','','303-555-7540','','','','','','2920 Juniper Drive','Denver','Colorado','80230','United States','','','','','','','1',''); -INSERT INTO "Contact" VALUES(3,'Mr.','Jermaine','Harmon','Intern','jermaine.harmon@steps.example.com','','','','303-555-7540','','','','','','2920 Juniper Drive','Denver','Colorado','80230','United States','','','','','','','1',''); -INSERT INTO "Contact" VALUES(4,'Mr.','Devon','Berger','Literacy Coach','devon.berger@hillside-elementary.example.com','','','','719-555-9914','','','','','','713 S. 8th Street','Englewood','Colorado','80110','United States','','','','','','','2',''); -INSERT INTO "Contact" VALUES(5,'Ms.','Adriana','Atterberry','Grants Manager','adriana.atterberry@takagawa-institute.example.com','','','','602-555-3543','','','','','','9834 Plateau Street','Phoenix','Arizona','85310','United States','','','','','','','4','6'); -INSERT INTO "Contact" VALUES(6,'Dr.','Meiko','Takagawa','Executive Director','meiko.takagawa@takagawa-institute.example.com','','','','602-555-3542','','','','','','9833 Plateau Street','Phoenix','Arizona','85310','United States','','','','','','','4',''); -INSERT INTO "Contact" VALUES(7,'Mr.','Dillon','Whitaker','Assistant City Manager','dillon.whitaker@gwcity.example.com','','','','719-555-2417','','','','','','445 North Peak Road','Grantwood','Colorado','80522','United States','','','','','','','5',''); +INSERT INTO "Contact" VALUES(1,'Ms.','Ellen','Perez','Program Coordinator','ellen.perez@steps.example.com','','','','303-555-7541','','','','','','2920 Juniper Drive','Denver','Colorado','80230','United States','','','','','','','2','2'); +INSERT INTO "Contact" VALUES(2,'Ms.','Grace','Walker','Development Director','grace.walker@steps.example.com','','','','303-555-7540','','','','','','2920 Juniper Drive','Denver','Colorado','80230','United States','','','','','','','2',''); +INSERT INTO "Contact" VALUES(3,'Mr.','Jermaine','Harmon','Intern','jermaine.harmon@steps.example.com','','','','303-555-7540','','','','','','2920 Juniper Drive','Denver','Colorado','80230','United States','','','','','','','2',''); +INSERT INTO "Contact" VALUES(4,'Mr.','Devon','Berger','Literacy Coach','devon.berger@hillside-elementary.example.com','','','','719-555-9914','','','','','','713 S. 8th Street','Englewood','Colorado','80110','United States','','','','','','','3',''); +INSERT INTO "Contact" VALUES(5,'Ms.','Adriana','Atterberry','Grants Manager','adriana.atterberry@takagawa-institute.example.com','','','','602-555-3543','','','','','','9834 Plateau Street','Phoenix','Arizona','85310','United States','','','','','','','5','6'); +INSERT INTO "Contact" VALUES(6,'Dr.','Meiko','Takagawa','Executive Director','meiko.takagawa@takagawa-institute.example.com','','','','602-555-3542','','','','','','9833 Plateau Street','Phoenix','Arizona','85310','United States','','','','','','','5',''); +INSERT INTO "Contact" VALUES(7,'Mr.','Dillon','Whitaker','Assistant City Manager','dillon.whitaker@gwcity.example.com','','','','719-555-2417','','','','','','445 North Peak Road','Grantwood','Colorado','80522','United States','','','','','','','6',''); +CREATE TABLE "GAU_Expenditure__c" ( + id INTEGER NOT NULL, + "Amount__c" VARCHAR(255), + general_accounting_unit__c VARCHAR(255), + disbursement__c VARCHAR(255), + PRIMARY KEY (id) +); +INSERT INTO "GAU_Expenditure__c" VALUES(1,'1250.0','1','5'); +INSERT INTO "GAU_Expenditure__c" VALUES(2,'3750.0','2','5'); +INSERT INTO "GAU_Expenditure__c" VALUES(3,'5000.0','2','4'); +INSERT INTO "GAU_Expenditure__c" VALUES(4,'4500.0','2','3'); +INSERT INTO "GAU_Expenditure__c" VALUES(5,'500.0','1','3'); +CREATE TABLE "npsp__General_Accounting_Unit__c" ( + id INTEGER NOT NULL, + "Name" VARCHAR(255), + PRIMARY KEY (id) +); +INSERT INTO "npsp__General_Accounting_Unit__c" VALUES(1,'Area of most need'); +INSERT INTO "npsp__General_Accounting_Unit__c" VALUES(2,'Classroom needs'); CREATE TABLE "outfunds__Disbursement__c" ( id INTEGER NOT NULL, "outfunds__Amount__c" VARCHAR(255), @@ -81,11 +101,11 @@ CREATE TABLE "outfunds__Disbursement__c" ( outfunds__funding_request__c VARCHAR(255), PRIMARY KEY (id) ); -INSERT INTO "outfunds__Disbursement__c" VALUES(1,'5000.0','2019-07-09','EFT','2019-07-09','Paid','Initial','6'); -INSERT INTO "outfunds__Disbursement__c" VALUES(2,'5000.0','2020-07-09','','2020-07-09','Paid','Interim','6'); -INSERT INTO "outfunds__Disbursement__c" VALUES(3,'5000.0','','EFT','2021-07-09','Scheduled','Final','6'); -INSERT INTO "outfunds__Disbursement__c" VALUES(4,'10000.0','2020-03-28','EFT','2020-03-28','Paid','Initial','2'); -INSERT INTO "outfunds__Disbursement__c" VALUES(5,'40000.0','2019-05-14','EFT','2019-05-14','Paid','Final','4'); +INSERT INTO "outfunds__Disbursement__c" VALUES(1,'10000.0','2020-03-28','EFT','2020-03-28','Paid','Initial','6'); +INSERT INTO "outfunds__Disbursement__c" VALUES(2,'40000.0','2019-05-14','EFT','2019-05-14','Paid','Final','3'); +INSERT INTO "outfunds__Disbursement__c" VALUES(3,'5000.0','2019-07-09','EFT','2019-07-09','Paid','Initial','5'); +INSERT INTO "outfunds__Disbursement__c" VALUES(4,'5000.0','2020-07-09','','2020-07-09','Paid','Interim','5'); +INSERT INTO "outfunds__Disbursement__c" VALUES(5,'5000.0','','EFT','2021-07-09','Scheduled','Final','5'); CREATE TABLE "outfunds__Funding_Program__c" ( id INTEGER NOT NULL, "Name" VARCHAR(255), @@ -117,9 +137,9 @@ CREATE TABLE "outfunds__Funding_Request_Role__c" ( outfunds__funding_request__c VARCHAR(255), PRIMARY KEY (id) ); -INSERT INTO "outfunds__Funding_Request_Role__c" VALUES(1,'Grant Manager','Current','5','2'); -INSERT INTO "outfunds__Funding_Request_Role__c" VALUES(2,'Applicant','Current','6','2'); -INSERT INTO "outfunds__Funding_Request_Role__c" VALUES(3,'Applicant','Former','3','4'); +INSERT INTO "outfunds__Funding_Request_Role__c" VALUES(1,'Applicant','Former','3','3'); +INSERT INTO "outfunds__Funding_Request_Role__c" VALUES(2,'Grant Manager','Current','5','6'); +INSERT INTO "outfunds__Funding_Request_Role__c" VALUES(3,'Applicant','Current','6','6'); CREATE TABLE "outfunds__Funding_Request__c" ( id INTEGER NOT NULL, "Name" VARCHAR(255), @@ -142,22 +162,22 @@ CREATE TABLE "outfunds__Funding_Request__c" ( funding_program_id VARCHAR(255), PRIMARY KEY (id) ); -INSERT INTO "outfunds__Funding_Request__c" VALUES(1,'Grantwood City Food Bank','','','','','','City','Below Poverty level;Economically Disadvantaged People;Homeless','','100000.0','Grantwood City Food Bank','In progress','','','','','5','4'); -INSERT INTO "outfunds__Funding_Request__c" VALUES(2,'Takagawa Institute: Relief and Reinvestment Grant','2020-03-11','10000.0','2020-03-28','2020-03-28','Fully awarded.','Country','Immigrants and Refugees','10000.0','10000.0','','Fully Disbursed','2021-03-27','2020-03-28','One time payment with one year follow up','6','4','5'); -INSERT INTO "outfunds__Funding_Request__c" VALUES(3,'STEPS to Leadership','2020-02-16','28000.0','','','','Region','Adults;Women','28000.0','28000.0','STEPS to Leadership is a proposed program that came from requests for additional leadership training for our graduates from Skills for Success, a successful program we currently offer to at-risk women in the community. After completing Skills for Success, graduates can learn leadership skills through various trainings, seminars, and one-on-one mentoring that will help prepare these future female leaders.','Submitted','','','','1','1','4'); -INSERT INTO "outfunds__Funding_Request__c" VALUES(4,'Skills for Success','2019-03-21','40000.0','2019-05-14','','','Region','Adults;Women','40000.0','40000.0','Skills for Success addresses an existing gap for at-risk women seeking to learn technical and soft skills to help them find gainful employment in the community. Participants of the program receive: +INSERT INTO "outfunds__Funding_Request__c" VALUES(1,'Grantwood City Food Bank','','','','','','City','Below Poverty level;Economically Disadvantaged People;Homeless','','100000.0','Grantwood City Food Bank','In progress','','','','','6','4'); +INSERT INTO "outfunds__Funding_Request__c" VALUES(2,'STEPS to Leadership','2020-02-16','28000.0','','','','Region','Adults;Women','28000.0','28000.0','STEPS to Leadership is a proposed program that came from requests for additional leadership training for our graduates from Skills for Success, a successful program we currently offer to at-risk women in the community. After completing Skills for Success, graduates can learn leadership skills through various trainings, seminars, and one-on-one mentoring that will help prepare these future female leaders.','Submitted','','','','1','2','4'); +INSERT INTO "outfunds__Funding_Request__c" VALUES(3,'Skills for Success','2019-03-21','40000.0','2019-05-14','','','Region','Adults;Women','40000.0','40000.0','Skills for Success addresses an existing gap for at-risk women seeking to learn technical and soft skills to help them find gainful employment in the community. Participants of the program receive: * Vouchers for free community college courses on select topics, such as bookkeeping, computer literacy, and communication skills. * Help developing a resume and interview preparation. * One-on-one mentoring with a female business owner in the community. -* Ongoing support from the STEPS staff.','Awarded','2020-05-30','2019-05-31','1 year','2','1','4'); -INSERT INTO "outfunds__Funding_Request__c" VALUES(5,'Skills for Success','2020-05-31','','','','','Region','Adults;Women','','46000.0','Skills for Success addresses an existing gap for at-risk women seeking to learn technical and soft skills to help them find gainful employment in the community. Participants of the program receive: +* Ongoing support from the STEPS staff.','Awarded','2020-05-30','2019-05-31','1 year','2','2','4'); +INSERT INTO "outfunds__Funding_Request__c" VALUES(4,'Skills for Success','2020-05-31','','','','','Region','Adults;Women','','46000.0','Skills for Success addresses an existing gap for at-risk women seeking to learn technical and soft skills to help them find gainful employment in the community. Participants of the program receive: * Vouchers for free community college courses on select topics, such as bookkeeping, computer literacy, and communication skills. * Help developing a resume and interview preparation. * One-on-one mentoring with a female business owner in the community. -* Ongoing support from the STEPS staff.','In progress','','','1 year','2','1','4'); -INSERT INTO "outfunds__Funding_Request__c" VALUES(6,'Eager Beavers Read!','2019-04-25','15000.0','2019-07-09','','','City','Children and Youth','15000.0','15000.0','Eager Beavers Read! is an after school program that helps foster a love of reading in our 1st - 5th grade classes and also provides a safe place for students to go between 3:00 pm and 4:30 pm. Younger children will be paired up with an older student to help expand their early literacy skills, while older students work to develop mentoring skills and confidence.','Awarded','2022-08-13','2019-08-14','3 years','4','2','2'); +* Ongoing support from the STEPS staff.','In progress','','','1 year','2','2','4'); +INSERT INTO "outfunds__Funding_Request__c" VALUES(5,'Eager Beavers Read!','2019-04-25','15000.0','2019-07-09','','','City','Children and Youth','15000.0','15000.0','Eager Beavers Read! is an after school program that helps foster a love of reading in our 1st - 5th grade classes and also provides a safe place for students to go between 3:00 pm and 4:30 pm. Younger children will be paired up with an older student to help expand their early literacy skills, while older students work to develop mentoring skills and confidence.','Awarded','2022-08-13','2019-08-14','3 years','4','3','2'); +INSERT INTO "outfunds__Funding_Request__c" VALUES(6,'Takagawa Institute: Relief and Reinvestment Grant','2020-03-11','10000.0','2020-03-28','2020-03-28','Fully awarded.','Country','Immigrants and Refugees','10000.0','10000.0','','Fully Disbursed','2021-03-27','2020-03-28','One time payment with one year follow up','6','5','5'); CREATE TABLE "outfunds__Requirement__c" ( id INTEGER NOT NULL, "Name" VARCHAR(255), @@ -171,16 +191,16 @@ CREATE TABLE "outfunds__Requirement__c" ( outfunds__primary_contact__c VARCHAR(255), PRIMARY KEY (id) ); -INSERT INTO "outfunds__Requirement__c" VALUES(1,'Letter of Intent','2019-04-25','2019-12-30','

Cover letter

','Accepted','Letter of Intent','','6','4'); -INSERT INTO "outfunds__Requirement__c" VALUES(2,'Application','2019-04-25','2019-12-30','

Application

','Accepted','Final Application','','6','4'); -INSERT INTO "outfunds__Requirement__c" VALUES(3,'Outcome Report','','2022-12-30','

Outcome Report

','Open','Outcome','','6','4'); -INSERT INTO "outfunds__Requirement__c" VALUES(4,'Budget Report','','2021-12-30','

Budget Report

','Open','Report','','6','4'); -INSERT INTO "outfunds__Requirement__c" VALUES(5,'Budget Report','','2020-09-14','

Budget Report

','Open','Report','','2','6'); -INSERT INTO "outfunds__Requirement__c" VALUES(6,'Outcome Report','','2021-03-31','

Outcome Report

','Open','Outcome','','2','6'); -INSERT INTO "outfunds__Requirement__c" VALUES(7,'Application','2020-03-11','2020-12-30','

Application

','Accepted','Final Application','','2','6'); -INSERT INTO "outfunds__Requirement__c" VALUES(8,'Application','2019-03-21','2019-12-30','

Application

','Accepted','Final Application','','4','2'); -INSERT INTO "outfunds__Requirement__c" VALUES(9,'Outcome Report','','2020-12-30','

Outcome Report

','Open','Outcome','','4','2'); -INSERT INTO "outfunds__Requirement__c" VALUES(10,'Proposed Budget','2019-03-21','2019-12-30','

Proposed budget

','Accepted','Report','','4','2'); -INSERT INTO "outfunds__Requirement__c" VALUES(11,'Budget Report','2020-02-11','2020-12-30','

Budget Report

','Complete','Report','','4','2'); -INSERT INTO "outfunds__Requirement__c" VALUES(12,'Letter of Intent','2019-03-21','2019-12-30','

Cover letter

','Accepted','Letter of Intent','','4','2'); +INSERT INTO "outfunds__Requirement__c" VALUES(1,'Budget Report','','2020-09-14','

Budget Report

','Open','Report','','6','6'); +INSERT INTO "outfunds__Requirement__c" VALUES(2,'Outcome Report','','2021-03-31','

Outcome Report

','Open','Outcome','','6','6'); +INSERT INTO "outfunds__Requirement__c" VALUES(3,'Application','2020-03-11','2020-12-30','

Application

','Accepted','Final Application','','6','6'); +INSERT INTO "outfunds__Requirement__c" VALUES(4,'Application','2019-03-21','2019-12-30','

Application

','Accepted','Final Application','','3','2'); +INSERT INTO "outfunds__Requirement__c" VALUES(5,'Outcome Report','','2020-12-30','

Outcome Report

','Open','Outcome','','3','2'); +INSERT INTO "outfunds__Requirement__c" VALUES(6,'Proposed Budget','2019-03-21','2019-12-30','

Proposed budget

','Accepted','Report','','3','2'); +INSERT INTO "outfunds__Requirement__c" VALUES(7,'Budget Report','2020-02-11','2020-12-30','

Budget Report

','Complete','Report','','3','2'); +INSERT INTO "outfunds__Requirement__c" VALUES(8,'Letter of Intent','2019-03-21','2019-12-30','

Cover letter

','Accepted','Letter of Intent','','3','2'); +INSERT INTO "outfunds__Requirement__c" VALUES(9,'Letter of Intent','2019-04-25','2019-12-30','

Cover letter

','Accepted','Letter of Intent','','5','4'); +INSERT INTO "outfunds__Requirement__c" VALUES(10,'Application','2019-04-25','2019-12-30','

Application

','Accepted','Final Application','','5','4'); +INSERT INTO "outfunds__Requirement__c" VALUES(11,'Outcome Report','','2022-12-30','

Outcome Report

','Open','Outcome','','5','4'); +INSERT INTO "outfunds__Requirement__c" VALUES(12,'Budget Report','','2021-12-30','

Budget Report

','Open','Report','','5','4'); COMMIT; diff --git a/datasets/mapping.yml b/datasets/mapping.yml index cca9f68..2772b39 100644 --- a/datasets/mapping.yml +++ b/datasets/mapping.yml @@ -169,3 +169,24 @@ outfunds__Requirement__c: outfunds__Primary_Contact__c: key_field: outfunds__primary_contact__c table: Contact + +npsp__General_Accounting_Unit__c: + sf_object: npsp__General_Accounting_Unit__c + table: npsp__General_Accounting_Unit__c + anchor_date: "2020-07-01" + fields: + - Name + +GAU_Expenditure__c: + sf_object: GAU_Expenditure__c + table: GAU_Expenditure__c + anchor_date: "2020-07-01" + fields: + - Amount__c + lookups: + General_Accounting_Unit__c: + key_field: general_accounting_unit__c + table: npsp__General_Accounting_Unit__c + Disbursement__c: + key_field: disbursement__c + table: outfunds__Disbursement__c From 9c35bc5388345fcb233f60f58755b88ea501ee9f Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Wed, 10 Feb 2021 17:19:54 -0500 Subject: [PATCH 11/24] Added assertEquals methods for wrapper classes This is a work in progress. The full unit test is coming soon. --- .../tests/GauExpendituresManager_TEST.cls | 311 +++++++----------- 1 file changed, 115 insertions(+), 196 deletions(-) diff --git a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls index 210493e..dc45374 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls +++ b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls @@ -1,221 +1,140 @@ @IsTest public with sharing class GauExpendituresManager_TEST { - /***************************************************************************** - * @description Create records needed for test and set class-level properties - */ - public static outfunds__Disbursement__c makeData() { - outfunds__Funding_Program__c fundingProgram = new outfunds__Funding_Program__c( - Name = 'My Funding Program' - ); - insert fundingProgram; - outfunds__Funding_Request__c fundingRequest = new outfunds__Funding_Request__c( - Name = 'My Funding Request', - outfunds__FundingProgram__c = fundingProgram.Id - ); - insert fundingRequest; - outfunds__Disbursement__c disbursement = new outfunds__Disbursement__c( - outfunds__Amount__c = 10000, - outfunds__Funding_Request__c = fundingRequest.Id - ); - insert disbursement; - return disbursement; - } - - /***************************************************************************** - * @description Create specified number of gaus with signifier in name - */ - public static List createGaus( - String namePrefix, - Integer count + public static void assertEquals( + GauExpendituresManager.GauExpenditureWrapper expected, + GauExpendituresManager.GauExpenditureWrapper actual, + Integer index ) { - Integer max = count > 200 ? 200 : count; - List gaus = new List(); - for (Integer itr = 0; itr < count; itr++) { - gaus.add( - new npsp__General_Accounting_Unit__c( - Name = namePrefix + '-' + String.valueOf(DateTime.now().getTime()) - ) + if (expected == null) { + System.assertEquals(null, actual); + } else { + System.assertNotEquals(null, actual); + + final String messageSuffix = index == null ? '' : ' at index "' + index + '"'; + + System.assertEquals( + expected.recordId, + actual.recordId, + 'GauExpenditureWrapper.recordId' + messageSuffix ); - } - insert gaus; - return gaus; - } - /***************************************************************************** - * @description Create one expenditure for each gau, and link to disbursement - */ - public static List createGauExpenditures( - Id disbursementId, - List gaus - ) { - List gauExpenditures = new List(); - for (npsp__General_Accounting_Unit__c gau : gaus) { - gauExpenditures.add( - new GAU_Expenditure__c( - Disbursement__c = disbursementId, - General_Accounting_Unit__c = gau.Id - ) + System.assertEquals( + expected.gauId, + actual.gauId, + 'GauExpenditureWrapper.gauId' + messageSuffix + ); + System.assertEquals( + expected.gauName, + actual.gauName, + 'gauName' + messageSuffix + ); + System.assertEquals( + expected.amount, + actual.amount, + 'GauExpenditureWrapper.amount' + messageSuffix + ); + System.assertEquals( + expected.gauIsActive, + actual.gauIsActive, + 'gauIsActive' + messageSuffix + ); + System.assertEquals( + expected.rowId, + actual.rowId, + 'GauExpenditureWrapper.rowId' + messageSuffix ); } - insert gauExpenditures; - return gauExpenditures; - } - - /***************************************************************************** - * @description Test expected use case of retrieving disbursement with several expenditures - */ - @IsTest - public static void validGetDisbursement() { - outfunds__Disbursement__c disbursement = makeData(); - List gaus = createGaus('GAU', 5); - List gauExpenditures = createGauExpenditures( - disbursement.Id, - gaus - ); - Test.startTest(); - GauExpendituresManager.DisbursementWrapper queriedDisbursement = GauExpendituresManager.getDisbursement( - String.valueOf(disbursement.Id) - ); - Test.stopTest(); - system.assertEquals( - 10000, - queriedDisbursement.amount, - 'expected disbursement amount of 10000' - ); - system.assertEquals( - 5, - queriedDisbursement.expenditures.size(), - 'expected list of 5 expenditures' - ); - System.debug('hello world'); } - /***************************************************************************** - * @description Test case of no disbursement returned due to invalid Id - */ - @IsTest - public static void badIdGetDisbursement() { - Test.startTest(); - GauExpendituresManager.DisbursementWrapper queriedDisbursement = GauExpendituresManager.getDisbursement( - 'I am not a record Id' - ); - Test.stopTest(); - System.assertEquals(null, queriedDisbursement, 'expected null'); + public static void assertEquals( + GauExpendituresManager.GauExpenditureWrapper expected, + GauExpendituresManager.GauExpenditureWrapper actual + ) { + GauExpendituresManager_TEST.assertEqual(expected, actual, (Integer) null); } - /***************************************************************************** - * @description Test update ability of new GAU Expenditure wrappers added - */ - @IsTest - public static void testValidUpdate() { - outfunds__Disbursement__c disbursement = makeData(); - List gaus = createGaus('GAU', 5); - List gauExpenditures = createGauExpenditures( - disbursement.Id, - gaus - ); - GauExpendituresManager.DisbursementWrapper queriedDisbursement = GauExpendituresManager.getDisbursement( - String.valueOf(disbursement.Id) - ); - // test update ability of list items - for ( - GauExpendituresManager.GauExpenditureWrapper expenditure : queriedDisbursement.expenditures - ) { - expenditure.amount = 56.78; - } - String expendituresString = JSON.serialize(queriedDisbursement.expenditures); - Test.startTest(); - GauExpendituresManager.upsertGauExpenditures(expendituresString, disbursement.Id); - Test.stopTest(); - List queriedExpenditures = [ - SELECT Id, Amount__c, Disbursement__c - FROM GAU_Expenditure__c - ]; - /* System.assertEquals( - 6, - queriedExpenditures.size(), - 'expected 6 expenditures' - ); */ - for (GAU_Expenditure__c queriedExpenditure : queriedExpenditures) { + public static void assertEquals( + GauExpendituresManager.DisbursementWrapper expected, + GauExpendituresManager.DisbursementWrapper actual + ) { + if (expected == null) { + System.assertEquals(null, actual); + } else { + System.assertNotEquals(null, actual); System.assertEquals( - disbursement.Id, - queriedExpenditure.Disbursement__c, - 'expected matching disbursement id' + expected.recordId, + actual.recordId, + 'DisbursementWrapper.recordId' ); + System.assertEquals(expected.name, actual.name, 'DisbursementWrapper.name'); System.assertEquals( - 56.78, - queriedExpenditure.Amount__c, - 'amount did not match assign values' + expected.amount, + actual.amount, + 'DisbursementWrapper.amount' ); - } - } - /***************************************************************************** - * @description Test insert ability of new GAU Expenditure wrappers added - */ - @IsTest - public static void testValidInsert() { - outfunds__Disbursement__c disbursement = makeData(); - List gaus = createGaus('GAU', 5); - List gauExpenditures = createGauExpenditures( - disbursement.Id, - gaus - ); - GauExpendituresManager.DisbursementWrapper queriedDisbursement = GauExpendituresManager.getDisbursement( - String.valueOf(disbursement.Id) - ); - List newGaus = createGaus('newGAU', 1); - - GauExpendituresManager.GauExpenditureWrapper newExpenditureWrapper = new GauExpendituresManager.GauExpenditureWrapper( - new GAU_Expenditure__c(General_Accounting_Unit__c = newGaus[0].Id), - 1 - ); - queriedDisbursement.expenditures.add(newExpenditureWrapper); - String expendituresString = JSON.serialize(queriedDisbursement.expenditures); - GauExpendituresManager.upsertGauExpenditures(expendituresString, disbursement.Id); - List queriedExpenditures = [ - SELECT Id, Disbursement__c - FROM GAU_Expenditure__c - ]; - System.assertEquals(6, queriedExpenditures.size(), 'expected 6 expenditures'); - for (GAU_Expenditure__c expenditure : queriedExpenditures) { System.assertEquals( - disbursement.Id, - expenditure.Disbursement__c, - 'Disbursement Id does not match' + expected.status, + actual.status, + 'DisbursementWrapper.status' ); + + if (expected.expenditures == null) { + System.assertEquals( + null, + actual.expenditures, + 'DisbursementWrapper.expenditures' + ); + } else { + System.assertNotEquals( + null, + actual.expenditures, + 'DisbursementWrapper.expenditures' + ); + + final Integer expendituresSize = expected.expenditures.size(); + System.assertEquals( + expendituresSize, + actual.expenditures.size(), + 'DisbursementWrapper.expenditures.size()' + ); + + for (Integer i = 0; i < expendituresSize; i++) { + GauExpendituresManager_TEST.assertEquals( + expected.expenditures[i], + actual.expenditures[i], + i + ); + } + } } } - /***************************************************************************** - * @description Test delete ability of new GAU Expenditure wrappers added - */ - @IsTest - public static void testValidDelete() { - outfunds__Disbursement__c disbursement = makeData(); - List gaus = createGaus('GAU', 5); - List gauExpenditures = createGauExpenditures( - disbursement.Id, - gaus - ); - GauExpendituresManager.DisbursementWrapper queriedDisbursement = GauExpendituresManager.getDisbursement( - String.valueOf(disbursement.Id) - ); - List newExpendituresList = new List{ - queriedDisbursement.expenditures[0] - }; + public static void assertEquals( + GauExpendituresManager.LookupSearchResult expected, + GauExpendituresManager.LookupSearchResult actual + ) { + if (expected == null) { + System.assertEquals(null, actual); + } else { + System.assertNotEquals(null, actual); - String expendituresString = JSON.serialize(newExpendituresList); - GauExpendituresManager.upsertGauExpenditures(expendituresString, disbursement.Id); - List queriedExpenditures = [ - SELECT Id, Disbursement__c - FROM GAU_Expenditure__c - ]; - System.assertEquals(1, queriedExpenditures.size(), 'expected 1 expenditure'); - for (GAU_Expenditure__c expenditure : queriedExpenditures) { + System.assertEquals(expected.id, actual.id, 'LookupSearchResult.id'); System.assertEquals( - disbursement.Id, - expenditure.Disbursement__c, - 'Disbursement Id does not match' + expected.sObjectType, + actual.sObjectType, + 'LookupSearchResult.sObjectType' + ); + System.assertEquals(expected.icon, actual.icon, 'LookupSearchResult.icon'); + System.assertEquals(expected.title, actual.title, 'LookupSearchResult.title'); + System.assertEquals( + expected.subtitle, + actual.subtitle, + 'LookupSearchResult.subtitle' ); } } + /* + @IsTest + private static void GauExpenditureWrapper_Constructor() { + } + */ } From 37881854f48d78db22e59b05513a2021d8ab489b Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 11 Feb 2021 10:53:16 -0500 Subject: [PATCH 12/24] GauExpendituresManager_TEST compiles - `GauExpendituresManager_TEST` Apex class compiles + added test `GauExpenditureWrapper_NonNullGauId`. - Formatted comments in `GauExpendituresManager`. --- .../GauExpendituresManager.cls | 11 ---- .../tests/GauExpendituresManager_TEST.cls | 58 +++++++++++++++++-- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls b/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls index d7d51bb..e78a0bd 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls +++ b/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls @@ -38,17 +38,6 @@ public with sharing class GauExpendituresManager { } } - /***************************************************************************** - * @description - * @param String expenditureString - * @param String disbursementId - * @return void - * @example - * GauExpendituresManager.upsertGauExpenditures( - * expendituresString, - * disbursement.Id - * ); - */ /** * Receives a JSON serialized version of the GAU Expenditures, with the id of the parent. Upsert and delete children as needed. */ diff --git a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls index dc45374..43f4e95 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls +++ b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls @@ -49,7 +49,7 @@ public with sharing class GauExpendituresManager_TEST { GauExpendituresManager.GauExpenditureWrapper expected, GauExpendituresManager.GauExpenditureWrapper actual ) { - GauExpendituresManager_TEST.assertEqual(expected, actual, (Integer) null); + GauExpendituresManager_TEST.assertEquals(expected, actual, (Integer) null); } public static void assertEquals( @@ -132,9 +132,59 @@ public with sharing class GauExpendituresManager_TEST { ); } } - /* + @IsTest - private static void GauExpenditureWrapper_Constructor() { + private static void GauExpenditureWrapper_NonNullGauId() { + final Id recordId = 'a0x9A000000ASvfQAG'; + final Id gauId = 'a0d9A0000003r1sQAA'; + final String gauName = 'Area of most need'; + final Decimal amount = 1250.0; + final Boolean gauIsActive = true; + + // prettier-ignore + final GAU_Expenditure__c gauExpenditure = (GAU_Expenditure__c) JSON.deserialize( + String.join( + new List { + '{', + '"attributes": {', + '"type": "GAU_Expenditure__c",', + '"url": "/services/data/v51.0/sobjects/GAU_Expenditure__c/a0x9A000000ASvfQAG"', + '},', + '"Disbursement__c": "a0s9A000000cuCSQAY",', + '"Id": "' + recordId + '",', + '"General_Accounting_Unit__c": "' + gauId + '",', + '"Amount__c": ' + amount.toPlainString() + ',', + '"General_Accounting_Unit__r": {', + '"attributes": {', + '"type": "npsp__General_Accounting_Unit__c",', + '"url": "/services/data/v51.0/sobjects/npsp__General_Accounting_Unit__c/a0d9A0000003r1sQAA"', + '},', + '"Id": "a0d9A0000003r1sQAA",', + '"Name": "Area of most need",', + '"npsp__Active__c": ' + gauIsActive + '', + '}', + '}' + }, + '' + ), + GAU_Expenditure__c.class + ); + final Integer rowId = Crypto.getRandomInteger(); + + Test.startTest(); + + final GauExpendituresManager.GauExpenditureWrapper actual = new GauExpendituresManager.GauExpenditureWrapper( + gauExpenditure, + rowId + ); + + Test.stopTest(); + + System.assertEquals(recordId, actual.recordId); + System.assertEquals(gauId, actual.gauId); + System.assertEquals(gauName, actual.gauName); + System.assertEquals(amount, actual.amount); + System.assertEquals(gauIsActive, actual.gauIsActive); + System.assertEquals(rowId, actual.rowId); } - */ } From 09a7458839ddc4205da584184e2d90c8fdc70a43 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 11 Feb 2021 10:54:55 -0500 Subject: [PATCH 13/24] Updated Label DatabaseService_NoDeleteAccessException --- force-app/main/default/labels/CustomLabels.labels-meta.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/force-app/main/default/labels/CustomLabels.labels-meta.xml b/force-app/main/default/labels/CustomLabels.labels-meta.xml index b8be0d8..fecfcdc 100644 --- a/force-app/main/default/labels/CustomLabels.labels-meta.xml +++ b/force-app/main/default/labels/CustomLabels.labels-meta.xml @@ -22,6 +22,6 @@ en_US true The user does not have delete access to the object. - You don't have delete access to this object. Ask your Salesforce admin for help. + You don't have delete access to this object. Contact your system administrator for help. From 0ae9e058b9db2b37339e71e19f335f0616681854 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 11 Feb 2021 11:10:59 -0500 Subject: [PATCH 14/24] Updated all labels --- force-app/main/default/labels/CustomLabels.labels-meta.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/force-app/main/default/labels/CustomLabels.labels-meta.xml b/force-app/main/default/labels/CustomLabels.labels-meta.xml index fecfcdc..7f3a774 100644 --- a/force-app/main/default/labels/CustomLabels.labels-meta.xml +++ b/force-app/main/default/labels/CustomLabels.labels-meta.xml @@ -6,7 +6,7 @@ en_US true The user does not have create access to fields required for this process. - You don't have field-level access to create this record. Contact your system administrator for help. + You don't have field-level access to create this record. Ask your Salesforce admin for help. DatabaseService_NoUpdateFlsException @@ -14,7 +14,7 @@ en_US true The user does not have edit or update access to required fields. - You don't have field-level access to edit this record. Contact your system administrator for help. + You don't have field-level access to edit this record. Ask your Salesforce admin for help. DatabaseService_NoDeleteAccessException @@ -22,6 +22,6 @@ en_US true The user does not have delete access to the object. - You don't have delete access to this object. Contact your system administrator for help. + You don't have delete access to this object. Ask your Salesforce admin for help. From a395b73f32b5c1a92bc46b9f022d5d9b4fdc23ab Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 11 Feb 2021 13:18:07 -0500 Subject: [PATCH 15/24] Safe AuraEnabled methods + fully tested - `GauExpendituresManager.searchActiveGeneralAccountingUnitsLikeName` is wrapped in a try/catch and re-throws Exceptions as System.AuraHandledExceptions. - Fully unit tested `GauExpendituresManager` in `GauExpendituresManager_TEST`. --- .../GauExpendituresManager.cls | 52 +- .../tests/GauExpendituresManager_TEST.cls | 835 +++++++++++++++++- 2 files changed, 824 insertions(+), 63 deletions(-) diff --git a/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls b/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls index e78a0bd..5bb6760 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls +++ b/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls @@ -111,23 +111,32 @@ public with sharing class GauExpendituresManager { public static List searchActiveGeneralAccountingUnitsLikeName( String searchTerm ) { - final List lookupSearchResults = new List(); - for ( - Schema.npsp__General_Accounting_Unit__c generalAccountingUnit : GauExpendituresManager.gauExpenditureSelector.getActiveGeneralAccountUnitsLikeName( - searchTerm - ) - ) { - final GauExpendituresManager.LookupSearchResult lookupSearchResult = new GauExpendituresManager.LookupSearchResult(); - lookupSearchResult.id = generalAccountingUnit.Id; - lookupSearchResult.sObjectType = Schema.SObjectType.npsp__General_Accounting_Unit__c.getName(); - lookupSearchResult.icon = 'custom:custom87'; - lookupSearchResult.title = generalAccountingUnit.Name; - lookupSearchResult.subtitle = generalAccountingUnit.npsp__Description__c; - - lookupSearchResults.add(lookupSearchResult); - } + try { + final List lookupSearchResults = new List(); + for ( + Schema.npsp__General_Accounting_Unit__c generalAccountingUnit : GauExpendituresManager.gauExpenditureSelector.getActiveGeneralAccountUnitsLikeName( + searchTerm + ) + ) { + final GauExpendituresManager.LookupSearchResult lookupSearchResult = new GauExpendituresManager.LookupSearchResult(); + lookupSearchResult.id = generalAccountingUnit.Id; + lookupSearchResult.sObjectType = Schema.SObjectType.npsp__General_Accounting_Unit__c.getName(); + lookupSearchResult.icon = 'custom:custom87'; + lookupSearchResult.title = generalAccountingUnit.Name; + lookupSearchResult.subtitle = generalAccountingUnit.npsp__Description__c; + + lookupSearchResults.add(lookupSearchResult); + } - return lookupSearchResults; + return lookupSearchResults; + } catch (Exception e) { + // Re-throw as an AuraHandledException that Lightning Components can handle. + final System.AuraHandledException auraHandledException = new System.AuraHandledException( + e.getMessage() + ); + auraHandledException.setMessage(e.getMessage()); + throw auraHandledException; + } } /** @@ -192,11 +201,20 @@ public with sharing class GauExpendituresManager { this.gauId = gauExpenditure.General_Accounting_Unit__c; if (this.gauId != null) { this.gauName = gauExpenditure.General_Accounting_Unit__r.Name; + this.gauIsActive = gauExpenditure.General_Accounting_Unit__r.npsp__Active__c; + } else { + this.gauIsActive = false; } this.amount = gauExpenditure.Amount__c; - this.gauIsActive = gauExpenditure.General_Accounting_Unit__r.npsp__Active__c; this.rowId = rowId; } + + /** + * Empty constructor for unit testing. + */ + @TestVisible + private GauExpenditureWrapper() { + } } /** diff --git a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls index 43f4e95..c308695 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls +++ b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls @@ -46,10 +46,25 @@ public with sharing class GauExpendituresManager_TEST { } public static void assertEquals( - GauExpendituresManager.GauExpenditureWrapper expected, - GauExpendituresManager.GauExpenditureWrapper actual + List expected, + List actual ) { - GauExpendituresManager_TEST.assertEquals(expected, actual, (Integer) null); + if (expected == null) { + System.assertEquals(null, actual); + } else { + System.assertNotEquals(null, actual); + + final Integer expendituresSize = expected.size(); + System.assertEquals( + expendituresSize, + actual.size(), + 'List.size()' + ); + + for (Integer i = 0; i < expendituresSize; i++) { + GauExpendituresManager_TEST.assertEquals(expected[i], actual[i], i); + } + } } public static void assertEquals( @@ -77,64 +92,83 @@ public with sharing class GauExpendituresManager_TEST { 'DisbursementWrapper.status' ); - if (expected.expenditures == null) { - System.assertEquals( - null, - actual.expenditures, - 'DisbursementWrapper.expenditures' - ); - } else { - System.assertNotEquals( - null, - actual.expenditures, - 'DisbursementWrapper.expenditures' - ); - - final Integer expendituresSize = expected.expenditures.size(); - System.assertEquals( - expendituresSize, - actual.expenditures.size(), - 'DisbursementWrapper.expenditures.size()' - ); - - for (Integer i = 0; i < expendituresSize; i++) { - GauExpendituresManager_TEST.assertEquals( - expected.expenditures[i], - actual.expenditures[i], - i - ); - } - } + GauExpendituresManager_TEST.assertEquals( + expected.expenditures, + actual.expenditures + ); } } public static void assertEquals( GauExpendituresManager.LookupSearchResult expected, - GauExpendituresManager.LookupSearchResult actual + GauExpendituresManager.LookupSearchResult actual, + Integer index ) { if (expected == null) { System.assertEquals(null, actual); } else { System.assertNotEquals(null, actual); - System.assertEquals(expected.id, actual.id, 'LookupSearchResult.id'); + final String messageSuffix = index == null ? '' : ' at index "' + index + '"'; + + System.assertEquals( + expected.id, + actual.id, + 'LookupSearchResult.id' + messageSuffix + ); System.assertEquals( expected.sObjectType, actual.sObjectType, - 'LookupSearchResult.sObjectType' + 'LookupSearchResult.sObjectType' + messageSuffix + ); + System.assertEquals( + expected.icon, + actual.icon, + 'LookupSearchResult.icon' + messageSuffix + ); + System.assertEquals( + expected.title, + actual.title, + 'LookupSearchResult.title' + messageSuffix ); - System.assertEquals(expected.icon, actual.icon, 'LookupSearchResult.icon'); - System.assertEquals(expected.title, actual.title, 'LookupSearchResult.title'); System.assertEquals( expected.subtitle, actual.subtitle, - 'LookupSearchResult.subtitle' + 'LookupSearchResult.subtitle' + messageSuffix ); } } + public static void assertEquals( + List expected, + List actual + ) { + if (expected == null) { + System.assertEquals(null, actual); + } else { + System.assertNotEquals(null, actual); + + final Integer expendituresSize = expected.size(); + System.assertEquals( + expendituresSize, + actual.size(), + 'List.size()' + ); + + for (Integer i = 0; i < expendituresSize; i++) { + GauExpendituresManager_TEST.assertEquals(expected[i], actual[i], i); + } + } + } + @IsTest private static void GauExpenditureWrapper_NonNullGauId() { + final String gauExpenditureObjectName = Schema.SObjectType.GAU_Expenditure__c.getName(); + final String disbursementFieldName = Schema.SObjectType.GAU_Expenditure__c.fields.Disbursement__c.getName(); + final String amountFieldName = Schema.SObjectType.GAU_Expenditure__c.fields.Amount__c.getName(); + final String gauFieldName = Schema.SObjectType.GAU_Expenditure__c.fields.General_Accounting_Unit__c.getName(); + final String gauRelationshipName = gauFieldName.removeEnd('__c') + '__r'; + final Id recordId = 'a0x9A000000ASvfQAG'; final Id gauId = 'a0d9A0000003r1sQAA'; final String gauName = 'Area of most need'; @@ -147,20 +181,20 @@ public with sharing class GauExpendituresManager_TEST { new List { '{', '"attributes": {', - '"type": "GAU_Expenditure__c",', - '"url": "/services/data/v51.0/sobjects/GAU_Expenditure__c/a0x9A000000ASvfQAG"', + '"type": "' + gauExpenditureObjectName + '",', + '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + recordId + '"', '},', - '"Disbursement__c": "a0s9A000000cuCSQAY",', + '"' + disbursementFieldName+ '": "a0s9A000000cuCSQAY",', '"Id": "' + recordId + '",', - '"General_Accounting_Unit__c": "' + gauId + '",', - '"Amount__c": ' + amount.toPlainString() + ',', - '"General_Accounting_Unit__r": {', + '"' + gauFieldName + '": "' + gauId + '",', + '"'+ amountFieldName + '": ' + amount.toPlainString() + ',', + '"' + gauRelationshipName + '": {', '"attributes": {', '"type": "npsp__General_Accounting_Unit__c",', - '"url": "/services/data/v51.0/sobjects/npsp__General_Accounting_Unit__c/a0d9A0000003r1sQAA"', + '"url": "/services/data/v51.0/sobjects/npsp__General_Accounting_Unit__c/' + gauId + '"', '},', - '"Id": "a0d9A0000003r1sQAA",', - '"Name": "Area of most need",', + '"Id": "' + gauId + '",', + '"Name": "' + gauName + '",', '"npsp__Active__c": ' + gauIsActive + '', '}', '}' @@ -187,4 +221,713 @@ public with sharing class GauExpendituresManager_TEST { System.assertEquals(gauIsActive, actual.gauIsActive); System.assertEquals(rowId, actual.rowId); } + + @IsTest + private static void GauExpenditureWrapper_NullGauId() { + final String gauExpenditureObjectName = Schema.SObjectType.GAU_Expenditure__c.getName(); + final String disbursementFieldName = Schema.SObjectType.GAU_Expenditure__c.fields.Disbursement__c.getName(); + final String amountFieldName = Schema.SObjectType.GAU_Expenditure__c.fields.Amount__c.getName(); + + final Id recordId = 'a0x9A000000ASvfQAG'; + final Id gauId = null; + final String gauName = null; + final Decimal amount = 1250.0; + final Boolean gauIsActive = false; + + // prettier-ignore + final GAU_Expenditure__c gauExpenditure = (GAU_Expenditure__c) JSON.deserialize( + String.join( + new List { + '{', + '"attributes": {', + '"type": "' + gauExpenditureObjectName + '",', + '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + recordId + '"', + '},', + '"' + disbursementFieldName + '": "a0s9A000000cuCSQAY",', + '"Id": "' + recordId + '",', + '"' + amountFieldName + '": ' + amount.toPlainString() + '', + '}' + }, + '' + ), + GAU_Expenditure__c.class + ); + final Integer rowId = Crypto.getRandomInteger(); + + Test.startTest(); + + final GauExpendituresManager.GauExpenditureWrapper actual = new GauExpendituresManager.GauExpenditureWrapper( + gauExpenditure, + rowId + ); + + Test.stopTest(); + + System.assertEquals(recordId, actual.recordId); + System.assertEquals(gauId, actual.gauId); + System.assertEquals(gauName, actual.gauName); + System.assertEquals(amount, actual.amount); + System.assertEquals(gauIsActive, actual.gauIsActive); + System.assertEquals(rowId, actual.rowId); + } + + @IsTest + private static void DisbursementWrapper() { + final String gauExpenditureObjectName = Schema.SObjectType.GAU_Expenditure__c.getName(); + final String disbursementFieldName = Schema.SObjectType.GAU_Expenditure__c.fields.Disbursement__c.getName(); + final String amountFieldName = Schema.SObjectType.GAU_Expenditure__c.fields.Amount__c.getName(); + final String gauFieldName = Schema.SObjectType.GAU_Expenditure__c.fields.General_Accounting_Unit__c.getName(); + final String gauRelationshipName = gauFieldName.removeEnd('__c') + '__r'; + + final Id gauExpenditureId1 = UnitTest.mockId( + Schema.SObjectType.GAU_Expenditure__c + ); + final Id gauId1 = UnitTest.mockId( + Schema.SObjectType.npsp__General_Accounting_Unit__c + ); + + final Id gauExpenditureId2 = UnitTest.mockId( + Schema.SObjectType.GAU_Expenditure__c + ); + + final Id recordId = UnitTest.mockId(Schema.SObjectType.outfunds__Disbursement__c); + final String name = 'D-00004'; + final Decimal amount = 314159.27; + final String status = 'Scheduled'; + + // prettier-ignore + final GAU_Expenditure__c gauExpenditure1 = (GAU_Expenditure__c) JSON.deserialize( + String.join( + new List { + '{', + '"attributes": {', + '"type": "' + gauExpenditureObjectName + '",', + '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + gauExpenditureId1 + '"', + '},', + '"' + disbursementFieldName+ '": "a0s9A000000cuCSQAY",', + '"Id": "' + gauExpenditureId1 + '",', + '"' + gauFieldName + '": "' + gauId1 + '",', + '"'+ amountFieldName + '": 1234.5,', + '"' + gauRelationshipName + '": {', + '"attributes": {', + '"type": "npsp__General_Accounting_Unit__c",', + '"url": "/services/data/v51.0/sobjects/npsp__General_Accounting_Unit__c/' + gauId1 + '"', + '},', + '"Id": "' + gauId1 + '",', + '"Name": "GAU 1",', + '"npsp__Active__c": true', + '}', + '}' + }, + '' + ), + GAU_Expenditure__c.class + ); + // prettier-ignore + final GAU_Expenditure__c gauExpenditure2 = (GAU_Expenditure__c) JSON.deserialize( + String.join( + new List { + '{', + '"attributes": {', + '"type": "' + gauExpenditureObjectName + '",', + '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + gauExpenditureId2 + '"', + '},', + '"' + disbursementFieldName+ '": "a0s9A000000cuCSQAY",', + '"Id": "' + gauExpenditureId2 + '",', + '"'+ amountFieldName + '": 5432.1', + '}' + }, + '' + ), + GAU_Expenditure__c.class + ); + final List expenditures = new List{ + gauExpenditure1, + gauExpenditure2 + }; + + final List expectedExpenditures = new List{ + new GauExpendituresManager.GauExpenditureWrapper(gauExpenditure1, 1), + new GauExpendituresManager.GauExpenditureWrapper(gauExpenditure2, 2) + }; + + // prettier-ignore + final outfunds__Disbursement__c disbursement = (outfunds__Disbursement__c) JSON.deserialize( + String.join( + new List { + '{', + '"attributes": {', + '"type": "outfunds__Disbursement__c",', + '"url": "/services/data/v51.0/sobjects/outfunds__Disbursement__c/' + recordId + '"', + '},', + '"Id": "' + recordId + '",', + '"Name": "' + name + '",', + '"outfunds__Amount__c": ' + amount.toPlainString() + ',', + '"outfunds__Status__c": "' + status + '",', + '"GAU_Expendatures__r": {', + '"totalSize": ' + expenditures.size() + ',', + '"done": true,', + '"records": ' + JSON.serialize(expenditures), + '}', + '}' + }, + '' + ), + outfunds__Disbursement__c.class + ); + Test.startTest(); + + final GauExpendituresManager.DisbursementWrapper actual = new GauExpendituresManager.DisbursementWrapper( + disbursement + ); + + Test.stopTest(); + + System.assertEquals(recordId, actual.recordId); + System.assertEquals(name, actual.name); + System.assertEquals(amount, actual.amount); + System.assertEquals(status, actual.status); + GauExpendituresManager_TEST.assertEquals( + expectedExpenditures, + actual.expenditures + ); + } + + @IsTest + private static void getDisbursement_QueryFindsNoRecords() { + // Set arguments, mock return values, and expected values. + final String disbursementId = 'disbursementId'; + final GauExpendituresManager.DisbursementWrapper expected; + final List disbursements = new List(); + + // Set mocks + final UnitTest.Mock gauExpenditureSelector = new UnitTest.Mock(); + + final UnitTest.Mock getDisbursementsById = gauExpenditureSelector.getMethod( + 'getDisbursementsById' + ); + getDisbursementsById.returnValue = disbursements; + + Test.startTest(); + + // Set stubs. + GauExpendituresManager.gauExpenditureSelector = (GauExpenditureSelector) gauExpenditureSelector.createStub( + GauExpenditureSelector.class + ); + + // Run the test. + final GauExpendituresManager.DisbursementWrapper actual = GauExpendituresManager.getDisbursement( + disbursementId + ); + + Test.stopTest(); + + System.assertEquals( + expected, + actual, + 'GauExpendituresManager.getDisbursement should return null if gauExpenditureSelector.getDisbursementsById returns no records.' + ); + + getDisbursementsById.assertCalledOnceWith(new List{ disbursementId }); + } + + @IsTest + private static void getDisbursement_QueryFindsRecords() { + // Set arguments, mock return values, and expected values. + final String disbursementId = 'disbursementId'; + final List disbursements = new List{ + new outfunds__Disbursement__c( + Id = UnitTest.mockId(Schema.SObjectType.outfunds__Disbursement__c) + ) + }; + final GauExpendituresManager.DisbursementWrapper expected = new GauExpendituresManager.DisbursementWrapper( + disbursements[0] + ); + + // Set mocks + final UnitTest.Mock gauExpenditureSelector = new UnitTest.Mock(); + + final UnitTest.Mock getDisbursementsById = gauExpenditureSelector.getMethod( + 'getDisbursementsById' + ); + getDisbursementsById.returnValue = disbursements; + + Test.startTest(); + + // Set stubs. + GauExpendituresManager.gauExpenditureSelector = (GauExpenditureSelector) gauExpenditureSelector.createStub( + GauExpenditureSelector.class + ); + + // Run the test. + final GauExpendituresManager.DisbursementWrapper actual = GauExpendituresManager.getDisbursement( + disbursementId + ); + + Test.stopTest(); + + GauExpendituresManager_TEST.assertEquals(expected, actual); + + getDisbursementsById.assertCalledOnceWith(new List{ disbursementId }); + } + + @IsTest + private static void getDisbursement_QueryThrowsException() { + // Set arguments, mock return values, and expected values. + final String disbursementId = 'disbursementId'; + final List disbursements = new List{ + new outfunds__Disbursement__c( + Id = UnitTest.mockId(Schema.SObjectType.outfunds__Disbursement__c) + ) + }; + + // Set mocks + final UnitTest.Mock gauExpenditureSelector = new UnitTest.Mock(); + + final UnitTest.Mock getDisbursementsById = gauExpenditureSelector.getMethod( + 'getDisbursementsById' + ); + getDisbursementsById.returnValue = new UnitTest.TestException( + 'This is only a test' + ); + + Test.startTest(); + + // Set stubs. + GauExpendituresManager.gauExpenditureSelector = (GauExpenditureSelector) gauExpenditureSelector.createStub( + GauExpenditureSelector.class + ); + + // Run the test. + System.AuraHandledException actualException; + + try { + GauExpendituresManager.getDisbursement(disbursementId); + } catch (System.AuraHandledException e) { + actualException = e; + } + + Test.stopTest(); + + System.assertNotEquals( + null, + actualException, + 'getDisbursementsById should have thrown an Exception so getDisbursement should have re-thrown the Exception as a System.AuraHandledException.' + ); + System.assertEquals( + 'This is only a test', + actualException.getMessage(), + 'getDisbursementsById should have thrown an Exception so getDisbursement should have re-thrown the Exception as a System.AuraHandledException with the same message.' + ); + + getDisbursementsById.assertCalledOnceWith(new List{ disbursementId }); + } + + @IsTest + private static void upsertGauExpenditures_DeletesUpdatesAndInserts() { + // Set arguments, mock return values, and expected values. + final Id disbursementId = UnitTest.mockId( + Schema.SObjectType.outfunds__Disbursement__c + ); + + final List expectedExpendituresToInsert = new List{ + new GAU_Expenditure__c( + Id = (Id) null, + Disbursement__c = disbursementId, + General_Accounting_Unit__c = UnitTest.mockId( + Schema.SObjectType.npsp__General_Accounting_Unit__c + ), + Amount__c = 0 + ), + new GAU_Expenditure__c( + Id = (Id) null, + Disbursement__c = disbursementId, + General_Accounting_Unit__c = UnitTest.mockId( + Schema.SObjectType.npsp__General_Accounting_Unit__c + ), + Amount__c = 2 + ) + }; + + final List expectedExpendituresToUpdate = new List{ + new GAU_Expenditure__c( + Id = UnitTest.mockId(Schema.SObjectType.GAU_Expenditure__c), + Disbursement__c = disbursementId, + General_Accounting_Unit__c = UnitTest.mockId( + Schema.SObjectType.npsp__General_Accounting_Unit__c + ), + Amount__c = 1 + ) + }; + + List expenditureWrappers = new List(); + for ( + GAU_Expenditure__c expenditure : new List{ + expectedExpendituresToInsert[0], + expectedExpendituresToUpdate[0], + expectedExpendituresToInsert[1] + } + ) { + final GauExpendituresManager.GauExpenditureWrapper wrapper = new GauExpendituresManager.GauExpenditureWrapper(); + wrapper.recordId = expenditure.Id; + wrapper.gauId = expenditure.General_Accounting_Unit__c; + wrapper.amount = expenditure.Amount__c; + + expenditureWrappers.add(wrapper); + } + + final String expendituresString = JSON.serialize(expenditureWrappers); + + // Set mocks. + final UnitTest.Mock gauExpenditureSelector = new UnitTest.Mock(); + + final UnitTest.Mock getExpendituresToDelete = gauExpenditureSelector.getMethod( + 'getExpendituresToDelete' + ); + getExpendituresToDelete.returnValue = new List{ + new GAU_Expenditure__c( + Id = UnitTest.mockId(Schema.SObjectType.GAU_Expenditure__c) + ) + }; + + final UnitTest.Mock databaseService = new UnitTest.Mock(); + + final UnitTest.Mock deleteRecords = databaseService.getMethod('deleteRecords'); + + final UnitTest.Mock insertRecordsEnforceFls = databaseService.getMethod( + 'insertRecordsEnforceFls' + ); + + final UnitTest.Mock updateRecordsEnforceFls = databaseService.getMethod( + 'updateRecordsEnforceFls' + ); + + final UnitTest.Mock setSavepoint = databaseService.getMethod('setSavepoint'); + setSavepoint.returnValue = new DatabaseService.Savepoint(); + + final UnitTest.Mock rollbackMethod = databaseService.getMethod('rollback'); + + Test.startTest(); + + // Set stubs. + GauExpendituresManager.gauExpenditureSelector = (GauExpenditureSelector) gauExpenditureSelector.createStub( + GauExpenditureSelector.class + ); + + GauExpendituresManager.databaseService = (DatabaseService) databaseService.createStub( + DatabaseService.class + ); + + // Run the test. + System.AuraHandledException actualException; + + GauExpendituresManager.upsertGauExpenditures(expendituresString, disbursementId); + + Test.stopTest(); + + setSavepoint.assertCalledOnce(); + + getExpendituresToDelete.assertCalledOnceWith( + new List{ + disbursementId, + new Set{ expectedExpendituresToUpdate[0].Id } + } + ); + + deleteRecords.assertCalledOnceWith( + new List{ getExpendituresToDelete.returnValue } + ); + + insertRecordsEnforceFls.assertCalledOnceWith( + new List{ expectedExpendituresToInsert } + ); + + updateRecordsEnforceFls.assertCalledOnceWith( + new List{ expectedExpendituresToUpdate } + ); + + rollbackMethod.assertNotCalled(); + } + + @IsTest + private static void upsertGauExpenditures_ExceptionThrown() { + // Set arguments, mock return values, and expected values. + final Id disbursementId = UnitTest.mockId( + Schema.SObjectType.outfunds__Disbursement__c + ); + + final List expectedExpendituresToInsert = new List{ + new GAU_Expenditure__c( + Id = (Id) null, + Disbursement__c = disbursementId, + General_Accounting_Unit__c = UnitTest.mockId( + Schema.SObjectType.npsp__General_Accounting_Unit__c + ), + Amount__c = 0 + ), + new GAU_Expenditure__c( + Id = (Id) null, + Disbursement__c = disbursementId, + General_Accounting_Unit__c = UnitTest.mockId( + Schema.SObjectType.npsp__General_Accounting_Unit__c + ), + Amount__c = 2 + ) + }; + + final List expectedExpendituresToUpdate = new List{ + new GAU_Expenditure__c( + Id = UnitTest.mockId(Schema.SObjectType.GAU_Expenditure__c), + Disbursement__c = disbursementId, + General_Accounting_Unit__c = UnitTest.mockId( + Schema.SObjectType.npsp__General_Accounting_Unit__c + ), + Amount__c = 1 + ) + }; + + List expenditureWrappers = new List(); + for ( + GAU_Expenditure__c expenditure : new List{ + expectedExpendituresToInsert[0], + expectedExpendituresToUpdate[0], + expectedExpendituresToInsert[1] + } + ) { + final GauExpendituresManager.GauExpenditureWrapper wrapper = new GauExpendituresManager.GauExpenditureWrapper(); + wrapper.recordId = expenditure.Id; + wrapper.gauId = expenditure.General_Accounting_Unit__c; + wrapper.amount = expenditure.Amount__c; + + expenditureWrappers.add(wrapper); + } + + final String expendituresString = JSON.serialize(expenditureWrappers); + + // Set mocks. + final UnitTest.Mock gauExpenditureSelector = new UnitTest.Mock(); + + final UnitTest.Mock getExpendituresToDelete = gauExpenditureSelector.getMethod( + 'getExpendituresToDelete' + ); + getExpendituresToDelete.returnValue = new UnitTest.TestException( + 'Query Exception' + ); + + final UnitTest.Mock databaseService = new UnitTest.Mock(); + + final UnitTest.Mock deleteRecords = databaseService.getMethod('deleteRecords'); + + final UnitTest.Mock insertRecordsEnforceFls = databaseService.getMethod( + 'insertRecordsEnforceFls' + ); + + final UnitTest.Mock updateRecordsEnforceFls = databaseService.getMethod( + 'updateRecordsEnforceFls' + ); + + final UnitTest.Mock setSavepoint = databaseService.getMethod('setSavepoint'); + setSavepoint.returnValue = new DatabaseService.Savepoint(); + + final UnitTest.Mock rollbackMethod = databaseService.getMethod('rollback'); + + Test.startTest(); + + // Set stubs. + GauExpendituresManager.gauExpenditureSelector = (GauExpenditureSelector) gauExpenditureSelector.createStub( + GauExpenditureSelector.class + ); + + GauExpendituresManager.databaseService = (DatabaseService) databaseService.createStub( + DatabaseService.class + ); + + // Run the test. + System.AuraHandledException actualException; + + try { + GauExpendituresManager.upsertGauExpenditures( + expendituresString, + disbursementId + ); + } catch (System.AuraHandledException e) { + actualException = e; + } + + Test.stopTest(); + + System.assertNotEquals( + null, + actualException, + 'getExpendituresToDelete should have thrown an Exception so upsertGauExpenditures should have re-thrown a System.AuraHandledException.' + ); + System.assertEquals( + 'Query Exception', + actualException.getMessage(), + 'getExpendituresToDelete should have thrown an Exception so upsertGauExpenditures should have re-thrown a System.AuraHandledException with the same message.' + ); + + setSavepoint.assertCalledOnce(); + + getExpendituresToDelete.assertCalledOnceWith( + new List{ + disbursementId, + new Set{ expectedExpendituresToUpdate[0].Id } + } + ); + + deleteRecords.assertNotCalled(); + + insertRecordsEnforceFls.assertNotCalled(); + + updateRecordsEnforceFls.assertNotCalled(); + + rollbackMethod.assertCalledOnceWith(new List{ setSavepoint.returnValue }); + } + + @IsTest + private static void searchActiveGeneralAccountingUnitsLikeName_QueryReturnsNoRecords() { + // Set arguments, mock return values, and expected values. + final String searchTerm = 'searchTerm'; + + final List expected = new List(); + + // Set mocks. + final UnitTest.Mock gauExpenditureSelector = new UnitTest.Mock(); + + final UnitTest.Mock getActiveGeneralAccountUnitsLikeName = gauExpenditureSelector.getMethod( + 'getActiveGeneralAccountUnitsLikeName' + ); + getActiveGeneralAccountUnitsLikeName.returnValue = new List(); + + Test.startTest(); + + // Set stubs. + GauExpendituresManager.gauExpenditureSelector = (GauExpenditureSelector) gauExpenditureSelector.createStub( + GauExpenditureSelector.class + ); + + // Run the test. + final List actual = GauExpendituresManager.searchActiveGeneralAccountingUnitsLikeName( + searchTerm + ); + + Test.stopTest(); + + GauExpendituresManager_TEST.assertEquals(expected, actual); + + getActiveGeneralAccountUnitsLikeName.assertCalledOnceWith( + new List{ searchTerm } + ); + } + + @IsTest + private static void searchActiveGeneralAccountingUnitsLikeName_QueryReturnsRecords() { + // Set arguments, mock return values, and expected values. + final String searchTerm = 'searchTerm'; + + final List expected = new List(); + for (Integer i = 0; i < 5; i++) { + GauExpendituresManager.LookupSearchResult result = new GauExpendituresManager.LookupSearchResult(); + result.id = UnitTest.mockId( + Schema.SObjectType.npsp__General_Accounting_Unit__c + ); + result.sObjectType = Schema.SObjectType.npsp__General_Accounting_Unit__c.getName(); + result.icon = 'custom:custom87'; + result.title = 'Name ' + i; + result.subtitle = 'Description ' + i; + + expected.add(result); + } + + final List generalAccountingUnits = new List(); + for (GauExpendituresManager.LookupSearchResult result : expected) { + generalAccountingUnits.add( + new npsp__General_Accounting_Unit__c( + Id = result.Id, + Name = result.title, + npsp__Description__c = result.subtitle + ) + ); + } + + // Set mocks. + final UnitTest.Mock gauExpenditureSelector = new UnitTest.Mock(); + + final UnitTest.Mock getActiveGeneralAccountUnitsLikeName = gauExpenditureSelector.getMethod( + 'getActiveGeneralAccountUnitsLikeName' + ); + getActiveGeneralAccountUnitsLikeName.returnValue = generalAccountingUnits; + + Test.startTest(); + + // Set stubs. + GauExpendituresManager.gauExpenditureSelector = (GauExpenditureSelector) gauExpenditureSelector.createStub( + GauExpenditureSelector.class + ); + + // Run the test. + final List actual = GauExpendituresManager.searchActiveGeneralAccountingUnitsLikeName( + searchTerm + ); + + Test.stopTest(); + + GauExpendituresManager_TEST.assertEquals(expected, actual); + + getActiveGeneralAccountUnitsLikeName.assertCalledOnceWith( + new List{ searchTerm } + ); + } + + @IsTest + private static void searchActiveGeneralAccountingUnitsLikeName_ThrowsException() { + // Set arguments, mock return values, and expected values. + final String searchTerm = 'searchTerm'; + + // Set mocks. + final UnitTest.Mock gauExpenditureSelector = new UnitTest.Mock(); + + final UnitTest.Mock getActiveGeneralAccountUnitsLikeName = gauExpenditureSelector.getMethod( + 'getActiveGeneralAccountUnitsLikeName' + ); + getActiveGeneralAccountUnitsLikeName.returnValue = new UnitTest.TestException( + 'Query Exception' + ); + + Test.startTest(); + + // Set stubs. + GauExpendituresManager.gauExpenditureSelector = (GauExpenditureSelector) gauExpenditureSelector.createStub( + GauExpenditureSelector.class + ); + + // Run the test. + System.AuraHandledException actualException; + + try { + final List actual = GauExpendituresManager.searchActiveGeneralAccountingUnitsLikeName( + searchTerm + ); + } catch (System.AuraHandledException e) { + actualException = e; + } + + Test.stopTest(); + + System.assertNotEquals( + null, + actualException, + 'getActiveGeneralAccountUnitsLikeName should have thrown an Exception so searchActiveGeneralAccountingUnitsLikeName should have re-thrown a System.AuraHandledException.' + ); + System.assertEquals( + 'Query Exception', + actualException.getMessage(), + 'getActiveGeneralAccountUnitsLikeName should have thrown an Exception so searchActiveGeneralAccountingUnitsLikeName should have re-thrown a System.AuraHandledException with the same message.' + ); + + getActiveGeneralAccountUnitsLikeName.assertCalledOnceWith( + new List{ searchTerm } + ); + } } From a8cd17f833ff6f3542cdbe4b23c173a46d2a8ecc Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 11 Feb 2021 13:25:53 -0500 Subject: [PATCH 16/24] Support 2GP orgs - Added `orgs/2gp.json`. - `cumulusci.yml` changes: - Added `2gp` org. - Added `2gp_org` flow. - Added customizations of `config_qa` and `config_managed` flows to mimic `config_dev`. --- cumulusci.yml | 21 +++++++++++++++++++++ orgs/2gp.json | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 orgs/2gp.json diff --git a/cumulusci.yml b/cumulusci.yml index a4b3108..42c33ca 100644 --- a/cumulusci.yml +++ b/cumulusci.yml @@ -19,6 +19,8 @@ orgs: config_file: orgs/prerelease.json beta_prerelease: config_file: orgs/beta_prerelease.json + 2gp: + config_file: orgs/2gp.json tasks: robot: @@ -51,3 +53,22 @@ flows: steps: 3: task: load_storytelling_data + + config_qa: + steps: + 3: + task: load_storytelling_data + + config_managed: + steps: + 3: + task: load_storytelling_data + + 2gp_org: + steps: + 1: + flow: install_2gp_commit + 2: + flow: config_managed + 3: + task: snapshot_changes diff --git a/orgs/2gp.json b/orgs/2gp.json new file mode 100644 index 0000000..f58711c --- /dev/null +++ b/orgs/2gp.json @@ -0,0 +1,39 @@ +{ + "orgName": "Outbound Funds (npsp) - 2gp Org", + "edition": "Developer", + "settings": { + "enhancedNotesSettings": { + "enableEnhancedNotes": true + }, + "accountSettings": { + "enableRelateContactToMultipleAccounts": true + }, + "lightningExperienceSettings": { + "enableS1DesktopEnabled": true + }, + "mobileSettings": { + "enableS1EncryptedStoragePref2": false + }, + "userManagementSettings": { + "enableEnhancedPermsetMgmt": true, + "enableEnhancedProfileMgmt": true, + "enableNewProfileUI": true, + "enableScrambleUserData": true + }, + "chatterSettings": { + "enableChatter": true + }, + "languageSettings": { + "enableTranslationWorkbench": true + }, + "securitySettings": { + "enableAdminLoginAsAnyUser": true, + "sessionSettings": { + "forceRelogin": false + } + }, + "sharingSettings": { + "enableSecureGuestAccess": true + } + } +} From c8fb2231a99935364f320cb3478c569061d088a0 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 11 Feb 2021 13:39:15 -0500 Subject: [PATCH 17/24] Temporarily building orgs on prerelease pods - `feature` for MetaCI tests. - `2gp` and `dev` for local builds. --- orgs/2gp.json | 1 + orgs/dev.json | 1 + orgs/feature.json | 1 + 3 files changed, 3 insertions(+) diff --git a/orgs/2gp.json b/orgs/2gp.json index f58711c..7c58e0f 100644 --- a/orgs/2gp.json +++ b/orgs/2gp.json @@ -1,6 +1,7 @@ { "orgName": "Outbound Funds (npsp) - 2gp Org", "edition": "Developer", + "instance": "cs46", "settings": { "enhancedNotesSettings": { "enableEnhancedNotes": true diff --git a/orgs/dev.json b/orgs/dev.json index 0fc172b..2e560e9 100644 --- a/orgs/dev.json +++ b/orgs/dev.json @@ -1,6 +1,7 @@ { "orgName": "Outbound Funds (npsp) - Dev Org", "edition": "Developer", + "instance": "cs46", "settings": { "enhancedNotesSettings": { "enableEnhancedNotes": true diff --git a/orgs/feature.json b/orgs/feature.json index 77c065b..afa83b8 100644 --- a/orgs/feature.json +++ b/orgs/feature.json @@ -1,6 +1,7 @@ { "orgName": "Outbound Funds (npsp) - Feature Test Org", "edition": "Developer", + "instance": "cs46", "settings": { "chatterSettings": { "enableChatter": true From 58c2dc9aa49d39d2bed7fa1a2ebc5e484ad1e031 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 11 Feb 2021 13:53:56 -0500 Subject: [PATCH 18/24] dev org doesn't use prerelease orgs --- orgs/dev.json | 1 - 1 file changed, 1 deletion(-) diff --git a/orgs/dev.json b/orgs/dev.json index 2e560e9..0fc172b 100644 --- a/orgs/dev.json +++ b/orgs/dev.json @@ -1,7 +1,6 @@ { "orgName": "Outbound Funds (npsp) - Dev Org", "edition": "Developer", - "instance": "cs46", "settings": { "enhancedNotesSettings": { "enableEnhancedNotes": true From 4e2b77900e3bbdf4fc2b2a4fc30edbbe16c49f99 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 11 Feb 2021 15:14:22 -0500 Subject: [PATCH 19/24] Fix attempt for namespaced JSON deserial. error --- .../tests/GauExpendituresManager_TEST.cls | 82 ++++++++++--------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls index c308695..e776325 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls +++ b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls @@ -296,49 +296,52 @@ public with sharing class GauExpendituresManager_TEST { final String status = 'Scheduled'; // prettier-ignore - final GAU_Expenditure__c gauExpenditure1 = (GAU_Expenditure__c) JSON.deserialize( - String.join( - new List { - '{', + final String gauExpenditure1Json = String.join( + new List { + '{', + '"attributes": {', + '"type": "' + gauExpenditureObjectName + '",', + '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + gauExpenditureId1 + '"', + '},', + '"' + disbursementFieldName + '": "a0s9A000000cuCSQAY",', + '"Id": "' + gauExpenditureId1 + '",', + '"' + gauFieldName + '": "' + gauId1 + '",', + '"'+ amountFieldName + '": 1234.5,', + '"' + gauRelationshipName + '": {', '"attributes": {', - '"type": "' + gauExpenditureObjectName + '",', - '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + gauExpenditureId1 + '"', + '"type": "npsp__General_Accounting_Unit__c",', + '"url": "/services/data/v51.0/sobjects/npsp__General_Accounting_Unit__c/' + gauId1 + '"', '},', - '"' + disbursementFieldName+ '": "a0s9A000000cuCSQAY",', - '"Id": "' + gauExpenditureId1 + '",', - '"' + gauFieldName + '": "' + gauId1 + '",', - '"'+ amountFieldName + '": 1234.5,', - '"' + gauRelationshipName + '": {', - '"attributes": {', - '"type": "npsp__General_Accounting_Unit__c",', - '"url": "/services/data/v51.0/sobjects/npsp__General_Accounting_Unit__c/' + gauId1 + '"', - '},', - '"Id": "' + gauId1 + '",', - '"Name": "GAU 1",', - '"npsp__Active__c": true', - '}', - '}' - }, - '' - ), + '"Id": "' + gauId1 + '",', + '"Name": "GAU 1",', + '"npsp__Active__c": true', + '}', + '}' + }, + '' + ); + final GAU_Expenditure__c gauExpenditure1 = (GAU_Expenditure__c) JSON.deserialize( + gauExpenditure1Json, GAU_Expenditure__c.class ); + // prettier-ignore + final String gauExpenditure2Json = String.join( + new List { + '{', + '"attributes": {', + '"type": "' + gauExpenditureObjectName + '",', + '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + gauExpenditureId2 + '"', + '},', + '"' + disbursementFieldName+ '": "a0s9A000000cuCSQAY",', + '"Id": "' + gauExpenditureId2 + '",', + '"'+ amountFieldName + '": 5432.1', + '}' + }, + '' + ); final GAU_Expenditure__c gauExpenditure2 = (GAU_Expenditure__c) JSON.deserialize( - String.join( - new List { - '{', - '"attributes": {', - '"type": "' + gauExpenditureObjectName + '",', - '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + gauExpenditureId2 + '"', - '},', - '"' + disbursementFieldName+ '": "a0s9A000000cuCSQAY",', - '"Id": "' + gauExpenditureId2 + '",', - '"'+ amountFieldName + '": 5432.1', - '}' - }, - '' - ), + gauExpenditure2Json, GAU_Expenditure__c.class ); final List expenditures = new List{ @@ -367,7 +370,10 @@ public with sharing class GauExpendituresManager_TEST { '"GAU_Expendatures__r": {', '"totalSize": ' + expenditures.size() + ',', '"done": true,', - '"records": ' + JSON.serialize(expenditures), + '"records": [', + gauExpenditure1Json + ',', + gauExpenditure2Json, + ']', '}', '}' }, From f7b728763846f0b8b6ea3125dc37ed9959d2fc67 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 11 Feb 2021 15:50:14 -0500 Subject: [PATCH 20/24] Properly namespaces JSON for test data - Was not dynamically getting `outfunds__Disbursement__c` Child Relationship Name for `GAU_Expenditure__c.Disbursement__c` lookup. --- .../tests/GauExpendituresManager_TEST.cls | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls index e776325..99e430e 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls +++ b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls @@ -290,6 +290,27 @@ public with sharing class GauExpendituresManager_TEST { Schema.SObjectType.GAU_Expenditure__c ); + Schema.ChildRelationship gauExpenditureChildRelationship; + for ( + Schema.ChildRelationship childRelationship : Schema.SObjectType.outfunds__Disbursement__c.getChildRelationships() + ) { + if ( + Schema.GAU_Expenditure__c.SObjectType == + childRelationship.getChildSObject() && + Schema.GAU_Expenditure__c.SObjectType.fields.Disbursement__c == + childRelationship.getField() + ) { + gauExpenditureChildRelationship = childRelationship; + break; + } + } + System.assertNotEquals( + null, + gauExpenditureChildRelationship, + 'No outfunds__Disbursement__c Child Relationship to GAU_Expenditure__c.Disbursement__c found.' + ); + final String gauExpenditureRelationshipName = gauExpenditureChildRelationship.getRelationshipName(); + final Id recordId = UnitTest.mockId(Schema.SObjectType.outfunds__Disbursement__c); final String name = 'D-00004'; final Decimal amount = 314159.27; @@ -367,7 +388,7 @@ public with sharing class GauExpendituresManager_TEST { '"Name": "' + name + '",', '"outfunds__Amount__c": ' + amount.toPlainString() + ',', '"outfunds__Status__c": "' + status + '",', - '"GAU_Expendatures__r": {', + '"' + gauExpenditureRelationshipName + '": {', '"totalSize": ' + expenditures.size() + ',', '"done": true,', '"records": [', From fc2eb3f5c629f8c1ca3ddedc88d49fea89ebd538 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 11 Feb 2021 15:55:36 -0500 Subject: [PATCH 21/24] Upgrading all Apex to API Version 50.0 - `GauExpendituresManager` - `GauExpendituresManager_TEST` - `TestUser` - `UnitTest` --- .../manageExpenditures/GauExpendituresManager.cls-meta.xml | 2 +- .../tests/GauExpendituresManager_TEST.cls-meta.xml | 2 +- .../default/classes/testUtils/TestUser/TestUser.cls-meta.xml | 2 +- .../default/classes/testUtils/UnitTest/UnitTest.cls-meta.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls-meta.xml b/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls-meta.xml index 252fbfd..541584f 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls-meta.xml +++ b/force-app/main/default/classes/controllers/manageExpenditures/GauExpendituresManager.cls-meta.xml @@ -1,5 +1,5 @@ - 47.0 + 50.0 Active diff --git a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls-meta.xml b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls-meta.xml index 252fbfd..541584f 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls-meta.xml +++ b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls-meta.xml @@ -1,5 +1,5 @@ - 47.0 + 50.0 Active diff --git a/force-app/main/default/classes/testUtils/TestUser/TestUser.cls-meta.xml b/force-app/main/default/classes/testUtils/TestUser/TestUser.cls-meta.xml index 8e4d11f..541584f 100644 --- a/force-app/main/default/classes/testUtils/TestUser/TestUser.cls-meta.xml +++ b/force-app/main/default/classes/testUtils/TestUser/TestUser.cls-meta.xml @@ -1,5 +1,5 @@ - 49.0 + 50.0 Active diff --git a/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls-meta.xml b/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls-meta.xml index db9bf8c..541584f 100644 --- a/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls-meta.xml +++ b/force-app/main/default/classes/testUtils/UnitTest/UnitTest.cls-meta.xml @@ -1,5 +1,5 @@ - 48.0 + 50.0 Active From ca50482ffd23b31410652ce87183c1d69aaf248d Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 11 Feb 2021 16:03:20 -0500 Subject: [PATCH 22/24] Removed hard-coded IDs in test data --- .../manageExpenditures/tests/GauExpendituresManager_TEST.cls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls index 99e430e..b98bea7 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls +++ b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls @@ -324,7 +324,7 @@ public with sharing class GauExpendituresManager_TEST { '"type": "' + gauExpenditureObjectName + '",', '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + gauExpenditureId1 + '"', '},', - '"' + disbursementFieldName + '": "a0s9A000000cuCSQAY",', + '"' + disbursementFieldName + '": "' + recordId + '",', '"Id": "' + gauExpenditureId1 + '",', '"' + gauFieldName + '": "' + gauId1 + '",', '"'+ amountFieldName + '": 1234.5,', @@ -354,7 +354,7 @@ public with sharing class GauExpendituresManager_TEST { '"type": "' + gauExpenditureObjectName + '",', '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + gauExpenditureId2 + '"', '},', - '"' + disbursementFieldName+ '": "a0s9A000000cuCSQAY",', + '"' + disbursementFieldName+ '": "' + recordId + '",', '"Id": "' + gauExpenditureId2 + '",', '"'+ amountFieldName + '": 5432.1', '}' From 940f2a952e1f9c8634f410b7a0d0fbd600364281 Mon Sep 17 00:00:00 2001 From: Scott Pelak Date: Thu, 11 Feb 2021 16:23:20 -0500 Subject: [PATCH 23/24] Removed creating SObjects from JSON where possible --- .../tests/GauExpendituresManager_TEST.cls | 143 ++++++------------ 1 file changed, 46 insertions(+), 97 deletions(-) diff --git a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls index b98bea7..8234aa0 100755 --- a/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls +++ b/force-app/main/default/classes/controllers/manageExpenditures/tests/GauExpendituresManager_TEST.cls @@ -169,40 +169,32 @@ public with sharing class GauExpendituresManager_TEST { final String gauFieldName = Schema.SObjectType.GAU_Expenditure__c.fields.General_Accounting_Unit__c.getName(); final String gauRelationshipName = gauFieldName.removeEnd('__c') + '__r'; - final Id recordId = 'a0x9A000000ASvfQAG'; - final Id gauId = 'a0d9A0000003r1sQAA'; + final Id recordId = UnitTest.mockId(Schema.SObjectType.GAU_Expenditure__c); + final Id gauId = UnitTest.mockId( + Schema.SObjectType.npsp__General_Accounting_Unit__c + ); final String gauName = 'Area of most need'; final Decimal amount = 1250.0; final Boolean gauIsActive = true; - // prettier-ignore - final GAU_Expenditure__c gauExpenditure = (GAU_Expenditure__c) JSON.deserialize( - String.join( - new List { - '{', - '"attributes": {', - '"type": "' + gauExpenditureObjectName + '",', - '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + recordId + '"', - '},', - '"' + disbursementFieldName+ '": "a0s9A000000cuCSQAY",', - '"Id": "' + recordId + '",', - '"' + gauFieldName + '": "' + gauId + '",', - '"'+ amountFieldName + '": ' + amount.toPlainString() + ',', - '"' + gauRelationshipName + '": {', - '"attributes": {', - '"type": "npsp__General_Accounting_Unit__c",', - '"url": "/services/data/v51.0/sobjects/npsp__General_Accounting_Unit__c/' + gauId + '"', - '},', - '"Id": "' + gauId + '",', - '"Name": "' + gauName + '",', - '"npsp__Active__c": ' + gauIsActive + '', - '}', - '}' - }, - '' - ), - GAU_Expenditure__c.class + final Id disbursementId = UnitTest.mockId( + Schema.SObjectType.outfunds__Disbursement__c ); + + final npsp__General_Accounting_Unit__c gau = new npsp__General_Accounting_Unit__c( + Id = gauId, + Name = gauName, + npsp__Active__c = gauIsActive + ); + + final GAU_Expenditure__c gauExpenditure = new GAU_Expenditure__c( + Id = recordId, + Disbursement__c = disbursementId, + Amount__c = amount, + General_Accounting_Unit__c = gauId, + General_Accounting_Unit__r = gau + ); + final Integer rowId = Crypto.getRandomInteger(); Test.startTest(); @@ -228,29 +220,20 @@ public with sharing class GauExpendituresManager_TEST { final String disbursementFieldName = Schema.SObjectType.GAU_Expenditure__c.fields.Disbursement__c.getName(); final String amountFieldName = Schema.SObjectType.GAU_Expenditure__c.fields.Amount__c.getName(); - final Id recordId = 'a0x9A000000ASvfQAG'; + final Id recordId = UnitTest.mockId(Schema.SObjectType.GAU_Expenditure__c); final Id gauId = null; final String gauName = null; final Decimal amount = 1250.0; final Boolean gauIsActive = false; - // prettier-ignore - final GAU_Expenditure__c gauExpenditure = (GAU_Expenditure__c) JSON.deserialize( - String.join( - new List { - '{', - '"attributes": {', - '"type": "' + gauExpenditureObjectName + '",', - '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + recordId + '"', - '},', - '"' + disbursementFieldName + '": "a0s9A000000cuCSQAY",', - '"Id": "' + recordId + '",', - '"' + amountFieldName + '": ' + amount.toPlainString() + '', - '}' - }, - '' - ), - GAU_Expenditure__c.class + final Id disbursementId = UnitTest.mockId( + Schema.SObjectType.outfunds__Disbursement__c + ); + + final GAU_Expenditure__c gauExpenditure = new GAU_Expenditure__c( + Id = recordId, + Disbursement__c = disbursementId, + Amount__c = amount ); final Integer rowId = Crypto.getRandomInteger(); @@ -316,55 +299,24 @@ public with sharing class GauExpendituresManager_TEST { final Decimal amount = 314159.27; final String status = 'Scheduled'; - // prettier-ignore - final String gauExpenditure1Json = String.join( - new List { - '{', - '"attributes": {', - '"type": "' + gauExpenditureObjectName + '",', - '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + gauExpenditureId1 + '"', - '},', - '"' + disbursementFieldName + '": "' + recordId + '",', - '"Id": "' + gauExpenditureId1 + '",', - '"' + gauFieldName + '": "' + gauId1 + '",', - '"'+ amountFieldName + '": 1234.5,', - '"' + gauRelationshipName + '": {', - '"attributes": {', - '"type": "npsp__General_Accounting_Unit__c",', - '"url": "/services/data/v51.0/sobjects/npsp__General_Accounting_Unit__c/' + gauId1 + '"', - '},', - '"Id": "' + gauId1 + '",', - '"Name": "GAU 1",', - '"npsp__Active__c": true', - '}', - '}' - }, - '' - ); - final GAU_Expenditure__c gauExpenditure1 = (GAU_Expenditure__c) JSON.deserialize( - gauExpenditure1Json, - GAU_Expenditure__c.class + final GAU_Expenditure__c gauExpenditure1 = new GAU_Expenditure__c( + Id = gauExpenditureId1, + Disbursement__c = recordId, + Amount__c = 1234.5, + General_Accounting_Unit__c = gauId1, + General_Accounting_Unit__r = new npsp__General_Accounting_Unit__c( + Id = gauId1, + Name = 'GAU 1', + npsp__Active__c = true + ) ); - // prettier-ignore - final String gauExpenditure2Json = String.join( - new List { - '{', - '"attributes": {', - '"type": "' + gauExpenditureObjectName + '",', - '"url": "/services/data/v51.0/sobjects/' + gauExpenditureObjectName + '/' + gauExpenditureId2 + '"', - '},', - '"' + disbursementFieldName+ '": "' + recordId + '",', - '"Id": "' + gauExpenditureId2 + '",', - '"'+ amountFieldName + '": 5432.1', - '}' - }, - '' - ); - final GAU_Expenditure__c gauExpenditure2 = (GAU_Expenditure__c) JSON.deserialize( - gauExpenditure2Json, - GAU_Expenditure__c.class + final GAU_Expenditure__c gauExpenditure2 = new GAU_Expenditure__c( + Id = gauExpenditureId2, + Disbursement__c = recordId, + Amount__c = 5432.1 ); + final List expenditures = new List{ gauExpenditure1, gauExpenditure2 @@ -391,10 +343,7 @@ public with sharing class GauExpendituresManager_TEST { '"' + gauExpenditureRelationshipName + '": {', '"totalSize": ' + expenditures.size() + ',', '"done": true,', - '"records": [', - gauExpenditure1Json + ',', - gauExpenditure2Json, - ']', + '"records": ' + JSON.serialize(expenditures), '}', '}' }, From 235d982900f1f220fe38cd74efa240b1b57d944c Mon Sep 17 00:00:00 2001 From: David Reed Date: Thu, 11 Feb 2021 17:39:43 -0700 Subject: [PATCH 24/24] Update orgs/feature.json --- orgs/feature.json | 1 - 1 file changed, 1 deletion(-) diff --git a/orgs/feature.json b/orgs/feature.json index afa83b8..77c065b 100644 --- a/orgs/feature.json +++ b/orgs/feature.json @@ -1,7 +1,6 @@ { "orgName": "Outbound Funds (npsp) - Feature Test Org", "edition": "Developer", - "instance": "cs46", "settings": { "chatterSettings": { "enableChatter": true